mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-23 07:22:03 +08:00
Compare commits
684 Commits
fix/codeql
...
v2026.4.19
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dc3df91e95 | ||
|
|
bb6ba38a10 | ||
|
|
9a93ea9d7a | ||
|
|
3e081c5d21 | ||
|
|
c72f539ced | ||
|
|
83801c49f7 | ||
|
|
812f96cf24 | ||
|
|
2f84c47b8b | ||
|
|
d385b96451 | ||
|
|
d4e1a790ab | ||
|
|
34abb441f6 | ||
|
|
984ecd98ca | ||
|
|
528f296cfc | ||
|
|
45381135df | ||
|
|
2a14f76964 | ||
|
|
ca3e5ffd89 | ||
|
|
9686e518bc | ||
|
|
5ca33f7cb4 | ||
|
|
dfe2e81829 | ||
|
|
3eed321081 | ||
|
|
2a35ea4f07 | ||
|
|
efda761724 | ||
|
|
c63d6bf508 | ||
|
|
bcbb3de760 | ||
|
|
590474a9a4 | ||
|
|
10e14bd5be | ||
|
|
bfea6bebc9 | ||
|
|
ab4eb5aa94 | ||
|
|
f5c49758fc | ||
|
|
394c7a2357 | ||
|
|
91ad6c2739 | ||
|
|
04697eca88 | ||
|
|
1908967cfa | ||
|
|
f54cf74ef6 | ||
|
|
44166f7cfe | ||
|
|
6a87d6e814 | ||
|
|
0f871664c5 | ||
|
|
0a5515297e | ||
|
|
97a3089cec | ||
|
|
555f74cf67 | ||
|
|
9e93aa0c32 | ||
|
|
bf5b6cba70 | ||
|
|
24b915ed41 | ||
|
|
8233ca6401 | ||
|
|
bf2fbf071b | ||
|
|
199f4d78d9 | ||
|
|
4da808da50 | ||
|
|
67bd9edd8b | ||
|
|
6de5f92835 | ||
|
|
83a0f1fd52 | ||
|
|
314654bd0f | ||
|
|
22d99ee9df | ||
|
|
8f92c0607c | ||
|
|
74f0dc87de | ||
|
|
43f6ffd0ae | ||
|
|
c560793482 | ||
|
|
1212412ff1 | ||
|
|
a56aa6ccbe | ||
|
|
59032f63b1 | ||
|
|
72f4b4186b | ||
|
|
aa8331c836 | ||
|
|
4862d34925 | ||
|
|
e39af9545f | ||
|
|
e28984c74a | ||
|
|
3d3d585165 | ||
|
|
5200ffb90c | ||
|
|
0969336ef6 | ||
|
|
2d6f44b6ce | ||
|
|
ff5904f5f4 | ||
|
|
faae8e08b3 | ||
|
|
f8f98c116e | ||
|
|
b7d362ddbb | ||
|
|
a4ac25972b | ||
|
|
b73103ab85 | ||
|
|
14435c8bdf | ||
|
|
9380128193 | ||
|
|
5bbfa40255 | ||
|
|
496ccc3f73 | ||
|
|
383fa94c92 | ||
|
|
d1485ada9c | ||
|
|
1917c09d1c | ||
|
|
6798cbbd52 | ||
|
|
10d7c4d50e | ||
|
|
37bed56c1d | ||
|
|
caf8d75dfb | ||
|
|
ac8f0c9c0d | ||
|
|
9a1761d80c | ||
|
|
89a5eadd4e | ||
|
|
77876bd05c | ||
|
|
805481c176 | ||
|
|
9d8e923ddb | ||
|
|
af711f9e9f | ||
|
|
346aa0ed47 | ||
|
|
6f076dcde7 | ||
|
|
1f71137d1e | ||
|
|
046d983d26 | ||
|
|
550b946696 | ||
|
|
13e707fb7f | ||
|
|
1bef457cb6 | ||
|
|
f40bd56793 | ||
|
|
473225c471 | ||
|
|
f62766b996 | ||
|
|
22a9dade9c | ||
|
|
3fb87b127c | ||
|
|
e1fe71872c | ||
|
|
c96a0b1112 | ||
|
|
0dda02515f | ||
|
|
a86c43e1fd | ||
|
|
60d83b1d32 | ||
|
|
7ac3c2ca88 | ||
|
|
1ce9c355ab | ||
|
|
7b7d69a31e | ||
|
|
ac0515ce7e | ||
|
|
6e18f0e59e | ||
|
|
ba58bc3787 | ||
|
|
58a3527e17 | ||
|
|
2172bf1cdd | ||
|
|
a5ea6d3cf4 | ||
|
|
f1d04006e0 | ||
|
|
861e23b02c | ||
|
|
6ffcf4523d | ||
|
|
cf3c1994dc | ||
|
|
e88a9e5ee4 | ||
|
|
158ebbb2ed | ||
|
|
aaad2468c8 | ||
|
|
1652707c6e | ||
|
|
f8f9f13e0d | ||
|
|
91bb931b0f | ||
|
|
e7343dbfa8 | ||
|
|
b7446a0c65 | ||
|
|
2070142c49 | ||
|
|
570fb5594c | ||
|
|
17fcbcefbc | ||
|
|
57326feb8d | ||
|
|
58da2f5897 | ||
|
|
0e9d63a417 | ||
|
|
cbe124689d | ||
|
|
a0919685be | ||
|
|
ecfd6cfa73 | ||
|
|
fc0c707b98 | ||
|
|
15a7869bbc | ||
|
|
b97d50f2fb | ||
|
|
5aaec6a389 | ||
|
|
cef82adf19 | ||
|
|
cea60d603e | ||
|
|
3455c857a0 | ||
|
|
0001551143 | ||
|
|
3ea27c63e2 | ||
|
|
d9b05e601e | ||
|
|
4a5a43fb98 | ||
|
|
212c4af50d | ||
|
|
de9f726add | ||
|
|
daabd058fc | ||
|
|
7bc3019691 | ||
|
|
73728127b6 | ||
|
|
6fb74d4985 | ||
|
|
9a94194329 | ||
|
|
4e2541e5fb | ||
|
|
f76883d46c | ||
|
|
1fd049e307 | ||
|
|
e90c89cf8b | ||
|
|
a4a34edd21 | ||
|
|
f48c91ac2f | ||
|
|
8bfa06e992 | ||
|
|
e89e214516 | ||
|
|
310d2db312 | ||
|
|
3b2db583cd | ||
|
|
7481478303 | ||
|
|
f0f4fa6978 | ||
|
|
da22866030 | ||
|
|
808be2cae7 | ||
|
|
7b2a723891 | ||
|
|
40d2e5aa45 | ||
|
|
d2c1b743c0 | ||
|
|
966a3ea27c | ||
|
|
b4543caf55 | ||
|
|
e069169765 | ||
|
|
127bafa0b9 | ||
|
|
23ff2a9cf7 | ||
|
|
db0d212835 | ||
|
|
f00ef03d91 | ||
|
|
607c855621 | ||
|
|
2bca977ced | ||
|
|
688adf732d | ||
|
|
1af8bd90c3 | ||
|
|
26f1f28ffe | ||
|
|
f60c3bf6e0 | ||
|
|
5530cec127 | ||
|
|
46d6f500f3 | ||
|
|
c6784493fc | ||
|
|
4db3c5145f | ||
|
|
cc8f4e98a6 | ||
|
|
d5f8f62ab2 | ||
|
|
eed0a93c59 | ||
|
|
57b55883c5 | ||
|
|
85826c83e4 | ||
|
|
3a20606c04 | ||
|
|
2dabf1932f | ||
|
|
2e1ddedc58 | ||
|
|
dc30298b29 | ||
|
|
8879ed153d | ||
|
|
5d6ee4f73e | ||
|
|
e8b401d0c8 | ||
|
|
2fc429dfbf | ||
|
|
f1cc8f0cfc | ||
|
|
b2ca265f11 | ||
|
|
4a4f52b097 | ||
|
|
a018257487 | ||
|
|
f6921fd733 | ||
|
|
20debfab90 | ||
|
|
78288e37ed | ||
|
|
859eb06662 | ||
|
|
f98e98ab66 | ||
|
|
d97d5c04f0 | ||
|
|
6f450c2d1f | ||
|
|
5f2e77a6e1 | ||
|
|
554507b413 | ||
|
|
de2a9459e5 | ||
|
|
ea1e933b29 | ||
|
|
848f154f3e | ||
|
|
f298f86a7f | ||
|
|
8f648078bd | ||
|
|
ed463f6de0 | ||
|
|
3a3ab31d2b | ||
|
|
1d7d268a63 | ||
|
|
1687c672a7 | ||
|
|
045010bb78 | ||
|
|
6794ff411a | ||
|
|
35e31ed351 | ||
|
|
2d59395883 | ||
|
|
67ebc433f9 | ||
|
|
93a6c93865 | ||
|
|
b3a97df754 | ||
|
|
8ba5865383 | ||
|
|
60baaf6e04 | ||
|
|
b928f360a1 | ||
|
|
a2b093cf6a | ||
|
|
0195da6b0e | ||
|
|
6d40de45c7 | ||
|
|
98316cfbbd | ||
|
|
3cb142ff2e | ||
|
|
501a68a69b | ||
|
|
b5038fd9a1 | ||
|
|
cfd796a515 | ||
|
|
7d728afa12 | ||
|
|
712644f0d9 | ||
|
|
511a6c0ad0 | ||
|
|
155162a8cd | ||
|
|
4fa961d4f1 | ||
|
|
0c245c35c5 | ||
|
|
cd783b9946 | ||
|
|
afebeb5e9a | ||
|
|
866d1eef0a | ||
|
|
ab1e091e39 | ||
|
|
d1fb2d25ea | ||
|
|
2b7b5774b6 | ||
|
|
73e497f9be | ||
|
|
85912849cc | ||
|
|
a5d6330f87 | ||
|
|
58759bb565 | ||
|
|
f168a62068 | ||
|
|
796f272f7d | ||
|
|
ebfab7bf84 | ||
|
|
90b8f3fba2 | ||
|
|
d8b18f1d96 | ||
|
|
a07b9fc840 | ||
|
|
9e27d04dc3 | ||
|
|
fe0055a1d1 | ||
|
|
6ccac3d208 | ||
|
|
5e7b5cf285 | ||
|
|
ec86d0f64a | ||
|
|
5dbfaa15fa | ||
|
|
d3eeadba94 | ||
|
|
858a3f72fa | ||
|
|
f6d336935d | ||
|
|
1cc9bc58a2 | ||
|
|
1f1ff0567a | ||
|
|
cc919db83b | ||
|
|
84aed919a9 | ||
|
|
162bf51adb | ||
|
|
28fe0296c4 | ||
|
|
00e613f12d | ||
|
|
7474b52584 | ||
|
|
438799e929 | ||
|
|
28be124cc1 | ||
|
|
a7e029fde9 | ||
|
|
c39314c14a | ||
|
|
3f3bc97cd3 | ||
|
|
235cdb3f81 | ||
|
|
6b525023d4 | ||
|
|
5cc4426f88 | ||
|
|
089e038dfe | ||
|
|
4a870300dd | ||
|
|
90c1ab2cef | ||
|
|
16bd427cb6 | ||
|
|
e45a50c828 | ||
|
|
4180e7cd59 | ||
|
|
6d776593ea | ||
|
|
df525b90f2 | ||
|
|
630f2bcabe | ||
|
|
106b770c40 | ||
|
|
960bc52e3c | ||
|
|
1a7d89e85b | ||
|
|
3d994aa03b | ||
|
|
72979129fb | ||
|
|
e11039087c | ||
|
|
cd2ef0f3a3 | ||
|
|
07785c6dbc | ||
|
|
753183e081 | ||
|
|
c95d6049c2 | ||
|
|
76891c9cf8 | ||
|
|
8aadca4c3e | ||
|
|
aad9a833c0 | ||
|
|
6368559c02 | ||
|
|
31e5cd6376 | ||
|
|
e7d33b4870 | ||
|
|
f38727acd9 | ||
|
|
8c5a4eb866 | ||
|
|
ca1aa08709 | ||
|
|
54f121f843 | ||
|
|
fa2f53993a | ||
|
|
53239102f8 | ||
|
|
6f9cebf1ca | ||
|
|
791dbf4f9d | ||
|
|
cdaa70facb | ||
|
|
d13869aab9 | ||
|
|
464cbbc9f9 | ||
|
|
aa73df571d | ||
|
|
4852935e8e | ||
|
|
c035c5c0d2 | ||
|
|
68502c90d1 | ||
|
|
66385670e4 | ||
|
|
3f2e73b723 | ||
|
|
cf88e4876d | ||
|
|
840bf00887 | ||
|
|
e85e6bc4fb | ||
|
|
40c30d0062 | ||
|
|
6d55fa19db | ||
|
|
e5747629c3 | ||
|
|
552c0f22a6 | ||
|
|
dd618aa545 | ||
|
|
de4429ceb3 | ||
|
|
334f0a4de2 | ||
|
|
442deb0816 | ||
|
|
25ce5a5822 | ||
|
|
992b2143dd | ||
|
|
f7ceb98b72 | ||
|
|
a2eb8fa48f | ||
|
|
26cc1bc681 | ||
|
|
9501656a8e | ||
|
|
ef3f9796c8 | ||
|
|
eaaab098fb | ||
|
|
13a0d7a9e0 | ||
|
|
49ae60d6ca | ||
|
|
31437b9e3b | ||
|
|
3b9e0da02d | ||
|
|
4b5987829d | ||
|
|
c778562379 | ||
|
|
f45bc09206 | ||
|
|
18c4fd5678 | ||
|
|
c2fb4007c2 | ||
|
|
dc3b10285d | ||
|
|
458a52610a | ||
|
|
996eb9a024 | ||
|
|
2c3542e315 | ||
|
|
a0dd5f7e8e | ||
|
|
e910fe446a | ||
|
|
110f8bd2e1 | ||
|
|
dee99f27d1 | ||
|
|
a50ec27d3b | ||
|
|
a09bf67fa5 | ||
|
|
361750775d | ||
|
|
a22b789547 | ||
|
|
36068281fb | ||
|
|
0e4ddf7b38 | ||
|
|
c8d722d093 | ||
|
|
27f34f0491 | ||
|
|
6b99917d4e | ||
|
|
3abb5fd291 | ||
|
|
569247cff8 | ||
|
|
576ce7c656 | ||
|
|
4143da0ffa | ||
|
|
ac39cef969 | ||
|
|
30cbfa3457 | ||
|
|
3213fcddbe | ||
|
|
4c12ff6d23 | ||
|
|
ed65e8017d | ||
|
|
7db9a53254 | ||
|
|
52b8e318bd | ||
|
|
ca34c7cd7b | ||
|
|
5cf01ac7c1 | ||
|
|
e493d1d2fd | ||
|
|
75ffa29054 | ||
|
|
4749993bb5 | ||
|
|
0266cf4d10 | ||
|
|
b295f4afd8 | ||
|
|
e2351b5fdc | ||
|
|
5d8dceb37f | ||
|
|
5af1a51f8e | ||
|
|
8e0bcd0585 | ||
|
|
3ca8ad3845 | ||
|
|
6f4d13f3bd | ||
|
|
c54464a887 | ||
|
|
41ee813a45 | ||
|
|
8567dcfdd4 | ||
|
|
c756d61cdc | ||
|
|
b1c032245c | ||
|
|
503b748a8e | ||
|
|
cad1d04491 | ||
|
|
c9dfb19001 | ||
|
|
5d6041de81 | ||
|
|
647c56ef66 | ||
|
|
1da928211b | ||
|
|
141c7f8eaa | ||
|
|
d834d270df | ||
|
|
8a0977f405 | ||
|
|
c86beb237e | ||
|
|
2482e70fb8 | ||
|
|
c03f97f954 | ||
|
|
8b5030447a | ||
|
|
48c4a026dd | ||
|
|
420b1da82f | ||
|
|
afdbf48914 | ||
|
|
c0b8250f4f | ||
|
|
d89cee8787 | ||
|
|
815e2fc529 | ||
|
|
18b45e63f2 | ||
|
|
855c7cf989 | ||
|
|
78f0fb660c | ||
|
|
30895f7135 | ||
|
|
76812401ca | ||
|
|
5edf876a5e | ||
|
|
1e7c7dd02f | ||
|
|
f61712437f | ||
|
|
99ef3a63c5 | ||
|
|
af0f7e1bc7 | ||
|
|
8742e8fae3 | ||
|
|
8dde0acbae | ||
|
|
ff55cd5c16 | ||
|
|
0e7a992d3f | ||
|
|
77e588ebc3 | ||
|
|
5ae059db16 | ||
|
|
a8a701291b | ||
|
|
2c7c06c9b3 | ||
|
|
d0cf6731aa | ||
|
|
a001b5343f | ||
|
|
50e71daaa0 | ||
|
|
8b76bcba90 | ||
|
|
c550642cde | ||
|
|
fde25bfb8c | ||
|
|
08e1eb7a9f | ||
|
|
c408bbe9c9 | ||
|
|
809f42eeea | ||
|
|
087f1584df | ||
|
|
f7422e1fbc | ||
|
|
169b68d709 | ||
|
|
014eaa8492 | ||
|
|
e9d052d728 | ||
|
|
f897025d9b | ||
|
|
5f075d3d49 | ||
|
|
8747351383 | ||
|
|
bb5d9948c2 | ||
|
|
be6dbd4084 | ||
|
|
bbbb57f7f8 | ||
|
|
4dd999274b | ||
|
|
7c862da6a1 | ||
|
|
125b1e0e20 | ||
|
|
55c7776364 | ||
|
|
16e7f04a43 | ||
|
|
2e2f927d5d | ||
|
|
0a38098248 | ||
|
|
f61896b03c | ||
|
|
2745e5b3bd | ||
|
|
f70b651b12 | ||
|
|
dadcfb574f | ||
|
|
729feb4b99 | ||
|
|
8c3a8f0b1b | ||
|
|
ee0c8177bf | ||
|
|
462074c4c2 | ||
|
|
c0a9b694f3 | ||
|
|
2c43c441b2 | ||
|
|
b39f3cf266 | ||
|
|
79dfb4db69 | ||
|
|
990bd81726 | ||
|
|
90979d7c3e | ||
|
|
8eb577b361 | ||
|
|
7edce9c8fa | ||
|
|
e75cd46ba6 | ||
|
|
38923d13a6 | ||
|
|
b303b6c492 | ||
|
|
b6e55bf819 | ||
|
|
c47c4b3574 | ||
|
|
d155d578eb | ||
|
|
3a1e469732 | ||
|
|
f334ca2b50 | ||
|
|
e606656b56 | ||
|
|
9feeb921f5 | ||
|
|
c050cdaa96 | ||
|
|
783bb1f759 | ||
|
|
4ba12bd134 | ||
|
|
f0c6b102be | ||
|
|
354dbf2161 | ||
|
|
1a3a040cc3 | ||
|
|
6184f17c91 | ||
|
|
bbac7773ff | ||
|
|
aaf9064a75 | ||
|
|
36b98f78b2 | ||
|
|
ddd2c2a602 | ||
|
|
f7eb746081 | ||
|
|
c2e4b47d7b | ||
|
|
628e6cd446 | ||
|
|
5d8cecbe7d | ||
|
|
2b08233a3e | ||
|
|
a464f5926b | ||
|
|
20cf51169b | ||
|
|
eed71160ae | ||
|
|
5c2f4afcce | ||
|
|
1df50183b2 | ||
|
|
0747a9c85a | ||
|
|
7876e3e736 | ||
|
|
1519b006b8 | ||
|
|
f93b7da4c4 | ||
|
|
9bcf8f8243 | ||
|
|
474b08bfbd | ||
|
|
00fadb978f | ||
|
|
f665c767e6 | ||
|
|
81d6cf9c82 | ||
|
|
f513bae67e | ||
|
|
76c8db3766 | ||
|
|
3ed0995fa9 | ||
|
|
df06343dfa | ||
|
|
8448569aca | ||
|
|
114b87caf2 | ||
|
|
dfca5bd0fe | ||
|
|
89d3117ad0 | ||
|
|
42817a1707 | ||
|
|
8c249a8cca | ||
|
|
8d7a722487 | ||
|
|
f8a0ae0b08 | ||
|
|
06e3d53c8a | ||
|
|
7815d25eef | ||
|
|
8caad53f57 | ||
|
|
769198e67e | ||
|
|
41ef752dd8 | ||
|
|
c580933623 | ||
|
|
b9d5c1a58b | ||
|
|
1d26f0cc6e | ||
|
|
75e09e21f2 | ||
|
|
a027a40c90 | ||
|
|
97f713f459 | ||
|
|
c0a16650d5 | ||
|
|
a71b810e43 | ||
|
|
ccc23f6cb6 | ||
|
|
c66703300a | ||
|
|
79cd5ed368 | ||
|
|
54d9a09912 | ||
|
|
24f8d6470e | ||
|
|
73d8d3b2eb | ||
|
|
d851f9e816 | ||
|
|
d7f9f67296 | ||
|
|
14c4d6457a | ||
|
|
1fad8efa12 | ||
|
|
7b27d08e56 | ||
|
|
8de7aefe0a | ||
|
|
d6c90b5af1 | ||
|
|
2535331e94 | ||
|
|
acace04c35 | ||
|
|
0b3d876e74 | ||
|
|
d565c2cc34 | ||
|
|
5f3bb53788 | ||
|
|
a954e2fb46 | ||
|
|
b2b835fb18 | ||
|
|
62703d8430 | ||
|
|
4d7d14cfa7 | ||
|
|
bac3d26fe7 | ||
|
|
2a0a498b0d | ||
|
|
3b81bf4c7c | ||
|
|
40c9da1d57 | ||
|
|
48aa076d12 | ||
|
|
310b5e4f6a | ||
|
|
418056f7a0 | ||
|
|
af954a81d1 | ||
|
|
605cb60586 | ||
|
|
a861da41b5 | ||
|
|
199bb1fe05 | ||
|
|
d3e12cee7e | ||
|
|
d3b70f9823 | ||
|
|
f7f88e52e4 | ||
|
|
675eb38ad0 | ||
|
|
a90daa5759 | ||
|
|
e477125608 | ||
|
|
7995d43625 | ||
|
|
68cf9e52a2 | ||
|
|
e53a8bd865 | ||
|
|
290371399f | ||
|
|
e2099301c5 | ||
|
|
f810cc4d58 | ||
|
|
efb37f8949 | ||
|
|
c93b2540ec | ||
|
|
ab726235bd | ||
|
|
824b5e4d91 | ||
|
|
bb70b41340 | ||
|
|
a9fab78f64 | ||
|
|
e4f04d92a3 | ||
|
|
24ef516879 | ||
|
|
2e3ef1b9e1 | ||
|
|
4ac8b08265 | ||
|
|
e00f9c7a9d | ||
|
|
e19e94ef07 | ||
|
|
cfba24fa3c | ||
|
|
8ebb3ff0d4 | ||
|
|
4451e8479a | ||
|
|
271fc360e7 | ||
|
|
82355d1d9f | ||
|
|
769a09842d | ||
|
|
82fe6f50ef | ||
|
|
a45ebf3281 | ||
|
|
be7a415eb0 | ||
|
|
f377db1015 | ||
|
|
0b6c39be18 | ||
|
|
3ea1bf4232 | ||
|
|
54e4e16844 | ||
|
|
00951dc9f9 | ||
|
|
82b529a6d9 | ||
|
|
5775fe272a | ||
|
|
8e79080bef | ||
|
|
7d4f1a6777 | ||
|
|
89706d323c | ||
|
|
e4c4f955b3 | ||
|
|
74c198f2e8 | ||
|
|
0ee5baf6c5 | ||
|
|
1ffc02e930 | ||
|
|
1ce2596195 | ||
|
|
857b9cd326 | ||
|
|
9d5ab4a54c | ||
|
|
299694d721 | ||
|
|
2713089220 | ||
|
|
b945248650 | ||
|
|
b1a3ad49a4 | ||
|
|
c66f16ac55 | ||
|
|
92859357bb | ||
|
|
dd9d2ebd01 | ||
|
|
5817a76236 | ||
|
|
a0d9598425 | ||
|
|
daaebb8558 | ||
|
|
f57ce21d73 | ||
|
|
b71c91022b | ||
|
|
d07c921ae3 | ||
|
|
132d3c76a0 | ||
|
|
35dcd06764 | ||
|
|
7ae670e501 | ||
|
|
878f2122e5 | ||
|
|
807c6648f9 | ||
|
|
178c36532d | ||
|
|
26f7198eda | ||
|
|
bec52e5f7e | ||
|
|
5aad79571e | ||
|
|
671579663b | ||
|
|
7b0e950e09 | ||
|
|
8205de84a9 | ||
|
|
c65f356ddc | ||
|
|
7e18c07e41 | ||
|
|
3fe8b24c4e | ||
|
|
c95507978f | ||
|
|
59d07f0ab4 | ||
|
|
5c1d6feb33 | ||
|
|
e8fd148437 | ||
|
|
2a283e87a7 | ||
|
|
15b2827fc1 | ||
|
|
65645ec54f | ||
|
|
e8ae3901b6 | ||
|
|
8e444ac5a6 | ||
|
|
6b45ba88a1 | ||
|
|
353950894a | ||
|
|
9da4d5f5df | ||
|
|
c6af0437c9 | ||
|
|
a2f2e5738e | ||
|
|
35fb3f7e1c | ||
|
|
a189394590 | ||
|
|
685f9903ec | ||
|
|
24431e5114 | ||
|
|
ee856ab31f | ||
|
|
acd86a06cd | ||
|
|
77e6e4cf87 |
@@ -127,7 +127,7 @@ The `fetch-content` output for `discussion_comment` includes `comment_node_id` a
|
||||
The recreated comment should follow this format:
|
||||
|
||||
```
|
||||
> **Note from maintainer (@<LOGIN>):** The original comment by @<AUTHOR> has been removed due to secret leakage. Below is the redacted version of the original content.
|
||||
> **Note:** The original comment by @<AUTHOR> has been removed due to secret leakage. Below is the redacted version of the original content.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -52,7 +52,11 @@ function ghGraphQL(query, options = {}) {
|
||||
|
||||
function failOnGraphQLFailure(result, message) {
|
||||
if (result?.gh_failed) {
|
||||
const details = (result.stderr || result.stdout || `gh exited with status ${result.status}`).trim();
|
||||
const details = (
|
||||
result.stderr ||
|
||||
result.stdout ||
|
||||
`gh exited with status ${result.status}`
|
||||
).trim();
|
||||
fail(`${message}: ${details}`);
|
||||
}
|
||||
if (Array.isArray(result?.errors) && result.errors.length > 0) {
|
||||
@@ -73,9 +77,7 @@ function formatGraphQLAfterClause(cursor) {
|
||||
}
|
||||
|
||||
function findDiscussionCommentNode(nodes, discussionCommentDbId) {
|
||||
return (
|
||||
nodes.find((node) => String(node.databaseId) === String(discussionCommentDbId)) || null
|
||||
);
|
||||
return nodes.find((node) => String(node.databaseId) === String(discussionCommentDbId)) || null;
|
||||
}
|
||||
|
||||
function fetchDiscussionReplyPage(commentNodeId, cursor) {
|
||||
@@ -169,9 +171,13 @@ function fetchDiscussionComment(discussionNumber, discussionCommentDbId) {
|
||||
|
||||
while (!reply && hasMoreReplies) {
|
||||
const replyPage = fetchDiscussionReplyPage(topLevelComment.id, replyCursor);
|
||||
failOnGraphQLFailure(replyPage, `Failed to fetch replies for discussion comment ${topLevelComment.id}`);
|
||||
failOnGraphQLFailure(
|
||||
replyPage,
|
||||
`Failed to fetch replies for discussion comment ${topLevelComment.id}`,
|
||||
);
|
||||
const replies = replyPage?.data?.node?.replies;
|
||||
if (!replies) fail(`Failed to paginate replies for discussion comment ${topLevelComment.id}`);
|
||||
if (!replies)
|
||||
fail(`Failed to paginate replies for discussion comment ${topLevelComment.id}`);
|
||||
|
||||
reply = findDiscussionCommentNode(replies.nodes, discussionCommentDbId);
|
||||
hasMoreReplies = replies.pageInfo.hasNextPage;
|
||||
@@ -189,9 +195,7 @@ function fetchDiscussionComment(discussionNumber, discussionCommentDbId) {
|
||||
}
|
||||
|
||||
function createDiscussionComment(discussionNodeId, body, replyToNodeId) {
|
||||
const replyToClause = replyToNodeId
|
||||
? `, replyToId: "${escapeGraphQLString(replyToNodeId)}"`
|
||||
: "";
|
||||
const replyToClause = replyToNodeId ? `, replyToId: "${escapeGraphQLString(replyToNodeId)}"` : "";
|
||||
const result = ghGraphQL(
|
||||
`mutation { addDiscussionComment(input: { discussionId: "${escapeGraphQLString(discussionNodeId)}"${replyToClause}, body: "${escapeGraphQLString(body)}" }) { comment { id url } } }`,
|
||||
);
|
||||
@@ -261,7 +265,10 @@ function cmdFetchContent(locationJson) {
|
||||
const discussionNumber = urlMatch[1];
|
||||
const discussionCommentDbId = urlMatch[2];
|
||||
|
||||
const { discussionId, comment } = fetchDiscussionComment(discussionNumber, discussionCommentDbId);
|
||||
const { discussionId, comment } = fetchDiscussionComment(
|
||||
discussionNumber,
|
||||
discussionCommentDbId,
|
||||
);
|
||||
if (!comment)
|
||||
fail(
|
||||
`Discussion comment #${discussionCommentDbId} not found in discussion #${discussionNumber}`,
|
||||
|
||||
4
.github/instructions/copilot.instructions.md
vendored
4
.github/instructions/copilot.instructions.md
vendored
@@ -50,13 +50,13 @@
|
||||
- Keep files under ~700 LOC - extract helpers when larger
|
||||
- Colocated tests: `*.test.ts` next to source files
|
||||
- Run `pnpm check` before commits (lint + format)
|
||||
- Run `pnpm tsgo` for type checking
|
||||
- Run `pnpm tsgo` for production type checking, or `pnpm tsgo:all` for production plus test types
|
||||
|
||||
## Stack & Commands
|
||||
|
||||
- **Package manager**: pnpm (`pnpm install`)
|
||||
- **Dev**: `pnpm openclaw ...` or `pnpm dev`
|
||||
- **Type-check**: `pnpm tsgo`
|
||||
- **Type-check**: `pnpm tsgo` (production), `pnpm tsgo:all` (production plus tests)
|
||||
- **Lint/format**: `pnpm check`
|
||||
- **Tests**: `pnpm test`
|
||||
- **Build**: `pnpm build`
|
||||
|
||||
815
.github/workflows/ci.yml
vendored
815
.github/workflows/ci.yml
vendored
@@ -212,7 +212,7 @@ jobs:
|
||||
{
|
||||
check_name: "checks-fast-contracts-protocol",
|
||||
runtime: "node",
|
||||
task: "contracts-protocol",
|
||||
task: "contracts",
|
||||
},
|
||||
]
|
||||
: [],
|
||||
@@ -408,10 +408,52 @@ jobs:
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: false
|
||||
shell: bash
|
||||
env:
|
||||
CHECKOUT_REPO: ${{ github.repository }}
|
||||
CHECKOUT_SHA: ${{ github.sha }}
|
||||
CHECKOUT_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
workdir="$GITHUB_WORKSPACE"
|
||||
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 30s git -C "$workdir" \
|
||||
-c protocol.version=2 \
|
||||
-c "http.https://github.com/.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}/2 succeeded"
|
||||
}
|
||||
|
||||
for attempt in 1 2; do
|
||||
if checkout_attempt "$attempt"; then
|
||||
exit 0
|
||||
fi
|
||||
echo "checkout attempt ${attempt}/2 failed"
|
||||
sleep $((attempt * 5))
|
||||
done
|
||||
|
||||
echo "checkout failed after 2 attempts" >&2
|
||||
exit 1
|
||||
|
||||
- name: Ensure secrets base commit (PR fast path)
|
||||
if: github.event_name == 'pull_request'
|
||||
@@ -467,10 +509,52 @@ jobs:
|
||||
matrix: ${{ fromJson(needs.preflight.outputs.checks_fast_core_matrix) }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: false
|
||||
shell: bash
|
||||
env:
|
||||
CHECKOUT_REPO: ${{ github.repository }}
|
||||
CHECKOUT_SHA: ${{ github.sha }}
|
||||
CHECKOUT_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
workdir="$GITHUB_WORKSPACE"
|
||||
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 30s git -C "$workdir" \
|
||||
-c protocol.version=2 \
|
||||
-c "http.https://github.com/.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}/2 succeeded"
|
||||
}
|
||||
|
||||
for attempt in 1 2; do
|
||||
if checkout_attempt "$attempt"; then
|
||||
exit 0
|
||||
fi
|
||||
echo "checkout attempt ${attempt}/2 failed"
|
||||
sleep $((attempt * 5))
|
||||
done
|
||||
|
||||
echo "checkout failed after 2 attempts" >&2
|
||||
exit 1
|
||||
|
||||
- name: Setup Node environment
|
||||
uses: ./.github/actions/setup-node-env
|
||||
@@ -489,10 +573,8 @@ jobs:
|
||||
bundled)
|
||||
pnpm test:bundled
|
||||
;;
|
||||
contracts|contracts-protocol)
|
||||
pnpm build
|
||||
contracts)
|
||||
pnpm test:contracts
|
||||
pnpm protocol:check
|
||||
;;
|
||||
*)
|
||||
echo "Unsupported checks-fast task: $TASK" >&2
|
||||
@@ -500,6 +582,72 @@ jobs:
|
||||
;;
|
||||
esac
|
||||
|
||||
checks-fast-protocol:
|
||||
permissions:
|
||||
contents: read
|
||||
name: "checks-fast-protocol"
|
||||
needs: [preflight]
|
||||
if: needs.preflight.outputs.run_checks_fast == 'true'
|
||||
runs-on: blacksmith-16vcpu-ubuntu-2404
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: Checkout
|
||||
shell: bash
|
||||
env:
|
||||
CHECKOUT_REPO: ${{ github.repository }}
|
||||
CHECKOUT_SHA: ${{ github.sha }}
|
||||
CHECKOUT_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
workdir="$GITHUB_WORKSPACE"
|
||||
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 30s git -C "$workdir" \
|
||||
-c protocol.version=2 \
|
||||
-c "http.https://github.com/.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}/2 succeeded"
|
||||
}
|
||||
|
||||
for attempt in 1 2; do
|
||||
if checkout_attempt "$attempt"; then
|
||||
exit 0
|
||||
fi
|
||||
echo "checkout attempt ${attempt}/2 failed"
|
||||
sleep $((attempt * 5))
|
||||
done
|
||||
|
||||
echo "checkout failed after 2 attempts" >&2
|
||||
exit 1
|
||||
|
||||
- name: Setup Node environment
|
||||
uses: ./.github/actions/setup-node-env
|
||||
with:
|
||||
install-bun: "false"
|
||||
use-sticky-disk: "false"
|
||||
|
||||
- name: Run protocol check
|
||||
run: pnpm protocol:check
|
||||
|
||||
checks-node-extensions-shard:
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -513,10 +661,52 @@ jobs:
|
||||
matrix: ${{ fromJson(needs.preflight.outputs.checks_node_extensions_matrix) }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: false
|
||||
shell: bash
|
||||
env:
|
||||
CHECKOUT_REPO: ${{ github.repository }}
|
||||
CHECKOUT_SHA: ${{ github.sha }}
|
||||
CHECKOUT_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
workdir="$GITHUB_WORKSPACE"
|
||||
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 30s git -C "$workdir" \
|
||||
-c protocol.version=2 \
|
||||
-c "http.https://github.com/.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}/2 succeeded"
|
||||
}
|
||||
|
||||
for attempt in 1 2; do
|
||||
if checkout_attempt "$attempt"; then
|
||||
exit 0
|
||||
fi
|
||||
echo "checkout attempt ${attempt}/2 failed"
|
||||
sleep $((attempt * 5))
|
||||
done
|
||||
|
||||
echo "checkout failed after 2 attempts" >&2
|
||||
exit 1
|
||||
|
||||
- name: Setup Node environment
|
||||
uses: ./.github/actions/setup-node-env
|
||||
@@ -565,10 +755,52 @@ jobs:
|
||||
|
||||
- name: Checkout
|
||||
if: github.event_name != 'pull_request' || matrix.task != 'compat-node22'
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: false
|
||||
shell: bash
|
||||
env:
|
||||
CHECKOUT_REPO: ${{ github.repository }}
|
||||
CHECKOUT_SHA: ${{ github.sha }}
|
||||
CHECKOUT_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
workdir="$GITHUB_WORKSPACE"
|
||||
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 30s git -C "$workdir" \
|
||||
-c protocol.version=2 \
|
||||
-c "http.https://github.com/.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}/2 succeeded"
|
||||
}
|
||||
|
||||
for attempt in 1 2; do
|
||||
if checkout_attempt "$attempt"; then
|
||||
exit 0
|
||||
fi
|
||||
echo "checkout attempt ${attempt}/2 failed"
|
||||
sleep $((attempt * 5))
|
||||
done
|
||||
|
||||
echo "checkout failed after 2 attempts" >&2
|
||||
exit 1
|
||||
|
||||
- name: Setup Node environment
|
||||
if: github.event_name != 'pull_request' || matrix.task != 'compat-node22'
|
||||
@@ -677,9 +909,10 @@ jobs:
|
||||
-c protocol.version=2 \
|
||||
-c "http.https://github.com/.extraheader=AUTHORIZATION: basic ${auth_header}" \
|
||||
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
|
||||
"+${CHECKOUT_SHA}:refs/remotes/origin/ci-target"
|
||||
"+${CHECKOUT_SHA}:refs/remotes/origin/ci-target" || return 1
|
||||
|
||||
git -C "$workdir" checkout --force --detach "$CHECKOUT_SHA"
|
||||
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}/2 succeeded"
|
||||
}
|
||||
|
||||
@@ -798,10 +1031,52 @@ jobs:
|
||||
matrix: ${{ fromJson(needs.preflight.outputs.extension_fast_matrix) }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: false
|
||||
shell: bash
|
||||
env:
|
||||
CHECKOUT_REPO: ${{ github.repository }}
|
||||
CHECKOUT_SHA: ${{ github.sha }}
|
||||
CHECKOUT_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
workdir="$GITHUB_WORKSPACE"
|
||||
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 30s git -C "$workdir" \
|
||||
-c protocol.version=2 \
|
||||
-c "http.https://github.com/.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}/2 succeeded"
|
||||
}
|
||||
|
||||
for attempt in 1 2; do
|
||||
if checkout_attempt "$attempt"; then
|
||||
exit 0
|
||||
fi
|
||||
echo "checkout attempt ${attempt}/2 failed"
|
||||
sleep $((attempt * 5))
|
||||
done
|
||||
|
||||
echo "checkout failed after 2 attempts" >&2
|
||||
exit 1
|
||||
|
||||
- name: Setup Node environment
|
||||
uses: ./.github/actions/setup-node-env
|
||||
@@ -825,10 +1100,52 @@ jobs:
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: false
|
||||
shell: bash
|
||||
env:
|
||||
CHECKOUT_REPO: ${{ github.repository }}
|
||||
CHECKOUT_SHA: ${{ github.sha }}
|
||||
CHECKOUT_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
workdir="$GITHUB_WORKSPACE"
|
||||
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 30s git -C "$workdir" \
|
||||
-c protocol.version=2 \
|
||||
-c "http.https://github.com/.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}/2 succeeded"
|
||||
}
|
||||
|
||||
for attempt in 1 2; do
|
||||
if checkout_attempt "$attempt"; then
|
||||
exit 0
|
||||
fi
|
||||
echo "checkout attempt ${attempt}/2 failed"
|
||||
sleep $((attempt * 5))
|
||||
done
|
||||
|
||||
echo "checkout failed after 2 attempts" >&2
|
||||
exit 1
|
||||
|
||||
- name: Setup Node environment
|
||||
uses: ./.github/actions/setup-node-env
|
||||
@@ -844,20 +1161,72 @@ jobs:
|
||||
- name: Strict TS build smoke
|
||||
run: pnpm build:strict-smoke
|
||||
|
||||
check-additional:
|
||||
check-additional-shard:
|
||||
permissions:
|
||||
contents: read
|
||||
name: "check-additional"
|
||||
name: ${{ matrix.check_name }}
|
||||
needs: [preflight]
|
||||
if: always() && needs.preflight.outputs.run_check_additional == 'true'
|
||||
runs-on: blacksmith-16vcpu-ubuntu-2404
|
||||
timeout-minutes: 20
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- check_name: check-additional-boundaries
|
||||
group: boundaries
|
||||
- check_name: check-additional-extension-surfaces
|
||||
group: extension-surfaces
|
||||
- check_name: check-additional-runtime-topology
|
||||
group: runtime-topology
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: false
|
||||
shell: bash
|
||||
env:
|
||||
CHECKOUT_REPO: ${{ github.repository }}
|
||||
CHECKOUT_SHA: ${{ github.sha }}
|
||||
CHECKOUT_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
workdir="$GITHUB_WORKSPACE"
|
||||
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 30s git -C "$workdir" \
|
||||
-c protocol.version=2 \
|
||||
-c "http.https://github.com/.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}/2 succeeded"
|
||||
}
|
||||
|
||||
for attempt in 1 2; do
|
||||
if checkout_attempt "$attempt"; then
|
||||
exit 0
|
||||
fi
|
||||
echo "checkout attempt ${attempt}/2 failed"
|
||||
sleep $((attempt * 5))
|
||||
done
|
||||
|
||||
echo "checkout failed after 2 attempts" >&2
|
||||
exit 1
|
||||
|
||||
- name: Setup Node environment
|
||||
uses: ./.github/actions/setup-node-env
|
||||
@@ -865,193 +1234,97 @@ jobs:
|
||||
install-bun: "false"
|
||||
use-sticky-disk: "false"
|
||||
|
||||
- name: Run plugin extension boundary guard
|
||||
id: plugin_extension_boundary
|
||||
continue-on-error: true
|
||||
run: pnpm run lint:plugins:no-extension-imports
|
||||
|
||||
- name: Run no-random-messaging guard
|
||||
id: no_random_messaging
|
||||
continue-on-error: true
|
||||
run: pnpm run lint:tmp:no-random-messaging
|
||||
|
||||
- name: Run channel-agnostic boundary guard
|
||||
id: channel_agnostic_boundaries
|
||||
continue-on-error: true
|
||||
run: pnpm run lint:tmp:channel-agnostic-boundaries
|
||||
|
||||
- name: Run no-raw-channel-fetch guard
|
||||
id: no_raw_channel_fetch
|
||||
continue-on-error: true
|
||||
run: pnpm run lint:tmp:no-raw-channel-fetch
|
||||
|
||||
- name: Run ingress owner guard
|
||||
id: ingress_owner
|
||||
continue-on-error: true
|
||||
run: pnpm run lint:agent:ingress-owner
|
||||
|
||||
- name: Run no-register-http-handler guard
|
||||
id: no_register_http_handler
|
||||
continue-on-error: true
|
||||
run: pnpm run lint:plugins:no-register-http-handler
|
||||
|
||||
- name: Run no-monolithic plugin-sdk entry import guard
|
||||
id: no_monolithic_plugin_sdk_entry_imports
|
||||
continue-on-error: true
|
||||
run: pnpm run lint:plugins:no-monolithic-plugin-sdk-entry-imports
|
||||
|
||||
- name: Run no-extension-src-imports guard
|
||||
id: no_extension_src_imports
|
||||
continue-on-error: true
|
||||
run: pnpm run lint:plugins:no-extension-src-imports
|
||||
|
||||
- name: Run no-extension-test-core-imports guard
|
||||
id: no_extension_test_core_imports
|
||||
continue-on-error: true
|
||||
run: pnpm run lint:plugins:no-extension-test-core-imports
|
||||
|
||||
- name: Run plugin-sdk subpaths exported guard
|
||||
id: plugin_sdk_subpaths_exported
|
||||
continue-on-error: true
|
||||
run: pnpm run lint:plugins:plugin-sdk-subpaths-exported
|
||||
|
||||
- name: Run web search provider boundary guard
|
||||
id: web_search_provider_boundary
|
||||
continue-on-error: true
|
||||
run: pnpm run lint:web-search-provider-boundaries
|
||||
|
||||
- name: Run web fetch provider boundary guard
|
||||
id: web_fetch_provider_boundary
|
||||
continue-on-error: true
|
||||
run: pnpm run lint:web-fetch-provider-boundaries
|
||||
|
||||
- name: Run extension src boundary guard
|
||||
id: extension_src_outside_plugin_sdk_boundary
|
||||
continue-on-error: true
|
||||
run: pnpm run lint:extensions:no-src-outside-plugin-sdk
|
||||
|
||||
- name: Run extension plugin-sdk-internal guard
|
||||
id: extension_plugin_sdk_internal_boundary
|
||||
continue-on-error: true
|
||||
run: pnpm run lint:extensions:no-plugin-sdk-internal
|
||||
|
||||
- name: Run extension relative-outside-package guard
|
||||
id: extension_relative_outside_package_boundary
|
||||
continue-on-error: true
|
||||
run: pnpm run lint:extensions:no-relative-outside-package
|
||||
|
||||
- name: Run extension channel lint
|
||||
id: extension_channel_lint
|
||||
continue-on-error: true
|
||||
run: pnpm run lint:extensions:channels
|
||||
|
||||
- name: Run bundled extension lint
|
||||
id: extension_bundled_lint
|
||||
continue-on-error: true
|
||||
run: pnpm run lint:extensions:bundled
|
||||
|
||||
- name: Run extension package boundary TypeScript check
|
||||
id: extension_package_boundary_tsc
|
||||
continue-on-error: true
|
||||
- name: Run additional check shard
|
||||
env:
|
||||
ADDITIONAL_CHECK_GROUP: ${{ matrix.group }}
|
||||
RUN_CONTROL_UI_I18N: ${{ needs.preflight.outputs.run_control_ui_i18n }}
|
||||
OPENCLAW_EXTENSION_BOUNDARY_CONCURRENCY: 4
|
||||
run: pnpm run test:extensions:package-boundary
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
- name: Enforce safe external URL opening policy
|
||||
id: no_raw_window_open
|
||||
continue-on-error: true
|
||||
run: pnpm lint:ui:no-raw-window-open
|
||||
failures=0
|
||||
|
||||
- name: Check control UI locale sync
|
||||
id: control_ui_i18n
|
||||
if: needs.preflight.outputs.run_control_ui_i18n == 'true'
|
||||
continue-on-error: true
|
||||
run: pnpm ui:i18n:check
|
||||
run_check() {
|
||||
local label="$1"
|
||||
shift
|
||||
|
||||
- name: Run gateway watch regression harness
|
||||
id: gateway_watch_regression
|
||||
continue-on-error: true
|
||||
run: pnpm test:gateway:watch-regression
|
||||
echo "::group::${label}"
|
||||
if "$@"; then
|
||||
echo "[ok] ${label}"
|
||||
else
|
||||
echo "::error title=${label} failed::${label} failed"
|
||||
failures=1
|
||||
fi
|
||||
echo "::endgroup::"
|
||||
}
|
||||
|
||||
- name: Run import cycle guard
|
||||
id: import_cycles
|
||||
continue-on-error: true
|
||||
run: pnpm check:import-cycles
|
||||
case "$ADDITIONAL_CHECK_GROUP" in
|
||||
boundaries)
|
||||
run_check "plugin-extension-boundary" pnpm run lint:plugins:no-extension-imports
|
||||
run_check "lint:tmp:no-random-messaging" pnpm run lint:tmp:no-random-messaging
|
||||
run_check "lint:tmp:channel-agnostic-boundaries" pnpm run lint:tmp:channel-agnostic-boundaries
|
||||
run_check "lint:tmp:tsgo-core-boundary" pnpm run lint:tmp:tsgo-core-boundary
|
||||
run_check "lint:tmp:no-raw-channel-fetch" pnpm run lint:tmp:no-raw-channel-fetch
|
||||
run_check "lint:agent:ingress-owner" pnpm run lint:agent:ingress-owner
|
||||
run_check "lint:plugins:no-register-http-handler" pnpm run lint:plugins:no-register-http-handler
|
||||
run_check "lint:plugins:no-monolithic-plugin-sdk-entry-imports" pnpm run lint:plugins:no-monolithic-plugin-sdk-entry-imports
|
||||
run_check "lint:plugins:no-extension-src-imports" pnpm run lint:plugins:no-extension-src-imports
|
||||
run_check "lint:plugins:no-extension-test-core-imports" pnpm run lint:plugins:no-extension-test-core-imports
|
||||
run_check "lint:plugins:plugin-sdk-subpaths-exported" pnpm run lint:plugins:plugin-sdk-subpaths-exported
|
||||
run_check "web-search-provider-boundary" pnpm run lint:web-search-provider-boundaries
|
||||
run_check "web-fetch-provider-boundary" pnpm run lint:web-fetch-provider-boundaries
|
||||
run_check "extension-src-outside-plugin-sdk-boundary" pnpm run lint:extensions:no-src-outside-plugin-sdk
|
||||
run_check "extension-plugin-sdk-internal-boundary" pnpm run lint:extensions:no-plugin-sdk-internal
|
||||
run_check "extension-relative-outside-package-boundary" pnpm run lint:extensions:no-relative-outside-package
|
||||
run_check "lint:ui:no-raw-window-open" pnpm lint:ui:no-raw-window-open
|
||||
;;
|
||||
extension-surfaces)
|
||||
run_check "lint:extensions:channels" pnpm run lint:extensions:channels
|
||||
run_check "lint:extensions:bundled" pnpm run lint:extensions:bundled
|
||||
run_check "test:extensions:package-boundary" pnpm run test:extensions:package-boundary
|
||||
;;
|
||||
runtime-topology)
|
||||
if [ "$RUN_CONTROL_UI_I18N" = "true" ]; then
|
||||
run_check "ui:i18n:check" pnpm ui:i18n:check
|
||||
fi
|
||||
run_check "gateway-watch-regression" pnpm test:gateway:watch-regression
|
||||
run_check "check:import-cycles" pnpm check:import-cycles
|
||||
run_check "check:madge-import-cycles" pnpm check:madge-import-cycles
|
||||
;;
|
||||
*)
|
||||
echo "Unsupported additional check group: $ADDITIONAL_CHECK_GROUP" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
- name: Run madge import cycle guard
|
||||
id: madge_import_cycles
|
||||
continue-on-error: true
|
||||
run: pnpm check:madge-import-cycles
|
||||
exit "$failures"
|
||||
|
||||
- name: Upload gateway watch regression artifacts
|
||||
if: always()
|
||||
if: always() && matrix.group == 'runtime-topology'
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: gateway-watch-regression
|
||||
path: .local/gateway-watch-regression/
|
||||
retention-days: 7
|
||||
|
||||
- name: Fail if any additional check failed
|
||||
if: always()
|
||||
check-additional:
|
||||
permissions:
|
||||
contents: read
|
||||
name: "check-additional"
|
||||
needs: [preflight, check-additional-shard]
|
||||
if: always() && needs.preflight.outputs.run_check_additional == 'true'
|
||||
runs-on: blacksmith-16vcpu-ubuntu-2404
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Verify additional check shards
|
||||
env:
|
||||
PLUGIN_EXTENSION_BOUNDARY_OUTCOME: ${{ steps.plugin_extension_boundary.outcome }}
|
||||
NO_RANDOM_MESSAGING_OUTCOME: ${{ steps.no_random_messaging.outcome }}
|
||||
CHANNEL_AGNOSTIC_BOUNDARIES_OUTCOME: ${{ steps.channel_agnostic_boundaries.outcome }}
|
||||
NO_RAW_CHANNEL_FETCH_OUTCOME: ${{ steps.no_raw_channel_fetch.outcome }}
|
||||
INGRESS_OWNER_OUTCOME: ${{ steps.ingress_owner.outcome }}
|
||||
NO_REGISTER_HTTP_HANDLER_OUTCOME: ${{ steps.no_register_http_handler.outcome }}
|
||||
NO_MONOLITHIC_PLUGIN_SDK_ENTRY_IMPORTS_OUTCOME: ${{ steps.no_monolithic_plugin_sdk_entry_imports.outcome }}
|
||||
NO_EXTENSION_SRC_IMPORTS_OUTCOME: ${{ steps.no_extension_src_imports.outcome }}
|
||||
NO_EXTENSION_TEST_CORE_IMPORTS_OUTCOME: ${{ steps.no_extension_test_core_imports.outcome }}
|
||||
PLUGIN_SDK_SUBPATHS_EXPORTED_OUTCOME: ${{ steps.plugin_sdk_subpaths_exported.outcome }}
|
||||
WEB_SEARCH_PROVIDER_BOUNDARY_OUTCOME: ${{ steps.web_search_provider_boundary.outcome }}
|
||||
WEB_FETCH_PROVIDER_BOUNDARY_OUTCOME: ${{ steps.web_fetch_provider_boundary.outcome }}
|
||||
EXTENSION_SRC_OUTSIDE_PLUGIN_SDK_BOUNDARY_OUTCOME: ${{ steps.extension_src_outside_plugin_sdk_boundary.outcome }}
|
||||
EXTENSION_PLUGIN_SDK_INTERNAL_BOUNDARY_OUTCOME: ${{ steps.extension_plugin_sdk_internal_boundary.outcome }}
|
||||
EXTENSION_RELATIVE_OUTSIDE_PACKAGE_BOUNDARY_OUTCOME: ${{ steps.extension_relative_outside_package_boundary.outcome }}
|
||||
EXTENSION_CHANNEL_LINT_OUTCOME: ${{ steps.extension_channel_lint.outcome }}
|
||||
EXTENSION_BUNDLED_LINT_OUTCOME: ${{ steps.extension_bundled_lint.outcome }}
|
||||
EXTENSION_PACKAGE_BOUNDARY_TSC_OUTCOME: ${{ steps.extension_package_boundary_tsc.outcome }}
|
||||
NO_RAW_WINDOW_OPEN_OUTCOME: ${{ steps.no_raw_window_open.outcome }}
|
||||
CONTROL_UI_I18N_OUTCOME: ${{ steps.control_ui_i18n.outcome == 'skipped' && 'success' || steps.control_ui_i18n.outcome }}
|
||||
GATEWAY_WATCH_REGRESSION_OUTCOME: ${{ steps.gateway_watch_regression.outcome }}
|
||||
IMPORT_CYCLES_OUTCOME: ${{ steps.import_cycles.outcome }}
|
||||
MADGE_IMPORT_CYCLES_OUTCOME: ${{ steps.madge_import_cycles.outcome }}
|
||||
SHARD_RESULT: ${{ needs.check-additional-shard.result }}
|
||||
run: |
|
||||
failures=0
|
||||
for result in \
|
||||
"plugin-extension-boundary|$PLUGIN_EXTENSION_BOUNDARY_OUTCOME" \
|
||||
"lint:tmp:no-random-messaging|$NO_RANDOM_MESSAGING_OUTCOME" \
|
||||
"lint:tmp:channel-agnostic-boundaries|$CHANNEL_AGNOSTIC_BOUNDARIES_OUTCOME" \
|
||||
"lint:tmp:no-raw-channel-fetch|$NO_RAW_CHANNEL_FETCH_OUTCOME" \
|
||||
"lint:agent:ingress-owner|$INGRESS_OWNER_OUTCOME" \
|
||||
"lint:plugins:no-register-http-handler|$NO_REGISTER_HTTP_HANDLER_OUTCOME" \
|
||||
"lint:plugins:no-monolithic-plugin-sdk-entry-imports|$NO_MONOLITHIC_PLUGIN_SDK_ENTRY_IMPORTS_OUTCOME" \
|
||||
"lint:plugins:no-extension-src-imports|$NO_EXTENSION_SRC_IMPORTS_OUTCOME" \
|
||||
"lint:plugins:no-extension-test-core-imports|$NO_EXTENSION_TEST_CORE_IMPORTS_OUTCOME" \
|
||||
"lint:plugins:plugin-sdk-subpaths-exported|$PLUGIN_SDK_SUBPATHS_EXPORTED_OUTCOME" \
|
||||
"web-search-provider-boundary|$WEB_SEARCH_PROVIDER_BOUNDARY_OUTCOME" \
|
||||
"web-fetch-provider-boundary|$WEB_FETCH_PROVIDER_BOUNDARY_OUTCOME" \
|
||||
"extension-src-outside-plugin-sdk-boundary|$EXTENSION_SRC_OUTSIDE_PLUGIN_SDK_BOUNDARY_OUTCOME" \
|
||||
"extension-plugin-sdk-internal-boundary|$EXTENSION_PLUGIN_SDK_INTERNAL_BOUNDARY_OUTCOME" \
|
||||
"extension-relative-outside-package-boundary|$EXTENSION_RELATIVE_OUTSIDE_PACKAGE_BOUNDARY_OUTCOME" \
|
||||
"lint:extensions:channels|$EXTENSION_CHANNEL_LINT_OUTCOME" \
|
||||
"lint:extensions:bundled|$EXTENSION_BUNDLED_LINT_OUTCOME" \
|
||||
"test:extensions:package-boundary|$EXTENSION_PACKAGE_BOUNDARY_TSC_OUTCOME" \
|
||||
"lint:ui:no-raw-window-open|$NO_RAW_WINDOW_OPEN_OUTCOME" \
|
||||
"ui:i18n:check|$CONTROL_UI_I18N_OUTCOME" \
|
||||
"gateway-watch-regression|$GATEWAY_WATCH_REGRESSION_OUTCOME" \
|
||||
"check:import-cycles|$IMPORT_CYCLES_OUTCOME" \
|
||||
"check:madge-import-cycles|$MADGE_IMPORT_CYCLES_OUTCOME"; do
|
||||
name="${result%%|*}"
|
||||
outcome="${result#*|}"
|
||||
if [ "$outcome" != "success" ]; then
|
||||
echo "::error title=${name} failed::${name} outcome: ${outcome}"
|
||||
failures=1
|
||||
fi
|
||||
done
|
||||
|
||||
exit "$failures"
|
||||
if [ "$SHARD_RESULT" != "success" ]; then
|
||||
echo "Additional check shards failed: $SHARD_RESULT" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
build-smoke:
|
||||
permissions:
|
||||
@@ -1063,10 +1336,52 @@ jobs:
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: false
|
||||
shell: bash
|
||||
env:
|
||||
CHECKOUT_REPO: ${{ github.repository }}
|
||||
CHECKOUT_SHA: ${{ github.sha }}
|
||||
CHECKOUT_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
workdir="$GITHUB_WORKSPACE"
|
||||
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 30s git -C "$workdir" \
|
||||
-c protocol.version=2 \
|
||||
-c "http.https://github.com/.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}/2 succeeded"
|
||||
}
|
||||
|
||||
for attempt in 1 2; do
|
||||
if checkout_attempt "$attempt"; then
|
||||
exit 0
|
||||
fi
|
||||
echo "checkout attempt ${attempt}/2 failed"
|
||||
sleep $((attempt * 5))
|
||||
done
|
||||
|
||||
echo "checkout failed after 2 attempts" >&2
|
||||
exit 1
|
||||
|
||||
- name: Setup Node environment
|
||||
uses: ./.github/actions/setup-node-env
|
||||
@@ -1118,10 +1433,52 @@ jobs:
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: false
|
||||
shell: bash
|
||||
env:
|
||||
CHECKOUT_REPO: ${{ github.repository }}
|
||||
CHECKOUT_SHA: ${{ github.sha }}
|
||||
CHECKOUT_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
workdir="$GITHUB_WORKSPACE"
|
||||
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 30s git -C "$workdir" \
|
||||
-c protocol.version=2 \
|
||||
-c "http.https://github.com/.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}/2 succeeded"
|
||||
}
|
||||
|
||||
for attempt in 1 2; do
|
||||
if checkout_attempt "$attempt"; then
|
||||
exit 0
|
||||
fi
|
||||
echo "checkout attempt ${attempt}/2 failed"
|
||||
sleep $((attempt * 5))
|
||||
done
|
||||
|
||||
echo "checkout failed after 2 attempts" >&2
|
||||
exit 1
|
||||
|
||||
- name: Setup Node environment
|
||||
uses: ./.github/actions/setup-node-env
|
||||
@@ -1376,6 +1733,15 @@ jobs:
|
||||
- name: Install XcodeGen / SwiftLint / SwiftFormat
|
||||
run: brew install xcodegen swiftlint swiftformat
|
||||
|
||||
- name: Detect Swift toolchain cache key
|
||||
id: swift-toolchain
|
||||
run: |
|
||||
set -euo pipefail
|
||||
xcode_version="$(xcodebuild -version | tr '\n' ' ' | sed 's/ */ /g; s/ $//')"
|
||||
swift_version="$(swift --version | head -n 1)"
|
||||
toolchain_key="$(printf '%s\n%s\n' "$xcode_version" "$swift_version" | shasum -a 256 | awk '{print $1}')"
|
||||
echo "key=$toolchain_key" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Cache SwiftPM
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
@@ -1384,10 +1750,24 @@ jobs:
|
||||
restore-keys: |
|
||||
${{ runner.os }}-swiftpm-
|
||||
|
||||
- name: Cache Swift build directory
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: apps/macos/.build
|
||||
key: ${{ runner.os }}-swift-build-v1-${{ steps.swift-toolchain.outputs.key }}-${{ hashFiles('apps/macos/Package.swift', 'apps/macos/Package.resolved', 'apps/shared/OpenClawKit/Package.swift', 'Swabble/Package.swift') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-swift-build-v1-${{ steps.swift-toolchain.outputs.key }}-
|
||||
|
||||
- name: Patch mlx-audio-swift manifest
|
||||
run: |
|
||||
set -euo pipefail
|
||||
swift package resolve --package-path apps/macos >/dev/null
|
||||
if [ ! -f apps/macos/.build/checkouts/mlx-audio-swift/Package.swift ]; then
|
||||
swift package resolve --package-path apps/macos >/dev/null
|
||||
fi
|
||||
if [ ! -f apps/macos/.build/checkouts/mlx-audio-swift/Package.swift ]; then
|
||||
echo "mlx-audio-swift checkout missing after swift package resolve" >&2
|
||||
exit 1
|
||||
fi
|
||||
chmod u+w apps/macos/.build/checkouts/mlx-audio-swift/Package.swift
|
||||
python <<'PY'
|
||||
from pathlib import Path
|
||||
@@ -1423,7 +1803,10 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
for attempt in 1 2 3; do
|
||||
if swift build --package-path apps/macos --configuration release; then
|
||||
# The macOS lane validates the desktop app build; the CLI product is
|
||||
# intentionally left to its own narrower surfaces instead of making
|
||||
# this lane rebuild the whole package graph.
|
||||
if swift build --package-path apps/macos --product OpenClaw --configuration release; then
|
||||
exit 0
|
||||
fi
|
||||
echo "swift build failed (attempt $attempt/3). Retrying…"
|
||||
|
||||
@@ -140,7 +140,8 @@ jobs:
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
OPENCLAW_CONTROL_UI_I18N_MODEL: gpt-5.4
|
||||
OPENCLAW_CONTROL_UI_I18N_THINKING: low
|
||||
run: node --import tsx scripts/control-ui-i18n.ts sync --locale "${{ matrix.locale }}" --write
|
||||
LOCALE: ${{ matrix.locale }}
|
||||
run: node --import tsx scripts/control-ui-i18n.ts sync --locale "${LOCALE}" --write
|
||||
|
||||
- name: Commit and push locale updates
|
||||
env:
|
||||
|
||||
20
.github/workflows/docker-release.yml
vendored
20
.github/workflows/docker-release.yml
vendored
@@ -362,28 +362,36 @@ jobs:
|
||||
|
||||
- name: Create and push default manifest
|
||||
shell: bash
|
||||
env:
|
||||
TAGS: ${{ steps.tags.outputs.value }}
|
||||
AMD64_DIGEST: ${{ needs.build-amd64.outputs.digest }}
|
||||
ARM64_DIGEST: ${{ needs.build-arm64.outputs.digest }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mapfile -t tags <<< "${{ steps.tags.outputs.value }}"
|
||||
mapfile -t tags <<< "${TAGS}"
|
||||
args=()
|
||||
for tag in "${tags[@]}"; do
|
||||
[ -z "$tag" ] && continue
|
||||
args+=("-t" "$tag")
|
||||
done
|
||||
docker buildx imagetools create "${args[@]}" \
|
||||
${{ needs.build-amd64.outputs.digest }} \
|
||||
${{ needs.build-arm64.outputs.digest }}
|
||||
"${AMD64_DIGEST}" \
|
||||
"${ARM64_DIGEST}"
|
||||
|
||||
- name: Create and push slim manifest
|
||||
shell: bash
|
||||
env:
|
||||
SLIM_TAGS: ${{ steps.tags.outputs.slim }}
|
||||
AMD64_SLIM_DIGEST: ${{ needs.build-amd64.outputs.slim-digest }}
|
||||
ARM64_SLIM_DIGEST: ${{ needs.build-arm64.outputs.slim-digest }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mapfile -t tags <<< "${{ steps.tags.outputs.slim }}"
|
||||
mapfile -t tags <<< "${SLIM_TAGS}"
|
||||
args=()
|
||||
for tag in "${tags[@]}"; do
|
||||
[ -z "$tag" ] && continue
|
||||
args+=("-t" "$tag")
|
||||
done
|
||||
docker buildx imagetools create "${args[@]}" \
|
||||
${{ needs.build-amd64.outputs.slim-digest }} \
|
||||
${{ needs.build-arm64.outputs.slim-digest }}
|
||||
"${AMD64_SLIM_DIGEST}" \
|
||||
"${ARM64_SLIM_DIGEST}"
|
||||
|
||||
@@ -144,6 +144,7 @@ on:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||
@@ -151,7 +152,63 @@ env:
|
||||
PNPM_VERSION: "10.32.1"
|
||||
|
||||
jobs:
|
||||
validate_selected_ref:
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
outputs:
|
||||
selected_sha: ${{ steps.validate.outputs.selected_sha }}
|
||||
trusted_reason: ${{ steps.validate.outputs.trusted_reason }}
|
||||
steps:
|
||||
- name: Checkout selected ref
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ inputs.ref }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Validate selected ref
|
||||
id: validate
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
INPUT_REF: ${{ inputs.ref }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
selected_sha="$(git rev-parse HEAD)"
|
||||
trusted_reason=""
|
||||
|
||||
git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main
|
||||
|
||||
if git merge-base --is-ancestor "$selected_sha" refs/remotes/origin/main; then
|
||||
trusted_reason="main-ancestor"
|
||||
elif git tag --points-at "$selected_sha" | grep -Eq '^v'; then
|
||||
trusted_reason="release-tag"
|
||||
else
|
||||
pr_head_count="$(
|
||||
gh api \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
"repos/${GITHUB_REPOSITORY}/commits/${selected_sha}/pulls" \
|
||||
--jq '[.[] | select(.state == "open" and .head.repo.full_name == "'"${GITHUB_REPOSITORY}"'" and .head.sha == "'"${selected_sha}"'")] | length'
|
||||
)"
|
||||
if [[ "$pr_head_count" != "0" ]]; then
|
||||
trusted_reason="open-pr-head"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -z "$trusted_reason" ]]; then
|
||||
echo "Ref '${INPUT_REF}' resolved to $selected_sha, which is not trusted for secret-bearing live/E2E checks." >&2
|
||||
echo "Allowed refs must be on main, point to a release tag, or match an open PR head in ${GITHUB_REPOSITORY}." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "selected_sha=$selected_sha" >> "$GITHUB_OUTPUT"
|
||||
echo "trusted_reason=$trusted_reason" >> "$GITHUB_OUTPUT"
|
||||
{
|
||||
echo "Validated ref: \`${INPUT_REF}\`"
|
||||
echo "Resolved SHA: \`$selected_sha\`"
|
||||
echo "Trust reason: \`$trusted_reason\`"
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
validate_release_live_cache:
|
||||
needs: validate_selected_ref
|
||||
if: inputs.include_live_suites
|
||||
runs-on: blacksmith-32vcpu-ubuntu-2404
|
||||
timeout-minutes: 60
|
||||
@@ -164,7 +221,7 @@ jobs:
|
||||
- name: Checkout selected ref
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ inputs.ref }}
|
||||
ref: ${{ needs.validate_selected_ref.outputs.selected_sha }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Node environment
|
||||
@@ -191,6 +248,7 @@ jobs:
|
||||
run: pnpm test:live:cache
|
||||
|
||||
validate_repo_e2e:
|
||||
needs: validate_selected_ref
|
||||
if: inputs.include_repo_e2e
|
||||
runs-on: blacksmith-32vcpu-ubuntu-2404
|
||||
timeout-minutes: 90
|
||||
@@ -200,7 +258,7 @@ jobs:
|
||||
- name: Checkout selected ref
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ inputs.ref }}
|
||||
ref: ${{ needs.validate_selected_ref.outputs.selected_sha }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Node environment
|
||||
@@ -218,6 +276,7 @@ jobs:
|
||||
run: pnpm test:e2e
|
||||
|
||||
validate_special_e2e:
|
||||
needs: validate_selected_ref
|
||||
if: inputs.include_repo_e2e || inputs.include_live_suites
|
||||
runs-on: blacksmith-32vcpu-ubuntu-2404
|
||||
timeout-minutes: ${{ matrix.timeout_minutes }}
|
||||
@@ -245,7 +304,7 @@ jobs:
|
||||
- name: Checkout selected ref
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ inputs.ref }}
|
||||
ref: ${{ needs.validate_selected_ref.outputs.selected_sha }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Node environment
|
||||
@@ -293,6 +352,7 @@ jobs:
|
||||
run: ${{ matrix.command }}
|
||||
|
||||
validate_docker_e2e:
|
||||
needs: validate_selected_ref
|
||||
if: inputs.include_release_path_suites || inputs.include_openwebui
|
||||
runs-on: blacksmith-32vcpu-ubuntu-2404
|
||||
timeout-minutes: ${{ matrix.timeout_minutes }}
|
||||
@@ -396,7 +456,7 @@ jobs:
|
||||
- name: Checkout selected ref
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ inputs.ref }}
|
||||
ref: ${{ needs.validate_selected_ref.outputs.selected_sha }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Node environment
|
||||
@@ -450,6 +510,7 @@ jobs:
|
||||
run: ${{ matrix.command }}
|
||||
|
||||
validate_live_provider_suites:
|
||||
needs: validate_selected_ref
|
||||
if: inputs.include_live_suites
|
||||
runs-on: blacksmith-32vcpu-ubuntu-2404
|
||||
timeout-minutes: ${{ matrix.timeout_minutes }}
|
||||
@@ -538,7 +599,7 @@ jobs:
|
||||
- name: Checkout selected ref
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ inputs.ref }}
|
||||
ref: ${{ needs.validate_selected_ref.outputs.selected_sha }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Node environment
|
||||
@@ -562,9 +623,39 @@ jobs:
|
||||
case "${{ matrix.suite_id }}" in
|
||||
live-cli-backend-docker)
|
||||
echo "OPENCLAW_LIVE_CLI_BACKEND_MODEL=codex-cli/gpt-5.4" >> "$GITHUB_ENV"
|
||||
# The CLI backend Docker lane should exercise the same staged
|
||||
# Codex auth path Peter uses locally so MCP cron creation and
|
||||
# multimodal probes stay covered in CI. Replace the staged
|
||||
# config.toml with a minimal CI-safe config so the repo stays
|
||||
# trusted for MCP/tool use without inheriting maintainer-local
|
||||
# provider/profile overrides that do not exist inside CI.
|
||||
# Codex's workspace-write sandbox relies on user namespaces that
|
||||
# this Docker lane does not provide, so run Codex unsandboxed
|
||||
# inside the already-isolated container to keep MCP cron/tool
|
||||
# execution representative instead of failing on nested sandbox
|
||||
# setup.
|
||||
echo 'OPENCLAW_LIVE_CLI_BACKEND_CLEAR_ENV=["OPENAI_API_KEY","OPENAI_BASE_URL"]' >> "$GITHUB_ENV"
|
||||
echo 'OPENCLAW_LIVE_CLI_BACKEND_ARGS=["exec","--json","--color","never","--sandbox","danger-full-access","--skip-git-repo-check"]' >> "$GITHUB_ENV"
|
||||
echo 'OPENCLAW_LIVE_CLI_BACKEND_RESUME_ARGS=["exec","resume","{sessionId}","-c","sandbox_mode=\"danger-full-access\"","--skip-git-repo-check"]' >> "$GITHUB_ENV"
|
||||
echo "OPENCLAW_LIVE_CLI_BACKEND_DEBUG=1" >> "$GITHUB_ENV"
|
||||
echo "OPENCLAW_CLI_BACKEND_LOG_OUTPUT=1" >> "$GITHUB_ENV"
|
||||
echo "OPENCLAW_LIVE_CLI_BACKEND_USE_CI_SAFE_CODEX_CONFIG=1" >> "$GITHUB_ENV"
|
||||
;;
|
||||
live-codex-harness-docker)
|
||||
# Keep CI on the API-key path for now. The staged Codex auth secret
|
||||
# is currently stale, but the wrapper still supports codex-auth for
|
||||
# local maintainer reruns without changing Peter's flow.
|
||||
echo "OPENCLAW_LIVE_CODEX_HARNESS_AUTH=api-key" >> "$GITHUB_ENV"
|
||||
;;
|
||||
live-acp-bind-docker)
|
||||
echo "OPENCLAW_LIVE_ACP_BIND_AGENTS=claude,codex,gemini" >> "$GITHUB_ENV"
|
||||
if [[ -n "${GEMINI_API_KEY:-}" || -n "${GOOGLE_API_KEY:-}" ]]; then
|
||||
echo "OPENCLAW_LIVE_ACP_BIND_AGENTS=claude,codex,gemini" >> "$GITHUB_ENV"
|
||||
else
|
||||
# The hydrated Gemini settings file only selects Gemini CLI auth
|
||||
# mode. CI still needs a usable Gemini or Google API key before
|
||||
# ACP bind can initialize a Gemini session.
|
||||
echo "OPENCLAW_LIVE_ACP_BIND_AGENTS=claude,codex" >> "$GITHUB_ENV"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
|
||||
3
.github/workflows/openclaw-npm-release.yml
vendored
3
.github/workflows/openclaw-npm-release.yml
vendored
@@ -397,9 +397,10 @@ jobs:
|
||||
env:
|
||||
OPENCLAW_PREPACK_PREPARED: "1"
|
||||
OPENCLAW_NPM_PUBLISH_TAG: ${{ inputs.npm_dist_tag }}
|
||||
PUBLISH_TARBALL_PATH: ${{ steps.publish_tarball.outputs.path }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
publish_target="${{ steps.publish_tarball.outputs.path }}"
|
||||
publish_target="${PUBLISH_TARBALL_PATH}"
|
||||
if [[ -n "${publish_target}" ]]; then
|
||||
publish_target="./${publish_target}"
|
||||
fi
|
||||
|
||||
54
.github/workflows/openclaw-release-checks.yml
vendored
54
.github/workflows/openclaw-release-checks.yml
vendored
@@ -130,12 +130,19 @@ jobs:
|
||||
ref: ${{ needs.resolve_target.outputs.ref }}
|
||||
provider: ${{ needs.resolve_target.outputs.provider }}
|
||||
mode: ${{ needs.resolve_target.outputs.mode }}
|
||||
secrets: inherit
|
||||
secrets:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
MINIMAX_API_KEY: ${{ secrets.MINIMAX_API_KEY }}
|
||||
OPENCLAW_DISCORD_SMOKE_BOT_TOKEN: ${{ secrets.OPENCLAW_DISCORD_SMOKE_BOT_TOKEN }}
|
||||
OPENCLAW_DISCORD_SMOKE_GUILD_ID: ${{ secrets.OPENCLAW_DISCORD_SMOKE_GUILD_ID }}
|
||||
OPENCLAW_DISCORD_SMOKE_CHANNEL_ID: ${{ secrets.OPENCLAW_DISCORD_SMOKE_CHANNEL_ID }}
|
||||
|
||||
live_and_e2e_release_checks:
|
||||
needs: [resolve_target]
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
uses: ./.github/workflows/openclaw-live-and-e2e-checks-reusable.yml
|
||||
with:
|
||||
ref: ${{ needs.resolve_target.outputs.ref }}
|
||||
@@ -143,4 +150,47 @@ jobs:
|
||||
include_release_path_suites: true
|
||||
include_openwebui: true
|
||||
include_live_suites: true
|
||||
secrets: inherit
|
||||
secrets:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
ANTHROPIC_API_KEY_OLD: ${{ secrets.ANTHROPIC_API_KEY_OLD }}
|
||||
ANTHROPIC_API_TOKEN: ${{ secrets.ANTHROPIC_API_TOKEN }}
|
||||
BYTEPLUS_API_KEY: ${{ secrets.BYTEPLUS_API_KEY }}
|
||||
CEREBRAS_API_KEY: ${{ secrets.CEREBRAS_API_KEY }}
|
||||
DASHSCOPE_API_KEY: ${{ secrets.DASHSCOPE_API_KEY }}
|
||||
GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
|
||||
KIMI_API_KEY: ${{ secrets.KIMI_API_KEY }}
|
||||
MODELSTUDIO_API_KEY: ${{ secrets.MODELSTUDIO_API_KEY }}
|
||||
MOONSHOT_API_KEY: ${{ secrets.MOONSHOT_API_KEY }}
|
||||
MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }}
|
||||
MINIMAX_API_KEY: ${{ secrets.MINIMAX_API_KEY }}
|
||||
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
||||
OPENCODE_ZEN_API_KEY: ${{ secrets.OPENCODE_ZEN_API_KEY }}
|
||||
OPENCLAW_LIVE_BROWSER_CDP_URL: ${{ secrets.OPENCLAW_LIVE_BROWSER_CDP_URL }}
|
||||
OPENCLAW_LIVE_SETUP_TOKEN: ${{ secrets.OPENCLAW_LIVE_SETUP_TOKEN }}
|
||||
OPENCLAW_LIVE_SETUP_TOKEN_MODEL: ${{ secrets.OPENCLAW_LIVE_SETUP_TOKEN_MODEL }}
|
||||
OPENCLAW_LIVE_SETUP_TOKEN_PROFILE: ${{ secrets.OPENCLAW_LIVE_SETUP_TOKEN_PROFILE }}
|
||||
OPENCLAW_LIVE_SETUP_TOKEN_VALUE: ${{ secrets.OPENCLAW_LIVE_SETUP_TOKEN_VALUE }}
|
||||
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
|
||||
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
|
||||
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
|
||||
QWEN_API_KEY: ${{ secrets.QWEN_API_KEY }}
|
||||
FAL_KEY: ${{ secrets.FAL_KEY }}
|
||||
RUNWAY_API_KEY: ${{ secrets.RUNWAY_API_KEY }}
|
||||
DEEPGRAM_API_KEY: ${{ secrets.DEEPGRAM_API_KEY }}
|
||||
TOGETHER_API_KEY: ${{ secrets.TOGETHER_API_KEY }}
|
||||
VYDRA_API_KEY: ${{ secrets.VYDRA_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 }}
|
||||
BYTEPLUS_ACCESS_KEY_ID: ${{ secrets.BYTEPLUS_ACCESS_KEY_ID }}
|
||||
BYTEPLUS_SECRET_ACCESS_KEY: ${{ secrets.BYTEPLUS_SECRET_ACCESS_KEY }}
|
||||
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
OPENCLAW_CODEX_AUTH_JSON: ${{ secrets.OPENCLAW_CODEX_AUTH_JSON }}
|
||||
OPENCLAW_CODEX_CONFIG_TOML: ${{ secrets.OPENCLAW_CODEX_CONFIG_TOML }}
|
||||
OPENCLAW_CLAUDE_JSON: ${{ secrets.OPENCLAW_CLAUDE_JSON }}
|
||||
OPENCLAW_CLAUDE_CREDENTIALS_JSON: ${{ secrets.OPENCLAW_CLAUDE_CREDENTIALS_JSON }}
|
||||
OPENCLAW_CLAUDE_SETTINGS_JSON: ${{ secrets.OPENCLAW_CLAUDE_SETTINGS_JSON }}
|
||||
OPENCLAW_CLAUDE_SETTINGS_LOCAL_JSON: ${{ secrets.OPENCLAW_CLAUDE_SETTINGS_LOCAL_JSON }}
|
||||
OPENCLAW_GEMINI_SETTINGS_JSON: ${{ secrets.OPENCLAW_GEMINI_SETTINGS_JSON }}
|
||||
|
||||
@@ -7,6 +7,7 @@ on:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
|
||||
concurrency:
|
||||
group: openclaw-scheduled-live-checks-${{ github.ref }}
|
||||
@@ -19,6 +20,7 @@ jobs:
|
||||
live_and_openwebui_checks:
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
uses: ./.github/workflows/openclaw-live-and-e2e-checks-reusable.yml
|
||||
with:
|
||||
ref: ${{ github.sha }}
|
||||
@@ -26,4 +28,47 @@ jobs:
|
||||
include_release_path_suites: false
|
||||
include_openwebui: true
|
||||
include_live_suites: true
|
||||
secrets: inherit
|
||||
secrets:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
ANTHROPIC_API_KEY_OLD: ${{ secrets.ANTHROPIC_API_KEY_OLD }}
|
||||
ANTHROPIC_API_TOKEN: ${{ secrets.ANTHROPIC_API_TOKEN }}
|
||||
BYTEPLUS_API_KEY: ${{ secrets.BYTEPLUS_API_KEY }}
|
||||
CEREBRAS_API_KEY: ${{ secrets.CEREBRAS_API_KEY }}
|
||||
DASHSCOPE_API_KEY: ${{ secrets.DASHSCOPE_API_KEY }}
|
||||
GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
|
||||
KIMI_API_KEY: ${{ secrets.KIMI_API_KEY }}
|
||||
MODELSTUDIO_API_KEY: ${{ secrets.MODELSTUDIO_API_KEY }}
|
||||
MOONSHOT_API_KEY: ${{ secrets.MOONSHOT_API_KEY }}
|
||||
MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }}
|
||||
MINIMAX_API_KEY: ${{ secrets.MINIMAX_API_KEY }}
|
||||
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
||||
OPENCODE_ZEN_API_KEY: ${{ secrets.OPENCODE_ZEN_API_KEY }}
|
||||
OPENCLAW_LIVE_BROWSER_CDP_URL: ${{ secrets.OPENCLAW_LIVE_BROWSER_CDP_URL }}
|
||||
OPENCLAW_LIVE_SETUP_TOKEN: ${{ secrets.OPENCLAW_LIVE_SETUP_TOKEN }}
|
||||
OPENCLAW_LIVE_SETUP_TOKEN_MODEL: ${{ secrets.OPENCLAW_LIVE_SETUP_TOKEN_MODEL }}
|
||||
OPENCLAW_LIVE_SETUP_TOKEN_PROFILE: ${{ secrets.OPENCLAW_LIVE_SETUP_TOKEN_PROFILE }}
|
||||
OPENCLAW_LIVE_SETUP_TOKEN_VALUE: ${{ secrets.OPENCLAW_LIVE_SETUP_TOKEN_VALUE }}
|
||||
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
|
||||
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
|
||||
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
|
||||
QWEN_API_KEY: ${{ secrets.QWEN_API_KEY }}
|
||||
FAL_KEY: ${{ secrets.FAL_KEY }}
|
||||
RUNWAY_API_KEY: ${{ secrets.RUNWAY_API_KEY }}
|
||||
DEEPGRAM_API_KEY: ${{ secrets.DEEPGRAM_API_KEY }}
|
||||
TOGETHER_API_KEY: ${{ secrets.TOGETHER_API_KEY }}
|
||||
VYDRA_API_KEY: ${{ secrets.VYDRA_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 }}
|
||||
BYTEPLUS_ACCESS_KEY_ID: ${{ secrets.BYTEPLUS_ACCESS_KEY_ID }}
|
||||
BYTEPLUS_SECRET_ACCESS_KEY: ${{ secrets.BYTEPLUS_SECRET_ACCESS_KEY }}
|
||||
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
OPENCLAW_CODEX_AUTH_JSON: ${{ secrets.OPENCLAW_CODEX_AUTH_JSON }}
|
||||
OPENCLAW_CODEX_CONFIG_TOML: ${{ secrets.OPENCLAW_CODEX_CONFIG_TOML }}
|
||||
OPENCLAW_CLAUDE_JSON: ${{ secrets.OPENCLAW_CLAUDE_JSON }}
|
||||
OPENCLAW_CLAUDE_CREDENTIALS_JSON: ${{ secrets.OPENCLAW_CLAUDE_CREDENTIALS_JSON }}
|
||||
OPENCLAW_CLAUDE_SETTINGS_JSON: ${{ secrets.OPENCLAW_CLAUDE_SETTINGS_JSON }}
|
||||
OPENCLAW_CLAUDE_SETTINGS_LOCAL_JSON: ${{ secrets.OPENCLAW_CLAUDE_SETTINGS_LOCAL_JSON }}
|
||||
OPENCLAW_GEMINI_SETTINGS_JSON: ${{ secrets.OPENCLAW_GEMINI_SETTINGS_JSON }}
|
||||
|
||||
@@ -9,20 +9,40 @@
|
||||
"rules": {
|
||||
"curly": "error",
|
||||
"eslint-plugin-unicorn/prefer-array-find": "error",
|
||||
"eslint/no-array-constructor": "error",
|
||||
"eslint/no-await-in-loop": "off",
|
||||
"eslint/no-new": "error",
|
||||
"eslint/no-object-constructor": "error",
|
||||
"eslint/no-return-assign": "error",
|
||||
"eslint/no-shadow": "off",
|
||||
"eslint/no-useless-call": "error",
|
||||
"eslint/no-useless-computed-key": "error",
|
||||
"eslint/no-useless-concat": "error",
|
||||
"eslint/no-useless-constructor": "error",
|
||||
"eslint/no-warning-comments": "error",
|
||||
"eslint/no-unmodified-loop-condition": "error",
|
||||
"eslint-plugin-unicorn/prefer-set-size": "error",
|
||||
"oxc/no-accumulating-spread": "error",
|
||||
"oxc/no-async-endpoint-handlers": "off",
|
||||
"oxc/no-map-spread": "off",
|
||||
"oxc/no-async-endpoint-handlers": "error",
|
||||
"oxc/no-map-spread": "error",
|
||||
"typescript/consistent-return": "error",
|
||||
"typescript/no-explicit-any": "error",
|
||||
"typescript/no-extraneous-class": "error",
|
||||
"typescript/no-meaningless-void-operator": "error",
|
||||
"typescript/no-unnecessary-type-assertion": "error",
|
||||
"typescript/no-unnecessary-type-arguments": "error",
|
||||
"typescript/no-unnecessary-type-constraint": "error",
|
||||
"typescript/no-unnecessary-type-conversion": "error",
|
||||
"typescript/no-unnecessary-type-parameters": "error",
|
||||
"typescript/no-unsafe-type-assertion": "off",
|
||||
"typescript/no-useless-default-assignment": "error",
|
||||
"typescript/prefer-ts-expect-error": "error",
|
||||
"unicorn/consistent-function-scoping": "off",
|
||||
"unicorn/no-unnecessary-array-flat-depth": "error",
|
||||
"unicorn/no-unnecessary-array-splice-count": "error",
|
||||
"unicorn/no-unnecessary-slice-end": "error",
|
||||
"unicorn/no-useless-promise-resolve-reject": "error",
|
||||
"unicorn/prefer-date-now": "error",
|
||||
"unicorn/prefer-set-size": "error",
|
||||
"unicorn/require-post-message-target-origin": "error"
|
||||
},
|
||||
@@ -47,6 +67,13 @@
|
||||
"**/node_modules/**"
|
||||
],
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["src/security/**"],
|
||||
"rules": {
|
||||
"eslint/no-warning-comments": "off",
|
||||
"oxc/no-map-spread": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": [
|
||||
"**/*.test.ts",
|
||||
|
||||
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -17,5 +17,6 @@
|
||||
"typescript.preferences.importModuleSpecifierEnding": "js",
|
||||
"typescript.reportStyleChecksAsWarnings": false,
|
||||
"typescript.updateImportsOnFileMove.enabled": "always",
|
||||
"typescript.tsdk": "node_modules/typescript/lib"
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"makefile.configureOnOpen": false
|
||||
}
|
||||
|
||||
49
AGENTS.md
49
AGENTS.md
@@ -1,12 +1,12 @@
|
||||
# Repository Guidelines
|
||||
|
||||
- Repo: https://github.com/openclaw/openclaw
|
||||
- In chat replies, file references must be repo-root relative only (example: `src/telegram/index.ts:80`); never absolute paths or `~/...`.
|
||||
- In chat replies, file references must be repo-root relative only (example: `extensions/telegram/src/index.ts:80`); never absolute paths or `~/...`.
|
||||
- Do not edit files covered by security-focused `CODEOWNERS` rules unless a listed owner explicitly asked for the change or is already reviewing it with you. Treat those paths as restricted surfaces, not drive-by cleanup.
|
||||
|
||||
## Project Structure & Module Organization
|
||||
|
||||
- Source code: `src/` (CLI wiring in `src/cli`, commands in `src/commands`, web provider in `src/provider-web.ts`, infra in `src/infra`, media pipeline in `src/media`).
|
||||
- Source code: `src/` (CLI wiring in `src/cli`, commands in `src/commands`, infra in `src/infra`, media pipeline in `src/media`, web provider helpers in `src/web` and `src/plugins/web-*provider*.ts`).
|
||||
- Tests: colocated `*.test.ts`.
|
||||
- Docs: `docs/` (images, queue, Pi config). Built output lives in `dist/`.
|
||||
- Nomenclature: use "plugin" / "plugins" in docs, UI, changelogs, and contributor guidance. The bundled workspace plugin tree remains the internal package layout to avoid repo-wide churn from a rename.
|
||||
@@ -17,8 +17,8 @@
|
||||
- Installers served from `https://openclaw.ai/*`: live in the sibling repo `../openclaw.ai` (`public/install.sh`, `public/install-cli.sh`, `public/install.ps1`).
|
||||
- Messaging channels: always consider **all** built-in + extension channels when refactoring shared logic (routing, allowlists, pairing, command gating, onboarding, docs).
|
||||
- Core channel docs: `docs/channels/`
|
||||
- Core channel code: `src/telegram`, `src/discord`, `src/slack`, `src/signal`, `src/imessage`, `src/web` (WhatsApp web), `src/channels`, `src/routing`
|
||||
- Bundled plugin channels: the workspace plugin tree (for example Matrix, Zalo, ZaloUser, Voice Call)
|
||||
- Core channel code: `src/channels`, `src/routing`, `src/web`
|
||||
- Bundled plugin channels: `extensions/<channel>/` (for example Discord, Telegram, Slack, Matrix, Zalo, ZaloUser, Voice Call)
|
||||
- When adding channels/plugins/apps/docs, update `.github/labeler.yml` and create matching GitHub labels (use existing channel/plugin label colors).
|
||||
|
||||
## Architecture Boundaries
|
||||
@@ -73,7 +73,7 @@
|
||||
- `hooks.internal.entries` is the canonical public hook config model. `hooks.internal.handlers` is compatibility-only input and must not be re-exposed in public schema/help/baseline surfaces.
|
||||
- Bundled plugin contract boundary:
|
||||
- Public docs: `docs/plugins/architecture.md`, `docs/plugins/manifest.md`, `docs/plugins/sdk-overview.md`
|
||||
- Definition files: `src/plugins/contracts/registry.ts`, `src/plugins/types.ts`, `src/plugins/public-artifacts.ts`
|
||||
- Definition files: `src/plugins/contracts/registry.ts`, `src/plugins/types.ts`, `src/plugins/public-surface-loader.ts`, `src/plugins/public-surface-runtime.ts`, `src/plugins/provider-public-artifacts.ts`, `src/plugins/web-provider-public-artifacts.ts`
|
||||
- Rule: keep manifest metadata, runtime registration, public SDK exports, and contract tests aligned. Do not create a hidden path around the declared plugin interfaces.
|
||||
- Extension test boundary:
|
||||
- Keep extension-owned onboarding/config/provider coverage under the owning bundled plugin package when feasible.
|
||||
@@ -87,6 +87,8 @@
|
||||
- `src/plugin-sdk/AGENTS.md` expands public SDK contract rules.
|
||||
- `src/plugins/AGENTS.md` expands plugin loading, registry, and manifest rules.
|
||||
- `src/gateway/protocol/AGENTS.md` expands typed Gateway protocol rules.
|
||||
- `src/gateway/AGENTS.md` expands Gateway server hot-path and plugin artifact rules.
|
||||
- `src/agents/AGENTS.md` expands agent test/import performance rules.
|
||||
- `test/helpers/AGENTS.md` and `test/helpers/channels/AGENTS.md` expand shared test helper boundary rules.
|
||||
- Plugin architecture direction:
|
||||
- Keep a manifest-first control plane: discovery, validation, enablement, setup hints, and activation planning should stay metadata-driven by default.
|
||||
@@ -117,19 +119,28 @@
|
||||
- Runtime baseline: Node **22+** (keep Node + Bun paths working).
|
||||
- Install deps: `pnpm install`
|
||||
- If deps are missing (for example `node_modules` missing, `vitest not found`, or `command not found`), run the repo’s package-manager install command (prefer lockfile/README-defined PM), then rerun the exact requested command once. Apply this to test/build/lint/typecheck/dev commands; if retry still fails, report the command and first actionable error.
|
||||
- Pre-commit hooks: `prek install`. The hook runs the repo verification flow, including `pnpm check`.
|
||||
- `FAST_COMMIT=1` skips the repo-wide `pnpm format` and `pnpm check` inside the pre-commit hook only. Use it when you intentionally want a faster commit path and are running equivalent targeted verification manually. It does not change CI and does not change what `pnpm check` itself does.
|
||||
- Pre-commit hooks are installed by the package `prepare` script (`git config core.hooksPath git-hooks`). The hook formats/lints staged source files and runs `pnpm check` unless the staged change is docs-only or `FAST_COMMIT=1` is set.
|
||||
- `FAST_COMMIT=1` skips the repo-wide `pnpm check` inside the pre-commit hook only. The hook still runs targeted formatting/linting for staged files and restages formatter changes. Use it when you intentionally want a faster commit path and are running equivalent targeted verification manually. It does not change CI and does not change what `pnpm check` itself does.
|
||||
- Also supported: `bun install` (keep `pnpm-lock.yaml` + Bun patching in sync when touching deps/patches).
|
||||
- Prefer Bun for TypeScript execution (scripts, dev, tests): `bun <file.ts>` / `bunx <tool>`.
|
||||
- Run CLI in dev: `pnpm openclaw ...` (bun) or `pnpm dev`.
|
||||
- Node remains supported for running built output (`dist/*`) and production installs.
|
||||
- Mac packaging (dev): `scripts/package-mac-app.sh` defaults to current arch.
|
||||
- Type-check/build: `pnpm build`
|
||||
- TypeScript checks: `pnpm tsgo`
|
||||
- TypeScript checks are split by architecture boundary, with four normal lanes:
|
||||
- `pnpm tsgo` / `pnpm tsgo:core`: core production roots (`src/`, `ui/`, `packages/`; no `extensions/` include roots).
|
||||
- `pnpm tsgo:core:test`: core colocated tests.
|
||||
- `pnpm tsgo:extensions`: bundled extension production graph.
|
||||
- `pnpm tsgo:extensions:test`: bundled extension colocated tests.
|
||||
- `pnpm tsgo:all`: every TypeScript graph above; this is what `pnpm check` runs.
|
||||
- `pnpm tsgo:profile [core-test|extensions-test|--all]`: profile fresh graph cost into `.artifacts/tsgo-profile/`. Diagnostic-only profile slices (`core-test-agents`, `core-test-non-agents`) exist for investigating agent graph cost; do not treat them as normal user-facing checks.
|
||||
- Narrow aliases remain for local loops: `pnpm tsgo:test:src`, `pnpm tsgo:test:ui`, `pnpm tsgo:test:packages`.
|
||||
- Do not add `tsc --noEmit`, `typecheck`, or `check:types` lanes for repo type checking. Use `tsgo` graphs. `tsc` is allowed only when emitting declaration/package-boundary compatibility artifacts that `tsgo` does not replace.
|
||||
- Boundary rule: core must not know extension implementation details. Extensions hook into core through manifests, registries, capabilities, and public `openclaw/plugin-sdk/*` contracts. If you find core production code naming a specific extension, or a core test that is really testing extension-owned behavior, call it out and prefer moving coverage/logic to the owning extension or a generic contract test.
|
||||
- Lint/format: `pnpm check`
|
||||
- Local agent/dev shells default to host-aware `OPENCLAW_LOCAL_CHECK=1` behavior for `pnpm tsgo` and `pnpm lint`; set `OPENCLAW_LOCAL_CHECK_MODE=throttled` to force the lower-memory profile, `OPENCLAW_LOCAL_CHECK_MODE=full` to keep lock-only behavior, or `OPENCLAW_LOCAL_CHECK=0` in CI/shared runs.
|
||||
- Format check: `pnpm format` (oxfmt --check)
|
||||
- Format fix: `pnpm format:fix` (oxfmt --write)
|
||||
- Format check: `pnpm format:check` (oxfmt --check)
|
||||
- Format fix: `pnpm format` or `pnpm format:fix` (oxfmt --write)
|
||||
- Terminology:
|
||||
- "gate" means a verification command or command set that must be green for the decision you are making.
|
||||
- A local dev gate is the fast default loop, usually `pnpm check` plus any scoped test you actually need.
|
||||
@@ -137,14 +148,14 @@
|
||||
- A CI gate is whatever the relevant workflow enforces for that lane (for example `check`, `check-additional`, `build-smoke`, or release validation).
|
||||
- Local dev gate: prefer `pnpm check` for the normal edit loop. It keeps the repo-architecture policy guards out of the default local loop.
|
||||
- CI architecture gate: `check-additional` enforces architecture and boundary policy guards that are intentionally kept out of the default local loop.
|
||||
- Formatting gate: the pre-commit hook runs `pnpm format` before `pnpm check`. If you want a formatting-only preflight locally, run `pnpm format` explicitly.
|
||||
- If you need a fast commit loop, `FAST_COMMIT=1 git commit ...` skips the hook’s repo-wide `pnpm format` and `pnpm check`; use that only when you are deliberately covering the touched surface some other way.
|
||||
- Formatting gate: the pre-commit hook runs targeted formatting on staged source files before `pnpm check`. If you want a repo-wide formatting-only preflight locally, run `pnpm format:check` explicitly.
|
||||
- If you need a fast commit loop, `FAST_COMMIT=1 git commit ...` skips the hook’s repo-wide `pnpm check`; targeted formatting/linting still runs, so use that only when you are deliberately covering the touched surface some other way.
|
||||
- Tests: `pnpm test` (vitest); coverage: `pnpm test:coverage`
|
||||
- Generated baseline drift detection uses SHA-256 hash files under `docs/.generated/` (`.sha256` files tracked in git; full JSON baselines are gitignored, generated locally for inspection).
|
||||
- Config schema drift uses `pnpm config:docs:gen` / `pnpm config:docs:check`.
|
||||
- Plugin SDK API drift uses `pnpm plugin-sdk:api:gen` / `pnpm plugin-sdk:api:check`.
|
||||
- If you change config schema/help or the public Plugin SDK surface, run the matching gen command and commit the updated `.sha256` hash file. Keep the two drift-check flows adjacent in scripts/workflows/docs guidance rather than inventing a third pattern.
|
||||
- When `pnpm tsgo` fails, triage by coherent surface instead of by raw error count: rerun the gate, group failures by package/module/type contract, open the source-of-truth type or export file first, fix the root mismatch, then rerun `pnpm tsgo` before widening into downstream consumers. Check `origin/main` before doing broad cleanup because some apparent type debt is already fixed upstream.
|
||||
- When a `tsgo` graph fails, triage by coherent surface instead of by raw error count: rerun the failing graph, group failures by package/module/type contract, open the source-of-truth type or export file first, fix the root mismatch, then rerun the failing graph before widening into downstream consumers. Check `origin/main` before doing broad cleanup because some apparent type debt is already fixed upstream.
|
||||
- For narrowly scoped changes, prefer narrowly scoped tests that directly validate the touched behavior. If no meaningful scoped test exists, say so explicitly and use the next most direct validation available.
|
||||
- Verification modes for work on `main`:
|
||||
- Default mode: `main` is relatively stable. Count pre-commit hook coverage when it already verified the current tree, avoid rerunning the exact same checks just for ceremony, and prefer keeping CI/main green before landing.
|
||||
@@ -215,6 +226,8 @@
|
||||
- Test performance guardrail: when production code already accepts `deps`, callbacks, or runtime injection, use that seam in tests before adding module-level mocks.
|
||||
- Test performance guardrail: prefer narrow public SDK subpaths such as `models-provider-runtime`, `skill-commands-runtime`, and `reply-dispatch-runtime` over older broad helper barrels when both expose the needed helper.
|
||||
- Test performance guardrail: treat import-dominated test time as a boundary bug. Refactor the import surface before adding more cases to the slow file.
|
||||
- Test performance guardrail: when replacing a slow integration test with helper-level coverage, extract the exact production composition into a named helper and test that helper. Do not trade coverage shape for speed without preserving the behavior proof somewhere cheaper.
|
||||
- Test performance guardrail: for plugin-owned static descriptors used by core tests or cold paths, prefer lightweight public artifacts with full-runtime fallback over loading broad bundled plugin barrels.
|
||||
- Agents MUST NOT modify baseline, inventory, ignore, snapshot, or expected-failure files to silence failing checks without explicit approval in this chat.
|
||||
- For targeted/local debugging, use the native root-project entrypoint: `pnpm test <path-or-filter> [vitest args...]` (for example `pnpm test src/commands/onboard-search.test.ts -t "shows registered plugin providers"`); do not default to raw `pnpm vitest run ...` because it bypasses the repo's default config/profile/pool routing.
|
||||
- Do not set test workers above 16; tried already.
|
||||
@@ -250,8 +263,8 @@
|
||||
|
||||
## Security & Configuration Tips
|
||||
|
||||
- Web provider stores creds at `~/.openclaw/credentials/`; rerun `openclaw login` if logged out.
|
||||
- Pi sessions live under `~/.openclaw/sessions/` by default; the base directory is not configurable.
|
||||
- Channel/provider state lives under `~/.openclaw/credentials/`; rerun `openclaw channels login` if logged out. Model auth profiles live under `~/.openclaw/agents/<agentId>/agent/auth-profiles.json`; legacy OAuth import still reads `~/.openclaw/credentials/oauth.json`.
|
||||
- Pi sessions live under `~/.openclaw/agents/<agentId>/sessions/` by default; `session.store` can override the session store path.
|
||||
- Environment variables: see `~/.profile`.
|
||||
- Never commit or publish real phone numbers, videos, or live configuration values. Use obviously fake placeholders in docs, tests, and examples.
|
||||
- Release flow: use the private [maintainer release docs](https://github.com/openclaw/maintainers/blob/main/release/README.md) for the actual runbook, `docs/reference/RELEASING.md` for the public release policy, and `$openclaw-release-maintainer` for the maintainership workflow.
|
||||
@@ -268,13 +281,13 @@
|
||||
- Signal: "update fly" => `fly ssh console -a flawd-bot -C "bash -lc 'cd /data/clawd/openclaw && git pull --rebase origin main'"` then `fly machines restart e825232f34d058 -a flawd-bot`.
|
||||
- CLI progress: use `src/cli/progress.ts` (`osc-progress` + `@clack/prompts` spinner); don’t hand-roll spinners/bars.
|
||||
- Status output: keep tables + ANSI-safe wrapping (`src/terminal/table.ts`); `status --all` = read-only/pasteable, `status --deep` = probes.
|
||||
- Gateway currently runs only as the menubar app; there is no separate LaunchAgent/helper label installed. Restart via the OpenClaw Mac app or `scripts/restart-mac.sh`; to verify/kill use `launchctl print gui/$UID | grep openclaw` rather than assuming a fixed label. **When debugging on macOS, start/stop the gateway via the app, not ad-hoc tmux sessions; kill any temporary tunnels before handoff.**
|
||||
- Gateway may run as an app-managed launchd job. Restart the gateway via the app or `openclaw gateway restart`; inspect with `openclaw gateway status --deep` or, for the default profile, `launchctl print gui/$UID/ai.openclaw.gateway`. Use `scripts/restart-mac.sh` when you need to rebuild/relaunch the local macOS app itself. The app LaunchAgent uses `ai.openclaw.mac`. **When debugging on macOS, start/stop the gateway via the app or gateway CLI, not ad-hoc tmux sessions; kill any temporary tunnels before handoff.**
|
||||
- macOS logs: use `./scripts/clawlog.sh` to query unified logs for the OpenClaw subsystem; it supports follow/tail/category filters and expects passwordless sudo for `/usr/bin/log`.
|
||||
- If shared guardrails are available locally, review them; otherwise follow this repo's guidance.
|
||||
- SwiftUI state management (iOS/macOS): prefer the `Observation` framework (`@Observable`, `@Bindable`) over `ObservableObject`/`@StateObject`; don’t introduce new `ObservableObject` unless required for compatibility, and migrate existing usages when touching related code.
|
||||
- Connection providers: when adding a new connection, update every UI surface and docs (macOS app, web UI, mobile if applicable, onboarding/overview docs) and add matching status + configuration forms so provider lists and settings stay in sync.
|
||||
- Version locations: `package.json` (CLI), `apps/android/app/build.gradle.kts` (versionName/versionCode), `apps/ios/Sources/Info.plist` + `apps/ios/Tests/Info.plist` (CFBundleShortVersionString/CFBundleVersion), `apps/macos/Sources/OpenClaw/Resources/Info.plist` (CFBundleShortVersionString/CFBundleVersion), `docs/install/updating.md` (pinned npm version), and Peekaboo Xcode projects/Info.plists (MARKETING_VERSION/CURRENT_PROJECT_VERSION).
|
||||
- "Bump version everywhere" means all version locations above **except** `appcast.xml` (only touch appcast when cutting a new macOS Sparkle release).
|
||||
- Version locations: `package.json` (CLI), `apps/android/app/build.gradle.kts` (versionName/versionCode), `apps/ios/version.json` (source for generated iOS config and Fastlane metadata), `apps/macos/Sources/OpenClaw/Resources/Info.plist` (CFBundleShortVersionString/CFBundleVersion), and `docs/install/updating.md` (pinned npm version).
|
||||
- "Bump version everywhere" means all version locations above, then run `pnpm ios:version:sync` for iOS generated outputs. Only touch appcast metadata when cutting a new macOS Sparkle release.
|
||||
- **Restart apps:** “restart iOS/Android apps” means rebuild (recompile/install) and relaunch, not just kill/launch.
|
||||
- **Device checks:** before testing, verify connected real devices (iOS/Android) before reaching for simulators/emulators.
|
||||
- Mobile pairing: `ws://` (cleartext) is allowed for private LAN addresses (RFC 1918, link-local, mDNS `.local`) and loopback. Private LAN hosts typically lack PKI-backed identity, so requiring TLS there adds complexity without meaningful security gain. `wss://` is required for Tailscale and public endpoints.
|
||||
|
||||
97
CHANGELOG.md
97
CHANGELOG.md
@@ -4,9 +4,101 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
## Unreleased
|
||||
|
||||
### Changes
|
||||
|
||||
### Fixes
|
||||
|
||||
## 2026.4.19-beta.2
|
||||
|
||||
### Fixes
|
||||
|
||||
- Agents/openai-completions: always send `stream_options.include_usage` on streaming requests, so local and custom OpenAI-compatible backends report real context usage instead of showing 0%. (#68746) Thanks @kagura-agent.
|
||||
- Agents/nested lanes: scope nested agent work per target session so a long-running nested run on one session no longer head-of-line blocks unrelated sessions across the gateway. (#67785) Thanks @stainlu.
|
||||
- Agents/status: preserve carried-forward session token totals for providers that omit usage metadata, so `/status` and `openclaw sessions` keep showing the last known context usage instead of dropping back to unknown/0%. (#67695) Thanks @stainlu.
|
||||
- Install/update: keep legacy update verification compatible with the QA Lab runtime shim, so updating older global installs to beta no longer fails after npm installs the package successfully.
|
||||
|
||||
## 2026.4.19-beta.1
|
||||
|
||||
### Fixes
|
||||
|
||||
- Agents/channels: route cross-agent subagent spawns through the target agent's bound channel account while preserving peer and workspace/role-scoped bindings, so child sessions no longer inherit the caller's account in shared rooms, workspaces, or multi-account setups. (#67508) Thanks @lukeboyett and @gumadeiras.
|
||||
- Telegram/callbacks: treat permanent callback edit errors as completed updates so stale command pagination buttons no longer wedge the update watermark and block newer Telegram updates. (#68588) Thanks @Lucenx9.
|
||||
- Browser/CDP: allow the selected remote CDP profile host for CDP health and control checks without widening browser navigation SSRF policy, so WSL-to-Windows Chrome endpoints no longer appear offline under strict defaults. Fixes #68108. (#68207) Thanks @Mlightsnow.
|
||||
- Codex: stop cumulative app-server token totals from being treated as fresh context usage, so session status no longer reports inflated context percentages after long Codex threads. (#64669) Thanks @cyrusaf.
|
||||
- Browser/CDP: add phase-specific CDP readiness diagnostics and normalize loopback WebSocket host aliases, so Windows browser startup failures surface whether HTTP discovery, WebSocket discovery, SSRF validation, or the `Browser.getVersion` health check failed.
|
||||
|
||||
## 2026.4.18
|
||||
|
||||
### Changes
|
||||
|
||||
- Anthropic/models: add Claude Opus 4.7 `xhigh` reasoning effort support and keep it separate from adaptive thinking.
|
||||
- Control UI/settings: overhaul the settings and slash-command experience with faster presets, quick-create flows, and refreshed command discovery. (#67819) Thanks @BunsDev.
|
||||
- macOS/gateway: add `screen.snapshot` support for macOS app nodes, including runtime plumbing, default macOS allowlisting, and docs for monitor preview flows. (#67954) Thanks @BunsDev.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Codex/gateway: fix gateway crashes when the codex-acp subprocess terminates abruptly; pending requests now shut down gracefully instead of propagating an uncaught EPIPE through the gateway daemon and connected channels. Fixes #67886. (#67947) Thanks @openperf.
|
||||
- Agents/bootstrap: resolve bootstrap from workspace truth instead of stale session transcript markers, keep embedded bootstrap instructions on a hidden user-context prelude, suppress normal `/new` and `/reset` greetings while `BOOTSTRAP.md` is still pending, and make the embedded runner read the bootstrap ritual before replying normally.
|
||||
- Agents/bootstrap: dedupe repeated bootstrap-truncation warnings so startup logs stay actionable. (#67906) Thanks @rubencu.
|
||||
- WhatsApp/multi-account: centralize named-account inbound policy, isolate per-account group activation and scoped session keys, preserve legacy activation backfill, and keep `accounts.default` shared defaults aligned across runtime, setup, and compat migration paths. Thanks @mcaxtr.
|
||||
- Cron/delivery: clean up isolated sessions after direct deliveries when `deleteAfterRun` is enabled, covering structured and threaded branches that previously bypassed cleanup. (#67807) Thanks @MonkeyLeeT.
|
||||
- Gateway/hello-ok: always report negotiated auth metadata and preserve scopes for reused device tokens on successful shared-auth handshakes, including control-ui bypass coverage when no device token is issued. (#67810, #68039) Thanks @BunsDev.
|
||||
- Onboarding/non-interactive: preserve existing gateway auth tokens during re-onboard so active local gateway clients are not disconnected by an implicit token rotation. (#67821) Thanks @BKF-Gitty.
|
||||
- OpenAI Codex/Responses: unify native Responses API capability detection so Codex OAuth requests emit the required `store: false` field on the native Responses path. (#67918) Thanks @obviyus.
|
||||
- WhatsApp/setup: guard personal-phone and allowlist prompt values so setup fails with clear validation errors instead of crashing on undefined prompt text. (#67895) Thanks @lawrence3699.
|
||||
- Models/config: preserve an existing `models.json` provider `baseUrl` during merge-mode regeneration so custom endpoints do not get reset on restart. (#67893) Thanks @lawrence3699.
|
||||
- Plugin SDK: preserve `secret-input-runtime` function exports in published builds so provider plugins can read SecretRef-backed setup inputs.
|
||||
- Plugins/discovery: reuse bundled and global plugin discovery results across workspace cache misses so Windows multi-workspace startup stops redoing the shared synchronous scan. (#67940) Thanks @obviyus.
|
||||
- Bundled plugins/install: keep staged bundled plugin runtime imports resolving through the packaged Plugin SDK while omitting checkout-only aliases from the dist inventory, so published installs do not fail on repo-local paths.
|
||||
- Plugins/webhooks: enforce synchronous plugin registration with full rollback of failed plugin side effects, and cache SecretRef-backed webhook auth per route so plugin startup and inbound webhook auth stay deterministic. (#67941) Thanks @obviyus.
|
||||
- Telegram/ACP bindings: drop persisted DM bindings that still point at missing or failed ACP sessions on restart, while preserving plugin-owned bindings and uncertain store reads. (#67822) Thanks @chinar-amrutkar.
|
||||
- Telegram/streaming: keep a transient preview on the same Telegram message when auto-compaction retries an in-flight answer, so streamed replies no longer appear duplicated after compaction. (#66939) Thanks @rubencu.
|
||||
- Memory/sqlite-vec: emit the degraded sqlite-vec warning once per degraded episode instead of repeating it for every file write, while preserving the latch across safe-reindex rollback and resetting it when vector state is genuinely rebuilt. (#67898) Thanks @rubencu.
|
||||
- Memory-core: preserve stored vector dimensions during read-only recovery so memory indexes do not lose vector metadata while repairing read-only state.
|
||||
- Reply/block streaming: preserve post-stream incomplete-turn error payloads after block streaming already emitted content, so users get the warning instead of silence. (#67991) Thanks @obviyus.
|
||||
- Telegram/streaming: clear the compaction replay guard after visible non-final boundaries so a post-tool assistant reply rotates to a fresh preview instead of editing the pre-compaction message. (#67993) Thanks @obviyus.
|
||||
- Matrix: fix `sessions_spawn --thread` subagent session spawning — thread binding creation, cleanup on session end, and completion-message delivery target resolution now work end-to-end. (#67643) Thanks @eejohnso-ops and @gumadeiras.
|
||||
- Slack/streaming: resolve native streaming recipient teams from the inbound user when available, with a monitor-team fallback, so DM and shared-workspace streams target the right recipient more reliably.
|
||||
- macOS/webchat: enable Undo and Redo in the composer text input by turning on the native `NSTextView` undo manager. (#34962) Thanks @tylerbittner.
|
||||
- macOS/remote SSH: require an already-trusted host key on the macOS remote command, gateway probe, port tunnel, and pairing probe paths by switching `StrictHostKeyChecking=accept-new` to `StrictHostKeyChecking=yes` and centralizing the shared SSH option fragments in `CommandResolver`, so first-time macOS remote connections no longer silently accept an unknown host key and must be trusted ahead of time via `~/.ssh/known_hosts`. (#68199)
|
||||
- CLI/configure: show the channel picker before probing statuses and let remove mode delete configured channel blocks directly from config. (#68007) Thanks @gumadeiras.
|
||||
- Control UI/settings: reset scroll position when switching settings pages and align details headers. (#68150) Thanks @BunsDev.
|
||||
- OpenAI Codex/OAuth: keep OpenClaw as the canonical owner for imported Codex CLI OAuth sessions, stop writing refreshed credentials back into `.codex`, and prefer fresher OpenClaw credentials over stale imported CLI state so refresh recovery stays stable. Thanks @vincentkoc.
|
||||
- OpenAI Codex/OAuth: treat the OpenAI TLS prerequisites probe as advisory instead of a hard blocker, so Codex sign-in can still proceed when the speculative Node/OpenSSL precheck fails but the real OAuth flow still works. Thanks @vincentkoc.
|
||||
- Models status/OAuth health: align OAuth health reporting with the same effective credential view runtime uses, so expired refreshable sessions stop showing healthy by default and fresher imported Codex CLI credentials surface correctly in `models status`, doctor, and gateway auth status. Thanks @vincentkoc.
|
||||
- OpenAI Codex/OAuth: keep external CLI OAuth imports runtime-only by overlaying fresher Codex CLI credentials without mutating `auth-profiles.json`, so `.codex` stays a bootstrap/runtime input instead of becoming durable OpenClaw state. Thanks @vincentkoc.
|
||||
- OpenAI Codex/OAuth: drop legacy CLI-manager routing from the remaining bootstrap path so Codex and MiniMax CLI imports are matched by their canonical OpenClaw profile ids instead of stale `managedBy` metadata. Thanks @vincentkoc.
|
||||
- OpenAI Codex/OAuth: only bootstrap from external CLI OAuth when the local OpenClaw profile is missing or unusable, so healthy local sessions are no longer overridden by fresher `.codex` tokens. Thanks @vincentkoc.
|
||||
- OpenAI Codex/OAuth: rename the external CLI bootstrap helper, reuse the same usable-oauth check across runtime fallback paths, and add debug logs plus health coverage so bootstrap decisions stay legible. Thanks @vincentkoc.
|
||||
- Twitch/setup: load Twitch through the bundled setup-entry discovery path and keep setup/status account detection aligned with runtime config. (#68008) Thanks @gumadeiras.
|
||||
- Feishu/card actions: resolve card-action chat type from the Feishu chat API when stored context is missing, preferring `chat_mode` over `chat_type`, so DM-originated card actions no longer bypass `dmPolicy` by falling through to the group handling path. (#68201)
|
||||
- Cron/isolated-agent: preserve `trusted: false` on isolated cron awareness events mirrored into the main session, and forward the optional `trusted` flag through the gateway cron wrapper so explicit trust downgrades survive session-key scoping. (#68210)
|
||||
- Agents/fallback: recognize bare leading ZenMux `402 ...` quota-refresh errors without misclassifying plain numeric `402 ...` text, and keep the embedded fallback regression coverage stable. (#47579) Thanks @bwjoke.
|
||||
- Failover/google: only treat `INTERNAL` status payloads as retryable timeouts when they also carry a `500` code, so malformed non-500 payloads do not enter the retry path. (#68238) Thanks @altaywtf and @Openbling.
|
||||
- Agents/tools: filter bundled MCP/LSP tools through the final owner-only and tool-policy pipeline after merging them into the effective tool list, so existing allowlists, deny rules, sandbox policy, subagent policy, and owner-only restrictions apply to bundled tools the same way they apply to core tools. (#68195)
|
||||
- Gateway/assistant media: require `operator.read` scope for assistant-media file and metadata requests on identity-bearing HTTP auth paths so callers without a read scope can no longer access assistant media. (#68175) Thanks @eleqtrizit.
|
||||
- Gateway/web: allow same-origin microphone access in the Permissions-Policy header so browser voice capture can work from the Control UI and webchat origin. (#68368)
|
||||
- Exec approvals/display: escape raw control characters (including newline and carriage return) in the shared and macOS approval-prompt command sanitizers, so trailing command payloads no longer render on hidden extra lines in the approval UI. (#68198)
|
||||
- Telegram/streaming: fence same-session stale preview and finalization work after aborts so Telegram no longer replays an older reply or flushes a hidden short preview after the abort confirmation lands. (#68100) Thanks @rubencu.
|
||||
- OpenAI Codex/OAuth + Pi: keep imported Codex CLI OAuth bootstrap, Pi auth export, and runtime overlay handling aligned so Codex sessions survive refresh and health checks without leaking transient CLI state into saved auth files. Thanks @vincentkoc.
|
||||
- OpenAI Codex/OAuth: keep Codex-specific auth bridging inside the owning plugins, preserve canonical imported CLI profiles, and allow legacy identity-less main-store OAuth sessions to upgrade during refresh mirroring. (#68284) Thanks @vincentkoc.
|
||||
- Config/redact: add `browser.cdpUrl` and `browser.profiles.*.cdpUrl` to sensitive URL config paths so embedded credentials (query tokens and HTTP Basic auth) are properly redacted in `config.get` API responses and availability error messages. (#67679) Thanks @Ziy1-Tan.
|
||||
- Agents/TTS: report failed speech synthesis as a real tool error so unconfigured providers no longer feed successful TTS failure output back into agent loops. (#67980) Thanks @lawrence3699.
|
||||
- Gateway/wake: allow unknown properties on wake payloads so external senders like Paperclip can attach opaque metadata without failing schema validation. (#68355) Thanks @kagura-agent.
|
||||
- Matrix: honor `channels.matrix.network.dangerouslyAllowPrivateNetwork` when creating clients for private-network homeservers. (#68332) Thanks @kagura-agent.
|
||||
- Cron/message tool: keep cron-owned runs with `delivery.mode: "none"` on the normal message-tool path so they can still send explicit messages, create threads, and route conditionally when no runner-owned delivery target is active. (#68482) Thanks @obviyus.
|
||||
- Agents/failover: avoid treating bare leading `402 ...` prose as billing errors while still recognizing proxy subscription failures. (#45827) Thanks @junyuc25.
|
||||
- Config/$schema: preserve root-authored `$schema` during partial config rewrites without injecting include-only schema URLs into the root config. (#47322) Thanks @EfeDurmaz16.
|
||||
- Agents/CLI delivery: run the same reply-media path normalizer the auto-reply flow uses before shipping `openclaw agent --deliver` payloads, so relative `MEDIA:./out/photo.png` tokens resolve against the agent workspace instead of being rejected downstream with `LocalMediaAccessError: Local media path is not under an allowed directory`. Thanks @frankekn.
|
||||
- Agents/Google: strip `thinkingBudget=0` for the thinking-required `gemini-2.5-pro` model in embedded-runner and native Google payloads, so requests no longer fail with `Budget 0 is invalid. This model only works in thinking mode.` and the API uses its default thinking behavior instead. (#68607) Thanks @josmithiii.
|
||||
- Slack/threads: log failed thread starter and history fetches at verbose level while preserving best-effort fallback behavior, so missing Slack thread context is diagnosable without interrupting inbound handling. (#68594) Thanks @martingarramon.
|
||||
- Gateway/restart: keep stale-gateway cleanup from terminating the current process's parent or ancestors, so plugin sidecars like WeChat no longer kill the active gateway and trigger an infinite supervisor restart loop. Fixes #68451. (#68517) Thanks @openperf.
|
||||
- Gateway/auth: reject gateway auth credentials that match published example placeholders at startup and secret reload, and keep cloud install snippets from publishing copy-paste gateway/keyring secrets. (#68404) Thanks @coygeek.
|
||||
- CLI/update: preserve macOS restart helper launchctl failures in the update restart log without letting log setup block the restart path. (#68492) Thanks @hclsys.
|
||||
- Slack/threads: keep file-only root messages as starter context so first thread replies can still hydrate starter media. (#68594) Thanks @martingarramon.
|
||||
- Google/Antigravity: resolve forward-compatible Gemini 3.1 Pro custom-tools and Flash variants from the bundled Google plugin templates, so `google-antigravity/gemini-3.1-pro-preview-customtools` no longer falls through to an unknown-model error. Fixes #35512.
|
||||
- Active Memory: raise the blocking recall timeout ceiling to 120 seconds and reject larger config values during plugin schema validation. Fixes #68410. (#68480) Thanks @Bartok9.
|
||||
- Control UI/chat: keep history-backed user image uploads visible after chat reload while filtering blocked or non-image transcript media paths. (#68415) Thanks @mraleko.
|
||||
|
||||
## 2026.4.15
|
||||
|
||||
@@ -113,6 +205,11 @@ Docs: https://docs.openclaw.ai
|
||||
- Webchat/security: reject remote-host `file://` URLs in the media embedding path. (#67293) Thanks @pgondhi987.
|
||||
- Dreaming/memory-core: use the ingestion day, not the source file day, for daily recall dedupe so repeat sweeps of the same daily note can increment `dailyCount` across days instead of stalling at `1`. (#67091) Thanks @Bartok9.
|
||||
- Node-host/tools.exec: let approval binding distinguish known native binaries from mutable shell payload files, while still fail-closing unknown or racy file probes so absolute-path node-host commands like `/usr/bin/whoami` no longer get rejected as unsafe interpreter/runtime commands. (#66731) Thanks @tmimmanuel.
|
||||
- Codex/gateway: fix gateway crash when the codex-acp subprocess terminates abruptly; an unhandled EPIPE on the child stdin stream now routes through graceful client shutdown, rejecting pending requests instead of propagating as an uncaught exception that crashes the entire gateway daemon and all connected channels. Fixes #67886. (#67947) thanks @openperf
|
||||
- Slack/streaming: resolve native streaming recipient teams from the inbound user when available, with a monitor-team fallback, so DM and shared-workspace streams target the right recipient more reliably.
|
||||
- OpenRouter/streaming: treat `reasoning_details.response.output_text` and `reasoning_details.response.text` as visible assistant output on OpenRouter-compatible completions streams, while keeping `reasoning.text` hidden and refusing to surface ambiguous bare `text` items by default so visible replies, thinking blocks, and tool calls can coexist in the same chunk. (#67410) Thanks @neeravmakwana.
|
||||
- Models/OpenRouter aliases: resolve `openrouter:auto` to the canonical `openrouter/auto` model and map `openrouter:free` to the first configured concrete `openrouter/...:free` model instead of mis-resolving these compatibility aliases under the default provider. (#57066) Thanks @sumiisiaran.
|
||||
- OpenRouter/Arcee: canonicalize stale OpenRouter `https://openrouter.ai/v1` base URLs during provider config normalization and runtime model/transport resolution, so fresh `models.json` writes and previously discovered rows self-heal back to `https://openrouter.ai/api/v1` instead of breaking OpenRouter-routed requests. (#67295) Thanks @achalkov.
|
||||
|
||||
## 2026.4.14
|
||||
|
||||
|
||||
64
README.md
64
README.md
@@ -285,133 +285,69 @@ Thanks to all clawtributors:
|
||||
<!-- clawtributors:start -->
|
||||
|
||||
[](https://github.com/steipete) [](https://github.com/vincentkoc) [](https://github.com/Takhoffman) [](https://github.com/obviyus) [](https://github.com/gumadeiras) [](https://github.com/mbelinky) [](https://github.com/vignesh07) [](https://github.com/joshavant) [](https://github.com/scoootscooob) [](https://github.com/jacobtomlinson)
|
||||
|
||||
[](https://github.com/shakkernerd) [](https://github.com/sebslight) [](https://github.com/tyler6204) [](https://github.com/ngutman) [](https://github.com/thewilloftheshadow) [](https://github.com/Sid-Qin) [](https://github.com/mcaxtr) [](https://github.com/eleqtrizit) [](https://github.com/BunsDev) [](https://github.com/cpojer)
|
||||
|
||||
[](https://github.com/Glucksberg) [](https://github.com/osolmaz) [](https://github.com/bmendonca3) [](https://github.com/jalehman) [](https://github.com/huntharo) [](https://github.com/neeravmakwana) [](https://github.com/openperf) [](https://github.com/joshp123) [](https://github.com/pgondhi987) [](https://github.com/altaywtf)
|
||||
|
||||
[](https://github.com/quotentiroler) [](https://github.com/liuxiaopai-ai) [](https://github.com/rodrigouroz) [](https://github.com/frankekn) [](https://github.com/drobison00) [](https://github.com/zerone0x) [](https://github.com/onutc) [](https://github.com/ademczuk) [](https://github.com/ImLukeF) [](https://github.com/hydro13)
|
||||
|
||||
[](https://github.com/hxy91819) [](https://github.com/coygeek) [](https://github.com/dutifulbob) [](https://github.com/sliverp) [](https://github.com/0xRaini) [](https://github.com/robbyczgw-cla) [](https://github.com/joelnishanth) [](https://github.com/echoVic) [](https://github.com/sallyom) [](https://github.com/yinghaosang)
|
||||
|
||||
[](https://github.com/BradGroux) [](https://github.com/christianklotz) [](https://github.com/odysseus0) [](https://github.com/hclsys) [](https://github.com/byungsker) [](https://github.com/pashpashpash) [](https://github.com/stakeswky) [![github-actions[bot]](https://avatars.githubusercontent.com/in/15368?v=4&s=48)](https://github.com/apps/github-actions) [](https://github.com/xinhuagu) [](https://github.com/MonkeyLeeT)
|
||||
|
||||
[](https://github.com/100yenadmin) [](https://github.com/mcinteerj) [](https://github.com/samzong) [](https://github.com/chilu18) [](https://github.com/darkamenosa) [](https://github.com/widingmarcus-cyber) [](https://github.com/cgdusek) [](https://github.com/Lukavyi) [](https://github.com/davidrudduck) [](https://github.com/VACInc)
|
||||
|
||||
[](https://github.com/MoerAI) [](https://github.com/velvet-shark) [](https://github.com/HenryLoenwind) [](https://github.com/omarshahine) [](https://github.com/bohdanpodvirnyi) [](https://github.com/VeriteIgiraneza) [](https://github.com/akramcodez) [](https://github.com/Kaneki-x) [](https://github.com/aether-ai-agent) [](https://github.com/joaohlisboa)
|
||||
|
||||
[](https://github.com/MaudeBot) [](https://github.com/davidguttman) [](https://github.com/justinhuangcode) [](https://github.com/lml2468) [](https://github.com/wirjo) [](https://github.com/iHildy) [](https://github.com/mudrii) [](https://github.com/advaitpaliwal) [](https://github.com/czekaj) [](https://github.com/dlauer)
|
||||
|
||||
[](https://github.com/Solvely-Colin) [](https://github.com/feiskyer) [](https://github.com/brandonwise) [](https://github.com/conroywhitney) [](https://github.com/mneves75) [](https://github.com/jaydenfyi) [](https://github.com/davemorin) [](https://github.com/joeykrug) [](https://github.com/kevinWangSheng) [](https://github.com/pejmanjohn)
|
||||
|
||||
[](https://github.com/Lanfei) [](https://github.com/liuy) [](https://github.com/lc0rp) [](https://github.com/teconomix) [](https://github.com/omair445) [](https://github.com/dorukardahan) [](https://github.com/mmaps) [](https://github.com/tobiasbischoff) [](https://github.com/adhitShet) [](https://github.com/pandego)
|
||||
|
||||
[](https://github.com/bradleypriest) [](https://github.com/bjesuiter) [](https://github.com/grp06) [](https://github.com/shadril238) [](https://github.com/kesku) [](https://github.com/YuriNachos) [](https://github.com/vrknetha) [](https://github.com/smartprogrammer93) [](https://github.com/Nachx639) [](https://github.com/jnMetaCode)
|
||||
|
||||
[](https://github.com/Phineas1500) [](https://github.com/dingn42) [](https://github.com/geekhuashan) [](https://github.com/Nanako0129) [](https://github.com/AytuncYildizli) [](https://github.com/BruceMacD) [](https://github.com/jjjojoj) [](https://github.com/mvanhorn) [](https://github.com/bugkill3r) [](https://github.com/rahthakor)
|
||||
|
||||
[](https://github.com/GodsBoy) [](https://github.com/SARAMALI15792) [](https://github.com/radek-paclt) [](https://github.com/Elarwei001) [](https://github.com/ingyukoh) [](https://github.com/SnowSky1) [](https://github.com/lewiswigmore) [](https://github.com/solavrc) [](https://github.com/aldoeliacim) [](https://github.com/jrusz)
|
||||
|
||||
[](https://github.com/tonydehnke) [](https://github.com/roshanasingh4) [](https://github.com/zssggle-rgb) [](https://github.com/adam91holt) [](https://github.com/graysurf) [](https://github.com/xadenryan) [](https://github.com/sfo2001) [](https://github.com/orlyjamie) [](https://github.com/hsrvc) [](https://github.com/tomsun28)
|
||||
|
||||
[](https://github.com/BillChirico) [](https://github.com/carrotRakko) [](https://github.com/ranausmanai) [](https://github.com/arkyu2077) [](https://github.com/hoyyeva) [](https://github.com/luoyanglang) [](https://github.com/sibbl) [](https://github.com/gregmousseau) [](https://github.com/sahilsatralkar) [](https://github.com/akoscz)
|
||||
|
||||
[](https://github.com/rrenamed) [](https://github.com/YuzuruS) [](https://github.com/Marvae) [](https://github.com/mitchmcalister) [](https://github.com/juanpablodlc) [](https://github.com/shtse8) [](https://github.com/thebenignhacker) [](https://github.com/nimbleenigma) [](https://github.com/Linux2010) [](https://github.com/shichangs)
|
||||
|
||||
[](https://github.com/efe-arv) [](https://github.com/hsiaoa) [](https://github.com/nabbilkhan) [](https://github.com/ayanesakura) [](https://github.com/lupuletic) [](https://github.com/polooooo) [](https://github.com/xaeon2026) [](https://github.com/shrey150) [](https://github.com/taw0002) [](https://github.com/dinakars777)
|
||||
|
||||
[](https://github.com/giulio-leone) [](https://github.com/nyanjou) [](https://github.com/meaningfool) [](https://github.com/kunalk16) [](https://github.com/ide-rea) [](https://github.com/JonathanJing) [](https://github.com/yelog) [](https://github.com/markmusson) [](https://github.com/kiranvk-2011) [](https://github.com/Sathvik-Chowdary-Veerapaneni)
|
||||
|
||||
[](https://github.com/rogerdigital) [](https://github.com/artwalker) [](https://github.com/azade-c) [](https://github.com/chinar-amrutkar) [](https://github.com/maxsumrall) [](https://github.com/Minidoracat) [](https://github.com/unisone) [](https://github.com/ly85206559) [](https://github.com/theSamPadilla) [](https://github.com/AnonO6)
|
||||
|
||||
[](https://github.com/afurm) [](https://github.com/jwchmodx) [](https://github.com/leszekszpunar) [](https://github.com/Mrseenz) [](https://github.com/Yida-Dev) [](https://github.com/kesor) [](https://github.com/mazhe-nerd) [](https://github.com/buerbaumer) [](https://github.com/magimetal) [](https://github.com/patelhiren)
|
||||
|
||||
[](https://github.com/BinHPdev) [](https://github.com/RyanLee-Dev) [](https://github.com/cathrynlavery) [](https://github.com/al3mart) [](https://github.com/JustYannicc) [](https://github.com/AbhisekBasu1) [](https://github.com/dbhurley) [](https://github.com/mpz4life) [](https://github.com/tmimmanuel) [](https://github.com/JustasMonkev)
|
||||
|
||||
[](https://github.com/simantak-dabhade) [](https://github.com/NicholasSpisak) [](https://github.com/natefikru) [](https://github.com/dunamismax) [](https://github.com/simonemacario) [](https://github.com/ENCHIGO) [](https://github.com/xingsy97) [](https://github.com/emonty) [](https://github.com/jadilson12) [](https://github.com/kirisame-wang)
|
||||
|
||||
[](https://github.com/mathiasnagler) [](https://github.com/Oceanswave) [](https://github.com/gumclaw) [](https://github.com/RichardCao) [](https://github.com/MKV21) [](https://github.com/petter-b) [](https://github.com/CodeForgeNet) [](https://github.com/johnsonshi) [](https://github.com/durenzidu) [](https://github.com/dougvk)
|
||||
|
||||
[](https://github.com/Whoaa512) [](https://github.com/zimeg) [](https://github.com/TsekaLuk) [](https://github.com/Ryan-Haines) [](https://github.com/uf-hy) [](https://github.com/Daanvdplas) [](https://github.com/bittoby) [](https://github.com/xuhao1) [](https://github.com/Lucenx9) [](https://github.com/HeMuling)
|
||||
|
||||
[](https://github.com/AaronLuo00) [](https://github.com/YUJIE2002) [](https://github.com/DhruvBhatia0) [](https://github.com/divanoli) [](https://github.com/derbronko) [](https://github.com/rubyrunsstuff) [](https://github.com/rabsef-bicrym) [](https://github.com/IVY-AI-gif) [](https://github.com/pvtclawn) [](https://github.com/stephenschoettler)
|
||||
|
||||
[](https://github.com/minupla) [](https://github.com/xzq-xu) [](https://github.com/mousberg) [](https://github.com/arifahmedjoy) [](https://github.com/harhogefoo) [](https://github.com/2233admin) [](https://github.com/ameno-) [](https://github.com/battman21) [](https://github.com/bcherny) [](https://github.com/bobashopcashier)
|
||||
|
||||
[](https://github.com/dguido) [](https://github.com/druide67) [](https://github.com/guirguispierre) [](https://github.com/jzakirov) [](https://github.com/loganprit) [](https://github.com/martinfrancois) [](https://github.com/neo1027144-creator) [](https://github.com/RealKai42) [](https://github.com/schumilin) [](https://github.com/shuofengzhang)
|
||||
|
||||
[](https://github.com/solstead) [](https://github.com/hengm3467) [](https://github.com/chziyue) [](https://github.com/jameslcowan) [](https://github.com/scifantastic) [](https://github.com/ryan-crabbe) [](https://github.com/alexfilatov) [](https://github.com/Luckymingxuan) [](https://github.com/Hollychou924) [](https://github.com/badlogic)
|
||||
|
||||
[](https://github.com/hnykda) [](https://github.com/dbachelder) [](https://github.com/heavenlost) [](https://github.com/shad0wca7) [](https://github.com/jared596) [](https://github.com/kiranjd) [](https://github.com/Mellowambience) [](https://github.com/KimGLee) [](https://github.com/seheepeak) [](https://github.com/TSavo)
|
||||
|
||||
[](https://github.com/mcrolly) [](https://github.com/dashed) [](https://github.com/Shuai-DaiDai) [](https://github.com/suboss87) [](https://github.com/emanuelst) [](https://github.com/magendary) [](https://github.com/PeterShanxin) [](https://github.com/j2h4u) [](https://github.com/bsormagec) [](https://github.com/mjamiv)
|
||||
|
||||
[](https://github.com/aerolalit) [](https://github.com/jessy2027) [](https://github.com/buddyh) [](https://github.com/aaron-he-zhu) [](https://github.com/hhhhao28) [](https://github.com/benostein) [](https://github.com/LyleLiu666) [](https://github.com/pingren) [](https://github.com/popomore) [](https://github.com/Dithilli)
|
||||
|
||||
[](https://github.com/fal3) [](https://github.com/mkbehr) [](https://github.com/mteam88) [](https://github.com/gupsammy) [](https://github.com/gut-puncture) [](https://github.com/garnetlyx) [](https://github.com/miloudbelarebia) [](https://github.com/Protocol-zero-0) [](https://github.com/pvoo) [](https://github.com/patrick-yingxi-pan)
|
||||
|
||||
[](https://github.com/ptahdunbar) [](https://github.com/keepitmello) [](https://github.com/artuskg) [](https://github.com/Anandesh-Sharma) [](https://github.com/zidongdesign) [](https://github.com/Innocent-children) [](https://github.com/El-Fitz) [](https://github.com/arthurbr11) [](https://github.com/jackheuberger) [](https://github.com/serkonyc)
|
||||
|
||||
[](https://github.com/guxu11) [](https://github.com/hyojin) [](https://github.com/jeann2013) [](https://github.com/jogelin) [](https://github.com/rmorse) [](https://github.com/scz2011) [](https://github.com/andyliu) [](https://github.com/benithors) [](https://github.com/xiwuqi) [](https://github.com/TigerInYourDream)
|
||||
|
||||
[](https://github.com/aaronagent) [](https://github.com/TonyDerek-dot) [](https://github.com/Zitzak) [](https://github.com/ruypang) [](https://github.com/stainlu) [](https://github.com/OpenCils) [](https://github.com/stefangalescu) [](https://github.com/sp-hk2ldn) [](https://github.com/MikeORed) [](https://github.com/graciegould)
|
||||
|
||||
[](https://github.com/cash-echo-bot) [](https://github.com/visionik) [](https://github.com/WalterSumbon) [](https://github.com/SubtleSpark) [](https://github.com/krizpoon) [](https://github.com/rodbland2021) [](https://github.com/thomasxm) [](https://github.com/sar618) [](https://github.com/fagemx) [](https://github.com/daymade)
|
||||
|
||||
[](https://github.com/tysoncung) [](https://github.com/pycckuu) [](https://github.com/omniwired) [](https://github.com/connorshea) [](https://github.com/bonald) [](https://github.com/BeeSting50) [](https://github.com/nachoiacovino) [](https://github.com/zhumengzhu) [](https://github.com/Vitalcheffe) [](https://github.com/zhoulongchao77)
|
||||
|
||||
[](https://github.com/navarrotech) [](https://github.com/CommanderCrowCode) [](https://github.com/paceyw) [](https://github.com/Aftabbs) [](https://github.com/Alex-Alaniz) [](https://github.com/jarvis-medmatic) [](https://github.com/tomron87) [](https://github.com/day253) [](https://github.com/Jaaneek) [](https://github.com/AnCoSONG)
|
||||
|
||||
[](https://github.com/ziomancer) [](https://github.com/shayan919293) [](https://github.com/edwluo) [](https://github.com/rjchien728) [](https://github.com/TinyTb) [](https://github.com/No898) [](https://github.com/ianderrington) [](https://github.com/L-U-C-K-Y) [](https://github.com/peschee) [](https://github.com/Kepler2024)
|
||||
|
||||
[](https://github.com/julianengel) [](https://github.com/markfietje) [](https://github.com/dakshaymehta) [](https://github.com/DavidNitZ) [](https://github.com/dominicnunez) [](https://github.com/danielwanwx) [](https://github.com/hongsw) [](https://github.com/Youyou972) [](https://github.com/boris721) [](https://github.com/damoahdominic)
|
||||
|
||||
[](https://github.com/dan-dr) [](https://github.com/doodlewind) [](https://github.com/kkarimi) [](https://github.com/brokemac79) [](https://github.com/ozbillwang) [](https://github.com/ravyg) [](https://github.com/jasonhargrove) [](https://github.com/BrianWang1990) [](https://github.com/hackersifu) [](https://github.com/Fologan)
|
||||
|
||||
[](https://github.com/AnonAmit) [](https://github.com/v1p0r) [](https://github.com/ajay99511) [](https://github.com/Iranb) [](https://github.com/yhyatt) [](https://github.com/codexGW) [](https://github.com/ShaunTsai) [](https://github.com/papago2355) [](https://github.com/cdorsey) [](https://github.com/tda1017)
|
||||
|
||||
[](https://github.com/0xJonHoldsCrypto) [](https://github.com/akyourowngames) [![clawdinator[bot]](https://avatars.githubusercontent.com/in/2607181?v=4&s=48)](https://github.com/apps/clawdinator) [](https://github.com/koala73) [](https://github.com/sircrumpet) [](https://github.com/thesomewhatyou) [](https://github.com/zats) [](https://github.com/duqaXxX) [](https://github.com/Joly0) [](https://github.com/hannasdev)
|
||||
|
||||
[](https://github.com/jlowin) [](https://github.com/peetzweg) [](https://github.com/adao-max) [](https://github.com/tumf) [](https://github.com/Huntterxx) [](https://github.com/nk1tz) [](https://github.com/lidamao633) [](https://github.com/liebertar) [](https://github.com/CornBrother0x) [](https://github.com/DukeDeSouth)
|
||||
|
||||
[](https://github.com/sahancava) [](https://github.com/CashWilliams) [](https://github.com/lumpinif) [](https://github.com/AdeboyeDN) [](https://github.com/Rohan5commit) [](https://github.com/srinivaspavan9) [](https://github.com/h0tp-ftw) [](https://github.com/neooriginal) [](https://github.com/Tianworld) [](https://github.com/Bermudarat)
|
||||
|
||||
[](https://github.com/asklee-klawd) [](https://github.com/yuting0624) [](https://github.com/constansino) [](https://github.com/ghsmc) [](https://github.com/ibrahimq21) [](https://github.com/irtiq7) [](https://github.com/kelvinCB) [](https://github.com/mitsuhiko) [](https://github.com/nohat) [](https://github.com/santiagomed)
|
||||
|
||||
[](https://github.com/suminhthanh) [](https://github.com/svkozak) [](https://github.com/zhangzhefang-github) [](https://github.com/HOYALIM) [](https://github.com/ping-Toven) [](https://github.com/0-CYBERDYNE-SYSTEMS-0) [](https://github.com/ylc0919) [](https://github.com/reed1898) [](https://github.com/ItsAditya-xyz) [](https://github.com/samrusani)
|
||||
|
||||
[](https://github.com/andyk-ms) [](https://github.com/18-RAJAT) [](https://github.com/cyb1278588254) [](https://github.com/zoherghadyali) [](https://github.com/manikv12) [](https://github.com/manueltarouca) [](https://github.com/GaosCode) [](https://github.com/pahdo) [](https://github.com/detecti1) [](https://github.com/JasonOA888)
|
||||
|
||||
[](https://github.com/sumukhj1219) [](https://github.com/bakhtiersizhaev) [](https://github.com/kyleok) [](https://github.com/AkashKobal) [](https://github.com/zhuisDEV) [](https://github.com/wu-tian807) [](https://github.com/vsabavat) [](https://github.com/kinfey) [](https://github.com/crimeacs) [](https://github.com/VibhorGautam)
|
||||
|
||||
[](https://github.com/John-Rood) [](https://github.com/velamints2) [](https://github.com/benjipeng) [](https://github.com/divisonofficer) [](https://github.com/Rahulkumar070) [](https://github.com/rockcent) [](https://github.com/Limitless2023) [](https://github.com/24601) [](https://github.com/awkoy) [](https://github.com/dawondyifraw)
|
||||
|
||||
[![google-labs-jules[bot]](https://avatars.githubusercontent.com/in/842251?v=4&s=48)](https://github.com/apps/google-labs-jules) [](https://github.com/henrino3) [](https://github.com/Kansodata) [](https://github.com/kaonash) [](https://github.com/p6l-richard) [](https://github.com/pi0) [](https://github.com/skainguyen1412) [](https://github.com/Starhappysh) [](https://github.com/xdanger) [](https://github.com/p3nchan)
|
||||
|
||||
[](https://github.com/scald) [](https://github.com/kashevk0) [](https://github.com/Yuandiaodiaodiao) [](https://github.com/doguabaris) [](https://github.com/ysqander) [](https://github.com/andranik-sahakyan) [](https://github.com/Wangnov) [](https://github.com/rixau) [](https://github.com/lisitan) [](https://github.com/kaizen403)
|
||||
|
||||
[](https://github.com/hirefrank) [](https://github.com/kennyklee) [](https://github.com/dddabtc) [](https://github.com/edincampara) [](https://github.com/fellanH) [](https://github.com/VarunChopra11) [](https://github.com/wangai-studio) [](https://github.com/sleontenko) [](https://github.com/yassine20011) [](https://github.com/ant1eicher)
|
||||
|
||||
[](https://github.com/ThomsenDrake) [](https://github.com/kakuteki) [](https://github.com/andreabadesso) [](https://github.com/chenxin-yan) [](https://github.com/cordx56) [](https://github.com/dvrshil) [](https://github.com/MarvinCui) [](https://github.com/Yeom-JinHo) [](https://github.com/17jmumford) [](https://github.com/KnHack)
|
||||
|
||||
[](https://github.com/SharoonSharif) [](https://github.com/orenyomtov) [](https://github.com/mattqdev) [](https://github.com/parkertoddbrooks) [](https://github.com/he-yufeng) [](https://github.com/Milofax) [](https://github.com/stevebot-alive) [](https://github.com/zhoulf1006) [](https://github.com/jrrcdev) [](https://github.com/feniix)
|
||||
|
||||
[](https://github.com/ZetiMente) [](https://github.com/QuantDeveloperUSA) [](https://github.com/alexstyl) [](https://github.com/ethanpalm) [](https://github.com/qkal) [](https://github.com/cygaar) [](https://github.com/U-C4N) [](https://github.com/jakobdylanc) [](https://github.com/antons) [](https://github.com/austinm911)
|
||||
|
||||
[](https://github.com/mahmoudashraf93) [](https://github.com/philipp-spiess) [](https://github.com/pkrmf) [](https://github.com/joshrad-dev) [](https://github.com/factnest365-ops) [](https://github.com/yingchunbai) [](https://github.com/aj47) [](https://github.com/Alg0rix) [](https://github.com/futhgar) [](https://github.com/YonganZhang)
|
||||
|
||||
[](https://github.com/remusao) [](https://github.com/danballance) [](https://github.com/GHesericsu) [](https://github.com/kimitaka) [](https://github.com/itsjling) [](https://github.com/RayBB) [](https://github.com/lutr0) [](https://github.com/claude) [](https://github.com/angrybirddd) [](https://github.com/fabianwilliams)
|
||||
|
||||
[](https://github.com/haoruilee) [](https://github.com/8BlT) [](https://github.com/atalovesyou) [](https://github.com/erikpr1994) [](https://github.com/jonasjancarik) [](https://github.com/longmaba) [](https://github.com/mitschabaude-bot) [](https://github.com/thesash) [](https://github.com/rdev) [](https://github.com/easternbloc)
|
||||
|
||||
[](https://github.com/chrisrodz) [](https://github.com/gabriel-trigo) [](https://github.com/manmal) [](https://github.com/neist) [](https://github.com/wes-davis) [](https://github.com/ManuelHettich) [](https://github.com/sktbrd) [](https://github.com/larlyssa) [](https://github.com/pcty-nextgen-service-account) [](https://github.com/Syhids)
|
||||
|
||||
[](https://github.com/tmchow) [](https://github.com/mgratch) [](https://github.com/xtao) [](https://github.com/JackyWay) [](https://github.com/j1philli) [](https://github.com/T5-AndyML) [](https://github.com/huohua-dev) [](https://github.com/imfing) [](https://github.com/RandyVentures) [](https://github.com/marcodd23)
|
||||
|
||||
[](https://github.com/Iamadig) [](https://github.com/humanwritten) [](https://github.com/robaxelsen) [](https://github.com/prathamdby) [](https://github.com/0oAstro) [](https://github.com/aaronn) [](https://github.com/afern247) [](https://github.com/Asleep123) [](https://github.com/dantelex) [](https://github.com/fcatuhe)
|
||||
|
||||
[](https://github.com/gtsifrikas) [](https://github.com/hrdwdmrbl) [](https://github.com/hugobarauna) [](https://github.com/jayhickey) [](https://github.com/jiulingyun) [](https://github.com/jdrhyne) [](https://github.com/jverdi) [](https://github.com/kitze) [](https://github.com/loukotal) [](https://github.com/minghinmatthewlam)
|
||||
|
||||
[](https://github.com/MSch) [](https://github.com/odrobnik) [](https://github.com/oswalpalash) [](https://github.com/ratulsarna) [](https://github.com/reeltimeapps) [](https://github.com/snopoke) [](https://github.com/sreekaransrinath) [](https://github.com/timkrase)
|
||||
|
||||
<!-- clawtributors:end -->
|
||||
|
||||
@@ -65,8 +65,8 @@ android {
|
||||
applicationId = "ai.openclaw.app"
|
||||
minSdk = 31
|
||||
targetSdk = 36
|
||||
versionCode = 2026041690
|
||||
versionName = "2026.4.16"
|
||||
versionCode = 2026041902
|
||||
versionName = "2026.4.19-beta.2"
|
||||
ndk {
|
||||
// Support all major ABIs — native libs are tiny (~47 KB per ABI)
|
||||
abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
# OpenClaw iOS Changelog
|
||||
|
||||
## 2026.4.16 - 2026-04-17
|
||||
## 2026.4.19 - 2026-04-19
|
||||
|
||||
Maintenance update for the current OpenClaw beta release.
|
||||
|
||||
## 2026.4.18 - 2026-04-18
|
||||
|
||||
Maintenance update for the current OpenClaw release.
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
// Source of truth: apps/ios/version.json
|
||||
// Generated by scripts/ios-sync-versioning.ts.
|
||||
|
||||
OPENCLAW_IOS_VERSION = 2026.4.16
|
||||
OPENCLAW_MARKETING_VERSION = 2026.4.16
|
||||
OPENCLAW_IOS_VERSION = 2026.4.19
|
||||
OPENCLAW_MARKETING_VERSION = 2026.4.19
|
||||
OPENCLAW_BUILD_VERSION = 1
|
||||
|
||||
#include? "../build/Version.xcconfig"
|
||||
|
||||
@@ -1 +1 @@
|
||||
Maintenance update for the current OpenClaw release.
|
||||
Maintenance update for the current OpenClaw beta release.
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"version": "2026.4.16"
|
||||
"version": "2026.4.19"
|
||||
}
|
||||
|
||||
@@ -3,6 +3,12 @@ import Foundation
|
||||
enum CommandResolver {
|
||||
private static let projectRootDefaultsKey = "openclaw.gatewayProjectRootPath"
|
||||
private static let helperName = "openclaw"
|
||||
static let strictHostKeyCheckingSSHOptions = [
|
||||
"-o", "StrictHostKeyChecking=yes",
|
||||
]
|
||||
static let updateHostKeysSSHOptions = [
|
||||
"-o", "UpdateHostKeys=yes",
|
||||
]
|
||||
|
||||
static func gatewayEntrypoint(in root: URL) -> String? {
|
||||
let distEntry = root.appendingPathComponent("dist/index.js").path
|
||||
@@ -397,9 +403,7 @@ enum CommandResolver {
|
||||
"""
|
||||
let options: [String] = [
|
||||
"-o", "BatchMode=yes",
|
||||
"-o", "StrictHostKeyChecking=accept-new",
|
||||
"-o", "UpdateHostKeys=yes",
|
||||
]
|
||||
] + self.strictHostKeyCheckingSSHOptions + self.updateHostKeysSSHOptions
|
||||
let args = self.sshArguments(
|
||||
target: parsed,
|
||||
identity: settings.identity,
|
||||
|
||||
@@ -22,7 +22,21 @@ enum ExecApprovalCommandDisplaySanitizer {
|
||||
}
|
||||
|
||||
private static func shouldEscape(_ scalar: UnicodeScalar) -> Bool {
|
||||
scalar.properties.generalCategory == .format || self.invisibleCodePoints.contains(scalar.value)
|
||||
let category = scalar.properties.generalCategory
|
||||
if category == .control
|
||||
|| category == .format
|
||||
|| category == .lineSeparator
|
||||
|| category == .paragraphSeparator
|
||||
{
|
||||
return true
|
||||
}
|
||||
// Escape non-ASCII space separators (NBSP, narrow NBSP, ideographic space, etc.) so
|
||||
// attackers cannot spoof token boundaries in the approval UI with spaces that render
|
||||
// like a plain space but are handled differently by shells/parsers.
|
||||
if category == .spaceSeparator, scalar.value != 0x20 {
|
||||
return true
|
||||
}
|
||||
return self.invisibleCodePoints.contains(scalar.value)
|
||||
}
|
||||
|
||||
private static func escape(_ scalar: UnicodeScalar) -> String {
|
||||
|
||||
@@ -146,6 +146,7 @@ final class MacNodeModeCoordinator {
|
||||
OpenClawCanvasA2UICommand.push.rawValue,
|
||||
OpenClawCanvasA2UICommand.pushJSONL.rawValue,
|
||||
OpenClawCanvasA2UICommand.reset.rawValue,
|
||||
MacNodeScreenCommand.snapshot.rawValue,
|
||||
MacNodeScreenCommand.record.rawValue,
|
||||
OpenClawSystemCommand.notify.rawValue,
|
||||
OpenClawSystemCommand.which.rawValue,
|
||||
|
||||
@@ -63,6 +63,8 @@ actor MacNodeRuntime {
|
||||
return try await self.handleCameraInvoke(req)
|
||||
case OpenClawLocationCommand.get.rawValue:
|
||||
return try await self.handleLocationInvoke(req)
|
||||
case MacNodeScreenCommand.snapshot.rawValue:
|
||||
return try await self.handleScreenSnapshotInvoke(req)
|
||||
case MacNodeScreenCommand.record.rawValue:
|
||||
return try await self.handleScreenRecordInvoke(req)
|
||||
case OpenClawSystemCommand.run.rawValue:
|
||||
@@ -352,6 +354,34 @@ actor MacNodeRuntime {
|
||||
return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: payload)
|
||||
}
|
||||
|
||||
private func handleScreenSnapshotInvoke(_ req: BridgeInvokeRequest) async throws -> BridgeInvokeResponse {
|
||||
let params = (try? Self.decodeParams(MacNodeScreenSnapshotParams.self, from: req.paramsJSON)) ??
|
||||
MacNodeScreenSnapshotParams()
|
||||
let services = await self.mainActorServices()
|
||||
let capturedAtMs = Int64(Date().timeIntervalSince1970 * 1000)
|
||||
let res = try await services.snapshotScreen(
|
||||
screenIndex: params.screenIndex,
|
||||
maxWidth: params.maxWidth,
|
||||
quality: params.quality,
|
||||
format: params.format)
|
||||
struct ScreenSnapshotPayload: Encodable {
|
||||
var format: String
|
||||
var base64: String
|
||||
var width: Int
|
||||
var height: Int
|
||||
var screenIndex: Int?
|
||||
var capturedAtMs: Int64
|
||||
}
|
||||
let payload = try Self.encodePayload(ScreenSnapshotPayload(
|
||||
format: res.format.rawValue,
|
||||
base64: res.data.base64EncodedString(),
|
||||
width: res.width,
|
||||
height: res.height,
|
||||
screenIndex: params.screenIndex,
|
||||
capturedAtMs: capturedAtMs))
|
||||
return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: payload)
|
||||
}
|
||||
|
||||
private func mainActorServices() async -> any MacNodeRuntimeMainActorServices {
|
||||
if let cachedMainActorServices { return cachedMainActorServices }
|
||||
let services = await self.makeMainActorServices()
|
||||
|
||||
@@ -4,6 +4,13 @@ import OpenClawKit
|
||||
|
||||
@MainActor
|
||||
protocol MacNodeRuntimeMainActorServices: Sendable {
|
||||
func snapshotScreen(
|
||||
screenIndex: Int?,
|
||||
maxWidth: Int?,
|
||||
quality: Double?,
|
||||
format: OpenClawScreenSnapshotFormat?) async throws
|
||||
-> (data: Data, format: OpenClawScreenSnapshotFormat, width: Int, height: Int)
|
||||
|
||||
func recordScreen(
|
||||
screenIndex: Int?,
|
||||
durationMs: Int?,
|
||||
@@ -21,9 +28,24 @@ protocol MacNodeRuntimeMainActorServices: Sendable {
|
||||
|
||||
@MainActor
|
||||
final class LiveMacNodeRuntimeMainActorServices: MacNodeRuntimeMainActorServices, @unchecked Sendable {
|
||||
private let screenSnapshotter = ScreenSnapshotService()
|
||||
private let screenRecorder = ScreenRecordService()
|
||||
private let locationService = MacNodeLocationService()
|
||||
|
||||
func snapshotScreen(
|
||||
screenIndex: Int?,
|
||||
maxWidth: Int?,
|
||||
quality: Double?,
|
||||
format: OpenClawScreenSnapshotFormat?) async throws
|
||||
-> (data: Data, format: OpenClawScreenSnapshotFormat, width: Int, height: Int)
|
||||
{
|
||||
try await self.screenSnapshotter.snapshot(
|
||||
screenIndex: screenIndex,
|
||||
maxWidth: maxWidth,
|
||||
quality: quality,
|
||||
format: format)
|
||||
}
|
||||
|
||||
func recordScreen(
|
||||
screenIndex: Int?,
|
||||
durationMs: Int?,
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
import Foundation
|
||||
import OpenClawKit
|
||||
|
||||
enum MacNodeScreenCommand: String, Codable {
|
||||
case snapshot = "screen.snapshot"
|
||||
case record = "screen.record"
|
||||
}
|
||||
|
||||
struct MacNodeScreenSnapshotParams: Codable, Equatable {
|
||||
var screenIndex: Int?
|
||||
var maxWidth: Int?
|
||||
var quality: Double?
|
||||
var format: OpenClawScreenSnapshotFormat?
|
||||
}
|
||||
|
||||
struct MacNodeScreenRecordParams: Codable, Equatable {
|
||||
var screenIndex: Int?
|
||||
var durationMs: Int?
|
||||
|
||||
@@ -483,8 +483,7 @@ final class NodePairingApprovalPrompter {
|
||||
"-o", "ConnectTimeout=5",
|
||||
"-o", "NumberOfPasswordPrompts=0",
|
||||
"-o", "PreferredAuthentications=publickey",
|
||||
"-o", "StrictHostKeyChecking=accept-new",
|
||||
]
|
||||
] + CommandResolver.strictHostKeyCheckingSSHOptions
|
||||
guard let target = CommandResolver.makeSSHTarget(user: user, host: host, port: port) else {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -200,9 +200,7 @@ enum RemoteGatewayProbe {
|
||||
let options = [
|
||||
"-o", "BatchMode=yes",
|
||||
"-o", "ConnectTimeout=5",
|
||||
"-o", "StrictHostKeyChecking=accept-new",
|
||||
"-o", "UpdateHostKeys=yes",
|
||||
]
|
||||
] + CommandResolver.strictHostKeyCheckingSSHOptions + CommandResolver.updateHostKeysSSHOptions
|
||||
let args = CommandResolver.sshArguments(
|
||||
target: parsed,
|
||||
identity: identity,
|
||||
|
||||
@@ -73,14 +73,12 @@ final class RemotePortTunnel {
|
||||
let options: [String] = [
|
||||
"-o", "BatchMode=yes",
|
||||
"-o", "ExitOnForwardFailure=yes",
|
||||
"-o", "StrictHostKeyChecking=accept-new",
|
||||
"-o", "UpdateHostKeys=yes",
|
||||
"-o", "ServerAliveInterval=15",
|
||||
"-o", "ServerAliveCountMax=3",
|
||||
"-o", "TCPKeepAlive=yes",
|
||||
"-N",
|
||||
"-L", "\(localPort):127.0.0.1:\(resolvedRemotePort)",
|
||||
]
|
||||
] + CommandResolver.strictHostKeyCheckingSSHOptions + CommandResolver.updateHostKeysSSHOptions
|
||||
let identity = settings.identity.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let args = CommandResolver.sshArguments(
|
||||
target: parsed,
|
||||
|
||||
@@ -15,9 +15,9 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>2026.4.16</string>
|
||||
<string>2026.4.19-beta.2</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>2026041690</string>
|
||||
<string>2026041902</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>OpenClaw</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
|
||||
109
apps/macos/Sources/OpenClaw/ScreenSnapshotService.swift
Normal file
109
apps/macos/Sources/OpenClaw/ScreenSnapshotService.swift
Normal file
@@ -0,0 +1,109 @@
|
||||
import AppKit
|
||||
import Foundation
|
||||
import OpenClawKit
|
||||
@preconcurrency import ScreenCaptureKit
|
||||
|
||||
@MainActor
|
||||
final class ScreenSnapshotService {
|
||||
enum ScreenSnapshotError: LocalizedError {
|
||||
case noDisplays
|
||||
case invalidScreenIndex(Int)
|
||||
case captureFailed(String)
|
||||
case encodeFailed(String)
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .noDisplays:
|
||||
"No displays available for screen snapshot"
|
||||
case let .invalidScreenIndex(idx):
|
||||
"Invalid screen index \(idx)"
|
||||
case let .captureFailed(message):
|
||||
message
|
||||
case let .encodeFailed(message):
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func snapshot(
|
||||
screenIndex: Int?,
|
||||
maxWidth: Int?,
|
||||
quality: Double?,
|
||||
format: OpenClawScreenSnapshotFormat?) async throws
|
||||
-> (data: Data, format: OpenClawScreenSnapshotFormat, width: Int, height: Int)
|
||||
{
|
||||
let format = format ?? .jpeg
|
||||
let normalized = Self.normalize(maxWidth: maxWidth, quality: quality, format: format)
|
||||
|
||||
let content = try await SCShareableContent.current
|
||||
let displays = content.displays.sorted { $0.displayID < $1.displayID }
|
||||
guard !displays.isEmpty else {
|
||||
throw ScreenSnapshotError.noDisplays
|
||||
}
|
||||
|
||||
let idx = screenIndex ?? 0
|
||||
guard idx >= 0, idx < displays.count else {
|
||||
throw ScreenSnapshotError.invalidScreenIndex(idx)
|
||||
}
|
||||
let display = displays[idx]
|
||||
|
||||
let filter = SCContentFilter(display: display, excludingWindows: [])
|
||||
let config = SCStreamConfiguration()
|
||||
let targetSize = Self.targetSize(
|
||||
width: display.width,
|
||||
height: display.height,
|
||||
maxWidth: normalized.maxWidth)
|
||||
config.width = targetSize.width
|
||||
config.height = targetSize.height
|
||||
config.showsCursor = true
|
||||
|
||||
let cgImage: CGImage
|
||||
do {
|
||||
cgImage = try await SCScreenshotManager.captureImage(
|
||||
contentFilter: filter,
|
||||
configuration: config)
|
||||
} catch {
|
||||
throw ScreenSnapshotError.captureFailed(error.localizedDescription)
|
||||
}
|
||||
|
||||
let bitmap = NSBitmapImageRep(cgImage: cgImage)
|
||||
let data: Data
|
||||
switch format {
|
||||
case .png:
|
||||
guard let encoded = bitmap.representation(using: .png, properties: [:]) else {
|
||||
throw ScreenSnapshotError.encodeFailed("png encode failed")
|
||||
}
|
||||
data = encoded
|
||||
case .jpeg:
|
||||
guard let encoded = bitmap.representation(
|
||||
using: .jpeg,
|
||||
properties: [.compressionFactor: normalized.quality])
|
||||
else {
|
||||
throw ScreenSnapshotError.encodeFailed("jpeg encode failed")
|
||||
}
|
||||
data = encoded
|
||||
}
|
||||
|
||||
return (data: data, format: format, width: cgImage.width, height: cgImage.height)
|
||||
}
|
||||
|
||||
private static func normalize(
|
||||
maxWidth: Int?,
|
||||
quality: Double?,
|
||||
format: OpenClawScreenSnapshotFormat)
|
||||
-> (maxWidth: Int, quality: Double)
|
||||
{
|
||||
let resolvedMaxWidth = maxWidth.flatMap { $0 > 0 ? $0 : nil } ?? (format == .png ? 900 : 1600)
|
||||
let resolvedQuality = min(1.0, max(0.05, quality ?? 0.72))
|
||||
return (maxWidth: resolvedMaxWidth, quality: resolvedQuality)
|
||||
}
|
||||
|
||||
private static func targetSize(width: Int, height: Int, maxWidth: Int) -> (width: Int, height: Int) {
|
||||
guard width > 0, height > 0, width > maxWidth else {
|
||||
return (width: width, height: height)
|
||||
}
|
||||
let scale = Double(maxWidth) / Double(width)
|
||||
let targetHeight = max(1, Int((Double(height) * scale).rounded()))
|
||||
return (width: maxWidth, height: targetHeight)
|
||||
}
|
||||
}
|
||||
@@ -164,6 +164,9 @@ import Testing
|
||||
} else {
|
||||
#expect(Bool(false))
|
||||
}
|
||||
#expect(cmd.contains("StrictHostKeyChecking=yes"))
|
||||
#expect(!cmd.contains("StrictHostKeyChecking=accept-new"))
|
||||
#expect(cmd.contains("UpdateHostKeys=yes"))
|
||||
#expect(cmd.contains("-i"))
|
||||
#expect(cmd.contains("/tmp/id_ed25519"))
|
||||
if let script = cmd.last {
|
||||
|
||||
@@ -9,4 +9,37 @@ struct ExecApprovalCommandDisplaySanitizerTests {
|
||||
ExecApprovalCommandDisplaySanitizer.sanitize(input) ==
|
||||
"date\\u{200B}\\u{3164}\\u{FFA0}\\u{115F}\\u{1160}가")
|
||||
}
|
||||
|
||||
@Test func `escapes control characters used to spoof line breaks`() {
|
||||
let input = "echo safe\n\rcurl https://example.test"
|
||||
#expect(
|
||||
ExecApprovalCommandDisplaySanitizer.sanitize(input) ==
|
||||
"echo safe\\u{A}\\u{D}curl https://example.test")
|
||||
}
|
||||
|
||||
@Test func `escapes Unicode line and paragraph separators`() {
|
||||
let lineInput = "echo ok\u{2028}curl https://example.test"
|
||||
#expect(
|
||||
ExecApprovalCommandDisplaySanitizer.sanitize(lineInput) ==
|
||||
"echo ok\\u{2028}curl https://example.test")
|
||||
let paragraphInput = "echo ok\u{2029}curl https://example.test"
|
||||
#expect(
|
||||
ExecApprovalCommandDisplaySanitizer.sanitize(paragraphInput) ==
|
||||
"echo ok\\u{2029}curl https://example.test")
|
||||
}
|
||||
|
||||
@Test func `escapes non-ASCII Unicode space separators while preserving ASCII space`() {
|
||||
let nbspInput = "echo ok\u{00A0}curl"
|
||||
#expect(
|
||||
ExecApprovalCommandDisplaySanitizer.sanitize(nbspInput) == "echo ok\\u{A0}curl")
|
||||
let narrowNbspInput = "echo ok\u{202F}curl"
|
||||
#expect(
|
||||
ExecApprovalCommandDisplaySanitizer.sanitize(narrowNbspInput) == "echo ok\\u{202F}curl")
|
||||
let ideographicSpaceInput = "echo ok\u{3000}curl"
|
||||
#expect(
|
||||
ExecApprovalCommandDisplaySanitizer.sanitize(ideographicSpaceInput) ==
|
||||
"echo ok\\u{3000}curl")
|
||||
let asciiSpaceInput = "echo ok curl"
|
||||
#expect(ExecApprovalCommandDisplaySanitizer.sanitize(asciiSpaceInput) == "echo ok curl")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,6 +78,19 @@ struct MacNodeRuntimeTests {
|
||||
@Test func `handle invoke screen record uses injected services`() async throws {
|
||||
@MainActor
|
||||
final class FakeMainActorServices: MacNodeRuntimeMainActorServices, @unchecked Sendable {
|
||||
func snapshotScreen(
|
||||
screenIndex: Int?,
|
||||
maxWidth: Int?,
|
||||
quality: Double?,
|
||||
format: OpenClawScreenSnapshotFormat?) async throws
|
||||
-> (data: Data, format: OpenClawScreenSnapshotFormat, width: Int, height: Int)
|
||||
{
|
||||
_ = screenIndex
|
||||
_ = maxWidth
|
||||
_ = quality
|
||||
return (Data("snapshot".utf8), format ?? .jpeg, 640, 360)
|
||||
}
|
||||
|
||||
func recordScreen(
|
||||
screenIndex: Int?,
|
||||
durationMs: Int?,
|
||||
@@ -127,6 +140,94 @@ struct MacNodeRuntimeTests {
|
||||
#expect(!payload.base64.isEmpty)
|
||||
}
|
||||
|
||||
@Test func `handle invoke screen snapshot uses injected services`() async throws {
|
||||
@MainActor
|
||||
final class FakeMainActorServices: MacNodeRuntimeMainActorServices, @unchecked Sendable {
|
||||
var snapshotCalledAtMs: Int64?
|
||||
|
||||
func snapshotScreen(
|
||||
screenIndex: Int?,
|
||||
maxWidth: Int?,
|
||||
quality: Double?,
|
||||
format: OpenClawScreenSnapshotFormat?) async throws
|
||||
-> (data: Data, format: OpenClawScreenSnapshotFormat, width: Int, height: Int)
|
||||
{
|
||||
self.snapshotCalledAtMs = Int64(Date().timeIntervalSince1970 * 1000)
|
||||
#expect(screenIndex == 0)
|
||||
#expect(maxWidth == 800)
|
||||
#expect(quality == 0.5)
|
||||
return (Data("ok".utf8), format ?? .jpeg, 800, 450)
|
||||
}
|
||||
|
||||
func recordScreen(
|
||||
screenIndex: Int?,
|
||||
durationMs: Int?,
|
||||
fps: Double?,
|
||||
includeAudio: Bool?,
|
||||
outPath: String?) async throws -> (path: String, hasAudio: Bool)
|
||||
{
|
||||
let url = FileManager().temporaryDirectory
|
||||
.appendingPathComponent("openclaw-test-screen-record-\(UUID().uuidString).mp4")
|
||||
try Data("ok".utf8).write(to: url)
|
||||
return (path: url.path, hasAudio: false)
|
||||
}
|
||||
|
||||
func locationAuthorizationStatus() -> CLAuthorizationStatus {
|
||||
.authorizedAlways
|
||||
}
|
||||
|
||||
func locationAccuracyAuthorization() -> CLAccuracyAuthorization {
|
||||
.fullAccuracy
|
||||
}
|
||||
|
||||
func currentLocation(
|
||||
desiredAccuracy: OpenClawLocationAccuracy,
|
||||
maxAgeMs: Int?,
|
||||
timeoutMs: Int?) async throws -> CLLocation
|
||||
{
|
||||
_ = desiredAccuracy
|
||||
_ = maxAgeMs
|
||||
_ = timeoutMs
|
||||
return CLLocation(latitude: 0, longitude: 0)
|
||||
}
|
||||
}
|
||||
|
||||
let services = await MainActor.run { FakeMainActorServices() }
|
||||
let runtime = MacNodeRuntime(makeMainActorServices: { services })
|
||||
|
||||
let params = MacNodeScreenSnapshotParams(
|
||||
screenIndex: 0,
|
||||
maxWidth: 800,
|
||||
quality: 0.5,
|
||||
format: .jpeg)
|
||||
let json = try String(data: JSONEncoder().encode(params), encoding: .utf8)
|
||||
let response = await runtime.handleInvoke(
|
||||
BridgeInvokeRequest(
|
||||
id: "req-screen-snapshot",
|
||||
command: MacNodeScreenCommand.snapshot.rawValue,
|
||||
paramsJSON: json))
|
||||
#expect(response.ok == true)
|
||||
let payloadJSON = try #require(response.payloadJSON)
|
||||
|
||||
struct Payload: Decodable {
|
||||
var format: String
|
||||
var base64: String
|
||||
var width: Int
|
||||
var height: Int
|
||||
var capturedAtMs: Int64
|
||||
}
|
||||
|
||||
let payload = try JSONDecoder().decode(Payload.self, from: Data(payloadJSON.utf8))
|
||||
#expect(payload.format == "jpeg")
|
||||
#expect(payload.base64 == Data("ok".utf8).base64EncodedString())
|
||||
#expect(payload.width == 800)
|
||||
#expect(payload.height == 450)
|
||||
#expect(payload.capturedAtMs > 0)
|
||||
let snapshotCalledAtMs = await MainActor.run { services.snapshotCalledAtMs }
|
||||
#expect(snapshotCalledAtMs != nil)
|
||||
#expect(payload.capturedAtMs <= snapshotCalledAtMs!)
|
||||
}
|
||||
|
||||
@Test func `handle invoke browser proxy uses injected request`() async {
|
||||
let runtime = MacNodeRuntime(browserProxyRequest: { paramsJSON in
|
||||
#expect(paramsJSON?.contains("/tabs") == true)
|
||||
|
||||
@@ -444,34 +444,18 @@ private struct ChatComposerTextView: NSViewRepresentable {
|
||||
func makeCoordinator() -> Coordinator { Coordinator(self) }
|
||||
|
||||
func makeNSView(context: Context) -> NSScrollView {
|
||||
let textView = ChatComposerNSTextView()
|
||||
textView.delegate = context.coordinator
|
||||
textView.drawsBackground = false
|
||||
textView.isRichText = false
|
||||
textView.isAutomaticQuoteSubstitutionEnabled = false
|
||||
textView.isAutomaticTextReplacementEnabled = false
|
||||
textView.isAutomaticDashSubstitutionEnabled = false
|
||||
textView.isAutomaticSpellingCorrectionEnabled = false
|
||||
textView.font = .systemFont(ofSize: 14, weight: .regular)
|
||||
textView.textContainer?.lineBreakMode = .byWordWrapping
|
||||
textView.textContainer?.lineFragmentPadding = 0
|
||||
textView.textContainerInset = NSSize(width: 2, height: 4)
|
||||
textView.focusRingType = .none
|
||||
let textView = ChatComposerTextViewFactory.makeConfiguredTextView()
|
||||
guard let composerTextView = textView as? ChatComposerNSTextView else {
|
||||
preconditionFailure("ChatComposerTextViewFactory must return ChatComposerNSTextView")
|
||||
}
|
||||
composerTextView.delegate = context.coordinator
|
||||
|
||||
textView.minSize = .zero
|
||||
textView.maxSize = NSSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude)
|
||||
textView.isHorizontallyResizable = false
|
||||
textView.isVerticallyResizable = true
|
||||
textView.autoresizingMask = [.width]
|
||||
textView.textContainer?.containerSize = NSSize(width: 0, height: CGFloat.greatestFiniteMagnitude)
|
||||
textView.textContainer?.widthTracksTextView = true
|
||||
|
||||
textView.string = self.text
|
||||
textView.onSend = { [weak textView] in
|
||||
textView?.window?.makeFirstResponder(nil)
|
||||
composerTextView.string = self.text
|
||||
composerTextView.onSend = { [weak composerTextView] in
|
||||
composerTextView?.window?.makeFirstResponder(nil)
|
||||
self.onSend()
|
||||
}
|
||||
textView.onPasteImageAttachment = self.onPasteImageAttachment
|
||||
composerTextView.onPasteImageAttachment = self.onPasteImageAttachment
|
||||
|
||||
let scroll = NSScrollView()
|
||||
scroll.drawsBackground = false
|
||||
@@ -522,6 +506,34 @@ private struct ChatComposerTextView: NSViewRepresentable {
|
||||
}
|
||||
}
|
||||
|
||||
enum ChatComposerTextViewFactory {
|
||||
// Internal for @testable import coverage of composer text view defaults.
|
||||
@MainActor
|
||||
static func makeConfiguredTextView() -> NSTextView {
|
||||
let textView = ChatComposerNSTextView()
|
||||
textView.drawsBackground = false
|
||||
textView.isRichText = false
|
||||
textView.isAutomaticQuoteSubstitutionEnabled = false
|
||||
textView.isAutomaticTextReplacementEnabled = false
|
||||
textView.isAutomaticDashSubstitutionEnabled = false
|
||||
textView.isAutomaticSpellingCorrectionEnabled = false
|
||||
textView.font = .systemFont(ofSize: 14, weight: .regular)
|
||||
textView.textContainer?.lineBreakMode = .byWordWrapping
|
||||
textView.textContainer?.lineFragmentPadding = 0
|
||||
textView.textContainerInset = NSSize(width: 2, height: 4)
|
||||
textView.focusRingType = .none
|
||||
textView.allowsUndo = true
|
||||
textView.minSize = .zero
|
||||
textView.maxSize = NSSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude)
|
||||
textView.isHorizontallyResizable = false
|
||||
textView.isVerticallyResizable = true
|
||||
textView.autoresizingMask = [.width]
|
||||
textView.textContainer?.containerSize = NSSize(width: 0, height: CGFloat.greatestFiniteMagnitude)
|
||||
textView.textContainer?.widthTracksTextView = true
|
||||
return textView
|
||||
}
|
||||
}
|
||||
|
||||
private final class ChatComposerNSTextView: NSTextView {
|
||||
var onSend: (() -> Void)?
|
||||
var onPasteImageAttachment: ((_ data: Data, _ fileName: String, _ mimeType: String) -> Void)?
|
||||
|
||||
@@ -1,9 +1,34 @@
|
||||
import Foundation
|
||||
|
||||
public enum OpenClawScreenCommand: String, Codable, Sendable {
|
||||
case snapshot = "screen.snapshot"
|
||||
case record = "screen.record"
|
||||
}
|
||||
|
||||
public enum OpenClawScreenSnapshotFormat: String, Codable, Sendable {
|
||||
case jpeg
|
||||
case png
|
||||
}
|
||||
|
||||
public struct OpenClawScreenSnapshotParams: Codable, Sendable, Equatable {
|
||||
public var screenIndex: Int?
|
||||
public var maxWidth: Int?
|
||||
public var quality: Double?
|
||||
public var format: OpenClawScreenSnapshotFormat?
|
||||
|
||||
public init(
|
||||
screenIndex: Int? = nil,
|
||||
maxWidth: Int? = nil,
|
||||
quality: Double? = nil,
|
||||
format: OpenClawScreenSnapshotFormat? = nil)
|
||||
{
|
||||
self.screenIndex = screenIndex
|
||||
self.maxWidth = maxWidth
|
||||
self.quality = quality
|
||||
self.format = format
|
||||
}
|
||||
}
|
||||
|
||||
public struct OpenClawScreenRecordParams: Codable, Sendable, Equatable {
|
||||
public var screenIndex: Int?
|
||||
public var durationMs: Int?
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
#if os(macOS)
|
||||
import AppKit
|
||||
import Testing
|
||||
@testable import OpenClawChatUI
|
||||
|
||||
@Suite
|
||||
@MainActor
|
||||
struct ChatComposerTextViewTests {
|
||||
@Test func configuredComposerTextViewEnablesUndo() {
|
||||
let textView = ChatComposerTextViewFactory.makeConfiguredTextView()
|
||||
|
||||
#expect(textView.allowsUndo)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -1,4 +1,4 @@
|
||||
3c87ac2fc4c234348eb88812d1904724d7492890498f101d953bc761da8fdead config-baseline.json
|
||||
eeed6fe659078632d9f95b3350b27103b4aba282d050ff38d3b0953a456d242d config-baseline.core.json
|
||||
99bb34fcf83ba6bb50a3fc11f170bd379bee5728b0938707fc39ebd7638e12eb config-baseline.channel.json
|
||||
5f5d4e850df6e9854a85b5d008236854ce185c707fdbb566efcf00f8c08b36e3 config-baseline.plugin.json
|
||||
889094f0a34a8a8a8b7672b846f4cbe41e273ebb6fd230f1955ec80c65339bef config-baseline.json
|
||||
10b7c57a6198526b846471e1bcda6e361c1f3db2e3b1cd24abd8bac11db56e16 config-baseline.core.json
|
||||
0982fc3d264047919333a57dfba1ba948e6639fb19659a400f947dfdd8b8d1de config-baseline.channel.json
|
||||
b695cb31b4c0cf1d31f842f2892e99cc3ff8d84263ae72b72977cae844b81d6e config-baseline.plugin.json
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
9683f324fae8f455f2b64d7e152a77009941e4c7558521bca2510d8bcf573af9 plugin-sdk-api-baseline.json
|
||||
097bf226e4e857e9296d0851852a2963c6263d176c4c470452d9a8efd36988e5 plugin-sdk-api-baseline.jsonl
|
||||
20db3f5afb93db334ad7456d26303c81b2a3eeaa5c1f8846a459eec72be20b96 plugin-sdk-api-baseline.json
|
||||
d02926e9facb3321a1018804d4c0370d9627963bee5e478942dda469e529c20b plugin-sdk-api-baseline.jsonl
|
||||
|
||||
@@ -59,6 +59,14 @@
|
||||
"source": "Feishu",
|
||||
"target": "Feishu"
|
||||
},
|
||||
{
|
||||
"source": "WeChat",
|
||||
"target": "微信"
|
||||
},
|
||||
{
|
||||
"source": "Weixin",
|
||||
"target": "微信"
|
||||
},
|
||||
{
|
||||
"source": "Mattermost",
|
||||
"target": "Mattermost"
|
||||
|
||||
@@ -34,7 +34,7 @@ Text is supported everywhere; media and reactions vary by channel.
|
||||
- [Twitch](/channels/twitch) — Twitch chat via IRC connection (bundled plugin).
|
||||
- [Voice Call](/plugins/voice-call) — Telephony via Plivo or Twilio (plugin, installed separately).
|
||||
- [WebChat](/web/webchat) — Gateway WebChat UI over WebSocket.
|
||||
- [WeChat](https://www.npmjs.com/package/@tencent-weixin/openclaw-weixin) — Tencent iLink Bot plugin via QR login; private chats only.
|
||||
- [WeChat](/channels/wechat) — Tencent iLink Bot plugin via QR login; private chats only (external plugin).
|
||||
- [WhatsApp](/channels/whatsapp) — Most popular; uses Baileys and requires QR pairing.
|
||||
- [Zalo](/channels/zalo) — Zalo Bot API; Vietnam's popular messenger (bundled plugin).
|
||||
- [Zalo Personal](/channels/zalouser) — Zalo personal account via QR login (bundled plugin).
|
||||
|
||||
168
docs/channels/wechat.md
Normal file
168
docs/channels/wechat.md
Normal file
@@ -0,0 +1,168 @@
|
||||
---
|
||||
summary: "WeChat channel setup through the external openclaw-weixin plugin"
|
||||
read_when:
|
||||
- You want to connect OpenClaw to WeChat or Weixin
|
||||
- You are installing or troubleshooting the openclaw-weixin channel plugin
|
||||
- You need to understand how external channel plugins run beside the Gateway
|
||||
title: "WeChat"
|
||||
---
|
||||
|
||||
# WeChat
|
||||
|
||||
OpenClaw connects to WeChat through Tencent's external
|
||||
`@tencent-weixin/openclaw-weixin` channel plugin.
|
||||
|
||||
Status: external plugin. Direct chats and media are supported. Group chats are not
|
||||
advertised by the current plugin capability metadata.
|
||||
|
||||
## Naming
|
||||
|
||||
- **WeChat** is the user-facing name in these docs.
|
||||
- **Weixin** is the name used by Tencent's package and by the plugin id.
|
||||
- `openclaw-weixin` is the OpenClaw channel id.
|
||||
- `@tencent-weixin/openclaw-weixin` is the npm package.
|
||||
|
||||
Use `openclaw-weixin` in CLI commands and config paths.
|
||||
|
||||
## How it works
|
||||
|
||||
The WeChat code does not live in the OpenClaw core repo. OpenClaw provides the
|
||||
generic channel plugin contract, and the external plugin provides the
|
||||
WeChat-specific runtime:
|
||||
|
||||
1. `openclaw plugins install` installs `@tencent-weixin/openclaw-weixin`.
|
||||
2. The Gateway discovers the plugin manifest and loads the plugin entrypoint.
|
||||
3. The plugin registers channel id `openclaw-weixin`.
|
||||
4. `openclaw channels login --channel openclaw-weixin` starts QR login.
|
||||
5. The plugin stores account credentials under the OpenClaw state directory.
|
||||
6. When the Gateway starts, the plugin starts its Weixin monitor for each
|
||||
configured account.
|
||||
7. Inbound WeChat messages are normalized through the channel contract, routed to
|
||||
the selected OpenClaw agent, and sent back through the plugin outbound path.
|
||||
|
||||
That separation matters: OpenClaw core should stay channel-agnostic. WeChat login,
|
||||
Tencent iLink API calls, media upload/download, context tokens, and account
|
||||
monitoring are owned by the external plugin.
|
||||
|
||||
## Install
|
||||
|
||||
Quick install:
|
||||
|
||||
```bash
|
||||
npx -y @tencent-weixin/openclaw-weixin-cli install
|
||||
```
|
||||
|
||||
Manual install:
|
||||
|
||||
```bash
|
||||
openclaw plugins install "@tencent-weixin/openclaw-weixin"
|
||||
openclaw config set plugins.entries.openclaw-weixin.enabled true
|
||||
```
|
||||
|
||||
Restart the Gateway after install:
|
||||
|
||||
```bash
|
||||
openclaw gateway restart
|
||||
```
|
||||
|
||||
## Login
|
||||
|
||||
Run QR login on the same machine that runs the Gateway:
|
||||
|
||||
```bash
|
||||
openclaw channels login --channel openclaw-weixin
|
||||
```
|
||||
|
||||
Scan the QR code with WeChat on your phone and confirm the login. The plugin saves
|
||||
the account token locally after a successful scan.
|
||||
|
||||
To add another WeChat account, run the same login command again. For multiple
|
||||
accounts, isolate direct-message sessions by account, channel, and sender:
|
||||
|
||||
```bash
|
||||
openclaw config set session.dmScope per-account-channel-peer
|
||||
```
|
||||
|
||||
## Access control
|
||||
|
||||
Direct messages use the normal OpenClaw pairing and allowlist model for channel
|
||||
plugins.
|
||||
|
||||
Approve new senders:
|
||||
|
||||
```bash
|
||||
openclaw pairing list openclaw-weixin
|
||||
openclaw pairing approve openclaw-weixin <CODE>
|
||||
```
|
||||
|
||||
For the full access-control model, see [Pairing](/channels/pairing).
|
||||
|
||||
## Compatibility
|
||||
|
||||
The plugin checks the host OpenClaw version at startup.
|
||||
|
||||
| Plugin line | OpenClaw version | npm tag |
|
||||
| ----------- | ----------------------- | -------- |
|
||||
| `2.x` | `>=2026.3.22` | `latest` |
|
||||
| `1.x` | `>=2026.1.0 <2026.3.22` | `legacy` |
|
||||
|
||||
If the plugin reports that your OpenClaw version is too old, either update
|
||||
OpenClaw or install the legacy plugin line:
|
||||
|
||||
```bash
|
||||
openclaw plugins install @tencent-weixin/openclaw-weixin@legacy
|
||||
```
|
||||
|
||||
## Sidecar process
|
||||
|
||||
The WeChat plugin can run helper work beside the Gateway while it monitors the
|
||||
Tencent iLink API. In issue #68451, that helper path exposed a bug in OpenClaw's
|
||||
generic stale-Gateway cleanup: a child process could try to clean up the parent
|
||||
Gateway process, causing restart loops under process managers such as systemd.
|
||||
|
||||
Current OpenClaw startup cleanup excludes the current process and its ancestors,
|
||||
so a channel helper must not kill the Gateway that launched it. This fix is
|
||||
generic; it is not a WeChat-specific path in core.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
Check install and status:
|
||||
|
||||
```bash
|
||||
openclaw plugins list
|
||||
openclaw channels status --probe
|
||||
openclaw --version
|
||||
```
|
||||
|
||||
If the channel shows as installed but does not connect, confirm that the plugin is
|
||||
enabled and restart:
|
||||
|
||||
```bash
|
||||
openclaw config set plugins.entries.openclaw-weixin.enabled true
|
||||
openclaw gateway restart
|
||||
```
|
||||
|
||||
If the Gateway restarts repeatedly after enabling WeChat, update both OpenClaw and
|
||||
the plugin:
|
||||
|
||||
```bash
|
||||
npm view @tencent-weixin/openclaw-weixin version
|
||||
openclaw plugins install "@tencent-weixin/openclaw-weixin" --force
|
||||
openclaw gateway restart
|
||||
```
|
||||
|
||||
Temporary disable:
|
||||
|
||||
```bash
|
||||
openclaw config set plugins.entries.openclaw-weixin.enabled false
|
||||
openclaw gateway restart
|
||||
```
|
||||
|
||||
## Related docs
|
||||
|
||||
- Channel overview: [Chat Channels](/channels)
|
||||
- Pairing: [Pairing](/channels/pairing)
|
||||
- Channel routing: [Channel Routing](/channels/channel-routing)
|
||||
- Plugin architecture: [Plugin Architecture](/plugins/architecture)
|
||||
- Channel plugin SDK: [Channel Plugin SDK](/plugins/sdk-channel-plugins)
|
||||
- External package: [@tencent-weixin/openclaw-weixin](https://www.npmjs.com/package/@tencent-weixin/openclaw-weixin)
|
||||
@@ -628,7 +628,7 @@ The most important fields are:
|
||||
| `config.thinking` | `"off" \| "minimal" \| "low" \| "medium" \| "high" \| "xhigh" \| "adaptive"` | Advanced thinking override for the blocking memory sub-agent; default `off` for speed |
|
||||
| `config.promptOverride` | `string` | Advanced full prompt replacement; not recommended for normal use |
|
||||
| `config.promptAppend` | `string` | Advanced extra instructions appended to the default or overridden prompt |
|
||||
| `config.timeoutMs` | `number` | Hard timeout for the blocking memory sub-agent |
|
||||
| `config.timeoutMs` | `number` | Hard timeout for the blocking memory sub-agent, capped at 120000 ms |
|
||||
| `config.maxSummaryChars` | `number` | Maximum total characters allowed in the active-memory summary |
|
||||
| `config.logging` | `boolean` | Emits active memory logs while tuning |
|
||||
| `config.persistTranscripts` | `boolean` | Keeps blocking memory sub-agent transcripts on disk instead of deleting temp files |
|
||||
|
||||
@@ -120,8 +120,8 @@ See [Memory](/concepts/memory) for the workflow and automatic memory flush.
|
||||
|
||||
If any bootstrap file is missing, OpenClaw injects a "missing file" marker into
|
||||
the session and continues. Large bootstrap files are truncated when injected;
|
||||
adjust limits with `agents.defaults.bootstrapMaxChars` (default: 20000) and
|
||||
`agents.defaults.bootstrapTotalMaxChars` (default: 150000).
|
||||
adjust limits with `agents.defaults.bootstrapMaxChars` (default: 12000) and
|
||||
`agents.defaults.bootstrapTotalMaxChars` (default: 60000).
|
||||
`openclaw setup` can recreate missing defaults without overwriting existing
|
||||
files.
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ Values vary by model, provider, tool policy, and what’s in your workspace.
|
||||
```
|
||||
🧠 Context breakdown
|
||||
Workspace: <workspaceDir>
|
||||
Bootstrap max/file: 20,000 chars
|
||||
Bootstrap max/file: 12,000 chars
|
||||
Sandbox: mode=non-main sandboxed=false
|
||||
System prompt (run): 38,412 chars (~9,603 tok) (Project Context 23,901 chars (~5,976 tok))
|
||||
|
||||
@@ -112,7 +112,7 @@ By default, OpenClaw injects a fixed set of workspace files (if present):
|
||||
- `HEARTBEAT.md`
|
||||
- `BOOTSTRAP.md` (first-run only)
|
||||
|
||||
Large files are truncated per-file using `agents.defaults.bootstrapMaxChars` (default `20000` chars). OpenClaw also enforces a total bootstrap injection cap across files with `agents.defaults.bootstrapTotalMaxChars` (default `150000` chars). `/context` shows **raw vs injected** sizes and whether truncation happened.
|
||||
Large files are truncated per-file using `agents.defaults.bootstrapMaxChars` (default `12000` chars). OpenClaw also enforces a total bootstrap injection cap across files with `agents.defaults.bootstrapTotalMaxChars` (default `60000` chars). `/context` shows **raw vs injected** sizes and whether truncation happened.
|
||||
|
||||
When truncation occurs, the runtime can inject an in-prompt warning block under Project Context. Configure this with `agents.defaults.bootstrapPromptTruncationWarning` (`off`, `once`, `always`; default `once`).
|
||||
|
||||
|
||||
@@ -120,7 +120,7 @@ can write back through the mounted workspace.
|
||||
Seed assets live in `qa/`:
|
||||
|
||||
- `qa/scenarios/index.md`
|
||||
- `qa/scenarios/*.md`
|
||||
- `qa/scenarios/<theme>/*.md`
|
||||
|
||||
These are intentionally in git so the QA plan is visible to both humans and the
|
||||
agent.
|
||||
@@ -129,6 +129,7 @@ agent.
|
||||
the source of truth for one test run and should define:
|
||||
|
||||
- scenario metadata
|
||||
- optional category, capability, lane, and risk metadata
|
||||
- docs and code refs
|
||||
- optional plugin requirements
|
||||
- optional gateway config patch
|
||||
@@ -139,6 +140,10 @@ and cross-cutting. For example, markdown scenarios can combine transport-side
|
||||
helpers with browser-side helpers that drive the embedded Control UI through the
|
||||
Gateway `browser.request` seam without adding a special-case runner.
|
||||
|
||||
Scenario files should be grouped by product capability rather than source tree
|
||||
folder. Keep scenario IDs stable when files move; use `docsRefs` and `codeRefs`
|
||||
for implementation traceability.
|
||||
|
||||
The baseline list should stay broad enough to cover:
|
||||
|
||||
- DM and channel chat
|
||||
|
||||
@@ -118,9 +118,9 @@ unexpectedly high context usage and more frequent compaction.
|
||||
> as a one-shot startup-context block for that first turn.
|
||||
|
||||
Large files are truncated with a marker. The max per-file size is controlled by
|
||||
`agents.defaults.bootstrapMaxChars` (default: 20000). Total injected bootstrap
|
||||
`agents.defaults.bootstrapMaxChars` (default: 12000). Total injected bootstrap
|
||||
content across files is capped by `agents.defaults.bootstrapTotalMaxChars`
|
||||
(default: 150000). Missing files inject a short missing-file marker. When truncation
|
||||
(default: 60000). Missing files inject a short missing-file marker. When truncation
|
||||
occurs, OpenClaw can inject a warning block in Project Context; control this with
|
||||
`agents.defaults.bootstrapPromptTruncationWarning` (`off`, `once`, `always`;
|
||||
default: `once`).
|
||||
|
||||
@@ -61,14 +61,14 @@ node --import tsx scripts/repro/tsx-name-repro.ts
|
||||
## Workarounds
|
||||
|
||||
- Use Bun for dev scripts (current temporary revert).
|
||||
- Use Node + tsc watch, then run compiled output:
|
||||
- Use `tsgo` for repo type checking, then run the built output:
|
||||
|
||||
```bash
|
||||
pnpm exec tsc --watch --preserveWatchOutput
|
||||
node --watch openclaw.mjs status
|
||||
pnpm tsgo
|
||||
node openclaw.mjs status
|
||||
```
|
||||
|
||||
- Confirmed locally: `pnpm exec tsc -p tsconfig.json` + `node openclaw.mjs status` works on Node 25.
|
||||
- Historical note: `tsc` was used here while debugging this Node/tsx issue, but repo type-check lanes now use `tsgo`.
|
||||
- Disable esbuild keepNames in the TS loader if possible (prevents `__name` helper insertion); tsx does not currently expose this.
|
||||
- Test Node LTS (22/24) with `tsx` to see if the issue is Node 25–specific.
|
||||
|
||||
|
||||
@@ -456,6 +456,14 @@
|
||||
"source": "/channels/grammy",
|
||||
"destination": "/channels/telegram"
|
||||
},
|
||||
{
|
||||
"source": "/channels/openclaw-weixin",
|
||||
"destination": "/channels/wechat"
|
||||
},
|
||||
{
|
||||
"source": "/channels/weixin",
|
||||
"destination": "/channels/wechat"
|
||||
},
|
||||
{
|
||||
"source": "/group-messages",
|
||||
"destination": "/channels/group-messages"
|
||||
@@ -1028,6 +1036,7 @@
|
||||
"channels/telegram",
|
||||
"channels/tlon",
|
||||
"channels/twitch",
|
||||
"channels/wechat",
|
||||
"channels/whatsapp",
|
||||
"channels/zalo",
|
||||
"channels/zalouser"
|
||||
|
||||
@@ -955,21 +955,21 @@ Controls when workspace bootstrap files are injected into the system prompt. Def
|
||||
|
||||
### `agents.defaults.bootstrapMaxChars`
|
||||
|
||||
Max characters per workspace bootstrap file before truncation. Default: `20000`.
|
||||
Max characters per workspace bootstrap file before truncation. Default: `12000`.
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: { defaults: { bootstrapMaxChars: 20000 } },
|
||||
agents: { defaults: { bootstrapMaxChars: 12000 } },
|
||||
}
|
||||
```
|
||||
|
||||
### `agents.defaults.bootstrapTotalMaxChars`
|
||||
|
||||
Max total characters injected across all workspace bootstrap files. Default: `150000`.
|
||||
Max total characters injected across all workspace bootstrap files. Default: `60000`.
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: { defaults: { bootstrapTotalMaxChars: 150000 } },
|
||||
agents: { defaults: { bootstrapTotalMaxChars: 60000 } },
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -89,7 +89,21 @@ Gateway → Client:
|
||||
```
|
||||
|
||||
`server`, `features`, `snapshot`, and `policy` are all required by the schema
|
||||
(`src/gateway/protocol/schema/frames.ts`). `auth` and `canvasHostUrl` are optional.
|
||||
(`src/gateway/protocol/schema/frames.ts`). `canvasHostUrl` is optional. `auth`
|
||||
reports the negotiated role/scopes when available, and includes `deviceToken`
|
||||
when the gateway issues one.
|
||||
|
||||
When no device token is issued, `hello-ok.auth` can still report the negotiated
|
||||
permissions:
|
||||
|
||||
```json
|
||||
{
|
||||
"auth": {
|
||||
"role": "operator",
|
||||
"scopes": ["operator.read", "operator.write"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
When a device token is issued, `hello-ok` also includes:
|
||||
|
||||
|
||||
@@ -213,7 +213,7 @@ The minimum adoption bar for a new channel is:
|
||||
4. Mount the runner as `openclaw qa <runner>` instead of registering a competing root command.
|
||||
Runner plugins should declare `qaRunners` in `openclaw.plugin.json` and export a matching `qaRunnerCliRegistrations` array from `runtime-api.ts`.
|
||||
Keep `runtime-api.ts` light; lazy CLI and runner execution should stay behind separate entrypoints.
|
||||
5. Author or adapt markdown scenarios under `qa/scenarios/`.
|
||||
5. Author or adapt markdown scenarios under the themed `qa/scenarios/` directories.
|
||||
6. Use the generic scenario helpers for new scenarios.
|
||||
7. Keep existing compatibility aliases working unless the repo is doing an intentional migration.
|
||||
|
||||
|
||||
@@ -213,18 +213,21 @@ For the generic Docker flow, see [Docker](/install/docker).
|
||||
|
||||
```bash
|
||||
OPENCLAW_IMAGE=openclaw:latest
|
||||
OPENCLAW_GATEWAY_TOKEN=change-me-now
|
||||
OPENCLAW_GATEWAY_TOKEN=
|
||||
OPENCLAW_GATEWAY_BIND=lan
|
||||
OPENCLAW_GATEWAY_PORT=18789
|
||||
|
||||
OPENCLAW_CONFIG_DIR=/home/$USER/.openclaw
|
||||
OPENCLAW_WORKSPACE_DIR=/home/$USER/.openclaw/workspace
|
||||
|
||||
GOG_KEYRING_PASSWORD=change-me-now
|
||||
GOG_KEYRING_PASSWORD=
|
||||
XDG_CONFIG_HOME=/home/node/.openclaw
|
||||
```
|
||||
|
||||
Generate strong secrets:
|
||||
Leave `OPENCLAW_GATEWAY_TOKEN` blank unless you explicitly want to
|
||||
manage it through `.env`; OpenClaw writes a random gateway token to
|
||||
config on first start. Generate a keyring password and paste it into
|
||||
`GOG_KEYRING_PASSWORD`:
|
||||
|
||||
```bash
|
||||
openssl rand -hex 32
|
||||
|
||||
@@ -134,18 +134,21 @@ For the generic Docker flow, see [Docker](/install/docker).
|
||||
|
||||
```bash
|
||||
OPENCLAW_IMAGE=openclaw:latest
|
||||
OPENCLAW_GATEWAY_TOKEN=change-me-now
|
||||
OPENCLAW_GATEWAY_TOKEN=
|
||||
OPENCLAW_GATEWAY_BIND=lan
|
||||
OPENCLAW_GATEWAY_PORT=18789
|
||||
|
||||
OPENCLAW_CONFIG_DIR=/root/.openclaw
|
||||
OPENCLAW_WORKSPACE_DIR=/root/.openclaw/workspace
|
||||
|
||||
GOG_KEYRING_PASSWORD=change-me-now
|
||||
GOG_KEYRING_PASSWORD=
|
||||
XDG_CONFIG_HOME=/home/node/.openclaw
|
||||
```
|
||||
|
||||
Generate strong secrets:
|
||||
Leave `OPENCLAW_GATEWAY_TOKEN` blank unless you explicitly want to
|
||||
manage it through `.env`; OpenClaw writes a random gateway token to
|
||||
config on first start. Generate a keyring password and paste it into
|
||||
`GOG_KEYRING_PASSWORD`:
|
||||
|
||||
```bash
|
||||
openssl rand -hex 32
|
||||
|
||||
@@ -55,7 +55,7 @@ The macOS app presents itself as a node. Common commands:
|
||||
|
||||
- Canvas: `canvas.present`, `canvas.navigate`, `canvas.eval`, `canvas.snapshot`, `canvas.a2ui.*`
|
||||
- Camera: `camera.snap`, `camera.clip`
|
||||
- Screen: `screen.record`
|
||||
- Screen: `screen.snapshot`, `screen.record`
|
||||
- System: `system.run`, `system.notify`
|
||||
|
||||
The node reports a `permissions` map so agents can decide what’s allowed.
|
||||
|
||||
@@ -95,7 +95,14 @@ Those belong in your plugin code and `package.json`.
|
||||
"modelSupport": {
|
||||
"modelPrefixes": ["router-"]
|
||||
},
|
||||
"providerEndpoints": [
|
||||
{
|
||||
"endpointClass": "xai-native",
|
||||
"hosts": ["api.x.ai"]
|
||||
}
|
||||
],
|
||||
"cliBackends": ["openrouter-cli"],
|
||||
"syntheticAuthRefs": ["openrouter-cli"],
|
||||
"providerAuthEnvVars": {
|
||||
"openrouter": ["OPENROUTER_API_KEY"]
|
||||
},
|
||||
@@ -152,7 +159,10 @@ Those belong in your plugin code and `package.json`.
|
||||
| `channels` | No | `string[]` | Channel ids owned by this plugin. Used for discovery and config validation. |
|
||||
| `providers` | No | `string[]` | Provider ids owned by this plugin. |
|
||||
| `modelSupport` | No | `object` | Manifest-owned shorthand model-family metadata used to auto-load the plugin before runtime. |
|
||||
| `providerEndpoints` | No | `object[]` | Manifest-owned endpoint host/baseUrl metadata for provider routes that core must classify before provider runtime loads. |
|
||||
| `cliBackends` | No | `string[]` | CLI inference backend ids owned by this plugin. Used for startup auto-activation from explicit config refs. |
|
||||
| `syntheticAuthRefs` | No | `string[]` | Provider or CLI backend refs whose plugin-owned synthetic auth hook should be probed during cold model discovery before runtime loads. |
|
||||
| `nonSecretAuthMarkers` | No | `string[]` | Bundled-plugin-owned placeholder API key values that represent non-secret local, OAuth, or ambient credential state. |
|
||||
| `commandAliases` | No | `object[]` | Command names owned by this plugin that should produce plugin-aware config and CLI diagnostics before runtime loads. |
|
||||
| `providerAuthEnvVars` | No | `Record<string, string[]>` | Cheap provider-auth env metadata that OpenClaw can inspect without loading plugin code. |
|
||||
| `providerAuthAliases` | No | `Record<string, string>` | Provider ids that should reuse another provider id for auth lookup, for example a coding provider that shares the base provider API key and auth profiles. |
|
||||
@@ -599,6 +609,17 @@ See [Configuration reference](/gateway/configuration) for the full `plugins.*` s
|
||||
- `providerAuthAliases` lets provider variants reuse another provider's auth
|
||||
env vars, auth profiles, config-backed auth, and API-key onboarding choice
|
||||
without hardcoding that relationship in core.
|
||||
- `providerEndpoints` lets provider plugins own simple endpoint host/baseUrl
|
||||
matching metadata. Use it only for endpoint classes core already supports;
|
||||
the plugin still owns runtime behavior.
|
||||
- `syntheticAuthRefs` is the cheap metadata path for provider-owned synthetic
|
||||
auth hooks that must be visible to cold model discovery before the runtime
|
||||
registry exists. Only list refs whose runtime provider or CLI backend actually
|
||||
implements `resolveSyntheticAuth`.
|
||||
- `nonSecretAuthMarkers` is the cheap metadata path for bundled plugin-owned
|
||||
placeholder API keys such as local, OAuth, or ambient credential markers.
|
||||
Core treats these as non-secrets for auth display and secret audits without
|
||||
hardcoding the owning provider.
|
||||
- `channelEnvVars` is the cheap metadata path for shell-env fallback, setup
|
||||
prompts, and similar channel surfaces that should not boot plugin runtime
|
||||
just to inspect env names.
|
||||
|
||||
@@ -185,7 +185,9 @@ Keep inbound mention handling split in two layers:
|
||||
- plugin-owned evidence gathering
|
||||
- shared policy evaluation
|
||||
|
||||
Use `openclaw/plugin-sdk/channel-inbound` for the shared layer.
|
||||
Use `openclaw/plugin-sdk/channel-mention-gating` for mention-policy decisions.
|
||||
Use `openclaw/plugin-sdk/channel-inbound` only when you need the broader inbound
|
||||
helper barrel.
|
||||
|
||||
Good fit for plugin-local logic:
|
||||
|
||||
@@ -255,6 +257,11 @@ bundled channel plugins that already depend on runtime injection:
|
||||
- `implicitMentionKindWhen`
|
||||
- `resolveInboundMentionDecision`
|
||||
|
||||
If you only need `implicitMentionKindWhen` and
|
||||
`resolveInboundMentionDecision`, import from
|
||||
`openclaw/plugin-sdk/channel-mention-gating` to avoid loading unrelated inbound
|
||||
runtime helpers.
|
||||
|
||||
The older `resolveMentionGating*` helpers remain on
|
||||
`openclaw/plugin-sdk/channel-inbound` as compatibility exports only. New code
|
||||
should use `resolveInboundMentionDecision({ facts, policy })`.
|
||||
|
||||
@@ -287,6 +287,7 @@ Current bundled provider examples:
|
||||
| `plugin-sdk/provider-tools` | Provider tool/schema compat helpers | `ProviderToolCompatFamily`, `buildProviderToolCompatFamilyHooks`, Gemini schema cleanup + diagnostics, and xAI compat helpers such as `resolveXaiModelCompatPatch` / `applyXaiModelCompat` |
|
||||
| `plugin-sdk/provider-usage` | Provider usage helpers | `fetchClaudeUsage`, `fetchGeminiUsage`, `fetchGithubCopilotUsage`, and other provider usage helpers |
|
||||
| `plugin-sdk/provider-stream` | Provider stream wrapper helpers | `ProviderStreamFamily`, `buildProviderStreamFamilyHooks`, `composeProviderStreamWrappers`, stream wrapper types, and shared Anthropic/Bedrock/Google/Kilocode/Moonshot/OpenAI/OpenRouter/Z.A.I/MiniMax/Copilot wrapper helpers |
|
||||
| `plugin-sdk/provider-transport-runtime` | Provider transport helpers | Native provider transport helpers such as guarded fetch, transport message transforms, and writable transport event streams |
|
||||
| `plugin-sdk/keyed-async-queue` | Ordered async queue | `KeyedAsyncQueue` |
|
||||
| `plugin-sdk/media-runtime` | Shared media helpers | Media fetch/transform/store helpers plus media payload builders |
|
||||
| `plugin-sdk/media-generation-runtime` | Shared media-generation helpers | Shared failover helpers, candidate selection, and missing-model messaging for image/video/music generation |
|
||||
@@ -318,7 +319,7 @@ Current bundled provider examples:
|
||||
| `plugin-sdk/memory-core` | Bundled memory-core helpers | Memory manager/config/file/CLI helper surface |
|
||||
| `plugin-sdk/memory-core-engine-runtime` | Memory engine runtime facade | Memory index/search runtime facade |
|
||||
| `plugin-sdk/memory-core-host-engine-foundation` | Memory host foundation engine | Memory host foundation engine exports |
|
||||
| `plugin-sdk/memory-core-host-engine-embeddings` | Memory host embedding engine | Memory host embedding engine exports |
|
||||
| `plugin-sdk/memory-core-host-engine-embeddings` | Memory host embedding engine | Memory embedding contracts, registry access, local provider, and generic batch/remote helpers; concrete remote providers live in their owning plugins |
|
||||
| `plugin-sdk/memory-core-host-engine-qmd` | Memory host QMD engine | Memory host QMD engine exports |
|
||||
| `plugin-sdk/memory-core-host-engine-storage` | Memory host storage engine | Memory host storage engine exports |
|
||||
| `plugin-sdk/memory-core-host-multimodal` | Memory host multimodal helpers | Memory host multimodal helpers |
|
||||
|
||||
@@ -88,6 +88,7 @@ explicitly promotes one as public.
|
||||
| `plugin-sdk/channel-config-helpers` | `createHybridChannelConfigAdapter` |
|
||||
| `plugin-sdk/channel-config-schema` | Channel config schema types |
|
||||
| `plugin-sdk/telegram-command-config` | Telegram custom-command normalization/validation helpers with bundled-contract fallback |
|
||||
| `plugin-sdk/command-gating` | Narrow command authorization gate helpers |
|
||||
| `plugin-sdk/channel-policy` | `resolveChannelGroupRequireMention` |
|
||||
| `plugin-sdk/channel-lifecycle` | `createAccountStatusSink` |
|
||||
| `plugin-sdk/inbound-envelope` | Shared inbound route + envelope builder helpers |
|
||||
@@ -95,6 +96,7 @@ explicitly promotes one as public.
|
||||
| `plugin-sdk/messaging-targets` | Target parsing/matching helpers |
|
||||
| `plugin-sdk/outbound-media` | Shared outbound media loading helpers |
|
||||
| `plugin-sdk/outbound-runtime` | Outbound identity/send delegate helpers |
|
||||
| `plugin-sdk/poll-runtime` | Narrow poll normalization helpers |
|
||||
| `plugin-sdk/thread-bindings-runtime` | Thread-binding lifecycle and adapter helpers |
|
||||
| `plugin-sdk/agent-media-payload` | Legacy agent media payload builder |
|
||||
| `plugin-sdk/conversation-runtime` | Conversation/thread binding, pairing, and configured-binding helpers |
|
||||
@@ -108,7 +110,10 @@ explicitly promotes one as public.
|
||||
| `plugin-sdk/group-access` | Shared group-access decision helpers |
|
||||
| `plugin-sdk/direct-dm` | Shared direct-DM auth/guard helpers |
|
||||
| `plugin-sdk/interactive-runtime` | Interactive reply payload normalization/reduction helpers |
|
||||
| `plugin-sdk/channel-inbound` | Inbound debounce, mention matching, mention-policy helpers, and envelope helpers |
|
||||
| `plugin-sdk/channel-inbound` | Compatibility barrel for inbound debounce, mention matching, mention-policy helpers, and envelope helpers |
|
||||
| `plugin-sdk/channel-mention-gating` | Narrow mention-policy helpers without the broader inbound runtime surface |
|
||||
| `plugin-sdk/channel-location` | Channel location context and formatting helpers |
|
||||
| `plugin-sdk/channel-logging` | Channel logging helpers for inbound drops and typing/ack failures |
|
||||
| `plugin-sdk/channel-send-result` | Reply result types |
|
||||
| `plugin-sdk/channel-actions` | `createMessageToolButtonsSchema`, `createMessageToolCardSchema` |
|
||||
| `plugin-sdk/channel-targets` | Target parsing/matching helpers |
|
||||
@@ -141,6 +146,7 @@ explicitly promotes one as public.
|
||||
| `plugin-sdk/provider-tools` | `ProviderToolCompatFamily`, `buildProviderToolCompatFamilyHooks`, Gemini schema cleanup + diagnostics, and xAI compat helpers such as `resolveXaiModelCompatPatch` / `applyXaiModelCompat` |
|
||||
| `plugin-sdk/provider-usage` | `fetchClaudeUsage` and similar |
|
||||
| `plugin-sdk/provider-stream` | `ProviderStreamFamily`, `buildProviderStreamFamilyHooks`, `composeProviderStreamWrappers`, stream wrapper types, and shared Anthropic/Bedrock/Google/Kilocode/Moonshot/OpenAI/OpenRouter/Z.A.I/MiniMax/Copilot wrapper helpers |
|
||||
| `plugin-sdk/provider-transport-runtime` | Native provider transport helpers such as guarded fetch, transport message transforms, and writable transport event streams |
|
||||
| `plugin-sdk/provider-onboard` | Onboarding config patch helpers |
|
||||
| `plugin-sdk/global-singleton` | Process-local singleton/map/cache helpers |
|
||||
</Accordion>
|
||||
@@ -166,6 +172,7 @@ explicitly promotes one as public.
|
||||
| `plugin-sdk/secret-ref-runtime` | Narrow `coerceSecretRef` and SecretRef typing helpers for secret-contract/config parsing |
|
||||
| `plugin-sdk/security-runtime` | Shared trust, DM gating, external-content, and secret-collection helpers |
|
||||
| `plugin-sdk/ssrf-policy` | Host allowlist and private-network SSRF policy helpers |
|
||||
| `plugin-sdk/ssrf-dispatcher` | Narrow pinned-dispatcher helpers without the broad infra runtime surface |
|
||||
| `plugin-sdk/ssrf-runtime` | Pinned-dispatcher, SSRF-guarded fetch, and SSRF policy helpers |
|
||||
| `plugin-sdk/secret-input` | Secret input parsing helpers |
|
||||
| `plugin-sdk/webhook-ingress` | Webhook request/target helpers |
|
||||
@@ -187,6 +194,7 @@ explicitly promotes one as public.
|
||||
| `plugin-sdk/gateway-runtime` | Gateway client and channel-status patch helpers |
|
||||
| `plugin-sdk/config-runtime` | Config load/write helpers |
|
||||
| `plugin-sdk/telegram-command-config` | Telegram command-name/description normalization and duplicate/conflict checks, even when the bundled Telegram contract surface is unavailable |
|
||||
| `plugin-sdk/text-autolink-runtime` | File-reference autolink detection without the broad text-runtime barrel |
|
||||
| `plugin-sdk/approval-runtime` | Exec/plugin approval helpers, approval-capability builders, auth/profile helpers, native routing/runtime helpers |
|
||||
| `plugin-sdk/reply-runtime` | Shared inbound/reply runtime helpers, chunking, dispatch, heartbeat, reply planner |
|
||||
| `plugin-sdk/reply-dispatch-runtime` | Narrow reply dispatch/finalize helpers |
|
||||
@@ -211,6 +219,7 @@ explicitly promotes one as public.
|
||||
| `plugin-sdk/file-lock` | Re-entrant file-lock helpers |
|
||||
| `plugin-sdk/persistent-dedupe` | Disk-backed dedupe cache helpers |
|
||||
| `plugin-sdk/acp-runtime` | ACP runtime/session and reply-dispatch helpers |
|
||||
| `plugin-sdk/acp-binding-resolve-runtime` | Read-only ACP binding resolution without lifecycle startup imports |
|
||||
| `plugin-sdk/agent-config-primitives` | Narrow agent runtime config-schema primitives |
|
||||
| `plugin-sdk/boolean-param` | Loose boolean param reader |
|
||||
| `plugin-sdk/dangerous-name-runtime` | Dangerous-name matching resolution helpers |
|
||||
@@ -226,6 +235,12 @@ explicitly promotes one as public.
|
||||
| `plugin-sdk/diagnostic-runtime` | Diagnostic flag and event helpers |
|
||||
| `plugin-sdk/error-runtime` | Error graph, formatting, shared error classification helpers, `isApprovalNotFoundError` |
|
||||
| `plugin-sdk/fetch-runtime` | Wrapped fetch, proxy, and pinned lookup helpers |
|
||||
| `plugin-sdk/runtime-fetch` | Dispatcher-aware runtime fetch without proxy/guarded-fetch imports |
|
||||
| `plugin-sdk/response-limit-runtime` | Bounded response-body reader without the broad media runtime surface |
|
||||
| `plugin-sdk/session-binding-runtime` | Current conversation binding state without configured binding routing or pairing stores |
|
||||
| `plugin-sdk/session-store-runtime` | Session-store read helpers without broad config writes/maintenance imports |
|
||||
| `plugin-sdk/context-visibility-runtime` | Context visibility resolution and supplemental context filtering without broad config/security imports |
|
||||
| `plugin-sdk/string-coerce-runtime` | Narrow primitive record/string coercion and normalization helpers without markdown/logging imports |
|
||||
| `plugin-sdk/host-runtime` | Hostname and SCP host normalization helpers |
|
||||
| `plugin-sdk/retry-runtime` | Retry config and retry runner helpers |
|
||||
| `plugin-sdk/agent-runtime` | Agent dir/identity/workspace helpers |
|
||||
@@ -264,7 +279,7 @@ explicitly promotes one as public.
|
||||
| `plugin-sdk/memory-core` | Bundled memory-core helper surface for manager/config/file/CLI helpers |
|
||||
| `plugin-sdk/memory-core-engine-runtime` | Memory index/search runtime facade |
|
||||
| `plugin-sdk/memory-core-host-engine-foundation` | Memory host foundation engine exports |
|
||||
| `plugin-sdk/memory-core-host-engine-embeddings` | Memory host embedding engine exports |
|
||||
| `plugin-sdk/memory-core-host-engine-embeddings` | Memory host embedding contracts, registry access, local provider, and generic batch/remote helpers |
|
||||
| `plugin-sdk/memory-core-host-engine-qmd` | Memory host QMD engine exports |
|
||||
| `plugin-sdk/memory-core-host-engine-storage` | Memory host storage engine exports |
|
||||
| `plugin-sdk/memory-core-host-multimodal` | Memory host multimodal helpers |
|
||||
|
||||
@@ -128,20 +128,25 @@ Choose your preferred auth method and follow the setup steps.
|
||||
|
||||
## Capabilities
|
||||
|
||||
| Capability | Supported |
|
||||
| ---------------------- | ----------------- |
|
||||
| Chat completions | Yes |
|
||||
| Image generation | Yes |
|
||||
| Music generation | Yes |
|
||||
| Text-to-speech | Yes |
|
||||
| Image understanding | Yes |
|
||||
| Audio transcription | Yes |
|
||||
| Video understanding | Yes |
|
||||
| Web search (Grounding) | Yes |
|
||||
| Thinking/reasoning | Yes (Gemini 3.1+) |
|
||||
| Gemma 4 models | Yes |
|
||||
| Capability | Supported |
|
||||
| ---------------------- | ----------------------------- |
|
||||
| Chat completions | Yes |
|
||||
| Image generation | Yes |
|
||||
| Music generation | Yes |
|
||||
| Text-to-speech | Yes |
|
||||
| Image understanding | Yes |
|
||||
| Audio transcription | Yes |
|
||||
| Video understanding | Yes |
|
||||
| Web search (Grounding) | Yes |
|
||||
| Thinking/reasoning | Yes (Gemini 2.5+ / Gemini 3+) |
|
||||
| Gemma 4 models | Yes |
|
||||
|
||||
<Tip>
|
||||
Gemini 3 models use `thinkingLevel` rather than `thinkingBudget`. OpenClaw maps
|
||||
Gemini 3, Gemini 3.1, and `gemini-*-latest` alias reasoning controls to
|
||||
`thinkingLevel` so default/low-latency runs do not send disabled
|
||||
`thinkingBudget` values.
|
||||
|
||||
Gemma 4 models (for example `gemma-4-26b-a4b-it`) support thinking mode. OpenClaw
|
||||
rewrites `thinkingBudget` to a supported Google `thinkingLevel` for Gemma 4.
|
||||
Setting thinking to `off` preserves thinking disabled instead of mapping to
|
||||
|
||||
@@ -18,7 +18,7 @@ The desired end state is a generic QA harness that loads powerful scenario defin
|
||||
## Current State
|
||||
|
||||
Primary source of truth now lives in `qa/scenarios/index.md` plus one file per
|
||||
scenario under `qa/scenarios/*.md`.
|
||||
scenario under `qa/scenarios/<theme>/*.md`.
|
||||
|
||||
Implemented:
|
||||
|
||||
@@ -26,7 +26,7 @@ Implemented:
|
||||
- canonical QA pack metadata
|
||||
- operator identity
|
||||
- kickoff mission
|
||||
- `qa/scenarios/*.md`
|
||||
- `qa/scenarios/<theme>/*.md`
|
||||
- one markdown file per scenario
|
||||
- scenario metadata
|
||||
- handler bindings
|
||||
@@ -107,8 +107,8 @@ These categories matter because they drive DSL requirements. A flat list of prom
|
||||
|
||||
### Single source of truth
|
||||
|
||||
Use `qa/scenarios/index.md` plus `qa/scenarios/*.md` as the authored source of
|
||||
truth.
|
||||
Use `qa/scenarios/index.md` plus `qa/scenarios/<theme>/*.md` as the authored
|
||||
source of truth.
|
||||
|
||||
The pack should stay:
|
||||
|
||||
@@ -363,7 +363,7 @@ Generated compatibility:
|
||||
Done.
|
||||
|
||||
- added `qa/scenarios/index.md`
|
||||
- split scenarios into `qa/scenarios/*.md`
|
||||
- split scenarios into `qa/scenarios/<theme>/*.md`
|
||||
- added parser for named markdown YAML pack content
|
||||
- validated with zod
|
||||
- switched consumers to the parsed pack
|
||||
|
||||
@@ -82,7 +82,10 @@ html.dark .nav-tabs-underline {
|
||||
border-radius: 8px;
|
||||
background: color-mix(in oklab, rgb(var(--primary)) 4%, transparent);
|
||||
text-decoration: none;
|
||||
transition: transform 0.16s ease, border-color 0.16s ease, background 0.16s ease;
|
||||
transition:
|
||||
transform 0.16s ease,
|
||||
border-color 0.16s ease,
|
||||
background 0.16s ease;
|
||||
}
|
||||
|
||||
.showcase-actions a:first-child {
|
||||
|
||||
@@ -60,6 +60,10 @@ third-party plugins see.
|
||||
- Do not rely on eager global registry seeding or import-time side effects to
|
||||
make a plugin “available”. Plugin availability should come from manifest
|
||||
ownership plus targeted activation.
|
||||
- When core needs plugin-owned static data on a hot path, expose a lightweight
|
||||
top-level artifact such as `gateway-auth-api.ts`, `message-tool-api.ts`, or a
|
||||
similarly narrow `*-api.ts`. Reuse the same local helper from the artifact and
|
||||
the full plugin so fast paths do not drift from runtime behavior.
|
||||
|
||||
## Expanding The Boundary
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/acpx",
|
||||
"version": "2026.4.16",
|
||||
"version": "2026.4.19-beta.1",
|
||||
"description": "OpenClaw ACP runtime backend",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
||||
@@ -21,4 +21,32 @@ describe("active-memory manifest config schema", () => {
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
});
|
||||
|
||||
it("accepts timeoutMs values at the runtime ceiling", () => {
|
||||
const result = validateJsonSchemaValue({
|
||||
schema: manifest.configSchema,
|
||||
cacheKey: "active-memory.manifest.timeout-ceiling",
|
||||
value: {
|
||||
enabled: true,
|
||||
agents: ["main"],
|
||||
timeoutMs: 120_000,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects timeoutMs values above the runtime ceiling", () => {
|
||||
const result = validateJsonSchemaValue({
|
||||
schema: manifest.configSchema,
|
||||
cacheKey: "active-memory.manifest.timeout-above-ceiling",
|
||||
value: {
|
||||
enabled: true,
|
||||
agents: ["main"],
|
||||
timeoutMs: 120_001,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -119,7 +119,7 @@ describe("active-memory plugin", () => {
|
||||
runEmbeddedPiAgent.mockResolvedValue({
|
||||
payloads: [{ text: "- lemon pepper wings\n- blue cheese" }],
|
||||
});
|
||||
await plugin.register(api as unknown as OpenClawPluginApi);
|
||||
plugin.register(api as unknown as OpenClawPluginApi);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
@@ -425,7 +425,7 @@ describe("active-memory plugin", () => {
|
||||
agents: ["main"],
|
||||
allowedChatTypes: ["direct", "group"],
|
||||
};
|
||||
await plugin.register(api as unknown as OpenClawPluginApi);
|
||||
plugin.register(api as unknown as OpenClawPluginApi);
|
||||
|
||||
const result = await hooks.before_prompt_build(
|
||||
{ prompt: "what wings should we order?", messages: [] },
|
||||
@@ -513,7 +513,7 @@ describe("active-memory plugin", () => {
|
||||
searchMode: "inherit",
|
||||
},
|
||||
};
|
||||
await plugin.register(api as unknown as OpenClawPluginApi);
|
||||
plugin.register(api as unknown as OpenClawPluginApi);
|
||||
|
||||
await hooks.before_prompt_build(
|
||||
{
|
||||
@@ -602,7 +602,7 @@ describe("active-memory plugin", () => {
|
||||
agents: ["main"],
|
||||
queryMode: "message",
|
||||
};
|
||||
await plugin.register(api as unknown as OpenClawPluginApi);
|
||||
plugin.register(api as unknown as OpenClawPluginApi);
|
||||
|
||||
await hooks.before_prompt_build(
|
||||
{
|
||||
@@ -630,7 +630,7 @@ describe("active-memory plugin", () => {
|
||||
queryMode: "message",
|
||||
promptStyle: "preference-only",
|
||||
};
|
||||
await plugin.register(api as unknown as OpenClawPluginApi);
|
||||
plugin.register(api as unknown as OpenClawPluginApi);
|
||||
|
||||
await hooks.before_prompt_build(
|
||||
{
|
||||
@@ -675,7 +675,7 @@ describe("active-memory plugin", () => {
|
||||
agents: ["main"],
|
||||
thinking: "medium",
|
||||
};
|
||||
await plugin.register(api as unknown as OpenClawPluginApi);
|
||||
plugin.register(api as unknown as OpenClawPluginApi);
|
||||
|
||||
await hooks.before_prompt_build(
|
||||
{
|
||||
@@ -701,7 +701,7 @@ describe("active-memory plugin", () => {
|
||||
agents: ["main"],
|
||||
promptAppend: "Prefer stable long-term preferences over one-off events.",
|
||||
};
|
||||
await plugin.register(api as unknown as OpenClawPluginApi);
|
||||
plugin.register(api as unknown as OpenClawPluginApi);
|
||||
|
||||
await hooks.before_prompt_build(
|
||||
{
|
||||
@@ -730,7 +730,7 @@ describe("active-memory plugin", () => {
|
||||
promptOverride: "Custom memory prompt. Return NONE or one user fact.",
|
||||
promptAppend: "Extra custom instruction.",
|
||||
};
|
||||
await plugin.register(api as unknown as OpenClawPluginApi);
|
||||
plugin.register(api as unknown as OpenClawPluginApi);
|
||||
|
||||
await hooks.before_prompt_build(
|
||||
{
|
||||
@@ -802,7 +802,7 @@ describe("active-memory plugin", () => {
|
||||
api.pluginConfig = {
|
||||
agents: ["main"],
|
||||
};
|
||||
await plugin.register(api as unknown as OpenClawPluginApi);
|
||||
plugin.register(api as unknown as OpenClawPluginApi);
|
||||
|
||||
await hooks.before_prompt_build(
|
||||
{ prompt: "what wings should i order? temp transcript", messages: [] },
|
||||
@@ -828,7 +828,7 @@ describe("active-memory plugin", () => {
|
||||
agents: ["main"],
|
||||
modelFallbackPolicy: "resolved-only",
|
||||
};
|
||||
await plugin.register(api as unknown as OpenClawPluginApi);
|
||||
plugin.register(api as unknown as OpenClawPluginApi);
|
||||
|
||||
const result = await hooks.before_prompt_build(
|
||||
{ prompt: "what wings should i order? no fallback", messages: [] },
|
||||
@@ -851,7 +851,7 @@ describe("active-memory plugin", () => {
|
||||
modelFallback: "google/gemini-3-flash",
|
||||
modelFallbackPolicy: "default-remote",
|
||||
};
|
||||
await plugin.register(api as unknown as OpenClawPluginApi);
|
||||
plugin.register(api as unknown as OpenClawPluginApi);
|
||||
|
||||
await hooks.before_prompt_build(
|
||||
{ prompt: "what wings should i order? custom fallback", messages: [] },
|
||||
@@ -878,7 +878,7 @@ describe("active-memory plugin", () => {
|
||||
agents: ["main"],
|
||||
modelFallbackPolicy: "default-remote",
|
||||
};
|
||||
await plugin.register(api as unknown as OpenClawPluginApi);
|
||||
plugin.register(api as unknown as OpenClawPluginApi);
|
||||
|
||||
const result = await hooks.before_prompt_build(
|
||||
{ prompt: "what wings should i order? built-in fallback", messages: [] },
|
||||
@@ -1027,7 +1027,7 @@ describe("active-memory plugin", () => {
|
||||
timeoutMs: 250,
|
||||
logging: true,
|
||||
};
|
||||
await plugin.register(api as unknown as OpenClawPluginApi);
|
||||
plugin.register(api as unknown as OpenClawPluginApi);
|
||||
let lastAbortSignal: AbortSignal | undefined;
|
||||
runEmbeddedPiAgent.mockImplementation(async (params: { abortSignal?: AbortSignal }) => {
|
||||
lastAbortSignal = params.abortSignal;
|
||||
@@ -1073,7 +1073,7 @@ describe("active-memory plugin", () => {
|
||||
agents: ["main"],
|
||||
logging: true,
|
||||
};
|
||||
await plugin.register(api as unknown as OpenClawPluginApi);
|
||||
plugin.register(api as unknown as OpenClawPluginApi);
|
||||
|
||||
await hooks.before_prompt_build(
|
||||
{ prompt: "what wings should i order? session id cache", messages: [] },
|
||||
@@ -1107,7 +1107,7 @@ describe("active-memory plugin", () => {
|
||||
timeoutMs: 250,
|
||||
logging: true,
|
||||
};
|
||||
await plugin.register(api as unknown as OpenClawPluginApi);
|
||||
plugin.register(api as unknown as OpenClawPluginApi);
|
||||
runEmbeddedPiAgent.mockImplementationOnce(async (params: { timeoutMs?: number }) => {
|
||||
await new Promise((resolve) => setTimeout(resolve, (params.timeoutMs ?? 0) + 25));
|
||||
return {
|
||||
@@ -1140,12 +1140,56 @@ describe("active-memory plugin", () => {
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("honors configured timeoutMs values above the former 60 000 ms ceiling", async () => {
|
||||
api.pluginConfig = {
|
||||
agents: ["main"],
|
||||
timeoutMs: 90_000,
|
||||
logging: true,
|
||||
};
|
||||
plugin.register(api as unknown as OpenClawPluginApi);
|
||||
|
||||
await hooks.before_prompt_build(
|
||||
{ prompt: "what wings should i order? high timeout", messages: [] },
|
||||
{
|
||||
agentId: "main",
|
||||
trigger: "user",
|
||||
sessionKey: "agent:main:high-timeout",
|
||||
messageProvider: "webchat",
|
||||
},
|
||||
);
|
||||
|
||||
const passedTimeoutMs = runEmbeddedPiAgent.mock.calls.at(-1)?.[0]?.timeoutMs;
|
||||
expect(passedTimeoutMs).toBe(90_000);
|
||||
});
|
||||
|
||||
it("clamps timeoutMs above the 120 000 ms ceiling to the ceiling", async () => {
|
||||
api.pluginConfig = {
|
||||
agents: ["main"],
|
||||
timeoutMs: 200_000,
|
||||
logging: true,
|
||||
};
|
||||
plugin.register(api as unknown as OpenClawPluginApi);
|
||||
|
||||
await hooks.before_prompt_build(
|
||||
{ prompt: "what wings should i order? capped timeout", messages: [] },
|
||||
{
|
||||
agentId: "main",
|
||||
trigger: "user",
|
||||
sessionKey: "agent:main:capped-timeout",
|
||||
messageProvider: "webchat",
|
||||
},
|
||||
);
|
||||
|
||||
const passedTimeoutMs = runEmbeddedPiAgent.mock.calls.at(-1)?.[0]?.timeoutMs;
|
||||
expect(passedTimeoutMs).toBe(120_000);
|
||||
});
|
||||
|
||||
it("sanitizes active-memory log fields onto a single line", async () => {
|
||||
api.pluginConfig = {
|
||||
agents: ["main"],
|
||||
logging: true,
|
||||
};
|
||||
await plugin.register(api as unknown as OpenClawPluginApi);
|
||||
plugin.register(api as unknown as OpenClawPluginApi);
|
||||
|
||||
await hooks.before_prompt_build(
|
||||
{ prompt: "what wings should i order? log sanitization", messages: [] },
|
||||
@@ -1179,7 +1223,7 @@ describe("active-memory plugin", () => {
|
||||
agents: ["main"],
|
||||
logging: true,
|
||||
};
|
||||
await plugin.register(api as unknown as OpenClawPluginApi);
|
||||
plugin.register(api as unknown as OpenClawPluginApi);
|
||||
const hugeSession = `agent:main:${"x".repeat(500)}`;
|
||||
|
||||
await hooks.before_prompt_build(
|
||||
@@ -1423,7 +1467,7 @@ describe("active-memory plugin", () => {
|
||||
agents: ["main"],
|
||||
queryMode: "message",
|
||||
};
|
||||
await plugin.register(api as unknown as OpenClawPluginApi);
|
||||
plugin.register(api as unknown as OpenClawPluginApi);
|
||||
|
||||
await hooks.before_prompt_build(
|
||||
{
|
||||
@@ -1451,7 +1495,7 @@ describe("active-memory plugin", () => {
|
||||
agents: ["main"],
|
||||
queryMode: "full",
|
||||
};
|
||||
await plugin.register(api as unknown as OpenClawPluginApi);
|
||||
plugin.register(api as unknown as OpenClawPluginApi);
|
||||
|
||||
await hooks.before_prompt_build(
|
||||
{
|
||||
@@ -1482,7 +1526,7 @@ describe("active-memory plugin", () => {
|
||||
agents: ["main"],
|
||||
queryMode: "recent",
|
||||
};
|
||||
await plugin.register(api as unknown as OpenClawPluginApi);
|
||||
plugin.register(api as unknown as OpenClawPluginApi);
|
||||
|
||||
await hooks.before_prompt_build(
|
||||
{
|
||||
@@ -1536,7 +1580,7 @@ describe("active-memory plugin", () => {
|
||||
agents: ["main"],
|
||||
queryMode: "recent",
|
||||
};
|
||||
await plugin.register(api as unknown as OpenClawPluginApi);
|
||||
plugin.register(api as unknown as OpenClawPluginApi);
|
||||
|
||||
await hooks.before_prompt_build(
|
||||
{
|
||||
@@ -1578,7 +1622,7 @@ describe("active-memory plugin", () => {
|
||||
agents: ["main"],
|
||||
queryMode: "recent",
|
||||
};
|
||||
await plugin.register(api as unknown as OpenClawPluginApi);
|
||||
plugin.register(api as unknown as OpenClawPluginApi);
|
||||
|
||||
await hooks.before_prompt_build(
|
||||
{
|
||||
@@ -1611,7 +1655,7 @@ describe("active-memory plugin", () => {
|
||||
agents: ["main"],
|
||||
queryMode: "recent",
|
||||
};
|
||||
await plugin.register(api as unknown as OpenClawPluginApi);
|
||||
plugin.register(api as unknown as OpenClawPluginApi);
|
||||
|
||||
await hooks.before_prompt_build(
|
||||
{
|
||||
@@ -1619,8 +1663,7 @@ describe("active-memory plugin", () => {
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content:
|
||||
"Active Memory: I really do want you to remember that I prefer aisle seats.",
|
||||
content: "Active Memory: I really do want you to remember that I prefer aisle seats.",
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
@@ -1674,7 +1717,7 @@ describe("active-memory plugin", () => {
|
||||
agents: ["main"],
|
||||
maxSummaryChars: 40,
|
||||
};
|
||||
await plugin.register(api as unknown as OpenClawPluginApi);
|
||||
plugin.register(api as unknown as OpenClawPluginApi);
|
||||
runEmbeddedPiAgent.mockResolvedValueOnce({
|
||||
payloads: [
|
||||
{
|
||||
@@ -1708,7 +1751,7 @@ describe("active-memory plugin", () => {
|
||||
agents: ["main"],
|
||||
maxSummaryChars: 90,
|
||||
};
|
||||
await plugin.register(api as unknown as OpenClawPluginApi);
|
||||
plugin.register(api as unknown as OpenClawPluginApi);
|
||||
|
||||
await hooks.before_prompt_build(
|
||||
{ prompt: "what wings should i order? prompt-count-check", messages: [] },
|
||||
@@ -1758,7 +1801,7 @@ describe("active-memory plugin", () => {
|
||||
transcriptDir: "active-memory-subagents",
|
||||
logging: true,
|
||||
};
|
||||
await plugin.register(api as unknown as OpenClawPluginApi);
|
||||
plugin.register(api as unknown as OpenClawPluginApi);
|
||||
const mkdirSpy = vi.spyOn(fs, "mkdir").mockResolvedValue(undefined);
|
||||
const mkdtempSpy = vi.spyOn(fs, "mkdtemp");
|
||||
const rmSpy = vi.spyOn(fs, "rm").mockResolvedValue(undefined);
|
||||
@@ -1802,7 +1845,7 @@ describe("active-memory plugin", () => {
|
||||
transcriptDir: "C:/temp/escape",
|
||||
logging: true,
|
||||
};
|
||||
await plugin.register(api as unknown as OpenClawPluginApi);
|
||||
plugin.register(api as unknown as OpenClawPluginApi);
|
||||
const mkdirSpy = vi.spyOn(fs, "mkdir").mockResolvedValue(undefined);
|
||||
|
||||
await hooks.before_prompt_build(
|
||||
@@ -1839,7 +1882,7 @@ describe("active-memory plugin", () => {
|
||||
transcriptDir: "active-memory-subagents",
|
||||
logging: true,
|
||||
};
|
||||
await plugin.register(api as unknown as OpenClawPluginApi);
|
||||
plugin.register(api as unknown as OpenClawPluginApi);
|
||||
const mkdirSpy = vi.spyOn(fs, "mkdir").mockResolvedValue(undefined);
|
||||
|
||||
await hooks.before_prompt_build(
|
||||
@@ -1906,7 +1949,7 @@ describe("active-memory plugin", () => {
|
||||
agents: ["main"],
|
||||
logging: true,
|
||||
};
|
||||
await plugin.register(api as unknown as OpenClawPluginApi);
|
||||
plugin.register(api as unknown as OpenClawPluginApi);
|
||||
|
||||
for (let index = 0; index <= 1000; index += 1) {
|
||||
await hooks.before_prompt_build(
|
||||
|
||||
@@ -633,7 +633,7 @@ function normalizePluginConfig(pluginConfig: unknown): ResolvedActiveRecallPlugi
|
||||
parseOptionalPositiveInt(raw.timeoutMs, DEFAULT_TIMEOUT_MS),
|
||||
DEFAULT_TIMEOUT_MS,
|
||||
250,
|
||||
60_000,
|
||||
120_000,
|
||||
),
|
||||
queryMode:
|
||||
raw.queryMode === "message" || raw.queryMode === "recent" || raw.queryMode === "full"
|
||||
@@ -1527,7 +1527,9 @@ function extractRecentTurns(messages: unknown[]): ActiveRecallRecentTurn[] {
|
||||
}
|
||||
const rawText = extractTextContent(typed.content);
|
||||
const text =
|
||||
role === "assistant" ? stripRecalledContextNoise(rawText) : stripInjectedActiveMemoryPrefixOnly(rawText);
|
||||
role === "assistant"
|
||||
? stripRecalledContextNoise(rawText)
|
||||
: stripInjectedActiveMemoryPrefixOnly(rawText);
|
||||
if (!text) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
"type": "string",
|
||||
"enum": ["off", "minimal", "low", "medium", "high", "xhigh", "adaptive"]
|
||||
},
|
||||
"timeoutMs": { "type": "integer", "minimum": 250 },
|
||||
"timeoutMs": { "type": "integer", "minimum": 250, "maximum": 120000 },
|
||||
"queryMode": {
|
||||
"type": "string",
|
||||
"enum": ["message", "recent", "full"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/alibaba-provider",
|
||||
"version": "2026.4.16",
|
||||
"version": "2026.4.19-beta.1",
|
||||
"private": true,
|
||||
"description": "OpenClaw Alibaba Model Studio video provider plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/amazon-bedrock-mantle-provider",
|
||||
"version": "2026.4.16",
|
||||
"version": "2026.4.19-beta.1",
|
||||
"private": true,
|
||||
"description": "OpenClaw Amazon Bedrock Mantle (OpenAI-compatible) provider plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js";
|
||||
import { sanitizeAndNormalizeEmbedding } from "./embedding-vectors.js";
|
||||
import { debugEmbeddingsLog } from "./embeddings-debug.js";
|
||||
import type { EmbeddingProvider, EmbeddingProviderOptions } from "./embeddings.types.js";
|
||||
import {
|
||||
debugEmbeddingsLog,
|
||||
sanitizeAndNormalizeEmbedding,
|
||||
type MemoryEmbeddingProvider,
|
||||
type MemoryEmbeddingProviderCreateOptions,
|
||||
} from "openclaw/plugin-sdk/memory-core-host-engine-embeddings";
|
||||
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types & constants
|
||||
@@ -254,8 +257,8 @@ function parseCohereBatch(family: Family, raw: string): number[][] {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export async function createBedrockEmbeddingProvider(
|
||||
options: EmbeddingProviderOptions,
|
||||
): Promise<{ provider: EmbeddingProvider; client: BedrockEmbeddingClient }> {
|
||||
options: MemoryEmbeddingProviderCreateOptions,
|
||||
): Promise<{ provider: MemoryEmbeddingProvider; client: BedrockEmbeddingClient }> {
|
||||
const client = resolveBedrockEmbeddingClient(options);
|
||||
const { BedrockRuntimeClient, InvokeModelCommand } = await loadSdk();
|
||||
const sdk = new BedrockRuntimeClient({ region: client.region });
|
||||
@@ -333,7 +336,7 @@ export async function createBedrockEmbeddingProvider(
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function resolveBedrockEmbeddingClient(
|
||||
options: EmbeddingProviderOptions,
|
||||
options: MemoryEmbeddingProviderCreateOptions,
|
||||
): BedrockEmbeddingClient {
|
||||
const model = normalizeBedrockEmbeddingModel(options.model);
|
||||
const spec = resolveSpec(model);
|
||||
37
extensions/amazon-bedrock/memory-embedding-adapter.ts
Normal file
37
extensions/amazon-bedrock/memory-embedding-adapter.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import {
|
||||
isMissingEmbeddingApiKeyError,
|
||||
type MemoryEmbeddingProviderAdapter,
|
||||
} from "openclaw/plugin-sdk/memory-core-host-engine-embeddings";
|
||||
import {
|
||||
createBedrockEmbeddingProvider,
|
||||
DEFAULT_BEDROCK_EMBEDDING_MODEL,
|
||||
} from "./embedding-provider.js";
|
||||
|
||||
export const bedrockMemoryEmbeddingProviderAdapter: MemoryEmbeddingProviderAdapter = {
|
||||
id: "bedrock",
|
||||
defaultModel: DEFAULT_BEDROCK_EMBEDDING_MODEL,
|
||||
transport: "remote",
|
||||
authProviderId: "amazon-bedrock",
|
||||
autoSelectPriority: 60,
|
||||
allowExplicitWhenConfiguredAuto: true,
|
||||
shouldContinueAutoSelection: isMissingEmbeddingApiKeyError,
|
||||
create: async (options) => {
|
||||
const { provider, client } = await createBedrockEmbeddingProvider({
|
||||
...options,
|
||||
provider: "bedrock",
|
||||
fallback: "none",
|
||||
});
|
||||
return {
|
||||
provider,
|
||||
runtime: {
|
||||
id: "bedrock",
|
||||
cacheKeyData: {
|
||||
provider: "bedrock",
|
||||
region: client.region,
|
||||
model: client.model,
|
||||
dimensions: client.dimensions,
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -2,6 +2,9 @@
|
||||
"id": "amazon-bedrock",
|
||||
"enabledByDefault": true,
|
||||
"providers": ["amazon-bedrock"],
|
||||
"contracts": {
|
||||
"memoryEmbeddingProviders": ["bedrock"]
|
||||
},
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
{
|
||||
"name": "@openclaw/amazon-bedrock-provider",
|
||||
"version": "2026.4.16",
|
||||
"version": "2026.4.19-beta.1",
|
||||
"private": true,
|
||||
"description": "OpenClaw Amazon Bedrock provider plugin",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-bedrock": "3.1028.0"
|
||||
"@aws-sdk/client-bedrock": "3.1032.0",
|
||||
"@aws-sdk/client-bedrock-runtime": "3.1032.0",
|
||||
"@aws-sdk/credential-provider-node": "3.972.32"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@openclaw/plugin-sdk": "workspace:*"
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
resolveBedrockConfigApiKey,
|
||||
resolveImplicitBedrockProvider,
|
||||
} from "./api.js";
|
||||
import { bedrockMemoryEmbeddingProviderAdapter } from "./memory-embedding-adapter.js";
|
||||
|
||||
type GuardrailConfig = {
|
||||
guardrailIdentifier: string;
|
||||
@@ -78,6 +79,8 @@ export function registerAmazonBedrockPlugin(api: OpenClawPluginApi): void {
|
||||
const pluginConfig = (api.pluginConfig ?? {}) as AmazonBedrockPluginConfig;
|
||||
const guardrail = pluginConfig.guardrail;
|
||||
|
||||
api.registerMemoryEmbeddingProvider(bedrockMemoryEmbeddingProviderAdapter);
|
||||
|
||||
const baseWrapStreamFn = ({ modelId, streamFn }: { modelId: string; streamFn?: StreamFn }) =>
|
||||
isAnthropicBedrockModel(modelId) ? streamFn : createBedrockNoCacheWrapper(streamFn);
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"enabledByDefault": true,
|
||||
"providers": ["anthropic-vertex"],
|
||||
"providerDiscoveryEntry": "./provider-discovery.ts",
|
||||
"nonSecretAuthMarkers": ["gcp-vertex-credentials"],
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/anthropic-vertex-provider",
|
||||
"version": "2026.4.16",
|
||||
"version": "2026.4.19-beta.1",
|
||||
"private": true,
|
||||
"description": "OpenClaw Anthropic Vertex provider plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export { CLAUDE_CLI_BACKEND_ID, isClaudeCliProvider } from "./cli-shared.js";
|
||||
export { buildAnthropicProvider } from "./register.runtime.js";
|
||||
export {
|
||||
createAnthropicBetaHeadersWrapper,
|
||||
createAnthropicFastModeWrapper,
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"modelPrefixes": ["claude-"]
|
||||
},
|
||||
"cliBackends": ["claude-cli"],
|
||||
"syntheticAuthRefs": ["claude-cli"],
|
||||
"providerAuthEnvVars": {
|
||||
"anthropic": ["ANTHROPIC_OAUTH_TOKEN", "ANTHROPIC_API_KEY"]
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/anthropic-provider",
|
||||
"version": "2026.4.16",
|
||||
"version": "2026.4.19-beta.1",
|
||||
"private": true,
|
||||
"description": "OpenClaw Anthropic provider plugin",
|
||||
"type": "module",
|
||||
|
||||
59
extensions/anthropic/provider-contract-api.ts
Normal file
59
extensions/anthropic/provider-contract-api.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import type { ProviderPlugin } from "openclaw/plugin-sdk/provider-model-shared";
|
||||
|
||||
const noopAuth = async () => ({ profiles: [] });
|
||||
|
||||
export function createAnthropicProvider(): ProviderPlugin {
|
||||
return {
|
||||
id: "anthropic",
|
||||
label: "Anthropic",
|
||||
docsPath: "/providers/models",
|
||||
hookAliases: ["claude-cli"],
|
||||
envVars: ["ANTHROPIC_OAUTH_TOKEN", "ANTHROPIC_API_KEY"],
|
||||
auth: [
|
||||
{
|
||||
id: "cli",
|
||||
kind: "custom",
|
||||
label: "Claude CLI",
|
||||
hint: "Reuse a local Claude CLI login and switch model selection to claude-cli/*",
|
||||
run: noopAuth,
|
||||
wizard: {
|
||||
choiceId: "anthropic-cli",
|
||||
choiceLabel: "Anthropic Claude CLI",
|
||||
choiceHint: "Reuse a local Claude CLI login on this host",
|
||||
groupId: "anthropic",
|
||||
groupLabel: "Anthropic",
|
||||
groupHint: "Claude CLI + API key",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "setup-token",
|
||||
kind: "token",
|
||||
label: "Anthropic setup-token",
|
||||
hint: "Manual bearer token path",
|
||||
run: noopAuth,
|
||||
wizard: {
|
||||
choiceId: "setup-token",
|
||||
choiceLabel: "Anthropic setup-token",
|
||||
choiceHint: "Manual token path",
|
||||
groupId: "anthropic",
|
||||
groupLabel: "Anthropic",
|
||||
groupHint: "Claude CLI + API key + token",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "api-key",
|
||||
kind: "api_key",
|
||||
label: "Anthropic API key",
|
||||
hint: "Direct Anthropic API key",
|
||||
run: noopAuth,
|
||||
wizard: {
|
||||
choiceId: "apiKey",
|
||||
choiceLabel: "Anthropic API key",
|
||||
groupId: "anthropic",
|
||||
groupLabel: "Anthropic",
|
||||
groupHint: "Claude CLI + API key",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -18,7 +18,10 @@ import {
|
||||
upsertAuthProfile,
|
||||
validateAnthropicSetupToken,
|
||||
} from "openclaw/plugin-sdk/provider-auth";
|
||||
import { cloneFirstTemplateModel } from "openclaw/plugin-sdk/provider-model-shared";
|
||||
import {
|
||||
cloneFirstTemplateModel,
|
||||
type ProviderPlugin,
|
||||
} from "openclaw/plugin-sdk/provider-model-shared";
|
||||
import { fetchClaudeUsage } from "openclaw/plugin-sdk/provider-usage";
|
||||
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
|
||||
import * as claudeCliAuth from "./cli-auth-seam.js";
|
||||
@@ -395,11 +398,10 @@ async function runAnthropicCliMigrationNonInteractive(ctx: {
|
||||
};
|
||||
}
|
||||
|
||||
export function registerAnthropicPlugin(api: OpenClawPluginApi): void {
|
||||
export function buildAnthropicProvider(): ProviderPlugin {
|
||||
const providerId = "anthropic";
|
||||
const defaultAnthropicModel = DEFAULT_ANTHROPIC_MODEL;
|
||||
api.registerCliBackend(buildAnthropicCliBackend());
|
||||
api.registerProvider({
|
||||
return {
|
||||
id: providerId,
|
||||
label: "Anthropic",
|
||||
docsPath: "/providers/models",
|
||||
@@ -505,6 +507,11 @@ export function registerAnthropicPlugin(api: OpenClawPluginApi): void {
|
||||
store: ctx.store,
|
||||
profileId: ctx.profileId,
|
||||
}),
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function registerAnthropicPlugin(api: OpenClawPluginApi): void {
|
||||
api.registerCliBackend(buildAnthropicCliBackend());
|
||||
api.registerProvider(buildAnthropicProvider());
|
||||
api.registerMediaUnderstandingProvider(anthropicMediaUnderstandingProvider);
|
||||
}
|
||||
|
||||
@@ -164,4 +164,48 @@ describe("arcee provider plugin", () => {
|
||||
} as never),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it("canonicalizes stale OpenRouter /v1 config and transport metadata", async () => {
|
||||
const provider = await registerSingleProviderPlugin(arceePlugin);
|
||||
|
||||
expect(
|
||||
provider.normalizeConfig?.({
|
||||
provider: "arcee",
|
||||
providerConfig: {
|
||||
api: "openai-completions",
|
||||
baseUrl: "https://openrouter.ai/v1/",
|
||||
models: [],
|
||||
},
|
||||
} as never),
|
||||
).toMatchObject({
|
||||
baseUrl: "https://openrouter.ai/api/v1",
|
||||
});
|
||||
|
||||
expect(
|
||||
provider.normalizeResolvedModel?.({
|
||||
modelId: "arcee/trinity-large-thinking",
|
||||
model: {
|
||||
provider: "arcee",
|
||||
id: "trinity-large-thinking",
|
||||
name: "Trinity Large Thinking",
|
||||
api: "openai-completions",
|
||||
baseUrl: "https://openrouter.ai/v1",
|
||||
},
|
||||
} as never),
|
||||
).toMatchObject({
|
||||
id: "arcee/trinity-large-thinking",
|
||||
baseUrl: "https://openrouter.ai/api/v1",
|
||||
});
|
||||
|
||||
expect(
|
||||
provider.normalizeTransport?.({
|
||||
provider: "arcee",
|
||||
api: "openai-completions",
|
||||
baseUrl: "https://openrouter.ai/v1",
|
||||
} as never),
|
||||
).toEqual({
|
||||
api: "openai-completions",
|
||||
baseUrl: "https://openrouter.ai/api/v1",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
type ProviderCatalogContext,
|
||||
} from "openclaw/plugin-sdk/provider-catalog-shared";
|
||||
import { OPENAI_COMPATIBLE_REPLAY_HOOKS } from "openclaw/plugin-sdk/provider-model-shared";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/provider-onboard";
|
||||
import {
|
||||
applyArceeConfig,
|
||||
applyArceeOpenRouterConfig,
|
||||
@@ -15,7 +14,7 @@ import {
|
||||
import {
|
||||
buildArceeProvider,
|
||||
buildArceeOpenRouterProvider,
|
||||
isArceeOpenRouterBaseUrl,
|
||||
normalizeArceeOpenRouterBaseUrl,
|
||||
toArceeOpenRouterModelId,
|
||||
} from "./provider-catalog.js";
|
||||
|
||||
@@ -70,13 +69,6 @@ function buildArceeAuthMethods() {
|
||||
];
|
||||
}
|
||||
|
||||
function readConfiguredArceeCatalogEntries(config: OpenClawConfig | undefined) {
|
||||
return readConfiguredProviderCatalogEntries({
|
||||
config,
|
||||
providerId: PROVIDER_ID,
|
||||
});
|
||||
}
|
||||
|
||||
async function resolveArceeCatalog(ctx: ProviderCatalogContext) {
|
||||
const directKey = ctx.resolveProviderApiKey(PROVIDER_ID).apiKey;
|
||||
if (directKey) {
|
||||
@@ -94,12 +86,18 @@ async function resolveArceeCatalog(ctx: ProviderCatalogContext) {
|
||||
function normalizeArceeResolvedModel<T extends { baseUrl?: string; id: string }>(
|
||||
model: T,
|
||||
): T | undefined {
|
||||
if (!isArceeOpenRouterBaseUrl(model.baseUrl)) {
|
||||
const normalizedBaseUrl = normalizeArceeOpenRouterBaseUrl(model.baseUrl);
|
||||
if (!normalizedBaseUrl) {
|
||||
return undefined;
|
||||
}
|
||||
const normalizedId = toArceeOpenRouterModelId(model.id);
|
||||
if (normalizedId === model.id && normalizedBaseUrl === model.baseUrl) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
...model,
|
||||
id: toArceeOpenRouterModelId(model.id),
|
||||
id: normalizedId,
|
||||
baseUrl: normalizedBaseUrl,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -117,8 +115,27 @@ export default definePluginEntry({
|
||||
catalog: {
|
||||
run: resolveArceeCatalog,
|
||||
},
|
||||
augmentModelCatalog: ({ config }) => readConfiguredArceeCatalogEntries(config),
|
||||
augmentModelCatalog: ({ config }) =>
|
||||
readConfiguredProviderCatalogEntries({
|
||||
config,
|
||||
providerId: PROVIDER_ID,
|
||||
}),
|
||||
normalizeConfig: ({ providerConfig }) => {
|
||||
const normalizedBaseUrl = normalizeArceeOpenRouterBaseUrl(providerConfig.baseUrl);
|
||||
return normalizedBaseUrl && normalizedBaseUrl !== providerConfig.baseUrl
|
||||
? { ...providerConfig, baseUrl: normalizedBaseUrl }
|
||||
: undefined;
|
||||
},
|
||||
normalizeResolvedModel: ({ model }) => normalizeArceeResolvedModel(model),
|
||||
normalizeTransport: ({ api, baseUrl }) => {
|
||||
const normalizedBaseUrl = normalizeArceeOpenRouterBaseUrl(baseUrl);
|
||||
return normalizedBaseUrl && normalizedBaseUrl !== baseUrl
|
||||
? {
|
||||
api,
|
||||
baseUrl: normalizedBaseUrl,
|
||||
}
|
||||
: undefined;
|
||||
},
|
||||
...OPENAI_COMPATIBLE_REPLAY_HOOKS,
|
||||
});
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/arcee-provider",
|
||||
"version": "2026.4.16",
|
||||
"version": "2026.4.19-beta.1",
|
||||
"private": true,
|
||||
"description": "OpenClaw Arcee provider plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -2,13 +2,25 @@ import type { ModelProviderConfig } from "openclaw/plugin-sdk/provider-model-sha
|
||||
import { buildArceeModelDefinition, ARCEE_BASE_URL, ARCEE_MODEL_CATALOG } from "./models.js";
|
||||
|
||||
export const OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1";
|
||||
const OPENROUTER_LEGACY_BASE_URL = "https://openrouter.ai/v1";
|
||||
|
||||
function normalizeBaseUrl(baseUrl: string | undefined): string {
|
||||
return (baseUrl ?? "").trim().replace(/\/+$/, "");
|
||||
}
|
||||
|
||||
export function normalizeArceeOpenRouterBaseUrl(baseUrl: string | undefined): string | undefined {
|
||||
const normalized = normalizeBaseUrl(baseUrl);
|
||||
if (!normalized) {
|
||||
return undefined;
|
||||
}
|
||||
if (normalized === OPENROUTER_BASE_URL || normalized === OPENROUTER_LEGACY_BASE_URL) {
|
||||
return OPENROUTER_BASE_URL;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function isArceeOpenRouterBaseUrl(baseUrl: string | undefined): boolean {
|
||||
return normalizeBaseUrl(baseUrl) === OPENROUTER_BASE_URL;
|
||||
return normalizeArceeOpenRouterBaseUrl(baseUrl) === OPENROUTER_BASE_URL;
|
||||
}
|
||||
|
||||
export function toArceeOpenRouterModelId(modelId: string): string {
|
||||
@@ -24,10 +36,9 @@ export function buildArceeCatalogModels(): NonNullable<ModelProviderConfig["mode
|
||||
}
|
||||
|
||||
export function buildArceeOpenRouterCatalogModels(): NonNullable<ModelProviderConfig["models"]> {
|
||||
return buildArceeCatalogModels().map((model) => ({
|
||||
...model,
|
||||
id: toArceeOpenRouterModelId(model.id),
|
||||
}));
|
||||
return buildArceeCatalogModels().map((model) =>
|
||||
Object.assign({}, model, { id: toArceeOpenRouterModelId(model.id) }),
|
||||
);
|
||||
}
|
||||
|
||||
export function buildArceeProvider(): ModelProviderConfig {
|
||||
|
||||
@@ -2,3 +2,7 @@ export {
|
||||
collectRuntimeConfigAssignments,
|
||||
secretTargetRegistryEntries,
|
||||
} from "./src/secret-contract.js";
|
||||
export {
|
||||
__testing as blueBubblesConversationBindingTesting,
|
||||
createBlueBubblesConversationBindingManager,
|
||||
} from "./src/conversation-bindings.js";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/bluebubbles",
|
||||
"version": "2026.4.16",
|
||||
"version": "2026.4.19-beta.1",
|
||||
"description": "OpenClaw BlueBubbles channel plugin",
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
@@ -8,7 +8,7 @@
|
||||
"openclaw": "workspace:*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"openclaw": ">=2026.4.16"
|
||||
"openclaw": ">=2026.4.19-beta.1"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"openclaw": {
|
||||
@@ -43,10 +43,10 @@
|
||||
"minHostVersion": ">=2026.4.10"
|
||||
},
|
||||
"compat": {
|
||||
"pluginApi": ">=2026.4.16"
|
||||
"pluginApi": ">=2026.4.19-beta.1"
|
||||
},
|
||||
"build": {
|
||||
"openclawVersion": "2026.4.16"
|
||||
"openclawVersion": "2026.4.19-beta.1"
|
||||
},
|
||||
"release": {
|
||||
"publishToClawHub": true,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/brave-plugin",
|
||||
"version": "2026.4.16",
|
||||
"version": "2026.4.19-beta.1",
|
||||
"private": true,
|
||||
"description": "OpenClaw Brave plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import fs from "node:fs";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { validateJsonSchemaValue } from "../../../src/plugins/schema-validator.js";
|
||||
import { __testing, createBraveWebSearchProvider } from "./brave-web-search-provider.js";
|
||||
import { __testing } from "../test-api.js";
|
||||
import { createBraveWebSearchProvider } from "./brave-web-search-provider.js";
|
||||
|
||||
const braveManifest = JSON.parse(
|
||||
fs.readFileSync(new URL("../openclaw.plugin.json", import.meta.url), "utf-8"),
|
||||
|
||||
@@ -3,25 +3,69 @@ import type {
|
||||
WebSearchProviderPlugin,
|
||||
WebSearchProviderToolDefinition,
|
||||
} from "openclaw/plugin-sdk/provider-web-search";
|
||||
import { isRecord } from "openclaw/plugin-sdk/text-runtime";
|
||||
import {
|
||||
createBraveSchema,
|
||||
mapBraveLlmContextResults,
|
||||
normalizeBraveCountry,
|
||||
normalizeBraveLanguageParams,
|
||||
resolveBraveConfig,
|
||||
resolveBraveMode,
|
||||
} from "./brave-web-search-provider.shared.js";
|
||||
import { createWebSearchProviderContractFields } from "openclaw/plugin-sdk/provider-web-search-config-contract";
|
||||
|
||||
type ConfigInput = Parameters<
|
||||
NonNullable<WebSearchProviderPlugin["getConfiguredCredentialValue"]>
|
||||
>[0];
|
||||
type ConfigTarget = Parameters<
|
||||
NonNullable<WebSearchProviderPlugin["setConfiguredCredentialValue"]>
|
||||
>[0];
|
||||
const BRAVE_CREDENTIAL_PATH = "plugins.entries.brave.config.webSearch.apiKey";
|
||||
|
||||
type BraveWebSearchRuntime = typeof import("./brave-web-search-provider.runtime.js");
|
||||
|
||||
let braveWebSearchRuntimePromise: Promise<BraveWebSearchRuntime> | undefined;
|
||||
|
||||
function loadBraveWebSearchRuntime(): Promise<BraveWebSearchRuntime> {
|
||||
braveWebSearchRuntimePromise ??= import("./brave-web-search-provider.runtime.js");
|
||||
return braveWebSearchRuntimePromise;
|
||||
}
|
||||
|
||||
const BraveSearchSchema = {
|
||||
type: "object",
|
||||
properties: {
|
||||
query: { type: "string", description: "Search query string." },
|
||||
count: {
|
||||
type: "number",
|
||||
description: "Number of results to return (1-10).",
|
||||
minimum: 1,
|
||||
maximum: 10,
|
||||
},
|
||||
country: {
|
||||
type: "string",
|
||||
description:
|
||||
"2-letter country code for region-specific results (e.g., 'DE', 'US', 'ALL'). Default: 'US'.",
|
||||
},
|
||||
language: {
|
||||
type: "string",
|
||||
description: "ISO 639-1 language code for results (e.g., 'en', 'de', 'fr').",
|
||||
},
|
||||
freshness: {
|
||||
type: "string",
|
||||
description: "Filter by time: 'day' (24h), 'week', 'month', or 'year'.",
|
||||
},
|
||||
date_after: {
|
||||
type: "string",
|
||||
description: "Only results published after this date (YYYY-MM-DD).",
|
||||
},
|
||||
date_before: {
|
||||
type: "string",
|
||||
description: "Only results published before this date (YYYY-MM-DD).",
|
||||
},
|
||||
search_lang: {
|
||||
type: "string",
|
||||
description:
|
||||
"Brave language code for search results (e.g., 'en', 'de', 'en-gb', 'zh-hans', 'zh-hant', 'pt-br').",
|
||||
},
|
||||
ui_lang: {
|
||||
type: "string",
|
||||
description:
|
||||
"Locale code for UI elements in language-region format (e.g., 'en-US', 'de-DE', 'fr-FR', 'tr-TR'). Must include region subtag.",
|
||||
},
|
||||
},
|
||||
} satisfies Record<string, unknown>;
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function resolveProviderWebSearchPluginConfig(
|
||||
config: ConfigInput,
|
||||
config: unknown,
|
||||
pluginId: string,
|
||||
): Record<string, unknown> | undefined {
|
||||
if (!isRecord(config)) {
|
||||
@@ -34,40 +78,6 @@ function resolveProviderWebSearchPluginConfig(
|
||||
return isRecord(pluginConfig?.webSearch) ? pluginConfig.webSearch : undefined;
|
||||
}
|
||||
|
||||
function ensureObject(target: Record<string, unknown>, key: string): Record<string, unknown> {
|
||||
const current = target[key];
|
||||
if (isRecord(current)) {
|
||||
return current;
|
||||
}
|
||||
const next: Record<string, unknown> = {};
|
||||
target[key] = next;
|
||||
return next;
|
||||
}
|
||||
|
||||
function setProviderWebSearchPluginConfigValue(
|
||||
configTarget: ConfigTarget,
|
||||
pluginId: string,
|
||||
key: string,
|
||||
value: unknown,
|
||||
): void {
|
||||
const plugins = ensureObject(configTarget as Record<string, unknown>, "plugins");
|
||||
const entries = ensureObject(plugins, "entries");
|
||||
const entry = ensureObject(entries, pluginId);
|
||||
if (entry.enabled === undefined) {
|
||||
entry.enabled = true;
|
||||
}
|
||||
const config = ensureObject(entry, "config");
|
||||
const webSearch = ensureObject(config, "webSearch");
|
||||
webSearch[key] = value;
|
||||
}
|
||||
|
||||
function setTopLevelCredentialValue(
|
||||
searchConfigTarget: Record<string, unknown>,
|
||||
value: unknown,
|
||||
): void {
|
||||
searchConfigTarget.apiKey = value;
|
||||
}
|
||||
|
||||
function mergeScopedSearchConfig(
|
||||
searchConfig: Record<string, unknown> | undefined,
|
||||
key: string,
|
||||
@@ -94,19 +104,24 @@ function mergeScopedSearchConfig(
|
||||
return next;
|
||||
}
|
||||
|
||||
function resolveBraveMode(searchConfig?: Record<string, unknown>): "web" | "llm-context" {
|
||||
const brave = isRecord(searchConfig?.brave) ? searchConfig.brave : undefined;
|
||||
return brave?.mode === "llm-context" ? "llm-context" : "web";
|
||||
}
|
||||
|
||||
function createBraveToolDefinition(
|
||||
searchConfig?: SearchConfigRecord,
|
||||
): WebSearchProviderToolDefinition {
|
||||
const braveMode = resolveBraveMode(resolveBraveConfig(searchConfig));
|
||||
const braveMode = resolveBraveMode(searchConfig);
|
||||
|
||||
return {
|
||||
description:
|
||||
braveMode === "llm-context"
|
||||
? "Search the web using Brave Search LLM Context API. Returns pre-extracted page content (text chunks, tables, code blocks) optimized for LLM grounding."
|
||||
: "Search the web using Brave Search API. Supports region-specific and localized search via country and language parameters. Returns titles, URLs, and snippets for fast research.",
|
||||
parameters: createBraveSchema(),
|
||||
parameters: BraveSearchSchema,
|
||||
execute: async (args) => {
|
||||
const { executeBraveSearch } = await import("./brave-web-search-provider.runtime.js");
|
||||
const { executeBraveSearch } = await loadBraveWebSearchRuntime();
|
||||
return await executeBraveSearch(args, searchConfig);
|
||||
},
|
||||
};
|
||||
@@ -124,15 +139,12 @@ export function createBraveWebSearchProvider(): WebSearchProviderPlugin {
|
||||
signupUrl: "https://brave.com/search/api/",
|
||||
docsUrl: "https://docs.openclaw.ai/brave-search",
|
||||
autoDetectOrder: 10,
|
||||
credentialPath: "plugins.entries.brave.config.webSearch.apiKey",
|
||||
inactiveSecretPaths: ["plugins.entries.brave.config.webSearch.apiKey"],
|
||||
getCredentialValue: (searchConfig) => searchConfig?.apiKey,
|
||||
setCredentialValue: setTopLevelCredentialValue,
|
||||
getConfiguredCredentialValue: (config) =>
|
||||
resolveProviderWebSearchPluginConfig(config, "brave")?.apiKey,
|
||||
setConfiguredCredentialValue: (configTarget, value) => {
|
||||
setProviderWebSearchPluginConfigValue(configTarget, "brave", "apiKey", value);
|
||||
},
|
||||
credentialPath: BRAVE_CREDENTIAL_PATH,
|
||||
...createWebSearchProviderContractFields({
|
||||
credentialPath: BRAVE_CREDENTIAL_PATH,
|
||||
searchCredential: { type: "top-level" },
|
||||
configuredCredential: { pluginId: "brave" },
|
||||
}),
|
||||
createTool: (ctx) =>
|
||||
createBraveToolDefinition(
|
||||
mergeScopedSearchConfig(
|
||||
@@ -144,10 +156,3 @@ export function createBraveWebSearchProvider(): WebSearchProviderPlugin {
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
export const __testing = {
|
||||
normalizeBraveCountry,
|
||||
normalizeBraveLanguageParams,
|
||||
resolveBraveMode,
|
||||
mapBraveLlmContextResults,
|
||||
} as const;
|
||||
|
||||
@@ -1 +1,13 @@
|
||||
export { __testing } from "./src/brave-web-search-provider.js";
|
||||
import {
|
||||
mapBraveLlmContextResults,
|
||||
normalizeBraveCountry,
|
||||
normalizeBraveLanguageParams,
|
||||
resolveBraveMode,
|
||||
} from "./src/brave-web-search-provider.shared.js";
|
||||
|
||||
export const __testing = {
|
||||
normalizeBraveCountry,
|
||||
normalizeBraveLanguageParams,
|
||||
resolveBraveMode,
|
||||
mapBraveLlmContextResults,
|
||||
} as const;
|
||||
|
||||
@@ -1 +1 @@
|
||||
export { __testing, createBraveWebSearchProvider } from "./src/brave-web-search-provider.js";
|
||||
export { createBraveWebSearchProvider } from "./src/brave-web-search-provider.js";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/browser-plugin",
|
||||
"version": "2026.4.16",
|
||||
"version": "2026.4.19-beta.1",
|
||||
"private": true,
|
||||
"description": "OpenClaw browser tool plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolveCdpReachabilityPolicy } from "./cdp-reachability-policy.js";
|
||||
import type { ResolvedBrowserProfile } from "./config.js";
|
||||
import { assertBrowserNavigationAllowed } from "./navigation-guard.js";
|
||||
|
||||
function createProfile(overrides: Partial<ResolvedBrowserProfile>): ResolvedBrowserProfile {
|
||||
return {
|
||||
name: "remote",
|
||||
cdpPort: 9223,
|
||||
cdpUrl: "http://172.29.128.1:9223",
|
||||
cdpHost: "172.29.128.1",
|
||||
cdpIsLoopback: false,
|
||||
color: "#123456",
|
||||
driver: "openclaw",
|
||||
attachOnly: false,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("CDP reachability policy", () => {
|
||||
it("allows the selected remote profile CDP host without widening browser navigation policy", async () => {
|
||||
const browserPolicy = {};
|
||||
const profile = createProfile({});
|
||||
|
||||
expect(resolveCdpReachabilityPolicy(profile, browserPolicy)).toEqual({
|
||||
allowedHostnames: ["172.29.128.1"],
|
||||
});
|
||||
expect(browserPolicy).toEqual({});
|
||||
await expect(
|
||||
assertBrowserNavigationAllowed({
|
||||
url: "http://172.29.128.1/",
|
||||
ssrfPolicy: browserPolicy,
|
||||
}),
|
||||
).rejects.toThrow(/private\/internal\/special-use ip address/i);
|
||||
});
|
||||
|
||||
it("merges the selected remote profile CDP host with existing CDP policy hostnames", () => {
|
||||
const profile = createProfile({});
|
||||
|
||||
expect(
|
||||
resolveCdpReachabilityPolicy(profile, {
|
||||
allowedHostnames: ["metadata.internal"],
|
||||
}),
|
||||
).toEqual({
|
||||
allowedHostnames: ["metadata.internal", "172.29.128.1"],
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps local managed loopback CDP control outside browser SSRF policy", () => {
|
||||
const profile = createProfile({
|
||||
cdpUrl: "http://127.0.0.1:18800",
|
||||
cdpHost: "127.0.0.1",
|
||||
cdpIsLoopback: true,
|
||||
});
|
||||
|
||||
expect(resolveCdpReachabilityPolicy(profile, {})).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,20 @@
|
||||
import type { SsrFPolicy } from "../infra/net/ssrf.js";
|
||||
import { isPrivateNetworkAllowedByPolicy, type SsrFPolicy } from "../infra/net/ssrf.js";
|
||||
import type { ResolvedBrowserProfile } from "./config.js";
|
||||
import { getBrowserProfileCapabilities } from "./profile-capabilities.js";
|
||||
import { withAllowedHostname } from "./ssrf-policy-helpers.js";
|
||||
|
||||
function withCdpHostnameAllowed(
|
||||
profile: ResolvedBrowserProfile,
|
||||
ssrfPolicy?: SsrFPolicy,
|
||||
): SsrFPolicy | undefined {
|
||||
if (!ssrfPolicy || !profile.cdpHost) {
|
||||
return ssrfPolicy;
|
||||
}
|
||||
if (isPrivateNetworkAllowedByPolicy(ssrfPolicy)) {
|
||||
return ssrfPolicy;
|
||||
}
|
||||
return withAllowedHostname(ssrfPolicy, profile.cdpHost);
|
||||
}
|
||||
|
||||
export function resolveCdpReachabilityPolicy(
|
||||
profile: ResolvedBrowserProfile,
|
||||
@@ -13,7 +27,7 @@ export function resolveCdpReachabilityPolicy(
|
||||
if (!capabilities.isRemote && profile.cdpIsLoopback && profile.driver === "openclaw") {
|
||||
return undefined;
|
||||
}
|
||||
return ssrfPolicy;
|
||||
return withCdpHostnameAllowed(profile, ssrfPolicy);
|
||||
}
|
||||
|
||||
export const resolveCdpControlPolicy = resolveCdpReachabilityPolicy;
|
||||
|
||||
@@ -13,6 +13,7 @@ import { getDirectAgentForCdp, withNoProxyForCdpUrl } from "./cdp-proxy-bypass.j
|
||||
import { CDP_HTTP_REQUEST_TIMEOUT_MS, CDP_WS_HANDSHAKE_TIMEOUT_MS } from "./cdp-timeouts.js";
|
||||
import { BrowserCdpEndpointBlockedError } from "./errors.js";
|
||||
import { resolveBrowserRateLimitMessage } from "./rate-limit-message.js";
|
||||
import { withAllowedHostname } from "./ssrf-policy-helpers.js";
|
||||
|
||||
export { isLoopbackHost };
|
||||
|
||||
@@ -70,12 +71,7 @@ export async function assertCdpEndpointAllowed(
|
||||
}
|
||||
try {
|
||||
const policy = isLoopbackHost(parsed.hostname)
|
||||
? {
|
||||
...ssrfPolicy,
|
||||
allowedHostnames: Array.from(
|
||||
new Set([...(ssrfPolicy?.allowedHostnames ?? []), parsed.hostname]),
|
||||
),
|
||||
}
|
||||
? withAllowedHostname(ssrfPolicy, parsed.hostname)
|
||||
: ssrfPolicy;
|
||||
await resolvePinnedHostnameWithPolicy(parsed.hostname, {
|
||||
policy,
|
||||
@@ -273,12 +269,7 @@ export async function fetchCdpChecked(
|
||||
const res = await withNoProxyForCdpUrl(url, async () => {
|
||||
const parsedUrl = new URL(url);
|
||||
const policy = isLoopbackHost(parsedUrl.hostname)
|
||||
? {
|
||||
...ssrfPolicy,
|
||||
allowedHostnames: Array.from(
|
||||
new Set([...(ssrfPolicy?.allowedHostnames ?? []), parsedUrl.hostname]),
|
||||
),
|
||||
}
|
||||
? withAllowedHostname(ssrfPolicy, parsedUrl.hostname)
|
||||
: (ssrfPolicy ?? { allowPrivateNetwork: true });
|
||||
const guarded = await fetchWithSsrFGuard({
|
||||
url,
|
||||
|
||||
@@ -404,6 +404,14 @@ describe("cdp", () => {
|
||||
expect(normalized).toBe("wss://user:pass@example.com/devtools/browser/ABC?token=abc");
|
||||
});
|
||||
|
||||
it("normalizes loopback websocket aliases to the configured CDP loopback host", () => {
|
||||
const normalized = normalizeCdpWsUrl(
|
||||
"ws://localhost.:18800/devtools/browser/ABC",
|
||||
"http://127.0.0.1:18800",
|
||||
);
|
||||
expect(normalized).toBe("ws://127.0.0.1:18800/devtools/browser/ABC");
|
||||
});
|
||||
|
||||
it("rewrites 0.0.0.0 wildcard bind address to remote CDP host", () => {
|
||||
const normalized = normalizeCdpWsUrl(
|
||||
"ws://0.0.0.0:3000/devtools/browser/ABC",
|
||||
|
||||
@@ -31,6 +31,8 @@ export function normalizeCdpWsUrl(wsUrl: string, cdpUrl: string): string {
|
||||
ws.port = cdpPort;
|
||||
}
|
||||
ws.protocol = cdp.protocol === "https:" ? "wss:" : "ws:";
|
||||
} else if (isLoopbackHost(ws.hostname) && isLoopbackHost(cdp.hostname)) {
|
||||
ws.hostname = cdp.hostname;
|
||||
}
|
||||
if (cdp.protocol === "https:" && ws.protocol === "ws:") {
|
||||
ws.protocol = "wss:";
|
||||
|
||||
342
extensions/browser/src/browser/chrome.diagnostics.ts
Normal file
342
extensions/browser/src/browser/chrome.diagnostics.ts
Normal file
@@ -0,0 +1,342 @@
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
|
||||
import type { SsrFPolicy } from "../infra/net/ssrf.js";
|
||||
import { rawDataToString } from "../infra/ws.js";
|
||||
import { redactSensitiveText } from "../logging/redact.js";
|
||||
import { CHROME_REACHABILITY_TIMEOUT_MS, CHROME_WS_READY_TIMEOUT_MS } from "./cdp-timeouts.js";
|
||||
import {
|
||||
appendCdpPath,
|
||||
assertCdpEndpointAllowed,
|
||||
fetchCdpChecked,
|
||||
isWebSocketUrl,
|
||||
openCdpWebSocket,
|
||||
redactCdpUrl,
|
||||
} from "./cdp.helpers.js";
|
||||
import { normalizeCdpWsUrl } from "./cdp.js";
|
||||
import { BrowserCdpEndpointBlockedError } from "./errors.js";
|
||||
|
||||
export type ChromeCdpDiagnosticCode =
|
||||
| "ssrf_blocked"
|
||||
| "http_unreachable"
|
||||
| "http_status_failed"
|
||||
| "invalid_json"
|
||||
| "missing_websocket_debugger_url"
|
||||
| "websocket_ssrf_blocked"
|
||||
| "websocket_handshake_failed"
|
||||
| "websocket_health_command_failed"
|
||||
| "websocket_health_command_timeout";
|
||||
|
||||
export type ChromeCdpDiagnostic =
|
||||
| {
|
||||
ok: true;
|
||||
cdpUrl: string;
|
||||
wsUrl: string;
|
||||
browser?: string;
|
||||
userAgent?: string;
|
||||
elapsedMs: number;
|
||||
}
|
||||
| {
|
||||
ok: false;
|
||||
code: ChromeCdpDiagnosticCode;
|
||||
cdpUrl: string;
|
||||
wsUrl?: string;
|
||||
message: string;
|
||||
elapsedMs: number;
|
||||
};
|
||||
|
||||
export type ChromeVersion = {
|
||||
webSocketDebuggerUrl?: string;
|
||||
Browser?: string;
|
||||
"User-Agent"?: string;
|
||||
};
|
||||
|
||||
function elapsedSince(startedAt: number): number {
|
||||
return Math.max(0, Date.now() - startedAt);
|
||||
}
|
||||
|
||||
export function safeChromeCdpErrorMessage(error: unknown): string {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
return redactSensitiveText(message || "unknown error");
|
||||
}
|
||||
|
||||
function failureDiagnostic(params: {
|
||||
cdpUrl: string;
|
||||
code: ChromeCdpDiagnosticCode;
|
||||
message: string;
|
||||
startedAt: number;
|
||||
wsUrl?: string;
|
||||
}): ChromeCdpDiagnostic {
|
||||
return {
|
||||
ok: false,
|
||||
cdpUrl: params.cdpUrl,
|
||||
wsUrl: params.wsUrl,
|
||||
code: params.code,
|
||||
message: redactSensitiveText(params.message),
|
||||
elapsedMs: elapsedSince(params.startedAt),
|
||||
};
|
||||
}
|
||||
|
||||
export async function readChromeVersion(
|
||||
cdpUrl: string,
|
||||
timeoutMs = CHROME_REACHABILITY_TIMEOUT_MS,
|
||||
ssrfPolicy?: SsrFPolicy,
|
||||
): Promise<ChromeVersion> {
|
||||
const ctrl = new AbortController();
|
||||
const t = setTimeout(ctrl.abort.bind(ctrl), timeoutMs);
|
||||
try {
|
||||
const versionUrl = appendCdpPath(cdpUrl, "/json/version");
|
||||
const { response, release } = await fetchCdpChecked(
|
||||
versionUrl,
|
||||
timeoutMs,
|
||||
{ signal: ctrl.signal },
|
||||
ssrfPolicy,
|
||||
);
|
||||
try {
|
||||
const data = (await response.json()) as ChromeVersion;
|
||||
if (!data || typeof data !== "object") {
|
||||
throw new Error("CDP /json/version returned non-object JSON");
|
||||
}
|
||||
return data;
|
||||
} finally {
|
||||
await release();
|
||||
}
|
||||
} finally {
|
||||
clearTimeout(t);
|
||||
}
|
||||
}
|
||||
|
||||
type CdpHealthDiagnostic =
|
||||
| { ok: true }
|
||||
| {
|
||||
ok: false;
|
||||
code:
|
||||
| "websocket_handshake_failed"
|
||||
| "websocket_health_command_failed"
|
||||
| "websocket_health_command_timeout";
|
||||
message: string;
|
||||
};
|
||||
|
||||
async function diagnoseCdpHealthCommand(
|
||||
wsUrl: string,
|
||||
timeoutMs = CHROME_WS_READY_TIMEOUT_MS,
|
||||
): Promise<CdpHealthDiagnostic> {
|
||||
return await new Promise<CdpHealthDiagnostic>((resolve) => {
|
||||
const ws = openCdpWebSocket(wsUrl, {
|
||||
handshakeTimeoutMs: timeoutMs,
|
||||
});
|
||||
let settled = false;
|
||||
let opened = false;
|
||||
const onMessage = (raw: Parameters<typeof rawDataToString>[0]) => {
|
||||
if (settled) {
|
||||
return;
|
||||
}
|
||||
let parsed: { id?: unknown; result?: unknown } | null = null;
|
||||
try {
|
||||
parsed = JSON.parse(rawDataToString(raw)) as { id?: unknown; result?: unknown };
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
if (parsed?.id !== 1) {
|
||||
return;
|
||||
}
|
||||
if (parsed.result && typeof parsed.result === "object") {
|
||||
finish({ ok: true });
|
||||
return;
|
||||
}
|
||||
finish({
|
||||
ok: false,
|
||||
code: "websocket_health_command_failed",
|
||||
message: "Browser.getVersion returned no result object",
|
||||
});
|
||||
};
|
||||
|
||||
const finish = (value: CdpHealthDiagnostic) => {
|
||||
if (settled) {
|
||||
return;
|
||||
}
|
||||
settled = true;
|
||||
clearTimeout(timer);
|
||||
ws.off("message", onMessage);
|
||||
try {
|
||||
ws.close();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
resolve(value);
|
||||
};
|
||||
const timer = setTimeout(
|
||||
() => {
|
||||
try {
|
||||
ws.terminate();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
finish({
|
||||
ok: false,
|
||||
code: opened ? "websocket_health_command_timeout" : "websocket_handshake_failed",
|
||||
message: opened
|
||||
? `Browser.getVersion did not respond within ${timeoutMs}ms`
|
||||
: `WebSocket handshake did not complete within ${timeoutMs}ms`,
|
||||
});
|
||||
},
|
||||
Math.max(50, timeoutMs + 25),
|
||||
);
|
||||
|
||||
ws.once("open", () => {
|
||||
opened = true;
|
||||
try {
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
id: 1,
|
||||
method: "Browser.getVersion",
|
||||
}),
|
||||
);
|
||||
} catch (err) {
|
||||
finish({
|
||||
ok: false,
|
||||
code: "websocket_health_command_failed",
|
||||
message: safeChromeCdpErrorMessage(err),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
ws.on("message", onMessage);
|
||||
|
||||
ws.once("error", (err) => {
|
||||
finish({
|
||||
ok: false,
|
||||
code: opened ? "websocket_health_command_failed" : "websocket_handshake_failed",
|
||||
message: safeChromeCdpErrorMessage(err),
|
||||
});
|
||||
});
|
||||
ws.once("close", () => {
|
||||
finish({
|
||||
ok: false,
|
||||
code: opened ? "websocket_health_command_failed" : "websocket_handshake_failed",
|
||||
message: opened
|
||||
? "WebSocket closed before Browser.getVersion completed"
|
||||
: "WebSocket closed before handshake completed",
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function classifyChromeVersionError(error: unknown): {
|
||||
code: ChromeCdpDiagnosticCode;
|
||||
message: string;
|
||||
} {
|
||||
const message = safeChromeCdpErrorMessage(error);
|
||||
if (error instanceof BrowserCdpEndpointBlockedError) {
|
||||
return { code: "ssrf_blocked", message };
|
||||
}
|
||||
if (/^HTTP \d+/.test(message)) {
|
||||
return { code: "http_status_failed", message };
|
||||
}
|
||||
if (error instanceof SyntaxError || message.includes("non-object JSON")) {
|
||||
return { code: "invalid_json", message };
|
||||
}
|
||||
return { code: "http_unreachable", message };
|
||||
}
|
||||
|
||||
export function formatChromeCdpDiagnostic(diagnostic: ChromeCdpDiagnostic): string {
|
||||
const redactedCdpUrl = redactCdpUrl(diagnostic.cdpUrl) ?? diagnostic.cdpUrl;
|
||||
const redactedWsUrl = redactCdpUrl(diagnostic.wsUrl) ?? diagnostic.wsUrl;
|
||||
if (diagnostic.ok) {
|
||||
const browser = diagnostic.browser ? ` browser=${diagnostic.browser}` : "";
|
||||
return `CDP diagnostic: ready after ${diagnostic.elapsedMs}ms; cdp=${redactedCdpUrl}; websocket=${redactedWsUrl}.${browser}`;
|
||||
}
|
||||
const websocket = redactedWsUrl ? `; websocket=${redactedWsUrl}` : "";
|
||||
return `CDP diagnostic: ${diagnostic.code} after ${diagnostic.elapsedMs}ms; cdp=${redactedCdpUrl}${websocket}; ${diagnostic.message}.`;
|
||||
}
|
||||
|
||||
export async function diagnoseChromeCdp(
|
||||
cdpUrl: string,
|
||||
timeoutMs = CHROME_REACHABILITY_TIMEOUT_MS,
|
||||
handshakeTimeoutMs = CHROME_WS_READY_TIMEOUT_MS,
|
||||
ssrfPolicy?: SsrFPolicy,
|
||||
): Promise<ChromeCdpDiagnostic> {
|
||||
const startedAt = Date.now();
|
||||
try {
|
||||
await assertCdpEndpointAllowed(cdpUrl, ssrfPolicy);
|
||||
} catch (err) {
|
||||
return failureDiagnostic({
|
||||
cdpUrl,
|
||||
code: "ssrf_blocked",
|
||||
message: safeChromeCdpErrorMessage(err),
|
||||
startedAt,
|
||||
});
|
||||
}
|
||||
|
||||
if (isWebSocketUrl(cdpUrl)) {
|
||||
const health = await diagnoseCdpHealthCommand(cdpUrl, handshakeTimeoutMs);
|
||||
if (!health.ok) {
|
||||
return failureDiagnostic({
|
||||
cdpUrl,
|
||||
wsUrl: cdpUrl,
|
||||
code: health.code,
|
||||
message: health.message,
|
||||
startedAt,
|
||||
});
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
cdpUrl,
|
||||
wsUrl: cdpUrl,
|
||||
elapsedMs: elapsedSince(startedAt),
|
||||
};
|
||||
}
|
||||
|
||||
let version: ChromeVersion;
|
||||
try {
|
||||
version = await readChromeVersion(cdpUrl, timeoutMs, ssrfPolicy);
|
||||
} catch (err) {
|
||||
const classified = classifyChromeVersionError(err);
|
||||
return failureDiagnostic({
|
||||
cdpUrl,
|
||||
code: classified.code,
|
||||
message: classified.message,
|
||||
startedAt,
|
||||
});
|
||||
}
|
||||
|
||||
const wsUrlRaw = normalizeOptionalString(version.webSocketDebuggerUrl) ?? "";
|
||||
if (!wsUrlRaw) {
|
||||
return failureDiagnostic({
|
||||
cdpUrl,
|
||||
code: "missing_websocket_debugger_url",
|
||||
message: "CDP /json/version did not include webSocketDebuggerUrl",
|
||||
startedAt,
|
||||
});
|
||||
}
|
||||
const wsUrl = normalizeCdpWsUrl(wsUrlRaw, cdpUrl);
|
||||
try {
|
||||
await assertCdpEndpointAllowed(wsUrl, ssrfPolicy);
|
||||
} catch (err) {
|
||||
return failureDiagnostic({
|
||||
cdpUrl,
|
||||
wsUrl,
|
||||
code: "websocket_ssrf_blocked",
|
||||
message: safeChromeCdpErrorMessage(err),
|
||||
startedAt,
|
||||
});
|
||||
}
|
||||
|
||||
const health = await diagnoseCdpHealthCommand(wsUrl, handshakeTimeoutMs);
|
||||
if (!health.ok) {
|
||||
return failureDiagnostic({
|
||||
cdpUrl,
|
||||
wsUrl,
|
||||
code: health.code,
|
||||
message: health.message,
|
||||
startedAt,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
cdpUrl,
|
||||
wsUrl,
|
||||
browser: version.Browser,
|
||||
userAgent: version["User-Agent"],
|
||||
elapsedMs: elapsedSince(startedAt),
|
||||
};
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user