Compare commits

..

1295 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
Vincent Koc
c053b90290 refactor: share shared auth rotation test helpers 2026-06-01 18:10:57 +02:00
Pavan Kumar Gondhi
fbdf593778 fix: bound remote media reference reads [AI] (#88974)
* fix: bound remote media reference reads

* fix: remove unreachable video timeout wiring

* test: cover remote video reference handoff
2026-06-01 21:35:40 +05:30
Vincent Koc
488b65ab87 refactor: share session reset test helpers 2026-06-01 18:00:39 +02:00
Peter Steinberger
6668eb8225 test(codex): drop unused live harness helper 2026-06-01 16:56:48 +01:00
Vincent Koc
72436217ff fix(e2e): isolate MCP channel client temp state 2026-06-01 17:51:04 +02:00
Peter Steinberger
460cf7ed75 test(codex): avoid sessions list wait in live harness start probe 2026-06-01 16:49:20 +01:00
Vincent Koc
461999c060 fix(dev): clean Telegram flow previews on failure 2026-06-01 17:37:15 +02:00
Vincent Koc
9cb347e4c3 fix(dev): close gateway smoke websocket on failures 2026-06-01 17:26:15 +02:00
Vincent Koc
1d7e5f48ed fix(dev): close stalled gateway websocket handshakes 2026-06-01 17:18:40 +02:00
Vincent Koc
1fd2259e28 refactor: share config patch test helpers 2026-06-01 17:15:48 +02:00
Peter Steinberger
3f54d150b3 test(openrouter): stabilize music timeout clamp assertion 2026-06-01 16:09:23 +01:00
Vincent Koc
a9866a405c test(agents): align provider auth alias fixtures 2026-06-01 17:08:31 +02:00
Vincent Koc
0b9187c780 test(gateway): fix node invoke capture race 2026-06-01 17:08:31 +02:00
Vincent Koc
b1ec23e05f fix(e2e): escalate stuck PTY children 2026-06-01 17:07:42 +02:00
Vincent Koc
050f0c0af6 refactor: share device pair authz test helpers 2026-06-01 16:58:10 +02:00
Vincent Koc
dfeb5b81ca fix(e2e): harden Parallels helper cleanup 2026-06-01 16:57:27 +02:00
Vincent Koc
d9f6e03e32 refactor: share silent reconnect test helpers 2026-06-01 16:54:10 +02:00
Peter Steinberger
fed7d1f385 test(release): stabilize beta validation regressions 2026-06-01 15:47:56 +01:00
Vincent Koc
0a9e594420 fix(scripts): clean Anthropic prompt probe temp state 2026-06-01 16:47:27 +02:00
Sally O'Malley
c1ce51546e fix(ui): clear chat composer after send (#89106) 2026-06-01 10:42:35 -04:00
Vincent Koc
1b928592ef refactor: share startup recovery test helpers 2026-06-01 16:37:09 +02:00
Vincent Koc
12087ac9d4 test(e2e): exercise Parallels smoke cleanup path 2026-06-01 16:33:11 +02:00
Peter Steinberger
00caead80a test: close oxlint signal readiness race 2026-06-01 10:26:08 -04:00
Peter Steinberger
4b54a423f0 test: harden changed-gate assertions 2026-06-01 10:26:08 -04:00
Peter Steinberger
bdd6cf3d5e test: stabilize order-sensitive assertions 2026-06-01 10:26:08 -04:00
Peter Steinberger
cb7a4239ef fix: stabilize full-suite regressions 2026-06-01 10:26:08 -04:00
Peter Steinberger
b226a752a1 test: stabilize slow shard regressions 2026-06-01 10:26:08 -04:00
Vincent Koc
110f7d55e3 fix(scripts): clean Z.AI fallback repro temp state 2026-06-01 16:25:05 +02:00
Vincent Koc
645c7dc40b refactor: share gateway misc test helpers 2026-06-01 16:18:22 +02:00
Vincent Koc
a4847297b8 fix(ci): clean check-changed pnpm shim temp dirs 2026-06-01 16:16:26 +02:00
Vincent Koc
4253517070 refactor: share node allowlist test helpers 2026-06-01 16:14:59 +02:00
Peter Steinberger
e8c126eaf2 fix(ci): use QA runtime build for release checks 2026-06-01 15:12:50 +01:00
Peter Steinberger
2075d19923 test(gateway): scope lazy server mock 2026-06-01 15:12:50 +01:00
Vincent Koc
9e58ef1c82 test(scripts): clean session log temp roots 2026-06-01 16:00:41 +02:00
Vincent Koc
eaeccf5fdf refactor: share node registry system run test helpers 2026-06-01 16:00:36 +02:00
Vincent Koc
2c0e835b48 test(codex): clean up fake timer spies 2026-06-01 14:57:47 +01:00
Vincent Koc
b942a958b3 test(qa): cover QA lab help runtime boundary 2026-06-01 15:54:16 +02:00
Vincent Koc
42bcf9cd0b fix(test): keep runtime tests raw-sync safe 2026-06-01 15:53:37 +02:00
Vincent Koc
a0fbb6cfe2 fix(test): keep app parity checks sparse safe 2026-06-01 15:53:37 +02:00
Vincent Koc
408fa6e951 fix(test): stabilize watch-node shutdown tests 2026-06-01 15:53:37 +02:00
Vincent Koc
671909d6d3 refactor: share server aux reload test helpers 2026-06-01 15:51:05 +02:00
Vincent Koc
409f78a1ea fix(e2e): clean OTEL collector startup failures 2026-06-01 15:46:02 +02:00
Vincent Koc
3e592a8bd7 refactor: share mcp http loopback test helpers 2026-06-01 15:39:28 +02:00
Vincent Koc
e895479a21 fix(ci): fail gateway watch spawn errors promptly 2026-06-01 15:38:16 +02:00
Peter Steinberger
930bc9691b fix(ci): page CI timing job reads 2026-06-01 14:33:39 +01:00
Vincent Koc
b9f181635f fix(ci): fail gateway CPU spawn errors 2026-06-01 15:27:13 +02:00
Vincent Koc
c2aaf8afec refactor: share sessions patch test helpers 2026-06-01 15:17:55 +02:00
Vincent Koc
cbc5f277bb refactor: share session reset hook test helpers 2026-06-01 15:11:10 +02:00
Vincent Koc
44b388f863 fix(e2e): keep kitchen-sink process snapshots wide 2026-06-01 15:09:33 +02:00
Vincent Koc
c0e49a2c52 fix(e2e): catch runtime package-manager descendants 2026-06-01 14:58:39 +02:00
Peter Steinberger
c1e132195d test(release): activate manifest channels in bundle smoke 2026-06-01 13:51:38 +01:00
Vincent Koc
5bd8dbd0b8 refactor: share system run approval test helpers 2026-06-01 14:44:46 +02:00
Vincent Koc
421ea1f458 fix(e2e): bound Parallels host VM commands 2026-06-01 14:41:46 +02:00
Vincent Koc
1f91e97353 refactor: share startup secrets test helpers 2026-06-01 14:31:58 +02:00
Vincent Koc
d4f6e0a1f2 fix(docs): clean link audit temp docs 2026-06-01 14:26:21 +02:00
Peter Steinberger
ec2455a842 test(memory): drive timeout tests with explicit fake clocks
(cherry picked from commit d75eea53c9)
2026-06-01 13:12:07 +01:00
Vincent Koc
1742f3f77c refactor: share mcp http test helpers 2026-06-01 14:10:41 +02:00
Vincent Koc
5117f457bb fix(ci): clean gateway watch temp home 2026-06-01 14:09:58 +02:00
Vincent Koc
8fe5e83462 refactor: share sessions list changed test helpers 2026-06-01 14:00:20 +02:00
Vincent Koc
27097bed65 fix(ci): bound deadcode knip scan 2026-06-01 13:57:16 +02:00
Vincent Koc
1849a86dd2 refactor: share session history revocation helpers 2026-06-01 13:47:39 +02:00
Vincent Koc
5280d1d95d fix(e2e): stream Parallels phase logs 2026-06-01 13:46:21 +02:00
Vincent Koc
bcdc93d651 refactor: share auth compat backend scope assertion 2026-06-01 13:31:03 +02:00
Vincent Koc
0751b6f2c9 fix(e2e): bound upgrade survivor config commands 2026-06-01 13:30:23 +02:00
Peter Steinberger
7d9fae5b3a fix(memory): keep embedding timeout watchdog active
(cherry picked from commit 591f310869)
2026-06-01 12:29:27 +01:00
Vincent Koc
a595aba60e refactor: share sessions send result assertions 2026-06-01 13:21:09 +02:00
Vincent Koc
75645aec08 fix(e2e): clean Telegram proof child processes 2026-06-01 13:20:03 +02:00
Vincent Koc
d10d71cdb6 fix(codex): stabilize app-server cleanup tests 2026-06-01 13:15:05 +02:00
Vincent Koc
c69a8d633d perf(control-ui): hydrate chat startup state
Add a combined chat.startup gateway method for Control UI startup hydration so first chat load can receive history and agents in one RPC, while falling back to chat.history for older/unadvertised gateways. Verified with focused UI/gateway tests, tsgo/oxlint/diff checks, clean autoreview, and Testbox changed gate tbx_01kt1dt6fqdtdbprsk48z8fn71.
2026-06-01 12:14:19 +01:00
Vincent Koc
d8ebbedf45 refactor: share plugin http auth request assertions 2026-06-01 13:10:09 +02:00
Peter Steinberger
9ed1766696 test(whatsapp): align direct last-route envelope
(cherry picked from commit 5d902b0f20)
2026-06-01 12:04:51 +01:00
Vincent Koc
bed0fb7bad refactor: share session resolve assertions 2026-06-01 13:00:51 +02:00
Vincent Koc
db6fc20559 fix(e2e): clean Windows background smoke timeouts 2026-06-01 12:55:15 +02:00
Vincent Koc
1364acbe4c refactor: share gateway http stage error assertions 2026-06-01 12:45:20 +02:00
Vincent Koc
d2988e0248 refactor: share preview resolve alias fixtures 2026-06-01 12:42:30 +02:00
Vincent Koc
8c8c8c8e32 perf(control-ui): prioritize first connect startup (#89030)
* perf(control-ui): prioritize first connect startup

* fix(control-ui): close connect timing gaps

* fix(control-ui): default embeds strict before bootstrap

* fix(control-ui): keep bootstrap identity deferred

* fix(control-ui): gate startup chat on bootstrap

* fix(control-ui): restore composer after hello

* fix(control-ui): restore drafts before hello
2026-06-01 11:41:22 +01:00
Vincent Koc
8bee3be90a fix(e2e): bound Parallels fresh lanes 2026-06-01 12:34:29 +02:00
Vincent Koc
87d890003d refactor: share shutdown drain session setup 2026-06-01 12:31:32 +02:00
Peter Steinberger
aed7de306e fix(qa-matrix): detect sqlite dedupe commits by payload
(cherry picked from commit 2fc497e67b)
2026-06-01 11:27:10 +01:00
Vincent Koc
859cb52b44 refactor: share unauthorized response assertions 2026-06-01 12:22:58 +02:00
Vincent Koc
4685a84e9b fix(e2e): bound bundled runtime gateway cleanup 2026-06-01 12:19:37 +02:00
Vincent Koc
f30235bed2 test: fix gateway test type fixtures 2026-06-01 12:13:36 +02:00
Vincent Koc
4f8f6c7693 refactor: share thinking e2e session setup 2026-06-01 12:13:36 +02:00
Peter Steinberger
055063f06b fix(qa-matrix): read sqlite inbound dedupe state 2026-06-01 11:07:53 +01:00
Vincent Koc
dac33c8ecb fix(e2e): cap pty transcript output 2026-06-01 11:49:58 +02:00
Vincent Koc
75ebf1c870 refactor: share device token authz test helpers 2026-06-01 11:49:06 +02:00
Vincent Koc
e4a32b9e8e lint(e2e): remove redundant channel fallback 2026-06-01 11:38:28 +02:00
Vincent Koc
22e3b2e94e fix(dev): wait for watch-node shutdown 2026-06-01 11:38:28 +02:00
Peter Steinberger
729420c34a test: split slow vitest shards 2026-06-01 05:34:59 -04:00
Peter Steinberger
0b5be66ef7 perf(gateway): trim startup plugin planning work 2026-06-01 10:33:28 +01:00
Peter Steinberger
8e28c773fe chore(release): prepare 2026.6.1 2026-06-01 10:30:15 +01:00
Vincent Koc
2dcb681f38 refactor: share session search test fixtures 2026-06-01 11:28:59 +02:00
Peter Steinberger
e733774e3c fix(test): repair telegram prerelease blockers 2026-06-01 10:26:12 +01:00
Mason Huang
004835f4c7 fix(plugins): block untrusted workspace setup-only channel loads (#86953)
Summary:
- This PR blocks disabled workspace-origin channel plugins from setup-only scoped imports, rejects their channel registrations at registry assembly, documents the trust rule, and adds regression coverage.
- PR surface: Source +46, Tests +610, Docs +13. Total +669 across 22 files.
- Reproducibility: yes. source inspection gives a high-confidence reproduction path: current main's setup-only ... ce channel plugin can be imported before this PR. I did not run the repro locally in this read-only review.

Automerge notes:
- PR branch already contained follow-up commit before automerge: test(plugins): cover workspace channel registry guard
- PR branch already contained follow-up commit before automerge: fix(plugins): isolate setup channel registration errors
- PR branch already contained follow-up commit before automerge: fix(channels): mark raw catalog listing internal
- PR branch already contained follow-up commit before automerge: test(channels): cover trusted catalog filtering
- PR branch already contained follow-up commit before automerge: test(channels): mock raw catalog helper
- PR branch already contained follow-up commit before automerge: docs(changelog): credit setup channel hardening

Validation:
- ClawSweeper review passed for head 11438bc1a0.
- Required merge gates passed before the squash merge.

Prepared head SHA: 11438bc1a0
Review: https://github.com/openclaw/openclaw/pull/86953#issuecomment-4545730044

Co-authored-by: masonxhuang <masonxhuang@tencent.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Mason Huang <masonxhuang@tencent.com>
Co-authored-by: Sebastien Tardif <sebtardif@ncf.ca>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: hxy91819
Co-authored-by: hxy91819 <8814856+hxy91819@users.noreply.github.com>
2026-06-01 09:25:56 +00:00
Vincent Koc
97d373ff37 perf(ui): speed up first global chat sends
Speed up Control UI first global chat sends by letting safe literal-global startup refresh use the fresh hello default before agents.list finishes, while keeping stale carried/cached agent ids out of that fast path. Adds chat history/send and gateway chat.send timing markers for the next latency pass.
2026-06-01 10:25:22 +01:00
Vincent Koc
3119f08009 fix(scripts): bound shrinkwrap npm commands 2026-06-01 11:23:20 +02:00
Peter Steinberger
9d55fc4579 fix(plugins): skip peer links in rollback snapshots 2026-06-01 10:18:30 +01:00
Vincent Koc
2bac970abc refactor: share node invoke policy test setup 2026-06-01 11:17:38 +02:00
Vincent Koc
f8e9ba3718 fix(codex): prevent aborted app-server turn handles 2026-06-01 10:12:36 +01:00
Vincent Koc
26aaf03719 fix(scripts): clean control ui i18n timeouts 2026-06-01 11:10:57 +02:00
Vincent Koc
e85be626a4 refactor: share plugin runtime scope test setup 2026-06-01 11:07:29 +02:00
Vincent Koc
9cb052ccef refactor: share plugin http route test setup 2026-06-01 10:56:09 +02:00
Peter Steinberger
637b073119 test(ui): update gateway session chat mock 2026-06-01 04:53:51 -04:00
Vincent Koc
174e7711f3 fix(build): clean CLI startup metadata timeouts 2026-06-01 10:52:27 +02:00
Vincent Koc
b13af38f99 perf(ui): trace chat first output latency
Add chat-send first visible assistant output telemetry in the Control UI, plus Gateway diagnostics correlation attributes for chat.send dispatch spans. Verified with focused UI/Gateway tests, tsgo, oxlint, autoreview, PR checks, and Testbox-through-Crabbox check:changed.
2026-06-01 09:47:45 +01:00
Vincent Koc
4094c94a8f refactor: share event loop health expectation 2026-06-01 10:47:05 +02:00
Peter Steinberger
32113e38ab perf(ci): speed up prompt snapshot checks 2026-06-01 04:44:41 -04:00
Peter Steinberger
07a425aa14 fix: preserve colon slash commands 2026-06-01 09:41:19 +01:00
Vincent Koc
db5bb1cbe7 refactor: share auth state test setup 2026-06-01 10:38:12 +02:00
Vincent Koc
947dde976c fix(release): bound plugin npm verification commands 2026-06-01 10:36:46 +02:00
Peter Steinberger
1d4c1ba56d fix: harden memory envelope sanitization
Co-authored-by: amittell <mittell@me.com>
2026-06-01 09:30:08 +01:00
Vincent Koc
de3ee3daa6 refactor: share auth context test helpers 2026-06-01 10:24:04 +02:00
Vincent Koc
61574eb50b perf(ui): keep chat draft local while typing (#88998) 2026-06-01 09:19:53 +01:00
Vincent Koc
e680604577 fix(e2e): clean telegram credential timeouts 2026-06-01 10:13:57 +02:00
Vincent Koc
2ea7c518a5 test(agents): avoid provider runtime in subagent spawn tests 2026-06-01 09:13:36 +01:00
Vincent Koc
7f95733bee refactor: share handshake locality test inputs 2026-06-01 10:12:30 +02:00
Peter Steinberger
a4196a4445 fix(ci): cache plugin sdk declarations safely 2026-06-01 04:09:07 -04:00
Vincent Koc
688634ccb9 refactor: share ws health test harness setup 2026-06-01 10:01:27 +02:00
Vincent Koc
060d4a4d2d test(gateway): widen live helper connect budget 2026-06-01 09:00:47 +01:00
Vincent Koc
f2d0fe6417 fix(release): clean cross-os process groups 2026-06-01 10:00:23 +02:00
Vincent Koc
6627b4fbdd perf(ui): guard chat composer controls
Reduce Control UI draft-update work by guarding chat composer controls while keeping locale, session, model, settings, and busy-state invalidation. Verification: focused UI tests, format/lint/typecheck, autoreview clean, and changed gate tbx_01kt12rgjs8c077p2s0wmcsbyf.
2026-06-01 08:56:14 +01:00
Peter Steinberger
3b64ea83e8 fix: migrate legacy OpenAI Codex lastGood auth state 2026-06-01 03:47:43 -04:00
Vincent Koc
1d62f4c014 fix(ci): satisfy scripts lint spread rule 2026-06-01 08:45:42 +01:00
Vincent Koc
3feeb95668 refactor: share minimal gateway test helpers 2026-06-01 09:44:48 +02:00
Vincent Koc
402e2bb81a perf(ui): guard chat transcript rerenders
Reduce Control UI draft-update work by guarding transcript group rendering while preserving assistant attachment availability invalidation. Verification: focused UI tests, format/lint/typecheck, autoreview clean, and changed gate tbx_01kt11qyc20ejbsbt8kd79bamx.
2026-06-01 08:41:04 +01:00
Peter Steinberger
bc470713bb fix(e2e): enable smoke-tested plugin channels 2026-06-01 08:38:50 +01:00
Vincent Koc
3322212f14 fix(ci): tolerate pnpm workspace state on Windows hydrate 2026-06-01 09:36:41 +02:00
Peter Steinberger
7591dc6f4b test(telegram): reset spooled polling handler state 2026-06-01 08:36:32 +01:00
Vincent Koc
6640d57b64 refactor: share websocket connection test harness 2026-06-01 09:29:43 +02:00
Vincent Koc
ac734d8e16 fix(e2e): clean package candidate timeouts 2026-06-01 09:22:07 +02:00
Vincent Koc
0ece07cc20 fix(test): wait for telegram timer flushes
Revert release-time extension lane isolation for Telegram and memory, and make Telegram timer-flush tests wait for async side effects after manually firing timers.

Verification:
- pnpm test:serial extensions/telegram/src/bot.create-telegram-bot.channel-post-media.test.ts extensions/telegram/src/bot.create-telegram-bot.media-group-skip-warning.test.ts extensions/telegram/src/bot.media.stickers-and-fragments.e2e.test.ts extensions/telegram/src/bot.media.downloads-media-file-path-no-file-download.e2e.test.ts test/vitest-scoped-config.test.ts
- pnpm exec oxfmt --check on touched files
- git diff --check on touched files
2026-06-01 08:17:33 +01:00
Vincent Koc
5e09113ede refactor: share selected global session test setup 2026-06-01 09:14:31 +02:00
Vincent Koc
bff66a3e49 perf(ui): skip closed slash menu rerenders
Reduce Control UI typing work by avoiding slash-menu rerenders for ordinary non-command drafts. Verification: focused UI tests, format/lint/typecheck, autoreview clean, and changed gate tbx_01kt1086xrbxfzm85vynsf25hq.
2026-06-01 08:14:16 +01:00
Vincent Koc
8071b06634 perf(ui): debounce chat draft persistence
Debounce draft-only Control UI chat composer persistence while snapshotting pending drafts so session changes and teardown still flush the correct state. Verified with focused UI lifecycle/composer tests, format, oxlint, tsgo core/UI test, clean autoreview, and PR checks.
2026-06-01 08:04:23 +01:00
Vincent Koc
61ffd6bc66 fix(ci): bootstrap raw changed gates from clean checkouts 2026-06-01 08:01:11 +01:00
Vincent Koc
474ec157bc test(scripts): use runner vitest resolver in expectations 2026-06-01 08:01:11 +01:00
Vincent Koc
1377fd82a9 refactor: share openai compat http test helpers 2026-06-01 08:55:28 +02:00
Vincent Koc
8fdb1d0f55 fix(e2e): stream Parallels fresh logs 2026-06-01 08:54:22 +02:00
Vincent Koc
68bfacae03 test(ci): wait for MCP tools list log 2026-06-01 07:49:01 +01:00
Vincent Koc
371617f9ed refactor: share gateway error response assertions 2026-06-01 08:42:59 +02:00
Vincent Koc
69b2c8bd15 perf(ui): record pending send paint timing (#88960) 2026-06-01 07:42:24 +01:00
Vincent Koc
c11ff35841 fix(e2e): bound Parallels update logs 2026-06-01 08:42:08 +02:00
Vincent Koc
ddbd595f2f fix(ci): link Windows hydrate node modules 2026-06-01 08:38:25 +02:00
Vincent Koc
01124cfca9 fix(e2e): clean secret proof timeouts 2026-06-01 08:30:17 +02:00
Vincent Koc
e8f3bce9f0 fix(ci): exempt child process test helper from sdk guard 2026-06-01 07:27:47 +01:00
Vincent Koc
cb0ad281ce perf(ui): cache chat transcript renders (#88952) 2026-06-01 07:27:08 +01:00
Vincent Koc
c429a3c472 fix(codex): skip stale bootstrap history without engine 2026-06-01 07:26:08 +01:00
Vincent Koc
444bdc4286 refactor: share child process test mock helper 2026-06-01 08:22:25 +02:00
Vincent Koc
28550c3847 fix(e2e): harden Parallels host timeouts 2026-06-01 08:15:34 +02:00
Vincent Koc
3e91c688ae fix(ui): scroll pending sends into view
Scroll the chat thread as soon as a submitted pending send is enqueued, so delayed `chat.send` ACKs no longer leave the user's just-sent message below the viewport.

Verification:
- focused UI Vitest suite: 86 tests passed
- oxlint, core tsgo, core-test tsgo, diff check
- Testbox changed gate: tbx_01kt0wspy1ks5wpb6kp5gr0512
- branch autoreview clean
2026-06-01 07:14:07 +01:00
Vincent Koc
4d49a76039 test(secrets): secure plugin exec fixtures 2026-06-01 07:11:28 +01:00
Vincent Koc
988ec0234e fix(agents): validate shell snapshots with trusted env 2026-06-01 07:11:28 +01:00
Vincent Koc
9a7e0d43da fix(codex): accept legacy app-server auth provider 2026-06-01 07:11:28 +01:00
Vincent Koc
f55ff8dd1b fix(codex): skip stale bootstrap history without engine 2026-06-01 07:11:28 +01:00
Vincent Koc
5314a39ee5 refactor: share usage UTC range assertions 2026-06-01 08:03:23 +02:00
Vincent Koc
44cad6f8a4 refactor: simplify wake APNs test mock 2026-06-01 07:59:17 +02:00
Vincent Koc
275caeb5f5 fix(ui): render pending sends in chat thread
Render submitted Control UI sends directly in the chat thread before the Gateway acknowledges `chat.send`.

Pending sends now share acknowledged user-message content rendering for text and attachments, stay searchable with active chat filters, and failed queued sends remain queue-only.

Verification:
- focused UI Vitest suite: 201 tests passed
- oxlint, core tsgo, core-test tsgo, diff check
- Testbox changed gate: tbx_01kt0vnr2bv55aa6x588r77x0z
- autoreview clean
2026-06-01 06:57:05 +01:00
Peter Steinberger
0f2732b066 test(release): isolate telegram extension vitest lane 2026-06-01 06:54:55 +01:00
Vincent Koc
59f1472bd5 refactor: share error coercion helper 2026-06-01 07:41:19 +02:00
Vincent Koc
630f0d6938 refactor: share push test response assertions 2026-06-01 07:36:51 +02:00
Peter Steinberger
6173a4babb docs(plugin-sdk): refresh API baseline 2026-06-01 06:29:51 +01:00
Vincent Koc
6a1b2e6463 refactor: share skills handler test helper 2026-06-01 07:27:52 +02:00
Vincent Koc
fb9e091852 fix(e2e): harden plugin gauntlet cleanup 2026-06-01 07:27:35 +02:00
Peter Steinberger
00399d6c75 test(release): repair beta validation blockers 2026-06-01 06:27:02 +01:00
Peter Steinberger
b23ace1d04 fix(agents): strip streamed reasoning tags (#88924) 2026-06-01 01:26:29 -04:00
Peter Steinberger
db4990d260 refactor: compact copilot sessions through sdk state
Route Copilot compaction through SDK-backed state, remove marker sidecars, preserve auth/session binding behavior in SQLite-backed plugin state, and route Copilot CLI budget compaction through native harness compaction.
2026-06-01 01:18:46 -04:00
Vincent Koc
4550cfa6a7 fix(qa): run plugin MCP probes from repo root 2026-06-01 07:13:24 +02:00
Chunyue Wang
c0195f7ed5 fix(diagnostics): clear embedded-run activity when recovery declares lane idle (#88820)
* fix(diagnostics): clear embedded-run activity when recovery declares lane idle

Stuck-session recovery transitions a lane to idle via the recovery
coordinator, but only mutated the session-state store. When an aborted
embedded run was removed without markDiagnosticEmbeddedRunEnded, the
activity store kept hasActiveEmbeddedRun set, so the liveness sweep
reported idle/embedded_run and isIdleQueuedRecoverableSessionStall
re-triggered recovery indefinitely.

Reconcile the activity store from the authoritative idle declaration by
clearing the session's embedded-run owners. The existing generation
guard already excludes any newer run that re-armed activity, so a live
requeued run is preserved.

* fix(diagnostics): reconcile tool/model activity on authoritative idle cleanup

clearDiagnosticEmbeddedRunActivityForSession (renamed from
clearDiagnosticEmbeddedRunsForSession) now clears the aborted run's tool and
model markers alongside the embedded-run owners, matching the default
markDiagnosticEmbeddedRunEnded teardown. Clearing only the owner set left the
lane as idle + orphaned tool/model activity, which
isIdleQueuedRecoverableSessionStall still treats as recoverable while work is
queued, so the liveness sweep kept re-triggering recovery instead of converging.
Adds regression cases with stale tool and model markers plus queued work.

* test(phone-control): align service mocks with keyed store API

* fix(diagnostics): preserve rearmed recovery activity

* fix(diagnostics): clear recovered owner markers

* fix(diagnostics): clear recovered embedded work keys

* fix(diagnostics): ignore stale same-key recovery owners

* fix(diagnostics): preserve same-session recovery rearm

* fix(diagnostics): ignore stale queued activity starts

* fix(diagnostics): record recovery cutoffs for empty activity

* fix(diagnostics): preserve fresh recovery markers

* fix(diagnostics): prune stale activity before fresh recovery block

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-06-01 01:07:35 -04:00
Tosko4
785849d395 fix(android): add notification app picker 2026-06-01 10:37:19 +05:30
Vincent Koc
12d5043913 refactor: share chat parentid test helpers 2026-06-01 07:06:05 +02:00
Peter Steinberger
d925249ac0 docs(plugin-sdk): refresh API baseline hash 2026-06-01 06:05:37 +01:00
Vincent Koc
74a075077c fix(e2e): harden docker all cleanup 2026-06-01 07:05:15 +02:00
Peter Steinberger
4e57546a87 test(memory): isolate qmd timer state in prerelease shard 2026-06-01 06:03:43 +01:00
Neerav Makwana
711ab45025 fix(agents): clear legacy auto fallback pins (#87484)
* fix(agents): clear legacy auto fallback pins

* fix(agents): repair legacy auto-fallback test mock and tighten review feedback

Add hasLegacyAutoFallbackWithoutOrigin to the live-model-switch agent-scope mock so the agents-core lane runs, simplify the redundant hasSessionModelOverride guard, use a single source of truth for the legacy-pin staleness check with a comment on the load-bearing modelKey guard, and add preservation/edge-case/guard regression coverage. Rename the misleading primary-probe agent test.

* style(agents): format rebased fallback fix

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-06-01 01:03:31 -04:00
Vincent Koc
e7e21caa20 fix(ui): keep first control chat sends responsive
Make first Control UI chat sends visibly queue during pending model saves, preserve early streaming deltas that arrive before chat.send ACK, and keep model-wait queued prompts scoped/retryable across session switches.
2026-06-01 05:59:04 +01:00
amittell
945faf8e67 fix(memory-lancedb): reject envelope metadata sludge
Summary:
- Strip memory-lancedb envelope and metadata sludge before auto-capture/recall, including pending history wrappers, current-message reply context, message-tool delivery hints, media annotations, and marker-free channel envelopes.
- Expose bundled chat-channel IDs/prefixes through the plugin SDK so sanitizer matching follows the channel catalog.
- Refactor cron tool schemas to fresh factory instances while preserving runtime nullable clears and provider-facing OpenAPI projection.

Verification:
- git diff --check origin/main...HEAD
- ./node_modules/.bin/oxfmt --check src/plugin-sdk/chat-channel-ids.ts src/plugin-sdk/chat-channel-ids.test.ts extensions/memory-lancedb/index.ts extensions/memory-lancedb/index.test.ts src/agents/tools/cron-tool.ts src/agents/tools/cron-tool.schema.test.ts
- pnpm plugin-sdk:api:check
- node scripts/run-vitest.mjs run src/plugin-sdk/chat-channel-ids.test.ts extensions/memory-lancedb src/agents/tools/cron-tool.schema.test.ts src/agents/tools/cron-tool.test.ts --reporter=dot
- pnpm lint:extensions --threads=8
- .agents/skills/autoreview/scripts/autoreview --mode branch --base origin/main
- GitHub checks on 62d1da1257: 139 pass, 0 pending, 0 fail, 22 skipped.
2026-06-01 00:57:25 -04:00
Vincent Koc
1aa1a70ac5 test(installer): isolate install shell HOME 2026-06-01 05:55:34 +01:00
Vincent Koc
abe2145153 refactor: share cron delivery test fixture 2026-06-01 06:52:52 +02:00
Vincent Koc
0ae0051ae7 feat(ui): improve Workboard task details
Make Workboard cards compact by moving expanded task/run metadata, proof, diagnostics, worker logs, automation, protocol state, events, and operator notes into a detail drawer.

Keep execution state simple and safe: active, linked, and archived cards avoid duplicate start paths; stale task cache is ignored when session lifecycle is authoritative; recent proof/events stay visible; dispatcher capacity distinguishes unclaimed review cards from claimed cards.
2026-06-01 05:52:40 +01:00
Vincent Koc
5957bfdc54 fix(e2e): fail bundled smoke on missing channels 2026-06-01 06:45:58 +02:00
Vincent Koc
e843a3612b refactor: inline secrets error response guard 2026-06-01 06:40:17 +02:00
Vincent Koc
8cab0f23f8 fix(e2e): clean bundled runtime smoke state 2026-06-01 06:35:28 +02:00
Vincent Koc
296cd8c912 fix(plugin-sdk): isolate provider catalog projection failures (#88767)
* fix(plugin-sdk): isolate provider catalog projection failures

* fix(plugin-sdk): share safe provider catalog projection

* fix(cron): preserve raw null clear schema

* fix(plugin-sdk): copy provider catalog model rows safely

* fix(plugin-sdk): keep id-only catalog models

* fix(plugin-sdk): require readable provider catalog base url

* fix(ci): satisfy cron and matrix lint checks

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-06-01 00:35:19 -04:00
Vincent Koc
3386bf989f refactor: share secrets resolve test helper 2026-06-01 06:31:03 +02:00
Peter Steinberger
5b79e81569 fix: harden CLI and plugin edge cases (#88896)
* fix: harden CLI and plugin edge cases

* fix: preserve explicit TTS provider credentials

* fix: preserve direct TTS credentials

* fix: type TTS credential hydration config

* fix: preserve scoped TTS channel credentials

* fix: pin hydrated TTS runtime config

* fix: satisfy TTS hydration lint

* fix: preserve inherited TTS provider keys

* fix: read resolved TTS provider keys
2026-06-01 00:30:12 -04:00
Vincent Koc
ec6ad888a4 fix(e2e): bound telegram proof commands 2026-06-01 06:26:44 +02:00
Kip
c213827aa5 fix(cron): include job name when reading single-job run history (#88294)
* fix(cron): include job name in single-job run history

The cron.runs gateway handler enriches log entries with jobName in the all-jobs scope, but the single-job scope did not pass any job-name lookup into the SQLite run-log reader. Entries returned for one job could therefore reach Control UI without jobName, making the run-history title fall back to the raw job id.

Build a one-entry jobNameById map for the current job and pass it through the same reader enrichment path used by all-jobs history. If the job no longer exists, the map stays undefined and existing fallback behavior is unchanged.

* test(cron): cover single-job run history job name enrichment

Asserts that readCronRunLogEntriesPage stamps a supplied jobNameById map onto single-job page entries, matching the gateway data shape used for both all-jobs and single-job cron.runs responses.

Addresses review feedback on #88294.

* test(cron): preserve nullable tool schema validation

* test(cron): assert runtime nullable tool schema

* test(cron): refresh prompt snapshots

---------

Co-authored-by: Kip Claw <kip@kipclaw.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-06-01 00:25:39 -04:00
Vincent Koc
dc9e67d2d4 refactor: share tools catalog test helpers 2026-06-01 06:13:33 +02:00
Peter Steinberger
b2a1c5caa8 test(matrix): keep async monitor callbacks lint-clean 2026-06-01 05:11:28 +01:00
Vincent Koc
51bad9b319 refactor: share config open file test helpers 2026-06-01 06:08:13 +02:00
Vincent Koc
fb17986af5 fix(ci): preserve hydrated Windows test deps 2026-06-01 06:08:10 +02:00
Vincent Koc
17245a0890 fix(test): bound qa otel smoke runs 2026-06-01 06:04:30 +02:00
Peter Steinberger
3b802a7fbc docs(plugin-sdk): refresh API baseline hash 2026-06-01 04:59:39 +01:00
Vincent Koc
e9c7a64c5e refactor: share update test helpers 2026-06-01 05:58:33 +02:00
Peter Steinberger
817c4ce4fc test(release): stabilize installer and matrix async checks 2026-06-01 04:55:21 +01:00
Vincent Koc
d4240cde5b refactor: share native hook relay test helpers 2026-06-01 05:48:14 +02:00
Ted Li
6cb06f5fbc fix(reply): preserve sessions_send external routes (#88803)
* fix(reply): preserve sessions_send external routes

* fix(reply): preserve inherited route thread ids

* fix(reply): keep sessions_send delivery single-owner

* fix(reply): satisfy dispatch route lint

* fix(reply): preserve inherited ACP route metadata

* test(reply): type inherited route event assertions

* test(ci): satisfy current lint rules

* fix(reply): avoid stale inherited route threads

* fix(reply): trust explicit inherited route threads

* fix(reply): require trusted route thread sources

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-05-31 23:43:05 -04:00
Firas Alswihry
70c59f59b2 feat(dreaming): score candidates with shadow trial results
Add report-only memory-core dreaming shadow trial scoring and ranking helpers. Keep rank lookup keyed by durable candidate keys and document the advisory behavior. Thanks @iFiras-Max1.
2026-05-31 23:40:20 -04:00
Vincent Koc
a30c12e711 refactor: share restart test helpers 2026-06-01 05:38:35 +02:00
Vincent Koc
56a7000b3e fix(mattermost): route send attachments through upload
Routes Mattermost send attachments through the upload path so local file paths and structured attachment media are uploaded instead of being posted as plain text. Preserves scoped media access for local uploads, rejects unsupported or ambiguous attachment payloads, and keeps HTTP media fallback behavior.

Fixes #87930.

Proof:
- autoreview clean
- node scripts/run-vitest.mjs extensions/mattermost/src/channel.test.ts extensions/mattermost/src/channel.message-adapter.test.ts extensions/mattermost/src/mattermost/send.test.ts src/infra/outbound/message-action-params.test.ts src/infra/outbound/outbound-send-service.test.ts src/infra/outbound/message-action-runner.media.test.ts src/media/load-options.test.ts
- pnpm prompt:snapshots:check
- GitHub Actions completed with no pending/failing checks for head 2a65cbb1ee
2026-05-31 23:38:17 -04:00
Vincent Koc
5054b20832 fix(test): harden secret provider proof cleanup 2026-06-01 05:37:45 +02:00
Vincent Koc
a5ee3569d3 test(ci): refresh cron prompt snapshots 2026-06-01 04:33:39 +01:00
Peter Steinberger
33349269fd fix: wake legacy cron jobs without enabled 2026-05-31 23:31:44 -04:00
Vincent Koc
2dcee8ac2b refactor: share webchat media audio fixture 2026-06-01 05:28:03 +02:00
Peter Steinberger
e2c9c06de1 fix: advance exact-boundary every schedules 2026-05-31 23:27:24 -04:00
Peter Steinberger
ebcdb637bb perf(memory-core): defer embedding engine startup imports 2026-06-01 04:22:22 +01:00
Peter Steinberger
592b6e2916 docs(config): refresh config baseline hash 2026-06-01 04:20:57 +01:00
Peter Steinberger
45b5f876dd fix: reject blank cron payloads 2026-05-31 23:20:04 -04:00
Vincent Koc
76fa1b99c3 fix(test): bound test group report runs 2026-06-01 05:17:27 +02:00
Vincent Koc
aab1e727c6 refactor: share chat abort authorization helpers 2026-06-01 05:16:37 +02:00
Vincent Koc
a46d331723 fix(ci): reword durable final alias comment 2026-06-01 04:12:46 +01:00
Vincent Koc
916ee82814 test(installer): isolate install shell snippets 2026-06-01 04:11:22 +01:00
Vincent Koc
fcc279e233 fix(test): avoid Vite runtime import in UI config helpers 2026-06-01 04:08:53 +01:00
Vincent Koc
9dd7f04b71 fix(ci): repair phone control and cron schema gates 2026-06-01 04:06:25 +01:00
Vincent Koc
6e985931de refactor: share models list test helper 2026-06-01 05:05:20 +02:00
Vincent Koc
dc1cfcc28d refactor: share tasks handler test helpers 2026-06-01 05:05:20 +02:00
Peter Steinberger
ee6373aa5f fix: preserve cron failure destination clears 2026-05-31 23:04:31 -04:00
Peter Steinberger
6deded6698 fix: raise bootstrap file default limit 2026-06-01 04:02:51 +01:00
Peter Steinberger
f879e3d6a0 docs(plugin-sdk): refresh API baseline hash 2026-06-01 04:01:25 +01:00
Vincent Koc
f42cf9059e fix(ci): repair phone control and cron schema gates 2026-06-01 04:00:18 +01:00
Andy Ye
c317fd2bd7 docs(imessage): document SSH wrapper TCC send failure (#88758) 2026-05-31 23:00:08 -04:00
Vincent Koc
be967545c5 fix(plugins): fail closed on trusted policy errors
Fail closed when bundled trusted tool policy registry, registration, owner id, evaluation, or decision reads fail, so malformed trusted-policy state cannot crash diagnostics or accidentally allow a tool call.

Route before-tool-call diagnostics through guarded trusted-policy readers and keep healthy no-op policy behavior unchanged.

Add focused host-hook contract and before-tool-call e2e coverage for the new fail-closed paths.

PR: #88394
2026-05-31 22:57:38 -04:00
Nayrosk
388ba3218b fix(ui): bypass service worker for top-level navigations
HTTP auth challenges (basic, digest, negotiate) only fire the browser's
native credentials dialog when the response comes straight from the
network. Service worker responses bypass the WWW-Authenticate flow, so
reverse-proxy deployments with HTTP auth in front of the gateway show
a bare 401 after the browser's HTTP-auth memory cache expires (e.g. on
full browser restart) — forcing users to clear site data to recover.

Skip event.request.mode === "navigate" so the browser handles those
requests natively. Offline navigation of the app shell is lost, but
the SPA cannot function without network (all API calls go to the
network), so the trade-off is acceptable.

Refs: #85939, #71669, #53274
2026-05-31 22:57:27 -04:00
Peter Steinberger
7722ade22e test(install): clear node lookup cache in floor check 2026-06-01 03:56:37 +01:00
Vincent Koc
b2b9fbe033 fix(test): bound mock OpenAI request bodies 2026-06-01 04:48:32 +02:00
Peter Steinberger
551c9637d8 fix(ios): polish iPad gateway setup 2026-06-01 03:47:09 +01:00
Vincent Koc
c5eddadd9d refactor: share channel start test helpers 2026-06-01 04:40:21 +02:00
Vincent Koc
98b8e85beb refactor: share agent wait dedupe test helpers 2026-06-01 04:35:37 +02:00
Vincent Koc
a9938907dc fix(test): harden MCP E2E proof checks 2026-06-01 04:34:25 +02:00
Peter Steinberger
4c824aa809 perf(phone-control): use startup config for expiry guard 2026-06-01 03:32:38 +01:00
Peter Steinberger
1e7510ae10 docs: continue inline comment pass (#88849)
Adds broad inline comments and JSDoc for CLI, cron, outbound/channel, plugin SDK, ACP, shared helpers, net policy, and related utility contracts. Proof: git diff --check on latest exact head plus focused cron tests passed; CI had no failing checks observed before merge attempt.
2026-05-31 22:32:28 -04:00
Peter Steinberger
4932391e8a fix(ui): scope global agent model controls 2026-05-31 22:25:43 -04:00
Vincent Koc
822864c539 refactor: share channel status test helpers 2026-06-01 04:24:57 +02:00
Vincent Koc
a7ae3f6707 refactor: share usage session test state setup 2026-06-01 04:24:57 +02:00
Dallin Romney
78165cc387 docs: clarify diffs language pack additions (#88865) 2026-05-31 19:24:45 -07:00
Peter Steinberger
44765cfabe fix(acpx): seed Codex ACP auth from API key 2026-05-31 22:24:29 -04:00
Vincent Koc
0c3644cb24 perf(ui): stream stable markdown blocks 2026-06-01 03:23:47 +01:00
Peter Steinberger
53a7545ae3 perf(phone-control): avoid disarmed startup state lookup 2026-06-01 03:19:08 +01:00
Andy Ye
921598442a fix(hooks): expose inbound reply metadata before dispatch
Fixes #88521.

Expose finalized inbound reply metadata on plugin-visible hook payloads so before_dispatch and message hooks can implement reply-aware behavior without channel-specific workarounds.
2026-05-31 22:15:17 -04:00
Peter Steinberger
e72def6983 Persist Discord thread bindings in SQLite (#88866)
* refactor: persist discord thread bindings in sqlite

* test: read discord thread bindings from sqlite smoke
2026-05-31 22:10:30 -04:00
ksj3421
45bdaa2f7b fix(agents): return schema lookup misses in-band
Return unknown config.schema.lookup paths as an in-band agent gateway tool result instead of throwing into channel warning surfaces.

The direct gateway RPC still reports INVALID_REQUEST, preserving the existing protocol contract, while the agent-facing gateway tool returns schema_path_not_found for exploratory misses.

Fixes #88813.
Thanks @ksj3421.
Reported by @cjalden.
2026-05-31 22:10:02 -04:00
Vincent Koc
91ca036717 test(agents): use neutral tool schema fixtures (#88848) 2026-05-31 22:09:48 -04:00
Ted Li
c002887223 fix(memory): rehydrate daily list promotions
* fix(memory): rehydrate daily list promotions

* fix(memory): preserve multi-line daily list promotions

* fix(memory): preserve daily list promotion context

* fix(memory): rehydrate capped daily list promotions

* test(memory): cover capped daily list promotion

* test(agents): update model selection mocks

* ci: ignore lazy three dependency

* fix(memory): skip heading-only rehydration

* fix(memory): preserve list rehydration mode

* fix(memory): match capped renamed heading bodies

* fix(memory): avoid duplicate tail heading matches

* fix(microsoft-foundry): satisfy provider lint

* perf(memory): precompute promotion heading context

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-05-31 22:08:35 -04:00
Peter Steinberger
912ea4897f fix: scope global in-flight history to default agent 2026-06-01 03:08:29 +01:00
Peter Steinberger
6ad73e173b fix: keep hidden runs out of visible chat state 2026-06-01 03:08:29 +01:00
Vincent Koc
6c73ffc51a fix(test): bound MCP code mode client responses 2026-06-01 04:05:23 +02:00
Dallin Romney
632447d66d test(ui): remove stylesheet grep tests (#88847) 2026-05-31 19:05:02 -07:00
Dallin Romney
4b56c44c02 test: consolidate plugin registration contracts (#88824) 2026-05-31 19:04:53 -07:00
Peter Steinberger
d86b6da012 fix: allow cron delivery clears 2026-05-31 22:04:25 -04:00
Vincent Koc
d2c5ad2b36 refactor: share commands test helpers 2026-06-01 04:01:02 +02:00
Dallin Romney
b097cec219 fix(microsoft-foundry): satisfy extension lint (#88855) 2026-05-31 18:58:56 -07:00
Vincent Koc
207359a056 fix(ci): repair current main checks
Summary:
- Guard child-session candidate lookup when the session store is absent.
- Refresh Talk UI and compaction rotation tests for current main.
- Clean up Microsoft Foundry provider lint that blocked the refreshed CI lane.

Verification:
- node scripts/run-vitest.mjs src/gateway/session-utils.test.ts ui/src/ui/views/chat.test.ts src/agents/agent-command.compaction-rotation.test.ts --reporter=dot
- node scripts/run-vitest.mjs extensions/microsoft-foundry/index.test.ts --reporter=dot
- node_modules/.bin/oxfmt --check --threads=1 extensions/microsoft-foundry/provider.ts src/gateway/session-utils.ts ui/src/ui/views/chat.test.ts src/agents/agent-command.compaction-rotation.test.ts
- node scripts/run-oxlint.mjs extensions/microsoft-foundry/provider.ts src/gateway/session-utils.ts ui/src/ui/views/chat.test.ts src/agents/agent-command.compaction-rotation.test.ts
- pnpm lint --threads=8
- autoreview clean
- GitHub checks on f96270ed7e: 135 success, 29 skipped, 1 neutral, 0 pending/failing

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-05-31 21:57:07 -04:00
Rohit
3fc485ca92 fix(browser): isolate Chrome MCP pending attach aborts (#88305)
* fix(browser): isolate Chrome MCP pending attach aborts

* fix(browser): evict closing Chrome MCP sessions

* fix(browser): clean chrome mcp pending session lifecycle

* fix(browser): handle stale chrome mcp pending sessions

* fix(browser): serialize stale chrome mcp replacement

* fix(browser): skip cancelled chrome mcp attach

* fix(browser): retire timed-out chrome mcp pending sessions

* fix(browser): retire stale chrome mcp after readiness

* fix(browser): keep shared chrome mcp timeouts isolated

* fix(browser): bound stale chrome mcp ready retries

* fix(browser): narrow pending session lease release

* fix(browser): keep ephemeral probes out of pending attaches

* fix(foundry): satisfy provider lint

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-05-31 21:55:38 -04:00
Peter Steinberger
2b184ac3a0 docs(changelog): refresh 2026.5.31 notes 2026-06-01 02:52:49 +01:00
Vincent Koc
be1273182e refactor: share models auth status test helpers 2026-06-01 03:49:46 +02:00
Vincent Koc
c764eb96c4 fix(test): tolerate vanished RPC gateway teardown 2026-06-01 03:48:59 +02:00
Peter Steinberger
0369672691 feat(minimax): add m3 model support (#88860) 2026-05-31 21:47:47 -04:00
Vincent Koc
9919e4601f refactor: share skills clawhub test helpers 2026-06-01 03:38:39 +02:00
Vincent Koc
b6bac3cc2b test(agents): include Ollama in small live model matrix (#87838)
* test(agents): include Ollama in small live model matrix

* test: avoid Ollama cloud key in local live runs

* test: recognize Ollama env secret refs

* test: type Ollama live key fixtures

* test: prevent Ollama cloud auth in local live probes

* test: preserve equivalent Ollama live credentials

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-05-31 21:38:31 -04:00
Peter Steinberger
72bc9ae952 fix: keep cron update delivery validation scoped 2026-05-31 21:32:23 -04:00
Peter Steinberger
d2f1c0eac8 fix: harden cron validation and restart state 2026-05-31 21:32:23 -04:00
kiranmagic7
cc97eca9b1 test(installer): keep Node floor tied to package engine
Adds a focused installer regression test tying install.sh's accepted Node 22 floor to the package engine floor. Thanks @kiranmagic7.
2026-05-31 21:32:00 -04:00
Vincent Koc
dbc83b4213 refactor: share chat reply media test helpers 2026-06-01 03:29:30 +02:00
Vincent Koc
2d0c755013 fix(test): order unit-fast fake-timer project 2026-06-01 02:24:48 +01:00
Peter Steinberger
fb64546d9e fix: preserve no-policy native hook fallback
Keep selected no-policy Codex PreToolUse relay hooks installed with an explicit unavailable no-op marker, while unknown unavailable PreToolUse and PermissionRequest still fail closed.

Refs #87543.
Replaces #88620.

Verification:
- pnpm test extensions/codex/src/app-server/native-hook-relay.test.ts src/agents/harness/native-hook-relay.test.ts src/cli/native-hook-relay-cli.test.ts
- pnpm lint --threads=8
- autoreview --mode branch --base origin/main
- GitHub CI run 26729700996, Real behavior proof 26729874455, OpenGrep 26729701010, CodeQL high 26729701003

Co-authored-by: woodym-dotcom <266261448+woodym-dotcom@users.noreply.github.com>
2026-05-31 21:24:09 -04:00
EmpX2025
83f290005a feat(ios): support native iPad display
Make the iOS app a universal iPhone+iPad app by targeting device family 1,2 in the XcodeGen source of truth.

Update iOS docs and App Store metadata so user-facing copy no longer describes the app as iPhone-only.

Verification:
- git diff --check
- cd apps/ios && xcodegen generate
- xcodebuild -project apps/ios/OpenClaw.xcodeproj -scheme OpenClaw -configuration Debug -destination 'platform=iOS Simulator,id=410B81D3-784E-4A01-B69C-490B79EAFCEA' CODE_SIGNING_ALLOWED=NO build
- GitHub CI: Real behavior proof, macos-swift, macos-node, check-docs, preflight, security-fast, actionlint, no-tabs, dependency-guard, OpenGrep

Thanks @EmpX2025.
2026-05-31 21:23:33 -04:00
William Liu AI
8eeb9300df fix: restore in-flight TUI run switch-back
Restore TUI switch-back adoption for backgrounded visible chat-send runs by surfacing a bounded `chat.history.inFlightRun` snapshot.

The snapshot keeps the run id even when buffered text is empty or over budget, filters live text through the same projection path as streaming chat, scopes bare global history to the default agent, and excludes hidden internal agent runs.

Proof:
- node scripts/run-vitest.mjs run src/gateway/chat-abort.test.ts src/tui/tui-session-actions.test.ts
- node scripts/run-tsgo.mjs -p tsconfig.core.json
- pnpm --silent exec oxfmt --check src/gateway/chat-abort.ts src/gateway/chat-abort.test.ts src/gateway/server-methods/chat.ts src/tui/tui-session-actions.ts src/tui/tui-session-actions.test.ts
- .agents/skills/autoreview/scripts/autoreview --mode branch --base origin/main
- CI: Real behavior proof, TUI PTY, dependency guard, OpenGrep precise diff, workflow sanity passed on PR head 2b8bf5f214.

Co-authored-by: William Liu <william@williamliu.ai>
2026-05-31 21:22:52 -04:00
Vincent Koc
52c809a759 fix(infra): bridge WSL clipboard through shell
* fix(infra): bridge WSL2 clipboard through shell

* test(infra): assert wsl clipboard argv stays token-free

* fix(infra): keep wsl clipboard timeout ownership
2026-05-31 21:22:08 -04:00
elfka toruviel
f22e39823d fix(doctor): respect explicit PI runtime policy
Respect explicit PI/OpenClaw runtime policy when deciding whether Codex plugin diagnostics are actionable.

Diagnostics now use the resolved OpenAI route: intentional PI and custom OpenAI-compatible routes suppress only the missing `plugins.entries.codex` noise, while enabled/stale Codex policy still warns.

Proof: focused doctor/config/agent routing Vitest coverage, full lint, test types, dependency checks, isolated live doctor configs, autoreview clean, and GitHub CI green at c5a84de4ca.

Fixes #88706.

Co-authored-by: Elfka Toruviel <aeb31988340aa87b@toruviel.online>
2026-05-31 21:21:11 -04:00
Vincent Koc
30bde29893 refactor: share config auth test helpers 2026-06-01 03:20:04 +02:00
Peter Steinberger
6b940ed3ca perf: streamline chat startup metadata (#88825)
* perf: streamline chat startup metadata

* fix: defer global queued agent selection

* style: format gateway startup refresh
2026-05-31 21:18:41 -04:00
Andy Ye
1b10739d60 fix(agents): guard vanished workspaces
Fixes #88333

Preserves contributor workspace contents when an attested workspace disappears or is partially regenerated, and clears OpenClaw-owned attestation state on delete/reset/uninstall.

Co-authored-by: Andy Ye <35905412+TurboTheTurtle@users.noreply.github.com>
2026-05-31 21:14:54 -04:00
Vincent Koc
efd5d07734 fix(ci): preserve lint cleanup behavior 2026-06-01 03:12:53 +02:00
Peter Steinberger
1d4277b071 refactor: persist openrouter model cache in sqlite (#88851) 2026-05-31 21:12:43 -04:00
Vincent Koc
b029634bd6 refactor: share cron validation test helpers 2026-06-01 03:08:54 +02:00
Vincent Koc
af927038cc test(gateway): fail strict codex subagent timeouts 2026-06-01 03:08:02 +02:00
Peter Steinberger
5b0c4c0491 fix: align Foundry chat reasoning metadata 2026-06-01 02:05:38 +01:00
Vincent Koc
570e2db252 fix(plugins): isolate cached tool runtime siblings 2026-05-31 21:05:23 -04:00
Vincent Koc
53990d5bbf fix(plugins): isolate web provider factory failures (#88807) 2026-05-31 21:04:18 -04:00
NVIDIAN
37169697d7 fix(status): resolve gateway auth secrets for deep audit
Resolve gateway auth SecretRef targets in status deep audit.

The static secret target coverage now includes gateway auth and remote token/password keys for both status and security audit scans. Focused status/secret-target tests passed, Auto Review reported no actionable findings, and CI is running on rebased head 41b052a181.

Fixes #87815
2026-05-31 21:02:11 -04:00
Alix-007
909c24e3b7 fix(config): skip state-dir dotenv values that are unresolved shell references (#88288)
* fix(config): skip state-dir dotenv values that are unresolved shell references

readStateDirDotEnvVarsFromStateDir accepted any non-empty value from the
state-dir .env file and passed it into the managed service env. When a value
contains an unresolved shell variable reference such as "${SUPERMEMORY_KEY}"
or "$MY_VAR", dotenv preserves the literal string. The value then reaches
the LaunchAgent/systemd wrapper as a single-quoted literal, so the credential
is never resolved.

Add containsUnresolvedShellReference() and skip any value matching
$IDENTIFIER, ${...}, or $(...) in parseStateDirDotEnvContent(). Real credential
values (e.g. "sm_abc123") are unaffected.

Fixes #88274

* fix(config): narrow shell-reference detector to whole-value patterns only

The previous /$[\w{(]/ regex matched any value containing $ followed by
a word character, which would incorrectly drop real credentials that merely
contain a dollar sign (e.g. a password like abc$2!xyz).

Replace with isUnresolvedShellReference() that only matches values whose
ENTIRE content is a recognised reference form:
  - $VAR_NAME (simple reference)
  - ${VAR_NAME} (brace-form reference)
  - $(command) (command substitution)

Add a regression test that verifies dollar-bearing real secrets are kept.

* fix(config): use letter/underscore-anchored pattern to avoid matching dollar-numbers

$100, $2, etc. are NOT shell variable references — shell variable names must
begin with a letter or underscore. The previous /^$[\w_]/ would match them.

Change to /^$[A-Za-z_]\w*$/ so only genuine named-variable references like
$MY_VAR are rejected. Dollar-number sequences are now preserved.

* fix(daemon): drop stale systemd env-file refs for skipped state-dir dotenv keys

When a state-dir .env value is an unresolved shell reference ($VAR/${VAR}/$(cmd))
the parser skips it from the managed environment. A prior install could have
written that literal reference into gateway.systemd.env; because the skipped key
no longer appeared in the incoming env or the managed-key removal sets, the stale
literal survived re-stage and could override fresh inline Environment= values.

Surface the skipped shell-reference keys from the state-dir dotenv parser and add
them to the systemd env-file managed-key removal set so re-staging strips the
obsolete literal while preserving operator-only secrets that were never managed
via state-dir .env. launchd regenerates its env file wholesale, so it is
unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(config): skip quoted shell parameter dotenv refs

* fix(config): preserve lowercase dollar-prefixed dotenv literals

* fix(daemon): clear stale unresolved systemd env refs

* fix(daemon): avoid re-staging unresolved file env refs

* fix(daemon): drop unresolved file env refs inline

* fix(daemon): drop inline-and-file unresolved env refs

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-05-31 21:01:33 -04:00
Vincent Koc
732748c8c5 perf(ui): skip markdown parsing while chat streams 2026-06-01 02:00:06 +01:00
Brian
fda5254e99 fix: preserve npm plugin root on blocked install (#77237)
Preserve the active per-plugin managed npm project when npm-backed install validation blocks a candidate after npm has already mutated local state.

This snapshots package.json, package-lock.json, and node_modules before managed npm installs, restores that exact project state on failed validation, and rolls back staged npm-pack archives so blocked pack installs do not leave candidate debris.

Validation:
- OPENCLAW_VITEST_MAX_WORKERS=1 node scripts/run-vitest.mjs src/plugins/install.npm-spec.test.ts
- pnpm tsgo:core && pnpm tsgo:core:test
- node scripts/run-oxlint.mjs src/plugins/install.ts src/plugins/install.npm-spec.test.ts
- .agents/skills/autoreview/scripts/autoreview --mode local
- GitHub CI 26729255950
- Crabbox run_26e9f9f7591c

Thanks @zhuisDEV.

Co-authored-by: Brian <95547369+zhuisDEV@users.noreply.github.com>
2026-05-31 20:59:32 -04:00
Vincent Koc
9da4835cdf refactor: share artifacts test helpers 2026-06-01 02:57:01 +02:00
Vincent Koc
43ced7bc49 fix(ui): preserve startup chat sends during history load 2026-06-01 01:52:58 +01:00
Vincent Koc
49b62079f7 fix(ui): unblock initial control chat send 2026-06-01 01:52:58 +01:00
Andy Ye
432312a17c test: cover Vertex API key model config
Adds regression coverage for Google Vertex API-key model config planning when the credential comes from an env-backed auth profile. This keeps the planner-level guard around the Vertex static catalog rows that fixed #88816 on main.

Verification:
- `node scripts/run-vitest.mjs src/agents/models-config.applies-config-env-vars.test.ts extensions/google/provider-catalog.test.ts extensions/google/provider-models.test.ts`
- `./node_modules/.bin/oxfmt --check --threads=1 src/agents/models-config.applies-config-env-vars.test.ts extensions/ollama/src/stream.ts extensions/qa-lab/src/mantis/slack-desktop-smoke.runtime.ts extensions/qa-lab/src/mantis/telegram-desktop-builder.runtime.ts extensions/qa-lab/src/mantis/visual-task.runtime.ts`
- `git diff --check`
- `pnpm deadcode:dependencies`

CI note: PR CI had an unrelated `check-dependencies` failure for `ui/package.json: three`; the PR diff is one `src/agents` test file.

Refs #88816
2026-05-31 20:51:50 -04:00
Peter Steinberger
5443baa852 Persist plugin install index in SQLite (#88794)
* refactor: persist plugin install index in sqlite

* fix: merge legacy plugin index records into sqlite

* test: update plugin index sqlite fixtures

* fix: migrate custom plugin install indexes

* test: update plugin index sentinel

* fix: exclude migrated plugin index archives

* fix: read post-upgrade plugin index from sqlite

* fix: migrate legacy plugin index before agent runs

* fix: respect disabled persisted plugin registry reads

* test: type plugin install record fixtures

* fix: simplify plugin index record reader type

* test: fix sqlite plugin index CI fallout

* test: mock provider normalization in agent command tests

# Conflicts:
#	src/commands/agent-command.test-mocks.ts

* build: remove unused ui three dependency
2026-05-31 20:51:33 -04:00
Vincent Koc
b475de834a refactor: share plugin approval test helpers 2026-06-01 02:45:37 +02:00
Matthew Schleder
6a96058f50 fix(minimax): use account oauth endpoints
Routes MiniMax OAuth device-code and token polling directly to account-hosted OAuth2 endpoints for global and CN regions, avoiding guarded-fetch cross-origin redirect body stripping. Keeps provider API base URLs unchanged and adds regression coverage for both endpoint pairs.

Proof: local minimax OAuth tests, oxfmt check, lint, autoreview clean, official MiniMax CLI/source check, live MiniMax endpoint probes, and CI run 26729242892 on 6bfe20eb06.

Co-authored-by: Matt Schleder <schledermatthew@gmail.com>
2026-05-31 20:44:41 -04:00
Vincent Koc
82d24b26ea fix(workboard): wire task-backed board runs
Summary:
- remove the leftover Workboard mini-game/prototype surface
- wire autonomous Workboard card starts through Gateway task-backed agent runs
- reconcile card task/session lifecycle for starts, stops, stale tasks, reassignment, and default-agent sessions
- clarify dispatch summary copy and admin-only model override behavior

Verification:
- autoreview clean: no accepted/actionable findings
- targeted Workboard/UI Vitest: 72 tests passed
- Workboard extension Vitest: 9 tests passed
- UI build, docs list, docs format, diff check, and focused oxlint passed
- PR CI checks: 50 ok, 0 attention
- Testbox tbx_01kt07mk5sjyj2whjq2sc967hg: pnpm verify check phase passed; broad test phase exposed unrelated latest-main failures/stalls in memory, Codex app-server, provider timeout, command daemon env, Telegram worker OOM, and gateway-client timeout suites
2026-06-01 01:41:21 +01:00
Vincent Koc
015c6b40ae fix(ci): clear extension lint regressions 2026-06-01 01:36:16 +01:00
Vincent Koc
915c156115 refactor: share tools effective test helpers 2026-06-01 02:33:47 +02:00
Vincent Koc
b3742b9edb fix(ui): stream chat deltas incrementally 2026-06-01 01:32:48 +01:00
Vincent Koc
bcaf326c3a refactor: share sessions abort scope test helpers 2026-06-01 02:21:44 +02:00
Vincent Koc
3c7c03f236 test(ci): update agent command model-selection mocks 2026-06-01 01:18:09 +01:00
Peter Steinberger
7562afdca3 fix(ollama): suppress disabled reasoning output 2026-06-01 01:16:47 +01:00
Peter Steinberger
27dde7a4d6 chore(lint): enable stricter error rules 2026-06-01 01:12:21 +01:00
Vincent Koc
0bfba7e26d fix(ui): detect system chromium for e2e 2026-06-01 01:09:46 +01:00
Vincent Koc
d95471afef test: type manifest catalog mock 2026-06-01 02:06:26 +02:00
Vincent Koc
69c948a752 refactor: share web start test snapshot 2026-06-01 02:06:26 +02:00
Andy Ye
002c1d2d5a test(agents): cover nonfatal trajectory flush timeout
Fixes #88520.

Adds focused regression coverage for the embedded attempt trajectory recorder cleanup boundary so a stalled trajectory flush resolves after the cleanup timeout and logs pending write details instead of rejecting attempt cleanup.

Verification:
- node scripts/run-vitest.mjs src/agents/run-cleanup-timeout.test.ts
- git diff --check origin/main...origin/pr/88802
- PR CI green: https://github.com/openclaw/openclaw/actions/runs/26727232564

Co-authored-by: Andy Ye <35905412+TurboTheTurtle@users.noreply.github.com>
2026-05-31 20:01:12 -04:00
Vincent Koc
2fc5072021 fix(ci): repair Copilot thinking compat types 2026-06-01 00:53:35 +01:00
Vincent Koc
160aad6fb3 fix(agents): preserve exact custom provider models 2026-06-01 01:50:30 +02:00
Vincent Koc
dd8d52c7d9 refactor: share optional model catalog loading 2026-06-01 01:49:51 +02:00
Peter Steinberger
219d854178 fix: keep tool detail redaction canonical 2026-06-01 00:49:43 +01:00
Vincent Koc
37d79a4303 test(ui): make chat sessions e2e deterministic 2026-06-01 00:45:29 +01:00
Ted Li
6316648bab fix(openai): keep stop-finished tool calls
Preserve silent structured OpenAI-compatible tool calls when providers stream tool_calls but finish with finish_reason stop, while keeping visible-text stop responses and unfinished streams from executing spurious tool calls.

Fixes #88791.

Verification:
- pnpm tsgo:prod
- node scripts/run-vitest.mjs src/llm/providers/openai-completions.test.ts src/agents/openai-transport-stream.test.ts
- loopback OpenAI-compatible SSE proof against createOpenAICompletionsTransportStreamFn
- .agents/skills/autoreview/scripts/autoreview --mode branch --base origin/main
2026-05-31 19:41:23 -04:00
Gio Della-Libera
bf777b9af2 fix(doctor): quiet tool policy audits during probes
Keep runtime tool-policy removal audits at the normal info level, but lower diagnostic-only doctor tool-schema probes to debug so expected profile filtering does not clutter normal doctor output.

Also updates current-base test expectations for the Talk custom select and a promise-executor lint rule so the PR remains green on the latest base.

Fixes #87798.

Proof:
- CI https://github.com/openclaw/openclaw/actions/runs/26727664397
- Real behavior proof https://github.com/openclaw/openclaw/actions/runs/26727667473
- Local focused Vitest, broad lint, touched-file format/lint, and autoreview clean.

Co-authored-by: Gio Della-Libera <40915808+giodl73-repo@users.noreply.github.com>
2026-05-31 19:37:13 -04:00
Peter Steinberger
fba9eac7eb fix(google): register Vertex static catalog rows 2026-06-01 00:36:31 +01:00
Peter Steinberger
5965522af5 fix(copilot): preserve Claude 1M capabilities 2026-06-01 00:36:31 +01:00
Peter Steinberger
f18fd2094f fix(agents): match provider-scoped context ids 2026-06-01 00:36:30 +01:00
Peter Steinberger
770ee8eba6 fix(models): refresh provider catalog metadata 2026-06-01 00:36:30 +01:00
Vincent Koc
b891d42f3a refactor: share talk session turn handling 2026-06-01 01:32:06 +02:00
Vincent Koc
705bdcec70 fix(gateway): harden MCP loopback tool schemas 2026-05-31 19:30:02 -04:00
github-actions[bot]
db7aff8843 chore(ui): refresh fa control ui locale 2026-05-31 23:23:45 +00:00
github-actions[bot]
d30329fb0e chore(ui): refresh nl control ui locale 2026-05-31 23:23:37 +00:00
github-actions[bot]
c7f3d60722 chore(ui): refresh vi control ui locale 2026-05-31 23:23:13 +00:00
github-actions[bot]
0ffaeb1273 chore(ui): refresh th control ui locale 2026-05-31 23:23:06 +00:00
github-actions[bot]
c43a571170 chore(ui): refresh pl control ui locale 2026-05-31 23:23:02 +00:00
github-actions[bot]
dd8b9bdcb8 chore(ui): refresh id control ui locale 2026-05-31 23:22:48 +00:00
github-actions[bot]
399f55e511 chore(ui): refresh uk control ui locale 2026-05-31 23:22:29 +00:00
github-actions[bot]
7e654b40b8 chore(ui): refresh tr control ui locale 2026-05-31 23:22:18 +00:00
github-actions[bot]
7b119ec60d chore(ui): refresh it control ui locale 2026-05-31 23:22:15 +00:00
github-actions[bot]
c1fffe1074 chore(ui): refresh ar control ui locale 2026-05-31 23:22:05 +00:00
github-actions[bot]
530f3aaab7 chore(ui): refresh fr control ui locale 2026-05-31 23:21:43 +00:00
github-actions[bot]
3ec1a25de4 chore(ui): refresh ja-JP control ui locale 2026-05-31 23:21:30 +00:00
github-actions[bot]
5a6ec67eb0 chore(ui): refresh es control ui locale 2026-05-31 23:21:27 +00:00
github-actions[bot]
0fdca6974d chore(ui): refresh ko control ui locale 2026-05-31 23:21:24 +00:00
Jerry-Xin
dc344a33fb fix(cron): retire MCP runtimes on isolated cron cleanup
Retire isolated cron session MCP runtimes on timeout and dispose so orphaned MCP servers do not accumulate after cron cleanup. Bound MCP session disposal to 5 seconds and force-close hung transports, including streamable-HTTP DELETE hangs, to prefer gateway availability over unbounded teardown.

Fixes #87821.
PR: #87981.
Proof: latest Real behavior proof check passed after body fix; local autoreview clean with focused cron/gateway/MCP tests covering 108 tests.

Co-authored-by: 忻役 <xinyi@mininglamp.com>
Co-authored-by: Jerry-Xin <jerryxin0@gmail.com>
2026-06-01 00:21:14 +01:00
github-actions[bot]
e4a766f2f4 chore(ui): refresh zh-TW control ui locale 2026-05-31 23:20:54 +00:00
github-actions[bot]
ad07ba141d chore(ui): refresh pt-BR control ui locale 2026-05-31 23:20:46 +00:00
github-actions[bot]
bd78737f94 chore(ui): refresh de control ui locale 2026-05-31 23:20:41 +00:00
github-actions[bot]
5f6e608c60 chore(ui): refresh zh-CN control ui locale 2026-05-31 23:20:37 +00:00
Vincent Koc
ddbd16a04a fix(ui): honor chromium executable override 2026-06-01 00:20:10 +01:00
Vincent Koc
03151a2ebe test(release): repair stale e2e mocks 2026-06-01 00:20:10 +01:00
Vincent Koc
1b69e7a005 fix(plugin-sdk): keep llm core alias on source graph 2026-06-01 00:20:10 +01:00
Vincent Koc
227530f906 test(imessage): align service-qualified target expectations 2026-06-01 00:20:10 +01:00
Vincent Koc
6df3fd5730 fix(gateway): list commands from gateway plugin registry 2026-06-01 00:20:10 +01:00
Vincent Koc
7c315252d6 test(whatsapp): wait on inbox delivery in monitor helper 2026-06-01 00:20:10 +01:00
Vincent Koc
0d7abcc94f test(telegram): exercise blocked spooled timeout lane 2026-06-01 00:20:09 +01:00
Vincent Koc
344773ba09 fix(openrouter): cap music stream request timeouts 2026-06-01 00:20:09 +01:00
Vincent Koc
ae4550f48b test(qa-lab): preserve cleanup phase labels 2026-06-01 00:20:09 +01:00
Vincent Koc
fdd02444b7 ci: add ARM Testbox lane 2026-06-01 00:20:09 +01:00
Peter Steinberger
3491834d49 Migrate iMessage monitor state to SQLite (#88797)
* refactor: move imessage monitor state to sqlite

* test: use OpenClaw temp root in iMessage state helper

* test: avoid pending promise lint in chat tests

* test: harden gateway ci flakes

* test: align session list merge expectation
2026-06-01 00:19:51 +01:00
Vincent Koc
12cf34a8ea refactor: share send inflight helpers 2026-06-01 01:18:38 +02:00
Peter Steinberger
d328a0d7a0 feat: calm chat composer controls 2026-06-01 00:18:04 +01:00
colmbrogan
421ad93203 fix(imessage): tolerate self-chat timestamp skew
Fixes iMessage self-chat reflection dedupe when reflected rows arrive with sub-second `created_at` skew, while keeping ambiguous normal-DM suppression exact-match only.

Maintainer follow-ups scoped skew tolerance to confirmed self-chat remembered rows and bounded cache cleanup so TTL-only expiry cannot leave the insertion-order queue growing indefinitely.

Verification:
- `node scripts/run-vitest.mjs extensions/imessage/src/monitor/self-chat-cache.test.ts extensions/imessage/src/monitor/self-chat-dedupe.test.ts extensions/imessage/src/monitor/inbound-processing.test.ts`
- `pnpm oxlint extensions/imessage/src/monitor/self-chat-cache.ts extensions/imessage/src/monitor/self-chat-cache.test.ts extensions/imessage/src/monitor/self-chat-dedupe.test.ts`
- `git diff --check origin/main...HEAD`
- autoreview clean on branch tip
- CI run 26727192244 green; Real behavior proof run 26727196218 green

Co-authored-by: Colm O Brogain <73212305+colmbrogan@users.noreply.github.com>
2026-06-01 00:14:47 +01:00
Vincent Koc
dc05f598bb fix(doctor): report runtime tool schema errors 2026-06-01 00:14:36 +01:00
Alix-007
3171278372 fix(gateway): hide phantom agent store rows from sessions.list
Fixes #57376.

Hide placeholder agent store keys from sessions.list while preserving real agent-scoped sessions.

Co-authored-by: Alix-007 <li.long15@xydigit.com>
2026-06-01 00:14:09 +01:00
Feelw00
01193dea26 fix: make task persistence failures explicit
Preserve task and TaskFlow durability by persisting before in-memory registry mutation and surfacing explicit persistence failures instead of reporting fake success.

Adds non-throwing try-create runtime helpers while keeping existing throwing public create APIs compatible. Maintainer follow-up keeps task/TaskFlow sync repair bounded, prevents split task/delivery-state writes, and keeps CI green on the current base.

Thanks @Feelw00.
2026-06-01 00:12:28 +01:00
Coder
cb9847968a fix(subagents): roll token usage formatters over to m
Roll both subagent token usage formatters over to the million unit when rounded thousands reach the next unit.

The original fix covers `formatTokenShort`, which feeds the subagent list usage line. The maintainer follow-up applies the same unit-boundary rule to compact subagent announcement stats, preserving that formatter's one-decimal style while preventing `1000.0k` output.

Verification:
- focused runtime probe for list and compact announce stats at 999,999 tokens
- `oxfmt --check` on touched formatter/test files
- `git diff --check origin/main..HEAD`
- `node scripts/run-tsgo.mjs -p test/tsconfig/tsconfig.core.test.json --incremental --tsBuildInfoFile .artifacts/tsgo-cache/core-test-pr88209.tsbuildinfo`
- autoreview local closeout clean
- exact-head CI passed for Real behavior proof, check-test-types, check-prod-types, check-guards, security-fast, and preflight

Known unrelated current-main reds at merge: `check-lint`, `checks-node-agentic-gateway-methods`, and `checks-node-agentic-control-plane-agent-chat`.

Co-authored-by: coder999999999 <coder999999999@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 00:07:45 +01:00
Vincent Koc
54987715f3 fix(ci): repair main lint and gateway session tests 2026-06-01 00:05:41 +01:00
Silvester
0c74f18a1c fix(microsoft-foundry): skip DeepSeek V4 thinking params on Foundry fallback
Skip the generic DeepSeek V4 OpenAI-compatible `thinking` payload wrapper for Microsoft Foundry fallback models. Foundry's OpenAI-compatible gateway rejects the non-standard top-level `thinking` argument, while the rest of the DeepSeek proxy path still keeps the wrapper.

Proof:
- .agents/skills/autoreview/scripts/autoreview --mode branch --base origin/main --parallel-tests "node scripts/run-vitest.mjs src/agents/embedded-agent-runner-extraparams.test.ts"
- node scripts/run-vitest.mjs src/agents/embedded-agent-runner-extraparams.test.ts passed, 130/130
- CI run 26681069909 passed for c950ac112e

Thanks @silvesterxm.
2026-06-01 00:03:32 +01:00
Vincent Koc
59122812c0 refactor: share agent id resolver 2026-06-01 01:03:07 +02:00
Alix-007
bc95af1b7c fix(memory-core): stop dream diary fallback leaks
Stop memory-core dream diary fallback paths from persisting raw memory staging snippets or promotions into DREAMS.md when narrative generation times out, returns empty output, or fails in request-scoped runtime. Successful generated narratives are unchanged.

Maintainer fixup: align current gateway session-list tests with the full loadSessionEntry mock shape and model-derived context token behavior on main.

Fixes #88391

Co-authored-by: Alix-007 <li.long15@xydigit.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 00:00:22 +01:00
yozakura-ava
144405e562 fix(agents): cap bootstrap snapshot cache
Cap the in-memory workspace bootstrap snapshot cache to 64 session keys so long-lived gateway processes do not retain one loaded bundle per distinct session key indefinitely. Older entries are evicted while active keys continue refreshing against the guarded workspace loader.

Verification:
- node scripts/run-vitest.mjs src/agents/bootstrap-cache.test.ts

PR: #88149
2026-05-31 23:56:47 +01:00
Vincent Koc
290b19275b refactor: share cron request helpers 2026-06-01 00:53:00 +02:00
Rain
72f74b33e1 fix(agents): guard transport payload sanitizer against non-string input
sanitizeTransportPayloadText() called text.replace() directly, so runtime-undefined content from malformed replay/error handling could crash embedded agent transport serialization with "Cannot read properties of undefined (reading 'replace')".

Return an empty string for non-string runtime payloads at the shared sanitizer boundary, preserving existing unpaired-surrogate cleanup for strings. Empty values still degrade through sanitizeNonEmptyTransportPayloadText() to "(no output)" where that non-empty fallback is required.

Proof:
- pnpm test src/agents/transport-stream-shared.test.ts
- pnpm exec oxfmt --check --threads=1 src/agents/transport-stream-shared.ts src/agents/transport-stream-shared.test.ts
- .agents/skills/autoreview/scripts/autoreview --mode branch --base origin/main --parallel-tests "pnpm test src/agents/transport-stream-shared.test.ts"

Fixes #60113

Co-authored-by: Pluviobyte <Pluviobyte@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-31 23:45:39 +01:00
Vincent Koc
bb673f47b2 refactor: share agent run snapshot shape 2026-06-01 00:39:13 +02:00
Vincent Koc
16ef9c1435 fix(ci): use reliable Azure Crabbox capacity 2026-06-01 00:38:11 +02:00
Peter Steinberger
2b30951b80 feat: calm composer controls (#88772) 2026-05-31 23:37:27 +01:00
Peter Steinberger
56b8030cd9 fix(qa-lab): avoid returning timer from promise executor 2026-05-31 23:34:13 +01:00
DocNR
5706619068 fix(nostr): decode npub allowFrom entries to hex correctly
Fix Nostr allowFrom npub normalization by returning the decoded hex string from nostr-tools instead of iterating the hex string as bytes.

Proof:
- node scripts/run-vitest.mjs extensions/nostr/src/nostr-bus.test.ts
- PR CI green at head 7c3433435b

Co-authored-by: DocNR <danieljwyler@gmail.com>
2026-05-31 23:33:45 +01:00
Vincent Koc
edc0a22179 fix(agents): quarantine tools before schema normalization 2026-05-31 23:33:03 +01:00
Peter Steinberger
2682c02774 perf: hydrate chat history session metadata
Use chat.history metadata to hydrate TUI and web startup state without the extra sessions.list refresh, with guards for aliases, stale active rows, blank-session defaults, and lightweight TUI usage metadata.
2026-05-31 23:31:15 +01:00
Vincent Koc
59683978e1 refactor: share voice-call config extraction 2026-06-01 00:19:33 +02:00
Peter Steinberger
c8f8907f15 fix(feishu): guard webhook readiness fetch 2026-05-31 23:18:09 +01:00
Vincent Koc
8eb1838dfa refactor: share web login unavailable response 2026-06-01 00:13:34 +02:00
Jason O'Neal
01f6ad6056 fix: suppress raw provider errors in channel delivery
Fixes #69737.

Suppresses raw and raw-derived provider error text at the user-facing assistant lifecycle and reply-payload boundaries, including structured provider payloads, escaped JSON payloads, and aborted turns carrying provider failures. Keeps safe schema rejection and rate-limit guidance while preserving internal diagnostics.

Proof:
- OPENCLAW_VITEST_MAX_WORKERS=1 node scripts/run-vitest.mjs run src/agents/embedded-agent-helpers.formatassistanterrortext.test.ts src/agents/embedded-agent-runner/run/payloads.errors.test.ts src/agents/embedded-agent-subscribe.handlers.lifecycle.test.ts src/agents/embedded-agent-helpers/errors.test.ts
- oxfmt --check on touched files
- git diff --check origin/main...HEAD
- autoreview --mode branch --base origin/main clean
- GitHub exact-head checks green on b46e197f62

Co-authored-by: Jason O'Neal <jason.allen.oneal@gmail.com>
2026-05-31 23:10:46 +01:00
Peter Steinberger
b7f657b3b0 chore(lint): fix app render promise executor 2026-05-31 23:10:00 +01:00
Peter Steinberger
22cb7fb6b7 chore(lint): enable no-promise-executor-return 2026-05-31 23:06:13 +01:00
Vincent Koc
48afba96a3 refactor: share agents handler helpers 2026-06-01 00:02:22 +02:00
github-actions[bot]
470a1ae8d1 chore(ui): refresh nl control ui locale 2026-05-31 21:59:56 +00:00
github-actions[bot]
a2acfc5049 chore(ui): refresh fa control ui locale 2026-05-31 21:59:48 +00:00
github-actions[bot]
fe8c781d67 chore(ui): refresh vi control ui locale 2026-05-31 21:59:18 +00:00
github-actions[bot]
ac2484f23e chore(ui): refresh pl control ui locale 2026-05-31 21:59:13 +00:00
github-actions[bot]
cabfbdfe0d chore(ui): refresh id control ui locale 2026-05-31 21:59:11 +00:00
github-actions[bot]
5e2472567a chore(ui): refresh th control ui locale 2026-05-31 21:59:07 +00:00
github-actions[bot]
79c4ac73d7 chore(ui): refresh tr control ui locale 2026-05-31 21:58:33 +00:00
github-actions[bot]
2a1882ebcc chore(ui): refresh uk control ui locale 2026-05-31 21:58:31 +00:00
github-actions[bot]
3bb04b67e9 chore(ui): refresh it control ui locale 2026-05-31 21:58:25 +00:00
github-actions[bot]
cd0a7b10e2 chore(ui): refresh ar control ui locale 2026-05-31 21:58:21 +00:00
github-actions[bot]
bc45c36dbc chore(ui): refresh fr control ui locale 2026-05-31 21:57:51 +00:00
github-actions[bot]
7184522fae chore(ui): refresh ko control ui locale 2026-05-31 21:57:45 +00:00
github-actions[bot]
aa74d93aff chore(ui): refresh es control ui locale 2026-05-31 21:57:40 +00:00
github-actions[bot]
be0d3489a6 chore(ui): refresh ja-JP control ui locale 2026-05-31 21:57:38 +00:00
github-actions[bot]
f06b4b9aab chore(ui): refresh pt-BR control ui locale 2026-05-31 21:57:10 +00:00
github-actions[bot]
0700f13d62 chore(ui): refresh zh-TW control ui locale 2026-05-31 21:57:05 +00:00
github-actions[bot]
3c6c247e0a chore(ui): refresh de control ui locale 2026-05-31 21:57:01 +00:00
github-actions[bot]
2e42b1372e chore(ui): refresh zh-CN control ui locale 2026-05-31 21:56:58 +00:00
Shakker
f78bb34cb4 fix: translate Skill Workshop locale strings 2026-05-31 22:55:03 +01:00
Shakker
85c7490f72 fix: refresh Skill Workshop i18n outputs 2026-05-31 22:55:03 +01:00
Shakker
63d93db867 fix: refresh Skill Workshop protocol models 2026-05-31 22:55:03 +01:00
Shakker
2976db4b2c fix: address Skill Workshop UI check failures 2026-05-31 22:55:03 +01:00
Shakker
025bb01268 fix: constrain Skill Workshop navigation 2026-05-31 22:55:03 +01:00
Shakker
7a292bb16e fix: improve Skill Workshop empty states 2026-05-31 22:55:03 +01:00
Shakker
a9e3eade5d fix: tighten Skill Workshop today actions 2026-05-31 22:55:03 +01:00
Shakker
3733cd8d63 fix: clarify Skill Workshop proposal preview 2026-05-31 22:55:03 +01:00
Shakker
190f935b53 fix: address Skill Workshop review findings 2026-05-31 22:55:03 +01:00
Shakker
c21e16c73d fix: add Skill Workshop empty state 2026-05-31 22:55:03 +01:00
Shakker
d52f1ea5ec fix: tighten Skill Workshop today actions 2026-05-31 22:55:03 +01:00
Shakker
13967e17e6 fix: distinguish created Skill Workshop proposals 2026-05-31 22:55:03 +01:00
Shakker
7ad2aa44dd fix: show assistant name in Skill Workshop 2026-05-31 22:55:03 +01:00
Shakker
874b3f921e fix: polish Skill Workshop revision handoff 2026-05-31 22:55:03 +01:00
Shakker
c11d5d6d65 feat: stage Skill Workshop revision handoff 2026-05-31 22:55:03 +01:00
Shakker
11631bf044 feat: animate Skill Workshop chat landing 2026-05-31 22:55:03 +01:00
Shakker
561e993282 fix: stabilize Skill Workshop revise handoff 2026-05-31 22:55:03 +01:00
Shakker
23bf48e69e feat: add reusable Control UI tooltip 2026-05-31 22:55:03 +01:00
Shakker
7d65ea3513 feat: style Skill Workshop revision controls 2026-05-31 22:55:03 +01:00
Shakker
bfac12a184 feat: route Skill Workshop revisions through reusable sessions 2026-05-31 22:55:03 +01:00
Shakker
cdcc151145 feat: attach agent session origin to workshop tool 2026-05-31 22:55:03 +01:00
Shakker
7681b95199 feat: persist Skill Workshop proposal origin 2026-05-31 22:55:03 +01:00
Shakker
caa08a6dc0 feat: show real Skill Workshop proposals 2026-05-31 22:55:03 +01:00
Shakker
4339d7c1d8 feat: add Skill Workshop revision dialog 2026-05-31 22:55:03 +01:00
Shakker
aa187c6496 feat: add Skill Workshop today view 2026-05-31 22:55:03 +01:00
Shakker
34010894c1 feat: preview Skill Workshop actions 2026-05-31 22:55:03 +01:00
Shakker
c74bb4475a feat: resize Skill Workshop proposal list 2026-05-31 22:55:03 +01:00
Shakker
299a023bd1 fix: track reviewed workshop proposals 2026-05-31 22:55:03 +01:00
Shakker
0c852036c7 fix: refine Skill Workshop action bar 2026-05-31 22:55:03 +01:00
Shakker
9cc759dd37 fix: hide Skill Workshop actions after pending 2026-05-31 22:55:03 +01:00
Shakker
d1378650bb fix: keep file preview row focus clean 2026-05-31 22:55:03 +01:00
Shakker
40f99e474a fix: keep file preview keyboard focus modal 2026-05-31 22:55:03 +01:00
Shakker
dc71b5867e fix: align live tool stream labels 2026-05-31 22:55:03 +01:00
Shakker
fd2c65f59b refactor: extract file preview modal component 2026-05-31 22:55:03 +01:00
Shakker
575f74293e feat: search Skill Workshop preview files 2026-05-31 22:55:03 +01:00
Shakker
b27ae3f6e7 fix: remove Skill Workshop modal search focus chrome 2026-05-31 22:55:03 +01:00
Shakker
b388d3dc71 style: add Skill Workshop file preview modal 2026-05-31 22:55:03 +01:00
Shakker
01b7ef9e88 feat: add Skill Workshop file preview modal 2026-05-31 22:55:03 +01:00
Shakker
4b89def277 fix: align Skill Workshop pane surface 2026-05-31 22:55:03 +01:00
Shakker
fabd9469cd fix: tighten Skill Workshop page spacing 2026-05-31 22:55:03 +01:00
Shakker
d3025b4007 fix: resolve Control UI public assets from base path 2026-05-31 22:55:03 +01:00
Shakker
c06096eabc fix: keep Control UI logo root-relative 2026-05-31 22:55:03 +01:00
Shakker
9577e0be5a feat: style Skill Workshop UI 2026-05-31 22:55:03 +01:00
Shakker
b12724b79b feat: add Skill Workshop demo view 2026-05-31 22:55:03 +01:00
Shakker
0de60cec12 feat: add Skill Workshop navigation tab 2026-05-31 22:55:03 +01:00
Vincent Koc
c6232347dc refactor: share exec approvals node invoke 2026-05-31 23:50:30 +02:00
xin zhuang
b73e135f97 fix: resolve google provider default API to google-generative-ai (#88480) (#88512)
When a configured Google provider/model row had no explicit
but had a baseUrl set, the fallback defaulted to openai-completions,
causing Gemini requests to route through the OpenAI Responses
transport instead of the native @google/genai transport.

Made resolveConfiguredProviderDefaultApi provider-aware: for the
google provider, the default API is now google-generative-ai.

Root cause: the generic fallback assumed any provider with a baseUrl
should use openai-completions, which is incorrect for Google's native
Gemini API.

Co-authored-by: xin <1052326311+xin@users.noreply.github.com>
2026-05-31 22:48:48 +01:00
github-actions[bot]
9b6c981260 chore(ui): refresh fa control ui locale 2026-05-31 21:46:04 +00:00
github-actions[bot]
02ac0ec48b chore(ui): refresh nl control ui locale 2026-05-31 21:46:00 +00:00
github-actions[bot]
d8329dedf6 chore(ui): refresh pl control ui locale 2026-05-31 21:45:39 +00:00
github-actions[bot]
b86e8bf359 chore(ui): refresh id control ui locale 2026-05-31 21:45:30 +00:00
github-actions[bot]
3bb9224836 chore(ui): refresh vi control ui locale 2026-05-31 21:45:24 +00:00
github-actions[bot]
fdc10a64e9 chore(ui): refresh th control ui locale 2026-05-31 21:45:20 +00:00
github-actions[bot]
87174c80b6 chore(ui): refresh uk control ui locale 2026-05-31 21:44:45 +00:00
github-actions[bot]
97c040f946 chore(ui): refresh it control ui locale 2026-05-31 21:44:40 +00:00
github-actions[bot]
f833e96a31 chore(ui): refresh tr control ui locale 2026-05-31 21:44:38 +00:00
github-actions[bot]
9a32c0f85d chore(ui): refresh ar control ui locale 2026-05-31 21:44:35 +00:00
github-actions[bot]
d306f5bf2e chore(ui): refresh fr control ui locale 2026-05-31 21:44:02 +00:00
github-actions[bot]
65d5f7436c chore(ui): refresh ko control ui locale 2026-05-31 21:43:55 +00:00
github-actions[bot]
b78ce079a3 chore(ui): refresh ja-JP control ui locale 2026-05-31 21:43:51 +00:00
github-actions[bot]
6c6cf41b14 chore(ui): refresh es control ui locale 2026-05-31 21:43:42 +00:00
github-actions[bot]
0d79cbab4e chore(ui): refresh pt-BR control ui locale 2026-05-31 21:43:18 +00:00
github-actions[bot]
b04c3e96d6 chore(ui): refresh zh-CN control ui locale 2026-05-31 21:43:11 +00:00
github-actions[bot]
3854a61bea chore(ui): refresh de control ui locale 2026-05-31 21:43:07 +00:00
github-actions[bot]
0d07e30725 chore(ui): refresh zh-TW control ui locale 2026-05-31 21:43:01 +00:00
Ted Li
bfc151e9d3 fix(feishu): preserve long streaming replies
Preserve long Feishu streaming replies by falling oversized finals back to chunked message/static-card delivery instead of closing through an over-limit streaming CardKit payload.

Keeps late-final suppression after a streaming card closes, and uses markdown-aware chunking for static card fallback replies.

Fixes #88631.

Co-authored-by: Ted Li <tl2493@columbia.edu>
2026-05-31 22:41:38 +01:00
Peter Steinberger
b653d94918 chore(lint): enable no-useless-assignment 2026-05-31 22:40:48 +01:00
Andy Ye
49e5091f18 fix(update): recognize manual-update launchd jobs (#88764)
* Recognize manual update launchd jobs

* fix(update): avoid stale launchd false positives

* fix(update): filter stale doctor launchd checks

* fix(update): narrow manual launchd updater labels

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-05-31 22:39:37 +01:00
zhang-guiping
cbdb59b255 fix(agents): keep light isolated subagents lightweight
Keep native subagent spawns with `lightContext=true` and resolved isolated context out of context-engine pre-spawn preparation so they remain lightweight.

The normal isolated and forked context-engine lifecycle stays intact, and docs now call out the lightweight isolated exception.

Fixes #81214
2026-05-31 22:37:59 +01:00
Vincent Koc
2ac2a8d210 refactor: share channel operation validation 2026-05-31 23:35:19 +02:00
Shubhankar Tripathy
d042452d20 fix(logging): refresh file log hostname per write
Fix JSONL file-log hostnames getting pinned to `unknown` when the first hostname read returns an empty value. The logger now retries empty hostname reads and caches the first non-empty value, keeping the top-level `hostname` and `_meta.hostname` fields aligned.

Fixes #87258.
Thanks @lonexreb for the fix.

Verification:
- `node scripts/run-vitest.mjs src/logging/logger-redaction-behavior.test.ts src/logger.test.ts`
- `node_modules/.bin/oxfmt --check --threads=1 src/logging/logger.ts src/logging/logger-redaction-behavior.test.ts`
- `.agents/skills/autoreview/scripts/autoreview --mode branch --base origin/main`
- `gh pr checks 88131 --watch=false`

Co-authored-by: lonexreb <reach2shubhankar@gmail.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-05-31 22:35:04 +01:00
Peter Steinberger
50f27ee91d docs: document code-mode MCP API files 2026-05-31 22:33:06 +01:00
charles-openclaw
84266cd30e fix(models): strip remaining provider self prefixes (#88781)
* fix(models): strip remaining provider self prefixes

* fix(models): keep catalog refs prefix-preserving

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-05-31 22:29:47 +01:00
David
61e9961abb fix(agents): expose session status route context
Expose session status route context so agents can distinguish session origin, active live route, and persisted delivery route.

Add maintainer fixup to keep active route metadata on the real live run key when policy and run keys differ.

Thanks @nxmxbbd.

Closes #84544
2026-05-31 22:25:47 +01:00
Ashd.LW.
7c04ce3a79 fix(daemon): preserve container service env across regen
Preserve the current container-related service opt-in environment when regenerating daemon service files, while continuing to drop stale or arbitrary `OPENCLAW_*` variables.

Verification:
- `git diff --check`
- `node scripts/run-vitest.mjs src/commands/daemon-install-helpers.test.ts -t "operator opt-in allowlist"`
- `.agents/skills/autoreview/scripts/autoreview --mode branch --base origin/main --prompt "Review PR #82828 fixup for daemon service env preservation. Focus on whether the allowlist should include only current container opt-in env keys and whether tests cover stale/arbitrary OPENCLAW_* filtering."`
- GitHub CI on `2e4b7f7fccbc46541c9c0ac271b1c97f1a6aa071`

Co-authored-by: wAngByg <281221101+wAngByg@users.noreply.github.com>
2026-05-31 22:22:24 +01:00
Vincent Koc
2ff9e27d4e refactor: share skill proposal workspace handling 2026-05-31 23:21:27 +02:00
Peter Steinberger
5ee3e5d8c0 docs: require real Crabbox visual proof 2026-05-31 22:18:31 +01:00
waterblue
03dec8bb3a fix(openai): avoid replay ids when Responses store is disabled
Avoid replaying prior OpenAI Responses reasoning/message/function-call item ids when the outgoing request disables store, while preserving encrypted reasoning and normalized summary arrays for stateless replay. Keep explicit store-enabled OpenAI wrapper paths opted into item-id replay, and cover shared/simple Responses, ChatGPT/Codex Responses, and GitHub Copilot sanitizer behavior.

Regression tests cover store-disabled id omission, encrypted reasoning preservation, idless Copilot reasoning replay, and direct builder payloads. Local proof included focused Vitest, broad lint, broad test-types, bundled-extension lint, plugin boundary checks, autoreview clean, and live OpenAI Responses gpt-5.5 proof.

Co-authored-by: hang <zhanghang02@gmail.com>
2026-05-31 22:17:32 +01:00
Arnab Saha
5bc80dbe27 fix(diagnostics): carry session UUID on interactive dispatch events
Carry the canonical session UUID from the session store into interactive dispatch diagnostic lifecycle events, matching the cron path so downstream diagnostic consumers can join events back to the JSONL transcript id.

Guard native command redirects by only attaching the UUID when the lifecycle session key matches the session-store lookup key, avoiding a target UUID under a source conversation key.

Verification:
- `pnpm test src/auto-reply/reply/dispatch-from-config.test.ts -t "carries the session store UUID|does not stamp a command target"`
- `.agents/skills/autoreview/scripts/autoreview --mode branch --base origin/main --prompt ...`
- synthetic merge-tree against current `origin/main`

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 22:15:15 +01:00
Peter Steinberger
8383e2e4d9 fix(doctor): keep post-upgrade JSON stable 2026-05-31 22:12:38 +01:00
Arnab Saha
7f93755206 fix(doctor): post-upgrade entry probe now delegates to install resolver
Address Codex review (P2 x3): replace the duplicate fs.access-based entry
checks in runPostUpgradeProbes with a call to
validatePackageExtensionEntriesForInstall so the doctor probe enforces the
same contract as plugin install/discovery:

- runtimeExtensions shape and length-mismatch validation
- plugin-root boundary enforcement (rejects absolute paths and ../ escapes)
- inferred dist/*.js peer for TypeScript entries; TS source-only entries
  without compiled output are now flagged

Adds 4 regression tests covering the boundary-escape, dist-peer accept,
TS-source-only reject, and runtimeExtensions length-mismatch cases.

Refs: https://github.com/openclaw/openclaw/pull/79260#issuecomment-4403594002
2026-05-31 22:12:38 +01:00
Arnab Saha
7dd1bd894b fix(doctor): drop unused listBuiltRuntimeEntryCandidates import and brace bare if continue 2026-05-31 22:12:38 +01:00
Arnab Saha
6ed6120977 docs(doctor): document --post-upgrade and --json flags 2026-05-31 22:12:38 +01:00
Arnab Saha
0f396368a9 fix(doctor): honor runtimeExtensions before flagging entry_unresolved 2026-05-31 22:12:38 +01:00
Arnab Saha
72679b16eb fix(doctor): resolve plugin index via state-dir helper 2026-05-31 22:12:38 +01:00
Arnab Saha
4a09fd43e2 docs(changelog): note doctor --post-upgrade --json 2026-05-31 22:12:38 +01:00
Arnab Saha
026ab6b882 feat(doctor): expose --post-upgrade and --json CLI flags 2026-05-31 22:12:38 +01:00
Arnab Saha
730492867f feat(doctor): branch into post-upgrade probe runner when --post-upgrade 2026-05-31 22:12:38 +01:00
Arnab Saha
ceda284845 feat(doctor): add plugin.manifest_drift post-upgrade probe 2026-05-31 22:12:38 +01:00
Arnab Saha
8da6b67607 fix(doctor): clean up post-upgrade probe test temp dirs and skip plugins with unreadable package.json 2026-05-31 22:12:38 +01:00
Arnab Saha
e0d3c78042 feat(doctor): add plugin.entry_unresolved post-upgrade probe 2026-05-31 22:12:38 +01:00
Arnab Saha
af7749123b feat(doctor): add post-upgrade finding types 2026-05-31 22:12:38 +01:00
alkor2000
9d97e683d4 feat(doctor): add disk space health check
Add a Doctor health contribution that checks free space on the partition containing the active OpenClaw state directory. Doctor now warns below 500 MB and reports critical below 100 MB so disk pressure is visible before config writes, session transcripts, or log rotation start failing.

The contribution reuses the shared `src/infra/disk-space.ts` probe, runs before state integrity, and is registered in the Doctor health conversion plan with focused coverage for thresholds, formatting, and note behavior.

PR: #59196
Proof: `pnpm test src/commands/doctor-disk-space.test.ts src/flows/doctor-health-conversion-plan.test.ts`; `git diff --check origin/main...HEAD`; `git merge-tree --write-tree origin/main refs/remotes/pr/59196`; GitHub CI run `26720861380`; Real behavior proof run `26720996848`.

Co-authored-by: alkor2000 <200923177@qq.com>
2026-05-31 22:09:36 +01:00
Vincent Koc
e2c745fc58 refactor: share agent wait terminal snapshot 2026-05-31 23:08:28 +02:00
Andy Ye
5df0ed3b9f fix(agents): publish owned announcement session writes
Forward prompt-submission owned session write publication into the embedded session lock controller so same-process announcement/completion writes can advance the requester fence while external edits still trigger takeover protection.

Adds regression coverage for a second controller publishing an owned announcement write and for preserving rejection of a later unowned edit.

Closes #88703.

Thanks @TurboTheTurtle.
2026-05-31 22:00:37 +01:00
Ted Li
e5acae4453 fix(ui): show Workboard comments in edit modal
Show existing Workboard card comments in the edit modal and allow operators to append a new comment through the existing `workboard.cards.comment` gateway method.

Refs #88592.

Verification:
- node scripts/run-vitest.mjs ui/src/ui/views/workboard.test.ts
- pnpm tsgo:test:ui
- git diff --check origin/main...HEAD
- .agents/skills/autoreview/scripts/autoreview --mode branch --base origin/main

Co-authored-by: Ted Li <tl2493@columbia.edu>
2026-05-31 21:54:26 +01:00
Peter Steinberger
8076eead77 test(channels): narrow pending ingress duplicate 2026-05-31 21:53:42 +01:00
Peter Steinberger
f6365d07c4 fix(agents): wait for cron media completions
Keep cron media generation detached while making cron runs wait for image/music/video completion before final closeout. Records async task IDs, falls back to the task registry for active run-scoped media work, handles timeout races, and scopes no-target generated-media delivery. Fixes #88001.
2026-05-31 21:51:38 +01:00
Sebastien Tardif
9a3e7d4f51 fix(hooks): pass media metadata to internal message_received hook
Forward canonical inbound media metadata to internal message:received hook consumers, matching the plugin received hook mapper and inbound-claim metadata path.

This fixes internal hook handlers losing mediaPath, mediaUrl, mediaType, mediaPaths, mediaUrls, and mediaTypes for received messages with attachments.

Verification:
- node scripts/run-vitest.mjs src/hooks/message-hook-mappers.test.ts
- .agents/skills/autoreview/scripts/autoreview --mode branch --base origin/main

Refs: https://github.com/openclaw/openclaw/pull/88740
Thanks @SebTardif.
Co-authored-by: Sebastien Tardif <sebtardif@ncf.ca>
2026-05-31 21:49:36 +01:00
Peter Steinberger
ce1165afda fix: repair providerless Codex session overrides
Co-authored-by: Earl Vanze <earlvanze@gmail.com>
2026-05-31 21:45:39 +01:00
brokemac79
90712f6d5e [codex] Surface disabled Codex plugin routes in doctor lint (#88761)
Merged via squash.

Prepared head SHA: 41bcde2d7d
Co-authored-by: brokemac79 <255583030+brokemac79@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman
2026-05-31 13:34:53 -07:00
ArthurNie
7c15c2765e fix(feishu): fallback when accepted turns send no visible reply (#87896)
* fix(feishu): fallback when accepted turns send no visible reply

* fix(feishu): cover no-visible-reply fallback gaps

* fix(feishu): mark media replies visible

* fix(feishu): honor suppressed delivery fallback

* test(auto-reply): trim fallback test churn

* fix(feishu): gate empty fallback eligibility

* test(auto-reply): expect fallback metadata after denied dispatch

* fix(feishu): fallback after failed visible final sends

* test(feishu): keep reply dispatcher mock shape aligned

* fix(auto-reply): respect silent policy for no-visible fallback

* fix(feishu): wait for streaming close before fallback

* fix(feishu): clear silent skip before later finals

* fix(feishu): preserve visible state across keepalives

* test(feishu): align lifecycle dispatcher mocks

* fix(feishu): require accepted streaming content for fallback

---------

Co-authored-by: ArthurNie <264332276+ArthurNie@users.noreply.github.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-05-31 21:33:13 +01:00
Peter Steinberger
e681569536 feat: add code-mode MCP API files
* feat: add code-mode MCP API files

* fix: satisfy code-mode MCP lint
2026-05-31 21:29:06 +01:00
Peter Steinberger
b0679d1f13 refactor(channels): store inbound queues in SQLite 2026-05-31 21:15:29 +01:00
Peter Steinberger
80b7f56603 ci: pin Azure crabbox lane to eastus2 2026-05-31 21:11:43 +01:00
Peter Steinberger
995a9bd702 chore(ui): refresh notification i18n metadata 2026-05-31 21:09:37 +01:00
Peter Steinberger
92b9cd21ec test: avoid positional CI check assertion 2026-05-31 16:00:04 -04:00
Peter Steinberger
d62bfab946 ci: split startup and shrinkwrap checks 2026-05-31 15:55:43 -04:00
Peter Steinberger
7aa309319f test(auto-reply): align debounce timer tests 2026-05-31 20:48:02 +01:00
Peter Steinberger
2df95c0b10 chore(lint): enable no-misused-promises 2026-05-31 20:42:13 +01:00
Peter Steinberger
6f58a71582 test(voice-call): install state runtime for events 2026-05-31 20:41:14 +01:00
stain lu
55fc3c10b0 fix(openai/tts): handle speed directives (#74089)
Adds OpenAI speech speed directive parsing with official OpenAI range validation and custom endpoint passthrough. Closes #12163.
2026-05-31 20:35:46 +01:00
Peter Steinberger
b4a6244ef4 ci: split agents core test shard 2026-05-31 15:35:36 -04:00
Peter Steinberger
6b2cb4db67 fix: polish notifications settings UI 2026-05-31 20:35:10 +01:00
Vincent Koc
0715081990 test(agents): narrow bundle mcp e2e setup 2026-05-31 21:31:52 +02:00
WT-WSL
462b52f62c fix(ci): guard workflow template injection
Guard the remaining Windows Testbox workflow ref logging against GitHub Actions template injection by moving `target_ref` through step env before PowerShell reads it.

Extend the local workflow check wrapper to run pinned `zizmor` across every workflow file, and keep Workflow Sanity's CI audit explicit with trusted-base pre-commit and zizmor configs for pull-request runs.

Thanks @WT-WSL for the original report and patch.

Co-authored-by: dev111-actor <captaintobb@outlook.com>
2026-05-31 20:28:40 +01:00
Peter Steinberger
118b9cacf6 refactor: split ACP manager session flows
Split ACP manager session-flow ownership into focused helpers for initialization, status reads, cancellation, and startup identity reconciliation.

Verification:
- `node scripts/run-oxlint.mjs src/acp/control-plane/manager.core.ts src/acp/control-plane/manager.initialize-session.ts src/acp/control-plane/manager.status.ts src/acp/control-plane/manager.cancel-session.ts src/acp/control-plane/manager.startup-identity-reconcile.ts src/acp/control-plane/manager.close-session.ts src/acp/control-plane/manager.turn-runner.ts src/acp/control-plane/manager.runtime-options-commands.ts src/acp/control-plane/manager.types.ts src/acp/control-plane/manager.test.ts src/acp/control-plane/manager.initialize-session.test.ts src/acp/control-plane/manager.cancel-session.test.ts src/acp/control-plane/manager.startup-identity-reconcile.test.ts src/acp/control-plane/manager.runtime-config.test.ts`
- `pnpm tsgo:prod`
- `pnpm test src/acp/control-plane/manager.test.ts src/acp/control-plane/manager.initialize-session.test.ts src/acp/control-plane/manager.cancel-session.test.ts src/acp/control-plane/manager.startup-identity-reconcile.test.ts src/acp/control-plane/manager.runtime-config.test.ts src/acp/control-plane/manager.runtime-handles.test.ts`
- `pnpm format:check src/acp/control-plane/manager.core.ts src/acp/control-plane/manager.initialize-session.ts src/acp/control-plane/manager.status.ts src/acp/control-plane/manager.cancel-session.ts src/acp/control-plane/manager.startup-identity-reconcile.ts src/acp/control-plane/manager.close-session.ts src/acp/control-plane/manager.turn-runner.ts src/acp/control-plane/manager.runtime-options-commands.ts src/acp/control-plane/manager.types.ts src/acp/control-plane/manager.test.ts src/acp/control-plane/manager.initialize-session.test.ts src/acp/control-plane/manager.cancel-session.test.ts src/acp/control-plane/manager.startup-identity-reconcile.test.ts src/acp/control-plane/manager.runtime-config.test.ts`
- `git diff --check`
- `pnpm check:test-types`
- `.agents/skills/autoreview/scripts/autoreview --mode branch --base origin/main`
- GitHub PR checks for #88752 passed

Real behavior proof:
Behavior addressed: ACP manager session-flow ownership is split out of `AcpSessionManager` without changing initialization, status, cancel, startup identity reconciliation, close, turn, or runtime-option behavior.
Real environment tested: Local OpenClaw checkout, Node/pnpm repo toolchain, GitHub Actions PR CI.
Exact steps or command run after this patch: Focused ACP manager/runtime config/runtime handle tests plus prod/test type checks, lint, format check, diff check, autoreview, and PR CI.
Evidence after fix: All listed local commands passed, autoreview reported no accepted/actionable findings, and GitHub PR checks passed.
Observed result after fix: `manager.core.ts` is down to 612 LOC, with init/status/cancel/startup identity flows in focused modules and matching focused tests.
What was not tested: Live ACP backend session initialization/cancel/status against a real external ACP provider.
2026-05-31 20:26:04 +01:00
Peter Steinberger
8cfccca4de docs(changelog): refresh 2026.5.31 notes 2026-05-31 20:24:49 +01:00
Peter Steinberger
01603bbbf4 docs: require WebVNC screenshot verification 2026-05-31 20:21:47 +01:00
Carmen Fernández Ruiz
2e1ae531bd fix: skip disabled skill snapshot env overrides (#79173)
Co-authored-by: hera8939 <279459669+hera8939@users.noreply.github.com>
2026-05-31 20:20:13 +01:00
Peter Steinberger
9c6f7553be test(gateway): widen tailscale hostname mock 2026-05-31 20:13:34 +01:00
Peter Steinberger
ccb50f89da fix(plugins): clarify loader failure guidance 2026-05-31 15:12:22 -04:00
Peter Steinberger
7c5a412b38 fix(whatsapp): satisfy baileys audio peer 2026-05-31 20:10:28 +01:00
Simon Peck
6653193fdb fix(openai): avoid orphan Responses message id replay
Omit provider-owned OpenAI Responses assistant message ids unless the paired reasoning item was replayed immediately before the message. Preserve commentary/final_answer phase metadata so replay quality stays intact without sending orphan msg_* ids that OpenAI rejects.

Verification:
- node scripts/run-vitest.mjs src/agents/openai-transport-stream.test.ts src/agents/openai-responses.reasoning-replay.test.ts src/llm/providers/openai-responses-shared.test.ts
- node scripts/run-oxlint.mjs on touched files
- git diff --check
- live OpenAI Responses API proof with gpt-5.5
- autoreview clean
- PR CI clean on d6902ed1a0

Co-authored-by: latensified <880715+latensified@users.noreply.github.com>
2026-05-31 20:08:33 +01:00
Feelw00
7a3a52cda9 fix(agents): atomic auth.json writes
Persist agent auth files via atomic sibling-temp replacement instead of truncating `auth.json` in place, preventing crash-time credential lockout. Preserve the existing auth directory mode during replacement and keep the credential file at `0600`.

Proof:
- `node scripts/run-vitest.mjs src/agents/sessions/auth-storage.test.ts` passed, 2 tests.
- `git diff --check` passed.
- `autoreview --mode local` clean.
- `autoreview --mode branch --base origin/main` clean.
- GitHub checks green on head `3fb1d767e70118a0e8db5b0fd64d807d456721a8`.

Closes #88028.

Co-authored-by: Feelw00 <dhrtn1006@naver.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 20:06:50 +01:00
Peter Steinberger
fa2b2ffab4 fix(channels): recover failed progress draft starts (#88749) 2026-05-31 20:06:28 +01:00
charles-openclaw
a6f4de4a66 feat(gateway): support Tailscale Serve service names
Adds optional `gateway.tailscale.serviceName` support for Tailscale Serve so the Gateway Control UI can be exposed through a named Tailscale Service while existing hostname-based Serve and Funnel behavior stays unchanged.

The implementation validates `svc:<dns-label>`, passes the Service name to `tailscale serve`, clears named Service config with `tailscale serve clear <service>` when resetOnExit runs, and uses the derived Service hostname in startup logs, status output, and pairing URLs.

Verification:
- node scripts/run-vitest.mjs src/infra/tailscale.test.ts src/gateway/server-tailscale.test.ts src/config/config.gateway-tailscale-bind.test.ts src/gateway/startup-auth.test.ts src/commands/status.scan.shared.test.ts src/pairing/setup-code.test.ts
- .agents/skills/autoreview/scripts/autoreview --mode branch --base origin/main --parallel-tests "node scripts/run-vitest.mjs src/infra/tailscale.test.ts src/gateway/server-tailscale.test.ts src/config/config.gateway-tailscale-bind.test.ts src/gateway/startup-auth.test.ts src/commands/status.scan.shared.test.ts src/pairing/setup-code.test.ts"
- git diff --check
- git merge-tree --write-tree origin/main origin/pr/88691

Closes #88629.
Co-authored-by: Charles OpenClaw <charles-openclaw@9bcfae.inboxapi.ai>
2026-05-31 20:05:02 +01:00
Peter Steinberger
b02c448585 docs(plugins): add npm readmes for channel providers 2026-05-31 20:02:45 +01:00
Vladyslav Levchuk
a93240e2c6 fix(ui): show communication notifications tab (#74715)
Expose the existing virtual Communication > Notifications settings tab for Web Push controls, while keeping it out of the unscoped root settings view. Adds browser regression coverage for the scoped virtual tab.

Thanks @VladyslavLevchuk.

Co-authored-by: Vladyslav Levchuk <32742736+VladyslavLevchuk@users.noreply.github.com>
2026-05-31 19:58:55 +01:00
Peter Steinberger
720071a6c6 refactor: extract ACP runtime option commands
Extract ACP runtime-option command flows from `AcpSessionManager` into `manager.runtime-options-commands.ts`.

Verification:
- `pnpm format:fix src/acp/control-plane/manager.core.ts src/acp/control-plane/manager.runtime-options-commands.ts`
- `node scripts/run-oxlint.mjs src/acp/control-plane/manager.core.ts src/acp/control-plane/manager.runtime-options-commands.ts`
- `pnpm tsgo:prod`
- `pnpm test src/acp/control-plane/manager.runtime-config.test.ts src/acp/control-plane/manager.runtime-handles.test.ts src/acp/control-plane/manager.test.ts`
- `pnpm format:check src/acp/control-plane/manager.core.ts src/acp/control-plane/manager.runtime-options-commands.ts`
- `git diff --check`
- `pnpm check:test-types`
- `.agents/skills/autoreview/scripts/autoreview --mode local`
- GitHub PR checks for #88747 passed

Real behavior proof:
Behavior addressed: ACP runtime-option mutation ownership moved out of `AcpSessionManager` without changing set-mode, set-config-option, raw update, reset, persistence, or runtime-cache invalidation semantics.
Real environment tested: Local OpenClaw checkout, Node/pnpm repo toolchain, GitHub Actions PR CI.
Exact steps or command run after this patch: Focused ACP runtime config/handle/manager tests plus prod/test type checks, lint, format, diff check, autoreview, and PR CI.
Evidence after fix: All listed local commands passed, autoreview reported no accepted/actionable findings, and GitHub PR checks passed.
Observed result after fix: `manager.core.ts` is down to 885 LOC, with runtime-option command logic isolated in `manager.runtime-options-commands.ts`.
What was not tested: Live ACP backend mode/config option mutation against a real external ACP provider.
2026-05-31 19:56:43 +01:00
Roee Jukin
2155450ed7 fix(acp): prefer clean command text for local bypass
Prefer the clean channel command body when ACP decides whether an inbound message should bypass the agent loop for local OpenClaw commands.

This keeps envelope-wrapped channel text, such as WhatsApp display bodies, from hiding commands like /status when the channel already provided a normalized command body. The ACP runtime prefilter now uses the same command-text resolution as dispatch, and dispatch still requires registry-backed local commands before bypassing.

Co-authored-by: RoeeJ <RoeeJ@users.noreply.github.com>
2026-05-31 19:56:04 +01:00
Jason
e74931778c fix: preserve workspaces during state-only uninstall
Preserve workspace directories when `openclaw uninstall --state` removes local state, including configured workspaces and implicit per-agent workspaces resolved by the runtime. State-only uninstall now uses a cleanup plan that keeps those workspace roots unless `--workspace` is selected.

Fixes #75052.

Proof:
- `git diff --check origin/main...HEAD`
- `pnpm exec oxfmt --check --threads=1 src/commands/cleanup-utils.ts src/commands/cleanup-utils.test.ts src/commands/uninstall.ts src/commands/uninstall.test.ts docs/cli/uninstall.md docs/install/uninstall.md`
- `node scripts/run-vitest.mjs src/commands/uninstall.test.ts src/commands/cleanup-utils.test.ts src/commands/reset.test.ts src/commands/agents.delete.test.ts`
- `node scripts/run-tsgo.mjs -p test/tsconfig/tsconfig.core.test.json --incremental --tsBuildInfoFile /tmp/openclaw-pr75061-core-test-final-rebase2.tsbuildinfo`
- `pnpm docs:list`
- `node scripts/check-docs-mdx.mjs docs/cli/uninstall.md docs/install/uninstall.md`
- `.agents/skills/autoreview/scripts/autoreview --mode branch --base origin/main`
- CI: https://github.com/openclaw/openclaw/actions/runs/26721260691

Co-authored-by: Jason-Bai <boybai.work@gmail.com>
2026-05-31 19:54:34 +01:00
ArthurNie
9d54285b0d fix: force preflight compaction before oversized agent turns
Force required preflight context compaction before oversized turns can enter the agent runtime. Treat required preflight compaction as a hard gate: compact, skip only explicit harmless no-op reasons, or surface a visible recovery message when compaction cannot recover.

Fixes #87234.

Co-authored-by: ArthurNie <264332276+ArthurNie@users.noreply.github.com>
2026-05-31 19:48:49 +01:00
Peter Steinberger
3ff86f3350 refactor: migrate voice-call call logs through doctor (#88731) 2026-05-31 19:43:03 +01:00
Peter Steinberger
2f449285b9 refactor: extract ACP close session flow
Refactor ACP close-session ownership by extracting the runtime close/recovery lifecycle into `manager.close-session.ts`.

Verification:
- `pnpm test src/acp/control-plane/manager.test.ts src/acp/control-plane/manager.runtime-config.test.ts src/acp/control-plane/manager.runtime-handles.test.ts`
- `pnpm tsgo:prod`
- `pnpm check:test-types`
- `node scripts/run-oxlint.mjs src/acp/control-plane/manager.core.ts src/acp/control-plane/manager.close-session.ts`
- `pnpm format:check src/acp/control-plane/manager.core.ts src/acp/control-plane/manager.close-session.ts`
- `git diff --check`
- `.agents/skills/autoreview/scripts/autoreview --mode local`
- GitHub PR checks for #88744 passed

Real behavior proof:
Behavior addressed: ACP close-session ownership moved out of `AcpSessionManager` without changing close/recovery behavior.
Real environment tested: Local OpenClaw checkout, Node/pnpm repo toolchain, GitHub Actions PR CI.
Exact steps or command run after this patch: Focused ACP manager tests covering close-session behavior, runtime config, and runtime handles, plus prod/test type checks, lint, format, diff check, autoreview, and PR CI.
Evidence after fix: All listed local commands passed, autoreview reported no accepted/actionable findings, and GitHub PR checks passed.
Observed result after fix: `manager.core.ts` dropped from 1149 LOC to 1038 LOC while close-session runtime lifecycle handling lives in `manager.close-session.ts`.
What was not tested: Live ACP backend close/recovery against a real external ACP provider.
2026-05-31 19:42:46 +01:00
Peter Steinberger
465a5456fe fix(agents): preserve disabled subagent delivery state 2026-05-31 19:42:00 +01:00
Federico Kamelhar
ecbd97e968 fix(gateway): rate-limit bootstrap-token verification
Gateway/security: rate-limits pre-auth bootstrap-token verification and serializes per-IP attempts to prevent mutex-stall DoS while preserving device-token fallback.

Fixes #77978.

Co-authored-by: Federico Kamelhar <federico.kamelhar@oracle.com>
2026-05-31 19:40:22 +01:00
Peter Steinberger
ef04c72f08 docs: require live external API tests 2026-05-31 19:39:41 +01:00
Federico Kamelhar
e76df691fe fix(skills): bound watcher workspace state
Bounds skills watcher subscriptions and workspace snapshot-version state to active workspaces on the current `src/skills/runtime` implementation.

The fix keeps shared path watchers as the owner boundary, evicts idle workspace subscriptions after 1 hour without closing watchers still used by other workspaces, and clears per-workspace version keys only after preserving/advancing invalidation so cached skill snapshots cannot miss changes across teardown or re-enable.

Thanks @fede-kamel.

Fixes #77997.

Co-authored-by: Federico Kamelhar <federico.kamelhar@oracle.com>
2026-05-31 19:35:42 +01:00
Vincent Koc
f983111166 perf(scripts): parallelize test group reports 2026-05-31 20:32:54 +02:00
Vincent Koc
7e0d275f7a fix(agents): preserve skipped subagent delivery state 2026-05-31 19:30:32 +01:00
Peter Steinberger
faae7529fd refactor: extract ACP turn runner
Refactor ACP turn execution ownership by extracting the backend attempt and cleanup loop into `manager.turn-runner.ts`.

Verification:
- `pnpm test src/acp/control-plane/manager.test.ts src/acp/control-plane/manager.turn-results.test.ts src/acp/control-plane/manager.failover.test.ts src/acp/control-plane/manager.runtime-handles.test.ts src/acp/control-plane/manager.runtime-config.test.ts`
- `pnpm tsgo:prod`
- `pnpm check:test-types`
- `node scripts/run-oxlint.mjs src/acp/control-plane/manager.core.ts src/acp/control-plane/manager.turn-runner.ts`
- `pnpm format:check src/acp/control-plane/manager.core.ts src/acp/control-plane/manager.turn-runner.ts`
- `git diff --check`
- `.agents/skills/autoreview/scripts/autoreview --mode local`
- GitHub PR checks for #88739 passed

Real behavior proof:
Behavior addressed: ACP turn execution ownership moved out of `AcpSessionManager` without changing runtime behavior.
Real environment tested: Local OpenClaw checkout, Node/pnpm repo toolchain, GitHub Actions PR CI.
Exact steps or command run after this patch: Focused ACP manager tests covering turn results, failover, runtime handles, runtime config, plus prod/test type checks, lint, format, diff check, autoreview, and PR CI.
Evidence after fix: All listed local commands passed, autoreview reported no accepted/actionable findings, and GitHub PR checks passed.
Observed result after fix: `manager.core.ts` dropped from 1495 LOC to 1149 LOC while turn execution lives in `manager.turn-runner.ts`.
What was not tested: Live ACP backend process recovery against a real external ACP provider.
2026-05-31 19:29:47 +01:00
Jeff
01ef169004 fix(agents): sanitize raw HTTP 401 provider errors
Sanitize credential-shaped provider HTTP 401 failures in embedded-agent replies so chat users see a re-authentication hint instead of raw provider text such as `HTTP 401: "Invalid token"`.

The classifier now requires auth classification plus positive 401 evidence, and it stays narrow to credential-shaped failures so billing, scope, replay-invalid, schema, message-only auth, and plain 403 paths keep their existing behavior.

Fixes #56197. Thanks @lokamir.

Co-authored-by: jeffrey701 <jeffreyconradtucker@gmail.com>
2026-05-31 19:26:42 +01:00
zhang-guiping
2fbddce881 fix(cli): avoid catalog validation in agents add (#88314)
Fixes #76284.

Thanks @zhangguiping-xydt.

Co-authored-by: 张贵萍0668001030 <zhang.guiping@xydigit.com>
2026-05-31 19:22:16 +01:00
Ben Newell
a88e4fb7e0 fix(memory-core): preserve phase signals on read errors
Phase-signal store reads now recover only missing files and corrupt JSON. Nonrecoverable filesystem read failures propagate so dreaming aborts before overwriting existing phase-signal history with an empty replacement.

Fixes #77881.
Thanks @bennewell35.

Co-authored-by: bennewell35 <newelljben@gmail.com>
2026-05-31 19:18:56 +01:00
Peter Steinberger
90329e2848 refactor: extract ACP runtime resume state
Extract ACP runtime resume/discard recovery helpers from `AcpSessionManager` into `manager.runtime-resume-state.ts`, and share the manager session-meta writer callback type from `manager.types.ts`. Keeps close-time fresh-session recovery, early-turn retry, persisted resume identifier clearing, and discard-persistent-state behavior intact while reducing `manager.core.ts` from 1655 LOC to 1495 LOC.

Proof: focused ACP manager runtime-handle/runtime-config/turn-result tests, prod + test type checks, narrow oxlint, format check, diff check, autoreview clean, PR CI green.
2026-05-31 19:18:18 +01:00
Vincent Koc
454a69a048 test(gateway): align startup refactor expectations 2026-05-31 19:10:25 +01:00
Federico Kamelhar
78f2a89e95 fix(discord): bound REST entity cache growth
Bound DiscordEntityCache entries with a write-time expired-entry sweep and a default 5,000-entry cap while preserving current safe expiry timestamp normalization. This prevents high-cardinality Discord user/channel/guild/member fetches from retaining stale Map entries for the gateway lifetime.

Fixes #77975.
Thanks @fede-kamel.

Co-authored-by: Federico Kamelhar <federico.kamelhar@oracle.com>
2026-05-31 19:08:27 +01:00
Peter Steinberger
3613981579 test(gateway): refresh startup assertions 2026-05-31 19:07:31 +01:00
Sebuh Honarchian
a129b912a4 fix(gateway): guard direct session display names
Guard group display-name generation behind group/channel classification so direct Telegram sessions fall back to their explicit or origin labels. Keep session-list search aligned with that visible fallback.

Fixes #55354.
Thanks @sebuh-infsol.
2026-05-31 19:03:42 +01:00
Peter Steinberger
2a30b937cb refactor: extract ACP runtime handle ensure flow
Extract ACP runtime-handle ensure/reuse/recreate flow into `manager.runtime-handle-ensure.ts`. Keeps `AcpSessionManager` focused on orchestration while preserving backend resolution, resume identity retry, metadata persistence, cache replacement, and concurrency-limit behavior.

Proof: focused ACP manager runtime-handle/runtime-config/turn-result tests, narrow oxlint, prod + test type checks, autoreview clean, PR CI green.
2026-05-31 19:01:59 +01:00
Mert Başar
0ff5fe3a80 fix(auth): add force re-login recovery and fallback auth skips
Summary:
- Add forced provider re-login support that clears cached auth profiles before running provider login again.
- Add provider-auth remediation guidance and a session-scoped skip cache for known-bad fallback auth attempts.
- Wire session ids through agent command, auto-reply, and embedded compaction fallback callers so the skip cache applies on real run paths.
- Fail closed when forced auth profile removal cannot update the profile store.

Verification:
- Local format, lint, diff-check, focused Vitest shards, and autoreview passed.
- PR CI, CodeQL Security High, and Critical Quality agent-runtime-boundary passed on head 1b4e9e753e.

Co-authored-by: Mert Basar <MertBasar0@users.noreply.github.com>
2026-05-31 19:01:51 +01:00
Vincent Koc
db0209ac5d perf(scripts): parallelize remote core oxlint shards 2026-05-31 20:01:41 +02:00
Peter Steinberger
3bac0bcbfb fix(codex): stream final answer partials (#88730) 2026-05-31 19:00:44 +01:00
Youssef Hemimy
beb499b4d1 fix(approvals): interpolate request id in fallback command
Fix approval fallback text so exec and plugin approval messages render a concrete request id in the chat copy-paste command instead of the literal <id> placeholder.

This makes the Reply with: /approve ... line directly usable for owners while keeping the existing approval resolver contract unchanged.

Proof:
- git diff --check origin/main...HEAD
- pnpm test src/infra/exec-approval-forwarder.test.ts src/infra/plugin-approval-forwarder.test.ts src/plugin-sdk/approval-renderers.test.ts
- .agents/skills/autoreview/scripts/autoreview --mode branch --base origin/main
- CI run 26720052738 passed

Thanks @itsuzef.
2026-05-31 18:59:43 +01:00
Peter Steinberger
7617d062fd chore(lint): fix rebased lint violations 2026-05-31 18:59:02 +01:00
Peter Steinberger
304e2c83c0 chore(lint): enable stricter oxlint rules 2026-05-31 18:59:02 +01:00
Peter Steinberger
cb569f6ad9 docs: clarify superseded PR close policy 2026-05-31 18:57:32 +01:00
Chunyue Wang
b8f25e9648 fix(memory): serialize qmd writes across processes (#85931)
Serialize QMD update and embed writes with one per-agent store lock so foreground memory search/index and gateway background QMD work do not write the same index.sqlite concurrently.

The embed path now waits for global embed capacity before taking the per-store lock, so queued embeds do not block same-agent foreground updates while no store write is active.

Fixes #66339
Thanks @openperf.

Co-authored-by: Chunyue Wang <16864032@qq.com>
2026-05-31 18:57:15 +01:00
Peter Steinberger
6b0ad98d62 test(extensions): update pairing challenge assertions 2026-05-31 18:56:20 +01:00
Alex Ho
d88767e819 fix(docker): refresh Node base image digests (#84988)
Refresh pinned node:24-bookworm and node:24-bookworm-slim manifest-list digests across the root, smoke, and e2e Dockerfiles. Update digest pin assertions to cover the plugin-binding e2e Dockerfile.

Verified with live Docker digest inspection, targeted Dockerfile tests, root base-runtime build, install-sh smoke build, and plugin-binding e2e build.

Thanks @LibraHo.
2026-05-31 18:55:33 +01:00
Yuval Dinodia
b988e2f92b fix(daemon): detect system-scope systemd gateway units on Linux
Detect OpenClaw gateway units installed in the system systemd scope, including marker-owned custom unit names such as `openclaw.service`. Route status/restart/stop through the system manager when appropriate, and show non-root users the matching `sudo systemctl ...` command instead of falling back to unmanaged process signaling.

Fixes #87577.
Thanks @yetval.

Verification:
- `node scripts/run-vitest.mjs src/daemon/systemd.test.ts src/cli/daemon-cli/lifecycle.test.ts src/daemon/inspect.test.ts src/cli/daemon-cli/lifecycle-core.test.ts src/cli/daemon-cli/status.gather.test.ts src/cli/daemon-cli/response.test.ts src/commands/doctor-gateway-daemon-flow.test.ts src/cli/update-cli/restart-helper.test.ts src/infra/outbound/message-action-runner.core-send.test.ts`
- AWS Crabbox `cbx_69f97dff5e5c`, run `run_a68431b3dad6`: exact SHA checkout, focused tests, real `/etc/systemd/system/openclaw.service` status/restart/stop proof.
2026-05-31 18:52:02 +01:00
Peter Steinberger
e014145ac1 docs: mention markdown host-local media sends (#79658) 2026-05-31 18:51:45 +01:00
Clever
14dbf80c74 Fix explicit text alias extension check 2026-05-31 18:51:45 +01:00
Clever
a9eefeea71 Remove changelog entry from text media PR 2026-05-31 18:51:45 +01:00
Clever
9f7eaf06e1 docs: clarify host-local text media boundary 2026-05-31 18:51:45 +01:00
Clever
7d3fc6f924 docs: update host-local media text policy 2026-05-31 18:51:45 +01:00
Clever
b454677874 Restrict plain text media sends to txt 2026-05-31 18:51:45 +01:00
Clever
d729811224 Add changelog for text document media sends 2026-05-31 18:51:45 +01:00
Clever
1e14f4400f Allow validated text document media sends 2026-05-31 18:51:45 +01:00
Peter Steinberger
d641126c1d feat(plugin-sdk): add typed presentation command actions (#88721)
* feat(plugin-sdk): add typed presentation command actions

* test: use shared env helper in telegram bot tests

* test: expect typed approval actions

* test: expect typed sdk approval actions
2026-05-31 18:48:45 +01:00
Peter Steinberger
4b1d2faa99 docs: harden Codex dependency review gate 2026-05-31 18:48:15 +01:00
Peter Steinberger
058152cf69 refactor: extract ACP manager runtime handle cache
Extract ACP manager runtime-handle cache ownership into a dedicated helper. Keeps the session manager focused on lifecycle orchestration while preserving cached handle reuse, close/clear, idle eviction, matching, and observability behavior.

Proof: focused ACP manager runtime-handle/runtime-config tests, narrow oxlint, pnpm check:test-types, autoreview clean, PR CI green.
2026-05-31 18:46:07 +01:00
Jerry-Xin
56362524ed fix(agents): prefer real tool results over repair synthetics
Ref #84134.

Prefer real tool results over generated missing-result placeholders during transcript repair, including late results after later assistant turns and explicitly marked custom-text repair placeholders. Keep real error outputs such as aborted when they are not generated repair placeholders.

Thanks @Jerry-Xin.

Co-authored-by: 忻役 <xinyi@mininglamp.com>
Co-authored-by: Jerry-Xin <jerryxin0@gmail.com>
2026-05-31 18:44:37 +01:00
Peter Steinberger
05b3f1c29d docs: require deeper PR review evidence 2026-05-31 18:42:39 +01:00
Sunjae Kim
201bf125af fix(session-store): rewrite generated transcript paths on rollover
Rewrite generated session transcript paths at the shared session-store merge boundary when a persisted session rolls from one session id to another. This prevents patches that carry a stale generated `sessionFile` from leaving a new logical session id attached to the old transcript file, while preserving custom transcript paths.

Refs #65564.

Proof:
- `node scripts/run-vitest.mjs src/config/sessions/sessions.test.ts`
- `node scripts/run-vitest.mjs src/agents/command/session-store.test.ts`
- `git diff --check origin/main...HEAD`
- `.agents/skills/autoreview/scripts/autoreview --mode branch --base origin/main`
- CI run 26719583889 attempt 2

Co-authored-by: Sunjae Kim <sunjaekim@bigvalue.co.kr>
2026-05-31 18:41:56 +01:00
Peter Steinberger
db40fde88c fix: persist ACP metadata in SQLite (#88724)
* fix: persist acp metadata in sqlite

* test: align session store acp expectations
2026-05-31 18:38:51 +01:00
Peter Steinberger
cdff174ce6 docs: note OpenAI Codex canonical provider 2026-05-31 18:37:30 +01:00
Vincent Koc
1ba9af1693 fix(ui): improve danger callout contrast 2026-05-31 18:36:14 +01:00
Peter Steinberger
bb5b6f38f4 test: harden release CI ordering 2026-05-31 18:33:26 +01:00
Vincent Koc
a3fa5b6577 test(vitest): classify Crabbox shared dependencies 2026-05-31 19:31:17 +02:00
Peter Steinberger
7061c1e5fd docs: raise bulk close confirmation threshold 2026-05-31 18:29:31 +01:00
Peter Steinberger
af58ed9554 docs: require external api proof search 2026-05-31 18:27:49 +01:00
Peter Steinberger
090ca19c05 refactor: make Telegram message cache SQLite-only
Remove Telegram runtime JSON sidecar read/write fallback for the prompt-context message cache. Keep legacy sidecar parsing for doctor import into SQLite plugin state and update docs/tests to match.
2026-05-31 18:27:24 +01:00
zhang-guiping
b6e9473e9f fix(auth): skip Anthropic API keys for usage status
Fixes #85124.

Anthropic standard API keys no longer resolve as provider usage auth for `openclaw status --usage`, so valid inference keys are not sent to Anthropic's OAuth usage endpoint and surfaced as misleading invalid bearer-token errors.

The provider usage-auth SDK result now has an explicit handled/no-token shape so provider hooks can suppress generic fallback without widening the OAuth helper contract. Docs, Plugin SDK API baseline, and extension package-boundary cache inputs were updated with the new contract.

Thanks @zhangguiping-xydt.

Proof:
- node scripts/run-vitest.mjs src/infra/provider-usage.auth.normalizes-keys.test.ts src/infra/provider-usage.auth.plugin.test.ts extensions/anthropic/index.test.ts
- pnpm plugin-sdk:api:check
- pnpm plugin-sdk:check-exports
- git diff --check origin/main...HEAD
- pnpm docs:list
- pnpm run test:extensions:package-boundary:compile
- autoreview clean: no accepted/actionable findings
- PR CI rollup green: 131 success, 22 skipped, 1 neutral, 0 failures

Co-authored-by: 张贵萍0668001030 <zhang.guiping@xydigit.com>
2026-05-31 18:26:03 +01:00
Peter Steinberger
fbc611ab4c docs: require fresh autoreview before landing code 2026-05-31 18:25:11 +01:00
Peter Steinberger
2b4f3e47b6 test(msteams): add keyed store to file consent runtime stub 2026-05-31 18:24:51 +01:00
Peter Steinberger
1a65425a6e refactor: extract ACP translator session updates
Extract ACP translator session-update and event-ledger emission into a dedicated helper. Keeps translator orchestration intact while preserving replay, recording, and fallback behavior.\n\nProof: focused ACP translator tests, narrow oxlint, pnpm check:test-types, autoreview clean, PR CI green.
2026-05-31 18:24:26 +01:00
Rain
301f17fb58 fix(agents): validate context engine assemble result shape
Validate context-engine assemble results at the shared harness boundary before embedded or Codex runners consume them.

Malformed plugins that return an object without a `messages` array now throw a descriptive engine-scoped error and use the existing runner fallback to pipeline messages, rather than poisoning session state and crashing prompt assembly on `.length`.

Proof:
- `node scripts/run-vitest.mjs src/agents/harness/context-engine-lifecycle.test.ts`
- `node scripts/run-vitest.mjs src/agents/embedded-agent-runner/run/attempt.spawn-workspace.context-engine.test.ts`
- `pnpm exec oxfmt --check src/agents/harness/context-engine-lifecycle.ts src/agents/harness/context-engine-lifecycle.test.ts`
- `git diff --check origin/main...HEAD`
- GitHub CI on `5b6b7b1bf69b8f30329fdf749161a192d3d016fe`: https://github.com/openclaw/openclaw/actions/runs/26719202811

Thanks @Pluviobyte.

Fixes #75541
2026-05-31 18:21:59 +01:00
Peter Steinberger
86ff92e7a8 docs: require best-fix PR review judgment 2026-05-31 13:21:15 -04:00
wAngByg
6c5cd7177f fix(doctor): detect stale gateway service version metadata 2026-05-31 18:17:30 +01:00
Peter Steinberger
6f2fbaaaf8 fix(gateway): track plugin subagent runs in agent handler
Plugin SDK subagent runs now register at the Gateway agent acceptance boundary so subagent_ended hooks fire without creating duplicate CLI task rows.

The registration stays best-effort: if the subagent registry cannot persist tracking state, the run still dispatches and falls back to the existing CLI task tracking path.

Closes #59164

Co-authored-by: Cornna <96944678+ymylive@users.noreply.github.com>
2026-05-31 18:16:00 +01:00
Peter Steinberger
21dcf2dd99 chore: stop tracking package dist output 2026-05-31 18:15:40 +01:00
Vincent Koc
938841cff3 fix(agents): count stream deltas incrementally
Count model stream diagnostic response bytes from snapshotless stream chunks, excluding accumulated partial snapshots on delta events. This avoids repeatedly serializing answer-so-far snapshots during streamed model calls and updates OTEL/docs wording for the new metric baseline.

Refs #86599.

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-05-31 18:13:58 +01:00
Peter Steinberger
a053ae5d65 test: align release CI expectations 2026-05-31 18:13:02 +01:00
Peter Steinberger
33c246dbba refactor: move plugin state slices to sqlite
* refactor: move plugin state slices to sqlite

* fix: keep legacy plugin state migration out of runtime

* fix: add doctor migrations for plugin sqlite state

* fix: preserve teams feedback learning migration keys

* fix: merge teams legacy feedback learnings

* fix: guard doctor imports against plugin state caps

* fix: leave lossy teams learning filenames unmigrated

* fix: preserve teams feedback learning scope

* fix: load plugin doctor contracts from package dist

* fix: satisfy plugin state migration gates
2026-05-31 18:09:27 +01:00
Peter Steinberger
12d4dda1bb perf(plugins): avoid duplicate provider hook load probes
Avoid duplicate provider hook load probes.

Summary:
- Route provider hook-list resolution through the existing provider resolver skip path instead of pre-checking provider load state separately.
- Preserve the provider runtime in-flight/reentrant guard because existing tests prove it prevents cached misses and nested provider-load recursion.

Verification:
- node scripts/run-vitest.mjs src/plugins/providers.runtime.consult-current-snapshot.test.ts
- node scripts/run-vitest.mjs src/plugins/provider-runtime.test.ts
- node scripts/run-vitest.mjs src/plugins/providers.test.ts
- pnpm exec oxfmt --check src/plugins/providers.runtime.ts src/plugins/provider-hook-runtime.ts
- git diff --check
- pnpm changed:lanes --json
- autoreview --mode local --prompt-file /tmp/provider-hotpath-cleanup-review.md
- Live E2E: https://github.com/openclaw/openclaw/actions/runs/26718818705
2026-05-31 18:08:13 +01:00
Peter Steinberger
f80a1e9e85 refactor: clean up ACP translator and manager tests (#88677)
* test: split ACP translator bridge coverage

* refactor: extract ACP translator session helpers

* refactor: extract ACP manager backend failover helpers

* test: split ACP manager failover coverage

* test: split ACP manager runtime config coverage

* test: split ACP manager turn result coverage

* test: split ACP manager runtime handle coverage

* test: keep ACP manager helpers within task boundaries

* ci: split gateway runtime state test shard
2026-05-31 18:04:28 +01:00
Sebastien Tardif
66bbcfdade fix(telegram): handle ENOENT race in spool drain recovery rename
Handle the Telegram isolated-polling spool recovery race where a stale `.processing` claim can disappear between discovery and the final rename back to pending. Recovery now treats `ENOENT` as benign and mirrors the existing duplicate-pending cleanup path for `EEXIST`, avoiding noisy drain-failure logs and spurious failure counters without changing claim ownership semantics.

Adds a regression test that removes the claim from inside `shouldRecover`, after recovery has discovered the entry and before the final rename path, so the old code would hit the reported `ENOENT` window.

Fixes #87847

Co-authored-by: Sebastien Tardif <sebtardif@ncf.ca>
2026-05-31 18:02:55 +01:00
alkor2000
3ceaafb2b3 fix: extend CA bundle auto-injection to all 8 Node version managers
Expand Linux CA bundle auto-injection to recognize fnm, Volta, asdf, mise, n, nodenv, nodebrew, and nvs paths in addition to nvm. Adds regression coverage for the new version-manager path layouts.

Fixes #59494.
Thanks @alkor2000.

Co-authored-by: alkor2000 <200923177@qq.com>
2026-05-31 18:02:34 +01:00
Vincent Koc
01a5e492b7 test(discord): fast-forward voice fallback timers 2026-05-31 19:02:16 +02:00
Peter Steinberger
772d13c19d fix: handle iOS global agent transcripts 2026-05-31 18:01:17 +01:00
Vincent Koc
0f6be951e0 fix(agents): avoid full stream replay on text deltas (#88252)
Prevent streaming assistant text updates from reparsing the full accumulated reply for plain deltas, avoiding repeated work for small-model streams while preserving full cleanup for directives, media, and final events.

Also load the normal Control UI Vite config in the mock browser server so browser E2E uses the same workspace aliases as dev.

Thanks @vincentkoc.
2026-05-31 17:59:45 +01:00
alkor2000
723d09ff85 fix(cli): extend holiday tagline dates through 2030
Extend the CLI holiday tagline tables for Lunar New Year, Eid al-Fitr, Easter, Diwali, and Hanukkah through 2030 so those taglines do not silently disappear after 2027.

Maintainer fixup: corrected the 2030 Diwali row to October 25 and added explicit regression coverage for that date.

Verification:
- node scripts/run-vitest.mjs src/cli/tagline.test.ts
- Direct pickTagline() probe confirmed 2030-10-25 activates Diwali and 2030-10-26 does not.

Co-authored-by: alkor2000 <200923177@qq.com>
2026-05-31 17:59:43 +01:00
Peter Steinberger
0ee5f47fba fix(feishu): enforce bitable account gates 2026-05-31 17:51:35 +01:00
OpenClaw Updater
73bb84e4bf fix: preserve explicit Feishu bitable gates 2026-05-31 17:51:35 +01:00
Gorin Lee
5cfb578cba plugin: gate Feishu bitable tools by config 2026-05-31 17:51:35 +01:00
Peter Steinberger
4150c6ff82 feat: add typed MCP code-mode API (#88678)
* feat: add typed MCP code-mode API

* fix: stabilize code-mode namespace drain

* fix: preserve code-mode run cap

* fix: reserve code-mode snapshot capacity
2026-05-31 17:51:22 +01:00
Peter Steinberger
d1b514af2e fix: remove webchat config surface 2026-05-31 12:49:18 -04:00
Masato Hoshino
3ef02ca818 fix(plugins): reuse current metadata snapshot in provider hot paths
Refactor provider metadata lookup so hot paths consult the current process snapshot before falling back to a metadata load.

Centralize provider metadata lookup in the provider runtime and update the focused tests/mocks that exercise embedded-agent and provider loading paths.

Verification:
- node scripts/run-vitest.mjs src/plugins/providers.runtime.consult-current-snapshot.test.ts
- node scripts/run-vitest.mjs src/agents/embedded-agent-runner/run/attempt.cwd-split.test.ts
- node scripts/run-vitest.mjs src/plugins/providers.test.ts
- autoreview --mode branch --base origin/main
- CPU profile loop: current-snapshot resolve 0.459 us/call vs warm direct metadata load 131.493 us/call
- GitHub CI on 728bd53510

Co-authored-by: masatohoshino <g515hoshino@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 17:48:58 +01:00
Jerry-Xin
4e84d0eaa5 fix(auto-reply): track memory flush failure exhaustion
Add durable memoryFlush failure metadata and lifecycle events so provider failures during memory flush no longer leave a session with no recorded recovery state.

After three consecutive non-abort flush failures, mark the current compaction cycle as exhausted so later messages can proceed without deleting transcript history. Successful flushes clear the failure metadata, and plugin session-entry slot reservations now protect the new fields.

Release-note: memoryFlush sessions can now fail open after repeated provider-side flush failures instead of retrying indefinitely before normal replies.

Refs #85645

Co-authored-by: 忻役 <xinyi@mininglamp.com>
2026-05-31 17:47:12 +01:00
Peter Steinberger
5bce222b0c docs(agents): require related issue search 2026-05-31 17:46:19 +01:00
Peter Steinberger
1af4c035e4 refactor: move delivery queues to SQLite (#88665)
* refactor: move delivery queues to sqlite

* fix: satisfy delivery queue sqlite boundaries

* test: remove stale reasoning replay assertion

* fix: migrate failed delivery queue entries

* test: stabilize exec shell snapshot mocks

* fix: clean legacy delivery queue markers
2026-05-31 17:43:03 +01:00
Vincent Koc
c7b190beec fix(ollama): yield during dense stream processing (#87818)
Co-authored-by: uday <udaymanish.thumma@gmail.com>
2026-05-31 17:38:13 +01:00
Yuval Dinodia
be29096081 fix(agents): resolve Codex static-catalog cold start
Fixes Codex/plugin-harness cold starts for exact static-catalog model ids such as openai/gpt-5.3-codex without adding a second resolver retry loop. The embedded runner now performs the normal provider-runtime attempt with agent discovery skipped, then consults the bundled static catalog before falling back to generic configured-provider synthesis when plugin harness owns transport.

The OpenAI static catalog row carries the Codex ChatGPT transport metadata, dynamic provider metadata still wins for runtime-owned models, and focused regression coverage exercises both paths.

Fixes #88510.

Co-authored-by: yetval <yetvald@gmail.com>
2026-05-31 17:38:10 +01:00
litang9
d446c26acb feat(deepseek): show provider balance in usage status
Show DeepSeek API-key account balance in status/auth-status usage surfaces by adding a summary-only provider usage snapshot path, a DeepSeek balance fetcher, SDK/docs coverage, and focused regression tests.

Maintainer verification accepted the additive provider-usage/status contract and the DeepSeek balance visibility boundary for authenticated status surfaces.

Proof:
- Live DeepSeek balance proof via 1Password-backed DEEPSEEK_API_KEY against https://api.deepseek.com/user/balance; key and balance amount redacted.
- GitHub CI run 26717953383 passed on the current head.
- Real behavior proof run 26718215605 passed after the PR body was refreshed.
- Local clean PR clone: git diff --check; node --max-old-space-size=8192 --import tsx scripts/generate-plugin-sdk-api-baseline.ts --check; node scripts/run-vitest.mjs run src/agents/bash-tools.exec.path.test.ts.

Co-authored-by: Alex Tang <tangli1987118@hotmail.com>
Co-authored-by: litang9 <141409885+litang9@users.noreply.github.com>
2026-05-31 17:35:41 +01:00
vortexopenclaw
fa0a323ebd fix(secrets): treat Codex app-server marker as non-secret
Treat the synthetic Codex app-server auth marker as a core non-secret marker so secrets audit does not flag it when bundled plugin discovery is disabled.\n\nVerified with focused model-auth marker tests, isolated secrets-audit CLI proof, autoreview, and green CI.\n\nThanks @vortexopenclaw.
2026-05-31 17:35:13 +01:00
Vincent Koc
dd79c8836a perf(scripts): parallelize startup metadata help rendering 2026-05-31 18:35:01 +02:00
Peter Steinberger
2e3650d5b3 fix: inset iOS onboarding action buttons 2026-05-31 17:31:17 +01:00
Peter Steinberger
d76627f232 ci: add crabbox prewarm jobs 2026-05-31 17:30:26 +01:00
Ron Cohen
5152d8beb4 fix(whatsapp): suppress silent-run typing indicators
Suppress WhatsApp typing indicators only for silent message-tool-only unmentioned group runs. Automatic visible replies and authorized group commands still show composing normally.

Fixes the autoreview regression risk by narrowing suppressTyping and adding coverage for both silent and visible group paths.

Proof:
- pnpm test src/auto-reply/reply/reply-utils.test.ts extensions/whatsapp/src/auto-reply/monitor/inbound-dispatch.test.ts
- .agents/skills/autoreview/scripts/autoreview --mode local
- .agents/skills/autoreview/scripts/autoreview --mode branch --base origin/main
- CI run 26717880577 green

Thanks @Bluetegu.
2026-05-31 17:28:58 +01:00
Yuval Dinodia
a6ee3dbbdd fix(ios): update group chats in realtime
Subscribe the iOS gateway chat transport to per-session transcript events so group chats update when other clients send messages. Constrain local user echo adoption to the optimistic row tied to the still-pending send run, so repeated same-content user messages from other clients append instead of replacing history.

Fixes #80231.

Co-authored-by: Yuval Dinodia <yetvald@gmail.com>
2026-05-31 17:24:59 +01:00
Gavin Zeng
4ab2eb45d0 fix(doctor): repair stale session snapshot paths
Fixes #85689.

Summary:
- Repair stale bundled skill paths in inline prompts, prompt blobs, resolved skill metadata, and resolved skill sourceInfo metadata.
- Keep repair scoped to cached snapshot fields and preserve unrelated session content.
- Replace the root reproduction script with colocated Vitest coverage.

Verification:
- pnpm test src/commands/doctor-session-snapshots.test.ts -- --reporter=verbose
- pnpm check:test-types
- pnpm lint --threads=8
- pnpm dup:check:coverage
- pnpm tsgo:prod
- pnpm check:changed (Testbox tbx_01kszd25ad7x81j0f1r7kfsqc6, Actions run 26717761222)
- PR CI green on 540b1a387e

Co-authored-by: GavinZ <zengganghui@zgh123.space>
2026-05-31 17:24:29 +01:00
samzong
5b310a7b27 fix(agents): release abandoned provider streams
Fix streamed provider cleanup so abandoned managed fetch bodies no longer keep undici sockets open, and cancel Anthropic/Gemini SSE readers deterministically when parsing exits early.

Keep the FinalizationRegistry abort path as a last-resort GC safety net for unmanaged/abandoned responses, while parser-owned paths cancel readers explicitly on thrown errors or malformed events.

Also records the browser-only Control UI redactor alias in the optional deadcode allowlist and keeps mocked exec supervisor tests off shell snapshot wrapping after the branch was rebased onto default shell snapshots.

Fixes #67461

Verification:
- node scripts/run-vitest.mjs src/agents/provider-transport-fetch.test.ts src/agents/anthropic-transport-stream.test.ts extensions/google/transport-stream.test.ts src/agents/bash-tools.test.ts src/agents/bash-tools.exec.path.test.ts test/scripts/test-live-shard.test.ts
- pnpm check:test-types
- node scripts/run-oxlint-shards.mjs --threads=8
- .agents/skills/autoreview/scripts/autoreview --mode branch --base origin/main --parallel-tests "node scripts/run-vitest.mjs src/agents/provider-transport-fetch.test.ts src/agents/anthropic-transport-stream.test.ts extensions/google/transport-stream.test.ts src/agents/bash-tools.test.ts src/agents/bash-tools.exec.path.test.ts test/scripts/test-live-shard.test.ts"
- git diff --check origin/main...HEAD
- PR CI on a1db789652

Co-authored-by: samzong <samzong.lu@gmail.com>
Signed-off-by: samzong <samzong.lu@gmail.com>
2026-05-31 17:17:53 +01:00
Peter Steinberger
31c83c6be1 chore(plugin-sdk): refresh API baseline 2026-05-31 17:17:02 +01:00
Peter Steinberger
fbfbe45fc6 fix(agents): use static shell snapshot temp prefix 2026-05-31 17:12:24 +01:00
Mike Harrison
63d0c1d513 fix(slack): keep progress drafts in one message (#85612)
Keep Slack progress-mode drafts on one rolling preview message across assistant and reasoning boundaries while preserving boundary cleanup and the latest visible tool-progress lines. Partial/replace modes still start a fresh draft at assistant boundaries.

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-05-31 17:07:41 +01:00
Peter Steinberger
71a516d644 ci: narrow legacy webchat migration value 2026-05-31 12:07:36 -04:00
Syu
63621eead2 fix(discord): route thread bindings to plugin owners
Route Discord thread follow-up messages to plugin-owned bindings by the raw thread id while retaining parent channel fallback matching. This fixes `/codex bind` follow-ups in Discord threads being claimed by the parent OpenClaw route instead of the bound Codex session.

Verification:
- `node scripts/run-vitest.mjs extensions/discord/src/channel.conversation.test.ts src/hooks/message-hook-mappers.test.ts extensions/discord/src/monitor/message-handler.process.test.ts -t "prefers bound session keys|passes Discord thread parent only|routes Discord thread plugin-owned bindings|passes thread parent ids|thread binding"`
- `node scripts/run-vitest.mjs src/auto-reply/reply/dispatch-from-config.test.ts -t "routes Discord thread plugin-owned bindings by raw thread id"`
- `pnpm build`
- `pnpm lint --threads=8`
- `CI=true FORCE_COLOR=0 pnpm lint --threads=8`
- `.agents/skills/autoreview/scripts/autoreview --mode local`
- GitHub: Real behavior proof, check-test-types, check-dependencies, check-prod-types, auto-reply dispatch shard, hooks shard, and extension package boundary passed on head 1e896d9835.

Known unrelated CI noise at merge: broad opengrep/test/lint CI failures are outside the touched Discord/session-binding surface and contradicted by focused local proof where applicable.

Co-authored-by: Hex <hex@openclaw.ai>
2026-05-31 17:03:55 +01:00
saju01
fbb776d92c feat(github-copilot): add Claude Opus 4.8 to default model catalog
Add Claude Opus 4.8 to the GitHub Copilot static model catalog and default model IDs.

Updates provider manifest metadata and regression coverage so fallback/default discovery includes claude-opus-4.8.

PR: #88547
Co-authored-by: saju01 <saju@coderedcorp.com>
2026-05-31 17:00:24 +01:00
Peter Steinberger
6f4ba7c80e ci: fix acp spawn defaults lint 2026-05-31 12:00:09 -04:00
Ho Lim
044f7f3790 fix: route iMessage DM media through attachment handoff (#87904)
* fix: route iMessage DM media through attachment handoff

* fix: close iMessage caption follow-up clients

* test: stabilize iMessage timeout recovery checks

* fix(imessage): keep attachment reply-cache identifiers aligned

* fix(imessage): preserve service for media handoff

* fix(imessage): prefer caption ids for placeholder attachments

* fix(imessage): preserve region fallback for media handles

* fix(imessage): retain chat id attachment cache scope

* fix(imessage): avoid premature caption echoes

---------

Co-authored-by: Omar Shahine <10343873+omarshahine@users.noreply.github.com>
2026-05-31 09:00:00 -07:00
Peter Steinberger
d07f508020 fix: retire webchat channel config 2026-05-31 11:58:54 -04:00
Peter Steinberger
e5097b3b09 fix(tlon): avoid bundling native skill packages 2026-05-31 16:58:46 +01:00
Jayesh Betala
29dd7847fd fix(terminal): clamp wide graphemes in narrow table cells
Clamps ANSI-aware terminal table cells before padding so width-2 graphemes cannot push borders out of alignment in width-1/narrow columns.

Fixes #88556.

Proof:
- node scripts/run-vitest.mjs run packages/terminal-core/src/ansi.test.ts packages/terminal-core/src/table.test.ts
- CI run 26717035619; check-dependencies red only for unrelated current-main deadcode issue ui/src/ui/browser-redact.ts, also red on main run 26717029674. checks-node-agentic-agents-core rerun failed in unrelated src/agents/bash-tools*.test.ts outside this PR diff.

Co-authored-by: Jayesh Betala <jayesh.betala7@gmail.com>
2026-05-31 16:54:47 +01:00
Alix-007
2870a28aa9 fix(memory-core): reclaim orphaned dreaming sessions
When dreaming narrative cleanup calls subagent.deleteSession() in the finally block and it throws, the store row can be left behind referencing a still-present transcript. The scrubber only pruned dreaming rows whose transcript was missing, so these orphans lingered in the recent sessions sidebar with no kind/status/endedAt and accumulated across restarts.

Reclaim a dreaming store row when its transcript is missing OR has aged past DREAMING_ORPHAN_MIN_AGE_MS, then leave the transcript unreferenced so the orphan-transcript pass archives it.

Fixes #88322

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 16:52:11 +01:00
Vincent Koc
9850ee65c9 test(doctor): cache default command in e2e 2026-05-31 17:50:38 +02:00
Peter Steinberger
d1c4c3344e ci: mark browser redactor as UI entry 2026-05-31 11:47:40 -04:00
Peter Steinberger
a3c6164a8d test: add ACP spawn defaults live Docker test 2026-05-31 16:46:20 +01:00
Franco Viotti
a71b121c69 fix(googlechat): preserve thread for message tool replies (#80996)
Use the Google Chat thread resource as the ambient message-tool reply target so replies stay in the inbound thread. Normalize the current Google Chat space target and let plugin threading adapters explicitly suppress the generic message-id fallback when a provider needs a thread resource instead of a message resource.

Co-authored-by: Peter Steinberger <steipete@gmail.com>
Co-authored-by: Franco Viotti <franco-viotti@users.noreply.github.com>
2026-05-31 16:43:46 +01:00
Peter Steinberger
ed74fa692b test(ui): narrow vite resolve hook in config test 2026-05-31 16:42:13 +01:00
Peter Steinberger
210adf1d11 fix(agents): retry transient stale session locks
Follow-up to #88658. Retries transient stale session-lock acquire failures when diagnostics show the old stale report disappeared, was replaced by a fresh valid lock, or was replaced by a fresh payload-less lock still inside the mtime/orphan grace window.

Preserves typed `SessionWriteLockStaleError` diagnostics for still-present live OpenClaw-owned stale locks.

Proof: 53 focused session-write-lock tests passed locally and in the agents-core CI shard; `pnpm tsgo:test:src`, touched-file oxlint, `git diff --check`, and autoreview passed locally. CI run 26716843811 has unrelated failures in UI deadcode/types and bash-tools tests; session-write-lock tests passed in that run.

Refs #87217.
2026-05-31 16:41:37 +01:00
clawSean
51228aecd4 fix(sms): cover native proof follow-ups (#88601)
Remove the duplicate plain approve command from pairing replies so SMS/native pairing shows one copyable owner approval command in the fenced block.

Add regression coverage for the single approve-command occurrence, plus Twilio guarded-egress release coverage for non-2xx and malformed-success send responses.

Verification:
- pnpm exec oxfmt --check src/pairing/pairing-messages.ts src/pairing/pairing-messages.test.ts extensions/sms/src/twilio.test.ts
- node scripts/run-vitest.mjs src/pairing/pairing-messages.test.ts src/pairing/pairing-challenge.test.ts src/plugin-sdk/channel-pairing.test.ts
- node scripts/run-vitest.mjs extensions/sms/src/twilio.test.ts

Thanks @clawSean.
2026-05-31 16:41:24 +01:00
Peter Steinberger
63de51ab96 refactor(cron): clarify sqlite store internals 2026-05-31 16:38:49 +01:00
Sanjay Santhanam
e0e7bae612 fix(discord): handle PluralKit DM pairing ids
Fix Discord DM pairing for PluralKit senders by storing the pairing identity with the same `pk:<member-id>` form used at inbound lookup time. Also recognizes both canonical direct DM session keys and account-scoped direct DM session keys as DM approval sessions.

Focused proof: `node scripts/run-vitest.mjs extensions/discord/src/approval-native.test.ts extensions/discord/src/monitor/dm-command-auth.test.ts extensions/discord/src/monitor/dm-command-decision.test.ts extensions/discord/src/monitor/message-handler.preflight.test.ts` passed with 4 files and 82 tests.

Closes #86332

Co-authored-by: Sanjays2402 <51058514+Sanjays2402@users.noreply.github.com>
2026-05-31 16:35:48 +01:00
Peter Steinberger
b9dc3c3894 perf: trim tui startup and refresh work 2026-05-31 16:30:04 +01:00
Lawrence Tran
507c6fd5ca fix(slack): avoid forced threads for replyToMode off
Slack top-level channel mentions with replyToMode off now reply at the channel root instead of inheriting stale or auto-created thread targets.

Existing Slack thread replies and Slack assistant DM thread targets continue to preserve their thread target.

Thanks @lawrencetran.
2026-05-31 16:29:34 +01:00
Alexander Falk
e18099b8c3 fix(macos): prevent duplicate menu bar icons
Fix macOS menu bar status-item storms during rapid gateway connection churn by removing stale SwiftUI-vended status items before adopting replacements and debouncing transient control-channel states.

Surface: macOS menu bar app, `MenuBarExtra` status item ownership, `ControlChannel` UI-observed connection state.

Proof:
- `git diff --check origin/main...pr/82739`
- `swift test --package-path apps/macos --filter ControlChannelStateDebouncerTests`
- PR CI: preflight, security-fast, macos-node, macos-swift, dependency-guard, changed-path scan, real behavior proof, Socket checks

Co-authored-by: Alexander Falk <al@falk.us>
2026-05-31 16:18:37 +01:00
Vincent Koc
a52c4d101a perf(agents): avoid full setup registry for runtime aliases 2026-05-31 17:14:09 +02:00
Peter Steinberger
4ef141d525 fix(agents): prevent embedded runtime shadowing 2026-05-31 16:13:01 +01:00
Peter Steinberger
1955f42bfe fix(outbound): route source replies through configured channels 2026-05-31 16:12:52 +01:00
Peter Steinberger
cd3b467f3c refactor(cron): split tool and doctor repair helpers 2026-05-31 16:11:45 +01:00
Peter Steinberger
45ab822918 perf: reduce tui refresh work 2026-05-31 16:10:09 +01:00
Peter Steinberger
6b1b2ff20a feat: default exec shell snapshots 2026-05-31 16:09:43 +01:00
Peter Steinberger
89cdf164ca fix(ui): keep chat usable during session loading 2026-05-31 16:08:56 +01:00
Peter Steinberger
972d2b66d1 fix(cron): guard flat atMs canonicalization 2026-05-31 16:02:06 +01:00
Peter Steinberger
a84819a639 refactor(cron): keep runtime on canonical sqlite rows 2026-05-31 16:02:06 +01:00
Peter Steinberger
827ceb55d0 fix(codex): restore bounded recovery continuity
Restore bounded Codex native recovery continuity without replaying covered mirrored transcript history. Closes #88352. Closes #88354.
2026-05-31 15:55:32 +01:00
Peter Steinberger
7b78941ea5 refactor: clean up ACP package metadata and helpers (#88659)
* refactor: derive acp core package subpath maps

* refactor: split acp manager task and timeout helpers

* refactor: split acp translator presentation helpers

* fix: keep packaged acp core plugin aliases

* ci: split gateway control plane runtime shard
2026-05-31 15:53:14 +01:00
Chunyue Wang
a5d8f09fd4 fix(discord): ping mention-bearing final replies
Fixes #88360.

Route Discord live-preview final replies containing targeted user or role mentions through fresh message delivery instead of edit finalization, preserving mention alias rewriting and notification behavior. Plain, broadcast-only, and mixed targeted-plus-broadcast replies keep the existing preview edit path.

Proof: CI run 26708866609 green for relevant lanes; Real behavior proof run 26708866194 successful; local git diff --check and git merge-tree clean.
2026-05-31 15:52:59 +01:00
Peter Steinberger
8f941ea0ac fix(telegram): preserve usage footer for tool-only replies
Route implicit message_tool_only current-source sends through the internal source-reply sink for non-webchat transports, preserving the final reply payload path where usage decoration runs. Also keep reply payload metadata when appending usage text so transcript mirror text matches the delivered footer-bearing reply.

Recreated from PR #87425 because the fork branch is draft, dirty against main, and not maintainer-pushable.

Co-authored-by: Gio Della-Libera <giodl73@gmail.com>
2026-05-31 15:51:41 +01:00
Vincent Koc
b334e7ef29 fix(agents): avoid alias setup load for matching refs 2026-05-31 16:48:27 +02:00
Peter Steinberger
d5ac97652a chore(ui): translate thinking default label 2026-05-31 15:47:58 +01:00
Vincent Koc
4d135ae28b fix(agents): preserve runtime tools in lean mode (#88381)
fix(agents): preserve runtime tools in lean mode

Keep runtime-required tools, especially `message`, available when local-model lean filtering is enabled. This preserves `forceMessageTool`, `message_tool_only` source replies, explicit runtime allowlists, and schema projection without disabling lean filtering for ordinary denied tools.

Proof: focused Vitest passed 190 tests; `git diff --check origin/main...HEAD` passed; PR CI had no failing or pending checks.
2026-05-31 15:43:48 +01:00
xiaotian
f547ea7668 fix(messages): use best-effort for implicit tool-only source replies (#84232)
fix(messages): use best-effort for implicit tool-only source replies

Preserve durable required-send semantics for explicit non-current targets while allowing current-source `message_tool_only` replies to be delivered through best-effort outbound sends. This fixes Slack source replies that otherwise fail when the adapter has no `reconcileUnknownSend` hook.

Fixes #84078.
2026-05-31 15:41:30 +01:00
Peter Steinberger
66775c037e docs: raise bulk PR close threshold 2026-05-31 15:40:16 +01:00
Peter Steinberger
c389839d30 feat: add exec shell snapshot cache
Add an opt-in bash/zsh shell snapshot cache for host exec runs, consolidate shell helper ownership into src/agents/shell-utils.ts, document OPENCLAW_EXEC_SHELL_SNAPSHOT, and keep Windows config command execution on the bash resolver. Also removes a redundant Discord gateway close-code type branch that was blocking test type checks.
2026-05-31 15:39:53 +01:00
Peter Steinberger
50c651900e fix: use typed tui empty session defaults 2026-05-31 15:38:55 +01:00
Peter Steinberger
18dc6e5cd4 perf: speed up tui session refresh 2026-05-31 15:38:54 +01:00
Peter Steinberger
9a4b631a1d fix(ci): align agent thinking default surfaces 2026-05-31 15:38:32 +01:00
Peter Steinberger
832b6487e0 docs: require live batch issue verification 2026-05-31 15:37:36 +01:00
Peter Steinberger
d689893a6f ci(release): extend QA runtime parity timeout 2026-05-31 15:36:35 +01:00
Peter Steinberger
d1bec469af ci: stabilize Testbox changed checks 2026-05-31 15:34:23 +01:00
Peter Steinberger
7ca77124fe fix(agents): report stale session locks without cleanup
Report live-owned stale session locks as typed acquisition failures instead of auto-removing them, while preserving safe reclaim for dead/orphaned lock files. Propagate stale lock acquisition through embedded runner takeover handling, failover/cache/delivery classifiers, and QA retry detection.

Refs #87779
2026-05-31 15:28:54 +01:00
Peter Steinberger
fb7e21796d fix(gateway): reject stale lifecycle session updates
Fixes #88538. Carry the owning run sessionId through lifecycle events, skip stale persistence and sessions.changed projection when sessions.reset rotated the row, and register the persisted owning id across session-backed run paths. Also aligns per-agent subagent thinking typing with existing runtime/test usage.\n\nCo-authored-by: openperf <16864032@qq.com>
2026-05-31 15:27:01 +01:00
Peter Steinberger
88c99ddf5f docs(agents): require typed presentation actions 2026-05-31 15:19:45 +01:00
Peter Steinberger
1bfae9d458 fix(models): keep auth login out of main config
Store provider login profiles in auth-state, preserve configured auth order/profile constraints, and keep legacy credential/keyRef normalization durable. Fixes #88565.
2026-05-31 15:14:16 +01:00
Peter Steinberger
2b61d38a45 fix: guard stale lifecycle snapshots (#88583) 2026-05-31 15:08:36 +01:00
openperf
613f51a7aa fix(gateway): reject pre-reset run lifecycle events from clobbering rotated session
sessions.reset rotates a channel session to a fresh sessionId under the same
sessionKey, but an old in-flight run could still emit late start/end/error
lifecycle events. persistGatewaySessionLifecycleEvent resolved the row purely
by sessionKey, so those stale events overwrote the new row's status
(running/failed with hasActiveRun=false).

Stamp the owning run's sessionId onto lifecycle events in emitAgentEvent and
skip persistence when it differs from the current row's sessionId. The embedded
runner refreshes the run context's sessionId on every live-session rotation
(mid-run compaction), so a legitimately rotated run's terminal event still
matches the rotated row; only an external sessions.reset stays mismatched.
Matching and unknown-owner events are unaffected.

Fixes #88538
2026-05-31 15:08:36 +01:00
Peter Steinberger
ff22b1e9e6 fix: apply ACP spawn model defaults 2026-05-31 15:07:33 +01:00
Logan Ye
fdf6092494 fix(agents): accept disabled thinking params
Fixes #74374.

Normalizes params.thinking false, disabled, and none to the existing off state for agent and auto-reply model selection. Thanks @yelog.

Known proof gap: build-artifacts is failing in an unrelated plugin prerelease plan assertion that expects an old Docker stats helper string; targeted tests, diff check, autoreview, and all touched-path checks pass.
2026-05-31 15:07:18 +01:00
Jayesh Betala
f8f52592c5 fix(gateway): expose agent thinking defaults
Fixes #81760.

Exposes existing agent and model thinking defaults through agents.list, including protocol and Swift model support. Thanks @jbetala7.
2026-05-31 15:05:55 +01:00
Peter Steinberger
d99934aacd ci: use normal node_modules for Blacksmith Testbox 2026-05-31 15:04:49 +01:00
Steven
13d2800489 fix(agents): inherit subagent thinking defaults
Fixes #55790.

Adds tested subagent thinking precedence for explicit tool input, requester agent subagent defaults, global subagent defaults, and inherited caller thinking. Thanks @stevenepalmer.
2026-05-31 15:03:10 +01:00
Peter Steinberger
82a0ba8c4c fix(plugins): remove redundant proxy assertion 2026-05-31 15:02:44 +01:00
Peter Steinberger
4d69fc23d0 fix(codex): clear completed dynamic tool release blockers 2026-05-31 15:02:44 +01:00
Peter Steinberger
1e82263492 fix(codex): let async media coexist with terminal batches 2026-05-31 15:02:44 +01:00
Peter Steinberger
d99c824ac1 fix(plugins): delegate wrapped tool properties 2026-05-31 15:02:44 +01:00
Peter Steinberger
3ebbf9a0c1 fix(agents): keep async media starts nonterminal 2026-05-31 15:02:44 +01:00
Peter Steinberger
f62a22ce56 fix(plugins): preserve wrapped tool descriptors 2026-05-31 15:02:44 +01:00
Peter Steinberger
643633c1e5 fix(plugins): scope tool callbacks during materialization 2026-05-31 15:02:44 +01:00
Lellansin Huang
0dfcf73a57 fix(gateway): enforce OpenAI tool_choice contracts
Enforce OpenAI-compatible `tool_choice` contracts for Gateway HTTP Chat Completions and Responses client function tools.

- Add shared request normalization and post-run enforcement for required and pinned client function tool choices.
- Buffer streaming output until the tool-choice contract is satisfied, so failed runs do not leak partial assistant prose.
- Document the client-function-tool scope and add regression coverage for Chat/Responses success and failure cases.

Thanks @Lellansin for the contribution.

Proof: exact-head CI passed for `79fa0947360d307cf4ecffe713489cdf5db61093` in run `26714604449`; focused gateway tests passed locally.
2026-05-31 15:02:29 +01:00
Peter Steinberger
ec8cb8bcbf feat: add MCP code-mode namespace (#88636)
* feat: add MCP code-mode namespace

* fix: unblock mcp namespace ci gates
2026-05-31 15:02:19 +01:00
Andy Ye
44c65de17a fix(agents): avoid synthetic tool results during parallel races
Fixes the session transcript race where a newer assistant tool-call turn could force pending older tool calls to be written as synthetic missing-result entries while real parallel tool results were still in flight.

The guard no longer synthesizes at that racing boundary when synthetic repair is enabled, and transcript repair now moves late real results back beside their matching assistant tool-call turn before adding any placeholder. This keeps provider replay strict while preserving useful tool output.

Regression coverage: focused guard and transcript-repair tests for late parallel results.

Closes #88168.
Follow-up lock-lifetime report tracked in #88647.
Thanks @TurboTheTurtle for the fix and @jhartman00 for the report.

Co-authored-by: Andy Ye <35905412+TurboTheTurtle@users.noreply.github.com>
2026-05-31 15:00:44 +01:00
Vincent Koc
0833c68b1b fix(e2e): show plugins docker sweep progress 2026-05-31 15:57:19 +02:00
Peter Steinberger
f2ace9ff4e docs: require gh comment drafts 2026-05-31 09:56:56 -04:00
Peter Steinberger
036acbd358 docs: require codex source citations 2026-05-31 09:55:07 -04:00
Peter Steinberger
95890fe150 fix(agents): release session lock on manual abort
Release the embedded attempt session lock on manual aborts through the same best-effort abort cleanup path used by timeout aborts.

Proof: focused Vitest for abort/session-lock cleanup, `pnpm check:test-types`, oxfmt, `git diff --check`, branch autoreview, and full PR CI on 56fa5420d6.

Fixes #88600
2026-05-31 14:53:42 +01:00
Peter Steinberger
a7075f3634 docs: clarify autoreview refactor follow-up 2026-05-31 14:52:45 +01:00
Chunyue Wang
582fea942b fix(agents): scope timeout cooldowns by model
Fixes #87462.

Timeout transport failures now record cooldowns against the attempted model when available. Model-scoped cooldown bypasses continue to respect profile-wide blocked/disabled windows, and timeout expiry selection stays per profile while rate-limit expiry keeps shared reset aggregation.

Verification:
- pnpm exec oxfmt --check --threads=1 src/agents/auth-profiles/usage-state.ts src/agents/auth-profiles/usage.test.ts src/agents/auth-profiles.getsoonestcooldownexpiry.test.ts src/agents/auth-profiles.markauthprofilefailure.test.ts src/agents/embedded-agent-runner/run.ts
- pnpm check:test-types
- pnpm test src/agents/auth-profiles.getsoonestcooldownexpiry.test.ts src/agents/auth-profiles/usage.test.ts src/agents/auth-profiles.markauthprofilefailure.test.ts src/agents/embedded-agent-runner/run.incomplete-turn.test.ts
- autoreview clean
- GitHub Actions green on PR head d64e2a4d2f
2026-05-31 14:51:20 +01:00
Peter Steinberger
d927e73609 test(discord): drive application id retry timer 2026-05-31 14:50:08 +01:00
Peter Steinberger
b36ed41559 docs: strengthen review dependency inspection rules 2026-05-31 09:49:03 -04:00
Peter Steinberger
7dea283756 refactor: expand acp core package (#88618)
* refactor: expand acp core package

* chore: drop acp core package symlink

* fix: keep acp core dependency graph stable

* fix: add acp core tsconfig subpaths

* fix: sync acp core boundary path artifacts

* fix: use kysely for cron run-log queries

* fix: resolve acp core subpaths in loaders
2026-05-31 14:48:57 +01:00
Vincent Koc
cc290050b4 fix(doctor): diagnose malformed provider catalogs
Move malformed static provider catalog diagnostics into `openclaw doctor` instead of adding fallback behavior to runtime projection.

Doctor now validates full provider registrations for malformed static catalog hooks, result containers, provider keys, model arrays, model iteration, model ids/names, invalid catalog order, and proxy/access errors. Runtime unified text provider catalog projection remains strict on the typed provider catalog contract.

Verification:
- `node scripts/run-vitest.mjs src/flows/doctor-core-checks.runtime.test.ts src/flows/doctor-core-checks.test.ts src/flows/doctor-health-contributions.test.ts src/flows/doctor-health-conversion-plan.test.ts src/plugin-sdk/provider-entry.test.ts`
- `node_modules/.bin/oxfmt --check src/plugins/provider-catalog-unified-text.ts src/flows/doctor-core-checks.ts src/flows/doctor-core-checks.test.ts src/flows/doctor-core-checks.runtime.ts src/flows/doctor-core-checks.runtime.test.ts src/flows/doctor-health-contributions.ts src/flows/doctor-health-contributions.test.ts src/flows/doctor-health-conversion-plan.ts src/flows/doctor-health-conversion-plan.test.ts`
- `node scripts/run-oxlint.mjs src/flows/doctor-core-checks.runtime.ts src/flows/doctor-core-checks.runtime.test.ts src/plugins/provider-catalog-unified-text.ts`
- `pnpm tsgo:test`
- `git diff --check origin/main...HEAD`
- `.agents/skills/autoreview/scripts/autoreview --mode branch --base origin/main --prompt-file /tmp/provider-catalog-doctor-review-context.txt`
- GitHub PR checks green on head `876fdda5a352b0f15bfbe2abe9be43ebada7c596`

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-05-31 14:48:15 +01:00
Andy Ye
826b378452 fix(agents): normalize prefixed Anthropic model ids (#88587) 2026-05-31 14:47:40 +01:00
Peter Steinberger
0d17623f00 chore: bump OpenClaw version to 2026.5.31
Bumps OpenClaw release metadata to 2026.5.31 across package manifests, app version files, plugin metadata, changelog headings, and generated shrinkwraps.

Verification:
- pnpm plugins:sync:check
- pnpm ios:version:check
- pnpm deps:shrinkwrap:check
- git diff --check
- stale 2026.5.30/build-code scan across changed files
- autoreview clean: no accepted/actionable findings
- PR CI green for real gates: Checks, security scans, dependency guard, app lanes, real behavior proof

Known non-code workflow issue:
- label workflow failed because this PR hits GitHub's 100-label issue cap before the size-label step.
2026-05-31 14:46:17 +01:00
Soham Patankar
400be62f76 feat(codex): add portable Codex command pickers (#82224)
Refactor Codex slash-command pickers so the Codex plugin owns the native command tree and returns portable presentation buttons for channels to render. Telegram now maps portable slash-command buttons to `tgcmd:` native callbacks while preserving approval callback shortening/bypass behavior, and the old Telegram-specific Codex callback menu path is gone.

Verification:
- `node scripts/run-vitest.mjs extensions/codex/src/command-plugins-management.test.ts extensions/codex/src/commands.test.ts extensions/telegram/src/button-types.test.ts`
- `node scripts/run-vitest.mjs extensions/telegram/src/bot.test.ts extensions/telegram/src/button-types.test.ts extensions/telegram/src/bot-native-commands.test.ts extensions/telegram/src/shared.test.ts`
- `node scripts/run-vitest.mjs run --config test/vitest/vitest.media-understanding.config.ts --reporter=verbose`
- `pnpm check:test-types`
- `pnpm tsgo:prod`
- `pnpm lint --threads=8`
- `git diff --check`
- `.agents/skills/autoreview/scripts/autoreview --mode branch --base origin/main`
- CI `26714121462`

Co-authored-by: Soham Patankar <102520430+yaanfpv@users.noreply.github.com>
2026-05-31 14:45:10 +01:00
Nao
5a0e67791f fix(tui): preserve pending local runs during session sync (#87959)
* fix(tui): preserve pending local runs during session sync

* fix(tui): guard optimistic run ownership

* fix(tui): consume early accepted run finals

* fix(tui): preserve deferred pending history reloads

---------

Co-authored-by: nao860226-rgb <nao860226-rgb@users.noreply.github.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-05-31 14:39:24 +01:00
Peter Steinberger
85beee613c docs: clarify inline code comments
Comment-only follow-up documenting reusable gateway, auth, proxy, device, Talk, session, and agent helper contracts.\n\nVerification: git diff --check plus targeted tests recorded in PR body.
2026-05-31 14:37:41 +01:00
yaoyi1222
75e0053cf9 fix(auto-reply): warn on substantive private message-tool finals
Warn operators when message_tool_only produces unusually substantive private final text without a delivered source reply. Keeps short/NO_REPLY silence quiet, avoids logging response bodies, and distinguishes unrelated side effects from source-reply delivery.
2026-05-31 14:35:58 +01:00
Sebastien Tardif
81b9da0bb0 fix(tui): use middle truncation for paths and commands in tool display (#88050)
* fix(tui): use middle truncation for paths and commands in tool display

Closes #87936

* fix(test): update channel-streaming test for middle truncation output

Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>

* chore: retrigger CI (vitest env teardown flake)

Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>

* fix(tui): redact tool details before middle truncation

Apply redactToolDetail() to command and generic string text before
middle truncation so credential-like suffixes are masked while full
flag/key context is still available. Previously, truncation could
remove the --flag prefix while preserving the raw secret at the tail,
causing redaction patterns to miss the value.

Add regression tests for sk- prefixed tokens in commands and ghp_
tokens in generic string details.

Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>

---------

Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
2026-05-31 14:35:55 +01:00
Mukunda Rao Katta
e452d16cea fix(webchat): suppress stale active session rows (#87962) 2026-05-31 14:35:50 +01:00
Sebastien Tardif
9a1b95c1e6 fix(tui): skip history reload when final event has displayable output (#88004)
* fix(tui): skip history reload when final event has displayable output

On external/gateway runs, handleChatEvent fires void loadHistory() on
every final event. loadHistory() does clearAll() + rebuild from server
data, but the server may not have persisted the just-finished message
yet, causing the rendered final message to vanish.

Add a hasDisplayableFinal option to maybeRefreshHistoryForRun that skips
the destructive reload when the final text is already rendered locally.
This mirrors the existing local-run guard. Compute finalText before the
reload decision so the guard has the information it needs.

Closes #87922

Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>

* retrigger proof check

Signed-off-by: Seb Tardif <sebtardif@ncf.ca>

---------

Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
Signed-off-by: Seb Tardif <sebtardif@ncf.ca>
2026-05-31 14:35:44 +01:00
Peter Steinberger
5dc4531fdf test(discord): isolate timer-sensitive request tests 2026-05-31 14:31:10 +01:00
Vincent Koc
9518d1f27c fix(auth): coerce persisted device auth tokens 2026-05-31 15:22:44 +02:00
Vincent Koc
fbde572491 fix(e2e): heartbeat resource-sampled docker lanes 2026-05-31 15:22:44 +02:00
Peter Steinberger
f24a138790 refactor: unify subagent handoffs into agent steering queue
Refactor the subagent completion handoff path into the generic agent steering queue, preserving legacy persisted handoff lease fields by normalizing them into steering lease fields on restore.

Also allowlists the split cron run-log SQLite boundary in the Kysely guardrail after rebasing onto current main.

Refs #88407.
2026-05-31 14:21:20 +01:00
Chunyue Wang
02c7b5b82f fix(tasks): reclaim ACP zombie runs blocking gateway restart (#88281)
* fix(tasks): reclaim ACP zombie runs blocking gateway restart (#88205)

hasBackingSession treated an ACP task as backed whenever its persisted
session-store entry existed, so a crashed mid-turn ACP run left a
status=running record that survived the crash and wedged gateway
restart/update forever.

Gate ACP backing on in-process live-turn liveness instead of entry
existence, behind the existing authoritative-process flag (generalized
from cron-only) so a standalone maintenance CLI with an empty live-turn
map stays conservative and never reclaims. The liveness signal lives in a
core-internal active-turns registry (mirroring cron active-jobs) so it
stays off the SDK-exported AcpSessionManager surface. It is marked once
before the backend loop and cleared when the task is marked terminal, so
a slow init or backend failover cleanup cannot let the sweep reclaim a
still-live turn.

* fix(tasks): preserve cron operator JSON diagnostic reason

Split the merged runtime_not_authoritative reason back into the existing cron_runtime_not_authoritative (shipped, consumed by openclaw tasks maintenance --json operator scripts) and a new acp_runtime_not_authoritative for the ACP branch. Strengthen the cron non-authoritative test to lock the reason string contract.

* fix(tasks): clear ACP turn liveness on retry failures

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-05-31 14:19:56 +01:00
Peter Steinberger
100dd79468 test(release): wait for bundled runtime commands 2026-05-31 14:09:27 +01:00
Chunyue Wang
318cae1500 fix(hooks): isolate slug-generator auth failures
Summary:
- route slug-generator embedded runs through lane-local auth profile failure handling
- add regression coverage for the run option
- repair current landing checks for media auth mocks, device auth parsing, transcript sanitizer harness typing, and lint

Verification:
- node scripts/run-vitest.mjs src/hooks/llm-slug-generator.test.ts src/agents/embedded-agent-runner/run/auth-profile-failure-policy.test.ts
- node scripts/run-vitest.mjs run --config test/vitest/vitest.media-understanding.config.ts --reporter=verbose
- pnpm check:test-types
- pnpm lint --threads=8
- git diff --check
- .agents/skills/autoreview/scripts/autoreview --mode branch --base origin/main

Fixes #71709.
Co-authored-by: openperf <16864032@qq.com>
2026-05-31 14:09:03 +01:00
Peter Steinberger
17c8602a9c docs: require issue summaries in agent replies 2026-05-31 14:07:08 +01:00
Peter Steinberger
3ca4e5f616 docs: clarify agent workflow rules 2026-05-31 14:06:22 +01:00
Peter Steinberger
7423e9cb66 refactor(openai): confine legacy codex repair to doctor
Confine retired OpenAI Codex identifiers to doctor repair and migration paths while keeping runtime OpenAI surfaces canonical.\n\nProof: focused Vitest; autoreview clean; AWS Crabbox check:changed run_3789cbe12413 (cbx_2c88b700810b) passed.
2026-05-31 14:03:17 +01:00
tynamite
2f7e6ec196 fix(auto-reply): honor per-model thinking params
Auto-reply now uses the existing per-model model params thinking value before falling back to the global thinkingDefault, matching gateway/shared model selection behavior.\n\nVerified with targeted auto-reply and agents Vitest coverage plus formatting and diff checks.\n\nThanks @tynamite for the fix.
2026-05-31 14:01:25 +01:00
Peter Steinberger
b222b5f6fa refactor(cron): keep legacy notify migration in doctor 2026-05-31 14:00:47 +01:00
Peter Steinberger
2fe019ccae fix(exec): allow predicate shell builtins in allowlist mode 2026-05-31 14:00:12 +01:00
Peter Steinberger
657a668d94 test(voice-call): drive Twilio stream failure timers 2026-05-31 13:59:48 +01:00
Peter Steinberger
c797f02ff7 fix(diagnostics): surface Bonjour state in support exports 2026-05-31 13:57:17 +01:00
Peter Steinberger
32c0279cec perf(cli): narrow gateway dispatch startup 2026-05-31 13:56:27 +01:00
Peter Steinberger
44512b5297 docs: tighten refactor storage policy 2026-05-31 13:51:43 +01:00
Peter Steinberger
f1fc204f5c docs: require PR review transparency 2026-05-31 08:50:47 -04:00
Peter Steinberger
c8f7e9102b docs: clarify runtime migration boundary 2026-05-31 13:42:59 +01:00
Peter Steinberger
cf315ddef6 fix(agents): preserve reasoning replay from model metadata
Preserve OpenAI-compatible replay reasoning when the selected custom or self-hosted model already has reasoning metadata enabled.

The transcript policy now treats existing model metadata as the replay contract instead of requiring a new provider config knob, and the OpenAI-compatible serializer preserves reasoning_content for those routes while keeping stock OpenAI, Gemma 4, and known non-replayable OpenRouter safeguards.

Fixes #88068.
Replaces #88071.
2026-05-31 13:41:44 +01:00
Peter Steinberger
7a22515972 test(release): harden beta validation gates 2026-05-31 13:39:48 +01:00
kinjitakabe
fee4e52f22 fix(exec): allow known safe shell builtins in allowlist mode
Treat pathless POSIX shell builtins (`:`, `cd`, `false`, `pwd`, `true`) as internally safe only during shell allowlist evaluation. This avoids approval prompts for chains like `cd /tmp && git status` when the executable segment is already allowlisted, without adding a `tools.exec.safeBuiltins` config knob.

Environment-mutating builtins (`export`, `unset`), code-evaluating builtins (`eval`, `source`, `.`), unknown commands, and direct argv execution remain approval-gated unless separately allowlisted.

Proof: `pnpm test src/infra/exec-safe-builtins.test.ts src/agents/bash-tools.exec.security-floor.test.ts -- --reporter=verbose`; `pnpm changed:lanes --json`; `pnpm check:no-conflict-markers`; `git diff --check origin/main...HEAD`. CI related failures were resolved on the final SHA; remaining `checks-node-core-runtime-media-ui` failure is unrelated to this PR.

Fixes #46056.
Thanks @kinjitakabe.

Co-authored-by: kevinkang-ai <273844887+kevinkang-ai@users.noreply.github.com>
2026-05-31 13:39:13 +01:00
Peter Steinberger
ca166a85d4 docs: explain per-agent model params 2026-05-31 13:38:17 +01:00
Peter Steinberger
e5c61383e5 refactor: move plugin state stores to sqlite (#88609) 2026-05-31 13:37:11 +01:00
Peter Steinberger
fd88f34a8f fix: preserve discord policy close narrowing 2026-05-31 13:28:53 +01:00
Peter Steinberger
1e54e908e2 fix: queue subagent completion handoffs (#88613) 2026-05-31 13:25:23 +01:00
Peter Steinberger
729712d194 docs(codex): clarify first-party plugin marketplaces 2026-05-31 13:22:00 +01:00
Peter Steinberger
97a97aded7 docs: tighten env surface policy 2026-05-31 13:21:12 +01:00
Peter Steinberger
2e254005a0 docs: tighten config surface policy 2026-05-31 13:14:53 +01:00
Peter Steinberger
703fae16a9 fix(devices): refresh paired device last-seen metadata
Refresh paired-device last-seen metadata on successful device-token auth, paired reconnect, and first silent auto-approved connect.

Centralize approved paired-device record construction so normal and bootstrap approvals preserve existing last-seen state unless the gateway passes explicit access metadata.

Fixes #81169.
Supersedes #81189.

Proof:
- node scripts/run-vitest.mjs src/infra/device-pairing.test.ts --reporter=verbose
- node scripts/run-vitest.mjs src/gateway/server.auth.control-ui.test.ts --reporter=verbose
- git diff --check
- pnpm exec oxfmt --check --threads=1 src/infra/device-pairing.ts src/infra/device-pairing.test.ts src/gateway/server/ws-connection/message-handler.ts src/gateway/server.auth.control-ui.suite.ts
- pnpm check:changed passed before final rebase; post-rebase rerun blocked before checks by local Crabbox 0.21.0 needing >=0.22.0
- autoreview clean: .agents/skills/autoreview/scripts/autoreview --mode branch --base origin/main

Known unrelated CI failure on latest origin/main/PR base: extensions/discord/src/monitor/gateway-plugin.ts TS2367 in check-prod-types/check-lint/check-test-types/extension-channel checks.

Co-authored-by: vyctorbrzezowski <krzyszchweski@gmail.com>
2026-05-31 13:12:55 +01:00
clawsweeper[bot]
fdf8dddf0a fix(agents): classify expired thinking signatures (#88340)
Summary:
- The branch adds thinking-signature replay-invalid classification, retries matching terminal stream-error eve ... output, preserves static fallback model params, and updates related tests including a Copilot hook fixture.
- PR surface: Source +57, Tests +177. Total +234 across 6 files.
- Reproducibility: yes. for the classifier boundary: current main lacks a thinking-signature replay-invalid ma ... ort supplies the exact provider error payload. The time-dependent live expiry path was not reproduced here.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(agents): classify expired thinking signatures
- PR branch already contained follow-up commit before automerge: fix(agents): recover thinking signature stream errors
- PR branch already contained follow-up commit before automerge: fix(agents): recover expired thinking signatures
- PR branch already contained follow-up commit before automerge: fix(clawsweeper): address review for automerge-openclaw-openclaw-8807…

Validation:
- ClawSweeper review passed for head b65f2b8bda.
- Required merge gates passed before the squash merge.

Prepared head SHA: b65f2b8bda
Review: https://github.com/openclaw/openclaw/pull/88340#issuecomment-4582955790

Co-authored-by: Bryan Tegomoh <bryan.tegomoh@gmail.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-31 12:11:30 +00:00
FMLS
3a88142ddd fix(browser): document stable tab references (#88393)
Summary:
- The branch documents friendly browser tab references across docs, the browser skill, CLI help, and tool schema descriptions, and adds tests for target reference resolution and tab alias behavior.
- PR surface: Source +24, Tests +328, Docs +9. Total +361 across 21 files.
- Reproducibility: yes. for the documentation mismatch by source inspection: current main supports friendly ta ... schema/help surfaces still emphasize raw CDP target ids. Runtime behavior itself is not a new failing path.

Automerge notes:
- PR branch already contained follow-up commit before automerge: refactor(browser): share tab reference CLI help

Validation:
- ClawSweeper review passed for head 118af80b0b.
- Required merge gates passed before the squash merge.

Prepared head SHA: 118af80b0b
Review: https://github.com/openclaw/openclaw/pull/88393#issuecomment-4583558133

Co-authored-by: FMLS <kfliuyang@gmail.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: hxy91819
Co-authored-by: hxy91819 <8814856+hxy91819@users.noreply.github.com>
2026-05-31 12:09:50 +00:00
Peter Steinberger
94b1427fdf fix(discord): log gateway websocket close details 2026-05-31 13:03:23 +01:00
Peter Steinberger
f83886c12d chore(lint): trim remaining suppressions 2026-05-31 13:01:19 +01:00
Peter Steinberger
63c6252389 test(release): stabilize beta validation after rebase 2026-05-31 13:00:09 +01:00
Ayaan Zaidi
4de9b79d30 refactor(agents): simplify stale cli retry cleanup 2026-05-31 17:28:05 +05:30
brokemac79
afe9826fc1 Stabilize lint suppression guard in CI 2026-05-31 17:28:05 +05:30
brokemac79
0b02148656 Fix stale CLI retry CI contracts 2026-05-31 17:28:05 +05:30
brokemac79
e8c7c933f8 Retry stale CLI sessions in runner lifecycle 2026-05-31 17:28:05 +05:30
Peter Steinberger
00d17e9df7 refactor: make OpenAI Codex legacy doctor-only (#88605) 2026-05-31 12:58:01 +01:00
Vincent Koc
5976f14832 docs(skills): full rewrite of skills section with Mintlify components
Rewrites all skills documentation pages with rich Mintlify components
(Steps, CardGroup, AccordionGroup, ParamField, Note, Warning, Tip) and
code-verified accuracy throughout.

- tools/skills.md: CardGroup quick-nav, verified precedence table from
  workspace.ts, Security accordions, Steps for env injection, token
  impact formula, Related CardGroup
- tools/creating-skills.md: Steps walkthrough, gating accordion,
  propose-update command (was missing), Best practices Tip, ClawHub
  publish flow, Related CardGroup
- tools/skills-config.md: ParamField for every config key, agent
  allowlist section, Workshop config, sandbox Warning
- tools/slash-commands.md: CardGroup for 3 command types, command tables
  in AccordionGroup sections, ParamFields for all config keys, dedicated
  sections for /tools /model /config /mcp /debug /plugins /trace /btw
- prose.md: Steps for install, CardGroup quick-nav, AccordionGroup for
  state backends, runtime mapping table

docs.json: adds skill-workshop nav entry and redirects
(/skill-workshop, /tools/skills-workshop -> /tools/skill-workshop)
2026-05-31 12:57:16 +01:00
Peter Steinberger
242eab9d20 fix(media): use typed auth for no-auth media providers 2026-05-31 12:56:38 +01:00
WhatsSkiLL
f59113cfd3 fix(gateway): avoid restarts for auth cooldown reloads
Fixes #88443.

Cooldown-only edits under auth.cooldowns now hot reload the active runtime config instead of scheduling a gateway restart. This avoids dropping active gateway work while preserving restart-required behavior for gateway.auth.* credential changes.

Verification:
- pnpm test src/gateway/config-reload.test.ts -- --reporter=verbose
- env -u OPENCLAW_TESTBOX pnpm check:changed
- .agents/skills/autoreview/scripts/autoreview --mode branch --base origin/main --prompt 'Review PR 88474 after rebase. Focus on whether auth.cooldowns hot reload correctly refreshes active runtime config without weakening gateway auth/token restart behavior. Treat no-op vs hot reload semantics as central.'
- GitHub CI, Real behavior proof, CodeQL, Dependency Guard, OpenGrep PR Diff, and Workflow Sanity passed on 51232ff66c.

Thanks @IWhatsskill.
2026-05-31 12:54:19 +01:00
Peter Steinberger
fde87f475f perf(cli): defer shell env for gateway dispatch 2026-05-31 12:42:35 +01:00
Vincent Koc
823c38a1f9 fix(e2e): keep plugin binding escape smoke focused 2026-05-31 13:37:41 +02:00
Sally O'Malley
1cb5a57631 fix: transient banner showing lastError leak into page headers (#88463)
Signed-off-by: sallyom <somalley@redhat.com>
2026-05-31 07:33:58 -04:00
Sally O'Malley
615f71a88f fix(gateway): guide dashboard auth after service repair (#88466)
Signed-off-by: sallyom <somalley@redhat.com>
2026-05-31 07:31:44 -04:00
Peter Steinberger
899dc5f248 fix(memory): retry transient embedding failures
Retry live query embeddings on transient provider transport failures and split eligible batch embedding socket failures after bounded retries.

Fixes #71784
Fixes #44166
Supersedes #44167

Co-authored-by: MrGeDiao <MrGeDiao@users.noreply.github.com>
2026-05-31 12:30:26 +01:00
stain lu
95b2f9c6f9 fix(boot): suppress fallback BOOT.md echoes
Suppress BOOT.md/internal-runtime-context echoes in fallback boot sends.

Wrap boot prompts as internal runtime context, track the active boot prompt during boot runs, and sanitize message-tool visible payloads before dispatch so fallback models cannot deliver copied BOOT.md instructions or leak them through raw-params errors. Preserves media/presentation sends that still contain non-text payload content after sanitization.

Fixes #53732.

Co-authored-by: stainlu <stainlu@newtype-ai.org>
2026-05-31 12:25:41 +01:00
sqsge
a76db8cff3 fix(media): allow explicit synthetic auth for media providers
Allow media understanding providers to opt into synthetic non-secret auth for local or self-hosted no-auth audio/video execution.

This preserves configured env/profile/literal provider credentials first, keeps explicit profile failures hard-fail, and leaves unmarked remote providers fail-closed.

Fixes #74644.
2026-05-31 12:20:50 +01:00
Peter Steinberger
9f5c981f9f perf: speed up chat hydration and add 3d workboard 2026-05-31 12:18:08 +01:00
Peter Steinberger
2bd07eead7 Refactor cron SQLite runtime paths (#88582)
* refactor: clean cron sqlite runtime paths

* fix: preserve legacy cron sqlite delivery migration

* fix: keep legacy cron notify fallback for invalid webhooks

* test: handle packaged lint suppression files

* fix: keep invalid cron notify migrations retryable

* test: fix ui timer lint
2026-05-31 12:14:48 +01:00
Peter Steinberger
3525a965ed test(release): stabilize beta validation lanes 2026-05-31 12:09:49 +01:00
WhatsSkiLL
22b8e1cf4f fix(plugins): scope startup metadata manifest reads
Limit plugin metadata snapshots to the channel, provider, and startup surfaces that need them, while preserving unscoped fallback for incomplete index data and provider runtime resolution.

Refs #70533.
Refs #84628.

Co-authored-by: IWhatsskill <IWhatsskill@users.noreply.github.com>
2026-05-31 11:58:56 +01:00
Peter Steinberger
1e08af453a fix(sms): add Twilio webhook diagnostics
* fix(sms): diagnose Twilio webhook setup

* test(sms): satisfy diagnostic lint gates

* fix(sms): redact recent probe participants

* docs(sms): refresh SecretRef credential matrix

* fix(sms): probe Messaging Service webhooks

* fix(sms): resolve env-backed SecretRefs
2026-05-31 11:44:39 +01:00
Vincent Koc
6d76acc258 fix(test): repair e2e standalone regressions 2026-05-31 12:42:17 +02:00
kinjitakabe
f7a1d3f3f6 fix(model-auth): resolve per-entry apiKey profile references
Fixes #67423.

Resolve provider-entry apiKey fields that intentionally reference model auth profiles through centralized binding logic, so runtime auth and status labeling agree. Preserve env-first precedence, SecretRef handling, provider/baseUrl compatibility checks, and model auth-mode guards.

Verification:
- node scripts/run-vitest.mjs src/agents/model-auth.profiles.test.ts src/agents/model-auth-label.test.ts
- PATH=/tmp/openclaw-corepack-shim.XXXXXX:$PATH CI=true pnpm check:changed
- .agents/skills/autoreview/scripts/autoreview --mode branch --base origin/main
- GitHub CI run 26710260760 and related CodeQL/proof checks on f55dec154d

Co-authored-by: kinjitakabe <273844887+kinjitakabe@users.noreply.github.com>
2026-05-31 11:39:55 +01:00
Peter Steinberger
7d8fdef995 ci(release): run npm preflight on larger runner 2026-05-31 11:37:04 +01:00
Peter Steinberger
9dc4c9ec2e fix: expose Feishu tools for named accounts 2026-05-31 11:36:48 +01:00
Peter Steinberger
77f1359612 refactor: extract media and ACP core packages (#88534)
* refactor: extract media and acp core packages

* refactor: remove relocated media and acp sources

* build: wire new core packages into dependency checks

* test: alias new core packages in vitest

* build: keep media sniffer runtime dependency

* docs: refresh plugin sdk api baseline

* fix: keep normalized proposal queries non-empty

* test: keep channel timer tests isolated

* fix: keep rebased plugin checks green

* fix: preserve sms numeric allowlist entries

* test: harden exec foreground timeout failure

* test: remove duplicate skill workshop assertion

* fix: remove channel config lint suppression

* test: refresh lint suppression allowlist
2026-05-31 11:30:33 +01:00
stain lu
4b1e5b7943 fix(cli): stabilize claude auth epochs on token rotation
Stabilizes Claude CLI reusable sessions when Claude token rotation causes transient token-shaped credential reads. Local Claude CLI OAuth and token credential encodings now share the same identity-only auth-epoch, while ref-backed token auth profiles ignore refreshed token material and plaintext token profiles remain epoch-sensitive on manual token replacement.

Fixes #74312.

Proof: focused local Vitest, autoreview, Testbox-through-Crabbox tbx_01ksyrcknbt743x32x6k1s95qw, and GitHub CI run 26709864094 all passed.

Co-authored-by: stainlu <stainlu@newtype-ai.org>
2026-05-31 11:19:42 +01:00
Ted Li
92b6af76d9 fix(reply): deliver plugin binding replies
Deliver plugin-owned bound-thread replies even when the source room is configured for `message_tool` visible replies. Normal agent final text still stays private unless the agent calls `message(action=send)`.

Document the distinction in the group/channel docs and root routing policy, and keep ambient room-event plus unauthorized text-slash suppression covered by regression tests.

Fixes #87721.
2026-05-31 11:17:45 +01:00
Peter Steinberger
53a9f13cf4 chore(lint): reduce lint suppressions 2026-05-31 11:17:16 +01:00
Firas Alswihry
b2f71db7bb feat(dreaming): add report-only shadow trial runner
Adds a report-only memory-core dreaming shadow-trial runner that writes inspectable artifacts without mutating durable memory. The public helper now stores default reports under daily directories with opaque content-hash filenames, so multiple same-day trials coexist without leaking candidate text into paths.

Verification:
- OPENCLAW_VITEST_MAX_WORKERS=1 node scripts/run-vitest.mjs run --config test/vitest/vitest.extension-memory.config.ts extensions/memory-core/src/dreaming-shadow-trial.test.ts --reporter=verbose --maxWorkers=1
- git diff --check
- pnpm exec oxfmt --check extensions/memory-core/src/dreaming-shadow-trial.ts extensions/memory-core/src/dreaming-shadow-trial.test.ts
- pnpm tsgo:extensions
- autoreview clean: no accepted/actionable findings
- GitHub CI run 26709794635 passed
- Real behavior proof run 26709798698 passed
- Dependency Guard run 26709794113 passed

Co-authored-by: Firas Alswihry <itzfiras@gmail.com>
2026-05-31 11:16:33 +01:00
Peter Steinberger
6fb1f386c6 perf(cli): slim agent command registration 2026-05-31 11:14:26 +01:00
Peter Steinberger
ae4ab2a41f refactor(logging): share stuck recovery session refs 2026-05-31 11:10:06 +01:00
Soham Patankar
4f3d8a57dd fix(codex): accept first-party OpenAI plugin marketplaces
Allow Codex native plugin config to target first-party OpenAI marketplaces, including openai-curated, openai-bundled, and openai-primary-runtime.

Fixes #82216.
Thanks @yaanfpv for the contribution.

Verification:
- node scripts/run-vitest.mjs test/scripts/lint-suppressions.test.ts
- pnpm build:ci-artifacts
- OPENCLAW_VITEST_MAX_WORKERS=2 node scripts/run-vitest.mjs run --config test/vitest/vitest.full-core-support-boundary.config.ts test/scripts/lint-suppressions.test.ts
- node scripts/run-vitest.mjs extensions/codex/src/app-server/config.test.ts extensions/codex/src/app-server/plugin-activation.test.ts extensions/codex/src/app-server/session-binding.test.ts extensions/codex/src/migration/provider.test.ts extensions/sms/src/channel.test.ts extensions/sms/src/inbound.test.ts
- git diff --check
- ./.agents/skills/autoreview/scripts/autoreview --mode local
- GitHub PR CI on head 896640060b, including build-artifacts run 26709647050
2026-05-31 11:08:42 +01:00
Ayaan Zaidi
f454d6202f fix(agents): preserve explicit active run aborts 2026-05-31 15:31:48 +05:30
Ayaan Zaidi
1556e3c68c fix(agents): surface internal abort incomplete turns 2026-05-31 15:31:48 +05:30
Ayaan Zaidi
a4d3add6da fix(agents): classify internal aborts as non-deliverable 2026-05-31 15:31:48 +05:30
Feelw00
b4cdc33fc9 fix(logging): align diagnostic recovery dedup keys
Align diagnostic stuck-session recovery in-flight dedup with the runtime recovery key. The coordinator now dedups by logical session ref only, so a mid-flight generation bump cannot emit a phantom `session.recovery.requested` event that runtime recovery skips as already in flight.

Adds a regression test for the idle-queued stall path where a queued message bumps generation while recovery is pending.

Fixes #88010
2026-05-31 11:00:42 +01:00
Chinar Amrutkar
c2c20a0b0d fix(ui): pair sequential tool results by fallback order
Fixes #70746 by pairing nameless same-name tool results with the earliest unmatched Control UI tool card while preserving exact ID matches. Empty fallback results now count as consumed, so later results do not overwrite the first card.

Focused regression coverage covers sequential same-name calls and empty-result fallback pairing. Thanks @chinar-amrutkar.

Co-authored-by: Chinar Amrutkar <chinar.amrutkar@gmail.com>
2026-05-31 11:00:00 +01:00
Vincent Koc
a753e6bc86 fix(test): extend e2e vitest watchdog 2026-05-31 11:50:18 +02:00
tanshanshan
425a4ab2f2 chore(lint): enable object-shorthand (#81808)
* fix: narrow current-main core type guards

* fix: preserve query and test guard narrowing

* fix(copilot): align client options with sdk rename

* test(sms): type fetch mocks

* fix(sms): preserve numeric allowlist entries

* test(sms): preserve pairing send count assertion

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-05-31 10:46:10 +01:00
Peter Steinberger
724160b7eb docs: clarify package guard policy 2026-05-31 10:45:28 +01:00
Peter Steinberger
6699e7331a docs: document scoped mention patterns
## Summary

- Document scoped configured mention-pattern policy on the Groups page, including allow/deny mode semantics, supported conversation IDs, account-level precedence, and native-mention behavior.
- Add config UI help for `mentionPatterns.mode`, `allowIn`, and `denyIn` on Discord, Matrix, Slack, Telegram, and WhatsApp.
- Regenerate channel config/docs/plugin SDK metadata baselines for the new hint copy.

Refs #70864.

## Verification

- git diff --check
- pnpm format:docs:check
- pnpm docs:check-mdx
- pnpm docs:check-links
- pnpm config:channels:check
- pnpm config:docs:check
- pnpm plugin-sdk:api:check
- node scripts/run-vitest.mjs src/config/schema.hints.test.ts
- .agents/skills/autoreview/scripts/autoreview --mode local

## Real behavior proof

Behavior addressed: Documentation and config UI metadata for scoped configured mention-pattern policy.
Real environment tested: Local OpenClaw checkout on macOS.
Exact steps or command run after this patch: The verification commands listed above.
Evidence after fix: Docs formatting, MDX, link audit, generated config/channel/API baselines, and config hint tests passed; autoreview reported no accepted/actionable findings.
Observed result after fix: The Groups page now explains how to scope `messages.groupChat.mentionPatterns` with `channels.<channel>.mentionPatterns`, and config metadata exposes field help for the supported channels.
What was not tested: Live Discord, Matrix, Slack, Telegram, or WhatsApp inbound messages; this PR is documentation/config metadata only and follows the already-landed runtime behavior from #70864.
2026-05-31 10:44:20 +01:00
Vincent Koc
b0625bdd1c fix(agents): strip malformed arg-value suffixes
Strip malformed terminal `</arg_value>>` suffixes from selected agent read/path and exec routing arguments before validation.

This keeps valid literal `</arg_value>` text intact, preserves payload fields such as write content and edit replacements, and prevents read/exec failures caused by malformed tool XML suffixes.

Fixes #48780.
Thanks @vincentkoc for the original fix.

Verification:
- `node scripts/run-vitest.mjs src/agents/agent-tools.params.test.ts src/agents/agent-tools.read.arg-value-suffix.test.ts src/agents/agent-tools.read.workspace-root-guard.test.ts src/agents/agent-tools.workspace-only-false.test.ts src/agents/bash-tools.exec.path.test.ts src/agents/bash-tools.exec-foreground-failures.test.ts`
- `node_modules/.bin/oxfmt --check src/agents/agent-tools.params.ts src/agents/agent-tools.params.test.ts src/agents/bash-tools.exec.path.test.ts`
- `node scripts/run-oxlint.mjs src/agents/agent-tools.params.ts src/agents/agent-tools.params.test.ts src/agents/bash-tools.exec.path.test.ts`
- `pnpm check:test-types`
- `.agents/skills/autoreview/scripts/autoreview --mode branch --base origin/main`
- GitHub Actions green on PR head `f1d8026352`.

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-05-31 10:44:12 +01:00
stain lu
4ca22b95bc test(plugins): cover Link agent wallet bundle shape (#75181)
* test(plugins): cover Link agent wallet bundle shape

* test(plugins): add bundle fixture helpers

* test(plugins): align Link manifest fixture expectation

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-05-31 10:43:12 +01:00
Peter Steinberger
3950605561 chore(lint): tighten lint exception coverage 2026-05-31 10:42:59 +01:00
4552 changed files with 165779 additions and 56729 deletions

View File

@@ -307,8 +307,7 @@ Live-provider debug template for direct AWS/Hetzner leases:
```sh
mkdir -p .crabbox/logs
CRABBOX_ENV_ALLOW=OPENAI_API_KEY,OPENAI_BASE_URL \
pnpm crabbox:run -- --provider aws \
pnpm crabbox:run -- --provider aws \
--preflight \
--allow-env OPENAI_API_KEY,OPENAI_BASE_URL \
--timing-json \
@@ -320,8 +319,10 @@ CRABBOX_ENV_ALLOW=OPENAI_API_KEY,OPENAI_BASE_URL \
```
Do not pass `--capture-*`, `--download`, `--checksum`, `--force-sync-large`, or
`--sync-only` to delegated providers. Crabbox rejects them because the provider
owns sync or command transport.
`--sync-only` to delegated providers. Also do not pass `--script*`,
`--fresh-pr`, `--full-resync`, or `--env-helper` there. Crabbox rejects these
because the provider owns sync or command transport. `--keep-on-failure` is OK
for delegated one-shots when you need to inspect a failed lease.
## Efficient Bug E2E Verification

View File

@@ -1,202 +0,0 @@
---
name: kysely-database-access
description: Use when adding, reviewing, or refactoring OpenClaw Kysely database access, native node:sqlite stores, generated DB types, SQLite schemas, migrations, raw SQL, transactions, or database access best practices.
---
# Kysely Database Access
Use this skill for OpenClaw database code that touches Kysely, `node:sqlite`,
generated DB types, SQLite schemas, migrations, or store/query design.
## Read First
- `docs/concepts/kysely.md` for the repo's Kysely rules and examples.
- The owning subtree `AGENTS.md`, if present.
- Relevant local Kysely source/types under `node_modules/kysely/dist/esm/...`
before assuming dialect behavior, result types, transactions, plugins, or raw
SQL semantics.
- For codegen behavior, inspect `scripts/generate-kysely-types.mjs` and
`kysely-codegen --help` from the repo package manager.
## Official Docs Cross-Check
When the behavior matters, verify against current Kysely docs/source before
patching:
- Generating types: production apps should keep schema types aligned with the
database through code generation.
- Data types: TypeScript types do not affect runtime values; the driver decides
runtime values, and Kysely returns what the driver returns unless a plugin
transforms results.
- Raw SQL: the `sql` tag can execute full raw SQL and embed snippets into
builders. Prefer typed builders/helpers when they express the same thing.
- Reusable helpers: take `Expression<T>` or an `ExpressionBuilder` when wrapping
SQL expressions; alias helper expressions explicitly in `select`. Extract a
helper only when it quarantines raw SQL, removes meaningful duplication, or
preserves a tricky inferred type.
- Split build/execute only at deliberate boundaries. Compiled-query execution
is useful for native sync adapters, but keep plugin/result-transform behavior
in mind.
- Migrations: Kysely migration files run without a schema type. In OpenClaw,
prefer the committed SQL-source-of-truth path unless a new owner explicitly
needs Kysely-managed migrations.
- Plugins: plugins can transform queries and results. Any sync shortcut that
bypasses Kysely's async executor needs a documented invariant or tests.
## Default Workflow
1. Identify the owner boundary:
- Core state DB: `src/state/*`
- Per-agent DB: `src/state/openclaw-agent-*`
- Feature store: owning `*.sqlite.ts` module
- Plugin-owned state: plugin/module owner, not generic core
2. Inspect the schema source first:
- `*.sql` is the source of truth when generated schema/types exist.
- Generated `*.generated.*` files are outputs, not hand-edit targets.
3. Prefer Kysely builders for normal CRUD:
- `selectFrom`, `insertInto`, `updateTable`, `deleteFrom`
- `executeTakeFirst`, `executeTakeFirstOrThrow`, `execute`
- `eb.fn.countAll`, `eb.fn.count`, `eb.fn.coalesce` for common functions
- Keep compile-time Kysely reference literals such as `"host"` and
`"flow_id as flowId"` when they are clearer than constants; they are
type-checked by Kysely.
- Let Kysely infer selected row shapes. Do not pass broad row generics to
sync helpers for normal builder queries.
- Treat `executeSqliteQuerySync<Row>(db, builder)` and
`executeSqliteQueryTakeFirstSync<Row>(db, builder)` as a smell: the generic
can lie about selected columns. Use no generic for builders; use an exact
raw boundary helper for raw SQL.
- For finite public query presets, use a preset-to-row type map plus a union
boundary type instead of `Record<string, ...>`.
- After touching Kysely/native SQLite code, run `pnpm lint:kysely`. The AST
guard rejects raw identifier helpers, unreviewed typed `sql<T>` snippets,
`db.dynamic`, explicit sync-helper row generics for builders, and new raw
`node:sqlite` runtime access outside owner allowlists. It also rejects
persisted enum-like casts in SQLite stores; keep row fields as `string` and
parse through closed validators.
4. Keep raw SQL deliberate:
- Good: pragmas, virtual tables, FTS, SQLite JSON functions, migrations,
`sqlite_master`, compact repeated expressions.
- Bad: raw `COUNT(*)` or dynamic SQL where Kysely has a typed builder shape.
- Use `${value}` parameters; use `sql.ref` / `sql.table` only for validated,
closed-set identifiers.
- Do not feed unconstrained runtime `string` values into table/column/group/
order/identifier positions. Narrow them to local unions or generated table
keys first.
- Prefer `eb.fn`, `eb.lit`, `eb.ref`, and expression callbacks for scalar
SQL such as `count`, `coalesce`, `max`, `exists`, and constant selections.
5. Align TypeScript with real driver values:
- Kysely does not coerce runtime values.
- Native `node:sqlite` returns BLOB columns as `Uint8Array`; convert with
`Buffer.from(...)` only at API boundaries that need Buffer helpers.
- Keep JSON/text/timestamp parsing at module boundaries.
- Keep persisted enum-like strings as `string` in row types, then parse them
through closed validator helpers such as `parseTaskStatus(value)`. Do not
cast corrupt persisted data into exported unions.
6. Decide migration need from shipped state:
- Unshipped schema/type cleanup: no SQLite migration.
- Shipped canonical schema change: add the appropriate migration or
doctor/fix repair path with tests.
- Legacy config repair belongs in doctor/fix paths, not startup surprises.
## Codegen
For committed SQL-backed generated types:
```bash
pnpm db:kysely:gen
pnpm db:kysely:check
```
The repo maps SQLite `blob` to `Uint8Array` through `kysely-codegen`
`--type-mapping`. Do not post-process generated files by hand; change the
generator or SQL source and regenerate.
## Native SQLite Guardrails
- Use `getNodeSqliteKysely(db)` and sync helpers from `src/infra/kysely-sync.ts`
for `DatabaseSync` stores.
- New direct `db.prepare(...)` / `db.exec(...)` runtime access should be rare.
Prefer Kysely or add an explicit `scripts/check-kysely-guardrails.mjs`
allowlist entry with a clear owner reason.
- If raw SQLite is repeated or cast-heavy, extract a narrow boundary helper
such as `assertSqliteIntegrityOk(db, message)` and allowlist that helper
instead of each caller.
- Keep sync helper result types derived from `CompiledQuery<Row>` / Kysely
builders. Explicit helper generics are for raw SQL or external boundaries,
not for widening a typed builder result into a generic record.
- Keep the native dialect in `src/infra/kysely-node-sqlite.ts` aligned with
Kysely's SQLite driver structure: single connection, mutex, SQLite adapter,
SQLite query compiler, SQLite introspector.
- Use `StatementSync.columns().length` behavior for row-returning statements;
do not parse SQL verbs.
- Return `insertId` only for changed Kysely insert nodes. Raw insert SQL and
ignored inserts must not expose stale `lastInsertRowid`.
- Remember that sync execution compiles through Kysely but bypasses async
`executeQuery` result plugins/logging. If plugins enter this path, add tests
or a documented invariant.
## Tests
Pick the smallest proof that covers the touched surface:
```bash
pnpm db:kysely:check
pnpm lint:kysely
pnpm test src/infra/kysely-node-sqlite.test.ts
pnpm test <owning-store>.test.ts
pnpm tsgo:core
```
Add or update focused tests for:
- generated type/runtime mismatches
- native dialect metadata (`insertId`, `numAffectedRows`, row-returning SQL)
- transactions/savepoints
- BLOB and JSON boundary conversions
- schema/codegen drift
- type inference contracts for sync helpers and public query result maps
- negative type contracts with `@ts-expect-error` for important column/preset
mistakes
- corruption-path tests that mutate SQLite directly and assert the public load
or read method rejects invalid persisted strings
- public store behavior, not just private SQL shape
## Helper Extraction
Good helpers:
- `readSqliteNumberPragma(db, pragma)` style helpers with a closed union for
PRAGMA names.
- Raw-expression helpers that accept Kysely expressions/refs instead of raw
column strings.
- Public query preset maps that preserve exact row types at the API boundary.
Avoid helpers that:
- Wrap obvious Kysely literals just to avoid strings.
- Take generic `string` table/column/order names.
- Return heavily generic query builders that are harder to type than the query
they hide.
## Performance
- Benchmark prepare/compile overhead before adding statement caches or compiled
query caches. Include the real public store method work: SQLite execution,
JSON/BLOB conversion, and result mapping.
- Keep caches local, close/dispose them with the owning store, and test invalid
or stale behavior. Clear builders are the default until numbers prove a hot
path.
## Avoid
- Do not introduce ORM/repository layers or hidden relation loading.
- Do not make root dependencies for plugin-only database needs.
- Do not migrate everything to raw SQL or everything to builders for purity.
- Do not hand-edit generated DB types.
- Do not hide finite query result shapes behind `Record<string, ...>` just to
make JSON output convenient; use exact row unions or map at the boundary.
- Do not replace every Kysely string literal with constants for aesthetics; fix
dynamic identifiers, raw SQL assertions, and public result boundaries instead.
- Do not add broad cache layers to hide repeated query/discovery work; carry the
known runtime fact earlier when possible.

View File

@@ -187,11 +187,37 @@ gh pr view <number> --json additions,deletions,changedFiles \
## Read beyond the diff
- Review the surrounding code path, not just changed lines. Open the caller, callee, data contracts, adjacent tests, and owner module.
- Before any verdict, read enough code to fill this map: changed surface, runtime entry point, owner boundary, one caller, one callee, sibling implementations sharing the invariant, adjacent tests, current `main` behavior, and shipped/dependency/Codex contracts when relevant.
- For large-codebase PRs, sample enough related files to understand the runtime boundary before deciding. Default to more code reading when the change touches agents, gateway, plugins, auth, sessions, process, config, or provider/runtime seams.
- Compare the PR against current `origin/main` behavior. Check whether recent main already changed the same surface.
- Dependency-backed behavior: MUST read upstream docs/source/types before judging API use, defaults, output shapes, errors, timeouts, memory behavior, or compatibility. Do not assume dependency contracts from memory or PR text.
- Judge solution quality, not only correctness. Ask whether the PR is the clean owner-boundary fix or a wart/workaround that should be replaced by a small refactor, moved seam, contract change, or deletion of duplicate logic.
- Mention the main files read when the verdict depends on code-path evidence.
- If the user challenges the verdict or asks whether the idea is really good, resume code reading first. Do not defend, soften, or reverse the verdict until the missing caller/callee/sibling/dependency path is checked.
## Best-fix review loop
Every PR review must explicitly answer: "Is this the best fix, or only a plausible fix?"
Before verdict:
1. Reconstruct the bug, feature need, or behavior claim from issue/PR/proof.
2. Trace current behavior from entry point to failure or decision point.
3. Read touched files, callers, callees, owner modules, adjacent tests, and relevant docs.
4. Read sibling surfaces that should share the invariant or could be broken by a one-sided fix.
5. Compare against current `origin/main` and shipped behavior when regression/compat matters.
6. Inspect upstream dependency/Codex source or docs for dependency-backed behavior.
7. Identify at least one alternative fix location or shape, then reject it with evidence.
8. If any required path above is uninspected, keep reading or mark `Remaining uncertainty`; do not call the PR best, blocked, proof-sufficient, or merge-ready.
Review output must include:
- `Best-fix verdict:` best / acceptable mitigation / wrong layer / too narrow / too broad.
- `Alternatives considered:` 1-3 concrete alternatives and why rejected.
- `Code read:` compact list of main files/contracts checked.
- `Remaining uncertainty:` what was not proven.
If the best-fix answer is only "maybe", keep reading or state the missing evidence. Do not call proof sufficient until the best-fix judgment is explicit.
## Enforce the bug-fix evidence bar

View File

@@ -2,7 +2,7 @@
// Secret scanning alert handler for OpenClaw maintainers.
// Usage: node secret-scanning.mjs <command> [options]
import { execFileSync, spawnSync } from "node:child_process";
import { spawnSync } from "node:child_process";
import crypto from "node:crypto";
import fs from "node:fs";
import os from "node:os";
@@ -39,7 +39,9 @@ function gh(args, { json = true, allowFailure = false } = {}) {
stderr: proc.stderr,
};
}
if (!json) return proc.stdout;
if (!json) {
return proc.stdout;
}
try {
return JSON.parse(proc.stdout);
} catch {
@@ -70,7 +72,9 @@ export function loadBodyRedactionResult(locationType, resultFile) {
if (!resultFile) {
fail("Body notifications require a redaction result file from redact-body-if-needed");
}
if (!fs.existsSync(resultFile)) fail(`File not found: ${resultFile}`);
if (!fs.existsSync(resultFile)) {
fail(`File not found: ${resultFile}`);
}
const result = JSON.parse(fs.readFileSync(resultFile, "utf8"));
if (typeof result.notify_required !== "boolean") {
@@ -182,10 +186,11 @@ function fetchDiscussionComment(discussionNumber, discussionCommentDbId) {
failOnGraphQLFailure(gql, `Failed to fetch discussion #${discussionNumber}`);
const discussion = gql?.data?.repository?.discussion;
if (!discussion)
if (!discussion) {
fail(
`Discussion #${discussionNumber} not found — it may have been deleted. The alert cannot be processed via this skill.`,
);
}
discussionId = discussion.id;
@@ -205,15 +210,18 @@ function fetchDiscussionComment(discussionNumber, discussionCommentDbId) {
`Failed to fetch replies for discussion comment ${topLevelComment.id}`,
);
const replies = replyPage?.data?.node?.replies;
if (!replies)
if (!replies) {
fail(`Failed to paginate replies for discussion comment ${topLevelComment.id}`);
}
reply = findDiscussionCommentNode(replies.nodes, discussionCommentDbId);
hasMoreReplies = replies.pageInfo.hasNextPage;
replyCursor = replies.pageInfo.endCursor;
}
if (reply) return { discussionId, comment: reply };
if (reply) {
return { discussionId, comment: reply };
}
}
hasNextPage = discussion.comments.pageInfo.hasNextPage;
@@ -241,7 +249,9 @@ function createDiscussionComment(discussionNodeId, body, replyToNodeId) {
* Fetch alert metadata + locations. Never exposes .secret.
*/
function cmdFetchAlert(alertNumber) {
if (!alertNumber) fail("Usage: fetch-alert <number>");
if (!alertNumber) {
fail("Usage: fetch-alert <number>");
}
const alert = gh(["api", `repos/${REPO}/secret-scanning/alerts/${alertNumber}?hide_secret=true`]);
@@ -280,17 +290,23 @@ function cmdFetchAlert(alertNumber) {
* Saves full body to a temp file. Prints metadata + file path to stdout.
*/
function cmdFetchContent(locationJson) {
if (!locationJson) fail("Usage: fetch-content '<location-json>'");
if (!locationJson) {
fail("Usage: fetch-content '<location-json>'");
}
const location = JSON.parse(locationJson);
const type = location.type;
const details = location.details;
if (type === "discussion_comment") {
const commentUrl = details.discussion_comment_url;
if (!commentUrl) fail("No discussion_comment_url in location details");
if (!commentUrl) {
fail("No discussion_comment_url in location details");
}
const urlMatch = commentUrl.match(/discussions\/(\d+)#discussioncomment-(\d+)/);
if (!urlMatch) fail(`Cannot parse discussion comment URL: ${commentUrl}`);
if (!urlMatch) {
fail(`Cannot parse discussion comment URL: ${commentUrl}`);
}
const discussionNumber = urlMatch[1];
const discussionCommentDbId = urlMatch[2];
@@ -298,10 +314,11 @@ function cmdFetchContent(locationJson) {
discussionNumber,
discussionCommentDbId,
);
if (!comment)
if (!comment) {
fail(
`Discussion comment #${discussionCommentDbId} not found in discussion #${discussionNumber}`,
);
}
const bodyFile = tmpFile("body.md");
fs.writeFileSync(bodyFile, comment.body || "");
@@ -334,7 +351,9 @@ function cmdFetchContent(locationJson) {
details.issue_comment_url ||
details.pull_request_comment_url ||
details.pull_request_review_comment_url;
if (!commentUrl) fail(`No comment URL in location details`);
if (!commentUrl) {
fail(`No comment URL in location details`);
}
const comment = gh(["api", commentUrl]);
const bodyFile = tmpFile("body.md");
@@ -378,7 +397,9 @@ function cmdFetchContent(locationJson) {
);
} else if (type === "issue_body") {
const issueUrl = details.issue_body_url || details.issue_url;
if (!issueUrl) fail("No issue URL in location details");
if (!issueUrl) {
fail("No issue URL in location details");
}
const issue = gh(["api", issueUrl]);
const bodyFile = tmpFile("body.md");
@@ -414,7 +435,9 @@ function cmdFetchContent(locationJson) {
);
} else if (type === "pull_request_body") {
const prUrl = details.pull_request_body_url || details.pull_request_url;
if (!prUrl) fail("No PR URL in location details");
if (!prUrl) {
fail("No PR URL in location details");
}
const pr = gh(["api", prUrl]);
const bodyFile = tmpFile("body.md");
@@ -490,7 +513,9 @@ function cmdRedactBody(kind, number, bodyFile) {
if (!kind || !number || !bodyFile) {
fail("Usage: redact-body <issue|pr> <number> <redacted-body-file>");
}
if (!fs.existsSync(bodyFile)) fail(`File not found: ${bodyFile}`);
if (!fs.existsSync(bodyFile)) {
fail(`File not found: ${bodyFile}`);
}
const endpoint =
kind === "pr" ? `repos/${REPO}/pulls/${number}` : `repos/${REPO}/issues/${number}`;
@@ -509,8 +534,12 @@ function cmdRedactBodyIfNeeded(kind, number, currentBodyFile, redactedBodyFile,
"Usage: redact-body-if-needed <issue|pr> <number> <current-body-file> <redacted-body-file> <result-file>",
);
}
if (!fs.existsSync(currentBodyFile)) fail(`File not found: ${currentBodyFile}`);
if (!fs.existsSync(redactedBodyFile)) fail(`File not found: ${redactedBodyFile}`);
if (!fs.existsSync(currentBodyFile)) {
fail(`File not found: ${currentBodyFile}`);
}
if (!fs.existsSync(redactedBodyFile)) {
fail(`File not found: ${redactedBodyFile}`);
}
const currentBody = fs.readFileSync(currentBodyFile, "utf8");
const redactedBody = fs.readFileSync(redactedBodyFile, "utf8");
@@ -541,7 +570,9 @@ function cmdRedactBodyIfNeeded(kind, number, currentBodyFile, redactedBodyFile,
* Delete a comment (and all its edit history).
*/
function cmdDeleteComment(commentId) {
if (!commentId) fail("Usage: delete-comment <comment-id>");
if (!commentId) {
fail("Usage: delete-comment <comment-id>");
}
gh(["api", `repos/${REPO}/issues/comments/${commentId}`, "-X", "DELETE"], { json: false });
console.log(JSON.stringify({ ok: true, deleted_comment_id: Number(commentId) }));
}
@@ -551,7 +582,9 @@ function cmdDeleteComment(commentId) {
* Delete a discussion comment via GraphQL (and all its edit history).
*/
function cmdDeleteDiscussionComment(nodeId) {
if (!nodeId) fail("Usage: delete-discussion-comment <node-id>");
if (!nodeId) {
fail("Usage: delete-discussion-comment <node-id>");
}
const result = ghGraphQL(
`mutation { deleteDiscussionComment(input: { id: "${nodeId}" }) { comment { id } } }`,
);
@@ -566,9 +599,12 @@ function cmdDeleteDiscussionComment(nodeId) {
* Create a new discussion comment via GraphQL.
*/
function cmdRecreateDiscussionComment(discussionNodeId, bodyFile, replyToNodeId) {
if (!discussionNodeId || !bodyFile)
if (!discussionNodeId || !bodyFile) {
fail("Usage: recreate-discussion-comment <discussion-node-id> <body-file> [reply-to-node-id]");
if (!fs.existsSync(bodyFile)) fail(`File not found: ${bodyFile}`);
}
if (!fs.existsSync(bodyFile)) {
fail(`File not found: ${bodyFile}`);
}
const body = fs.readFileSync(bodyFile, "utf8");
const newComment = createDiscussionComment(discussionNodeId, body, replyToNodeId);
@@ -586,8 +622,12 @@ function cmdRecreateDiscussionComment(discussionNodeId, bodyFile, replyToNodeId)
* Create a new comment from a file.
*/
function cmdRecreateComment(issueNumber, bodyFile) {
if (!issueNumber || !bodyFile) fail("Usage: recreate-comment <issue-number> <body-file>");
if (!fs.existsSync(bodyFile)) fail(`File not found: ${bodyFile}`);
if (!issueNumber || !bodyFile) {
fail("Usage: recreate-comment <issue-number> <body-file>");
}
if (!fs.existsSync(bodyFile)) {
fail(`File not found: ${bodyFile}`);
}
const result = gh([
"api",
@@ -715,7 +755,9 @@ function cmdNotify(target, author, locationType, secretTypes, replyToNodeId) {
* Close a secret scanning alert.
*/
function cmdResolve(alertNumber, resolution, comment) {
if (!alertNumber) fail("Usage: resolve <alert-number> [resolution] [comment]");
if (!alertNumber) {
fail("Usage: resolve <alert-number> [resolution] [comment]");
}
const res = resolution || "revoked";
const resComment = comment || "Content redacted and author notified to rotate credentials.";
@@ -773,8 +815,12 @@ function cmdListOpen() {
* Print a formatted summary table from a JSON results file.
*/
function cmdSummary(jsonFile) {
if (!jsonFile) fail("Usage: summary <json-file>");
if (!fs.existsSync(jsonFile)) fail(`File not found: ${jsonFile}`);
if (!jsonFile) {
fail("Usage: summary <json-file>");
}
if (!fs.existsSync(jsonFile)) {
fail(`File not found: ${jsonFile}`);
}
const results = JSON.parse(fs.readFileSync(jsonFile, "utf8"));
const lines = [];

View File

@@ -19,7 +19,7 @@ or validating a change without wasting hours.
Prove the touched surface first. Do not reflexively run the whole suite.
1. Inspect the diff and classify the touched surface:
- normal source checkout, source change: `pnpm changed:lanes --json`, then `pnpm check:changed`
- normal source checkout, source change: `pnpm changed:lanes --json`, then `pnpm check:changed` (delegates to Crabbox/Testbox)
- normal source checkout, tests only: `pnpm test:changed`
- normal source checkout, one failing file: `pnpm test <path-or-filter> -- --reporter=verbose`
- Codex worktree or linked/sparse checkout, one/few explicit files: `node scripts/run-vitest.mjs <path-or-filter>`
@@ -27,7 +27,7 @@ Prove the touched surface first. Do not reflexively run the whole suite.
use the Crabbox wrapper with the provider that matches the proof surface.
For maintainer heavy `pnpm` gates, that is usually delegated Blacksmith
Testbox through Crabbox, e.g. `node scripts/crabbox-wrapper.mjs run
--provider blacksmith-testbox ... -- pnpm check:changed`. For direct AWS
--provider blacksmith-testbox ... -- env OPENCLAW_CHECK_CHANGED_REMOTE_CHILD=1 OPENCLAW_CHANGED_LANES_RAW_SYNC=1 corepack pnpm check:changed`. For direct AWS
Crabbox proof, omit `--provider` and let `.crabbox.yaml` choose AWS.
- workflow-only: `git diff --check`, workflow syntax/lint (`actionlint` when available)
- docs-only: `pnpm docs:list`, docs formatter/lint only if docs tooling changed or requested
@@ -66,7 +66,7 @@ scripts/crabbox-wrapper.mjs` for Testbox, and `git commit --no-verify` only
```bash
pnpm changed:lanes --json
pnpm check:changed # changed typecheck/lint/guards; no Vitest
pnpm check:changed # Crabbox/Testbox changed typecheck/lint/guards; no Vitest
pnpm test:changed # cheap smart changed Vitest targets
pnpm verify # full check, then full Vitest
OPENCLAW_TEST_CHANGED_BROAD=1 pnpm test:changed

View File

@@ -4,11 +4,11 @@ profile: openclaw-check
provider: azure
class: standard
capacity:
market: spot
market: on-demand
strategy: most-available
# Fail closed instead of silently falling back to on-demand while the
# Azure-backed billing account is the default runner path.
fallback: spot-only
# The Azure-backed billing account carries the OpenClaw runner credits; use
# explicit on-demand capacity instead of low-priority spot, whose regional
# quota is too small for broad maintainer proof or parallel Crabbox lanes.
hints: true
actions:
workflow: .github/workflows/crabbox-hydrate.yml
@@ -28,11 +28,30 @@ blacksmith:
workflow: .github/workflows/ci-check-testbox.yml
job: check
ref: main
cache:
pnpm: true
npm: true
git: true
volumes:
- name: pnpm
key: openclaw-linux-node24-pnpm
path: /var/cache/crabbox/pnpm
sizeGB: 80
required: false
- name: npm
key: openclaw-linux-node24-npm
path: /var/cache/crabbox/npm
sizeGB: 40
required: false
aws:
# AWS-specific overrides still pin direct `--provider aws` runs without
# leaking AWS region names into the Azure default capacity fallback list.
region: eu-west-1
rootGB: 400
azure:
# The OpenClaw Azure subscription is reliable in eastus2; eastus rejects the
# same SKUs and can stall provisioning.
location: eastus2
sync:
delete: true
checksum: false
@@ -52,4 +71,64 @@ env:
- OPENCLAW_*
ssh:
user: crabbox
port: "2222"
# Azure coordinator leases expose SSH on 22. The run wrapper can fall back
# from 2222, but `crabbox job run` hydrates via the configured port directly.
port: "22"
jobs:
prewarm:
provider: azure
target: linux
class: standard
type: Standard_D4ads_v6
market: on-demand
idleTimeout: 90m
hydrate:
actions: true
waitTimeout: 20m
actions:
workflow: .github/workflows/crabbox-hydrate.yml
job: hydrate
ref: main
noSync: true
shell: true
command: "true"
stop: never
changed:
provider: azure
target: linux
class: standard
type: Standard_D4ads_v6
market: on-demand
idleTimeout: 90m
hydrate:
actions: true
waitTimeout: 20m
actions:
workflow: .github/workflows/crabbox-hydrate.yml
job: hydrate
ref: main
shell: true
command: |
set -euo pipefail
if ! git status --short >/dev/null 2>&1; then
rm -rf .git
git init -q
git add -A
if ! git diff --cached --quiet; then
git -c user.name=OpenClaw -c user.email=ci@openclaw.local commit -q --no-gpg-sign -m remote-check-tree
fi
fi
env CI=1 corepack pnpm check --timed
stop: always
testbox-changed:
provider: blacksmith-testbox
target: linux
idleTimeout: 90m
hydrate:
actions: false
actions:
workflow: .github/workflows/ci-check-testbox.yml
job: check
ref: main
command: env OPENCLAW_CHECK_CHANGED_REMOTE_CHILD=1 OPENCLAW_CHANGED_LANES_RAW_SYNC=1 CI=1 corepack pnpm check:changed
stop: always

View File

@@ -128,6 +128,7 @@ runs:
if [ -n "${PNPM_CONFIG_MODULES_DIR:-}" ]; then
mkdir -p "$PNPM_CONFIG_MODULES_DIR"
ln -sfn . "$PNPM_CONFIG_MODULES_DIR/node_modules"
export NODE_PATH="$PNPM_CONFIG_MODULES_DIR${NODE_PATH:+:$NODE_PATH}"
fi
pnpm "${install_args[@]}" || pnpm "${install_args[@]}"
if [ -n "${PNPM_CONFIG_MODULES_DIR:-}" ]; then

View File

@@ -15,9 +15,8 @@ permissions:
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
PNPM_CONFIG_MODULES_DIR: "/tmp/openclaw-pnpm-node-modules"
PNPM_CONFIG_STORE_DIR: "/tmp/openclaw-pnpm-store"
PNPM_CONFIG_VIRTUAL_STORE_DIR: "/tmp/openclaw-pnpm-virtual-store"
PNPM_CONFIG_VERIFY_DEPS_BEFORE_RUN: "false"
jobs:
check:
@@ -140,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

@@ -1079,6 +1079,7 @@ jobs:
node openclaw.mjs --help
node openclaw.mjs status --json --timeout 1
pnpm test:build:singleton
checks-node-core-test-nondist-shard:
permissions:
contents: read
@@ -1201,6 +1202,9 @@ jobs:
- check_name: check-guards
task: guards
runner: blacksmith-4vcpu-ubuntu-2404
- check_name: check-shrinkwrap
task: shrinkwrap
runner: blacksmith-4vcpu-ubuntu-2404
- check_name: check-prod-types
task: prod-types
runner: blacksmith-4vcpu-ubuntu-2404
@@ -1276,7 +1280,6 @@ jobs:
pnpm tool-display:check
pnpm check:host-env-policy:swift
pnpm dup:check:coverage
pnpm deps:shrinkwrap:check
pnpm deps:patches:check
pnpm lint:webhook:no-low-level-body-read
pnpm lint:auth:no-pairing-store-group
@@ -1285,6 +1288,9 @@ jobs:
# build-artifacts already runs the tsdown/runtime build for the same Node-relevant changes.
NODE_OPTIONS=--max-old-space-size=8192 pnpm build:plugin-sdk:strict-smoke
;;
shrinkwrap)
pnpm deps:shrinkwrap:check
;;
prod-types)
pnpm tsgo:prod
;;
@@ -1403,7 +1409,7 @@ jobs:
packages/plugin-sdk/dist
extensions/*/dist/.boundary-tsc.tsbuildinfo
extensions/*/dist/.boundary-tsc.stamp
key: ${{ runner.os }}-extension-package-boundary-v1-${{ hashFiles('tsconfig.json', 'tsconfig.plugin-sdk.dts.json', 'packages/plugin-sdk/tsconfig.json', 'packages/llm-core/package.json', 'packages/model-catalog-core/package.json', 'scripts/check-extension-package-tsc-boundary.mjs', 'scripts/prepare-extension-package-boundary-artifacts.mjs', 'scripts/write-plugin-sdk-entry-dts.ts', 'scripts/lib/plugin-sdk-entrypoints.json', 'scripts/lib/plugin-sdk-entries.mjs', 'src/plugin-sdk/**', 'src/auto-reply/**', 'packages/llm-core/src/**', 'packages/model-catalog-core/src/**', 'src/video-generation/dashscope-compatible.ts', 'src/video-generation/types.ts', 'src/types/**', 'extensions/**', 'extensions/tsconfig.package-boundary*.json', 'package.json', 'pnpm-lock.yaml') }}
key: ${{ runner.os }}-extension-package-boundary-v1-${{ hashFiles('tsconfig.json', 'tsconfig.plugin-sdk.dts.json', 'packages/plugin-sdk/tsconfig.json', 'packages/llm-core/package.json', 'packages/model-catalog-core/package.json', 'scripts/check-extension-package-tsc-boundary.mjs', 'scripts/prepare-extension-package-boundary-artifacts.mjs', 'scripts/write-plugin-sdk-entry-dts.ts', 'scripts/lib/plugin-sdk-entrypoints.json', 'scripts/lib/plugin-sdk-entries.mjs', 'src/plugin-sdk/**', 'src/plugins/types.ts', 'src/auto-reply/**', 'packages/llm-core/src/**', 'packages/model-catalog-core/src/**', 'src/video-generation/dashscope-compatible.ts', 'src/video-generation/types.ts', 'src/types/**', 'extensions/**', 'extensions/tsconfig.package-boundary*.json', 'package.json', 'pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-extension-package-boundary-v1-

View File

@@ -123,6 +123,7 @@ jobs:
if [ -n "${PNPM_CONFIG_MODULES_DIR:-}" ]; then
mkdir -p "$PNPM_CONFIG_MODULES_DIR"
ln -sfn . "$PNPM_CONFIG_MODULES_DIR/node_modules"
export NODE_PATH="$PNPM_CONFIG_MODULES_DIR${NODE_PATH:+:$NODE_PATH}"
fi
pnpm "${install_args[@]}" || pnpm "${install_args[@]}"
if [ -n "${PNPM_CONFIG_MODULES_DIR:-}" ]; then
@@ -357,8 +358,8 @@ jobs:
$env:COREPACK_HOME = Join-Path $env:XDG_CACHE_HOME "corepack"
$env:PNPM_HOME = Join-Path $cacheRoot "pnpm-home"
$env:PNPM_CONFIG_STORE_DIR = Join-Path $cacheRoot "openclaw-pnpm-store"
$env:PNPM_CONFIG_MODULES_DIR = Join-Path $workspace "node_modules"
$env:PNPM_CONFIG_VIRTUAL_STORE_DIR = Join-Path $workspace "node_modules\.pnpm"
$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"
@@ -430,6 +431,25 @@ jobs:
if ($LASTEXITCODE -ne 0) {
exit $LASTEXITCODE
}
$workspaceNodeModules = Join-Path $workspace "node_modules"
if (Test-Path $workspaceNodeModules) {
$workspaceNodeModulesItem = Get-Item $workspaceNodeModules -Force
if (($workspaceNodeModulesItem.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -eq 0) {
$nodeModulesChildren = @(Get-ChildItem -LiteralPath $workspaceNodeModules -Force)
$hasOnlyPnpmWorkspaceState = $nodeModulesChildren.Count -eq 1 -and $nodeModulesChildren[0].Name -eq ".pnpm-workspace-state-v1.json"
if ($nodeModulesChildren.Count -ne 0 -and -not $hasOnlyPnpmWorkspaceState) {
throw "workspace node_modules exists and is not a link: $workspaceNodeModules"
}
foreach ($nodeModulesChild in $nodeModulesChildren) {
Remove-Item -LiteralPath $nodeModulesChild.FullName -Force
}
Remove-Item -LiteralPath $workspaceNodeModules -Force
New-Item -ItemType Junction -Path $workspaceNodeModules -Target $env:PNPM_CONFIG_MODULES_DIR | Out-Null
}
} else {
New-Item -ItemType Junction -Path $workspaceNodeModules -Target $env:PNPM_CONFIG_MODULES_DIR | Out-Null
}
$corepackShimDir = Join-Path $nodeBin "node_modules\corepack\shims"
if (Test-Path $corepackShimDir) {
$env:PNPM_HOME = $corepackShimDir

View File

@@ -43,7 +43,7 @@ jobs:
if: env.OPENCLAW_DOCS_SYNC_TOKEN != ''
uses: actions/setup-node@v6
with:
node-version: "22.19.0"
node-version: "24.x"
- name: Clone publish repo
if: env.OPENCLAW_DOCS_SYNC_TOKEN != ''

View File

@@ -115,7 +115,6 @@ jobs:
issue_number: pullRequest.number,
per_page: 100,
});
const labelNames = new Set(currentLabels.map((label) => label.name ?? ""));
for (const label of currentLabels) {
const name = label.name ?? "";
@@ -131,17 +130,14 @@ jobs:
issue_number: pullRequest.number,
name,
});
labelNames.delete(name);
}
if (!labelNames.has(targetSizeLabel)) {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pullRequest.number,
labels: [targetSizeLabel],
});
}
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pullRequest.number,
labels: [targetSizeLabel],
});
- name: Apply maintainer or trusted-contributor label
uses: actions/github-script@v9
with:

View File

@@ -1953,7 +1953,7 @@ jobs:
profiles: stable full
- suite_id: native-live-src-gateway-profiles-minimax
label: Native live gateway profiles MiniMax
command: OPENCLAW_LIVE_GATEWAY_PROVIDERS=minimax,minimax-portal OPENCLAW_LIVE_GATEWAY_MODELS=minimax/MiniMax-M2.7,minimax-portal/MiniMax-M2.7 OPENCLAW_LIVE_GATEWAY_MAX_MODELS=2 node .release-harness/scripts/test-live-shard.mjs native-live-src-gateway-profiles
command: OPENCLAW_LIVE_GATEWAY_PROVIDERS=minimax,minimax-portal OPENCLAW_LIVE_GATEWAY_MODELS=minimax/MiniMax-M3,minimax-portal/MiniMax-M3 OPENCLAW_LIVE_GATEWAY_MAX_MODELS=2 node .release-harness/scripts/test-live-shard.mjs native-live-src-gateway-profiles
timeout_minutes: 60
profile_env_only: false
profiles: stable full
@@ -2252,7 +2252,7 @@ jobs:
profiles: stable full
- suite_id: live-gateway-minimax-docker
label: Docker live gateway MiniMax
command: OPENCLAW_LIVE_GATEWAY_PROVIDERS=minimax,minimax-portal OPENCLAW_LIVE_GATEWAY_MODELS=minimax/MiniMax-M2.7,minimax-portal/MiniMax-M2.7 OPENCLAW_LIVE_GATEWAY_MAX_MODELS=1 OPENCLAW_LIVE_GATEWAY_STEP_TIMEOUT_MS=90000 OPENCLAW_LIVE_GATEWAY_MODEL_TIMEOUT_MS=180000 OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --foreground --kill-after=30s 35m bash .release-harness/scripts/test-live-gateway-models-docker.sh
command: OPENCLAW_LIVE_GATEWAY_PROVIDERS=minimax,minimax-portal OPENCLAW_LIVE_GATEWAY_MODELS=minimax/MiniMax-M3,minimax-portal/MiniMax-M3 OPENCLAW_LIVE_GATEWAY_MAX_MODELS=1 OPENCLAW_LIVE_GATEWAY_STEP_TIMEOUT_MS=90000 OPENCLAW_LIVE_GATEWAY_MODEL_TIMEOUT_MS=180000 OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --foreground --kill-after=30s 35m bash .release-harness/scripts/test-live-gateway-models-docker.sh
timeout_minutes: 40
profile_env_only: false
profiles: stable full

View File

@@ -51,7 +51,8 @@ jobs:
# so this source workflow can stay focused on OIDC publish only.
preflight_openclaw_npm:
if: ${{ inputs.preflight_only }}
runs-on: ubuntu-latest
# Preflight builds the full release package before publish; ubuntu-latest can OOM in tsdown.
runs-on: blacksmith-16vcpu-ubuntu-2404
permissions:
contents: read
steps:
@@ -256,7 +257,8 @@ jobs:
return -1;
}
for (let start = input.indexOf("["); start !== -1; start = input.indexOf("[", start + 1)) {
for (const match of input.matchAll(/\[/g)) {
const start = match.index;
const end = arrayEndFrom(start);
if (end === -1) {
continue;

View File

@@ -798,7 +798,7 @@ jobs:
- name: Build private QA runtime
env:
NODE_OPTIONS: --max-old-space-size=8192
run: pnpm build
run: node scripts/build-all.mjs qaRuntime
- name: Run parity lane
env:
@@ -876,7 +876,7 @@ jobs:
- name: Build private QA runtime
env:
NODE_OPTIONS: --max-old-space-size=8192
run: pnpm build
run: node scripts/build-all.mjs qaRuntime
- name: Generate parity report
run: |
@@ -903,7 +903,7 @@ jobs:
if: contains(fromJSON('["all","qa","qa-parity"]'), needs.resolve_target.outputs.rerun_group)
continue-on-error: true
runs-on: blacksmith-8vcpu-ubuntu-2404
timeout-minutes: 30
timeout-minutes: 45
permissions:
contents: read
env:
@@ -934,7 +934,7 @@ jobs:
- name: Build private QA runtime
env:
NODE_OPTIONS: --max-old-space-size=8192
run: pnpm build
run: node scripts/build-all.mjs qaRuntime
- name: Run runtime parity lane
id: runtime_parity_lane
@@ -1075,7 +1075,7 @@ jobs:
needs: [resolve_target]
if: contains(fromJSON('["all","qa","qa-live"]'), needs.resolve_target.outputs.rerun_group) && needs.resolve_target.outputs.qa_live_matrix_enabled == 'true'
continue-on-error: true
runs-on: ubuntu-24.04
runs-on: blacksmith-16vcpu-ubuntu-2404
timeout-minutes: 60
permissions:
contents: read
@@ -1101,7 +1101,7 @@ jobs:
- name: Build private QA runtime
env:
NODE_OPTIONS: --max-old-space-size=8192
run: pnpm build
run: node scripts/build-all.mjs qaRuntime
- name: Run Matrix live lane
id: run_lane
@@ -1199,7 +1199,7 @@ jobs:
- name: Build private QA runtime
env:
NODE_OPTIONS: --max-old-space-size=8192
run: pnpm build
run: node scripts/build-all.mjs qaRuntime
- name: Run Telegram live lane
id: run_lane
@@ -1295,7 +1295,7 @@ jobs:
- name: Build private QA runtime
env:
NODE_OPTIONS: --max-old-space-size=8192
run: pnpm build
run: node scripts/build-all.mjs qaRuntime
- name: Run Discord live lane
id: run_lane
@@ -1393,7 +1393,7 @@ jobs:
- name: Build private QA runtime
env:
NODE_OPTIONS: --max-old-space-size=8192
run: pnpm build
run: node scripts/build-all.mjs qaRuntime
- name: Run WhatsApp live lane
id: run_lane
@@ -1488,7 +1488,7 @@ jobs:
- name: Build private QA runtime
env:
NODE_OPTIONS: --max-old-space-size=8192
run: pnpm build
run: node scripts/build-all.mjs qaRuntime
- name: Run Slack live lane
id: run_lane

View File

@@ -43,4 +43,4 @@ jobs:
published_upgrade_survivor_baselines: ${{ inputs.baselines }}
published_upgrade_survivor_scenarios: ${{ inputs.scenarios }}
telegram_mode: none
secrets: inherit
secrets: inherit # zizmor: ignore[secrets-inherit] Maintainer-dispatched package acceptance lane intentionally forwards its declared live-test secret matrix.

View File

@@ -61,12 +61,14 @@ jobs:
submodules: false
- name: Probe native Windows
env:
TARGET_REF: ${{ inputs.target_ref || github.ref }}
run: |
$ErrorActionPreference = "Stop"
Write-Host "runner=$env:RUNNER_NAME"
Write-Host "machine=$env:COMPUTERNAME"
Write-Host "workspace=$env:GITHUB_WORKSPACE"
Write-Host "target_ref=${{ inputs.target_ref || github.ref }}"
Write-Host "target_ref=$env:TARGET_REF"
Write-Host ("os=" + [System.Environment]::OSVersion.VersionString)
Write-Host ("arch=" + [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture)
Write-Host ("powershell=" + $PSVersionTable.PSVersion.ToString())

View File

@@ -84,6 +84,65 @@ jobs:
"+${CHECKOUT_SHA}:refs/remotes/origin/checkout"
git -C "$GITHUB_WORKSPACE" checkout --detach refs/remotes/origin/checkout
- name: Setup Python
uses: actions/setup-python@v6
with:
python-version: "3.12"
- name: Prepare trusted workflow audit configs
if: github.event_name == 'pull_request'
env:
BASE_REF: ${{ github.event.pull_request.base.ref }}
BASE_SHA: ${{ github.event.pull_request.base.sha }}
run: |
set -euo pipefail
trusted_config="$RUNNER_TEMP/pre-commit-base.yaml"
trusted_zizmor_config="$RUNNER_TEMP/zizmor-base.yml"
if ! git cat-file -e "${BASE_SHA}^{commit}" 2>/dev/null; then
timeout --signal=TERM --kill-after=10s 30s git fetch --no-tags --depth=1 origin \
"+${BASE_SHA}:refs/remotes/origin/security-base" ||
timeout --signal=TERM --kill-after=10s 30s git fetch --no-tags --depth=1 origin \
"+refs/heads/${BASE_REF}:refs/remotes/origin/${BASE_REF}"
fi
if git cat-file -e "${BASE_SHA}:.pre-commit-config.yaml" 2>/dev/null; then
git show "${BASE_SHA}:.pre-commit-config.yaml" > "$trusted_config"
elif git show "refs/remotes/origin/${BASE_REF}:.pre-commit-config.yaml" \
> "$trusted_config" 2>/dev/null; then
echo "Base SHA ${BASE_SHA} does not expose .pre-commit-config.yaml; using origin/${BASE_REF} instead."
else
echo "::error title=trusted pre-commit config unavailable::Could not read .pre-commit-config.yaml from ${BASE_SHA} or origin/${BASE_REF}."
exit 1
fi
if git cat-file -e "${BASE_SHA}:.github/zizmor.yml" 2>/dev/null; then
git show "${BASE_SHA}:.github/zizmor.yml" > "$trusted_zizmor_config"
elif git show "refs/remotes/origin/${BASE_REF}:.github/zizmor.yml" \
> "$trusted_zizmor_config" 2>/dev/null; then
echo "Base SHA ${BASE_SHA} does not expose .github/zizmor.yml; using origin/${BASE_REF} instead."
else
echo "::error title=trusted zizmor config unavailable::Could not read .github/zizmor.yml from ${BASE_SHA} or origin/${BASE_REF}."
exit 1
fi
python3 - "$trusted_config" "$trusted_zizmor_config" <<'PY'
from pathlib import Path
import sys
config_path = Path(sys.argv[1])
zizmor_config_path = sys.argv[2]
text = config_path.read_text()
if ".github/zizmor.yml" not in text:
raise SystemExit("trusted pre-commit config does not reference .github/zizmor.yml")
config_path.write_text(text.replace(".github/zizmor.yml", zizmor_config_path))
PY
echo "PRE_COMMIT_CONFIG_PATH=$trusted_config" >> "$GITHUB_ENV"
- name: Install pre-commit
run: python -m pip install --disable-pip-version-check pre-commit==4.2.0
- name: Install actionlint
shell: bash
run: |
@@ -103,6 +162,15 @@ jobs:
- name: Lint workflows
run: actionlint
- name: Audit all workflows with zizmor
shell: bash
run: |
set -euo pipefail
mapfile -t workflow_files < <(
find .github/workflows -maxdepth 1 -type f \( -name '*.yml' -o -name '*.yaml' \) | sort
)
pre-commit run --config "${PRE_COMMIT_CONFIG_PATH:-.pre-commit-config.yaml}" zizmor --files "${workflow_files[@]}"
- name: Disallow direct inputs interpolation in composite run blocks
run: python3 scripts/check-composite-action-input-interpolation.py

12
.gitignore vendored
View File

@@ -42,6 +42,7 @@ apps/macos-mlx-tts/.build/
apps/shared/MoltbotKit/.build/
apps/shared/OpenClawKit/.build/
apps/shared/*/.build/
packages/*/dist/
apps/shared/OpenClawKit/Package.resolved
**/ModuleCache/
bin/
@@ -59,8 +60,6 @@ apps/ios/.swiftpm/
apps/ios/.derivedData/
apps/ios/.local-signing.xcconfig
vendor/
!src/auto-reply/reply/export-html/vendor/
!src/auto-reply/reply/export-html/vendor/**
apps/ios/Clawdbot.xcodeproj/
apps/ios/Clawdbot.xcodeproj/**
apps/macos/.build/**
@@ -103,13 +102,9 @@ USER.md
# though the bare names match the local-untracked rule above.
!extensions/oc-path/src/oc-path/tests/fixtures/real/IDENTITY.md
!extensions/oc-path/src/oc-path/tests/fixtures/real/USER.md
!docs/reference/templates/IDENTITY.md
!docs/reference/templates/USER.md
*.tgz
*.tar.gz
*.zip
!test/fixtures/plugins-install/*.tgz
!test/fixtures/plugins-install/*.zip
.idea
.vscode/
@@ -134,7 +129,10 @@ mantis/
!.agents/skills/control-ui-e2e/**
!.agents/skills/gitcrawl/
!.agents/skills/gitcrawl/**
!.agents/skills/openclaw-docs/**
!.agents/skills/technical-documentation/
!.agents/skills/technical-documentation/**
!.agents/skills/openclaw-refactor-docs/
!.agents/skills/openclaw-refactor-docs/**
!.agents/skills/openclaw-debugging/
!.agents/skills/openclaw-debugging/**
!.agents/skills/openclaw-ghsa-maintainer/

View File

@@ -22,11 +22,12 @@
"eslint/no-object-constructor": "error",
"eslint/no-param-reassign": "error",
"eslint/no-proto": "error",
"eslint/no-promise-executor-return": "error",
"eslint/no-regex-spaces": "error",
"eslint/no-return-assign": "error",
"eslint/no-sequences": "error",
"eslint/no-self-compare": "error",
"eslint/no-shadow": "off",
"eslint/no-shadow": "error",
"eslint/no-implicit-coercion": "error",
"eslint/no-var": "error",
"eslint/no-useless-call": "error",
@@ -35,7 +36,8 @@
"eslint/no-useless-constructor": "error",
"eslint/no-useless-rename": "error",
"eslint/no-useless-return": "error",
"eslint/no-unused-vars": "off",
"eslint/no-useless-assignment": "error",
"eslint/no-unused-vars": "error",
"eslint/no-warning-comments": "error",
"eslint/no-unmodified-loop-condition": "error",
"eslint/no-new-wrappers": "error",
@@ -78,8 +80,12 @@
"typescript/no-extraneous-class": "error",
"typescript/no-import-type-side-effects": "error",
"typescript/no-meaningless-void-operator": "error",
"typescript/no-misused-promises": "error",
"typescript/no-inferrable-types": "error",
"typescript/only-throw-error": "error",
"typescript/no-non-null-asserted-nullish-coalescing": "error",
"typescript/prefer-promise-reject-errors": "error",
"typescript/restrict-plus-operands": "error",
"typescript/no-unnecessary-qualifier": "error",
"typescript/no-unnecessary-type-assertion": "error",
"typescript/no-unnecessary-type-arguments": "error",
@@ -106,6 +112,8 @@
"typescript/require-array-sort-compare": "error",
"typescript/restrict-template-expressions": "error",
"typescript/triple-slash-reference": "error",
"typescript/unbound-method": "error",
"typescript/use-unknown-in-catch-callback-variable": "error",
"unicorn/consistent-date-clone": "error",
"unicorn/consistent-empty-array-spread": "error",
"unicorn/consistent-function-scoping": "off",
@@ -125,6 +133,7 @@
"unicorn/no-unnecessary-slice-end": "error",
"unicorn/no-useless-error-capture-stack-trace": "error",
"unicorn/no-useless-promise-resolve-reject": "error",
"unicorn/no-useless-switch-case": "error",
"unicorn/no-zero-fractions": "error",
"unicorn/prefer-date-now": "error",
"unicorn/prefer-dom-node-text-content": "error",
@@ -218,13 +227,6 @@
"**/node_modules/**"
],
"overrides": [
{
"files": ["src/security/**"],
"rules": {
"eslint/no-warning-comments": "off",
"oxc/no-map-spread": "off"
}
},
{
"files": [
"**/*.test.ts",
@@ -236,9 +238,7 @@
"**/*test-support.ts"
],
"rules": {
"typescript/no-explicit-any": "off",
"typescript/unbound-method": "off",
"eslint/no-unsafe-optional-chaining": "off"
"typescript/no-explicit-any": "off"
}
}
]

View File

@@ -9,7 +9,11 @@ Skills own workflows; root owns hard policy and routing.
- Replies: repo-root refs only: `extensions/telegram/src/index.ts:80`. No absolute paths, no `~/`.
- Docs/user-visible work: `pnpm docs:list`, then read relevant docs only.
- Fix/triage answers need source, tests, current/shipped behavior, and dependency contract proof.
- Reviews/answers: high confidence required. Default to exhaustive relevant codebase search/read, including owners, callers, siblings, tests, docs, and upstream/dependency contracts before verdict. Diff-only review is insufficient.
- Review default: read the whole changed function/module plus callers, callees, sibling implementations, adjacent tests, scoped docs, and dependency/Codex contracts before saying `good`, `bad`, `best fix`, `proof sufficient`, or posting a comment. If challenged, keep reading first; do not defend the earlier verdict until the missing path is checked.
- Dependency-touching work: direct dependency inspection is mandatory when feasible; do not rely on assumptions, wrappers, or memory. Most dependencies are OSS, so read their source/docs/types. Codex-related work has a hard gate: the acting agent must personally inspect sibling `../codex` source for the exact protocol/runtime behavior before any verdict, comment, approval, merge recommendation, code change, or `proof sufficient` claim. If missing, clone `https://github.com/openai/codex.git` there first. Subagent reports, PR text, OpenClaw wrappers, generated schemas, memory, and prior bot reviews do not satisfy this gate. No direct `../codex` check means no Codex verdict. Cite Codex files/lines checked in final/review/comment.
- Dependency-backed behavior: read upstream docs/source/types first. No API/default/error/timing guesses.
- External API work: live test required. Google/search for additional proof. Prefer official docs/source/types; cite current proof. No memory-only API claims.
- Live-verify when feasible. Never print secrets.
- Missing deps: `pnpm install`, retry once, then report first actionable error.
- CODEOWNERS: maint/refactor/tests ok. Larger behavior/product/security/ownership: owner ask/review.
@@ -26,6 +30,8 @@ Skills own workflows; root owns hard policy and routing.
- Plugin APIs, provider routing, auth/session state, persisted preferences, config loading, config/default additions, migrations, setup, startup checks, and fallback behavior are compatibility/upgrade-sensitive. Treat config breaks, new config/default surfaces, removed fallbacks, fail-closed changes, stricter validation, or new operator action as merge risk even with green CI when they can affect existing users, upgrades, provider/plugin behavior, or maintainer operations.
- For PRs that add, remove, or change config/default surfaces with possible compatibility, upgrade, provider/plugin, operator, setup, startup, or fallback impact, ClawSweeper review should emit a `reviewMetrics` entry when practical. The metric should name the count and direction of the changes, such as added, changed, or removed config/default surfaces, and explain why the metric matters before merge. When the metric indicates concrete merge risk, also surface the concern in `risks`, use `mergeRiskLabels` when the risk matches the label rubric, make `bestSolution` name the desired pre-merge state, and ensure `labelJustifications` explain the specific reason rather than restating the label.
- Review whole decision surfaces, not only the touched runtime, provider, channel, harness, plugin seam, or context path. Check sibling Codex/Pi-style runtimes, provider/model routing, channel delivery, gateway/protocol, plugin SDK, and context-management paths when relevant.
- Every PR review must explicitly ask whether the PR is the best fix, not merely a plausible fix. Verdicts need a best-fix judgment backed by enough code reading to compare owner boundaries, callers, siblings, tests, docs, current `main`, shipped behavior when relevant, and dependency/Codex contracts when involved.
- Before a PR verdict, build a small evidence map: changed surface, entry point, owner boundary, at least one caller and callee, sibling surfaces that share the invariant, existing tests, and current `main` behavior. If any cell is missing, say the gap instead of concluding.
- One-sided fixes need sibling-surface proof, an explanation for why siblings are unaffected, or explicit follow-up work.
- Changelog findings: see Docs / Changelog.
- Public ClawSweeper comments prefer `https://docs.openclaw.ai/...` when a public docs page exists; structured evidence still cites repo files, lines, SHAs.
@@ -57,12 +63,25 @@ Skills own workflows; root owns hard policy and routing.
- External official plugins own package/deps and are excluded from core dist; core uses registry-aware `facade-runtime` or generic contracts.
- Externalizing a bundled plugin: update package excludes, official catalogs, docs, tests, and prove core runtime paths resolve installed plugin roots before root-dep removal.
- Runtime reads canonical config only. No silent compat for old/malformed config keys. If a config change invalidates existing files, add a matching `openclaw doctor --fix` migration. Core/auth config repairs live in core doctor; plugin-owned config repairs live in that plugin's doctor contract (`legacyConfigRules` / `normalizeCompatibilityConfig`).
- OpenAI Codex is folded into `openai`. No new/live `openai-codex` provider/plugin/auth/model routes; treat them as legacy input only. Runtime/setup/auth/catalog use `openai` + `openai/*`; doctor/migrations repair stale `openai-codex/*` profiles/metadata.
- Config/env surface bar is high; `openclaw.json` and environment variables are already large. Before adding a config option or env var, first prove existing product behavior, provider selection, defaults, or doctor migration cannot solve it. Prefer removing or consolidating config/env options when touching these surfaces. Core supports only the latest config shape; `openclaw doctor --fix` migrates older shipped shapes into the current one.
- CLI setup flows are public API when external docs, installers, or integrations can copy them. Changes to `openclaw onboard`, `openclaw configure`, their documented flags, non-interactive behavior, or generated config shape are compatibility-sensitive API contract changes; prefer additive flags/aliases, deprecation windows, and backward-preserving migrations over breaking existing snippets.
- Fix shape: default to clean bounded refactor, not smallest patch. Move ownership to right boundary; delete stale abstractions, duplicate policy, dead branches, wrappers, fallback stacks.
- Fix observed local failures with generic product rules; do not hardcode names, ids, log phrases, or user examples in prod code unless they are an explicit contract.
- Tests may use observed examples, but prod literals need a short contract reason.
- Compatibility is opt-in. "Shipped" means reachable from a release Git tag; main/GitHub/PR/unreleased code is not shipped.
- Refactor default: one canonical path. Delete the old path unless user explicitly wants compat or the shipped public contract is obvious and cited.
- Core runtime consumes only current canonical shapes/config/data. Legacy or retired shapes normalize only in doctor/migration code before runtime; no runtime shims, aliases, or fallback readers.
- State/storage migrations are database-first. Runtime reads/writes the canonical store only. Old file stores, sidecars, aliases, and fallback readers belong in `openclaw doctor --fix` migration code only, never steady-state runtime.
- Storage default: SQLite only. Do not add JSON/JSONL/TXT/sidecar files for OpenClaw-owned runtime state, caches, queues, registries, indexes, cursors, checkpoints, or plugin scratch data.
- SQLite runtime access uses Kysely helpers, not raw SQL statement strings, except schema DDL, migrations, low-level DB bootstrap, or narrowly justified SQLite primitives.
- Use the shared state DB (`state/openclaw.sqlite`) for global runtime state and plugin KV data. Use the per-agent DB (`agents/<agentId>/agent/openclaw-agent.sqlite`) for agent-scoped state/cache. Use a dedicated SQLite DB only when schema, volume, or lifecycle clearly does not fit those stores.
- Legacy state/cache files are migration debt. When touching code that reads/writes them, prefer moving the data into SQLite or calling out the refactor follow-up; do not add parallel file paths.
- File storage must be a named product artifact: import/export, user attachment, log, backup, or external tool contract. If it is app state or cache, it belongs in SQLite.
- Before adding any path under state dirs, choose one: shared state DB, plugin KV, agent DB, or dedicated SQLite schema. If none fits, design the SQLite owner/schema first.
- Cache/transient state gets no compat migration unless a shipped user contract is cited. Prefer delete/drop/rebuild over import. If old state can be lost without user-visible data loss, remove the old path entirely.
- Persistent user state gets one migration owner. Doctor migrates, verifies, and then runtime assumes the new shape. No dual-write, read-through fallback, lazy import, or "if SQLite fails use JSON" branches.
- Fallback is a product decision, not an implementation convenience. Before adding one, name the shipped contract, failure mode, removal plan, and why doctor cannot solve it. Otherwise delete it.
- Keep old behavior only for an explicit public API/config/plugin SDK/data contract, tagged upgrade path, security/migration boundary, dependency contract, or observed prod state.
- If unsure, ask before preserving compat. Do not keep aliases, shims, fallback stacks, stale names, or obsolete tests just in case.
- Tests alone do not make internals contracts. If compat stays, name the contract and migration/removal plan in code, test, or PR.
@@ -72,6 +91,9 @@ Skills own workflows; root owns hard policy and routing.
- Plugin SDK exception: shipped external API gets new API first plus named compat/deprecation, small tests/docs if useful, removal plan.
- Migrate internal/bundled callers to modern API in the same change. Do not let internal compat become permanent architecture.
- Channels are implementation under `src/channels/**`; plugin authors get SDK seams. Providers own auth/catalog/runtime hooks; core owns generic loop.
- Message/channel plugins stay transport-only. They render portable presentation/actions, enforce transport limits, and map native callback envelopes. They do not own product command trees, plugin/provider policy, or feature-specific menus.
- Portable command UI must use typed presentation actions, not raw string inference. Do not make channels guess that `value` starting with `/` means a native command; core/owner plugins declare command actions, channels map them when supported.
- Raw callback data is transport/private. Approval, command, URL, web-app, and select actions must stay distinguishable before channel encoding so transport adapters do not special-case product strings.
- Agent run terminal state: normalize/merge via `src/agents/agent-run-terminal-outcome.ts`; do not rederive timeout/cancel precedence in projections.
- Hot paths should carry prepared facts forward: provider id, model ref, channel id, target, capability family, attachment class. Do not rediscover with broad plugin/provider/channel/capability loaders.
- Do not fix repeated request-time discovery with scattered caches. Move the canonical fact earlier; reuse prepared runtime objects; delete duplicate lookup branches.
@@ -95,8 +117,8 @@ Skills own workflows; root owns hard policy and routing.
- Tests in a normal source checkout: `pnpm test <path-or-filter> [vitest args...]`, `pnpm test:changed`, `pnpm test:serial`, `pnpm test:coverage`; never raw `vitest`.
- If raw Vitest is unavoidable, use `vitest run ...`; bare `vitest ...` starts local watch mode and will not exit on its own.
- Tests in a Codex worktree or linked/sparse checkout: avoid direct local `pnpm test*`; use `node scripts/run-vitest.mjs <path-or-filter>` for tiny explicit-file proof, or Crabbox/Testbox for anything broader.
- Checks in a normal source checkout: `pnpm check:changed`; lanes: `pnpm changed:lanes --json`; staged: `pnpm check:changed --staged`; full: `pnpm check`.
- Checks in a Codex worktree or linked/sparse checkout: avoid direct local `pnpm check*`; use `node scripts/crabbox-wrapper.mjs run ... --shell -- "pnpm check:changed"` so pnpm runs inside Testbox, not locally.
- Checks in a normal source checkout: `pnpm check:changed` delegates to Crabbox/Testbox; lanes: `pnpm changed:lanes --json`; staged: `pnpm check:changed --staged`; full: `pnpm check`.
- Checks in a Codex worktree or linked/sparse checkout: avoid direct local `pnpm check*`; use `node scripts/crabbox-wrapper.mjs run ... -- env OPENCLAW_CHECK_CHANGED_REMOTE_CHILD=1 OPENCLAW_CHANGED_LANES_RAW_SYNC=1 corepack pnpm check:changed` so pnpm runs inside Testbox, not locally.
- Extension tests: `pnpm test:extensions`, `pnpm test extensions`, `pnpm test extensions/<id>`.
- Typecheck: `tsgo` lanes only (`pnpm tsgo*`, `pnpm check:test-types`); never add `tsc --noEmit`, `typecheck`, `check:types`.
- Formatting: `oxfmt`, not Prettier. Use repo wrappers (`pnpm format:*`, `pnpm lint:*`, `scripts/run-oxlint.mjs`).
@@ -106,12 +128,13 @@ Skills own workflows; root owns hard policy and routing.
- Use `$openclaw-testing` for test/CI choice and `$crabbox` for remote/full/E2E proof.
- Crabbox request means real scenario proof: install/update/call/repro user path; not just copy tests and run them remotely.
- Visual proof: use Crabbox, set up like a user, then screenshot-verify. No harness/bypass/shortcut unless explicitly asked.
- Small/narrow tests, lints, format checks, and type probes are fine locally only in a healthy normal checkout.
- In Codex worktrees, direct local `pnpm test*`, `pnpm check*`, `pnpm crabbox:run`, and `scripts/committer` can trigger pnpm dependency reconciliation or install prompts. Prefer `node` wrappers locally and Crabbox/Testbox for pnpm-gated proof.
- Full suites, broad changed gates, Docker/package/E2E/live/cross-OS proof, or anything that bogs down the Mac: Crabbox/Testbox.
- One/few files local. If a local command fans out, stop and move broad proof to Crabbox/Testbox.
- Before handoff/push: prove touched surface. Before landing to `main`: issue proof plus appropriate full/broad proof unless scope is clearly narrow.
- Pre-land/pre-commit code changes: use `$autoreview` until no accepted/actionable findings remain, unless equivalent manual review already done, trivial/docs-only, or user opts out.
- Pre-land/pre-commit code changes: mandatory fresh `$autoreview` until no accepted/actionable findings remain. Do not land code on CI, ClawSweeper, prior review comments, or your own manual review alone unless user explicitly opts out or scope is truly trivial/docs-only. If findings want refactor, refactor; no ugly fixes.
- If proof is blocked, say exactly what is missing and why.
- Do not land related failing format/lint/type/build/tests. If unrelated on latest `origin/main`, say so with scoped proof.
- Docs/changelog-only and CI/workflow metadata-only: `git diff --check` plus relevant docs/workflow sanity; escalate only if scripts/config/generated/package/runtime behavior changed.
@@ -120,27 +143,35 @@ Skills own workflows; root owns hard policy and routing.
## GitHub / PRs
- Use `$openclaw-pr-maintainer` immediately for maintainer-side OpenClaw issue/PR review, triage, duplicates, labels, comments, close, land, or evidence. Contributor PR creation/refresh follows the requested contributor workflow; linked refs alone do not require maintainer archive tooling.
- Pasted GitHub issue/PR: first `git status -sb`; if dirty, yell; then `git push` + `git pull --ff-only`.
- Issue/PR start: `git status -sb`; if clean, `git pull --ff-only`; if dirty, yell before pull/rebase.
- PR refs: `gh pr view/diff` or `gh api`, not web search. Prefer `gitcrawl` for maintainer discovery; missing/stale `gitcrawl` falls through to live `gh`, not contributor setup. Verify live with `gh` before mutation.
- Bare issue/PR URL/number means review/report in chat. Suggest comment/close/merge when appropriate; mutate only when asked.
- No unsolicited PR comments/reviews/labels/retitles/rebases/fixups/landing. Exception: close/duplicate action that needs a reason comment after explicit close/sweep/landing request.
- Bare issue/PR URL/number: inspect live and take the efficient maintainer path; switch branches/refs when useful.
- No unsolicited PR labels/retitles/rebases/fixups/landing. Comments/reviews ok only for reviewable findings, pre-merge proof, or close/duplicate reason after explicit close/sweep/landing request.
- Maintainer decision closes the cluster: if deciding reported behavior/proposed fix is not planned, comment+close all directly associated open issues/PRs unless explicitly told to keep one open. Associated means linked PRs/issues, duplicates, companion workaround PRs, and the canonical issue for the rejected behavior.
- Do not leave associated issues open for hypothetical future repros. Close with rationale; ask for a new issue or reopen only if concrete new evidence appears. Close comment states: decision, why, supported alternative, and what evidence would change the decision.
- Issue/PR work: search strong related issues/PRs before final; close proven dupes/fixed siblings. If none close, suggest one next related follow-up.
- PR superseded by `main`: if code proof shows `main` already has same-or-better behavior, comment canonical commit/PR + focused proof, then close. Bar high: inspect PR diff, current code/tests, linked issue, caller/sibling path. If unsure, leave open.
- Issue/PR numbers need a short summary every time; assume the reader has not opened or read them.
- Before presenting a batch of issues/PRs, use smart subagents to verify live state and current `main`; omit closed/fixed items, and comment+close items already fixed on `main` when maintainer action is authorized.
- PR review answer: bug/behavior, URL(s), affected surface, provenance for regressions when traceable, best-fix judgment, evidence from code/tests/CI/current or shipped behavior.
- PR reviewable findings: post them on the PR, not chat-only, so author sees actionable feedback.
- Issue/PR final answer: last line is the full GitHub URL.
- PR verification: before merge, post exact local commands, CI/Testbox run IDs, before/after proof when used, and known proof gaps.
- PR verification: before merge, post land-ready work done, exact local commands, CI/Testbox run IDs, before/after proof when used, and known proof gaps.
- Issue fixed on `main` with proof: comment proof + commit/PR, then close.
- After landing or requested close/sweep: search duplicates; comment proof + canonical commit/PR/release before closing.
- After landing/ship final: include 2-5 sentence recap of what landed: behavior change, key files/surface, proof run, issue/PR state. Do not answer with only status/links.
- `ship` that fixes an issue: after push, comment proof + commit link, then close the issue.
- Public GH comments: show draft in chat first unless user explicitly asked to post/comment/reply/close/merge/land. After work starts and changes/proof exist, post the review/proof/commit comment.
- Representing user: if user already has a comment/thread for the point, update/reply there when possible; avoid duplicate PR/issue comments.
- No surprise GH writes: chat must mention every posted/updated public comment with URL.
- GH comments with backticks, `$`, or shell snippets: use heredoc/body file, not inline double-quoted `--body`.
- PR create: real body required. Include Summary + Verification; mention refs, behavior, and proof.
- PR create/refresh: keep PR branches takeover-ready. Use a branch maintainers can push to, or for fork PRs ensure `maintainer_can_modify` / GitHub's `Allow edits by maintainers` is enabled unless explicitly told otherwise or GitHub's Actions/secrets warning makes that unsafe.
- GitHub issue/PR create: read `$agent-transcript`; ask about sanitized transcript logs when available.
- Real behavior proof section is parsed. Use exact `field: value` labels: `Behavior addressed`, `Real environment tested`, `Exact steps or command run after this patch`, `Evidence after fix`, `Observed result after fix`, `What was not tested`.
- Contributor PRs: parsed `Real behavior proof` uses exact `field: value` labels: `Behavior addressed`, `Real environment tested`, `Exact steps or command run after this patch`, `Evidence after fix`, `Observed result after fix`, `What was not tested`.
- PR artifacts/screenshots: attach to PR/comment/external artifact store. Never push screenshots, videos, proof images, or proof assets to OpenClaw or any product repo branch, including temp artifact branches. Use Crabbox artifact publishing plus the manifest URL. Do not commit `.github/pr-assets`.
- CI polling: exact SHA, relevant checks only, minimal fields. Skip routine noise (`Auto response`, `Labeler`, docs agents, performance/stale). Logs only after failure/completion or concrete need.
- Maintainers: may skip/ignore `Real behavior proof` when local tests or Crabbox verified behavior; record proof in PR verification.
- OpenClaw write-access maintainers may skip `Real behavior proof` when local tests or Crabbox verified behavior; record proof in PR verification.
- `/landpr`: use `~/.codex/prompts/landpr.md`; do not idle on `auto-response` or `check-docs`.
## Code
@@ -154,9 +185,24 @@ Skills own workflows; root owns hard policy and routing.
- Calls should be boring: complex decisions happen above; call args/object fields are names, literals, or simple property reads.
- Prefer early returns over nested condition pyramids. Split code into gather -> normalize -> decide -> act.
- Use named intermediates only for domain meaning or readability; avoid temp-variable soup.
- Storage adapters: quarantine schema/nullability mess at the boundary. Use one named mapper from domain object to DB row, one mapper from DB row to domain object, and keep read/write paths boring.
- Discriminated unions: use exhaustive `switch` mappers instead of repeated inline conditionals. If insert/update share shape, build the row once and reuse it; split primary keys once for update sets.
- Kysely rows: prefer generated `Insertable`/`Selectable` types for mapper contracts. Do not duplicate nullable-column logic inside `values(...)` and `doUpdateSet(...)`.
- Code size matters. Prefer small clear code; maintainability includes not growing LOC without payoff.
- Refactors should delete about as much local complexity as they add. If LOC grows, the new ownership/API needs to clearly pay for it.
- Refactors should reduce non-test LOC unless they remove a larger architectural cost. Treat positive prod LOC as a smell. Before closeout, run `git diff --numstat`; if non-test LOC grew, trim or explicitly justify why fewer paths now exist.
- Prefer deleting branches, modes, adapters, and tests over preserving them. A refactor that adds a second path has probably failed unless the old path is a cited shipped contract.
- New helpers/files must pay rent immediately: fewer call paths, fewer concepts, or less repeated logic. No helpers for one-off compat, naming translation, or speculative resilience.
- Before adding helpers/files, check whether existing code can absorb the behavior with less new surface.
- Keep APIs narrow: export only current caller needs; keep types/helpers local by default.
- Return the smallest useful shape. Avoid broad result objects, flags, metadata unless callers use them.
- Avoid adapter layers that only rename fields. Move real responsibility or leave code local.
- Inline simple one-use objects/spreads when clearer. Extract only when it removes duplication or hard logic.
- Tests prove behavior/regressions, not every internal branch.
- Tests are welcome, but review them before landing for duplication and value. Delete useless tests, such as assertions for behavior or paths just removed.
- Tests protect canonical behavior and migration boundaries, not obsolete internals. Delete tests for removed fallback paths instead of updating them.
- For non-trivial refactors, check `git diff --numstat` before closeout. If LOC grew, trim or explain why.
- Prefer existing narrow helpers over repeated casts/guards. Add local helpers when 2+ nearby call sites share real boundary logic.
- Prefer ctor parameter properties for injected deps/config. Do not ban them for erasable-syntax purity.
- Prefer `satisfies` for registries/config maps; derive types from schemas when a runtime schema already exists.
- Table-drive repetitive tests when it reduces code and keeps failure names clear.
- Dynamic import: no static+dynamic import for same prod module. Use `*.runtime.ts` lazy boundary. After edits: `pnpm build`; check `[INEFFECTIVE_DYNAMIC_IMPORT]`.
- Cycles: keep `pnpm check:import-cycles` + architecture/madge green.
- Classes: no prototype mixins/mutations. Prefer inheritance/composition. Tests prefer per-instance stubs.
@@ -181,7 +227,7 @@ Skills own workflows; root owns hard policy and routing.
- Use `$technical-documentation` for docs writing/review. Docs change with behavior/API.
- Codex harness upgrade (`extensions/codex/package.json` `@openai/codex`): refresh `docs/plugins/codex-harness.md` model snapshot from the new harness `model/list`.
- Docs final answers: include relevant full `https://docs.openclaw.ai/...` URL(s). If issue/PR work too, GitHub URL last.
- `CHANGELOG.md`: release-owned. Do not edit for normal PRs, direct `main` fixes, or `ship it`; only explicit release/changelog generation may rewrite it. Do not ask contributors/agents for changelog edits.
- `CHANGELOG.md`: release-only. Do not edit for normal PRs, direct `main` fixes, or `ship it`; release generation owns it. Do not ask contributors/agents for changelog edits.
- User-facing `fix`/`feat`/`perf`: put release-note context in PR body, squash message, or direct commit: behavior, surface, issue/PR refs, credited human author/reporter.
- Release generation: derive `CHANGELOG.md` from merged PRs + all direct `main` commits. Entries: active `### Changes`/`### Fixes`, single-line, thank credited humans; never thank bots/forbidden handles: `@openclaw`, `@clawsweeper`, `@codex`, `@steipete`.
@@ -189,18 +235,19 @@ Skills own workflows; root owns hard policy and routing.
- Commit via `scripts/committer "<msg>" <file...>`; stage intended files only.
- Commits: conventional-ish, concise, grouped.
- No manual stash/autostash unless explicit. No branch/worktree changes unless requested.
- No manual stash/autostash unless explicit. Branch switches ok when useful; no new worktrees unless requested.
- `main`: no merge commits; rebase on latest `origin/main` before push. After one green run plus clean rebase sanity, do not chase moving `main` with repeated full gates.
- User says `commit`: your changes only. `commit all`: all changes in grouped chunks. `push`: may `git pull --rebase` first.
- User says `ship it`: commit intended changes, pull --rebase, push.
- Do not delete/rename unexpected files; ask if blocking, else ignore.
- Bulk PR close/reopen >5: ask with count/scope.
- Bulk PR close/reopen >50: ask with count/scope.
## Security / Release
- Never commit real phone numbers, videos, credentials, live config.
- Secrets: channel/provider creds in `~/.openclaw/credentials/`; model auth profiles in `~/.openclaw/agents/<agentId>/agent/auth-profiles.json`.
- Dependency patches/overrides/vendor changes need explicit approval. `pnpm-workspace.yaml` patched dependencies use exact versions only.
- Release/package guards: no hard-coded retired-package denylists; use generic artifact/dependency checks or fix build source.
- Lockfiles/shrinkwrap are security surface: review `pnpm-lock.yaml`, `npm-shrinkwrap.json`, `package-lock.json`; root/plugin npm packages ship shrinkwrap, not package-lock.
- Carbon pins owner-only: do not change `@buape/carbon` unless Shadow (`@thewilloftheshadow`, verified by `gh`) asks.
- Releases/publish/version bumps need explicit approval. Use `$release-openclaw-maintainer`.
@@ -218,9 +265,10 @@ Skills own workflows; root owns hard policy and routing.
- Version bump surfaces live in `$release-openclaw-maintainer`.
- Parallels: `$openclaw-parallels-smoke`; Discord roundtrip: `$parallels-discord-roundtrip`.
- Crabbox/WebVNC human demos: keep remote desktop visible/windowed; no fullscreen remote browser unless video/capture-style output.
- Before sharing WebVNC links, use Crabbox screenshot first; verify real app/path works and target UI is not broken.
- ClawSweeper ops: `$clawsweeper`. Deployed hook sessions may post one concise `#clawsweeper` note only when surprising/actionable/risky; if using message tool, reply exactly `NO_REPLY`.
- Generated-media completions wake the requester agent first. Requester visible-reply config decides final text vs message tool; direct media send is fallback/recovery only.
- `message_tool_only`: visible source reply = current-source `message(action=send)` only. No `NO_REPLY` prompt/contract; no message call = no source reply. Never auto-publish private final.
- `message_tool_only`: normal agent final visible reply = current-source `message(action=send)` only. No `NO_REPLY` prompt/contract; no message call = no source reply. Plugin-owned bound-thread reply = plugin return value; no message tool needed. Never auto-publish private final.
- Memory wiki prompt digest stays tiny; prefer `wiki_search` / `wiki_get`; verify contact data before use; source-class provenance for generated people facts.
- Rebrand/migration/config warnings: run `openclaw doctor`.
- Never edit `node_modules`.

View File

@@ -2,7 +2,7 @@
Docs: https://docs.openclaw.ai
## 2026.5.30
## 2026.6.1
### Highlights
@@ -10,8 +10,13 @@ Docs: https://docs.openclaw.ai
- 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)
- Release, CI, Docker, E2E, and diagnostics lanes now cap more logs, response bodies, readiness probes, artifact checks, and status polling so failures report bounded proof instead of stalling.
- 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
@@ -20,24 +25,46 @@ Docs: https://docs.openclaw.ai
- 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.
- Skills: add Skill Workshop with pending proposals, CLI/Gateway review actions, rollback metadata, and the `skill_workshop` agent tool. Thanks @shakkernerd.
- 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.
- Plugins: externalize Tokenjuice as the official `@openclaw/tokenjuice` plugin with npm and ClawHub publish metadata.
- 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.
- 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.
- 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)
- 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)
- 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
- 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, 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.
- 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.
- 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: 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 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.
- 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, 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)
@@ -45,13 +72,19 @@ Docs: https://docs.openclaw.ai
- 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, changelog restore, kitchen-sink and bundled plugin readiness probes, secret-provider probes, Vitest routing, and mainline test flakes. (#88127, #88137, #88155, #88160)
- 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.
- 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.
- CI/Crabbox: keep default runner capacity spot-only and provider-neutral so OpenClaw remote validation does not silently fall back to on-demand leases or stale AWS region hints.
- 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.
- CI/workflows: route workflow sanity helper edits to their guard tests and cover composite-action input interpolation checks.
- CI/tooling: route CI scope, dependency, changelog, and docs helper edits to their owner tests instead of silently skipping changed-test coverage.

View File

@@ -9,9 +9,9 @@
# 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="node:24-bookworm@sha256:3a09aa6354567619221ef6c45a5051b671f953f0a1924d1f819ffb236e520e6b"
ARG OPENCLAW_NODE_BOOKWORM_SLIM_IMAGE="node:24-bookworm-slim@sha256:e8e2e91b1378f83c5b2dd15f0247f34110e2fe895f6ca7719dbb780f929368eb"
ARG OPENCLAW_NODE_BOOKWORM_SLIM_DIGEST="sha256:e8e2e91b1378f83c5b2dd15f0247f34110e2fe895f6ca7719dbb780f929368eb"
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 oven/bun:<version> and use the manifest-list digest.
ARG OPENCLAW_BUN_IMAGE="oven/bun:1.3.13@sha256:87416c977a612a204eb54ab9f3927023c2a3c971f4f345a01da08ea6262ae30e"

View File

@@ -65,8 +65,8 @@ android {
applicationId = "ai.openclaw.app"
minSdk = 31
targetSdk = 36
versionCode = 2026053001
versionName = "2026.5.30"
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

@@ -2,10 +2,18 @@ package ai.openclaw.app
import android.content.Intent
/** Android Assistant entry point used by manifest-declared app actions. */
const val actionAskOpenClaw = "ai.openclaw.app.action.ASK_OPENCLAW"
/** Debug action that opens the Voice tab directly for Android E2E automation. */
const val actionOpenVoiceE2e = "ai.openclaw.app.debug.OPEN_VOICE_E2E"
/** Intent extra that carries an optional assistant prompt for app actions. */
const val extraAssistantPrompt = "prompt"
/**
* Top-level home destinations that external actions may request.
*/
enum class HomeDestination {
Connect,
Chat,
@@ -14,20 +22,30 @@ enum class HomeDestination {
Settings,
}
/**
* Normalized launch request from Android Assistant or explicit app actions.
*/
data class AssistantLaunchRequest(
val source: String,
val prompt: String?,
val autoSend: Boolean,
)
/**
* Parses app-owned navigation actions that should open a specific home tab.
*/
fun parseHomeDestinationIntent(intent: Intent?): HomeDestination? {
val action = intent?.action ?: return null
return when {
// Debug-only shortcut keeps E2E navigation out of release builds.
BuildConfig.DEBUG && action == actionOpenVoiceE2e -> HomeDestination.Voice
else -> null
}
}
/**
* Parse external assistant entry points without starting any UI side effects.
*/
fun parseAssistantLaunchIntent(intent: Intent?): AssistantLaunchRequest? {
val action = intent?.action ?: return null
return when (action) {

View File

@@ -1,5 +1,6 @@
package ai.openclaw.app
/** Camera HUD state categories shown over the Android UI during capture. */
enum class CameraHudKind {
Photo,
Recording,
@@ -7,6 +8,7 @@ enum class CameraHudKind {
Error,
}
/** One-shot camera HUD message keyed by token so repeated text still replays. */
data class CameraHudState(
val token: Long,
val kind: CameraHudKind,

View File

@@ -5,6 +5,7 @@ import android.os.Build
import android.provider.Settings
object DeviceNames {
/** Prefers the user-visible Android device name, then falls back to manufacturer/model text. */
fun bestDefaultNodeName(context: Context): String {
val deviceName =
runCatching {
@@ -15,6 +16,8 @@ object DeviceNames {
if (deviceName.isNotEmpty()) return deviceName
// Manufacturer/model are best-effort platform fields; keep the final
// fallback stable so stored default names do not become blank.
val model =
listOfNotNull(Build.MANUFACTURER?.takeIf { it.isNotBlank() }, Build.MODEL?.takeIf { it.isNotBlank() })
.joinToString(" ")

View File

@@ -1,5 +1,8 @@
package ai.openclaw.app
/**
* Persisted location capture mode advertised to the gateway.
*/
enum class LocationMode(
val rawValue: String,
) {
@@ -8,8 +11,10 @@ enum class LocationMode(
;
companion object {
/** Parses persisted location mode text while migrating old always-on configs to while-using. */
fun fromRawValue(raw: String?): LocationMode {
val normalized = raw?.trim()?.lowercase()
// Older configs used "always"; Android node currently exposes while-using location only.
if (normalized == "always") return WhileUsing
return entries.firstOrNull { it.rawValue.lowercase() == normalized } ?: Off
}

View File

@@ -15,6 +15,9 @@ import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import kotlinx.coroutines.launch
/**
* Main Android activity that owns Compose UI attachment and runtime UI wiring.
*/
class MainActivity : ComponentActivity() {
private val viewModel: MainViewModel by viewModels()
private lateinit var permissionRequester: PermissionRequester
@@ -43,6 +46,7 @@ class MainActivity : ComponentActivity() {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.runtimeInitialized.collect { ready ->
if (!ready || didAttachRuntimeUi) return@collect
// Runtime UI helpers need an Activity owner, so attach once after NodeRuntime is ready.
viewModel.attachRuntimeUi(owner = this@MainActivity, permissionRequester = permissionRequester)
didAttachRuntimeUi = true
if (!didStartNodeService) {
@@ -78,6 +82,9 @@ class MainActivity : ComponentActivity() {
handleAssistantIntent(intent)
}
/**
* Routes assistant/app-action intents into ViewModel state without recreating the activity.
*/
private fun handleAssistantIntent(intent: android.content.Intent?) {
parseHomeDestinationIntent(intent)?.let { destination ->
viewModel.requestHomeDestination(destination)

View File

@@ -22,6 +22,9 @@ import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.stateIn
/**
* UI-facing bridge that exposes NodeRuntime and preference state as Compose-friendly StateFlows.
*/
@OptIn(ExperimentalCoroutinesApi::class)
class MainViewModel(
app: Application,
@@ -39,6 +42,9 @@ class MainViewModel(
private val _pendingAssistantAutoSend = MutableStateFlow<String?>(null)
val pendingAssistantAutoSend: StateFlow<String?> = _pendingAssistantAutoSend
/**
* Lazily starts NodeRuntime and preserves the current foreground bit across startup.
*/
private fun ensureRuntime(): NodeRuntime {
runtimeRef.value?.let { return it }
val runtime = nodeApp.ensureRuntime()
@@ -47,6 +53,9 @@ class MainViewModel(
return runtime
}
/**
* Adapts a runtime StateFlow to a stable ViewModel StateFlow before runtime startup.
*/
private fun <T> runtimeState(
initial: T,
selector: (NodeRuntime) -> StateFlow<T>,
@@ -185,6 +194,9 @@ class MainViewModel(
val sms: SmsManager
get() = ensureRuntime().sms
/**
* Attaches Activity-owned permission and lifecycle seams after runtime initialization.
*/
fun attachRuntimeUi(
owner: LifecycleOwner,
permissionRequester: PermissionRequester,
@@ -195,6 +207,9 @@ class MainViewModel(
runtime.sms.attachPermissionRequester(permissionRequester)
}
/**
* Starts runtime on foreground entry only after onboarding has completed.
*/
fun setForeground(value: Boolean) {
foreground = value
val runtime =
@@ -254,10 +269,12 @@ class MainViewModel(
prefs.setGatewayPassword(value)
}
/** Clears setup credentials through the runtime so active gateway sessions drop stale auth state. */
fun resetGatewaySetupAuth() {
ensureRuntime().resetGatewaySetupAuth()
}
/** Marks onboarding complete and starts the runtime before UI observes connected-state flows. */
fun setOnboardingCompleted(value: Boolean) {
if (value) {
ensureRuntime()
@@ -265,6 +282,7 @@ class MainViewModel(
prefs.setOnboardingCompleted(value)
}
/** Re-enters gateway setup after disconnecting and clearing one-time setup credentials. */
fun pairNewGateway() {
runtimeRef.value?.disconnect()
resetGatewaySetupAuth()
@@ -272,6 +290,7 @@ class MainViewModel(
prefs.setOnboardingCompleted(false)
}
/** Acknowledges the one-shot request that opens onboarding at the gateway setup step. */
fun clearGatewaySetupStartRequest() {
_startOnboardingAtGatewaySetup.value = false
}
@@ -315,6 +334,7 @@ class MainViewModel(
ensureRuntime().setVoiceScreenActive(active)
}
/** Routes assistant intents into chat, either as a draft or queued auto-send prompt. */
fun handleAssistantLaunch(request: AssistantLaunchRequest) {
_requestedHomeDestination.value = HomeDestination.Chat
if (request.autoSend) {

View File

@@ -3,11 +3,17 @@ package ai.openclaw.app
import android.app.Application
import android.os.StrictMode
/**
* Android Application singleton that owns process-wide secure prefs and lazy NodeRuntime startup.
*/
class NodeApp : Application() {
val prefs: SecurePrefs by lazy { SecurePrefs(this) }
@Volatile private var runtimeInstance: NodeRuntime? = null
/**
* Returns the single NodeRuntime for this process, creating it on first use.
*/
fun ensureRuntime(): NodeRuntime {
runtimeInstance?.let { return it }
return synchronized(this) {
@@ -15,6 +21,9 @@ class NodeApp : Application() {
}
}
/**
* Reads the runtime without forcing startup, used by lifecycle probes and services.
*/
fun peekRuntime(): NodeRuntime? = runtimeInstance
override fun onCreate() {

View File

@@ -19,6 +19,7 @@ import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch
/** Foreground service that keeps the Android node connection and voice capture visible to the OS. */
class NodeForegroundService : Service() {
private val scope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
private var notificationJob: Job? = null
@@ -36,6 +37,8 @@ class NodeForegroundService : Service() {
stopSelf()
return
}
// Split connection and capture flows before combining so notification text
// can update without restarting runtime-owned connection work.
notificationJob =
scope.launch {
combine(
@@ -181,6 +184,7 @@ class NodeForegroundService : Service() {
private fun startForegroundWithTypes(notification: Notification) {
val serviceTypes = foregroundServiceTypesForVoiceMode(voiceCaptureMode)
if (didStartForeground) {
// Re-issue startForeground when Talk mode toggles so Android sees the microphone service type.
ServiceCompat.startForeground(this, NOTIFICATION_ID, notification, serviceTypes)
return
}
@@ -196,16 +200,19 @@ class NodeForegroundService : Service() {
private const val ACTION_SET_VOICE_CAPTURE_MODE = "ai.openclaw.app.action.SET_VOICE_CAPTURE_MODE"
private const val EXTRA_VOICE_CAPTURE_MODE = "ai.openclaw.app.extra.VOICE_CAPTURE_MODE"
/** Starts the persistent node foreground service from UI lifecycle code. */
fun start(context: Context) {
val intent = Intent(context, NodeForegroundService::class.java)
context.startForegroundService(intent)
}
/** Requests disconnect through the service action path so notification actions and UI share behavior. */
fun stop(context: Context) {
val intent = Intent(context, NodeForegroundService::class.java).setAction(ACTION_STOP)
context.startService(intent)
}
/** Updates Android's foreground-service type before voice capture mode changes require microphone access. */
fun setVoiceCaptureMode(
context: Context,
mode: VoiceCaptureMode,
@@ -215,6 +222,7 @@ class NodeForegroundService : Service() {
.setAction(ACTION_SET_VOICE_CAPTURE_MODE)
.putExtra(EXTRA_VOICE_CAPTURE_MODE, mode.name)
if (mode == VoiceCaptureMode.TalkMode) {
// Microphone foreground service type must be declared before Talk capture starts.
ContextCompat.startForegroundService(context, intent)
} else {
context.startService(intent)
@@ -223,6 +231,9 @@ class NodeForegroundService : Service() {
}
}
/**
* Foreground-service type mask required by Android for the current voice capture mode.
*/
internal fun foregroundServiceTypesForVoiceMode(mode: VoiceCaptureMode): Int {
val base = ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
return if (mode == VoiceCaptureMode.TalkMode) {
@@ -232,6 +243,9 @@ internal fun foregroundServiceTypesForVoiceMode(mode: VoiceCaptureMode): Int {
}
}
/**
* Compact notification suffix for voice state; kept pure for service-notification tests.
*/
internal fun voiceNotificationSuffix(
mode: VoiceCaptureMode,
manualMicEnabled: Boolean,
@@ -260,6 +274,7 @@ private fun String?.toVoiceCaptureMode(): VoiceCaptureMode =
it.name == this
} ?: VoiceCaptureMode.Off
/** Connection fields that drive foreground notification title/body text. */
private data class VoiceNotificationBase(
val status: String,
val server: String?,
@@ -267,6 +282,7 @@ private data class VoiceNotificationBase(
val mode: VoiceCaptureMode,
)
/** Voice capture fields that affect foreground-service type and suffix. */
private data class VoiceNotificationCapture(
val micEnabled: Boolean,
val micListening: Boolean,
@@ -274,6 +290,7 @@ private data class VoiceNotificationCapture(
val talkSpeaking: Boolean,
)
/** Aggregated notification state from runtime flows. */
private data class VoiceNotificationState(
val base: VoiceNotificationBase,
val capture: VoiceNotificationCapture,

View File

@@ -6,7 +6,6 @@ import ai.openclaw.app.chat.ChatPendingToolCall
import ai.openclaw.app.chat.ChatSessionEntry
import ai.openclaw.app.chat.OutgoingAttachment
import ai.openclaw.app.gateway.DeviceAuthStore
import ai.openclaw.app.gateway.DeviceAuthTokenStore
import ai.openclaw.app.gateway.DeviceIdentityStore
import ai.openclaw.app.gateway.GatewayDiscovery
import ai.openclaw.app.gateway.GatewayEndpoint
@@ -76,12 +75,17 @@ import kotlinx.serialization.json.buildJsonObject
import java.util.UUID
import java.util.concurrent.atomic.AtomicLong
/**
* Process runtime that owns gateway sessions, node command handlers, capture managers, and UI-facing state.
*/
class NodeRuntime(
context: Context,
val prefs: SecurePrefs = SecurePrefs(context.applicationContext),
private val tlsFingerprintProbe: suspend (String, Int) -> GatewayTlsProbeResult = ::probeGatewayTlsFingerprint,
private val deviceAuthStore: DeviceAuthTokenStore = DeviceAuthStore(context.applicationContext),
) {
/**
* Authentication material supplied by setup/manual connect flows before gateway session routing.
*/
data class GatewayConnectAuth(
val token: String?,
val bootstrapToken: String?,
@@ -90,6 +94,7 @@ class NodeRuntime(
private val appContext = context.applicationContext
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private val deviceAuthStore = DeviceAuthStore(prefs)
val canvas = CanvasController()
val camera = CameraCaptureManager(appContext)
val location = LocationCaptureManager(appContext)
@@ -110,6 +115,7 @@ class NodeRuntime(
private val cameraHandler: CameraHandler =
CameraHandler(
appContext = appContext,
camera = camera,
externalAudioCaptureActive = externalAudioCaptureActive,
showCameraHud = ::showCameraHud,
@@ -119,6 +125,7 @@ class NodeRuntime(
private val debugHandler: DebugHandler =
DebugHandler(
appContext = appContext,
identityStore = identityStore,
)
@@ -250,6 +257,9 @@ class NodeRuntime(
motionPedometerAvailable = { motionHandler.isPedometerAvailable() },
)
/**
* Pending TLS trust decision when a gateway certificate is new or has changed.
*/
data class GatewayTrustPrompt(
val endpoint: GatewayEndpoint,
val fingerprintSha256: String,
@@ -281,6 +291,9 @@ class NodeRuntime(
val pendingGatewayTrust: StateFlow<GatewayTrustPrompt?> = _pendingGatewayTrust.asStateFlow()
private val connectAttemptSeq = AtomicLong(0)
/**
* Builds the node-owned session key from stable device identity plus optional active agent.
*/
private fun resolveNodeMainSessionKey(agentId: String? = null): String {
val deviceId = identityStore.loadOrCreate().deviceId
return buildNodeMainSessionKey(deviceId, agentId)
@@ -840,6 +853,7 @@ class NodeRuntime(
fun setGatewayPassword(value: String) = prefs.setGatewayPassword(value)
/** Clears setup credentials plus paired device tokens for both Android gateway roles. */
fun resetGatewaySetupAuth() {
prefs.clearGatewaySetupAuth()
val deviceId = identityStore.loadOrCreate().deviceId
@@ -847,6 +861,7 @@ class NodeRuntime(
deviceAuthStore.clearToken(deviceId, "operator")
}
/** Persists onboarding state; callers decide whether runtime startup is needed first. */
fun setOnboardingCompleted(value: Boolean) = prefs.setOnboardingCompleted(value)
val lastDiscoveredStableId: StateFlow<String> = prefs.lastDiscoveredStableId
@@ -916,6 +931,7 @@ class NodeRuntime(
updateHomeCanvasState()
}
/** Updates foreground state and triggers reconnect/presence behavior on app visibility changes. */
fun setForeground(value: Boolean) {
_isForeground.value = value
if (value) {
@@ -1005,6 +1021,8 @@ class NodeRuntime(
if (didAutoConnect) return
if (_isConnected.value) return
val endpoint = resolvePreferredGatewayEndpoint() ?: return
// Only attempt the stored preferred gateway once per runtime lifetime; users
// can still reconnect explicitly from the UI after a failed auto attempt.
didAutoConnect = true
connect(endpoint)
}

View File

@@ -3,6 +3,7 @@ package ai.openclaw.app
import java.time.Instant
import java.time.ZoneId
/** Package-filter mode used before notification events are forwarded to the gateway. */
enum class NotificationPackageFilterMode(
val rawValue: String,
) {
@@ -11,10 +12,12 @@ enum class NotificationPackageFilterMode(
;
companion object {
/** Parses persisted filter mode text, defaulting to blocklist for safer forwarding. */
fun fromRawValue(raw: String?): NotificationPackageFilterMode = entries.firstOrNull { it.rawValue == raw?.trim()?.lowercase() } ?: Blocklist
}
}
/** Runtime policy used before forwarding notification events to a node session. */
internal data class NotificationForwardingPolicy(
val enabled: Boolean,
val mode: NotificationPackageFilterMode,
@@ -26,6 +29,7 @@ internal data class NotificationForwardingPolicy(
val sessionKey: String?,
)
/** Applies the operator-configured package allow/block list after trimming input. */
internal fun NotificationForwardingPolicy.allowsPackage(packageName: String): Boolean {
val normalized = packageName.trim()
if (normalized.isEmpty()) {
@@ -37,6 +41,7 @@ internal fun NotificationForwardingPolicy.allowsPackage(packageName: String): Bo
}
}
/** Returns true for both same-day and overnight quiet-hour windows. */
internal fun NotificationForwardingPolicy.isWithinQuietHours(
nowEpochMs: Long,
zoneId: ZoneId = ZoneId.systemDefault(),
@@ -64,12 +69,14 @@ internal fun NotificationForwardingPolicy.isWithinQuietHours(
private val localHourMinuteRegex = Regex("""^([01]\d|2[0-3]):([0-5]\d)$""")
/** Normalizes persisted or user-entered local times to strict HH:mm form. */
internal fun normalizeLocalHourMinute(raw: String): String? {
val trimmed = raw.trim()
val match = localHourMinuteRegex.matchEntire(trimmed) ?: return null
return "${match.groupValues[1]}:${match.groupValues[2]}"
}
/** Converts strict local HH:mm text to minutes since midnight for window checks. */
internal fun parseLocalHourMinute(raw: String): Int? {
val normalized = normalizeLocalHourMinute(raw) ?: return null
val parts = normalized.split(':')
@@ -78,11 +85,13 @@ internal fun parseLocalHourMinute(raw: String): Int? {
return hour * 60 + minute
}
/** Fixed-window limiter that bounds notification bursts per wall-clock minute. */
internal class NotificationBurstLimiter {
private val lock = Any()
private var windowStartMs: Long = -1L
private var eventsInWindow: Int = 0
/** Returns true when the current minute bucket still has forwarding capacity. */
fun allow(
nowEpochMs: Long,
maxEventsPerMinute: Int,
@@ -90,6 +99,8 @@ internal class NotificationBurstLimiter {
if (maxEventsPerMinute <= 0) {
return false
}
// Align all callers to the same minute bucket so concurrent notifications
// share the quota even when they arrive with slightly different timestamps.
val currentWindow = nowEpochMs - (nowEpochMs % 60_000L)
synchronized(lock) {
if (currentWindow != windowStartMs) {

View File

@@ -26,6 +26,9 @@ import kotlinx.coroutines.withTimeout
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.coroutines.resume
/**
* Serializes Android runtime-permission prompts behind coroutine-friendly request calls.
*/
class PermissionRequester internal constructor(
private val activity: ComponentActivity,
launcherFactory: ((Map<String, Boolean>) -> Unit) -> ActivityResultLauncher<Array<String>>,
@@ -50,8 +53,12 @@ class PermissionRequester internal constructor(
private val mutex = Mutex()
private val requestSlotsLock = Any()
private val mainHandler = Handler(Looper.getMainLooper())
// ActivityResult launchers cannot be registered after start; pre-register a small pool for nested UI flows.
private val launchers = List(4) { createPermissionRequestSlot(launcherFactory) }
/**
* Request missing Android runtime permissions and return the final grant state for every requested permission.
*/
suspend fun requestIfMissing(
permissions: List<String>,
timeoutMs: Long = 20_000,
@@ -93,6 +100,7 @@ class PermissionRequester internal constructor(
try {
withTimeout(timeoutMs) { deferred.await() }
} catch (err: TimeoutCancellationException) {
// Late ActivityResult callbacks are ignored by completePermissionRequest.
request.timedOut = true
throw err
}
@@ -130,6 +138,7 @@ class PermissionRequester internal constructor(
private fun reservePermissionRequestSlot(request: PendingPermissionRequest): PermissionRequestSlot =
synchronized(requestSlotsLock) {
// The outer mutex serializes normal callers; this guard catches accidental concurrent launchers in tests.
val slot = launchers.firstOrNull { it.request == null } ?: error("permission request launcher busy")
slot.request = request
slot
@@ -145,6 +154,7 @@ class PermissionRequester internal constructor(
slot.request = null
}
} ?: return
// Timed-out requests have already resumed callers with failure; ignore any late platform callback.
if (request.timedOut) return
request.deferred.complete(result)
}
@@ -186,6 +196,7 @@ class PermissionRequester internal constructor(
val actualObserver =
LifecycleEventObserver { _, event ->
if (event != Lifecycle.Event.ON_DESTROY) return@LifecycleEventObserver
// Do not resume a destroyed Activity with a positive result.
finish(false)
}
observer = actualObserver

View File

@@ -15,6 +15,9 @@ import kotlinx.serialization.json.JsonNull
import kotlinx.serialization.json.JsonPrimitive
import java.util.UUID
/**
* Reactive settings facade for Android node preferences and encrypted gateway credentials.
*/
class SecurePrefs(
context: Context,
private val securePrefsOverride: SharedPreferences? = null,
@@ -42,9 +45,11 @@ class SecurePrefs(
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)
// Gateway credentials and arbitrary secret strings are isolated behind EncryptedSharedPreferences.
private val masterKey by lazy {
MasterKey
.Builder(appContext)
@@ -253,6 +258,7 @@ class SecurePrefs(
val configuredPackages = loadNotificationForwardingPackages()
val normalizedAppPackage = appPackageName.trim()
// Always block OpenClaw's own notifications in blocklist mode to prevent forwarding loops.
val defaultBlockedPackages =
if (normalizedAppPackage.isNotEmpty()) setOf(normalizedAppPackage) else emptySet()
@@ -311,6 +317,7 @@ class SecurePrefs(
.toSet()
.toList()
.sorted()
// Persist deterministic JSON so settings diffs and state restoration are stable.
val encoded = JsonArray(sanitized.map { JsonPrimitive(it) }).toString()
plainPrefs.edit { putString(notificationsForwardingPackagesKey, encoded) }
_notificationForwardingPackages.value = sanitized.toSet()
@@ -355,6 +362,7 @@ class SecurePrefs(
_notificationForwardingSessionKey.value = normalized
}
/** Loads manual or instance-scoped gateway token material from encrypted preferences. */
fun loadGatewayToken(): String? {
val manual =
_gatewayToken.value.trim().ifEmpty {
@@ -363,16 +371,19 @@ class SecurePrefs(
stored
}
if (manual.isNotEmpty()) return manual
// Per-instance tokens keep reused Android installs from sharing stale gateway auth.
val key = "gateway.token.${_instanceId.value}"
val stored = securePrefs.getString(key, null)?.trim()
return stored?.takeIf { it.isNotEmpty() }
}
/** Saves the paired gateway token under the current Android instance id. */
fun saveGatewayToken(token: String) {
val key = "gateway.token.${_instanceId.value}"
securePrefs.edit { putString(key, token.trim()) }
}
/** Loads the bootstrap token used during gateway setup and device-token handoff. */
fun loadGatewayBootstrapToken(): String? {
val key = "gateway.bootstrapToken.${_instanceId.value}"
val stored =
@@ -404,9 +415,11 @@ class SecurePrefs(
securePrefs.edit { putString(key, password.trim()) }
}
/** Clears manual/setup credentials without removing persisted role-specific device tokens. */
fun clearGatewaySetupAuth() {
val instanceId = _instanceId.value
securePrefs.edit {
// Clear both current manual credentials and instance-scoped setup credentials after pairing/reset.
remove("gateway.manual.token")
remove("gateway.token.$instanceId")
remove("gateway.bootstrapToken.$instanceId")
@@ -416,11 +429,13 @@ class SecurePrefs(
_gatewayBootstrapToken.value = ""
}
/** Loads the pinned gateway TLS fingerprint for a discovered/manual stable endpoint id. */
fun loadGatewayTlsFingerprint(stableId: String): String? {
val key = "gateway.tls.$stableId"
return plainPrefs.getString(key, null)?.trim()?.takeIf { it.isNotEmpty() }
}
/** Persists the gateway TLS fingerprint captured through TOFU or explicit trust. */
fun saveGatewayTlsFingerprint(
stableId: String,
fingerprint: String,
@@ -442,23 +457,6 @@ class SecurePrefs(
securePrefs.edit { remove(key) }
}
fun keysWithPrefix(prefix: String): Set<String> =
securePrefs
.all
.keys
.filter { it.startsWith(prefix) }
.toSet()
fun removeKeysWithPrefix(prefix: String) {
val keys = keysWithPrefix(prefix)
if (keys.isEmpty()) return
securePrefs.edit {
for (key in keys) {
remove(key)
}
}
}
private fun createSecurePrefs(
context: Context,
name: String,
@@ -474,6 +472,7 @@ class SecurePrefs(
private fun loadOrCreateInstanceId(): String {
val existing = plainPrefs.getString("node.instanceId", null)?.trim()
if (!existing.isNullOrBlank()) return existing
// Instance id is not secret; it scopes local credentials and survives display-name changes.
val fresh = UUID.randomUUID().toString()
plainPrefs.edit { putString("node.instanceId", fresh) }
return fresh
@@ -483,6 +482,7 @@ class SecurePrefs(
val existing = plainPrefs.getString(displayNameKey, null)?.trim().orEmpty()
if (existing.isNotEmpty() && existing != "Android Node") return existing
// Replace the historical generic name with a device-specific default once.
val candidate = DeviceNames.bestDefaultNodeName(context).trim()
val resolved = candidate.ifEmpty { "Android Node" }
@@ -490,6 +490,7 @@ class SecurePrefs(
return resolved
}
/** Persists sanitized voice wake triggers and updates the reactive settings flow. */
fun setWakeWords(words: List<String>) {
val sanitized = WakeWords.sanitize(words, defaultWakeWords)
val encoded =
@@ -538,7 +539,7 @@ class SecurePrefs(
val raw = plainPrefs.getString(voiceWakeModeKey, null)
val resolved = VoiceWakeMode.fromRawValue(raw)
// Default ON (foreground) when unset.
// Default ON (foreground) when unset, but keep "always" opt-in through explicit settings.
if (raw.isNullOrBlank()) {
plainPrefs.edit { putString(voiceWakeModeKey, resolved.rawValue) }
}
@@ -550,6 +551,7 @@ class SecurePrefs(
val raw = plainPrefs.getString(locationModeKey, "off")
val resolved = LocationMode.fromRawValue(raw)
if (raw?.trim()?.lowercase() == "always") {
// Migrate old "always" configs to the current while-using contract.
plainPrefs.edit { putString(locationModeKey, resolved.rawValue) }
}
return resolved

View File

@@ -1,10 +1,12 @@
package ai.openclaw.app
/** Normalizes blank gateway session keys to the legacy main session alias. */
internal fun normalizeMainKey(raw: String?): String {
val trimmed = raw?.trim()
return if (!trimmed.isNullOrEmpty()) trimmed else "main"
}
/** Accepts only gateway session keys that can represent the main chat stream. */
internal fun isCanonicalMainSessionKey(raw: String?): Boolean {
val trimmed = raw?.trim().orEmpty()
if (trimmed.isEmpty()) return false
@@ -12,6 +14,7 @@ internal fun isCanonicalMainSessionKey(raw: String?): Boolean {
return trimmed.startsWith("agent:")
}
/** Extracts the agent id from canonical agent-scoped main session keys. */
internal fun resolveAgentIdFromMainSessionKey(raw: String?): String? {
val trimmed = raw?.trim().orEmpty()
if (!trimmed.startsWith("agent:")) return null
@@ -22,6 +25,7 @@ internal fun resolveAgentIdFromMainSessionKey(raw: String?): String? {
.ifEmpty { null }
}
/** Builds the node session key shape consumed by gateway chat and presence APIs. */
internal fun buildNodeMainSessionKey(
deviceId: String,
agentId: String?,

View File

@@ -1,5 +1,8 @@
package ai.openclaw.app
/**
* Persisted voice capture mode that controls foreground-service microphone requirements.
*/
enum class VoiceCaptureMode {
Off,
ManualMic,

View File

@@ -1,5 +1,8 @@
package ai.openclaw.app
/**
* Persisted wake-word mode; raw values are stored in secure preferences.
*/
enum class VoiceWakeMode(
val rawValue: String,
) {
@@ -9,6 +12,9 @@ enum class VoiceWakeMode(
;
companion object {
/**
* Invalid stored values fall back to foreground wake so hands-free behavior stays opt-in.
*/
fun fromRawValue(raw: String?): VoiceWakeMode = entries.firstOrNull { it.rawValue == raw?.trim()?.lowercase() } ?: Foreground
}
}

View File

@@ -1,11 +1,16 @@
package ai.openclaw.app
/**
* Wake-word parsing limits and sanitizers shared by settings and voice runtime paths.
*/
object WakeWords {
const val maxWords: Int = 32
const val maxWordLength: Int = 64
/** Splits comma-separated user input into non-empty wake-word entries. */
fun parseCommaSeparated(input: String): List<String> = input.split(",").map { it.trim() }.filter { it.isNotEmpty() }
/** Returns null when edited text normalizes to the current wake-word list. */
fun parseIfChanged(
input: String,
current: List<String>,
@@ -14,6 +19,7 @@ object WakeWords {
return if (parsed == current) null else parsed
}
/** Applies persisted-list bounds and falls back to defaults when all entries are empty. */
fun sanitize(
words: List<String>,
defaults: List<String>,

View File

@@ -61,12 +61,15 @@ class ChatController(
private val pendingRuns = mutableSetOf<String>()
private val pendingRunTimeoutJobs = ConcurrentHashMap<String, Job>()
// Preserve sent messages locally until chat.history includes the gateway-confirmed copy.
private val optimisticMessagesByRunId = LinkedHashMap<String, ChatMessage>()
private val pendingRunTimeoutMs = 120_000L
// Drops stale history responses after session switches or refresh races.
private val historyLoadGeneration = AtomicLong(0)
private var lastHealthPollAtMs: Long? = null
/** Clears transient chat state when the operator gateway session disconnects. */
fun onDisconnected(message: String) {
_healthOk.value = false
_errorText.value = null
@@ -78,6 +81,7 @@ class ChatController(
_sessionId.value = null
}
/** Loads a chat session, normalizing "main" to the current gateway-provided main session key. */
fun load(sessionKey: String) {
val key = normalizeRequestedSessionKey(sessionKey)
val generation = beginHistoryLoad(key, clearMessages = key != _sessionKey.value)
@@ -86,6 +90,7 @@ class ChatController(
}
}
/** Rebinds chat to a new canonical main session key after gateway hello/agent changes. */
fun applyMainSessionKey(mainSessionKey: String) {
val trimmed = mainSessionKey.trim()
if (trimmed.isEmpty()) return
@@ -108,6 +113,7 @@ class ChatController(
}
}
/** Refreshes current chat history and session list without clearing optimistic messages first. */
fun refresh() {
val key = normalizeRequestedSessionKey(_sessionKey.value)
val generation = beginHistoryLoad(key, clearMessages = false)
@@ -120,12 +126,14 @@ class ChatController(
scope.launch { fetchSessions(limit = limit) }
}
/** Persists the normalized thinking level used for subsequent chat sends. */
fun setThinkingLevel(thinkingLevel: String) {
val normalized = normalizeThinking(thinkingLevel)
if (normalized == _thinkingLevel.value) return
_thinkingLevel.value = normalized
}
/** Switches to another gateway chat session and starts a fresh history load. */
fun switchSession(sessionKey: String) {
val key = normalizeRequestedSessionKey(sessionKey)
if (key.isEmpty()) return
@@ -163,6 +171,7 @@ class ChatController(
return key
}
/** Queues a chat send without waiting for gateway acceptance. */
fun sendMessage(
message: String,
thinkingLevel: String,
@@ -177,6 +186,7 @@ class ChatController(
}
}
/** Sends a chat message and returns once the gateway accepts or rejects the request. */
suspend fun sendMessageAwaitAcceptance(
message: String,
thinkingLevel: String,
@@ -194,7 +204,7 @@ class ChatController(
val sessionKey = _sessionKey.value
val thinking = normalizeThinking(thinkingLevel)
// Optimistic user message.
// Optimistic user message keeps the composer responsive while chat.send and history refresh complete.
val userContent =
buildList {
add(ChatMessageContent(type = "text", text = text))
@@ -257,6 +267,7 @@ class ChatController(
val res = session.request("chat.send", params.toString())
val actualRunId = parseRunId(res) ?: runId
if (actualRunId != runId) {
// Gateway may return a canonical run id; move all pending bookkeeping to that id.
optimisticMessagesByRunId[actualRunId] = optimisticMessagesByRunId.remove(runId) ?: optimisticMessage
clearPendingRun(runId)
armPendingRunTimeout(actualRunId)
@@ -274,6 +285,7 @@ class ChatController(
}
}
/** Sends best-effort abort requests for every currently pending gateway run. */
fun abort() {
val runIds =
synchronized(pendingRuns) {
@@ -296,6 +308,7 @@ class ChatController(
}
}
/** Applies gateway chat/agent stream events to local transcript and pending-run state. */
fun handleGatewayEvent(
event: String,
payloadJson: String?,
@@ -396,7 +409,7 @@ class ChatController(
val state = payload["state"].asStringOrNull()
when (state) {
"delta" -> {
// Only show streaming text for runs we initiated
// Only show streaming text for runs we initiated in this controller.
if (!isPending) return
val text = parseAssistantDeltaText(payload)
if (!text.isNullOrEmpty()) {
@@ -637,6 +650,9 @@ internal fun isCurrentHistoryLoad(
activeGeneration: Long,
): Boolean = requestedSessionKey == currentSessionKey && requestGeneration == activeGeneration
/**
* Convert gateway chat content parts into Android UI content parts.
*/
internal fun parseChatMessageContent(el: JsonElement): ChatMessageContent? {
val obj = el.asObjectOrNull() ?: return null
return when (obj["type"].asStringOrNull() ?: "text") {
@@ -663,6 +679,9 @@ internal data class MainSessionState(
val appliedMainSessionKey: String,
)
/**
* Rewrite only the active "main" alias when the gateway publishes a new canonical main session key.
*/
internal fun applyMainSessionKey(
currentSessionKey: String,
appliedMainSessionKey: String,
@@ -680,6 +699,9 @@ internal fun applyMainSessionKey(
)
}
/**
* Keep Compose item identity stable across history refreshes by matching existing messages to incoming copies.
*/
internal fun reconcileMessageIds(
previous: List<ChatMessage>,
incoming: List<ChatMessage>,
@@ -729,6 +751,9 @@ internal fun mergeOptimisticMessages(
return (incoming + missingOptimistic).sortedWith(compareBy<ChatMessage> { it.timestampMs ?: Long.MAX_VALUE }.thenBy { it.id })
}
/**
* Message identity used only for refresh reconciliation; it avoids exposing gateway ids as UI keys.
*/
internal fun messageIdentityKey(message: ChatMessage): String? {
val contentKey = messageContentIdentityKey(message) ?: return null
val timestamp = message.timestampMs?.toString().orEmpty()

View File

@@ -1,5 +1,8 @@
package ai.openclaw.app.chat
/**
* Chat transcript item as delivered by gateway chat history and live chat events.
*/
data class ChatMessage(
val id: String,
val role: String,
@@ -7,6 +10,9 @@ data class ChatMessage(
val timestampMs: Long?,
)
/**
* One content part in a chat message; binary parts carry base64 plus their MIME metadata.
*/
data class ChatMessageContent(
val type: String = "text",
val text: String? = null,
@@ -15,6 +21,9 @@ data class ChatMessageContent(
val base64: String? = null,
)
/**
* Tool call placeholder shown while a gateway run is still streaming.
*/
data class ChatPendingToolCall(
val toolCallId: String,
val name: String,
@@ -23,12 +32,18 @@ data class ChatPendingToolCall(
val isError: Boolean? = null,
)
/**
* Stable session selector row; [key] is the gateway session key used in chat requests.
*/
data class ChatSessionEntry(
val key: String,
val updatedAtMs: Long?,
val displayName: String? = null,
)
/**
* Snapshot of one chat session, including optional thinking level selected on the gateway.
*/
data class ChatHistory(
val sessionKey: String,
val sessionId: String?,
@@ -36,6 +51,9 @@ data class ChatHistory(
val messages: List<ChatMessage>,
)
/**
* User-selected attachment payload sent to the gateway as inline base64.
*/
data class OutgoingAttachment(
val type: String,
val mimeType: String,

View File

@@ -1,6 +1,10 @@
package ai.openclaw.app.gateway
/**
* Decoder for Bonjour DNS-SD service names returned with decimal byte escapes.
*/
object BonjourEscapes {
/** Decodes Bonjour DNS-SD decimal escapes while preserving ordinary UTF-8. */
fun decode(input: String): String {
if (input.isEmpty()) return input
@@ -15,6 +19,7 @@ object BonjourEscapes {
val value =
((d0.code - '0'.code) * 100) + ((d1.code - '0'.code) * 10) + (d2.code - '0'.code)
if (value in 0..255) {
// Bonjour escape bytes are decimal octets, not Unicode code points.
bytes.add(value.toByte())
i += 4
continue

View File

@@ -1,6 +1,10 @@
package ai.openclaw.app.gateway
/**
* Canonical device-auth payload builder shared with gateway verification rules.
*/
internal object DeviceAuthPayload {
/** Builds the canonical v3 auth string signed by device registration flows. */
fun buildV3(
deviceId: String,
clientId: String,
@@ -32,6 +36,7 @@ internal object DeviceAuthPayload {
).joinToString("|")
}
/** Normalizes signed metadata fields without locale-sensitive lowercasing. */
internal fun normalizeMetadataField(value: String?): String {
val trimmed = value?.trim().orEmpty()
if (trimmed.isEmpty()) {

View File

@@ -1,12 +1,11 @@
package ai.openclaw.app.gateway
import ai.openclaw.app.SecurePrefs
import android.content.Context
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
/** Stored gateway device-token material scoped by device id and role. */
data class DeviceAuthEntry(
val token: String,
val role: String,
@@ -20,21 +19,21 @@ private data class PersistedDeviceAuthMetadata(
val updatedAtMs: Long = 0L,
)
private const val deviceAuthTokenPrefix = "gateway.deviceToken."
private const val deviceAuthMetadataPrefix = "gateway.deviceTokenMeta."
private const val sqliteSecurePrefsTokenMarker = "__openclaw_secure_prefs__"
/** Persistence interface used by gateway pairing/session code for role tokens. */
interface DeviceAuthTokenStore {
/** Loads the stored token plus metadata for one device/role pair. */
fun loadEntry(
deviceId: String,
role: String,
): DeviceAuthEntry?
/** Loads only the bearer token when callers do not need scope metadata. */
fun loadToken(
deviceId: String,
role: String,
): String? = loadEntry(deviceId, role)?.token
/** Persists a role token and deterministic scope metadata under normalized keys. */
fun saveToken(
deviceId: String,
role: String,
@@ -42,110 +41,37 @@ interface DeviceAuthTokenStore {
scopes: List<String> = emptyList(),
)
/** Removes both token and metadata for the normalized device/role pair. */
fun clearToken(
deviceId: String,
role: String,
)
}
internal interface DeviceAuthStateStore {
fun readDeviceAuthToken(
deviceId: String,
role: String,
): OpenClawSQLiteDeviceAuthTokenRow?
fun readLatestDeviceAuthDeviceId(): String?
fun upsertDeviceAuthToken(row: OpenClawSQLiteDeviceAuthTokenRow)
fun deleteDeviceAuthToken(
deviceId: String,
role: String,
)
fun deleteAllDeviceAuthTokens()
}
private class OpenClawSQLiteDeviceAuthStateStore(
private val store: OpenClawSQLiteStateStore,
) : DeviceAuthStateStore {
override fun readDeviceAuthToken(
deviceId: String,
role: String,
): OpenClawSQLiteDeviceAuthTokenRow? = store.readDeviceAuthToken(deviceId, role)
override fun readLatestDeviceAuthDeviceId(): String? = store.readLatestDeviceAuthDeviceId()
override fun upsertDeviceAuthToken(row: OpenClawSQLiteDeviceAuthTokenRow) {
store.upsertDeviceAuthToken(row)
}
override fun deleteDeviceAuthToken(
deviceId: String,
role: String,
) {
store.deleteDeviceAuthToken(deviceId, role)
}
override fun deleteAllDeviceAuthTokens() {
store.deleteAllDeviceAuthTokens()
}
}
class DeviceAuthStore private constructor(
private val context: Context,
private val legacyPrefsOverride: SecurePrefs? = null,
private val stateStore: DeviceAuthStateStore,
/** SecurePrefs-backed implementation of Android gateway device-token storage. */
class DeviceAuthStore(
private val prefs: SecurePrefs,
) : DeviceAuthTokenStore {
constructor(
context: Context,
legacyPrefsOverride: SecurePrefs? = null,
) : this(
context = context,
legacyPrefsOverride = legacyPrefsOverride,
stateStore = OpenClawSQLiteDeviceAuthStateStore(OpenClawSQLiteStateStore(context)),
)
internal companion object {
fun createForTesting(
context: Context,
legacyPrefsOverride: SecurePrefs? = null,
stateStoreOverride: DeviceAuthStateStore,
): DeviceAuthStore =
DeviceAuthStore(
context = context,
legacyPrefsOverride = legacyPrefsOverride,
stateStore = stateStoreOverride,
)
}
private val json = Json { ignoreUnknownKeys = true }
private val legacyPrefs by lazy { legacyPrefsOverride ?: SecurePrefs(context) }
override fun loadEntry(
deviceId: String,
role: String,
): DeviceAuthEntry? {
val normalizedDevice = normalizeDeviceId(deviceId)
val key = tokenKey(deviceId, role)
val token = prefs.getString(key)?.trim()?.takeIf { it.isNotEmpty() } ?: return null
val normalizedRole = normalizeRole(role)
val row =
stateStore.readDeviceAuthToken(normalizedDevice, normalizedRole)
?: return migrateLegacyEntryIfNoSqliteAuthRows(normalizedDevice, normalizedRole)
val token =
legacyPrefs
.getString(tokenKey(normalizedDevice, normalizedRole))
?.trim()
?.takeIf { it.isNotEmpty() }
?: row.token.trim().takeIf { it.isNotEmpty() && it != sqliteSecurePrefsTokenMarker }?.also {
legacyPrefs.putString(tokenKey(normalizedDevice, normalizedRole), it)
stateStore.upsertDeviceAuthToken(row.copy(token = sqliteSecurePrefsTokenMarker))
val metadata =
prefs
.getString(metadataKey(deviceId, role))
?.let { raw ->
runCatching { json.decodeFromString<PersistedDeviceAuthMetadata>(raw) }.getOrNull()
}
?: return null
return DeviceAuthEntry(
token = token,
role = normalizedRole,
scopes = decodeScopes(row.scopesJson),
updatedAtMs = row.updatedAtMs,
scopes = metadata?.scopes ?: emptyList(),
updatedAtMs = metadata?.updatedAtMs ?: 0L,
)
}
@@ -155,35 +81,16 @@ class DeviceAuthStore private constructor(
token: String,
scopes: List<String>,
) {
val normalizedDevice = normalizeDeviceId(deviceId)
val normalizedRole = normalizeRole(role)
val normalizedScopes = normalizeScopes(scopes)
val latestDeviceId = stateStore.readLatestDeviceAuthDeviceId()
val shouldSeedSameDeviceLegacyRoles = latestDeviceId == null
val sqliteDeviceChanged = latestDeviceId != null && latestDeviceId != normalizedDevice
val shouldDropLegacyAuth =
sqliteDeviceChanged ||
legacyPrefs.keysWithPrefix(deviceAuthTokenPrefix).any {
!it.startsWith(tokenKeyPrefix(normalizedDevice))
}
if (sqliteDeviceChanged) {
stateStore.deleteAllDeviceAuthTokens()
}
if (shouldDropLegacyAuth) {
removeForeignLegacyEntries(normalizedDevice)
}
if (shouldSeedSameDeviceLegacyRoles) {
migrateLegacyEntriesForDevice(normalizedDevice)
}
legacyPrefs.putString(tokenKey(normalizedDevice, normalizedRole), token.trim())
removeLegacyMetadata(normalizedDevice, normalizedRole)
stateStore.upsertDeviceAuthToken(
OpenClawSQLiteDeviceAuthTokenRow(
deviceId = normalizedDevice,
role = normalizedRole,
token = sqliteSecurePrefsTokenMarker,
scopesJson = json.encodeToString(normalizedScopes),
updatedAtMs = System.currentTimeMillis(),
val key = tokenKey(deviceId, role)
prefs.putString(key, token.trim())
prefs.putString(
metadataKey(deviceId, role),
json.encodeToString(
PersistedDeviceAuthMetadata(
scopes = normalizedScopes,
updatedAtMs = System.currentTimeMillis(),
),
),
)
}
@@ -192,133 +99,44 @@ class DeviceAuthStore private constructor(
deviceId: String,
role: String,
) {
val normalizedDevice = normalizeDeviceId(deviceId)
val normalizedRole = normalizeRole(role)
removeLegacyEntry(normalizedDevice, normalizedRole)
stateStore.deleteDeviceAuthToken(
deviceId = normalizedDevice,
role = normalizedRole,
)
val key = tokenKey(deviceId, role)
prefs.remove(key)
prefs.remove(metadataKey(deviceId, role))
}
private fun migrateLegacyEntryIfNoSqliteAuthRows(
normalizedDevice: String,
normalizedRole: String,
): DeviceAuthEntry? {
if (stateStore.readLatestDeviceAuthDeviceId() != null) {
removeLegacyEntry(normalizedDevice, normalizedRole)
return null
}
return migrateLegacyEntriesForDevice(normalizedDevice)[normalizedRole]
}
private fun migrateLegacyEntriesForDevice(normalizedDevice: String): Map<String, DeviceAuthEntry> {
val prefix = tokenKeyPrefix(normalizedDevice)
return legacyPrefs
.keysWithPrefix(prefix)
.mapNotNull { key ->
val role = normalizeRole(key.removePrefix(prefix))
if (role.isEmpty()) {
null
} else {
migrateLegacyEntry(normalizedDevice, role)?.let { role to it }
}
}.toMap()
}
private fun migrateLegacyEntry(
normalizedDevice: String,
normalizedRole: String,
): DeviceAuthEntry? {
val token =
legacyPrefs
.getString(tokenKey(normalizedDevice, normalizedRole))
?.trim()
?.takeIf { it.isNotEmpty() }
?: return null
val metadata =
legacyPrefs
.getString(metadataKey(normalizedDevice, normalizedRole))
?.let { raw -> runCatching { json.decodeFromString<PersistedDeviceAuthMetadata>(raw) }.getOrNull() }
val entry =
DeviceAuthEntry(
token = token,
role = normalizedRole,
scopes = normalizeScopes(metadata?.scopes ?: emptyList()),
updatedAtMs = metadata?.updatedAtMs?.takeIf { it > 0L } ?: System.currentTimeMillis(),
)
val migrated =
runCatching {
stateStore.upsertDeviceAuthToken(
OpenClawSQLiteDeviceAuthTokenRow(
deviceId = normalizedDevice,
role = normalizedRole,
token = sqliteSecurePrefsTokenMarker,
scopesJson = json.encodeToString(entry.scopes),
updatedAtMs = entry.updatedAtMs,
),
)
}.isSuccess
if (migrated) {
legacyPrefs.putString(tokenKey(normalizedDevice, normalizedRole), entry.token)
removeLegacyMetadata(normalizedDevice, normalizedRole)
}
return entry
}
private fun removeLegacyMetadata(
normalizedDevice: String,
normalizedRole: String,
) {
legacyPrefs.remove(metadataKey(normalizedDevice, normalizedRole))
}
private fun removeLegacyEntry(
normalizedDevice: String,
normalizedRole: String,
) {
legacyPrefs.remove(tokenKey(normalizedDevice, normalizedRole))
legacyPrefs.remove(metadataKey(normalizedDevice, normalizedRole))
}
private fun removeForeignLegacyEntries(normalizedDevice: String) {
val currentTokenPrefix = tokenKeyPrefix(normalizedDevice)
legacyPrefs
.keysWithPrefix(deviceAuthTokenPrefix)
.filterNot { it.startsWith(currentTokenPrefix) }
.forEach { legacyPrefs.remove(it) }
val currentMetadataPrefix = "$deviceAuthMetadataPrefix$normalizedDevice."
legacyPrefs
.keysWithPrefix(deviceAuthMetadataPrefix)
.filterNot { it.startsWith(currentMetadataPrefix) }
.forEach { legacyPrefs.remove(it) }
}
private fun tokenKeyPrefix(normalizedDevice: String): String = "$deviceAuthTokenPrefix$normalizedDevice."
private fun tokenKey(
normalizedDevice: String,
normalizedRole: String,
): String = "${tokenKeyPrefix(normalizedDevice)}$normalizedRole"
deviceId: String,
role: String,
): String {
val normalizedDevice = normalizeDeviceId(deviceId)
val normalizedRole = normalizeRole(role)
// Keep key normalization shared with metadata keys so token and metadata
// are added/removed as one logical auth entry.
return "gateway.deviceToken.$normalizedDevice.$normalizedRole"
}
private fun metadataKey(
normalizedDevice: String,
normalizedRole: String,
): String = "$deviceAuthMetadataPrefix$normalizedDevice.$normalizedRole"
private fun decodeScopes(raw: String): List<String> =
runCatching { json.decodeFromString<List<String>>(raw) }
.getOrDefault(emptyList())
.let(::normalizeScopes)
deviceId: String,
role: String,
): String {
val normalizedDevice = normalizeDeviceId(deviceId)
val normalizedRole = normalizeRole(role)
return "gateway.deviceTokenMeta.$normalizedDevice.$normalizedRole"
}
/** Normalizes device ids before they become encrypted preference key segments. */
private fun normalizeDeviceId(deviceId: String): String = deviceId.trim().lowercase()
/** Normalizes role names so node/operator token slots are stable across callers. */
private fun normalizeRole(role: String): String = role.trim().lowercase()
/** Stores scopes in deterministic order for display and restart comparisons. */
private fun normalizeScopes(scopes: List<String>): List<String> =
scopes
.map { it.trim() }
.filter { it.isNotEmpty() }
// Persist deterministic scope lists because they are displayed and may be
// compared across process restarts.
.distinct()
.sorted()
}

View File

@@ -7,6 +7,7 @@ import kotlinx.serialization.json.Json
import java.io.File
import java.security.MessageDigest
/** Persistent Ed25519 identity used to register this Android node with gateways. */
@Serializable
data class DeviceIdentity(
val deviceId: String,
@@ -15,40 +16,44 @@ data class DeviceIdentity(
val createdAtMs: Long,
)
/** Owns device identity generation, persistence, and auth payload signatures. */
class DeviceIdentityStore(
context: Context,
) {
private val json = Json { ignoreUnknownKeys = true }
private val stateStore = OpenClawSQLiteStateStore(context)
private val legacyIdentityFile = File(context.filesDir, "openclaw/identity/device.json")
private val identityFile = File(context.filesDir, "openclaw/identity/device.json")
@Volatile private var cachedIdentity: DeviceIdentity? = null
/** Loads the persisted identity or creates one, repairing old device-id drift. */
@Synchronized
fun loadOrCreate(): DeviceIdentity {
cachedIdentity?.let { return it }
val existing = load()
if (existing != null) {
val derived = deriveDeviceId(existing.publicKeyRawBase64)
if (derived != null && derived != existing.deviceId) {
val updated = existing.copy(deviceId = derived)
save(updated)
cachedIdentity = updated
return updated
}
cachedIdentity = existing
return existing
}
if (legacyIdentityFile.exists()) {
val migrated = migrateLegacyIdentity()
cachedIdentity = migrated
return migrated
}
val fresh = generate()
save(fresh)
cachedIdentity = fresh
return fresh
}
/** Signs gateway connect payload text with the persisted Ed25519 private key. */
fun signPayload(
payload: String,
identity: DeviceIdentity,
): String? =
try {
// Use BC lightweight API directly JCA provider registration is broken by R8
// Use BC lightweight API directly; R8 can break JCA provider registration.
val privateKeyBytes = Base64.decode(identity.privateKeyPkcs8Base64, Base64.DEFAULT)
val pkInfo =
org.bouncycastle.asn1.pkcs.PrivateKeyInfo
@@ -73,6 +78,7 @@ class DeviceIdentityStore(
null
}
/** Verifies a signature against the persisted public key for debug diagnostics. */
fun verifySelfSignature(
payload: String,
signatureBase64Url: String,
@@ -96,12 +102,16 @@ class DeviceIdentityStore(
false
}
/** Decodes gateway URL-safe base64 signatures, accepting unpadded input. */
private fun base64UrlDecode(input: String): ByteArray {
val normalized = input.replace('-', '+').replace('_', '/')
// Android Base64 expects padded input; gateway signatures are URL-safe
// unpadded strings.
val padded = normalized + "=".repeat((4 - normalized.length % 4) % 4)
return Base64.decode(padded, Base64.DEFAULT)
}
/** Returns the public key in the gateway's unpadded URL-safe base64 format. */
fun publicKeyBase64Url(identity: DeviceIdentity): String? =
try {
val raw = Base64.decode(identity.publicKeyRawBase64, Base64.DEFAULT)
@@ -110,80 +120,38 @@ class DeviceIdentityStore(
null
}
private fun load(): DeviceIdentity? {
val row = stateStore.readDeviceIdentity(IDENTITY_KEY) ?: return null
return readIdentity(row)
?: throw IllegalStateException(
"Stored OpenClaw device identity is invalid. Run openclaw doctor --fix.",
)
}
private fun load(): DeviceIdentity? = readIdentity(identityFile)
private fun migrateLegacyIdentity(): DeviceIdentity {
val raw =
try {
legacyIdentityFile.readText(Charsets.UTF_8)
} catch (error: Throwable) {
throw IllegalStateException("Failed to read legacy OpenClaw device identity.", error)
private fun readIdentity(file: File): DeviceIdentity? {
return try {
if (!file.exists()) return null
val raw = file.readText(Charsets.UTF_8)
val decoded = json.decodeFromString(DeviceIdentity.serializer(), raw)
if (decoded.deviceId.isBlank() ||
decoded.publicKeyRawBase64.isBlank() ||
decoded.privateKeyPkcs8Base64.isBlank()
) {
null
} else {
decoded
}
val identity =
runCatching { json.decodeFromString(DeviceIdentity.serializer(), raw) }
.getOrNull()
?.let(::normalizeRawIdentity)
?: throw IllegalStateException(
"Legacy OpenClaw device identity is invalid. Run openclaw doctor --fix.",
)
save(identity)
legacyIdentityFile.delete()
return identity
}
private fun normalizeRawIdentity(identity: DeviceIdentity): DeviceIdentity? =
try {
if (identity.publicKeyRawBase64.isBlank() || identity.privateKeyPkcs8Base64.isBlank()) {
return null
}
val publicRaw = Base64.decode(identity.publicKeyRawBase64, Base64.DEFAULT)
val privateDer = Base64.decode(identity.privateKeyPkcs8Base64, Base64.DEFAULT)
if (publicRaw.size != ED25519_KEY_SIZE || privateDer.isEmpty()) {
return null
}
val normalized = identity.copy(deviceId = sha256Hex(publicRaw))
if (!hasMatchingKeyPair(normalized)) {
return null
}
normalized
} catch (_: Throwable) {
null
}
private fun readIdentity(row: OpenClawSQLiteDeviceIdentityRow): DeviceIdentity? =
PersistedDeviceIdentity(
deviceId = row.deviceId,
publicKeyPem = row.publicKeyPem,
privateKeyPem = row.privateKeyPem,
createdAtMs = row.createdAtMs,
).toRuntimeIdentity()?.takeIf(::hasMatchingKeyPair)
private fun hasMatchingKeyPair(identity: DeviceIdentity): Boolean {
val signature = signPayload(KEYPAIR_VALIDATION_PAYLOAD, identity) ?: return false
return verifySelfSignature(KEYPAIR_VALIDATION_PAYLOAD, signature, identity)
}
private fun save(identity: DeviceIdentity) {
val persisted = PersistedDeviceIdentity.fromRuntimeIdentity(identity)
stateStore.writeDeviceIdentity(
OpenClawSQLiteDeviceIdentityRow(
deviceId = persisted.deviceId,
publicKeyPem = persisted.publicKeyPem,
privateKeyPem = persisted.privateKeyPem,
createdAtMs = persisted.createdAtMs,
),
identityKey = IDENTITY_KEY,
)
try {
identityFile.parentFile?.mkdirs()
val encoded = json.encodeToString(DeviceIdentity.serializer(), identity)
identityFile.writeText(encoded, Charsets.UTF_8)
} catch (_: Throwable) {
// best-effort only
}
}
private fun generate(): DeviceIdentity {
// Use BC lightweight API directly to avoid JCA provider issues with R8
// Use BC lightweight API directly to avoid JCA provider issues with R8.
val kpGen =
org.bouncycastle.crypto.generators
.Ed25519KeyPairGenerator()
@@ -196,7 +164,8 @@ class DeviceIdentityStore(
val privKey = kp.private as org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters
val rawPublic = pubKey.encoded // 32 bytes
val deviceId = sha256Hex(rawPublic)
// Encode private key as PKCS8 for storage
// Store private key as PKCS8 so signPayload can parse the same persisted
// shape after app restarts and upgrades.
val privKeyInfo =
org.bouncycastle.crypto.util.PrivateKeyInfoFactory
.createPrivateKeyInfo(privKey)
@@ -209,6 +178,15 @@ class DeviceIdentityStore(
)
}
/** Re-derives the stable device id from the raw Ed25519 public key bytes. */
private fun deriveDeviceId(publicKeyRawBase64: String): String? =
try {
val raw = Base64.decode(publicKeyRawBase64, Base64.DEFAULT)
sha256Hex(raw)
} catch (_: Throwable) {
null
}
private fun sha256Hex(data: ByteArray): String {
val digest = MessageDigest.getInstance("SHA-256").digest(data)
val out = CharArray(digest.size * 2)
@@ -227,92 +205,7 @@ class DeviceIdentityStore(
Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING,
)
@Serializable
private data class PersistedDeviceIdentity(
val version: Int = 1,
val deviceId: String,
val publicKeyPem: String,
val privateKeyPem: String,
val createdAtMs: Long,
) {
fun toRuntimeIdentity(): DeviceIdentity? {
if (version != 1 || deviceId.isBlank() || publicKeyPem.isBlank() || privateKeyPem.isBlank()) {
return null
}
val publicDer = decodePem(publicKeyPem, "PUBLIC KEY") ?: return null
if (!publicDer.startsWith(PUBLIC_KEY_INFO_PREFIX)) return null
val publicRaw = publicDer.copyOfRange(PUBLIC_KEY_INFO_PREFIX.size, publicDer.size)
if (publicRaw.size != ED25519_KEY_SIZE) return null
val derivedDeviceId = sha256HexStatic(publicRaw)
if (derivedDeviceId != deviceId.lowercase()) return null
val privateDer = decodePem(privateKeyPem, "PRIVATE KEY") ?: return null
return DeviceIdentity(
deviceId = derivedDeviceId,
publicKeyRawBase64 = Base64.encodeToString(publicRaw, Base64.NO_WRAP),
privateKeyPkcs8Base64 = Base64.encodeToString(privateDer, Base64.NO_WRAP),
createdAtMs = createdAtMs,
)
}
companion object {
fun fromRuntimeIdentity(identity: DeviceIdentity): PersistedDeviceIdentity {
val publicRaw = Base64.decode(identity.publicKeyRawBase64, Base64.DEFAULT)
val privateDer = Base64.decode(identity.privateKeyPkcs8Base64, Base64.DEFAULT)
return PersistedDeviceIdentity(
deviceId = identity.deviceId,
publicKeyPem = encodePem("PUBLIC KEY", PUBLIC_KEY_INFO_PREFIX + publicRaw),
privateKeyPem = encodePem("PRIVATE KEY", privateDer),
createdAtMs = identity.createdAtMs,
)
}
}
}
companion object {
private const val IDENTITY_KEY = "default"
private const val KEYPAIR_VALIDATION_PAYLOAD = "openclaw-device-identity-keypair-validation"
private const val ED25519_KEY_SIZE = 32
private val HEX = "0123456789abcdef".toCharArray()
private val PUBLIC_KEY_INFO_PREFIX =
byteArrayOf(0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x03, 0x21, 0x00)
private fun ByteArray.startsWith(prefix: ByteArray): Boolean = size >= prefix.size && prefix.indices.all { this[it] == prefix[it] }
private fun encodePem(
label: String,
bytes: ByteArray,
): String {
val body = Base64.encodeToString(bytes, Base64.NO_WRAP)
val wrapped = body.chunked(64).joinToString("\n")
return "-----BEGIN $label-----\n$wrapped\n-----END $label-----\n"
}
private fun decodePem(
pem: String,
label: String,
): ByteArray? {
val header = "-----BEGIN $label-----"
val footer = "-----END $label-----"
val trimmed = pem.trim()
if (!trimmed.startsWith(header) || !trimmed.endsWith(footer)) return null
val body =
trimmed
.removePrefix(header)
.removeSuffix(footer)
.replace("\\s".toRegex(), "")
return runCatching { Base64.decode(body, Base64.DEFAULT) }.getOrNull()
}
private fun sha256HexStatic(data: ByteArray): String {
val digest = MessageDigest.getInstance("SHA-256").digest(data)
val out = CharArray(digest.size * 2)
var i = 0
for (byte in digest) {
val v = byte.toInt() and 0xff
out[i++] = HEX[v ushr 4]
out[i++] = HEX[v and 0x0f]
}
return String(out)
}
}
}

View File

@@ -49,6 +49,9 @@ import java.util.concurrent.Executors
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
/**
* Watches local DNS-SD and optional wide-area DNS-SD for reachable OpenClaw gateways.
*/
class GatewayDiscovery(
context: Context,
private val scope: CoroutineScope,
@@ -63,9 +66,11 @@ class GatewayDiscovery(
private val localById = ConcurrentHashMap<String, GatewayEndpoint>()
private val unicastById = ConcurrentHashMap<String, GatewayEndpoint>()
private val _gateways = MutableStateFlow<List<GatewayEndpoint>>(emptyList())
/** Current discovered gateway list, merged from local DNS-SD and optional wide-area DNS-SD. */
val gateways: StateFlow<List<GatewayEndpoint>> = _gateways.asStateFlow()
private val _statusText = MutableStateFlow("Searching…")
/** Short diagnostic text shown by connect UI while discovery is running. */
val statusText: StateFlow<String> = _statusText.asStateFlow()
private var unicastJob: Job? = null
@@ -130,6 +135,8 @@ class GatewayDiscovery(
val cm = connectivity ?: return
cm.activeNetwork?.let(availableNetworks::add)
try {
// Track all networks so wide-area DNS can prefer VPN/split-DNS answers
// even when Android's active network is not the VPN.
cm.registerNetworkCallback(NetworkRequest.Builder().build(), networkCallback)
} catch (_: Throwable) {
// ignore (best-effort)
@@ -168,6 +175,7 @@ class GatewayDiscovery(
private fun resolve(serviceInfo: NsdServiceInfo) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
// Android 14+ streams service updates; older releases require one-shot resolve calls.
resolveWithServiceInfoCallback(serviceInfo)
} else {
resolveLegacy(serviceInfo)
@@ -255,6 +263,7 @@ class GatewayDiscovery(
val tlsEnabled = txtBool(resolved, "gatewayTls")
val tlsFingerprint = txt(resolved, "gatewayTlsSha256")
val id = stableId(serviceName, "local.")
// Local NSD gives the socket host/port; TXT ports are retained as gateway metadata only.
localById[id] =
GatewayEndpoint(
stableId = id,
@@ -288,6 +297,7 @@ class GatewayDiscovery(
private fun publish() {
_gateways.value =
// Merge local and wide-area results deterministically for stable UI selection.
(localById.values + unicastById.values).sortedBy { it.name.lowercase() }
_statusText.value = buildStatusText()
}
@@ -369,6 +379,7 @@ class GatewayDiscovery(
?: resolveHostUnicast(targetFqdn)
?: continue
// Wide-area DNS-SD may put TXT in additional records; fall back to a direct TXT query.
val txtFromPtr =
recordsByName(ptrMsg, Section.ADDITIONAL)[keyName(instanceFqdn)]
.orEmpty()
@@ -454,6 +465,7 @@ class GatewayDiscovery(
val system = queryViaSystemDns(query)
if (records(system, Section.ANSWER).any { it.type == type }) return system
// Android's DnsResolver can miss split-DNS answers; retry with dnsjava against network DNS servers.
val direct = createDirectResolver() ?: return system
return try {
val msg = direct.send(query)
@@ -548,6 +560,7 @@ class GatewayDiscovery(
val candidateNetworks =
buildList {
// Put VPN DNS first so Tailscale split-horizon names win over public DNS.
trackedNetworks(cm)
.firstOrNull { n ->
val caps = cm.getNetworkCapabilities(n) ?: return@firstOrNull false

View File

@@ -1,5 +1,6 @@
package ai.openclaw.app.gateway
/** Resolved gateway address and optional metadata discovered from Bonjour/manual entry. */
data class GatewayEndpoint(
val stableId: String,
val name: String,
@@ -13,6 +14,7 @@ data class GatewayEndpoint(
val tlsFingerprintSha256: String? = null,
) {
companion object {
/** Builds a stable manual endpoint key that survives display-name changes. */
fun manual(
host: String,
port: Int,

View File

@@ -4,6 +4,7 @@ import android.os.Build
import java.net.InetAddress
import java.util.Locale
/** Returns true only for loopback hosts safe to treat as local gateway origins. */
internal fun isLoopbackGatewayHost(
rawHost: String?,
allowEmulatorBridgeAlias: Boolean = isAndroidEmulatorRuntime(),
@@ -18,9 +19,12 @@ internal fun isLoopbackGatewayHost(
host = host.dropLast(1)
}
val zoneIndex = host.indexOf('%')
// Scoped IPv6 literals are not stable origin identifiers; reject them for
// loopback trust instead of guessing which interface the zone names.
if (zoneIndex >= 0) return false
if (host.isEmpty()) return false
if (host == "localhost") return true
// Android emulator maps host loopback through this bridge alias.
if (allowEmulatorBridgeAlias && host == "10.0.2.2") return true
parseIpv4Address(host)?.let { ipv4 ->
@@ -44,6 +48,7 @@ internal fun isLoopbackGatewayHost(
return isMappedIpv4 && address[12] == 127.toByte()
}
/** Allows cleartext only for loopback and private/link-local network ranges. */
internal fun isLocalCleartextGatewayHost(
rawHost: String?,
allowEmulatorBridgeAlias: Boolean = isAndroidEmulatorRuntime(),
@@ -59,6 +64,8 @@ internal fun isLocalCleartextGatewayHost(
}
val zoneIndex = host.indexOf('%')
if (zoneIndex >= 0) {
// Link-local cleartext policy is about the address range; strip the
// interface zone before InetAddress parsing rejects otherwise valid hosts.
host = host.substring(0, zoneIndex)
}
if (host.isEmpty()) return false
@@ -107,6 +114,7 @@ private fun isAndroidEmulatorRuntime(): Boolean {
product.contains("simulator")
}
/** Parses strict dotted-quad IPv4, rejecting shorthand and out-of-range octets. */
private fun parseIpv4Address(host: String): ByteArray? {
val parts = host.split('.')
if (parts.size != 4) return null
@@ -119,4 +127,5 @@ private fun parseIpv4Address(host: String): ByteArray? {
return bytes
}
/** Cheap prefilter before handing potential IPv6 literals to InetAddress. */
private fun isIpv6LiteralChar(char: Char): Boolean = char in '0'..'9' || char in 'a'..'f' || char == ':' || char == '.'

View File

@@ -1,4 +1,7 @@
package ai.openclaw.app.gateway
/** Gateway protocol version emitted by Android node clients. */
const val GATEWAY_PROTOCOL_VERSION = 4
/** Oldest gateway protocol version this Android client can speak safely. */
const val GATEWAY_MIN_PROTOCOL_VERSION = 4

View File

@@ -33,6 +33,9 @@ import java.util.UUID
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicBoolean
/**
* Identity advertised during gateway connect; these fields become the device row users approve.
*/
data class GatewayClientInfo(
val id: String,
val displayName: String?,
@@ -44,6 +47,9 @@ data class GatewayClientInfo(
val modelIdentifier: String?,
)
/**
* Role, scopes, commands, and permission snapshot sent with the connect frame.
*/
data class GatewayConnectOptions(
val role: String,
val scopes: List<String>,
@@ -62,6 +68,9 @@ private enum class GatewayConnectAuthSource {
NONE,
}
/**
* Structured auth failure guidance from the gateway, preserved for reconnect and UI decisions.
*/
data class GatewayConnectErrorDetails(
val code: String?,
val canRetryWithDeviceToken: Boolean,
@@ -70,6 +79,9 @@ data class GatewayConnectErrorDetails(
val reason: String? = null,
)
/**
* Server hello fields cached by the Android runtime after a successful connect.
*/
data class GatewayHelloSummary(
val serverName: String?,
val remoteAddress: String?,
@@ -99,6 +111,9 @@ private class GatewayConnectFailure(
val gatewayError: GatewaySession.ErrorShape,
) : IllegalStateException(gatewayError.message)
/**
* WebSocket RPC session that maintains gateway connection lifecycle, auth, events, and node invokes.
*/
class GatewaySession(
private val scope: CoroutineScope,
private val identityStore: DeviceIdentityStore,
@@ -114,6 +129,9 @@ class GatewaySession(
private const val CONNECT_RPC_TIMEOUT_MS = 12_000L
}
/**
* Gateway node.invoke request routed to Android command handlers.
*/
data class InvokeRequest(
val id: String,
val nodeId: String,
@@ -143,6 +161,9 @@ class GatewaySession(
val details: GatewayConnectErrorDetails? = null,
)
/**
* Structured RPC result used by callers that need error codes without exceptions.
*/
data class RpcResult(
val ok: Boolean,
val payloadJson: String?,
@@ -174,12 +195,15 @@ class GatewaySession(
@Volatile private var currentConnection: Connection? = null
// One reconnect can retry a shared-token mismatch by pairing the shared token with the stored device token.
@Volatile private var pendingDeviceTokenRetry = false
// Keep the mismatch retry single-shot so an invalid stored token cannot create an auth loop.
@Volatile private var deviceTokenRetryBudgetUsed = false
@Volatile private var reconnectPausedForAuthFailure = false
/** Starts or replaces the desired gateway connection and launches the reconnect loop. */
fun connect(
endpoint: GatewayEndpoint,
token: String?,
@@ -202,6 +226,7 @@ class GatewaySession(
connectionToClose?.closeQuietly()
}
/** Clears desired connection state, closes the socket, and stops reconnect attempts. */
fun disconnect() {
val jobToCancel: Job?
val connectionToClose: Connection?
@@ -225,6 +250,7 @@ class GatewaySession(
}
}
/** Forces the current socket closed so the loop reconnects to the current desired endpoint. */
fun reconnect() {
reconnectPausedForAuthFailure = false
currentConnection?.closeQuietly()
@@ -232,6 +258,7 @@ class GatewaySession(
fun currentCanvasHostUrl(): String? = pluginSurfaceUrls["canvas"]
/** Refreshes the canvas plugin surface URL and caches the normalized Android-reachable URL. */
suspend fun refreshCanvasHostUrl(timeoutMs: Long = 8_000): String? {
val refreshed =
refreshPluginSurfaceUrl(
@@ -247,6 +274,7 @@ class GatewaySession(
fun currentMainSessionKey(): String? = mainSessionKey
/** Sends a best-effort node.event and returns false instead of throwing on failure. */
suspend fun sendNodeEvent(
event: String,
payloadJson: String?,
@@ -287,6 +315,7 @@ class GatewaySession(
}
}
/** Sends node.event and preserves the gateway RPC error shape for callers that need diagnostics. */
suspend fun sendNodeEventDetailed(
event: String,
payloadJson: String?,
@@ -319,9 +348,11 @@ class GatewaySession(
): JsonObject =
buildJsonObject {
put("event", JsonPrimitive(event))
// Gateway node events carry payloadJSON as a string for compatibility with non-JSON payload producers.
put("payloadJSON", JsonPrimitive(payloadJson ?: "{}"))
}
/** Sends an RPC request and throws a code-prefixed exception when the gateway returns an error. */
suspend fun request(
method: String,
paramsJson: String?,
@@ -333,6 +364,7 @@ class GatewaySession(
throw IllegalStateException("${err?.code ?: "UNAVAILABLE"}: ${err?.message ?: "request failed"}")
}
/** Sends an RPC request and returns the structured success/error payload. */
suspend fun requestDetailed(
method: String,
paramsJson: String?,
@@ -349,6 +381,7 @@ class GatewaySession(
return RpcResult(ok = res.ok, payloadJson = res.payloadJson, error = res.error)
}
/** Sends an RPC request frame and reports errors asynchronously through [onError]. */
suspend fun sendRequestFrame(
method: String,
paramsJson: String?,
@@ -705,6 +738,7 @@ class GatewaySession(
persistIssuedDeviceToken(authSource, deviceId, authRole, deviceToken, authScopes)
}
if (shouldPersistBootstrapHandoffTokens(authSource)) {
// Bootstrap connects can mint role-specific device tokens; store only locally trusted handoffs.
authObj
?.get("deviceTokens")
.asArrayOrNull()
@@ -725,6 +759,7 @@ class GatewaySession(
val rawPluginSurfaceUrls = obj["pluginSurfaceUrls"].asObjectOrNull()
val normalizedPluginSurfaceUrls =
rawPluginSurfaceUrls?.mapNotNull { (surface, value) ->
// Canvas URLs may be loopback gateway metadata; normalize them to the reachable Android endpoint.
normalizeCanvasHostUrl(value.asStringOrNull(), endpoint, isTlsConnection = tls != null)
?.let { normalized -> surface to normalized }
} ?: emptyList()
@@ -797,6 +832,7 @@ class GatewaySession(
val connectScopes = resolveConnectScopes(selectedAuth)
val signedAtMs = System.currentTimeMillis()
// V3 signatures bind the auth token, nonce, role, and scopes so replayed connect frames fail.
val payload =
DeviceAuthPayload.buildV3(
deviceId = identity.deviceId,
@@ -966,6 +1002,7 @@ class GatewaySession(
if (parsedPayload != null) {
put("payload", parsedPayload)
} else if (result.payloadJson != null) {
// Preserve malformed/non-object payloads as payloadJSON so the gateway can report handler output.
put("payloadJSON", JsonPrimitive(result.payloadJson))
}
result.error?.let { err ->
@@ -1189,6 +1226,7 @@ class GatewaySession(
if (!isTrustedDeviceRetryEndpoint(endpoint, tls)) return false
val detailCode = error.details?.code
val recommendedNextStep = error.details?.recommendedNextStep
// New gateways set canRetryWithDeviceToken; older builds expose equivalent string codes.
return error.details?.canRetryWithDeviceToken == true ||
recommendedNextStep == "retry_with_device_token" ||
detailCode == "AUTH_TOKEN_MISMATCH"
@@ -1213,10 +1251,13 @@ class GatewaySession(
tls: GatewayTlsParams?,
): Boolean {
if (isLocalCleartextGatewayHost(endpoint.host)) return true
// Retrying a stored device token alongside a shared token is only safe for
// remote gateways when an existing TLS pin already identifies the endpoint.
return tls?.expectedFingerprint?.trim()?.isNotEmpty() == true
}
}
/** Decides whether auth failures should stop reconnect churn until the user changes credentials. */
internal fun shouldPauseGatewayReconnectAfterAuthFailure(
error: GatewaySession.ErrorShape,
hasBootstrapToken: Boolean,
@@ -1249,6 +1290,7 @@ internal fun shouldPauseGatewayReconnectAfterAuthFailure(
else -> false
}
/** Builds the gateway WebSocket URL from endpoint authority and TLS policy. */
internal fun buildGatewayWebSocketUrl(
host: String,
port: Int,
@@ -1258,6 +1300,7 @@ internal fun buildGatewayWebSocketUrl(
return "$scheme://${formatGatewayAuthority(host, port)}"
}
/** Formats host/port for gateway URLs, including IPv6 bracket wrapping. */
internal fun formatGatewayAuthority(
host: String,
port: Int,
@@ -1308,6 +1351,7 @@ private fun parseJsonOrNull(payload: String): JsonElement? {
}
}
/** Keeps invoke-result ack waits inside the gateway-supported timeout window. */
internal fun resolveInvokeResultAckTimeoutMs(invokeTimeoutMs: Long?): Long {
val normalized = invokeTimeoutMs?.takeIf { it > 0L } ?: 15_000L
return normalized.coerceIn(15_000L, 120_000L)

View File

@@ -25,6 +25,7 @@ import javax.net.ssl.SSLSocketFactory
import javax.net.ssl.TrustManagerFactory
import javax.net.ssl.X509TrustManager
/** TLS pinning inputs for a discovered or manually configured gateway endpoint. */
data class GatewayTlsParams(
val required: Boolean,
val expectedFingerprint: String?,
@@ -32,22 +33,26 @@ data class GatewayTlsParams(
val stableId: String,
)
/** SSL primitives installed into OkHttp when a gateway needs TLS pinning/TOFU. */
data class GatewayTlsConfig(
val sslSocketFactory: SSLSocketFactory,
val trustManager: X509TrustManager,
val hostnameVerifier: HostnameVerifier,
)
/** Distinguishes non-TLS endpoints from unreachable endpoints during probing. */
enum class GatewayTlsProbeFailure {
TLS_UNAVAILABLE,
ENDPOINT_UNREACHABLE,
}
/** Result of probing a gateway TLS endpoint for first-use fingerprint capture. */
data class GatewayTlsProbeResult(
val fingerprintSha256: String? = null,
val failure: GatewayTlsProbeFailure? = null,
)
/** Builds a TLS config that supports pinned fingerprints and trust-on-first-use. */
fun buildGatewayTlsConfig(
params: GatewayTlsParams?,
onStore: ((String) -> Unit)? = null,
@@ -82,6 +87,9 @@ fun buildGatewayTlsConfig(
return
}
if (params.allowTOFU) {
// Store only after the TLS stack presents a concrete server cert; the
// caller persists the fingerprint against the endpoint's stable id,
// and later connects must come back through the pinned branch above.
onStore?.invoke(fingerprint)
return
}
@@ -107,6 +115,7 @@ fun buildGatewayTlsConfig(
)
}
/** Connects with a probe trust manager that captures the presented cert hash. */
suspend fun probeGatewayTlsFingerprint(
host: String,
port: Int,
@@ -132,6 +141,7 @@ suspend fun probeGatewayTlsFingerprint(
) {
if (chain.isEmpty()) throw CertificateException("empty certificate chain")
fingerprintRef.set(sha256Hex(chain[0].encoded))
// Abort validation after capture; the probe is not deciding trust.
throw CertificateException("gateway TLS probe captured fingerprint")
}
@@ -154,7 +164,8 @@ suspend fun probeGatewayTlsFingerprint(
socket.sslParameters = params
}
} catch (_: Throwable) {
// ignore
// SNI is only a probe hint. IP literals and odd Bonjour names should
// still be probed instead of failing before the TLS handshake.
}
socket.startHandshake()
@@ -203,6 +214,7 @@ private fun sha256Hex(data: ByteArray): String {
return out.toString()
}
/** Normalizes user-visible fingerprint text to lowercase bare SHA-256 hex. */
fun normalizeGatewayTlsFingerprint(raw: String): String {
val stripped =
raw

View File

@@ -5,10 +5,15 @@ data class ParsedInvokeError(
val message: String,
val hadExplicitCode: Boolean,
) {
/** Gateway-facing form expected by UI and retry copy. */
val prefixedMessage: String
get() = "$code: $message"
}
/**
* Parses gateway invoke errors encoded as CODE: message while preserving legacy
* plain-text errors as UNAVAILABLE.
*/
fun parseInvokeErrorMessage(raw: String): ParsedInvokeError {
val trimmed = raw.trim()
if (trimmed.isEmpty()) {
@@ -30,6 +35,7 @@ fun parseInvokeErrorMessage(raw: String): ParsedInvokeError {
return ParsedInvokeError(code = "UNAVAILABLE", message = trimmed, hadExplicitCode = false)
}
/** Extracts an invoke error from a throwable without exposing blank messages. */
fun parseInvokeErrorFromThrowable(
err: Throwable,
fallbackMessage: String = "error",

View File

@@ -1,310 +0,0 @@
package ai.openclaw.app.gateway
import android.content.ContentValues
import android.content.Context
import android.database.sqlite.SQLiteDatabase
import java.io.File
data class OpenClawSQLiteDeviceIdentityRow(
val deviceId: String,
val publicKeyPem: String,
val privateKeyPem: String,
val createdAtMs: Long,
)
data class OpenClawSQLiteDeviceAuthTokenRow(
val deviceId: String,
val role: String,
val token: String,
val scopesJson: String,
val updatedAtMs: Long,
)
class OpenClawSQLiteStateStore(
context: Context,
) {
private val appContext = context.applicationContext
private val databaseFile = File(appContext.filesDir, "openclaw/state/openclaw.sqlite")
fun databaseFile(): File = databaseFile
@Synchronized
fun readDeviceIdentity(identityKey: String = "default"): OpenClawSQLiteDeviceIdentityRow? {
if (!databaseFile.exists()) return null
return openDatabase().use { db ->
db
.rawQuery(
"""
SELECT device_id, public_key_pem, private_key_pem, created_at_ms
FROM device_identities
WHERE identity_key = ?
""".trimIndent(),
arrayOf(identityKey),
).use { cursor ->
if (!cursor.moveToFirst()) return@use null
OpenClawSQLiteDeviceIdentityRow(
deviceId = cursor.getString(0),
publicKeyPem = cursor.getString(1),
privateKeyPem = cursor.getString(2),
createdAtMs = cursor.getLong(3),
)
}
}
}
@Synchronized
fun writeDeviceIdentity(
identity: OpenClawSQLiteDeviceIdentityRow,
identityKey: String = "default",
updatedAtMs: Long = System.currentTimeMillis(),
) {
openDatabase().use { db ->
db.inWriteTransaction {
val values =
ContentValues().apply {
put("identity_key", identityKey)
put("device_id", identity.deviceId)
put("public_key_pem", identity.publicKeyPem)
put("private_key_pem", identity.privateKeyPem)
put("created_at_ms", identity.createdAtMs)
put("updated_at_ms", updatedAtMs)
}
db.insertWithOnConflict("device_identities", null, values, SQLiteDatabase.CONFLICT_REPLACE)
}
}
}
@Synchronized
fun readDeviceAuthToken(
deviceId: String,
role: String,
): OpenClawSQLiteDeviceAuthTokenRow? {
if (!databaseFile.exists()) return null
return openDatabase().use { db ->
db
.rawQuery(
"""
SELECT device_id, role, token, scopes_json, updated_at_ms
FROM device_auth_tokens
WHERE device_id = ? AND role = ?
""".trimIndent(),
arrayOf(deviceId, role),
).use { cursor ->
if (!cursor.moveToFirst()) return@use null
OpenClawSQLiteDeviceAuthTokenRow(
deviceId = cursor.getString(0),
role = cursor.getString(1),
token = cursor.getString(2),
scopesJson = cursor.getString(3),
updatedAtMs = cursor.getLong(4),
)
}
}
}
@Synchronized
fun readLatestDeviceAuthDeviceId(): String? {
if (!databaseFile.exists()) return null
return openDatabase().use { db ->
db
.rawQuery(
"""
SELECT device_id
FROM device_auth_tokens
ORDER BY updated_at_ms DESC, device_id ASC
LIMIT 1
""".trimIndent(),
emptyArray(),
).use { cursor ->
if (cursor.moveToFirst()) cursor.getString(0) else null
}
}
}
@Synchronized
fun upsertDeviceAuthToken(row: OpenClawSQLiteDeviceAuthTokenRow) {
openDatabase().use { db ->
db.inWriteTransaction {
val values =
ContentValues().apply {
put("device_id", row.deviceId)
put("role", row.role)
put("token", row.token)
put("scopes_json", row.scopesJson)
put("updated_at_ms", row.updatedAtMs)
}
db.insertWithOnConflict("device_auth_tokens", null, values, SQLiteDatabase.CONFLICT_REPLACE)
}
}
}
@Synchronized
fun deleteDeviceAuthToken(
deviceId: String,
role: String,
) {
openDatabase().use { db ->
db.inWriteTransaction {
db.delete("device_auth_tokens", "device_id = ? AND role = ?", arrayOf(deviceId, role))
}
}
}
@Synchronized
fun deleteAllDeviceAuthTokens() {
openDatabase().use { db ->
db.inWriteTransaction {
db.delete("device_auth_tokens", null, null)
}
}
}
@Synchronized
fun readRecentNotificationPackages(limit: Int = 64): List<String> {
if (!databaseFile.exists()) return emptyList()
return openDatabase().use { db ->
db
.rawQuery(
"""
SELECT package_name
FROM android_notification_recent_packages
ORDER BY sort_order ASC, package_name ASC
LIMIT ?
""".trimIndent(),
arrayOf(limit.coerceAtLeast(0).toString()),
).use { cursor ->
val packages = mutableListOf<String>()
while (cursor.moveToNext()) {
packages += cursor.getString(0)
}
packages
}
}
}
@Synchronized
fun replaceRecentNotificationPackages(
packageNames: List<String>,
limit: Int = 64,
updatedAtMs: Long = System.currentTimeMillis(),
) {
val normalized =
packageNames
.asSequence()
.map { it.trim() }
.filter { it.isNotEmpty() }
.distinct()
.take(limit.coerceAtLeast(0))
.toList()
openDatabase().use { db ->
db.inWriteTransaction {
db.delete("android_notification_recent_packages", null, null)
normalized.forEachIndexed { index, packageName ->
val values =
ContentValues().apply {
put("package_name", packageName)
put("sort_order", index)
put("updated_at_ms", updatedAtMs)
}
db.insertWithOnConflict(
"android_notification_recent_packages",
null,
values,
SQLiteDatabase.CONFLICT_REPLACE,
)
}
}
}
}
private fun openDatabase(): SQLiteDatabase {
databaseFile.parentFile?.mkdirs()
val db =
SQLiteDatabase.openDatabase(
databaseFile.absolutePath,
null,
SQLiteDatabase.OPEN_READWRITE or SQLiteDatabase.CREATE_IF_NECESSARY,
)
configure(db)
return db
}
private fun configure(db: SQLiteDatabase) {
db.enableWriteAheadLogging()
executePragma(db, "PRAGMA synchronous = NORMAL")
executePragma(db, "PRAGMA busy_timeout = 30000")
executePragma(db, "PRAGMA foreign_keys = ON")
db.execSQL(
"""
CREATE TABLE IF NOT EXISTS device_identities (
identity_key TEXT NOT NULL PRIMARY KEY,
device_id TEXT NOT NULL,
public_key_pem TEXT NOT NULL,
private_key_pem TEXT NOT NULL,
created_at_ms INTEGER NOT NULL,
updated_at_ms INTEGER NOT NULL
)
""".trimIndent(),
)
db.execSQL(
"""
CREATE INDEX IF NOT EXISTS idx_device_identities_device
ON device_identities(device_id, updated_at_ms DESC)
""".trimIndent(),
)
db.execSQL(
"""
CREATE TABLE IF NOT EXISTS device_auth_tokens (
device_id TEXT NOT NULL,
role TEXT NOT NULL,
token TEXT NOT NULL,
scopes_json TEXT NOT NULL,
updated_at_ms INTEGER NOT NULL,
PRIMARY KEY (device_id, role)
)
""".trimIndent(),
)
db.execSQL(
"""
CREATE INDEX IF NOT EXISTS idx_device_auth_tokens_updated
ON device_auth_tokens(updated_at_ms DESC, device_id, role)
""".trimIndent(),
)
db.execSQL(
"""
CREATE TABLE IF NOT EXISTS android_notification_recent_packages (
package_name TEXT NOT NULL PRIMARY KEY,
sort_order INTEGER NOT NULL,
updated_at_ms INTEGER NOT NULL
)
""".trimIndent(),
)
db.execSQL(
"""
CREATE INDEX IF NOT EXISTS idx_android_notification_recent_packages_order
ON android_notification_recent_packages(sort_order, package_name)
""".trimIndent(),
)
}
private fun executePragma(
db: SQLiteDatabase,
sql: String,
) {
db.rawQuery(sql, null).use { cursor ->
if (cursor.moveToFirst()) {
// Some PRAGMA assignments return their new value; reading it closes the cursor cleanly.
}
}
}
private inline fun SQLiteDatabase.inWriteTransaction(body: () -> Unit) {
beginTransaction()
try {
body()
setTransactionSuccessful()
} finally {
endTransaction()
}
}
}

View File

@@ -6,6 +6,9 @@ import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
/**
* Android bridge for applying gateway A2UI messages to the canvas WebView.
*/
class A2UIHandler(
private val canvas: CanvasController,
private val json: Json,
@@ -21,6 +24,7 @@ class A2UIHandler(
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('/')
@@ -36,6 +40,7 @@ class A2UIHandler(
}
canvas.navigate(a2uiUrl)
// A2UI host bootstraps asynchronously after navigation; poll briefly before failing the command.
repeat(50) {
try {
val ready = canvas.eval(a2uiReadyCheckJS)
@@ -65,6 +70,7 @@ class A2UIHandler(
if (command == "canvas.a2ui.pushJSONL" || (!hasMessagesArray && jsonlField.isNotBlank())) {
val jsonl = jsonlField
if (jsonl.isBlank()) throw IllegalArgumentException("INVALID_REQUEST: jsonl required")
// JSONL keeps large A2UI streams model-friendly while still validating each message.
val messages =
jsonl
.lineSequence()
@@ -98,6 +104,7 @@ class A2UIHandler(
lineNumber: Int,
) {
if (msg.containsKey("createSurface")) {
// Android scaffold currently implements A2UI v0.8, not the v0.9 createSurface shape.
throw IllegalArgumentException(
"A2UI JSONL line $lineNumber: looks like A2UI v0.9 (`createSurface`). Canvas supports v0.8 messages only.",
)

View File

@@ -20,12 +20,18 @@ import java.util.TimeZone
private const val DEFAULT_CALENDAR_LIMIT = 50
/**
* Parsed calendar.events request; times are epoch millis for CalendarContract queries.
*/
internal data class CalendarEventsRequest(
val startMs: Long,
val endMs: Long,
val limit: Int,
)
/**
* Parsed calendar.add request before resolving the target Android calendar.
*/
internal data class CalendarAddRequest(
val title: String,
val startMs: Long,
@@ -37,6 +43,9 @@ internal data class CalendarAddRequest(
val calendarTitle: String?,
)
/**
* Normalized calendar event returned through gateway calendar commands.
*/
internal data class CalendarEventRecord(
val identifier: String,
val title: String,
@@ -47,6 +56,9 @@ internal data class CalendarEventRecord(
val calendarTitle: String?,
)
/**
* Injectable CalendarProvider facade for command tests and Android runtime access.
*/
internal interface CalendarDataSource {
fun hasReadPermission(context: Context): Boolean
@@ -78,6 +90,7 @@ private object SystemCalendarDataSource : CalendarDataSource {
): List<CalendarEventRecord> {
val resolver = context.contentResolver
val builder = CalendarContract.Instances.CONTENT_URI.buildUpon()
// Instances expands recurring events inside the requested time window.
ContentUris.appendId(builder, request.startMs)
ContentUris.appendId(builder, request.endMs)
val projection =
@@ -155,10 +168,12 @@ private object SystemCalendarDataSource : CalendarDataSource {
calendarTitle: String?,
): Long {
if (calendarId != null) {
// Explicit id wins over title/default selection and must already exist.
if (calendarExists(resolver, calendarId)) return calendarId
throw IllegalArgumentException("CALENDAR_NOT_FOUND: no calendar id $calendarId")
}
if (!calendarTitle.isNullOrEmpty()) {
// Title lookup is exact to avoid adding events to a similarly named calendar.
findCalendarByTitle(resolver, calendarTitle)?.let { return it }
throw IllegalArgumentException("CALENDAR_NOT_FOUND: no calendar named $calendarTitle")
}
@@ -209,6 +224,7 @@ private object SystemCalendarDataSource : CalendarDataSource {
projection,
"${CalendarContract.Calendars.VISIBLE}=1",
null,
// Prefer Android's primary visible calendar, then lowest id for deterministic fallback.
"${CalendarContract.Calendars.IS_PRIMARY} DESC, ${CalendarContract.Calendars._ID} ASC",
).use { cursor ->
if (cursor == null || !cursor.moveToFirst()) return null
@@ -342,6 +358,7 @@ class CalendarHandler private constructor(
if (paramsJson.isNullOrBlank()) {
val start = Instant.now()
val end = start.plus(7, ChronoUnit.DAYS)
// Default calendar read is a one-week window, not the full calendar store.
return CalendarEventsRequest(startMs = start.toEpochMilli(), endMs = end.toEpochMilli(), limit = DEFAULT_CALENDAR_LIMIT)
}
val params =
@@ -354,6 +371,7 @@ class CalendarHandler private constructor(
val end = parseISO((params["endISO"] as? JsonPrimitive)?.content)
val resolvedStart = start ?: Instant.now()
val resolvedEnd = end ?: resolvedStart.plus(7, ChronoUnit.DAYS)
// Keep model-driven calendar reads bounded.
val limit = ((params["limit"] as? JsonPrimitive)?.content?.toIntOrNull() ?: DEFAULT_CALENDAR_LIMIT).coerceIn(1, 500)
return CalendarEventsRequest(
startMs = resolvedStart.toEpochMilli(),
@@ -390,6 +408,7 @@ class CalendarHandler private constructor(
private fun parseISO(raw: String?): Instant? {
val value = raw?.trim().orEmpty()
if (value.isEmpty()) return null
// Gateway calendar payloads use UTC ISO-8601 instants for unambiguous Android storage.
return try {
Instant.parse(value)
} catch (_: Throwable) {

View File

@@ -41,19 +41,25 @@ import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.math.roundToInt
/**
* CameraX-backed capture service used by gateway camera commands.
*/
class CameraCaptureManager(
private val context: Context,
) {
/** Base64 JSON response for camera.snap after resize and JPEG budget enforcement. */
data class Payload(
val payloadJson: String,
)
/** Temporary MP4 response for camera.clip before CameraHandler validates invoke size. */
data class FilePayload(
val file: File,
val durationMs: Long,
val hasAudio: Boolean,
)
/** Camera device metadata exposed through camera.list. */
data class CameraDeviceInfo(
val id: String,
val name: String,
@@ -65,14 +71,19 @@ class CameraCaptureManager(
@Volatile private var permissionRequester: PermissionRequester? = null
/** Supplies the foreground Activity lifecycle required by CameraX use-case binding. */
fun attachLifecycleOwner(owner: LifecycleOwner) {
// CameraX binds use cases to an Activity lifecycle; background services cannot capture alone.
lifecycleOwner = owner
}
/** Supplies the Activity-owned permission launcher used by camera and microphone commands. */
fun attachPermissionRequester(requester: PermissionRequester) {
// Permission prompts must be launched by the Activity that owns the ActivityResult registry.
permissionRequester = requester
}
/** Lists CameraX devices with stable Camera2 ids where available. */
suspend fun listDevices(): List<CameraDeviceInfo> =
withContext(Dispatchers.Main) {
val provider = context.cameraProvider()
@@ -107,6 +118,7 @@ class CameraCaptureManager(
}
}
/** Captures one still image and returns a gateway-sized JPEG payload. */
suspend fun snap(paramsJson: String?): Payload =
withContext(Dispatchers.Main) {
ensureCameraPermission()
@@ -122,6 +134,7 @@ class CameraCaptureManager(
val selector = resolveCameraSelector(provider, facing, deviceId)
provider.unbindAll()
// Bind only the still capture use case; CameraX owns camera open/close through the lifecycle owner.
provider.bindToLifecycle(owner, selector, capture)
val (bytes, orientation) = capture.takeJpegWithExif(context.mainExecutor(), context.cacheDir)
@@ -179,6 +192,7 @@ class CameraCaptureManager(
}
}
/** Records a short MP4 clip into a temporary cache file for the caller to encode/delete. */
@SuppressLint("MissingPermission")
suspend fun clip(paramsJson: String?): FilePayload =
withContext(Dispatchers.Main) {
@@ -303,6 +317,7 @@ class CameraCaptureManager(
orientation: Int,
): Bitmap {
val matrix = Matrix()
// CameraX JPEG bytes keep sensor orientation in EXIF; normalize before resizing/encoding.
when (orientation) {
ExifInterface.ORIENTATION_ROTATE_90 -> matrix.postRotate(90f)
ExifInterface.ORIENTATION_ROTATE_180 -> matrix.postRotate(180f)
@@ -365,6 +380,7 @@ class CameraCaptureManager(
}
return CameraSelector
.Builder()
// CameraX selectors are filters over CameraInfo; pin by Camera2 id for stable device selection.
.addCameraFilter { infos -> infos.filter { cameraIdOrNull(it) == deviceId } }
.build()
}
@@ -419,7 +435,9 @@ private suspend fun Context.cameraProvider(): ProcessCameraProvider =
)
}
/** Returns (jpegBytes, exifOrientation) so caller can rotate the decoded bitmap. */
/**
* Returns JPEG bytes plus EXIF orientation so callers can normalize the decoded bitmap.
*/
private suspend fun ImageCapture.takeJpegWithExif(
executor: Executor,
tempDir: File,

View File

@@ -3,6 +3,7 @@ package ai.openclaw.app.node
import ai.openclaw.app.BuildConfig
import ai.openclaw.app.CameraHudKind
import ai.openclaw.app.gateway.GatewaySession
import android.content.Context
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.withContext
@@ -15,15 +16,23 @@ import kotlinx.serialization.json.put
internal const val CAMERA_CLIP_MAX_RAW_BYTES: Long = 18L * 1024L * 1024L
/**
* Raw MP4 size guard before base64 encoding the clip into a node.invoke response.
*/
internal fun isCameraClipWithinPayloadLimit(rawBytes: Long): Boolean = rawBytes in 0L..CAMERA_CLIP_MAX_RAW_BYTES
/**
* Gateway camera command adapter that adds HUD feedback and payload-size enforcement.
*/
class CameraHandler(
private val appContext: Context,
private val camera: CameraCaptureManager,
private val externalAudioCaptureActive: MutableStateFlow<Boolean>,
private val showCameraHud: (message: String, kind: CameraHudKind, autoHideMs: Long?) -> Unit,
private val triggerCameraFlash: () -> Unit,
private val invokeErrorFromThrowable: (err: Throwable) -> Pair<String, String>,
) {
/** Handles camera.list by exposing CameraX devices through gateway metadata. */
suspend fun handleList(_paramsJson: String?): GatewaySession.InvokeResult =
try {
val devices = camera.listDevices()
@@ -51,13 +60,18 @@ class CameraHandler(
GatewaySession.InvokeResult.error(code = code, message = message)
}
/** Handles camera.snap with HUD progress, flash feedback, and normalized invoke errors. */
suspend fun handleSnap(paramsJson: String?): GatewaySession.InvokeResult {
val logFile = if (BuildConfig.DEBUG) java.io.File(appContext.cacheDir, "camera_debug.log") else null
fun camLog(msg: String) {
if (!BuildConfig.DEBUG) return
val ts = java.text.SimpleDateFormat("HH:mm:ss.SSS", java.util.Locale.US).format(java.util.Date())
android.util.Log.w("openclaw", "camera.snap[$ts]: $msg")
logFile?.appendText("[$ts] $msg\n")
android.util.Log.w("openclaw", "camera.snap: $msg")
}
try {
logFile?.writeText("") // clear
camLog("starting, params=$paramsJson")
camLog("calling showCameraHud")
showCameraHud("Taking photo…", CameraHudKind.Photo, null)
@@ -86,15 +100,20 @@ class CameraHandler(
}
}
/** Handles camera.clip and keeps external audio capture paused while camera audio is active. */
suspend fun handleClip(paramsJson: String?): GatewaySession.InvokeResult {
val clipLogFile = if (BuildConfig.DEBUG) java.io.File(appContext.cacheDir, "camera_debug.log") else null
fun clipLog(msg: String) {
if (!BuildConfig.DEBUG) return
val ts = java.text.SimpleDateFormat("HH:mm:ss.SSS", java.util.Locale.US).format(java.util.Date())
android.util.Log.w("openclaw", "camera.clip[$ts]: $msg")
clipLogFile?.appendText("[CLIP $ts] $msg\n")
android.util.Log.w("openclaw", "camera.clip: $msg")
}
val includeAudio = parseIncludeAudio(paramsJson) ?: true
if (includeAudio) externalAudioCaptureActive.value = true
try {
clipLogFile?.writeText("") // clear
clipLog("starting, params=$paramsJson includeAudio=$includeAudio")
clipLog("calling showCameraHud")
showCameraHud("Recording…", CameraHudKind.Recording, null)
@@ -114,6 +133,7 @@ class CameraHandler(
val rawBytes = filePayload.file.length()
if (!isCameraClipWithinPayloadLimit(rawBytes)) {
clipLog("payload too large: bytes=$rawBytes max=$CAMERA_CLIP_MAX_RAW_BYTES")
// Delete oversized clips before returning so cache files do not accumulate after failed invokes.
withContext(Dispatchers.IO) { filePayload.file.delete() }
showCameraHud("Clip too large", CameraHudKind.Error, 2400)
return GatewaySession.InvokeResult.error(
@@ -142,6 +162,7 @@ class CameraHandler(
clipLog("stack: ${err.stackTraceToString().take(2000)}")
return GatewaySession.InvokeResult.error(code = "UNAVAILABLE", message = err.message ?: "camera clip failed")
} finally {
// Prevent talk/transcription capture from competing with camera audio after every exit path.
if (includeAudio) externalAudioCaptureActive.value = false
}
}

View File

@@ -2,9 +2,14 @@ package ai.openclaw.app.node
import java.net.URI
/**
* Trust helper for WebView-originated canvas/A2UI actions.
*/
object CanvasActionTrust {
/** Local canvas scaffold is the only trusted file URL. */
const val scaffoldAssetUrl: String = "file:///android_asset/CanvasScaffold/scaffold.html"
/** Accepts local scaffold or exact remote A2UI URLs advertised by the gateway. */
fun isTrustedCanvasActionUrl(
rawUrl: String?,
trustedA2uiUrls: List<String>,
@@ -28,11 +33,14 @@ object CanvasActionTrust {
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.
@@ -52,6 +60,7 @@ object CanvasActionTrust {
}
}
/** Parses untrusted WebView/gateway URL text without throwing into UI event handlers. */
private fun parseUri(raw: String): URI? =
try {
URI(raw)

View File

@@ -23,6 +23,9 @@ import org.json.JSONObject
import java.io.ByteArrayOutputStream
import kotlin.coroutines.resume
/**
* Owns the Android WebView canvas surface used by canvas and A2UI commands.
*/
class CanvasController {
enum class SnapshotFormat(
val rawValue: String,
@@ -60,19 +63,23 @@ class CanvasController {
return scale(maxWidth, scaledHeight)
}
/** Attaches the active WebView and replays state that may have arrived before the view existed. */
fun attach(webView: WebView) {
this.webView = webView
// Replay persisted state because WebView attachment can happen after gateway events arrive.
reload()
applyDebugStatus()
applyHomeCanvasState()
}
/** Detaches only the currently attached WebView instance. */
fun detach(webView: WebView) {
if (this.webView === webView) {
this.webView = null
}
}
/** Navigates the canvas to a remote URL or back to the bundled scaffold for blank/root input. */
fun navigate(url: String) {
val trimmed = url.trim()
this.url = if (trimmed.isBlank() || trimmed == "/") null else trimmed
@@ -113,6 +120,7 @@ class CanvasController {
if (Looper.myLooper() == Looper.getMainLooper()) {
block(wv)
} else {
// WebView APIs must run on the main thread.
wv.post { block(wv) }
}
}
@@ -178,6 +186,7 @@ class CanvasController {
}
}
/** Evaluates JavaScript against the attached WebView on the main thread. */
suspend fun eval(javaScript: String): String =
withContext(Dispatchers.Main) {
val wv = webView ?: throw IllegalStateException("no webview")
@@ -206,6 +215,7 @@ class CanvasController {
}
}
/** Captures the WebView as PNG/JPEG base64 with optional width and quality bounds. */
suspend fun snapshotBase64(
format: SnapshotFormat,
quality: Double?,
@@ -246,17 +256,22 @@ class CanvasController {
}
companion object {
/**
* Parsed canvas.snapshot options used by invoke dispatch.
*/
data class SnapshotParams(
val format: SnapshotFormat,
val quality: Double?,
val maxWidth: Int?,
)
/** Parses canvas.navigate params and returns blank when the payload is missing or invalid. */
fun parseNavigateUrl(paramsJson: String?): String {
val obj = parseParamsObject(paramsJson) ?: return ""
return obj.string("url").trim()
}
/** Parses non-blank JavaScript from canvas.eval params. */
fun parseEvalJs(paramsJson: String?): String? {
val obj = parseParamsObject(paramsJson) ?: return null
val js = obj.string("javaScript").trim()
@@ -286,9 +301,11 @@ class CanvasController {
if (!obj.containsKey("quality")) return null
val q = obj.double("quality") ?: Double.NaN
if (!q.isFinite()) return null
// Keep JPEG quality inside encoder-safe bounds; PNG ignores it.
return q.coerceIn(0.1, 1.0)
}
/** Parses canvas.snapshot params using JPEG defaults and encoder-safe bounds. */
fun parseSnapshotParams(paramsJson: String?): SnapshotParams =
SnapshotParams(
format = parseSnapshotFormat(paramsJson),

View File

@@ -12,6 +12,9 @@ import ai.openclaw.app.gateway.isLocalCleartextGatewayHost
import ai.openclaw.app.gateway.isLoopbackGatewayHost
import android.os.Build
/**
* Builds gateway connect metadata from current Android permissions, settings, and device identity.
*/
class ConnectionManager(
private val prefs: SecurePrefs,
private val cameraEnabled: () -> Boolean,
@@ -28,6 +31,9 @@ class ConnectionManager(
private val manualTls: () -> Boolean,
) {
companion object {
/**
* Decide whether a discovered/manual endpoint must use pinned TLS or can stay local cleartext.
*/
internal fun resolveTlsParamsForEndpoint(
endpoint: GatewayEndpoint,
storedFingerprint: String?,
@@ -44,6 +50,7 @@ class ConnectionManager(
}
if (isManual) {
// Manual remote hosts default to TLS; only local manual hosts may honor the cleartext toggle.
if (!manualTlsEnabled && cleartextAllowedHost) return null
if (!stored.isNullOrBlank()) {
return GatewayTlsParams(
@@ -83,6 +90,7 @@ class ConnectionManager(
}
if (!cleartextAllowedHost) {
// Non-loopback discovered hosts require TLS even without TXT hints.
return GatewayTlsParams(
required = true,
expectedFingerprint = null,
@@ -110,10 +118,15 @@ class ConnectionManager(
debugBuild = BuildConfig.DEBUG,
)
/** Builds the gateway-advertised node.invoke command list from current permission and feature state. */
fun buildInvokeCommands(): List<String> = InvokeCommandRegistry.advertisedCommands(runtimeFlags())
/** Builds the gateway-advertised capability list from current permission and feature state. */
fun buildCapabilities(): List<String> = InvokeCommandRegistry.advertisedCapabilities(runtimeFlags())
/**
* Debug Android builds advertise a dev version so gateway logs do not look like release clients.
*/
fun resolvedVersionName(): String {
val versionName = BuildConfig.VERSION_NAME.trim().ifEmpty { "dev" }
return if (BuildConfig.DEBUG && !versionName.contains("dev", ignoreCase = true)) {
@@ -123,12 +136,16 @@ class ConnectionManager(
}
}
/** Human-readable Android device model used in gateway client metadata. */
fun resolveModelIdentifier(): String? =
listOfNotNull(Build.MANUFACTURER, Build.MODEL)
.joinToString(" ")
.trim()
.ifEmpty { null }
/**
* User-Agent used for gateway telemetry and troubleshooting.
*/
fun buildUserAgent(): String {
val version = resolvedVersionName()
val release =
@@ -139,6 +156,7 @@ class ConnectionManager(
return "OpenClawAndroid/$version (Android $releaseLabel; SDK ${Build.VERSION.SDK_INT})"
}
/** Client identity block shared by node and operator gateway sessions. */
fun buildClientInfo(
clientId: String,
clientMode: String,
@@ -154,6 +172,7 @@ class ConnectionManager(
modelIdentifier = resolveModelIdentifier(),
)
/** Connect options for the Android node session that exposes phone capabilities. */
fun buildNodeConnectOptions(): GatewayConnectOptions =
GatewayConnectOptions(
role = "node",
@@ -165,6 +184,7 @@ class ConnectionManager(
userAgent = buildUserAgent(),
)
/** Connect options for the Android operator session that drives approvals and UI actions. */
fun buildOperatorConnectOptions(): GatewayConnectOptions =
GatewayConnectOptions(
role = "operator",
@@ -181,6 +201,7 @@ class ConnectionManager(
userAgent = buildUserAgent(),
)
/** Resolves persisted TLS pin policy for a concrete gateway endpoint. */
fun resolveTlsParams(endpoint: GatewayEndpoint): GatewayTlsParams? {
val stored = prefs.loadGatewayTlsFingerprint(endpoint.stableId)
return resolveTlsParamsForEndpoint(endpoint, storedFingerprint = stored, manualTlsEnabled = manualTls())

View File

@@ -17,6 +17,9 @@ import kotlinx.serialization.json.put
private const val DEFAULT_CONTACTS_LIMIT = 25
/**
* Normalized Android contact row returned through the contacts commands.
*/
internal data class ContactRecord(
val identifier: String,
val displayName: String,
@@ -27,11 +30,17 @@ internal data class ContactRecord(
val emails: List<String>,
)
/**
* Parsed contacts.search request with bounded result count.
*/
internal data class ContactsSearchRequest(
val query: String?,
val limit: Int,
)
/**
* Parsed contacts.add request before ContentProviderOperation batching.
*/
internal data class ContactsAddRequest(
val givenName: String?,
val familyName: String?,
@@ -41,6 +50,9 @@ internal data class ContactsAddRequest(
val emails: List<String>,
)
/**
* Injectable ContactsProvider facade for command tests and Android runtime access.
*/
internal interface ContactsDataSource {
fun hasReadPermission(context: Context): Boolean
@@ -82,6 +94,7 @@ private object SystemContactsDataSource : ContactsDataSource {
selection = null
selectionArgs = null
} else {
// Escape wildcard characters so user text remains a substring search, not a LIKE pattern.
selection = "${ContactsContract.Contacts.DISPLAY_NAME_PRIMARY} LIKE ? ESCAPE '\\'"
selectionArgs = arrayOf("%${escapeLikePattern(request.query)}%")
}
@@ -119,6 +132,7 @@ private object SystemContactsDataSource : ContactsDataSource {
.withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, null)
.withValue(ContactsContract.RawContacts.ACCOUNT_NAME, null)
.build()
// Subsequent Data rows use back-reference 0 to attach to the RawContact inserted above.
if (!request.givenName.isNullOrEmpty() || !request.familyName.isNullOrEmpty() || !request.displayName.isNullOrEmpty()) {
operations +=
ContentProviderOperation
@@ -168,6 +182,7 @@ private object SystemContactsDataSource : ContactsDataSource {
rawContactUri.lastPathSegment?.toLongOrNull()
?: throw IllegalStateException("contact insert failed")
val contactId =
// Android returns the RawContact id; resolve the aggregate Contact id used by search APIs.
resolveContactIdForRawContact(resolver, rawContactId)
?: throw IllegalStateException("contact insert failed")
return loadContactRecord(
@@ -330,12 +345,16 @@ private object SystemContactsDataSource : ContactsDataSource {
}
}
/**
* Handles contacts.search and contacts.add gateway commands through Android ContactsProvider.
*/
class ContactsHandler private constructor(
private val appContext: Context,
private val dataSource: ContactsDataSource,
) {
constructor(appContext: Context) : this(appContext = appContext, dataSource = SystemContactsDataSource)
/** Searches contacts by optional display-name substring with bounded result count. */
fun handleContactsSearch(paramsJson: String?): GatewaySession.InvokeResult {
if (!dataSource.hasReadPermission(appContext)) {
return GatewaySession.InvokeResult.error(
@@ -369,6 +388,7 @@ class ContactsHandler private constructor(
}
}
/** Adds a local contact after validating that at least one user-visible field is present. */
fun handleContactsAdd(paramsJson: String?): GatewaySession.InvokeResult {
if (!dataSource.hasWritePermission(appContext)) {
return GatewaySession.InvokeResult.error(
@@ -418,6 +438,7 @@ class ContactsHandler private constructor(
null
} ?: return null
val query = (params["query"] as? JsonPrimitive)?.content?.trim()?.ifEmpty { null }
// Keep gateway-driven searches bounded even if the model asks for a large contact dump.
val limit = ((params["limit"] as? JsonPrimitive)?.content?.toIntOrNull() ?: DEFAULT_CONTACTS_LIMIT).coerceIn(1, 200)
return ContactsSearchRequest(query = query, limit = limit)
}
@@ -435,6 +456,7 @@ class ContactsHandler private constructor(
organizationName = (params["organizationName"] as? JsonPrimitive)?.content?.trim()?.ifEmpty { null },
displayName = (params["displayName"] as? JsonPrimitive)?.content?.trim()?.ifEmpty { null },
phoneNumbers = stringArray(params["phoneNumbers"] as? JsonArray),
// Store emails case-normalized so repeated model calls do not create casing-only duplicates.
emails = stringArray(params["emails"] as? JsonArray).map { it.lowercase() },
)
}
@@ -458,6 +480,7 @@ class ContactsHandler private constructor(
}
companion object {
/** Creates a handler with an injected contacts source for parser and payload tests. */
internal fun forTesting(
appContext: Context,
dataSource: ContactsDataSource,

View File

@@ -3,23 +3,26 @@ package ai.openclaw.app.node
import ai.openclaw.app.BuildConfig
import ai.openclaw.app.gateway.DeviceIdentityStore
import ai.openclaw.app.gateway.GatewaySession
import android.content.Context
import kotlinx.serialization.json.JsonPrimitive
import java.io.InputStream
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicReference
private const val LOGCAT_PATH = "/system/bin/logcat"
private const val LOGCAT_TIMEOUT_MS = 4_000L
private const val LOGCAT_MAX_CHARS = 128_000
/**
* Debug-only node.invoke commands for Android cryptography and log diagnostics.
*/
class DebugHandler(
private val appContext: Context,
private val identityStore: DeviceIdentityStore,
) {
/**
* Runs an Ed25519 self-test and returns redacted diagnostics for debug builds.
*/
fun handleEd25519(): GatewaySession.InvokeResult {
if (!BuildConfig.DEBUG) {
return GatewaySession.InvokeResult.error(code = "UNAVAILABLE", message = "debug commands are disabled in release builds")
}
// Self-test Ed25519 signing and return diagnostic info
// Self-test Ed25519 signing without returning full private/public key material.
try {
val identity = identityStore.loadOrCreate()
val testPayload = "test|${identity.deviceId}|${System.currentTimeMillis()}"
@@ -28,15 +31,14 @@ class DebugHandler(
results.add("publicKeyRawBase64: ${identity.publicKeyRawBase64.take(20)}...")
results.add("privateKeyPkcs8Base64: ${identity.privateKeyPkcs8Base64.take(20)}...")
// Test publicKeyBase64Url
// Public-key URL encoding must match the gateway device-auth payload contract.
val pubKeyUrl = identityStore.publicKeyBase64Url(identity)
results.add("publicKeyBase64Url: ${pubKeyUrl ?: "NULL (FAILED)"}")
// Test signing
// Sign/verify through DeviceIdentityStore to catch provider and key-format failures together.
val signature = identityStore.signPayload(testPayload, identity)
results.add("signPayload: ${if (signature != null) "${signature.take(20)}... (OK)" else "NULL (FAILED)"}")
// Test self-verify
if (signature != null) {
val verifyOk = identityStore.verifySelfSignature(testPayload, signature, identity)
results.add("verifySelfSignature: $verifyOk")
@@ -77,6 +79,9 @@ class DebugHandler(
}
}
/**
* Returns a filtered logcat snapshot plus CameraX debug log for debug builds.
*/
fun handleLogs(): GatewaySession.InvokeResult {
if (!BuildConfig.DEBUG) {
return GatewaySession.InvokeResult.error(code = "UNAVAILABLE", message = "debug commands are disabled in release builds")
@@ -84,14 +89,24 @@ class DebugHandler(
val pid = android.os.Process.myPid()
val rt = Runtime.getRuntime()
val info = "v6 pid=$pid thread=${Thread.currentThread().name} free=${rt.freeMemory() / 1024}K total=${rt.totalMemory() / 1024}K max=${rt.maxMemory() / 1024}K uptime=${android.os.SystemClock.elapsedRealtime() / 1000}s sdk=${android.os.Build.VERSION.SDK_INT} device=${android.os.Build.MODEL}\n"
// Run logcat on current dispatcher thread; output is bounded by -t and never staged to disk.
// Capture only this process and redirect through a temp file to avoid blocking on pipe backpressure.
val logResult =
try {
val tmpFile = java.io.File(appContext.cacheDir, "debug_logs.txt")
if (tmpFile.exists()) tmpFile.delete()
val pb = ProcessBuilder(LOGCAT_PATH, "-d", "-t", "200", "--pid=$pid")
pb.redirectOutput(tmpFile)
pb.redirectErrorStream(true)
val proc = pb.start()
val (finished, raw) = collectProcessOutput(proc, LOGCAT_TIMEOUT_MS, LOGCAT_MAX_CHARS)
val normalizedRaw = raw.ifBlank { "(no output, finished=$finished)" }
val finished = proc.waitFor(4, java.util.concurrent.TimeUnit.SECONDS)
if (!finished) proc.destroyForcibly()
val raw =
if (tmpFile.exists() && tmpFile.length() > 0) {
tmpFile.readText().take(128000)
} else {
"(no output, finished=$finished, exists=${tmpFile.exists()})"
}
tmpFile.delete()
val spamPatterns =
listOf(
"setRequestedFrameRate",
@@ -112,65 +127,29 @@ class DebugHandler(
"IncorrectContextUseViolation",
)
val sb = StringBuilder()
for (line in normalizedRaw.lineSequence()) {
for (line in raw.lineSequence()) {
if (line.isBlank()) continue
if (spamPatterns.any { line.contains(it) }) continue
if (sb.length + line.length > 16000) {
// Keep debug.invoke responses small enough for the gateway WebSocket frame budget.
sb.append("\n(truncated)")
break
}
if (sb.isNotEmpty()) sb.append('\n')
sb.append(line)
}
sb.toString().ifEmpty { "(all ${normalizedRaw.lines().size} lines filtered as spam)" }
sb.toString().ifEmpty { "(all ${raw.lines().size} lines filtered as spam)" }
} catch (e: Throwable) {
"(logcat error: ${e::class.java.simpleName}: ${e.message})"
}
return GatewaySession.InvokeResult.ok("""{"logs":${JsonPrimitive(info + logResult)}}""")
// Camera capture writes a separate debug file because CameraX failures often happen off logcat's hot path.
val camLogFile = java.io.File(appContext.cacheDir, "camera_debug.log")
val camLog =
if (camLogFile.exists() && camLogFile.length() > 0) {
"\n--- camera_debug.log ---\n" + camLogFile.readText().take(4000)
} else {
""
}
return GatewaySession.InvokeResult.ok("""{"logs":${JsonPrimitive(info + logResult + camLog)}}""")
}
}
internal fun collectProcessOutput(
process: Process,
timeoutMs: Long,
maxChars: Int,
): Pair<Boolean, String> {
val output = AtomicReference("")
val failure = AtomicReference<Throwable?>(null)
val reader =
Thread({
try {
output.set(readBoundedText(process.inputStream, maxChars))
} catch (error: Throwable) {
failure.set(error)
}
}, "openclaw-debug-output-reader")
reader.isDaemon = true
reader.start()
val finished = process.waitFor(timeoutMs, TimeUnit.MILLISECONDS)
if (!finished) {
process.destroyForcibly()
}
reader.join(1_000)
failure.get()?.let { throw it }
return finished to output.get()
}
private fun readBoundedText(
stream: InputStream,
maxChars: Int,
): String =
stream.bufferedReader().use { reader ->
val out = StringBuilder(minOf(maxChars, 8192))
val buffer = CharArray(4096)
while (true) {
val read = reader.read(buffer)
if (read < 0) break
val remaining = maxChars - out.length
if (remaining > 0) {
out.append(buffer, 0, minOf(read, remaining))
}
}
out.toString()
}

View File

@@ -24,6 +24,9 @@ import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
import java.util.Locale
/**
* Gateway device command adapter for Android status, info, permission, and health snapshots.
*/
class DeviceHandler(
private val appContext: Context,
private val smsEnabled: Boolean = SensitiveFeatureConfig.smsEnabled,
@@ -31,6 +34,9 @@ class DeviceHandler(
private val photosEnabled: Boolean = SensitiveFeatureConfig.photosEnabled,
) {
companion object {
/**
* SMS is available only when the feature flag, telephony hardware, and at least one SMS permission align.
*/
internal fun hasAnySmsCapability(
smsEnabled: Boolean,
telephonyAvailable: Boolean,
@@ -38,6 +44,9 @@ class DeviceHandler(
smsReadGranted: Boolean,
): Boolean = smsEnabled && telephonyAvailable && (smsSendGranted || smsReadGranted)
/**
* Prompt only when Android can grant a missing SMS permission that this build can use.
*/
internal fun isSmsPromptable(
smsEnabled: Boolean,
telephonyAvailable: Boolean,
@@ -53,12 +62,16 @@ class DeviceHandler(
val temperatureC: Double?,
)
/** Returns battery, storage, network, and uptime state for device.status. */
fun handleDeviceStatus(_paramsJson: String?): GatewaySession.InvokeResult = GatewaySession.InvokeResult.ok(statusPayloadJson())
/** Returns stable Android hardware, OS, app, and locale metadata for device.info. */
fun handleDeviceInfo(_paramsJson: String?): GatewaySession.InvokeResult = GatewaySession.InvokeResult.ok(infoPayloadJson())
/** Returns permission and promptability state for Android capabilities exposed to the gateway. */
fun handleDevicePermissions(_paramsJson: String?): GatewaySession.InvokeResult = GatewaySession.InvokeResult.ok(permissionsPayloadJson())
/** Returns coarse device health for memory, power, thermal, battery, and security patch state. */
fun handleDeviceHealth(_paramsJson: String?): GatewaySession.InvokeResult = GatewaySession.InvokeResult.ok(healthPayloadJson())
private fun statusPayloadJson(): String {
@@ -71,6 +84,7 @@ class DeviceHandler(
val connectivity = appContext.getSystemService(ConnectivityManager::class.java)
val activeNetwork = connectivity?.activeNetwork
val caps = activeNetwork?.let { connectivity.getNetworkCapabilities(it) }
// elapsedRealtime is monotonic device uptime, not wall-clock time.
val uptimeSeconds = SystemClock.elapsedRealtime() / 1_000.0
return buildJsonObject {
@@ -154,6 +168,7 @@ class DeviceHandler(
if (!photosEnabled) {
false
} else if (Build.VERSION.SDK_INT >= 33) {
// Android 13 split media permissions; earlier versions use external storage.
hasPermission(Manifest.permission.READ_MEDIA_IMAGES)
} else {
hasPermission(Manifest.permission.READ_EXTERNAL_STORAGE)
@@ -161,6 +176,7 @@ class DeviceHandler(
val motionGranted = hasPermission(Manifest.permission.ACTIVITY_RECOGNITION)
val notificationsGranted =
if (Build.VERSION.SDK_INT >= 33) {
// POST_NOTIFICATIONS exists only on Android 13+.
hasPermission(Manifest.permission.POST_NOTIFICATIONS)
} else {
true
@@ -295,6 +311,7 @@ class DeviceHandler(
if (currentNowUa == null || currentNowUa == Long.MIN_VALUE) {
null
} else {
// BatteryManager reports microamps; expose milliamps in the gateway payload.
currentNowUa.toDouble() / 1_000.0
}
@@ -349,6 +366,7 @@ class DeviceHandler(
}
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))
val status =
intent?.getIntExtra(BatteryManager.EXTRA_STATUS, BatteryManager.BATTERY_STATUS_UNKNOWN)
@@ -410,6 +428,7 @@ class DeviceHandler(
if (caps == null) return "unsatisfied"
return when {
caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) -> "satisfied"
// Internet without validation mirrors iOS "requiresConnection" for captive or unproven networks.
caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) -> "requiresConnection"
else -> "unsatisfied"
}
@@ -436,6 +455,7 @@ class DeviceHandler(
if (totalBytes <= 0L) return if (lowMemory) "critical" else "unknown"
if (lowMemory) return "critical"
val freeRatio = availableBytes.toDouble() / totalBytes.toDouble()
// Thresholds intentionally mirror coarse OS health labels instead of exact memory pressure.
return when {
freeRatio <= 0.05 -> "critical"
freeRatio <= 0.15 -> "high"

View File

@@ -3,7 +3,6 @@ package ai.openclaw.app.node
import ai.openclaw.app.NotificationBurstLimiter
import ai.openclaw.app.SecurePrefs
import ai.openclaw.app.allowsPackage
import ai.openclaw.app.gateway.OpenClawSQLiteStateStore
import ai.openclaw.app.isWithinQuietHours
import android.app.Notification
import android.app.NotificationManager
@@ -13,6 +12,7 @@ import android.content.Context
import android.content.Intent
import android.service.notification.NotificationListenerService
import android.service.notification.StatusBarNotification
import androidx.core.content.edit
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.buildJsonObject
@@ -21,11 +21,18 @@ import kotlinx.serialization.json.put
private const val MAX_NOTIFICATION_TEXT_CHARS = 512
private const val NOTIFICATIONS_CHANGED_EVENT = "notifications.changed"
/**
* Trims notification text and caps payload size before it enters gateway-visible state.
*/
internal fun sanitizeNotificationText(value: CharSequence?): String? {
val normalized = value?.toString()?.trim().orEmpty()
// Notification extras can include long previews; cap before sending over node events.
return normalized.take(MAX_NOTIFICATION_TEXT_CHARS).ifEmpty { null }
}
/**
* Stable notification snapshot entry exposed through the Android notifications command.
*/
data class DeviceNotificationEntry(
val key: String,
val packageName: String,
@@ -53,24 +60,36 @@ internal fun DeviceNotificationEntry.toJsonObject(): JsonObject =
channelId?.let { put("channelId", JsonPrimitive(it)) }
}
/**
* Listener state exposed to the gateway, including whether Android has connected the service.
*/
data class DeviceNotificationSnapshot(
val enabled: Boolean,
val connected: Boolean,
val notifications: List<DeviceNotificationEntry>,
)
/**
* Gateway-supported notification actions mapped to Android listener operations.
*/
enum class NotificationActionKind {
Open,
Dismiss,
Reply,
}
/**
* Gateway action request; [key] must match Android's StatusBarNotification key.
*/
data class NotificationActionRequest(
val key: String,
val kind: NotificationActionKind,
val replyText: String? = null,
)
/**
* Normalized notification action result returned through node.invoke.
*/
data class NotificationActionResult(
val ok: Boolean,
val code: String? = null,
@@ -79,6 +98,9 @@ data class NotificationActionResult(
internal fun actionRequiresClearableNotification(kind: NotificationActionKind): Boolean = kind == NotificationActionKind.Dismiss
/**
* Process-local cache of active notifications mirrored from Android listener callbacks.
*/
private object DeviceNotificationStore {
private val lock = Any()
private var connected = false
@@ -109,6 +131,7 @@ private object DeviceNotificationStore {
synchronized(lock) {
connected = value
if (!value) {
// Android invalidates activeNotifications when the listener disconnects.
byKey.clear()
}
}
@@ -127,6 +150,9 @@ private object DeviceNotificationStore {
}
}
/**
* Android notification listener that mirrors notification state and executes gateway actions.
*/
class DeviceNotificationListenerService : NotificationListenerService() {
private val securePrefs by lazy { SecurePrefs(applicationContext) }
private val forwardingLimiter = NotificationBurstLimiter()
@@ -226,6 +252,7 @@ class DeviceNotificationListenerService : NotificationListenerService() {
if (policy.isWithinQuietHours(nowEpochMs = nowEpochMs)) {
return null
}
// Apply burst limits after package/quiet-hour filters so blocked notifications do not consume quota.
if (!forwardingLimiter.allow(nowEpochMs, policy.maxEventsPerMinute)) {
return null
}
@@ -278,9 +305,8 @@ class DeviceNotificationListenerService : NotificationListenerService() {
}
companion object {
private const val notificationsPrefsPrefix = "notifications."
private const val recentPackagesPref = notificationsPrefsPrefix + "forwarding.recentPackages"
private const val legacyRecentPackagesPref = notificationsPrefsPrefix + "recentPackages"
private const val recentPackagesPref = "notifications.forwarding.recentPackages"
private const val legacyRecentPackagesPref = "notifications.recentPackages"
private const val recentPackagesLimit = 64
@Volatile private var activeService: DeviceNotificationListenerService? = null
@@ -289,67 +315,60 @@ class DeviceNotificationListenerService : NotificationListenerService() {
private fun serviceComponent(context: Context): ComponentName = ComponentName(context, DeviceNotificationListenerService::class.java)
/** Installs the node event sink used to emit filtered notification change events. */
fun setNodeEventSink(sink: ((event: String, payloadJson: String?) -> Unit)?) {
nodeEventSink = sink
}
private fun recentPackagesPrefs(context: Context) =
context.applicationContext
.getSharedPreferences("openclaw.secure", Context.MODE_PRIVATE)
private fun recentPackagesPrefs(context: Context) = context.applicationContext.getSharedPreferences("openclaw.secure", Context.MODE_PRIVATE)
private fun migrateLegacyRecentPackagesIfNeeded(
context: Context,
stateStore: OpenClawSQLiteStateStore,
): List<String> {
private fun migrateLegacyRecentPackagesIfNeeded(context: Context) {
val prefs = recentPackagesPrefs(context)
val raw =
prefs.getString(recentPackagesPref, null)?.trim()?.takeIf { it.isNotEmpty() }
?: prefs.getString(legacyRecentPackagesPref, null)?.trim().orEmpty()
val packages =
raw
.split(',')
.map { it.trim() }
.filter { it.isNotEmpty() }
.distinct()
.take(recentPackagesLimit)
if (packages.isNotEmpty()) {
stateStore.replaceRecentNotificationPackages(packages, recentPackagesLimit)
val hasNew = prefs.contains(recentPackagesPref)
val legacy = prefs.getString(legacyRecentPackagesPref, null)?.trim().orEmpty()
if (!hasNew && legacy.isNotEmpty()) {
// Keep recent package suggestions across the preference-key rename.
prefs.edit {
putString(recentPackagesPref, legacy)
remove(legacyRecentPackagesPref)
}
} else if (hasNew && prefs.contains(legacyRecentPackagesPref)) {
prefs.edit { remove(legacyRecentPackagesPref) }
}
if (prefs.contains(recentPackagesPref) || prefs.contains(legacyRecentPackagesPref)) {
prefs
.edit()
.remove(recentPackagesPref)
.remove(legacyRecentPackagesPref)
.apply()
}
return packages
}
/** Returns recent third-party packages seen by the listener for settings suggestions. */
fun recentPackages(context: Context): List<String> {
val stateStore = OpenClawSQLiteStateStore(context)
val stored = stateStore.readRecentNotificationPackages(recentPackagesLimit)
if (stored.isNotEmpty()) {
return stored
}
return migrateLegacyRecentPackagesIfNeeded(context, stateStore)
migrateLegacyRecentPackagesIfNeeded(context)
val prefs = recentPackagesPrefs(context)
val stored = prefs.getString(recentPackagesPref, null).orEmpty()
return stored
.split(',')
.map { it.trim() }
.filter { it.isNotEmpty() }
.distinct()
}
/** Checks whether Android has granted listener access to this service component. */
fun isAccessEnabled(context: Context): Boolean {
val manager = context.getSystemService(NotificationManager::class.java) ?: return false
return manager.isNotificationListenerAccessGranted(serviceComponent(context))
}
/** Reads the current mirrored notification snapshot without forcing service startup. */
fun snapshot(
context: Context,
enabled: Boolean = isAccessEnabled(context),
): DeviceNotificationSnapshot = DeviceNotificationStore.snapshot(enabled = enabled)
/** Asks Android to rebind the listener after settings grant access but callbacks have not arrived. */
fun requestServiceRebind(context: Context) {
runCatching {
NotificationListenerService.requestRebind(serviceComponent(context))
}
}
/** Executes an open, dismiss, or reply action through the active listener instance. */
fun executeAction(
context: Context,
request: NotificationActionRequest,
@@ -381,13 +400,19 @@ class DeviceNotificationListenerService : NotificationListenerService() {
val service = activeService ?: return
val normalized = packageName?.trim().orEmpty()
if (normalized.isEmpty() || normalized == service.packageName) return
migrateLegacyRecentPackagesIfNeeded(service.applicationContext)
val prefs = recentPackagesPrefs(service.applicationContext)
val existing =
recentPackages(service.applicationContext)
.filter { it != normalized }
prefs
.getString(recentPackagesPref, null)
.orEmpty()
.split(',')
.map { it.trim() }
.filter { it.isNotEmpty() && it != normalized }
.take(recentPackagesLimit - 1)
// Most recent package first keeps settings suggestions useful without storing notification content.
val updated = listOf(normalized) + existing
OpenClawSQLiteStateStore(service.applicationContext)
.replaceRecentNotificationPackages(updated, recentPackagesLimit)
prefs.edit { putString(recentPackagesPref, updated.joinToString(",")) }
}
}
@@ -459,6 +484,7 @@ class DeviceNotificationListenerService : NotificationListenerService() {
val action =
sbn.notification.actions
?.firstOrNull { candidate ->
// Android reply actions are identified by RemoteInput, not by a stable action title.
candidate.actionIntent != null && !candidate.remoteInputs.isNullOrEmpty()
}
?: return NotificationActionResult(

View File

@@ -9,6 +9,9 @@ import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
/**
* Handles gateway-originated events that need to update local Android preferences.
*/
class GatewayEventHandler(
private val scope: CoroutineScope,
private val prefs: SecurePrefs,
@@ -19,12 +22,14 @@ class GatewayEventHandler(
private var suppressWakeWordsSync = false
private var wakeWordsSyncJob: Job? = null
/** Applies gateway wake words locally without echoing the same change back to the gateway. */
fun applyWakeWordsFromGateway(words: List<String>) {
suppressWakeWordsSync = true
prefs.setWakeWords(words)
suppressWakeWordsSync = false
}
/** Debounces local wake-word edits before sending voicewake.set to the operator session. */
fun scheduleWakeWordsSyncIfNeeded() {
if (suppressWakeWordsSync) return
if (!isConnected()) return
@@ -44,6 +49,7 @@ class GatewayEventHandler(
}
}
/** Loads gateway wake words on connect so Android settings show server truth. */
suspend fun refreshWakeWordsFromGateway() {
if (!isConnected()) return
try {
@@ -57,6 +63,7 @@ class GatewayEventHandler(
}
}
/** Applies voicewake.changed event payloads emitted by the gateway. */
fun handleVoiceWakeChangedEvent(payloadJson: String?) {
if (payloadJson.isNullOrBlank()) return
try {

View File

@@ -16,6 +16,7 @@ import ai.openclaw.app.protocol.OpenClawSmsCommand
import ai.openclaw.app.protocol.OpenClawSystemCommand
import ai.openclaw.app.protocol.OpenClawTalkCommand
/** Runtime feature flags used to decide which node tools are advertised. */
data class NodeRuntimeFlags(
val cameraEnabled: Boolean,
val locationEnabled: Boolean,
@@ -30,6 +31,7 @@ data class NodeRuntimeFlags(
val debugBuild: Boolean,
)
/** Per-command availability gates checked before advertising invoke methods. */
enum class InvokeCommandAvailability {
Always,
CameraEnabled,
@@ -44,6 +46,7 @@ enum class InvokeCommandAvailability {
DebugBuild,
}
/** Per-capability availability gates for the node capabilities manifest. */
enum class NodeCapabilityAvailability {
Always,
CameraEnabled,
@@ -55,11 +58,13 @@ enum class NodeCapabilityAvailability {
MotionAvailable,
}
/** Capability entry reported to the gateway when its availability gate passes. */
data class NodeCapabilitySpec(
val name: String,
val availability: NodeCapabilityAvailability = NodeCapabilityAvailability.Always,
)
/** Invoke method entry advertised to gateway plus foreground routing metadata. */
data class InvokeCommandSpec(
val name: String,
val requiresForeground: Boolean = false,
@@ -67,6 +72,7 @@ data class InvokeCommandSpec(
)
object InvokeCommandRegistry {
/** Capabilities mirror gateway protocol ids and are filtered by device state. */
val capabilityManifest: List<NodeCapabilitySpec> =
listOf(
NodeCapabilitySpec(name = OpenClawCapability.Canvas.rawValue),
@@ -106,6 +112,7 @@ object InvokeCommandRegistry {
),
)
/** Complete Android node command catalog before runtime availability filtering. */
val all: List<InvokeCommandSpec> =
listOf(
InvokeCommandSpec(
@@ -240,8 +247,10 @@ object InvokeCommandRegistry {
private val byNameInternal: Map<String, InvokeCommandSpec> = all.associateBy { it.name }
/** Finds the command metadata used by dispatch and advertised-method builders. */
fun find(command: String): InvokeCommandSpec? = byNameInternal[command]
/** Returns gateway capability ids the current Android device can actually serve. */
fun advertisedCapabilities(flags: NodeRuntimeFlags): List<String> =
capabilityManifest
.filter { spec ->
@@ -257,6 +266,7 @@ object InvokeCommandRegistry {
}
}.map { it.name }
/** Returns gateway invoke method ids available under current permissions/build flags. */
fun advertisedCommands(flags: NodeRuntimeFlags): List<String> =
all
.filter { spec ->

View File

@@ -15,12 +15,16 @@ import ai.openclaw.app.protocol.OpenClawSmsCommand
import ai.openclaw.app.protocol.OpenClawSystemCommand
import ai.openclaw.app.protocol.OpenClawTalkCommand
/** Runtime state for SMS search, split so permission prompts are not reported as hard unavailability. */
internal enum class SmsSearchAvailabilityReason {
Available,
PermissionRequired,
Unavailable,
}
/**
* Distinguish permanent SMS search unavailability from permission-gated search.
*/
internal fun classifySmsSearchAvailability(
readSmsAvailable: Boolean,
smsFeatureEnabled: Boolean,
@@ -53,6 +57,9 @@ internal fun smsSearchAvailabilityError(
)
}
/**
* Gateway node.invoke command router for Android-owned capabilities.
*/
class InvokeDispatcher(
private val canvas: CanvasController,
private val cameraHandler: CameraHandler,
@@ -85,6 +92,7 @@ class InvokeDispatcher(
private val motionActivityAvailable: () -> Boolean,
private val motionPedometerAvailable: () -> Boolean,
) {
/** Dispatches one gateway node.invoke command after foreground and availability gates pass. */
suspend fun handleInvoke(
command: String,
paramsJson: String?,
@@ -96,6 +104,7 @@ class InvokeDispatcher(
message = "INVALID_REQUEST: unknown command",
)
if (spec.requiresForeground && !isForeground()) {
// Canvas, camera, and screen-backed commands need an active Activity/WebView surface.
return GatewaySession.InvokeResult.error(
code = "NODE_BACKGROUND_UNAVAILABLE",
message = "NODE_BACKGROUND_UNAVAILABLE: canvas/camera/screen commands require foreground",
@@ -103,6 +112,7 @@ class InvokeDispatcher(
}
availabilityError(spec.availability)?.let { return it }
// Command strings come from OpenClawProtocolConstants; the registry above owns advertised availability.
return when (command) {
// Canvas commands
OpenClawCanvasCommand.Present.rawValue -> {
@@ -239,6 +249,7 @@ class InvokeDispatcher(
)
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)) {
@@ -255,6 +266,7 @@ class InvokeDispatcher(
try {
block()
} catch (_: Throwable) {
// WebView calls throw when the Activity is backgrounded between the foreground check and execution.
GatewaySession.InvokeResult.error(
code = "NODE_BACKGROUND_UNAVAILABLE",
message = "NODE_BACKGROUND_UNAVAILABLE: canvas unavailable",
@@ -312,6 +324,7 @@ class InvokeDispatcher(
InvokeCommandAvailability.ReadSmsAvailable,
InvokeCommandAvailability.RequestableSmsSearchAvailable,
->
// SMS search may still be advertised as promptable; runtime invoke fails only on permanent unavailability.
smsSearchAvailabilityError(
readSmsAvailable = readSmsAvailable(),
smsFeatureEnabled = smsFeatureEnabled(),
@@ -347,12 +360,19 @@ class InvokeDispatcher(
}
}
/**
* Talk-mode command adapter implemented by the voice subsystem.
*/
interface TalkHandler {
/** Starts a push-to-talk capture session and keeps it open until stop or cancel. */
suspend fun handlePttStart(paramsJson: String?): GatewaySession.InvokeResult
/** Finishes the active push-to-talk capture and submits recognized speech. */
suspend fun handlePttStop(paramsJson: String?): GatewaySession.InvokeResult
/** Aborts the active push-to-talk capture without submitting speech. */
suspend fun handlePttCancel(paramsJson: String?): GatewaySession.InvokeResult
/** Runs a bounded one-shot push-to-talk capture. */
suspend fun handlePttOnce(paramsJson: String?): GatewaySession.InvokeResult
}

View File

@@ -4,6 +4,9 @@ import kotlin.math.max
import kotlin.math.min
import kotlin.math.roundToInt
/**
* Result of a JPEG compression attempt after quality and scale reductions.
*/
internal data class JpegSizeLimiterResult(
val bytes: ByteArray,
val width: Int,
@@ -11,7 +14,11 @@ internal data class JpegSizeLimiterResult(
val quality: Int,
)
/**
* Utility that searches quality/scale combinations until a JPEG fits a byte budget.
*/
internal object JpegSizeLimiter {
/** Compresses with the caller-provided encoder, reducing quality before image dimensions. */
fun compressToLimit(
initialWidth: Int,
initialHeight: Int,

View File

@@ -14,6 +14,9 @@ import kotlinx.coroutines.withTimeout
import java.time.Instant
import java.time.format.DateTimeFormatter
/**
* Android LocationManager-backed capture used by gateway location commands.
*/
class LocationCaptureManager(
private val context: Context,
) {
@@ -35,6 +38,7 @@ class LocationCaptureManager(
throw IllegalStateException("LOCATION_UNAVAILABLE: no location providers enabled")
}
// Prefer a recent cached fix before waking GPS/network providers.
val cached = bestLastKnown(manager, desiredProviders, maxAgeMs)
val location =
cached ?: requestCurrent(manager, desiredProviders, timeoutMs)
@@ -81,6 +85,7 @@ class LocationCaptureManager(
val candidates =
providers.mapNotNull { provider -> manager.getLastKnownLocation(provider) }
val freshest = candidates.maxByOrNull { it.time } ?: return null
// maxAgeMs is a caller contract; stale cached fixes force a live provider request.
if (maxAgeMs != null && now - freshest.time > maxAgeMs) return null
return freshest
}
@@ -102,6 +107,7 @@ class LocationCaptureManager(
val resolved =
providers.firstOrNull { manager.isProviderEnabled(it) }
?: throw IllegalStateException("LOCATION_UNAVAILABLE: no providers available")
// getCurrentLocation can return null; the handler maps timeout/null fixes to gateway error shapes.
val location =
withTimeout(timeoutMs.coerceAtLeast(1)) {
suspendCancellableCoroutine<Location?> { cont ->

View File

@@ -10,6 +10,9 @@ import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonPrimitive
/**
* Injectable location facade for command tests and Android runtime access.
*/
internal interface LocationDataSource {
fun hasFinePermission(context: Context): Boolean
@@ -69,11 +72,14 @@ class LocationHandler private constructor(
locationPreciseEnabled = locationPreciseEnabled,
)
/** Reports whether precise GPS-backed location can be requested from Android. */
fun hasFineLocationPermission(): Boolean = dataSource.hasFinePermission(appContext)
/** Reports whether network/coarse location can be requested from Android. */
fun hasCoarseLocationPermission(): Boolean = dataSource.hasCoarsePermission(appContext)
companion object {
/** Creates a handler with injected location state for permission and payload tests. */
internal fun forTesting(
appContext: Context,
dataSource: LocationDataSource,
@@ -90,8 +96,10 @@ class LocationHandler private constructor(
)
}
/** Handles location.get with foreground, permission, and user precision gates applied. */
suspend fun handleLocationGet(paramsJson: String?): GatewaySession.InvokeResult {
if (!isForeground()) {
// Android foreground restrictions and user expectation keep live location tied to the visible app.
return GatewaySession.InvokeResult.error(
code = "LOCATION_BACKGROUND_UNAVAILABLE",
message = "LOCATION_BACKGROUND_UNAVAILABLE: location requires OpenClaw to stay open",
@@ -105,6 +113,8 @@ class LocationHandler private constructor(
}
val (maxAgeMs, timeoutMs, desiredAccuracy) = parseLocationParams(paramsJson)
val preciseEnabled = locationPreciseEnabled()
// Gateway requests are advisory; Android permission and user settings decide
// whether precise capture is actually allowed for this invocation.
val accuracy =
when (desiredAccuracy) {
"precise" -> if (preciseEnabled && dataSource.hasFinePermission(appContext)) "precise" else "balanced"
@@ -113,6 +123,7 @@ class LocationHandler private constructor(
}
val providers =
when (accuracy) {
// Provider order is part of the accuracy policy: GPS first for precise, network first otherwise.
"precise" -> listOf(LocationManager.GPS_PROVIDER, LocationManager.NETWORK_PROVIDER)
"coarse" -> listOf(LocationManager.NETWORK_PROVIDER, LocationManager.GPS_PROVIDER)
else -> listOf(LocationManager.NETWORK_PROVIDER, LocationManager.GPS_PROVIDER)
@@ -151,6 +162,7 @@ class LocationHandler private constructor(
val timeoutMs =
(root?.get("timeoutMs") as? JsonPrimitive)?.content?.toLongOrNull()?.coerceIn(1_000L, 60_000L)
?: 10_000L
// desiredAccuracy is advisory; invalid values fall through to the default policy.
val desiredAccuracy =
(root?.get("desiredAccuracy") as? JsonPrimitive)?.content?.trim()?.lowercase()
return Triple(maxAgeMs, timeoutMs, desiredAccuracy)

View File

@@ -25,17 +25,20 @@ import kotlin.math.sqrt
private const val ACCELEROMETER_SAMPLE_TARGET = 20
private const val ACCELEROMETER_SAMPLE_TIMEOUT_MS = 6_000L
/** Gateway request for motion.activity after parsing and limit bounds. */
internal data class MotionActivityRequest(
val startISO: String?,
val endISO: String?,
val limit: Int,
)
/** Gateway request for motion.pedometer. */
internal data class MotionPedometerRequest(
val startISO: String?,
val endISO: String?,
)
/** Motion activity sample returned in gateway-compatible boolean flags. */
internal data class MotionActivityRecord(
val startISO: String,
val endISO: String,
@@ -48,6 +51,7 @@ internal data class MotionActivityRecord(
val isUnknown: Boolean,
)
/** Pedometer sample returned from Android's cumulative step counter. */
internal data class PedometerRecord(
val startISO: String,
val endISO: String,
@@ -57,6 +61,7 @@ internal data class PedometerRecord(
val floorsDescended: Int?,
)
/** Motion data seam for Android sensors and tests. */
internal interface MotionDataSource {
fun isActivityAvailable(context: Context): Boolean
@@ -97,6 +102,8 @@ private object SystemMotionDataSource : MotionDataSource {
request: MotionActivityRequest,
): MotionActivityRecord {
if (!request.startISO.isNullOrBlank() || !request.endISO.isNullOrBlank()) {
// Android does not expose historical activity samples here; fail with a
// stable gateway code instead of pretending the range is empty.
throw IllegalArgumentException("MOTION_RANGE_UNAVAILABLE: historical activity range not supported on Android")
}
val sensorManager =
@@ -130,6 +137,7 @@ private object SystemMotionDataSource : MotionDataSource {
request: MotionPedometerRequest,
): PedometerRecord {
if (!request.startISO.isNullOrBlank() || !request.endISO.isNullOrBlank()) {
// TYPE_STEP_COUNTER is cumulative since boot, not a historical query API.
throw IllegalArgumentException("PEDOMETER_RANGE_UNAVAILABLE: historical pedometer range not supported on Android")
}
val sensorManager =
@@ -216,6 +224,8 @@ private object SystemMotionDataSource : MotionDataSource {
sumDelta += abs(magnitude - SensorManager.GRAVITY_EARTH.toDouble())
count += 1
if (count >= ACCELEROMETER_SAMPLE_TARGET) {
// Average gravity-adjusted magnitude across a short window so
// one noisy sensor event cannot decide the activity label.
val result =
AccelerometerSample(
samples = count,
@@ -260,12 +270,14 @@ private object SystemMotionDataSource : MotionDataSource {
}
}
/** Handles Android motion-related node.invoke commands backed by live sensors. */
class MotionHandler private constructor(
private val appContext: Context,
private val dataSource: MotionDataSource,
) {
constructor(appContext: Context) : this(appContext = appContext, dataSource = SystemMotionDataSource)
/** Classifies a short accelerometer sample into the gateway activity shape. */
suspend fun handleMotionActivity(paramsJson: String?): GatewaySession.InvokeResult {
if (!dataSource.hasPermission(appContext)) {
return GatewaySession.InvokeResult.error(
@@ -313,6 +325,7 @@ class MotionHandler private constructor(
}
}
/** Returns the current boot-scoped Android step-counter reading. */
suspend fun handleMotionPedometer(paramsJson: String?): GatewaySession.InvokeResult {
if (!dataSource.hasPermission(appContext)) {
return GatewaySession.InvokeResult.error(
@@ -350,8 +363,10 @@ class MotionHandler private constructor(
fun isAvailable(): Boolean = dataSource.isAvailable(appContext)
/** Returns true when live accelerometer classification can be sampled. */
fun isActivityAvailable(): Boolean = dataSource.isActivityAvailable(appContext)
/** Returns true when Android exposes a cumulative step-counter sensor. */
fun isPedometerAvailable(): Boolean = dataSource.isPedometerAvailable(appContext)
private fun parseActivityRequest(paramsJson: String?): MotionActivityRequest? {
@@ -364,6 +379,8 @@ class MotionHandler private constructor(
} catch (_: Throwable) {
null
} ?: return null
// Keep the accepted gateway parameter even though Android can only return
// one live classification sample for now.
val limit = ((params["limit"] as? JsonPrimitive)?.content?.toIntOrNull() ?: 200).coerceIn(1, 1000)
return MotionActivityRequest(
startISO = (params["startISO"] as? JsonPrimitive)?.content?.trim()?.ifEmpty { null },
@@ -389,8 +406,10 @@ class MotionHandler private constructor(
}
companion object {
/** Static capability probe used before a MotionHandler instance is needed. */
fun isMotionCapabilityAvailable(context: Context): Boolean = SystemMotionDataSource.isAvailable(context)
/** Creates a handler with an injected sensor source for parser and payload tests. */
internal fun forTesting(
appContext: Context,
dataSource: MotionDataSource,

View File

@@ -6,10 +6,16 @@ import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.buildJsonObject
internal object NodePresenceAliveBeacon {
/** Gateway event emitted by Android when background execution confirms liveness. */
const val EVENT_NAME: String = "node.presence.alive"
/** Avoids spamming presence when multiple background triggers fire together. */
const val MIN_SUCCESS_INTERVAL_MS: Long = 10 * 60 * 1000
private const val MAX_RESPONSE_JSON_CHARS: Int = 16 * 1024
/**
* Source of the liveness event, serialized as gateway-stable wire values.
*/
enum class Trigger(
val rawValue: String,
) {
@@ -21,6 +27,9 @@ internal object NodePresenceAliveBeacon {
Connect("connect"),
}
/**
* Minimal gateway response fields used to decide whether a liveness event was accepted.
*/
data class ResponsePayload(
val ok: Boolean?,
val event: String?,
@@ -30,6 +39,7 @@ internal object NodePresenceAliveBeacon {
private val json = Json { ignoreUnknownKeys = true }
/** Skips sends after a recent successful presence update. */
fun shouldSkipRecentSuccess(
nowMs: Long,
lastSuccessAtMs: Long?,
@@ -41,6 +51,7 @@ internal object NodePresenceAliveBeacon {
return elapsed >= 0 && elapsed < minIntervalMs
}
/** Human-readable Android version label included in presence payloads. */
fun androidPlatformLabel(): String {
val release =
Build.VERSION.RELEASE
@@ -50,6 +61,7 @@ internal object NodePresenceAliveBeacon {
return "Android $release (SDK ${Build.VERSION.SDK_INT})"
}
/** Builds the compact JSON payload consumed by gateway node-presence handlers. */
fun makePayloadJson(
trigger: Trigger,
sentAtMs: Long,
@@ -71,8 +83,11 @@ internal object NodePresenceAliveBeacon {
pushTransport?.trim()?.takeIf { it.isNotEmpty() }?.let { put("pushTransport", JsonPrimitive(it)) }
}.toString()
/** Parses the gateway response while rejecting empty, oversized, or malformed payloads. */
fun decodeResponse(payloadJson: String?): ResponsePayload? {
val raw = payloadJson?.trim()?.takeIf { it.isNotEmpty() } ?: return null
// Bound log/IPC responses before JSON parsing to avoid memory spikes from
// malformed gateway replies.
if (raw.length > MAX_RESPONSE_JSON_CHARS) return null
val obj =
try {
@@ -88,6 +103,7 @@ internal object NodePresenceAliveBeacon {
)
}
/** Sanitizes gateway response reasons before writing them into Android logs. */
fun sanitizeReasonForLog(raw: String?): String {
val value = raw?.trim()?.takeIf { it.isNotEmpty() } ?: "unsupported"
return value

View File

@@ -8,8 +8,10 @@ import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.contentOrNull
/** Default canvas seam color used when gateway/user params omit a hex color. */
const val DEFAULT_SEAM_COLOR_ARGB: Long = 0xFF4F7A9A
/** Small tuple used by Android node handlers that need four return values. */
data class Quad<A, B, C, D>(
val first: A,
val second: B,
@@ -17,6 +19,7 @@ data class Quad<A, B, C, D>(
val fourth: D,
)
/** Escapes a Kotlin string into a JSON string literal without building a JsonElement. */
fun String.toJsonString(): String {
val escaped =
this
@@ -29,6 +32,7 @@ fun String.toJsonString(): String {
fun JsonElement?.asObjectOrNull(): JsonObject? = this as? JsonObject
/** Parses invoke params into a JSON object, returning null for absent/malformed input. */
fun parseJsonParamsObject(paramsJson: String?): JsonObject? {
if (paramsJson.isNullOrBlank()) return null
return try {
@@ -38,26 +42,31 @@ fun parseJsonParamsObject(paramsJson: String?): JsonObject? {
}
}
/** Reads a primitive field from invoke params without accepting arrays/objects. */
fun readJsonPrimitive(
params: JsonObject?,
key: String,
): JsonPrimitive? = params?.get(key) as? JsonPrimitive
/** Parses an optional integer invoke param. */
fun parseJsonInt(
params: JsonObject?,
key: String,
): Int? = readJsonPrimitive(params, key)?.contentOrNull?.toIntOrNull()
/** Parses an optional decimal invoke param. */
fun parseJsonDouble(
params: JsonObject?,
key: String,
): Double? = readJsonPrimitive(params, key)?.contentOrNull?.toDoubleOrNull()
/** Parses an optional string invoke param. */
fun parseJsonString(
params: JsonObject?,
key: String,
): String? = readJsonPrimitive(params, key)?.contentOrNull
/** Parses strict true/false flags from string-like JSON primitives. */
fun parseJsonBooleanFlag(
params: JsonObject?,
key: String,
@@ -70,6 +79,7 @@ fun parseJsonBooleanFlag(
}
}
/** Converts JSON null to Kotlin null while preserving primitive text content. */
fun JsonElement?.asStringOrNull(): String? =
when (this) {
is JsonNull -> null
@@ -77,6 +87,7 @@ fun JsonElement?.asStringOrNull(): String? =
else -> null
}
/** Parses #RRGGBB or RRGGBB into opaque ARGB. */
fun parseHexColorArgb(raw: String?): Long? {
val trimmed = raw?.trim().orEmpty()
if (trimmed.isEmpty()) return null
@@ -86,15 +97,18 @@ fun parseHexColorArgb(raw: String?): Long? {
return 0xFF000000L or rgb
}
/** Converts gateway invocation throwables into protocol code/message pairs. */
fun invokeErrorFromThrowable(err: Throwable): Pair<String, String> {
val parsed = parseInvokeErrorFromThrowable(err, fallbackMessage = "UNAVAILABLE: error")
val message = if (parsed.hadExplicitCode) parsed.prefixedMessage else parsed.message
return parsed.code to message
}
/** Normalizes user/session keys while preserving main as the canonical session id. */
fun normalizeMainKey(raw: String?): String? {
val trimmed = raw?.trim().orEmpty()
return if (trimmed.isEmpty()) null else trimmed
}
/** Returns true only for the canonical main-session key understood by gateway UI. */
fun isCanonicalMainSessionKey(key: String): Boolean = key == "main"

View File

@@ -10,6 +10,9 @@ import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.contentOrNull
import kotlinx.serialization.json.put
/**
* Injectable notification listener facade so command parsing can be tested without Android service state.
*/
internal interface NotificationsStateProvider {
fun readSnapshot(context: Context): DeviceNotificationSnapshot
@@ -22,6 +25,7 @@ internal interface NotificationsStateProvider {
}
private object SystemNotificationsStateProvider : NotificationsStateProvider {
/** Reads listener state through Android APIs and returns a disabled snapshot when access is missing. */
override fun readSnapshot(context: Context): DeviceNotificationSnapshot {
val enabled = DeviceNotificationListenerService.isAccessEnabled(context)
if (!enabled) {
@@ -34,27 +38,32 @@ private object SystemNotificationsStateProvider : NotificationsStateProvider {
return DeviceNotificationListenerService.snapshot(context, enabled = true)
}
/** Requests a platform listener rebind after access has been granted. */
override fun requestServiceRebind(context: Context) {
DeviceNotificationListenerService.requestServiceRebind(context)
}
/** Delegates actions to the active listener service instance. */
override fun executeAction(
context: Context,
request: NotificationActionRequest,
): NotificationActionResult = DeviceNotificationListenerService.executeAction(context, request)
}
/** Handles notification listing and actions via the Android listener service. */
class NotificationsHandler private constructor(
private val appContext: Context,
private val stateProvider: NotificationsStateProvider,
) {
constructor(appContext: Context) : this(appContext = appContext, stateProvider = SystemNotificationsStateProvider)
/** Lists the current listener snapshot after nudging Android to reconnect if needed. */
suspend fun handleNotificationsList(_paramsJson: String?): GatewaySession.InvokeResult {
val snapshot = readSnapshotWithRebind()
return GatewaySession.InvokeResult.ok(snapshotPayloadJson(snapshot))
}
/** Executes an action against a notification key from the current listener snapshot. */
suspend fun handleNotificationsActions(paramsJson: String?): GatewaySession.InvokeResult {
readSnapshotWithRebind()
@@ -76,6 +85,8 @@ class NotificationsHandler private constructor(
code = "INVALID_REQUEST",
message = "INVALID_REQUEST: action required (open|dismiss|reply)",
)
// Keep accepted action names aligned with the cross-platform notification
// command contract rather than Android-specific PendingIntent labels.
val action =
when (actionRaw) {
"open" -> NotificationActionKind.Open
@@ -123,6 +134,7 @@ class NotificationsHandler private constructor(
private fun readSnapshotWithRebind(): DeviceNotificationSnapshot {
val snapshot = stateProvider.readSnapshot(appContext)
if (snapshot.enabled && !snapshot.connected) {
// Access can be granted while Android has not rebound the listener yet.
stateProvider.requestServiceRebind(appContext)
}
return snapshot

View File

@@ -29,12 +29,14 @@ private const val DEFAULT_PHOTOS_QUALITY = 0.85
private const val MAX_TOTAL_BASE64_CHARS = 340 * 1024
private const val MAX_PER_PHOTO_BASE64_CHARS = 300 * 1024
/** Request shape for photos.latest after defaults and bounds are applied. */
internal data class PhotosLatestRequest(
val limit: Int,
val maxWidth: Int,
val quality: Double,
)
/** Encoded photo payload returned to the gateway. */
internal data class EncodedPhotoPayload(
val format: String,
val base64: String,
@@ -43,6 +45,7 @@ internal data class EncodedPhotoPayload(
val createdAt: String?,
)
/** Photo access seam for Android MediaStore and tests. */
internal interface PhotosDataSource {
fun hasPermission(context: Context): Boolean
@@ -53,6 +56,7 @@ internal interface PhotosDataSource {
}
private object SystemPhotosDataSource : PhotosDataSource {
/** Checks the API-specific image read permission used by MediaStore image access. */
override fun hasPermission(context: Context): Boolean {
val permission =
if (Build.VERSION.SDK_INT >= 33) {
@@ -77,6 +81,8 @@ private object SystemPhotosDataSource : PhotosDataSource {
if (remainingBudget <= 0) break
val bitmap = decodeScaledBitmap(resolver, row.uri, request.maxWidth) ?: continue
try {
// Enforce both per-photo and total payload budgets before returning
// base64 data through the gateway invoke response.
val encoded = encodeJpegUnderBudget(bitmap, request.quality, MAX_PER_PHOTO_BASE64_CHARS)
if (encoded == null) continue
if (encoded.base64.length > remainingBudget) break
@@ -172,6 +178,8 @@ private object SystemPhotosDataSource : PhotosDataSource {
} ?: return null
if (decoded.width <= maxWidth) return decoded
// Decode sampling is power-of-two only; finish with exact scaling when the
// sampled bitmap is still wider than the requested max width.
val targetHeight = max(1, ((decoded.height.toDouble() * maxWidth) / decoded.width).roundToInt())
return try {
decoded.scale(maxWidth, targetHeight, true)
@@ -215,6 +223,7 @@ private object SystemPhotosDataSource : PhotosDataSource {
)
}
if (jpegQuality > 35) {
// Try quality reduction before resizing so small images keep detail.
jpegQuality = max(25, jpegQuality - 15)
return@repeat
}
@@ -232,12 +241,14 @@ private object SystemPhotosDataSource : PhotosDataSource {
}
}
/** Handles photos.latest by querying MediaStore and returning bounded JPEG payloads. */
class PhotosHandler private constructor(
private val appContext: Context,
private val dataSource: PhotosDataSource,
) {
constructor(appContext: Context) : this(appContext = appContext, dataSource = SystemPhotosDataSource)
/** Returns the newest accessible photos as gateway-sized base64 JPEGs. */
fun handlePhotosLatest(paramsJson: String?): GatewaySession.InvokeResult {
if (!dataSource.hasPermission(appContext)) {
return GatewaySession.InvokeResult.error(
@@ -300,6 +311,7 @@ class PhotosHandler private constructor(
val maxWidthRaw = (params["maxWidth"] as? JsonPrimitive)?.content?.toIntOrNull()
val qualityRaw = (params["quality"] as? JsonPrimitive)?.content?.toDoubleOrNull()
// Clamp model-supplied values to protect memory and response-size limits.
val limit = (limitRaw ?: DEFAULT_PHOTOS_LIMIT).coerceIn(1, 20)
val maxWidth = (maxWidthRaw ?: DEFAULT_PHOTOS_MAX_WIDTH).coerceIn(240, 4096)
val quality = (qualityRaw ?: DEFAULT_PHOTOS_QUALITY).coerceIn(0.1, 1.0)
@@ -307,6 +319,7 @@ class PhotosHandler private constructor(
}
companion object {
/** Creates a handler with an injected photo source for parser and payload tests. */
internal fun forTesting(
appContext: Context,
dataSource: PhotosDataSource,

View File

@@ -17,6 +17,7 @@ import kotlinx.serialization.json.contentOrNull
private const val NOTIFICATION_CHANNEL_BASE_ID = "openclaw.system.notify"
/** Parsed payload for system.notify invocations. */
internal data class SystemNotifyRequest(
val title: String,
val body: String,
@@ -24,6 +25,7 @@ internal data class SystemNotifyRequest(
val priority: String?,
)
/** Notification posting seam used by production Android and unit tests. */
internal interface SystemNotificationPoster {
fun isAuthorized(): Boolean
@@ -33,6 +35,7 @@ internal interface SystemNotificationPoster {
private class AndroidSystemNotificationPoster(
private val appContext: Context,
) : SystemNotificationPoster {
/** Checks both Android 13 runtime permission and app-level notification enablement. */
override fun isAuthorized(): Boolean {
if (Build.VERSION.SDK_INT >= 33) {
val granted =
@@ -43,6 +46,7 @@ private class AndroidSystemNotificationPoster(
return NotificationManagerCompat.from(appContext).areNotificationsEnabled()
}
/** Posts through a priority-specific channel so Android's immutable channel importance is respected. */
override fun post(request: SystemNotifyRequest) {
val channelId = ensureChannel(request.priority)
val silent = isSilentSound(request.sound)
@@ -69,6 +73,8 @@ private class AndroidSystemNotificationPoster(
private fun ensureChannel(priority: String?): String {
val normalizedPriority = priority.orEmpty().trim().lowercase()
// Android channel importance is immutable after creation, so priority maps
// to stable channel ids instead of mutating one shared channel.
val (suffix, importance, name) =
when (normalizedPriority) {
"passive" -> Triple("passive", NotificationManager.IMPORTANCE_LOW, "OpenClaw Passive")
@@ -97,11 +103,13 @@ private class AndroidSystemNotificationPoster(
}
}
/** Handles system-level node.invoke commands implemented by Android services. */
class SystemHandler private constructor(
private val poster: SystemNotificationPoster,
) {
constructor(appContext: Context) : this(poster = AndroidSystemNotificationPoster(appContext))
/** Posts an Android notification from the gateway system.notify command. */
fun handleSystemNotify(paramsJson: String?): GatewaySession.InvokeResult {
val params =
parseNotifyRequest(paramsJson)
@@ -139,6 +147,8 @@ class SystemHandler private constructor(
private fun parseNotifyRequest(paramsJson: String?): SystemNotifyRequest? {
val params = parseParamsObject(paramsJson) ?: return null
// title/body are required by the gateway contract; optional fields only
// influence Android channel/silence behavior.
val rawTitle =
(params["title"] as? JsonPrimitive)
?.contentOrNull
@@ -167,6 +177,7 @@ class SystemHandler private constructor(
}
companion object {
/** Creates a handler with a fake poster for parser and authorization tests. */
internal fun forTesting(poster: SystemNotificationPoster): SystemHandler = SystemHandler(poster)
}
}

View File

@@ -4,6 +4,7 @@ import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
object OpenClawCanvasA2UIAction {
/** Reads the agent-facing action name from either the modern name field or legacy action field. */
fun extractActionName(userAction: JsonObject): String? {
val name =
(userAction["name"] as? JsonPrimitive)
@@ -19,6 +20,7 @@ object OpenClawCanvasA2UIAction {
return action.ifEmpty { null }
}
/** Normalizes prompt tag values so the compact CANVAS_A2UI envelope stays parser-friendly. */
fun sanitizeTagValue(value: String): String {
val trimmed = value.trim().ifEmpty { "-" }
val normalized = trimmed.replace(" ", "_")
@@ -35,6 +37,7 @@ object OpenClawCanvasA2UIAction {
return out.toString()
}
/** Formats the compact text envelope sent to the agent when a canvas UI action fires. */
fun formatAgentMessage(
actionName: String,
sessionKey: String,
@@ -57,6 +60,7 @@ object OpenClawCanvasA2UIAction {
).joinToString(separator = " ")
}
/** Builds JS that reports an agent action result back to the canvas runtime. */
fun jsDispatchA2UIActionStatus(
actionId: String,
ok: Boolean,

View File

@@ -1,5 +1,6 @@
package ai.openclaw.app.protocol
/** Capability ids advertised by the Android node to the OpenClaw gateway. */
enum class OpenClawCapability(
val rawValue: String,
) {
@@ -19,6 +20,7 @@ enum class OpenClawCapability(
CallLog("callLog"),
}
/** Canvas command ids mirrored from the gateway tool namespace. */
enum class OpenClawCanvasCommand(
val rawValue: String,
) {
@@ -34,6 +36,7 @@ enum class OpenClawCanvasCommand(
}
}
/** Streaming canvas commands sent from agents back into the Android UI. */
enum class OpenClawCanvasA2UICommand(
val rawValue: String,
) {
@@ -47,6 +50,7 @@ enum class OpenClawCanvasA2UICommand(
}
}
/** Camera command ids accepted by the Android node. */
enum class OpenClawCameraCommand(
val rawValue: String,
) {
@@ -60,6 +64,7 @@ enum class OpenClawCameraCommand(
}
}
/** SMS command ids accepted by the Android node. */
enum class OpenClawSmsCommand(
val rawValue: String,
) {
@@ -72,6 +77,7 @@ enum class OpenClawSmsCommand(
}
}
/** Push-to-talk command ids accepted by the Android node. */
enum class OpenClawTalkCommand(
val rawValue: String,
) {
@@ -86,6 +92,7 @@ enum class OpenClawTalkCommand(
}
}
/** Location command ids accepted by the Android node. */
enum class OpenClawLocationCommand(
val rawValue: String,
) {
@@ -97,6 +104,7 @@ enum class OpenClawLocationCommand(
}
}
/** Device status and metadata command ids accepted by the Android node. */
enum class OpenClawDeviceCommand(
val rawValue: String,
) {
@@ -111,6 +119,7 @@ enum class OpenClawDeviceCommand(
}
}
/** Notification command ids accepted by the Android node. */
enum class OpenClawNotificationsCommand(
val rawValue: String,
) {
@@ -123,6 +132,7 @@ enum class OpenClawNotificationsCommand(
}
}
/** System command ids accepted by the Android node. */
enum class OpenClawSystemCommand(
val rawValue: String,
) {
@@ -134,6 +144,7 @@ enum class OpenClawSystemCommand(
}
}
/** Photos command ids accepted by the Android node. */
enum class OpenClawPhotosCommand(
val rawValue: String,
) {
@@ -145,6 +156,7 @@ enum class OpenClawPhotosCommand(
}
}
/** Contacts command ids accepted by the Android node. */
enum class OpenClawContactsCommand(
val rawValue: String,
) {
@@ -157,6 +169,7 @@ enum class OpenClawContactsCommand(
}
}
/** Calendar command ids accepted by the Android node. */
enum class OpenClawCalendarCommand(
val rawValue: String,
) {
@@ -169,6 +182,7 @@ enum class OpenClawCalendarCommand(
}
}
/** Motion sensor command ids accepted by the Android node. */
enum class OpenClawMotionCommand(
val rawValue: String,
) {
@@ -181,6 +195,7 @@ enum class OpenClawMotionCommand(
}
}
/** Call-log command ids accepted by the Android node. */
enum class OpenClawCallLogCommand(
val rawValue: String,
) {

View File

@@ -31,6 +31,7 @@ private data class ToolDisplayConfig(
val tools: Map<String, ToolDisplaySpec>? = null,
)
/** Compact UI summary for a running or pending tool call. */
data class ToolDisplaySummary(
val name: String,
val emoji: String,
@@ -39,6 +40,7 @@ data class ToolDisplaySummary(
val verb: String?,
val detail: String?,
) {
/** Optional second-line detail assembled from the action verb and best argument preview. */
val detailLine: String?
get() {
val parts = mutableListOf<String>()
@@ -47,10 +49,12 @@ data class ToolDisplaySummary(
return if (parts.isEmpty()) null else parts.joinToString(" · ")
}
/** Single-line fallback for compact tool rows that do not render detail separately. */
val summaryLine: String
get() = if (detailLine != null) "$emoji $label: $detailLine" else "$emoji $label"
}
/** Resolves tool-call names and args into user-facing Android display text. */
object ToolDisplayRegistry {
private const val CONFIG_ASSET = "tool-display.json"
@@ -58,6 +62,7 @@ object ToolDisplayRegistry {
@Volatile private var cachedConfig: ToolDisplayConfig? = null
/** Resolves a raw tool call into stable, bounded UI text for pending-tool surfaces. */
fun resolve(
context: Context,
name: String?,
@@ -86,6 +91,8 @@ object ToolDisplayRegistry {
detail = pathDetail(args)
}
// Action-specific detail keys win over tool defaults so commands like
// read/write can surface the most useful argument for that action.
val detailKeys = actionSpec?.detailKeys ?: spec?.detailKeys ?: fallback?.detailKeys ?: emptyList()
if (detail == null) {
detail = firstValue(args, detailKeys)
@@ -122,6 +129,8 @@ object ToolDisplayRegistry {
cachedConfig = decoded
decoded
} catch (_: Throwable) {
// The chat UI should still render pending tools if the asset is absent or
// malformed in debug builds.
val fallback = ToolDisplayConfig()
cachedConfig = fallback
fallback

View File

@@ -14,6 +14,7 @@ import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color
import kotlinx.coroutines.delay
/** Full-screen white flash keyed by camera capture tokens. */
@Composable
fun CameraFlashOverlay(
token: Long,
@@ -29,6 +30,8 @@ private fun CameraFlash(token: Long) {
var alpha by remember { mutableFloatStateOf(0f) }
LaunchedEffect(token) {
if (token == 0L) return@LaunchedEffect
// Token changes replay the animation even when consecutive captures use
// the same HUD message.
alpha = 0.85f
delay(110)
alpha = 0f

View File

@@ -26,6 +26,7 @@ import androidx.webkit.WebViewCompat
import androidx.webkit.WebViewFeature
import java.util.concurrent.atomic.AtomicReference
/** Hosts the gateway canvas WebView and attaches it to the runtime canvas controller. */
@SuppressLint("SetJavaScriptEnabled")
@Suppress("DEPRECATION")
@Composable
@@ -151,6 +152,9 @@ fun CanvasScreen(
}
}
// 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()) },
@@ -184,6 +188,7 @@ fun CanvasScreen(
)
}
/** Filters WebView postMessage payloads before they enter the A2UI action handler. */
internal class CanvasA2UIActionBridge(
private val isTrustedPage: () -> Boolean,
private val onMessage: (String) -> Unit,

View File

@@ -31,6 +31,7 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
/** Settings detail surface for live canvas status, refresh, and embedded preview. */
@Composable
internal fun CanvasSettingsScreen(
viewModel: MainViewModel,
@@ -47,6 +48,8 @@ internal fun CanvasSettingsScreen(
LaunchedEffect(isConnected) {
if (isConnected) {
// Refresh once when the gateway comes online so the settings preview is
// populated before the user manually asks for a rehydrate.
viewModel.refreshHomeCanvasOverviewIfConnected()
}
}

View File

@@ -25,6 +25,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
/** Settings screen for gateway channel readiness and account status. */
@Composable
internal fun ChannelsSettingsScreen(
viewModel: MainViewModel,
@@ -71,6 +72,8 @@ internal fun ChannelsSettingsScreen(
}
}
if (summary.partial || summary.warnings.isNotEmpty()) {
// Partial channel scans still include useful rows; surface the warning
// without hiding successful channel status.
ClawPanel {
Text(text = channelsWarningText(summary), style = ClawTheme.type.body, color = ClawTheme.colors.textMuted)
}
@@ -156,4 +159,5 @@ private fun channelBadge(label: String): String =
.joinToString("")
.ifBlank { "C" }
/** Chooses the first gateway warning or a generic partial-scan message. */
private fun channelsWarningText(summary: GatewayChannelsSummary): String = summary.warnings.firstOrNull()?.takeIf { it.isNotBlank() } ?: "Some channel status checks did not complete."

View File

@@ -4,6 +4,7 @@ import ai.openclaw.app.MainViewModel
import ai.openclaw.app.ui.chat.ChatSheetContent
import androidx.compose.runtime.Composable
/** Keeps the public shell entry point stable while chat internals live under ui.chat. */
@Composable
fun ChatSheet(viewModel: MainViewModel) {
ChatSheetContent(viewModel = viewModel)

View File

@@ -50,6 +50,7 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
/** Full-screen command palette for navigation and recent-session search. */
@Composable
internal fun CommandPalette(
viewModel: MainViewModel,
@@ -158,6 +159,7 @@ private data class CommandItem(
val icon: ImageVector,
val onClick: () -> Unit,
) {
/** Matches palette queries against both action title and explanatory subtitle. */
fun matches(query: String): Boolean = query.isEmpty() || title.lowercase().contains(query) || subtitle.lowercase().contains(query)
}
@@ -295,6 +297,7 @@ private fun CommandSectionLabel(title: String) {
}
}
/** Builds provider quick-action metadata from current gateway/catalog state. */
private fun providerCommandSubtitle(
isConnected: Boolean,
providers: List<GatewayModelProviderSummary>,
@@ -307,8 +310,10 @@ private fun providerCommandSubtitle(
return "Configure model access"
}
/** Falls back to the canonical main-session label when gateway display names are blank. */
private fun commandSessionTitle(displayName: String?): String = displayName?.takeIf { it.isNotBlank() } ?: "Main session"
/** Formats command-palette session timestamps for compact rows. */
private fun commandRelativeTime(updatedAtMs: Long): String {
val deltaMs = (System.currentTimeMillis() - updatedAtMs).coerceAtLeast(0L)
val minutes = deltaMs / 60_000L

View File

@@ -61,6 +61,7 @@ private enum class ConnectInputMode {
Manual,
}
/** Gateway connection screen for setup-code and manual endpoint pairing. */
@Composable
fun ConnectTabScreen(viewModel: MainViewModel) {
val context = LocalContext.current
@@ -291,6 +292,8 @@ fun ConnectTabScreen(viewModel: MainViewModel) {
validationText = null
if (inputMode == ConnectInputMode.SetupCode) {
// Setup-code auth should replace old bootstrap/shared credentials;
// manual reconnects keep existing typed credentials.
viewModel.resetGatewaySetupAuth()
}
viewModel.setManualEnabled(true)

View File

@@ -32,6 +32,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
/** Settings screen for gateway dreaming state and recent dream diary entries. */
@Composable
internal fun DreamingSettingsScreen(
viewModel: MainViewModel,
@@ -187,6 +188,7 @@ private fun DreamDiaryRow(entry: GatewayDreamDiaryEntry) {
}
}
/** Formats the next dreaming cycle as a compact relative label. */
private fun formatDreamingNextRun(nextRunAtMs: Long?): String {
val next = nextRunAtMs ?: return "Not scheduled"
val deltaMinutes = ((next - System.currentTimeMillis()) / 60_000L).coerceAtLeast(0L)

View File

@@ -10,6 +10,7 @@ import java.net.URI
import java.util.Base64
import java.util.Locale
/** Parsed endpoint fields after URL validation and cleartext-safety checks. */
internal data class GatewayEndpointConfig(
val host: String,
val port: Int,
@@ -17,6 +18,7 @@ internal data class GatewayEndpointConfig(
val displayUrl: String,
)
/** Decoded setup-code payload; only one credential family is expected to be populated. */
internal data class GatewaySetupCode(
val url: String,
val bootstrapToken: String?,
@@ -24,6 +26,7 @@ internal data class GatewaySetupCode(
val password: String?,
)
/** Final gateway connection fields selected from setup-code or manual UI input. */
internal data class GatewayConnectConfig(
val host: String,
val port: Int,
@@ -33,22 +36,26 @@ internal data class GatewayConnectConfig(
val password: String,
)
/** Validation reason used by setup, QR, and manual endpoint copy. */
internal enum class GatewayEndpointValidationError {
INVALID_URL,
INSECURE_REMOTE_URL,
}
/** User input source used to choose endpoint-validation wording. */
internal enum class GatewayEndpointInputSource {
SETUP_CODE,
MANUAL,
QR_SCAN,
}
/** Endpoint parse result that preserves the reason when no usable config exists. */
internal data class GatewayEndpointParseResult(
val config: GatewayEndpointConfig? = null,
val error: GatewayEndpointValidationError? = null,
)
/** QR scan result that separates a usable setup code from validation copy. */
internal data class GatewayScannedSetupCodeResult(
val setupCode: String? = null,
val error: GatewayEndpointValidationError? = null,
@@ -60,6 +67,7 @@ private const val remoteGatewaySecurityRule =
private const val remoteGatewaySecurityFix =
"Use a private LAN IP for local setup, or enable Tailscale Serve / expose a wss:// gateway URL for remote access."
/** Resolves setup-code or manual UI fields into a connection config. */
internal fun resolveGatewayConnectConfig(
useSetupCode: Boolean,
setupCode: String,
@@ -77,6 +85,8 @@ internal fun resolveGatewayConnectConfig(
val setup = decodeGatewaySetupCode(setupCode) ?: return null
val parsed = parseGatewayEndpointResult(setup.url).config ?: return null
val setupBootstrapToken = setup.bootstrapToken?.trim().orEmpty()
// Bootstrap setup codes intentionally suppress stale shared credentials;
// the bootstrap token owns the first authenticated pairing exchange.
val sharedToken =
when {
!setup.token.isNullOrBlank() -> setup.token.trim()
@@ -121,8 +131,10 @@ internal fun resolveGatewayConnectConfig(
)
}
/** Parses an endpoint string and returns only the valid connection config. */
internal fun parseGatewayEndpoint(rawInput: String): GatewayEndpointConfig? = parseGatewayEndpointResult(rawInput).config
/** Parses and validates gateway endpoint input with user-facing error reasons. */
internal fun parseGatewayEndpointResult(rawInput: String): GatewayEndpointParseResult {
val raw = rawInput.trim()
if (raw.isEmpty()) return GatewayEndpointParseResult(error = GatewayEndpointValidationError.INVALID_URL)
@@ -166,6 +178,7 @@ internal fun parseGatewayEndpointResult(rawInput: String): GatewayEndpointParseR
)
}
/** Decodes base64url setup-code payloads produced by gateway onboarding. */
internal fun decodeGatewaySetupCode(rawInput: String): GatewaySetupCode? {
val trimmed = rawInput.trim()
if (trimmed.isEmpty()) return null
@@ -193,8 +206,10 @@ internal fun decodeGatewaySetupCode(rawInput: String): GatewaySetupCode? {
}
}
/** Extracts a setup code from QR scanner text when the embedded endpoint is valid. */
internal fun resolveScannedSetupCode(rawInput: String): String? = resolveScannedSetupCodeResult(rawInput).setupCode
/** Resolves QR scanner text to setup-code or validation error for UI copy. */
internal fun resolveScannedSetupCodeResult(rawInput: String): GatewayScannedSetupCodeResult {
val setupCode =
resolveSetupCodeCandidate(rawInput)
@@ -209,6 +224,7 @@ internal fun resolveScannedSetupCodeResult(rawInput: String): GatewayScannedSetu
return GatewayScannedSetupCodeResult(setupCode = setupCode)
}
/** Converts endpoint validation errors into setup-source-specific UI copy. */
internal fun gatewayEndpointValidationMessage(
error: GatewayEndpointValidationError,
source: GatewayEndpointInputSource,
@@ -231,6 +247,7 @@ internal fun gatewayEndpointValidationMessage(
}
}
/** Builds a URL from manual host/port/tls fields for shared endpoint parsing. */
internal fun composeGatewayManualUrl(
hostInput: String,
portInput: String,

View File

@@ -7,6 +7,7 @@ import android.content.Context
import android.os.Build
import android.widget.Toast
/** App version label shared by diagnostics and gateway-facing Android metadata. */
internal fun openClawAndroidVersionLabel(): String {
val versionName = BuildConfig.VERSION_NAME.trim().ifEmpty { "dev" }
return if (BuildConfig.DEBUG && !versionName.contains("dev", ignoreCase = true)) {
@@ -16,18 +17,22 @@ internal fun openClawAndroidVersionLabel(): String {
}
}
/** Normalizes blank gateway status text for display and diagnostics copy. */
internal fun gatewayStatusForDisplay(statusText: String): String = statusText.trim().ifEmpty { "Offline" }
/** Returns true when the status has enough signal to show diagnostics affordances. */
internal fun gatewayStatusHasDiagnostics(statusText: String): Boolean {
val lower = gatewayStatusForDisplay(statusText).lowercase()
return lower != "offline" && !lower.contains("connecting")
}
/** Detects pairing/approval status text so UI can offer pairing-specific actions. */
internal fun gatewayStatusLooksLikePairing(statusText: String): Boolean {
val lower = gatewayStatusForDisplay(statusText).lowercase()
return lower.contains("pair") || lower.contains("approve")
}
/** Builds the copyable support prompt with device, endpoint, and exact status context. */
internal fun buildGatewayDiagnosticsReport(
screen: String,
gatewayAddress: String,
@@ -67,6 +72,7 @@ internal fun buildGatewayDiagnosticsReport(
""".trimIndent()
}
/** Copies the diagnostics report to Android clipboard and shows a short confirmation toast. */
internal fun copyGatewayDiagnosticsReport(
context: Context,
screen: String,

View File

@@ -15,6 +15,7 @@ import kotlinx.coroutines.delay
internal const val PAIRING_INITIAL_AUTO_RETRY_MS = 1_500L
internal const val PAIRING_AUTO_RETRY_MS = 4_000L
/** Retries pairing-only gateway refreshes while the screen is visible and started. */
@Composable
internal fun PairingAutoRetryEffect(
enabled: Boolean,
@@ -41,6 +42,8 @@ internal fun PairingAutoRetryEffect(
if (!enabled || !lifecycleStarted) {
return@LaunchedEffect
}
// Give the gateway a short settling window before the first retry so an
// approval response is not immediately chased by a redundant reconnect.
delay(PAIRING_INITIAL_AUTO_RETRY_MS)
while (true) {
onRetry()

View File

@@ -27,6 +27,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
/** Settings health screen for gateway/node status and recent gateway logs. */
@Composable
internal fun HealthLogsSettingsScreen(
viewModel: MainViewModel,
@@ -45,6 +46,8 @@ internal fun HealthLogsSettingsScreen(
LaunchedEffect(isConnected) {
if (isConnected) {
// Load logs when the gateway becomes available; manual refresh covers
// later updates so this screen does not poll.
viewModel.refreshHealthLogs()
}
}
@@ -202,6 +205,8 @@ private fun GatewayLogRow(entry: GatewayLogEntry) {
private fun compactLogTime(value: String?): String {
val raw = value?.trim().orEmpty()
if (raw.isEmpty()) return "--:--"
// Gateway log timestamps may be ISO strings or already-compact fragments;
// keep only the HH:mm portion when present.
val time =
raw
.substringAfter('T', raw)

View File

@@ -97,6 +97,8 @@ internal fun darkMobileColors() =
chipBorderError = Color(0xFF3E1E1E),
)
// Defaulting to light tokens keeps previews/tests usable when a screen forgets to
// provide the app theme; production roots override this composition local.
internal val LocalMobileColors = staticCompositionLocalOf { lightMobileColors() }
internal object MobileColorsAccessor {
@@ -104,9 +106,8 @@ internal object MobileColorsAccessor {
@Composable get() = LocalMobileColors.current
}
// These allow existing call sites to keep using `mobileSurface`, `mobileText`, etc.
// without converting every file at once. Each resolves to the themed value.
// Keep these accessors while screens migrate to `MobileColorsAccessor.current`.
// Each getter must stay composable so callers always read the active theme.
internal val mobileSurface: Color @Composable get() = LocalMobileColors.current.surface
internal val mobileSurfaceStrong: Color @Composable get() = LocalMobileColors.current.surfaceStrong
internal val mobileCardSurface: Color @Composable get() = LocalMobileColors.current.cardSurface
@@ -129,7 +130,8 @@ internal val mobileCodeText: Color @Composable get() = LocalMobileColors.current
internal val mobileCodeBorder: Color @Composable get() = LocalMobileColors.current.codeBorder
internal val mobileCodeAccent: Color @Composable get() = LocalMobileColors.current.codeAccent
// Background gradient light fades white→gray, dark fades near-black→dark-gray
// Build the page backdrop from semantic surfaces so light/dark palettes keep
// their contrast relationship without duplicating raw color stops.
internal val mobileBackgroundGradient: Brush
@Composable get() {
val colors = LocalMobileColors.current

View File

@@ -29,6 +29,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
/** Settings screen for gateway nodes, paired devices, and pending pairing requests. */
@Composable
internal fun NodesDevicesSettingsScreen(
viewModel: MainViewModel,
@@ -41,6 +42,8 @@ internal fun NodesDevicesSettingsScreen(
LaunchedEffect(isConnected) {
if (isConnected) {
// Refresh once on connection; user-triggered refresh handles later changes
// so device admin state is not polled from Compose.
viewModel.refreshNodesDevices()
}
}
@@ -195,6 +198,7 @@ private fun DeviceListRow(
)
}
/** True when the gateway returned no node or device rows to render. */
private fun GatewayNodesDevicesSummary.isEmpty(): Boolean = nodes.isEmpty() && pendingDevices.isEmpty() && pairedDevices.isEmpty()
private fun nodeSubtitle(node: GatewayNodeSummary): String {

View File

@@ -0,0 +1,82 @@
package ai.openclaw.app.ui
import ai.openclaw.app.node.DeviceNotificationListenerService
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
/** App entry shown in the notification-forwarding package picker. */
data class InstalledApp(
val label: String,
val packageName: String,
val isSystemApp: Boolean,
)
/** Reads launcher, recent-notification, and configured packages for the picker. */
internal fun queryInstalledApps(
context: Context,
configuredPackages: Set<String>,
): List<InstalledApp> {
val packageManager = context.packageManager
val launcherIntent = Intent(Intent.ACTION_MAIN).apply { addCategory(Intent.CATEGORY_LAUNCHER) }
val launcherPackages =
packageManager
.queryIntentActivities(launcherIntent, PackageManager.MATCH_ALL)
.asSequence()
.mapNotNull {
it.activityInfo
?.packageName
?.trim()
?.takeIf(String::isNotEmpty)
}.toMutableSet()
val recentNotificationPackages =
DeviceNotificationListenerService
.recentPackages(context)
.asSequence()
.map { it.trim() }
.filter { it.isNotEmpty() }
.toList()
val candidatePackages =
resolveNotificationCandidatePackages(
launcherPackages = launcherPackages,
recentPackages = recentNotificationPackages,
configuredPackages = configuredPackages,
appPackageName = context.packageName,
)
return candidatePackages
.asSequence()
.mapNotNull { packageName ->
runCatching {
val appInfo = packageManager.getApplicationInfo(packageName, 0)
val label = packageManager.getApplicationLabel(appInfo).toString().trim()
InstalledApp(
label = if (label.isEmpty()) packageName else label,
packageName = packageName,
isSystemApp = (appInfo.flags and android.content.pm.ApplicationInfo.FLAG_SYSTEM) != 0,
)
}.getOrNull()
}.sortedWith(compareBy<InstalledApp> { it.label.lowercase() }.thenBy { it.packageName })
.toList()
}
/** Merges package sources while excluding OpenClaw from its own forwarding filter. */
internal fun resolveNotificationCandidatePackages(
launcherPackages: Set<String>,
recentPackages: List<String>,
configuredPackages: Set<String>,
appPackageName: String,
): Set<String> {
val blockedPackage = appPackageName.trim()
return sequenceOf(
configuredPackages.asSequence(),
launcherPackages.asSequence(),
recentPackages.asSequence(),
).flatten()
.map { it.trim() }
.filter { it.isNotEmpty() && it != blockedPackage }
.toSet()
}

View File

@@ -113,6 +113,7 @@ private enum class OnboardingStep {
private const val GATEWAY_CONNECT_SETTLING_MS = 2_500L
/** First-run Android onboarding flow for gateway pairing and permission setup. */
@Composable
fun OnboardingFlow(
viewModel: MainViewModel,
@@ -273,6 +274,8 @@ fun OnboardingFlow(
setupError = null
attemptedConnect = true
connectAttemptStartedAtMs = SystemClock.elapsedRealtime()
// Setup-code pairing replaces any stale shared credentials before
// the bootstrap token is stored for the first authenticated connect.
viewModel.resetGatewaySetupAuth()
viewModel.setManualEnabled(true)
viewModel.setManualHost(config.host)
@@ -905,6 +908,7 @@ internal enum class GatewayRecoveryUiState(
),
}
/** Derives recovery screen state from gateway/node readiness and transient status text. */
internal fun gatewayRecoveryUiState(
ready: Boolean,
statusText: String,
@@ -918,6 +922,7 @@ internal fun gatewayRecoveryUiState(
else -> GatewayRecoveryUiState.Failed
}
/** Detects gateway-approved states where the Android node is still coming online. */
internal fun gatewayStatusLooksLikePartialConnect(statusText: String): Boolean {
val lower = gatewayStatusForDisplay(statusText).lowercase()
return lower.contains("operator offline") || lower.contains("node offline")
@@ -932,6 +937,7 @@ private data class GatewayConfig(
val password: String,
)
/** Resolves setup-code or manual fields into the gateway config used for first connect. */
private fun resolveGatewayConfig(
setupCode: String,
manualHost: String,
@@ -944,6 +950,8 @@ private fun resolveGatewayConfig(
if (setup != null) {
val endpoint = parseGatewayEndpointResult(setup.url).config ?: return null
val bootstrapToken = setup.bootstrapToken?.trim().orEmpty()
// Bootstrap setup codes own first-pairing auth; fall back to typed token or
// password only for non-bootstrap setup payloads.
return GatewayConfig(
host = endpoint.host,
port = endpoint.port,
@@ -974,6 +982,7 @@ private fun resolveGatewayConfig(
)
}
/** Selects the recovery detail line from endpoint metadata and transient gateway status. */
private fun recoveryGatewayDetail(
ready: Boolean,
remoteAddress: String?,
@@ -991,6 +1000,7 @@ private fun recoveryGatewayDetail(
"Gateway unreachable"
}
/** Copies the onboarding recovery snapshot for support without including credentials. */
private fun copyGatewayDiagnostic(
context: Context,
statusText: String,
@@ -1011,6 +1021,7 @@ private fun copyGatewayDiagnostic(
Toast.makeText(context, "Diagnostic copied", Toast.LENGTH_SHORT).show()
}
/** One permission row plus launcher callback for onboarding's final setup step. */
private data class PermissionRowModel(
val title: String,
val subtitle: String,
@@ -1019,16 +1030,19 @@ private data class PermissionRowModel(
val onClick: () -> Unit,
)
/** Permission screen model plus a commit hook that persists granted feature toggles. */
private class PermissionState(
val rows: List<PermissionRowModel>,
val applyToViewModel: () -> Unit,
)
/** Onboarding can finish only after gateway and node channels are both ready. */
internal fun canFinishOnboarding(
isConnected: Boolean,
isNodeConnected: Boolean,
): Boolean = isConnected && isNodeConnected
/** Builds permission rows and applies granted feature toggles after onboarding. */
@Composable
private fun rememberPermissionState(
context: Context,
@@ -1170,6 +1184,7 @@ private fun hasPermission(
permission: String,
): Boolean = ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED
/** Returns true when Android exposes any motion sensor that can back node motion commands. */
private fun hasMotionCapabilities(context: Context): Boolean {
val sensorManager = context.getSystemService(SensorManager::class.java) ?: return false
return sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) != null ||

View File

@@ -13,6 +13,9 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.core.view.WindowCompat
/**
* App theme wrapper that installs dynamic Material colors and legacy mobile color tokens.
*/
@Composable
fun OpenClawTheme(content: @Composable () -> Unit) {
val context = LocalContext.current
@@ -35,6 +38,9 @@ fun OpenClawTheme(content: @Composable () -> Unit) {
}
}
/**
* Overlay background token tuned for panels floating over the mobile canvas.
*/
@Composable
fun overlayContainerColor(): Color {
val scheme = MaterialTheme.colorScheme
@@ -44,5 +50,8 @@ fun overlayContainerColor(): Color {
return if (isDark) base else base.copy(alpha = 0.88f)
}
/**
* Overlay icon token kept next to overlayContainerColor for callers outside the design package.
*/
@Composable
fun overlayIconColor(): Color = MaterialTheme.colorScheme.onSurfaceVariant

View File

@@ -68,6 +68,7 @@ private enum class StatusVisual {
Offline,
}
/** Legacy tab scaffold used by the mobile post-onboarding experience. */
@Composable
fun PostOnboardingTabs(
viewModel: MainViewModel,
@@ -150,6 +151,8 @@ fun PostOnboardingTabs(
.background(mobileBackgroundGradient),
) {
if (chatTabStarted) {
// Keep chat mounted after first use so session state and scroll position
// survive tab switches.
Box(
modifier =
Modifier
@@ -162,6 +165,8 @@ fun PostOnboardingTabs(
}
if (screenTabStarted) {
// Canvas can be expensive to initialize; keep it mounted once visited
// and hide it by alpha/z-order instead of destroying the view tree.
ScreenTabScreen(
viewModel = viewModel,
visible = activeTab == HomeTab.Screen,
@@ -184,6 +189,7 @@ fun PostOnboardingTabs(
}
}
/** Screen tab wrapper that refreshes canvas data once per gateway connection. */
@Composable
private fun ScreenTabScreen(
viewModel: MainViewModel,
@@ -205,6 +211,7 @@ private fun ScreenTabScreen(
}
}
/** Top status chip derived from gateway connection text. */
@Composable
private fun TopStatusBar(
statusText: String,
@@ -295,6 +302,7 @@ private fun TopStatusBar(
}
}
/** Bottom navigation for the legacy tab scaffold. */
@Composable
private fun BottomTabBar(
activeTab: HomeTab,

View File

@@ -55,6 +55,7 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
/** Android providers/models browser backed by the gateway catalog. */
@Composable
internal fun ProvidersModelsScreen(
viewModel: MainViewModel,
@@ -190,6 +191,7 @@ private data class ProviderRow(
val modelCount: Int,
)
/** Combines auth-provider readiness rows with catalog-only providers. */
private fun providerRows(
providers: List<GatewayModelProviderSummary>,
models: List<GatewayModelSummary>,
@@ -206,6 +208,8 @@ private fun providerRows(
modelCount = modelCounts[provider.id] ?: 0,
)
}
// 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 } }
@@ -245,6 +249,7 @@ private fun providerSetupSubtitle(
else -> "Add provider credentials on your Gateway"
}
/** Normalizes gateway provider status strings into a ready/not-ready boolean. */
internal fun modelProviderReady(status: String): Boolean {
val normalized = status.trim().lowercase()
return normalized == "ok" ||
@@ -254,6 +259,7 @@ internal fun modelProviderReady(status: String): Boolean {
normalized == "static"
}
/** Groups models by provider using the same display priority as provider rows. */
private fun sortedModelGroups(models: List<GatewayModelSummary>): List<Pair<String, List<GatewayModelSummary>>> =
models
.groupBy { it.provider }
@@ -483,6 +489,7 @@ private fun ModelRow(model: GatewayModelSummary) {
}
}
/** Derives compact capability chips for model catalog rows. */
private fun modelCapabilityLabels(model: GatewayModelSummary): List<String> =
buildList {
if (model.supportsReasoning) add("Reasoning")

View File

@@ -7,6 +7,7 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
/** Chooses onboarding or the authenticated app shell from persisted app state. */
@Composable
fun RootScreen(viewModel: MainViewModel) {
val onboardingCompleted by viewModel.onboardingCompleted.collectAsState()

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