mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-22 15:03:05 +08:00
Compare commits
958 Commits
feeds-read
...
externaliz
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cafa438f16 | ||
|
|
04994f1046 | ||
|
|
9b4c5822eb | ||
|
|
b39e905b69 | ||
|
|
df6c71736c | ||
|
|
2b50bbf152 | ||
|
|
1bd85e3cc3 | ||
|
|
984efdb0b6 | ||
|
|
abdd81db11 | ||
|
|
3cc05d590c | ||
|
|
8246e8dace | ||
|
|
5df513c895 | ||
|
|
1529958067 | ||
|
|
7d3bc4d944 | ||
|
|
5fbb5f75ed | ||
|
|
4506d8bad6 | ||
|
|
7668ef2d35 | ||
|
|
8e4213b1c4 | ||
|
|
a0ab5c00a8 | ||
|
|
dc09324ec2 | ||
|
|
3be0fe722a | ||
|
|
6be98022da | ||
|
|
cc89e155c3 | ||
|
|
0111afe9e2 | ||
|
|
674b4f3372 | ||
|
|
4db829646a | ||
|
|
e046dbb52d | ||
|
|
a964132d80 | ||
|
|
0374892fd8 | ||
|
|
a2b3aab7b0 | ||
|
|
2ece2945ae | ||
|
|
f05fd56d66 | ||
|
|
a9a75b2b77 | ||
|
|
e29381a172 | ||
|
|
62456d65eb | ||
|
|
696c624008 | ||
|
|
b3b8b289dd | ||
|
|
692c5e34f0 | ||
|
|
536c8a840b | ||
|
|
99551c499b | ||
|
|
2c3519c1d7 | ||
|
|
e114001cca | ||
|
|
3ff59df960 | ||
|
|
a594d2ce73 | ||
|
|
c92f366c14 | ||
|
|
2b92706dcf | ||
|
|
a9be81d510 | ||
|
|
97a015bace | ||
|
|
93c7ec645a | ||
|
|
920bd04e19 | ||
|
|
5ae53cf9fb | ||
|
|
1168ac2fcd | ||
|
|
112a0ddaf8 | ||
|
|
c3ab1feb61 | ||
|
|
0bd2aa8ee0 | ||
|
|
7d4d8a7f3d | ||
|
|
e9be15ff19 | ||
|
|
05580342f7 | ||
|
|
17dc9902f2 | ||
|
|
540ec53f99 | ||
|
|
a182811070 | ||
|
|
4e2f0157c7 | ||
|
|
0f64e3c052 | ||
|
|
65388233e2 | ||
|
|
044df2516e | ||
|
|
190ca52882 | ||
|
|
ad304e790d | ||
|
|
e913e0739d | ||
|
|
8e24695a8d | ||
|
|
b0ecf6e1e7 | ||
|
|
dc5c2f6360 | ||
|
|
2405d029d4 | ||
|
|
4e96ca0d12 | ||
|
|
84ccba6b32 | ||
|
|
d17bb9c3e9 | ||
|
|
2f9107f672 | ||
|
|
6328c8637b | ||
|
|
3053cbc8a5 | ||
|
|
eb9318e953 | ||
|
|
f80f4a8b95 | ||
|
|
034629404d | ||
|
|
ef67ffd697 | ||
|
|
fe524d2a46 | ||
|
|
6c8dcc9d35 | ||
|
|
34ab295734 | ||
|
|
a5139a8c5c | ||
|
|
7f99824164 | ||
|
|
cbaeaa8856 | ||
|
|
a6cac347b6 | ||
|
|
4d0aec8095 | ||
|
|
fa51a624c0 | ||
|
|
075e328c62 | ||
|
|
0d21d489ab | ||
|
|
d8f1000600 | ||
|
|
227b4c81ed | ||
|
|
d28d6c2399 | ||
|
|
d09f728208 | ||
|
|
9e8ab083dd | ||
|
|
6eb72a830e | ||
|
|
6b1eef9959 | ||
|
|
14e448e0e1 | ||
|
|
aa3797c8d0 | ||
|
|
93ad397725 | ||
|
|
cf1b6fef44 | ||
|
|
d990115d19 | ||
|
|
8a75c4dd5f | ||
|
|
efd3172662 | ||
|
|
8afc1f770b | ||
|
|
77012f9807 | ||
|
|
2732f58215 | ||
|
|
3eeccbe782 | ||
|
|
2b75806197 | ||
|
|
adb9abe721 | ||
|
|
2a6554ac12 | ||
|
|
4113982fa8 | ||
|
|
2800ce4e28 | ||
|
|
45a93b8450 | ||
|
|
708c1b31e0 | ||
|
|
3fc1284fe6 | ||
|
|
1a075c375c | ||
|
|
95f314e822 | ||
|
|
efe3cbd695 | ||
|
|
79fac9fda9 | ||
|
|
ef6dc8f7e5 | ||
|
|
f31306eb4e | ||
|
|
db24112617 | ||
|
|
4fd19adf25 | ||
|
|
62dcc9bc3b | ||
|
|
bda4404f69 | ||
|
|
30925601ae | ||
|
|
a2675756b8 | ||
|
|
095a44c8de | ||
|
|
29eba5aaef | ||
|
|
b99812b3b1 | ||
|
|
4f7d1f4977 | ||
|
|
c310f8cfa4 | ||
|
|
08442c4b38 | ||
|
|
fe7b78b05f | ||
|
|
dd89898133 | ||
|
|
851b65c060 | ||
|
|
75c6a8fff5 | ||
|
|
f9fc380e90 | ||
|
|
8ef73be8e8 | ||
|
|
afadf1f7da | ||
|
|
29185aed68 | ||
|
|
3e5ca880bf | ||
|
|
a09e1b9aa0 | ||
|
|
11959ad100 | ||
|
|
e37b0f8cd3 | ||
|
|
dbb58341b5 | ||
|
|
790dfb66a8 | ||
|
|
4cb94cc2cf | ||
|
|
298a0cd55f | ||
|
|
c578608b78 | ||
|
|
8c0767ffa4 | ||
|
|
a0714a3d68 | ||
|
|
f719813a7e | ||
|
|
11a3903ede | ||
|
|
1f6ae32cab | ||
|
|
880425b03c | ||
|
|
4c736df975 | ||
|
|
6c85b90469 | ||
|
|
ef41560059 | ||
|
|
88b21fc30b | ||
|
|
4b6182ee2a | ||
|
|
d43bc3760e | ||
|
|
8cd0c11227 | ||
|
|
66b94ba577 | ||
|
|
77b6ca9a9b | ||
|
|
1425bb3a03 | ||
|
|
11484f8a14 | ||
|
|
ec7a548062 | ||
|
|
8ecdb97b63 | ||
|
|
a39e548ede | ||
|
|
328a44695f | ||
|
|
c21dcfc7c2 | ||
|
|
2bdcc8314d | ||
|
|
464adfe5e5 | ||
|
|
b66b4504f8 | ||
|
|
f6d432e545 | ||
|
|
fd13192adc | ||
|
|
1c63da09d8 | ||
|
|
735505442c | ||
|
|
108d6d7eca | ||
|
|
984c8f6ea0 | ||
|
|
cd9060e06a | ||
|
|
5b96eb0172 | ||
|
|
b95b725c83 | ||
|
|
4b881509eb | ||
|
|
a03032a272 | ||
|
|
c94ebdbebd | ||
|
|
7b46167607 | ||
|
|
0e53358945 | ||
|
|
6b45e9af7a | ||
|
|
2ef61eb782 | ||
|
|
6823f56d8e | ||
|
|
3e1d3c5feb | ||
|
|
dfbc9ab246 | ||
|
|
179eb15554 | ||
|
|
c3b1e926e8 | ||
|
|
89768d456b | ||
|
|
609d7a14b1 | ||
|
|
3bae0d6b82 | ||
|
|
75a997dd7c | ||
|
|
ab8dc3af52 | ||
|
|
bebc5d847d | ||
|
|
a0f28bd3f5 | ||
|
|
c638617897 | ||
|
|
b77d6149e1 | ||
|
|
36db108fc1 | ||
|
|
e00c1eebc4 | ||
|
|
32d22d04cc | ||
|
|
648ef73bde | ||
|
|
beebb35de4 | ||
|
|
55959148ca | ||
|
|
4684bbba97 | ||
|
|
409adfbe10 | ||
|
|
2609b97222 | ||
|
|
03bc600e67 | ||
|
|
8102d5ebc3 | ||
|
|
6399eb8191 | ||
|
|
1a5839fbd8 | ||
|
|
d17045db6f | ||
|
|
2a8db1fc23 | ||
|
|
82f43f0a62 | ||
|
|
9adf3d92bd | ||
|
|
e21164933a | ||
|
|
1b17517969 | ||
|
|
86fea26797 | ||
|
|
2e7c3ace9c | ||
|
|
e34204a1e0 | ||
|
|
6daf9307e0 | ||
|
|
ebb670b208 | ||
|
|
52672c7af1 | ||
|
|
690efd2a16 | ||
|
|
102ab759e7 | ||
|
|
7da955fae4 | ||
|
|
06a0148072 | ||
|
|
edd1d3319c | ||
|
|
0ea39a2276 | ||
|
|
6fa944e80f | ||
|
|
4c453c931f | ||
|
|
13b0976c70 | ||
|
|
43e00c06c3 | ||
|
|
03ce3d41b1 | ||
|
|
bda05dbc2f | ||
|
|
7069d95720 | ||
|
|
8086cffd17 | ||
|
|
d91aee7220 | ||
|
|
c8ab37f6fe | ||
|
|
a823cb2b30 | ||
|
|
5230ec66ae | ||
|
|
eea777c9fc | ||
|
|
0b28a72be1 | ||
|
|
adcba85264 | ||
|
|
5b79fa13e2 | ||
|
|
124ea48549 | ||
|
|
12756fc4c8 | ||
|
|
5bf459e23b | ||
|
|
d64a27feeb | ||
|
|
6c42f73619 | ||
|
|
d460f00eb9 | ||
|
|
b83dce7b33 | ||
|
|
d3c907193f | ||
|
|
b47c930e7e | ||
|
|
0befd3c8f2 | ||
|
|
c2ee9b0be8 | ||
|
|
9d27583190 | ||
|
|
04c8c50cc4 | ||
|
|
5abf4ce2e2 | ||
|
|
07d5cdec99 | ||
|
|
f5f23e739e | ||
|
|
0c183283e5 | ||
|
|
514b3365b5 | ||
|
|
2804c24dc6 | ||
|
|
94c7b5a874 | ||
|
|
93ec8b8c5c | ||
|
|
9d83eeaccf | ||
|
|
33eb6ab9de | ||
|
|
757ab933f4 | ||
|
|
63fdc57b3a | ||
|
|
a39a3b74de | ||
|
|
77a859f4ae | ||
|
|
84cf64770f | ||
|
|
0ad48dad2c | ||
|
|
b50a5aebba | ||
|
|
b84665222c | ||
|
|
6441e56465 | ||
|
|
6daabd23f8 | ||
|
|
2f33999898 | ||
|
|
a60947fb3e | ||
|
|
ac5d219be3 | ||
|
|
ae41b00922 | ||
|
|
b28e68e0ce | ||
|
|
5d1e649aea | ||
|
|
d6d17709e8 | ||
|
|
6fd6bddb92 | ||
|
|
f4dee99574 | ||
|
|
db33402af0 | ||
|
|
088cab5ee4 | ||
|
|
bd74a62118 | ||
|
|
9f888d95e0 | ||
|
|
11a2e03bd4 | ||
|
|
5693fcda78 | ||
|
|
eb00d499d1 | ||
|
|
8a7906c716 | ||
|
|
c85113e30e | ||
|
|
bdf81a825f | ||
|
|
d9dfcd6c8a | ||
|
|
2cafbd0774 | ||
|
|
54b2836eab | ||
|
|
5b6f4b2919 | ||
|
|
c037a34ba7 | ||
|
|
12c34fc3a9 | ||
|
|
e366349730 | ||
|
|
89a73d08c8 | ||
|
|
e6f41a4df0 | ||
|
|
b7fef7fca6 | ||
|
|
d1cbe29f3d | ||
|
|
9dbc21d283 | ||
|
|
5e86c7eef4 | ||
|
|
6f3af56952 | ||
|
|
03ee22666b | ||
|
|
0d351b9875 | ||
|
|
f69ba12a37 | ||
|
|
b796890b97 | ||
|
|
b5fc9514c0 | ||
|
|
2a140e6e6a | ||
|
|
6e5f4d685e | ||
|
|
600bace853 | ||
|
|
b8a5dac1a2 | ||
|
|
15c880aeff | ||
|
|
3a53eb5d77 | ||
|
|
66e5cfdd86 | ||
|
|
c4facb2bb3 | ||
|
|
a70b34a3cb | ||
|
|
59bf85c586 | ||
|
|
e486a1d1cf | ||
|
|
72b9bc7303 | ||
|
|
d3b44442f6 | ||
|
|
6ddbcbd460 | ||
|
|
7bd4aab21f | ||
|
|
a4c8b17b9e | ||
|
|
d87f8325d0 | ||
|
|
a5fde9119c | ||
|
|
e6c899dfa5 | ||
|
|
425f512897 | ||
|
|
735d70d9db | ||
|
|
15300291ed | ||
|
|
eb7789c8cb | ||
|
|
f19052b3f3 | ||
|
|
61d1fd1f72 | ||
|
|
1435fc123f | ||
|
|
6c4028e073 | ||
|
|
9242137ca7 | ||
|
|
35d7cb0bff | ||
|
|
c000e4811d | ||
|
|
d25549f142 | ||
|
|
a192b2ea52 | ||
|
|
7975ec0b11 | ||
|
|
e9b694ef9c | ||
|
|
3b332fd0a4 | ||
|
|
7dd01d15c5 | ||
|
|
675c56692a | ||
|
|
3f597619c8 | ||
|
|
91531ba35c | ||
|
|
206bbb01b0 | ||
|
|
c9758bf2a0 | ||
|
|
282eb74128 | ||
|
|
9940110b88 | ||
|
|
73b35cc3ca | ||
|
|
ac0537e363 | ||
|
|
0321c04663 | ||
|
|
78b717a54c | ||
|
|
7b00fd6c45 | ||
|
|
1f1155597b | ||
|
|
e9e42d5db4 | ||
|
|
bc2f4ce923 | ||
|
|
7dbae1b2cd | ||
|
|
63f2c56222 | ||
|
|
675cae58d7 | ||
|
|
c372f6ef0b | ||
|
|
1d4b712f9a | ||
|
|
e016f0b496 | ||
|
|
f8d2c4b25a | ||
|
|
b15f745a60 | ||
|
|
8c9c8aad2e | ||
|
|
0a7b009647 | ||
|
|
fc8542b377 | ||
|
|
37a4b565ea | ||
|
|
6b0210a5fd | ||
|
|
c22e300084 | ||
|
|
5134dd0c54 | ||
|
|
830691b201 | ||
|
|
ab39bab52a | ||
|
|
3f4d1cfcce | ||
|
|
06574920dd | ||
|
|
d46b64df66 | ||
|
|
b06e2f9149 | ||
|
|
b574da57cf | ||
|
|
bfe0caefd1 | ||
|
|
0004cfd59e | ||
|
|
604aa30189 | ||
|
|
5dd30c3995 | ||
|
|
5b22409389 | ||
|
|
b970d57175 | ||
|
|
9ab8e466d2 | ||
|
|
c43822077a | ||
|
|
8797564254 | ||
|
|
273eb88874 | ||
|
|
140a2fa520 | ||
|
|
c8b48c78d0 | ||
|
|
29033e67af | ||
|
|
e9a47fe554 | ||
|
|
2f213a1606 | ||
|
|
992ddf6310 | ||
|
|
7b259bd2a4 | ||
|
|
e91ca8df86 | ||
|
|
af3e509ab8 | ||
|
|
dec76bb5eb | ||
|
|
862ef1cec1 | ||
|
|
486c9e6ba3 | ||
|
|
b2d78abe94 | ||
|
|
2f38b5aa2e | ||
|
|
4d17a52924 | ||
|
|
4b2298e8cb | ||
|
|
f640ca11f9 | ||
|
|
2029f87f29 | ||
|
|
ca1aa33eba | ||
|
|
5b212162d3 | ||
|
|
3df5207389 | ||
|
|
78f30a010c | ||
|
|
83785a6e79 | ||
|
|
195890f815 | ||
|
|
0f8df48a91 | ||
|
|
7b28b73e78 | ||
|
|
3650766f26 | ||
|
|
eb03d0ee2b | ||
|
|
38c8b0c196 | ||
|
|
0030a192c8 | ||
|
|
34806b39cd | ||
|
|
b0f21f8af7 | ||
|
|
5af318b95d | ||
|
|
8d2e6d7686 | ||
|
|
b039e949b6 | ||
|
|
2ca5b7c93e | ||
|
|
51d1789cea | ||
|
|
89e73240a1 | ||
|
|
f3ee317f71 | ||
|
|
ecb82f1be9 | ||
|
|
f1a48dac18 | ||
|
|
cba9c02095 | ||
|
|
715dc718fc | ||
|
|
85f71f4c8f | ||
|
|
66f84a9bf1 | ||
|
|
50c2cc6a45 | ||
|
|
e3ccf8743f | ||
|
|
d4c2fa7aed | ||
|
|
6c1041339d | ||
|
|
db54a3268b | ||
|
|
9750d887f5 | ||
|
|
fce586538a | ||
|
|
97b97a9999 | ||
|
|
cbbb466852 | ||
|
|
c2de9d0822 | ||
|
|
e46aaead2c | ||
|
|
c191d7978b | ||
|
|
1b5e1e2d53 | ||
|
|
ab41a311cf | ||
|
|
9e70d251b0 | ||
|
|
025f8fb087 | ||
|
|
e012f2cd3c | ||
|
|
73c988a9c8 | ||
|
|
8b4eedf1bc | ||
|
|
b43eedbb18 | ||
|
|
ebbf506a77 | ||
|
|
e0c1add79a | ||
|
|
eae1e8f3f8 | ||
|
|
ba34052a0e | ||
|
|
ce78ac3efb | ||
|
|
6c2c43d63f | ||
|
|
0168ff0c2e | ||
|
|
22bdda2555 | ||
|
|
2755112353 | ||
|
|
57c7fa22bb | ||
|
|
269e44e164 | ||
|
|
2e75d925ad | ||
|
|
0e763c1499 | ||
|
|
6ed9fb8ec2 | ||
|
|
a826d6a4a4 | ||
|
|
9d519c1481 | ||
|
|
b09b35c13c | ||
|
|
15f2a56590 | ||
|
|
e66c36df37 | ||
|
|
42c504b8b1 | ||
|
|
9ac3759ffc | ||
|
|
d7f747af3b | ||
|
|
9cb3b4ea2b | ||
|
|
f257116c92 | ||
|
|
a88ce96ee1 | ||
|
|
448b3fa0be | ||
|
|
88ad407be2 | ||
|
|
13e77cc055 | ||
|
|
b78718f42a | ||
|
|
54dddda68d | ||
|
|
6084442ab6 | ||
|
|
692c7e78f4 | ||
|
|
3f166b1f64 | ||
|
|
8e8905560b | ||
|
|
3968fea383 | ||
|
|
43e8c29fbf | ||
|
|
7e80bb8abf | ||
|
|
9826619e22 | ||
|
|
8f3672beaa | ||
|
|
0d25928fa4 | ||
|
|
807641548b | ||
|
|
f87f30d429 | ||
|
|
22c5ced69f | ||
|
|
faa4f5a23b | ||
|
|
fc0b28cb73 | ||
|
|
8265acaacb | ||
|
|
d20e96a650 | ||
|
|
8ff1d3e67b | ||
|
|
49a9032705 | ||
|
|
aab1dd88e0 | ||
|
|
a81a505c72 | ||
|
|
7d219bd6e7 | ||
|
|
ece7d0945c | ||
|
|
979238dbb3 | ||
|
|
ab165d119c | ||
|
|
6f3df79f17 | ||
|
|
7c2fb845db | ||
|
|
00f67b845b | ||
|
|
6ef9207201 | ||
|
|
3283540c78 | ||
|
|
03b022b88e | ||
|
|
45f9358877 | ||
|
|
29ec5b331c | ||
|
|
fa08942396 | ||
|
|
2c9192a9a8 | ||
|
|
955f3ed094 | ||
|
|
7217477553 | ||
|
|
97b0b559ad | ||
|
|
1b299f4dbf | ||
|
|
7064d198a3 | ||
|
|
f18ff7551e | ||
|
|
58d295840e | ||
|
|
896b3c612d | ||
|
|
2f240a4a4c | ||
|
|
172412d756 | ||
|
|
58578b3250 | ||
|
|
ce9769faae | ||
|
|
e8920f6f6b | ||
|
|
9be53b4aa2 | ||
|
|
3ec2a46907 | ||
|
|
15a2d74320 | ||
|
|
77f07a11e7 | ||
|
|
7a0d36f3d0 | ||
|
|
0a707afb9a | ||
|
|
bdeda6553b | ||
|
|
3499b277e3 | ||
|
|
8c8857c3ef | ||
|
|
d75613e794 | ||
|
|
beb8897f49 | ||
|
|
add5f76a1e | ||
|
|
9a9f4dbefe | ||
|
|
5beaaf343c | ||
|
|
1db811282c | ||
|
|
aa23d9f34e | ||
|
|
2962c95010 | ||
|
|
80d3b132a5 | ||
|
|
1a5d84d3fe | ||
|
|
71a75b9b28 | ||
|
|
b1f562570a | ||
|
|
bdcc691745 | ||
|
|
4461e257e3 | ||
|
|
76014cfe95 | ||
|
|
498ff1fb5a | ||
|
|
ae81aa018d | ||
|
|
1706bfda2c | ||
|
|
a1201e99fc | ||
|
|
90d2f161c9 | ||
|
|
bff7134a69 | ||
|
|
e59d0b540e | ||
|
|
aa5fcf70f7 | ||
|
|
63ac2e2ce0 | ||
|
|
803064c6e0 | ||
|
|
577e5a4692 | ||
|
|
a49f3f9362 | ||
|
|
7b9ddbda99 | ||
|
|
0f83051353 | ||
|
|
4341cf24cc | ||
|
|
6a3f990140 | ||
|
|
81abc2b21b | ||
|
|
09fcafffbc | ||
|
|
2a93d7b9c5 | ||
|
|
0eaefc9050 | ||
|
|
52e01676be | ||
|
|
df68b81006 | ||
|
|
a5417b5c6c | ||
|
|
da2c7e2d2b | ||
|
|
3a14f247ad | ||
|
|
5c36001fcb | ||
|
|
05bed72a8d | ||
|
|
c2433d41a7 | ||
|
|
d368fd620c | ||
|
|
7dc7deaa13 | ||
|
|
a2ff59fdb2 | ||
|
|
b12223a79f | ||
|
|
f519ceab9c | ||
|
|
1f1b1aee6b | ||
|
|
62b2e9ef14 | ||
|
|
0f67474251 | ||
|
|
e56fd1dc04 | ||
|
|
b3968f69c9 | ||
|
|
b0df6dc10e | ||
|
|
141fb2b119 | ||
|
|
64b6488f6c | ||
|
|
e1fc4683bb | ||
|
|
85ab952956 | ||
|
|
abd5fb4494 | ||
|
|
aea050b43e | ||
|
|
85f552bf37 | ||
|
|
dafd98dd98 | ||
|
|
3632c62f85 | ||
|
|
ad5d2cbc1b | ||
|
|
7cda58c109 | ||
|
|
5c0b99ae2b | ||
|
|
979925c194 | ||
|
|
2f9f45f734 | ||
|
|
32cbaecd09 | ||
|
|
1989726eb6 | ||
|
|
2454acc287 | ||
|
|
fce5db415b | ||
|
|
2166652eb3 | ||
|
|
7a9c269541 | ||
|
|
aa893b9228 | ||
|
|
98a7741468 | ||
|
|
3df4341e5a | ||
|
|
ecac665bf3 | ||
|
|
021fd5de2b | ||
|
|
60159b9f00 | ||
|
|
165440117e | ||
|
|
fddfcbe10e | ||
|
|
7c850bdf38 | ||
|
|
2bc20f2ec5 | ||
|
|
ed500dda25 | ||
|
|
bc754b3160 | ||
|
|
b972956173 | ||
|
|
29444b26f2 | ||
|
|
7fc5a72433 | ||
|
|
a590f7f690 | ||
|
|
2252674168 | ||
|
|
60612ff492 | ||
|
|
c5623e72f3 | ||
|
|
947c21ee5a | ||
|
|
99f58ae6d6 | ||
|
|
3f0e740f83 | ||
|
|
106961b513 | ||
|
|
d0001f96f0 | ||
|
|
527bd807b9 | ||
|
|
7546231762 | ||
|
|
a977dc843d | ||
|
|
6ad7f66af2 | ||
|
|
1b4fb6291d | ||
|
|
ee69465fe9 | ||
|
|
7b329ade32 | ||
|
|
44422b2151 | ||
|
|
48b338a5a9 | ||
|
|
d4f68475fd | ||
|
|
d81ae7a441 | ||
|
|
99d8549de6 | ||
|
|
7a077ffead | ||
|
|
b980d678a4 | ||
|
|
e02e3d6971 | ||
|
|
6fa05685ea | ||
|
|
6585cb3b44 | ||
|
|
730c7269ef | ||
|
|
d72f7edf2d | ||
|
|
24b6e6ba96 | ||
|
|
c33f8c20ef | ||
|
|
1c0c072bc2 | ||
|
|
aaf335af04 | ||
|
|
ad049ef083 | ||
|
|
6dc121eb6a | ||
|
|
0742a2f37a | ||
|
|
e2c567538d | ||
|
|
5c8fa5da5c | ||
|
|
9953b85e6d | ||
|
|
048014d1ab | ||
|
|
0cd6975352 | ||
|
|
5384b91866 | ||
|
|
19ec9d8979 | ||
|
|
e65619dd0c | ||
|
|
2f0f085826 | ||
|
|
0cd8db97f9 | ||
|
|
087d999fce | ||
|
|
4514b5a387 | ||
|
|
6b82d4ecb7 | ||
|
|
f719f0cf77 | ||
|
|
8ee638236a | ||
|
|
36934fd9f5 | ||
|
|
84895e9276 | ||
|
|
a6e41a0cc1 | ||
|
|
1ede829fbf | ||
|
|
b93b07ee1b | ||
|
|
405e5072fd | ||
|
|
b79dfc739c | ||
|
|
ff4808f94d | ||
|
|
602bc0baa9 | ||
|
|
a1d278b174 | ||
|
|
0fd5dae36f | ||
|
|
984e058624 | ||
|
|
a6e4afe0fa | ||
|
|
66c62d52ad | ||
|
|
9e3ef487eb | ||
|
|
739636fc33 | ||
|
|
ccc1415f6d | ||
|
|
b1608b4a4e | ||
|
|
703dfbf453 | ||
|
|
7cd58cca2a | ||
|
|
2d603c90dc | ||
|
|
4296ecb78c | ||
|
|
fe1d981a47 | ||
|
|
5cf8ba973d | ||
|
|
cb394309fe | ||
|
|
dd29a6de52 | ||
|
|
93a0b5d353 | ||
|
|
4f8fd48ea7 | ||
|
|
7679872ddf | ||
|
|
cd7385c5c6 | ||
|
|
88cf142c98 | ||
|
|
1988e1a0c5 | ||
|
|
138ffa2992 | ||
|
|
6069a030c4 | ||
|
|
0767118c26 | ||
|
|
120d08c730 | ||
|
|
6ee7714306 | ||
|
|
84c96ddb14 | ||
|
|
8b8d791472 | ||
|
|
8246b49cc5 | ||
|
|
3d5aefb50c | ||
|
|
628314f53c | ||
|
|
5c19699cb2 | ||
|
|
75fd2464cc | ||
|
|
bd5a5a0cfc | ||
|
|
9111d8ed85 | ||
|
|
1c3da22bcd | ||
|
|
7de3e0e0bb | ||
|
|
29df94382e | ||
|
|
629f78b77b | ||
|
|
68790eb4b9 | ||
|
|
9a92c3d24a | ||
|
|
3fa12177dd | ||
|
|
8855b21f99 | ||
|
|
a4e2113e1b | ||
|
|
f26f45c050 | ||
|
|
a127183094 | ||
|
|
22f696d010 | ||
|
|
3a1e49dbaa | ||
|
|
0e14a3f09b | ||
|
|
244857adbf | ||
|
|
1dbf4dbd40 | ||
|
|
fbea5b023a | ||
|
|
4e7a717868 | ||
|
|
00c2dc66b1 | ||
|
|
df7e4788ed | ||
|
|
5b93f829e5 | ||
|
|
6f1cc2f8df | ||
|
|
d9ee08a76e | ||
|
|
da35f8b4d1 | ||
|
|
bb7150de94 | ||
|
|
3d05e973f0 | ||
|
|
fb022a2b07 | ||
|
|
9afc333bd7 | ||
|
|
7b44157bc6 | ||
|
|
075965e32f | ||
|
|
73d393f812 | ||
|
|
1cbcc3e1f0 | ||
|
|
ee709f3b0f | ||
|
|
eb5bb7f6a0 | ||
|
|
592373f0ea | ||
|
|
a63230008c | ||
|
|
d0812126c8 | ||
|
|
352141a1be | ||
|
|
904c035d1c | ||
|
|
a085db6b64 | ||
|
|
819b1a3e3e | ||
|
|
e19ad8c0fd | ||
|
|
f8675b3b70 | ||
|
|
3285a10c7f | ||
|
|
e451a4e875 | ||
|
|
8e375242be | ||
|
|
93cbd16c88 | ||
|
|
7779bc64d2 | ||
|
|
5e7bb9cf9b | ||
|
|
2248aa4315 | ||
|
|
6bfe7a2b06 | ||
|
|
2babcf026e | ||
|
|
9192ff8416 | ||
|
|
118f3f3312 | ||
|
|
7d658dfd97 | ||
|
|
84a36057e9 | ||
|
|
b44e39b82c | ||
|
|
e89c255a01 | ||
|
|
a635e97965 | ||
|
|
4f278ef71c | ||
|
|
1df2cc5f02 | ||
|
|
1cda1fc9a0 | ||
|
|
af9b026241 | ||
|
|
6a23a72d74 | ||
|
|
14d362039e | ||
|
|
9391dac56d | ||
|
|
61ee4ffdfc | ||
|
|
78d1b4a9b3 | ||
|
|
0d6e0a2263 | ||
|
|
33a4845555 | ||
|
|
4a75171190 | ||
|
|
c946df0239 | ||
|
|
9ce68d0920 | ||
|
|
2c65b9b407 | ||
|
|
78a2a31a6b | ||
|
|
c719ff3183 | ||
|
|
0479da9bfb | ||
|
|
13e76544e5 | ||
|
|
c81391e270 | ||
|
|
69216f1745 | ||
|
|
a824df2e35 | ||
|
|
f60aec6e9d | ||
|
|
6293e6e3ca | ||
|
|
f4baeab47f | ||
|
|
8f06e65f33 | ||
|
|
3518fa575a | ||
|
|
a5e33b3a6b | ||
|
|
86d1e397f4 | ||
|
|
d6c7e95c7b | ||
|
|
445317a38b | ||
|
|
2844ec2bb0 | ||
|
|
459edec9ba | ||
|
|
e27c9a9a41 | ||
|
|
c80f4c110e | ||
|
|
cfc699d3f6 | ||
|
|
f04c3d6575 | ||
|
|
da03996ab7 | ||
|
|
5fd947c661 | ||
|
|
622955b3fc | ||
|
|
cd69760628 | ||
|
|
3e41587992 | ||
|
|
214a28affd | ||
|
|
9f6d5e4750 | ||
|
|
033455b6f1 | ||
|
|
8b5b150e02 | ||
|
|
4db7d6a90a | ||
|
|
d76c1daa52 | ||
|
|
9491e9187d | ||
|
|
e0ec42e0e0 | ||
|
|
a971641a54 | ||
|
|
50b5238b38 | ||
|
|
0cf941344c | ||
|
|
e6823c3d16 | ||
|
|
4b2b70ec79 | ||
|
|
b6d91d96ef | ||
|
|
dadec4500f | ||
|
|
f76a3a3bbe | ||
|
|
c2e26db61b | ||
|
|
41691a82d5 | ||
|
|
49b0487e5b | ||
|
|
4575734f59 | ||
|
|
7e7dc7505b | ||
|
|
7dca9210c9 | ||
|
|
208bed06e1 | ||
|
|
87358d7a7c | ||
|
|
e02bee6aab | ||
|
|
56c0405018 | ||
|
|
b6d754e3cb | ||
|
|
6e732b3063 | ||
|
|
423b1b3a42 | ||
|
|
faeb731a29 | ||
|
|
d6075c1694 | ||
|
|
a67f809b33 | ||
|
|
1f1c434ede | ||
|
|
3c3f1010aa | ||
|
|
0e980be284 | ||
|
|
27450f6b42 | ||
|
|
d491e9c69b | ||
|
|
6fc0a3a9bd | ||
|
|
0a1ce14dd1 | ||
|
|
f9f94e7dcd | ||
|
|
1e105d5340 | ||
|
|
21c966616f | ||
|
|
be7807f65e | ||
|
|
7ee1dafd4f | ||
|
|
3a7a385baf | ||
|
|
c4d1f37d33 | ||
|
|
ba43be9424 | ||
|
|
aa479ac7d8 | ||
|
|
d6cefe26f4 | ||
|
|
0eed410bd0 | ||
|
|
b073d7cc11 | ||
|
|
d97574aae6 | ||
|
|
a54a56fb98 | ||
|
|
45971784c9 | ||
|
|
6a27300a5b | ||
|
|
023993249f | ||
|
|
cd061a4c7b | ||
|
|
b554c470a2 | ||
|
|
8972bff98d | ||
|
|
6f5fdb1e6b | ||
|
|
0f18e82932 | ||
|
|
9594300f8c | ||
|
|
c2c19a883d | ||
|
|
4a0f497f16 | ||
|
|
3706047d60 | ||
|
|
e35e5f123d | ||
|
|
b5811ea2b3 | ||
|
|
bb1043b14c | ||
|
|
16fba65cb6 | ||
|
|
7e5901752d | ||
|
|
806a37fca8 | ||
|
|
753ff96771 | ||
|
|
3fa4fdaec1 | ||
|
|
efc36d71bd | ||
|
|
6cfb025143 | ||
|
|
061a3705db | ||
|
|
9e5ac0cea4 | ||
|
|
aff6e221a7 | ||
|
|
5df5aa1640 | ||
|
|
59a93a817f | ||
|
|
23b8f5d037 | ||
|
|
17e2fbfa86 | ||
|
|
cbff4fa5bc | ||
|
|
330545f3e9 | ||
|
|
2b0a72bb48 | ||
|
|
583829a342 | ||
|
|
7b94ae9944 | ||
|
|
1609365b3e | ||
|
|
d216f7c876 | ||
|
|
d41a3d28a0 | ||
|
|
8aa58c5fb0 | ||
|
|
e7e85f5436 | ||
|
|
458904037f | ||
|
|
1e53ee4fd5 | ||
|
|
6037d1a85c | ||
|
|
2c8d19d73e | ||
|
|
70a48a680d | ||
|
|
0c210e5e52 | ||
|
|
38807ffba4 | ||
|
|
fb06df6cad | ||
|
|
50614c51a8 | ||
|
|
1f244f60ed | ||
|
|
10b8b32380 | ||
|
|
3b65f1d279 | ||
|
|
1c711048f9 | ||
|
|
f69f81af9e | ||
|
|
cdf4268540 | ||
|
|
b4651f3781 | ||
|
|
107c49e936 | ||
|
|
ffd8c6e5d9 |
@@ -4,6 +4,7 @@ import { execFileSync } from "node:child_process";
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
|
||||
const repo = "openclaw/openclaw";
|
||||
const commitAssociationQueryBatchSize = 20;
|
||||
const excludedHandles = new Set(["openclaw", "clawsweeper", "claude", "codex", "steipete"]);
|
||||
const nonEditorialTypes = new Set([
|
||||
"build",
|
||||
@@ -618,13 +619,25 @@ function graphql(query) {
|
||||
let lastError;
|
||||
for (let attempt = 0; attempt < 5; attempt += 1) {
|
||||
try {
|
||||
return githubApi(["graphql", "-f", `query=${query}`]).data;
|
||||
const response = githubApi(["graphql", "-f", `query=${query}`]);
|
||||
if (response?.data && typeof response.data === "object") {
|
||||
return response.data;
|
||||
}
|
||||
const errors = Array.isArray(response?.errors)
|
||||
? response.errors.map((error) => error?.message).filter(Boolean)
|
||||
: [];
|
||||
const detail = [...errors, response?.message].filter(Boolean).join("\n");
|
||||
throw new Error(
|
||||
detail
|
||||
? `GitHub GraphQL response did not include data:\n${detail}`
|
||||
: "GitHub GraphQL response did not include data.",
|
||||
);
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
const message = [error?.message, error?.stdout, error?.stderr].filter(Boolean).join("\n");
|
||||
// Historical ranges batch hundreds of objects; only retry transient transport failures.
|
||||
if (
|
||||
!/(?:operation timed out|ECONNRESET|ETIMEDOUT|EAI_AGAIN|TLS handshake timeout|stream error: .*CANCEL|unexpected end of JSON input|upstream connect error|connection termination|error connecting to api\.github\.com|Unexpected token '<')/i.test(
|
||||
!/(?:operation timed out|ECONNRESET|ETIMEDOUT|EAI_AGAIN|TLS handshake timeout|stream error: .*CANCEL|unexpected end of JSON input|upstream connect error|connection termination|connection reset by peer|error connecting to api\.github\.com|Unexpected token '<'|something went wrong|temporarily unavailable|internal server error|rate limit)/i.test(
|
||||
message,
|
||||
)
|
||||
) {
|
||||
@@ -657,8 +670,8 @@ function resolveAssociatedPullRequests(commitHashes, targetTimestamp) {
|
||||
pending.push({ commitHash, cursor: connection.pageInfo.endCursor });
|
||||
}
|
||||
}
|
||||
for (let index = 0; index < commitHashes.length; index += 40) {
|
||||
const chunk = commitHashes.slice(index, index + 40);
|
||||
for (let index = 0; index < commitHashes.length; index += commitAssociationQueryBatchSize) {
|
||||
const chunk = commitHashes.slice(index, index + commitAssociationQueryBatchSize);
|
||||
const fields = chunk
|
||||
.map(
|
||||
(hash, offset) =>
|
||||
|
||||
@@ -107,16 +107,9 @@ Reject:
|
||||
|
||||
## PR Body Proof
|
||||
|
||||
Use the repo PR template. Include these exact labels:
|
||||
|
||||
```text
|
||||
Behavior addressed:
|
||||
Real environment tested:
|
||||
Exact steps or command run after this patch:
|
||||
Evidence after fix:
|
||||
Observed result after fix:
|
||||
What was not tested:
|
||||
```
|
||||
Use the repo PR template. Include authored `## What Problem This Solves` and
|
||||
`## Evidence` sections. Keep the body focused on intent and the most useful
|
||||
validation evidence; inspect the code, tests, and CI before judging correctness.
|
||||
|
||||
## Existing PR Rules
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ paths:
|
||||
- src/plugins/memory-*.ts
|
||||
- src/gateway/server-startup-memory.ts
|
||||
- src/commands/doctor-memory-search.ts
|
||||
- src/commands/doctor-cron-dreaming-payload-migration.ts
|
||||
- src/commands/doctor/cron/dreaming-payload-migration.ts
|
||||
|
||||
paths-ignore:
|
||||
- "**/node_modules"
|
||||
|
||||
@@ -19,7 +19,6 @@ paths:
|
||||
- src/plugins/bundled-compat.ts
|
||||
- src/plugins/bundled-dir.ts
|
||||
- src/plugins/bundled-plugin-metadata.ts
|
||||
- src/plugins/bundled-public-surface-runtime-root.ts
|
||||
- src/plugins/plugin-sdk-dist-alias.ts
|
||||
- src/plugins/captured-registration.ts
|
||||
- src/plugins/config-activation-shared.ts
|
||||
@@ -46,7 +45,6 @@ paths:
|
||||
- src/plugins/runtime-state.ts
|
||||
- src/plugins/runtime.ts
|
||||
- src/plugins/sdk-alias.ts
|
||||
- src/plugins/source-loader.ts
|
||||
- src/plugins/types.ts
|
||||
- src/plugins/validation-diagnostics.ts
|
||||
- src/plugins/web-provider-public-artifacts*.ts
|
||||
|
||||
@@ -51,7 +51,6 @@ paths:
|
||||
- src/plugins/runtime
|
||||
- src/plugins/runtime-state.ts
|
||||
- src/plugins/runtime.ts
|
||||
- src/plugins/source-loader.ts
|
||||
- src/plugins/update.ts
|
||||
- src/plugins/validation-diagnostics.ts
|
||||
- src/plugin-sdk/*entry*.ts
|
||||
|
||||
22
.github/labeler.yml
vendored
22
.github/labeler.yml
vendored
@@ -41,12 +41,6 @@
|
||||
- any-glob-to-any-file:
|
||||
- "extensions/google-meet/**"
|
||||
- "docs/plugins/google-meet.md"
|
||||
"plugin: meeting-notes":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "extensions/meeting-notes/**"
|
||||
- "docs/plugins/meeting-notes.md"
|
||||
- "src/meeting-notes/**"
|
||||
"plugin: workboard":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
@@ -109,6 +103,11 @@
|
||||
- any-glob-to-any-file:
|
||||
- "extensions/qqbot/**"
|
||||
- "docs/channels/qqbot.md"
|
||||
"channel: raft":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "extensions/raft/**"
|
||||
- "docs/channels/raft.md"
|
||||
"channel: qa-channel":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
@@ -252,12 +251,12 @@
|
||||
- "src/agents/sandbox*.ts"
|
||||
- "src/commands/sandbox*.ts"
|
||||
- "src/cli/sandbox-cli.ts"
|
||||
- "src/docker-setup.test.ts"
|
||||
- "src/docker-setup.e2e.test.ts"
|
||||
- "src/config/**/*sandbox*"
|
||||
- "docs/cli/sandbox.md"
|
||||
- "docs/gateway/sandbox*.md"
|
||||
- "docs/install/docker.md"
|
||||
- "docs/multi-agent-sandbox-tools.md"
|
||||
- "docs/tools/multi-agent-sandbox-tools.md"
|
||||
|
||||
"agents":
|
||||
- changed-files:
|
||||
@@ -270,7 +269,7 @@
|
||||
- ".github/workflows/opengrep-*.yml"
|
||||
- ".semgrepignore"
|
||||
- "docs/cli/security.md"
|
||||
- "docs/gateway/security.md"
|
||||
- "docs/gateway/security/**"
|
||||
- "security/**"
|
||||
|
||||
"extensions: admin-http-rpc":
|
||||
@@ -322,11 +321,6 @@
|
||||
- any-glob-to-any-file:
|
||||
- "extensions/policy/**"
|
||||
- "docs/cli/policy.md"
|
||||
"extensions: feeds":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "extensions/feeds/**"
|
||||
- "docs/plugins/reference/feeds.md"
|
||||
"extensions: open-prose":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
|
||||
151
.github/pull_request_template.md
vendored
151
.github/pull_request_template.md
vendored
@@ -1,118 +1,57 @@
|
||||
## Summary
|
||||
<!--
|
||||
Optional linked context:
|
||||
Add a visible `Closes #<issue-number>` or `Related: #<issue-number>` line
|
||||
below this comment.
|
||||
|
||||
What problem does this PR solve?
|
||||
Required PR title:
|
||||
type: user-facing description
|
||||
Use a parenthesized scope only when it adds clarity:
|
||||
fix(auth): login redirect loops when session cookie is expired
|
||||
|
||||
Why does this matter now?
|
||||
Types: feat, fix, improve, refactor, docs, chore.
|
||||
For fixes, describe the user-visible symptom and trigger:
|
||||
fix: task list fails to load when user has no environments
|
||||
Avoid implementation details such as:
|
||||
fix: add null check to task query
|
||||
-->
|
||||
|
||||
What is the intended outcome?
|
||||
## What Problem This Solves
|
||||
|
||||
What is intentionally out of scope?
|
||||
<!--
|
||||
Describe the concrete user, product, or operational problem.
|
||||
For fixes, begin with:
|
||||
"Fixes an issue where users <do X> would <experience Y> when <condition>."
|
||||
or:
|
||||
"Resolves a problem where..."
|
||||
|
||||
What does success look like?
|
||||
Name the affected UI surface or workflow. Do not describe the code-level cause here.
|
||||
-->
|
||||
|
||||
What should reviewers focus on?
|
||||
## Why This Change Was Made
|
||||
|
||||
<details>
|
||||
<summary>Summary guidance</summary>
|
||||
<!--
|
||||
In one or two sentences, explain the complete shipped solution, key design
|
||||
decisions, and relevant boundaries or non-goals. Include implementation detail
|
||||
only when it helps reviewers understand user-visible behavior or risk.
|
||||
Avoid file-by-file narration.
|
||||
-->
|
||||
|
||||
This PR description is the contributor's durable explanation of the change. Write it for human maintainers first; ClawSweeper and Barnacle use the same text to understand intent, proof, risk, and current review state.
|
||||
## User Impact
|
||||
|
||||
Describe the intent and outcome in 2-5 bullets. Avoid restating the diff; reviewers and bots can read the changed files.
|
||||
<!--
|
||||
State what users, operators, or developers can now do or expect. Lead with the
|
||||
concrete benefit and use user-facing language. If there is no user-visible
|
||||
impact, say so plainly.
|
||||
-->
|
||||
|
||||
If this PR fixes a plugin beta-release blocker, title it `fix(<plugin-id>): beta blocker - <summary>` and link the matching `Beta blocker: <plugin-name> - <summary>` issue labeled `beta-blocker`. Contributors cannot label PRs, so the title is the PR-side signal for maintainers and automation.
|
||||
## Evidence
|
||||
|
||||
</details>
|
||||
<!--
|
||||
Show the most useful proof that this change works. Screenshots, screencasts,
|
||||
terminal output, focused tests, CI results, live observations, redacted logs,
|
||||
and artifact links are all useful. Include before/after evidence for visual
|
||||
changes when it clarifies the result.
|
||||
|
||||
## Linked context
|
||||
|
||||
Which issue does this close?
|
||||
|
||||
Closes #
|
||||
|
||||
Which issues, PRs, or discussions are related?
|
||||
|
||||
Related #
|
||||
|
||||
Was this requested by a maintainer or owner?
|
||||
|
||||
<details>
|
||||
<summary>Linked context guidance</summary>
|
||||
|
||||
Link the issue, PR, discussion, maintainer request, or owner request that explains why this PR should exist. Maintainer context helps reviewers and automation distinguish intended work from drive-by churn.
|
||||
|
||||
</details>
|
||||
|
||||
## Real behavior proof (required for external PRs)
|
||||
|
||||
- Behavior or issue addressed:
|
||||
- Real environment tested:
|
||||
- Exact steps or command run after this patch:
|
||||
- Evidence after fix (screenshot, recording, terminal capture, console output, redacted runtime log, linked artifact, or copied live output):
|
||||
- Observed result after fix:
|
||||
- What was not tested:
|
||||
- Proof limitations or environment constraints:
|
||||
- Before evidence (optional but encouraged):
|
||||
|
||||
<details>
|
||||
<summary>Real behavior proof guidance</summary>
|
||||
|
||||
External contributors must show after-fix evidence from a real OpenClaw setup. Unit tests, mocks, lint, typechecks, snapshots, and CI are supplemental only.
|
||||
|
||||
Screenshots are encouraged even for CLI, console, text, or log changes. Terminal screenshots, copied live output, redacted runtime logs, recordings, and linked artifacts count.
|
||||
|
||||
If your environment cannot produce the ideal proof, explain that under `Proof limitations or environment constraints` so reviewers and ClawSweeper can direct the next step properly.
|
||||
|
||||
Be mindful of private information like IP addresses, API keys, phone numbers, non-public endpoints, or other private details when providing evidence.
|
||||
|
||||
</details>
|
||||
|
||||
## Tests and validation
|
||||
|
||||
Which commands did you run?
|
||||
|
||||
What regression coverage was added or updated?
|
||||
|
||||
What failed before this fix, if known?
|
||||
|
||||
If no test was added, why not?
|
||||
|
||||
<details>
|
||||
<summary>Testing guidance</summary>
|
||||
|
||||
List focused commands, not every incidental check. CI is useful support, but external PRs still need real behavior proof above when behavior changes.
|
||||
|
||||
</details>
|
||||
|
||||
## Risk checklist
|
||||
|
||||
Did user-visible behavior change? (`Yes/No`)
|
||||
|
||||
Did config, environment, or migration behavior change? (`Yes/No`)
|
||||
|
||||
Did security, auth, secrets, network, or tool execution behavior change? (`Yes/No`)
|
||||
|
||||
What is the highest-risk area?
|
||||
|
||||
How is that risk mitigated?
|
||||
|
||||
<details>
|
||||
<summary>Risk guidance</summary>
|
||||
|
||||
Use this for author judgment that is not obvious from the diff. ClawSweeper can see touched files, but it cannot know which behavior you think is risky, why the risk is acceptable, or what mitigation reviewers should verify.
|
||||
|
||||
</details>
|
||||
|
||||
## Current review state
|
||||
|
||||
What is the next action?
|
||||
|
||||
What is still waiting on author, maintainer, CI, or external proof?
|
||||
|
||||
Which bot or reviewer comments were addressed?
|
||||
|
||||
<details>
|
||||
<summary>Review state guidance</summary>
|
||||
|
||||
Keep this as the durable state for review progress. If useful information appears in comments, fold the current next action or blocker back here so maintainers and ClawSweeper do not need to reconstruct state from comment history.
|
||||
|
||||
</details>
|
||||
Reviewers will inspect the code, tests, and CI. Use this section to make the
|
||||
validation easy to understand, not to restate the diff.
|
||||
-->
|
||||
|
||||
29
.github/workflows/ci-build-artifacts-testbox.yml
vendored
29
.github/workflows/ci-build-artifacts-testbox.yml
vendored
@@ -14,6 +14,10 @@ on:
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.event_name == 'pull_request' && format('{0}-pr-v1-{1}', github.workflow, github.event.pull_request.number) || format('{0}-manual-v1-{1}', github.workflow, github.run_id) }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||
|
||||
@@ -210,24 +214,49 @@ jobs:
|
||||
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 }}
|
||||
DEEPINFRA_API_KEY: ${{ secrets.DEEPINFRA_API_KEY }}
|
||||
DASHSCOPE_API_KEY: ${{ secrets.DASHSCOPE_API_KEY }}
|
||||
FACTORY_API_KEY: ${{ secrets.FACTORY_API_KEY }}
|
||||
FIREWORKS_API_KEY: ${{ secrets.FIREWORKS_API_KEY }}
|
||||
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
|
||||
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
|
||||
GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
|
||||
KIMI_API_KEY: ${{ secrets.KIMI_API_KEY }}
|
||||
MINIMAX_API_KEY: ${{ secrets.MINIMAX_API_KEY }}
|
||||
MODELSTUDIO_API_KEY: ${{ secrets.MODELSTUDIO_API_KEY }}
|
||||
MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }}
|
||||
MOONSHOT_API_KEY: ${{ secrets.MOONSHOT_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 }}
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
|
||||
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
|
||||
QWEN_API_KEY: ${{ secrets.QWEN_API_KEY }}
|
||||
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 }}
|
||||
run: bash scripts/ci-hydrate-testbox-env.sh
|
||||
|
||||
- name: Run Testbox
|
||||
|
||||
28
.github/workflows/ci-check-arm-testbox.yml
vendored
28
.github/workflows/ci-check-arm-testbox.yml
vendored
@@ -13,6 +13,10 @@ on:
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.event_name == 'pull_request' && format('{0}-pr-v1-{1}', github.workflow, github.event.pull_request.number) || format('{0}-manual-v1-{1}', github.workflow, github.run_id) }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||
PNPM_CONFIG_STORE_DIR: "/tmp/openclaw-pnpm-store"
|
||||
@@ -128,8 +132,10 @@ jobs:
|
||||
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 }}
|
||||
DEEPINFRA_API_KEY: ${{ secrets.DEEPINFRA_API_KEY }}
|
||||
DASHSCOPE_API_KEY: ${{ secrets.DASHSCOPE_API_KEY }}
|
||||
FACTORY_API_KEY: ${{ secrets.FACTORY_API_KEY }}
|
||||
FIREWORKS_API_KEY: ${{ secrets.FIREWORKS_API_KEY }}
|
||||
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
|
||||
@@ -137,16 +143,38 @@ jobs:
|
||||
GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
|
||||
KIMI_API_KEY: ${{ secrets.KIMI_API_KEY }}
|
||||
MINIMAX_API_KEY: ${{ secrets.MINIMAX_API_KEY }}
|
||||
MODELSTUDIO_API_KEY: ${{ secrets.MODELSTUDIO_API_KEY }}
|
||||
MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }}
|
||||
MOONSHOT_API_KEY: ${{ secrets.MOONSHOT_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 }}
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
|
||||
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
|
||||
QWEN_API_KEY: ${{ secrets.QWEN_API_KEY }}
|
||||
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 }}
|
||||
run: bash scripts/ci-hydrate-testbox-env.sh
|
||||
|
||||
- name: Run Testbox
|
||||
|
||||
28
.github/workflows/ci-check-testbox.yml
vendored
28
.github/workflows/ci-check-testbox.yml
vendored
@@ -17,6 +17,10 @@ on:
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.event_name == 'pull_request' && format('{0}-pr-v1-{1}', github.workflow, github.event.pull_request.number) || format('{0}-manual-v1-{1}', github.workflow, github.run_id) }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||
PNPM_CONFIG_STORE_DIR: "/tmp/openclaw-pnpm-store"
|
||||
@@ -117,8 +121,10 @@ jobs:
|
||||
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 }}
|
||||
DEEPINFRA_API_KEY: ${{ secrets.DEEPINFRA_API_KEY }}
|
||||
DASHSCOPE_API_KEY: ${{ secrets.DASHSCOPE_API_KEY }}
|
||||
FACTORY_API_KEY: ${{ secrets.FACTORY_API_KEY }}
|
||||
FIREWORKS_API_KEY: ${{ secrets.FIREWORKS_API_KEY }}
|
||||
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
|
||||
@@ -126,16 +132,38 @@ jobs:
|
||||
GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
|
||||
KIMI_API_KEY: ${{ secrets.KIMI_API_KEY }}
|
||||
MINIMAX_API_KEY: ${{ secrets.MINIMAX_API_KEY }}
|
||||
MODELSTUDIO_API_KEY: ${{ secrets.MODELSTUDIO_API_KEY }}
|
||||
MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }}
|
||||
MOONSHOT_API_KEY: ${{ secrets.MOONSHOT_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 }}
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
|
||||
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
|
||||
QWEN_API_KEY: ${{ secrets.QWEN_API_KEY }}
|
||||
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 }}
|
||||
run: bash scripts/ci-hydrate-testbox-env.sh
|
||||
|
||||
- name: Run Testbox
|
||||
|
||||
39
.github/workflows/ci.yml
vendored
39
.github/workflows/ci.yml
vendored
@@ -197,7 +197,7 @@ jobs:
|
||||
node --input-type=module <<'EOF'
|
||||
import { appendFileSync } from "node:fs";
|
||||
import {
|
||||
createNodeTestShards,
|
||||
createNodeTestShardBundles,
|
||||
} from "./scripts/lib/ci-node-test-plan.mjs";
|
||||
import {
|
||||
createChannelContractTestShards,
|
||||
@@ -273,7 +273,7 @@ jobs:
|
||||
}
|
||||
|
||||
const nodeTestShards = runNodeFull
|
||||
? createNodeTestShards({
|
||||
? createNodeTestShardBundles({
|
||||
includeReleaseOnlyPluginShards: false,
|
||||
}).map((shard) => ({
|
||||
check_name: shard.checkName,
|
||||
@@ -320,7 +320,14 @@ jobs:
|
||||
run_checks_windows: runWindows,
|
||||
checks_windows_matrix: createMatrix(
|
||||
runWindows
|
||||
? [{ check_name: "checks-windows-node-test", runtime: "node", task: "test" }]
|
||||
? [
|
||||
{
|
||||
check_name: "checks-windows-node-test",
|
||||
runtime: "node",
|
||||
task: "test",
|
||||
runner: "blacksmith-8vcpu-windows-2025",
|
||||
},
|
||||
]
|
||||
: [],
|
||||
),
|
||||
run_macos_node: runMacos,
|
||||
@@ -558,7 +565,7 @@ jobs:
|
||||
contents: read
|
||||
needs: [preflight]
|
||||
if: needs.preflight.outputs.run_build_artifacts == 'true'
|
||||
runs-on: ${{ github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && 'blacksmith-32vcpu-ubuntu-2404' || 'ubuntu-24.04') }}
|
||||
runs-on: ${{ github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && 'blacksmith-16vcpu-ubuntu-2404' || 'ubuntu-24.04') }}
|
||||
timeout-minutes: 20
|
||||
outputs:
|
||||
channels-result: ${{ steps.built_artifact_checks.outputs['channels-result'] }}
|
||||
@@ -819,6 +826,7 @@ jobs:
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: 4
|
||||
matrix: ${{ fromJson(needs.preflight.outputs.checks_fast_core_matrix) }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -908,6 +916,7 @@ jobs:
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: 4
|
||||
matrix: ${{ fromJson(needs.preflight.outputs.plugin_contracts_matrix) }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -988,6 +997,7 @@ jobs:
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: 4
|
||||
matrix: ${{ fromJson(needs.preflight.outputs.channel_contracts_matrix) }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -1136,10 +1146,11 @@ jobs:
|
||||
name: ${{ matrix.check_name }}
|
||||
needs: [preflight]
|
||||
if: needs.preflight.outputs.run_checks_node_core_nondist == 'true'
|
||||
runs-on: ${{ github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && (matrix.runner || 'blacksmith-8vcpu-ubuntu-2404') || 'ubuntu-24.04') }}
|
||||
runs-on: ${{ github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && (matrix.runner || 'blacksmith-4vcpu-ubuntu-2404') || 'ubuntu-24.04') }}
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: 6
|
||||
matrix: ${{ fromJson(needs.preflight.outputs.checks_node_core_nondist_matrix) }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -1248,6 +1259,7 @@ jobs:
|
||||
timeout-minutes: 20
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: 4
|
||||
matrix:
|
||||
include:
|
||||
- check_name: check-guards
|
||||
@@ -1264,7 +1276,7 @@ jobs:
|
||||
runner: blacksmith-16vcpu-ubuntu-2404
|
||||
- check_name: check-dependencies
|
||||
task: dependencies
|
||||
runner: blacksmith-8vcpu-ubuntu-2404
|
||||
runner: blacksmith-4vcpu-ubuntu-2404
|
||||
- check_name: check-test-types
|
||||
task: test-types
|
||||
runner: blacksmith-4vcpu-ubuntu-2404
|
||||
@@ -1385,30 +1397,39 @@ jobs:
|
||||
name: ${{ matrix.check_name }}
|
||||
needs: [preflight]
|
||||
if: ${{ !cancelled() && always() && needs.preflight.outputs.run_check_additional == 'true' }}
|
||||
runs-on: ${{ github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && 'blacksmith-8vcpu-ubuntu-2404' || 'ubuntu-24.04') }}
|
||||
runs-on: ${{ github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && (matrix.runner || 'blacksmith-4vcpu-ubuntu-2404') || 'ubuntu-24.04') }}
|
||||
timeout-minutes: 20
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: 4
|
||||
matrix:
|
||||
include:
|
||||
- check_name: check-additional-boundaries-a
|
||||
group: boundaries
|
||||
boundary_shard: 1/4
|
||||
runner: blacksmith-8vcpu-ubuntu-2404
|
||||
- check_name: check-additional-boundaries-bcd
|
||||
group: boundaries
|
||||
boundary_shard: 2/4,3/4,4/4
|
||||
runner: blacksmith-8vcpu-ubuntu-2404
|
||||
- check_name: check-session-accessor-boundary
|
||||
group: session-accessor-boundary
|
||||
runner: blacksmith-4vcpu-ubuntu-2404
|
||||
- check_name: check-session-transcript-reader-boundary
|
||||
group: session-transcript-reader-boundary
|
||||
runner: blacksmith-4vcpu-ubuntu-2404
|
||||
- check_name: check-additional-extension-channels
|
||||
group: extension-channels
|
||||
runner: blacksmith-8vcpu-ubuntu-2404
|
||||
- check_name: check-additional-extension-bundled
|
||||
group: extension-bundled
|
||||
runner: blacksmith-8vcpu-ubuntu-2404
|
||||
- check_name: check-additional-extension-package-boundary
|
||||
group: extension-package-boundary
|
||||
runner: blacksmith-8vcpu-ubuntu-2404
|
||||
- check_name: check-additional-runtime-topology-architecture
|
||||
group: runtime-topology-architecture
|
||||
runner: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
- name: Checkout
|
||||
shell: bash
|
||||
@@ -1751,7 +1772,7 @@ jobs:
|
||||
name: ${{ matrix.check_name }}
|
||||
needs: [preflight]
|
||||
if: needs.preflight.outputs.run_checks_windows == 'true'
|
||||
runs-on: ${{ github.event_name == 'workflow_dispatch' && 'windows-2025' || (github.repository == 'openclaw/openclaw' && 'blacksmith-16vcpu-windows-2025' || 'windows-2025') }}
|
||||
runs-on: ${{ github.event_name == 'workflow_dispatch' && 'windows-2025' || (github.repository == 'openclaw/openclaw' && (matrix.runner || 'blacksmith-8vcpu-windows-2025') || 'windows-2025') }}
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=8192
|
||||
@@ -1763,6 +1784,7 @@ jobs:
|
||||
shell: bash
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: 2
|
||||
matrix: ${{ fromJson(needs.preflight.outputs.checks_windows_matrix) }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -2092,6 +2114,7 @@ jobs:
|
||||
timeout-minutes: 20
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: 2
|
||||
matrix: ${{ fromJson(needs.preflight.outputs.android_matrix) }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
|
||||
81
.github/workflows/clawsweeper-dispatch.yml
vendored
81
.github/workflows/clawsweeper-dispatch.yml
vendored
@@ -18,15 +18,16 @@ permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: clawsweeper-dispatch-${{ github.repository }}-${{ github.event.issue.number || github.event.pull_request.number || github.run_id }}
|
||||
cancel-in-progress: ${{ github.event.action == 'edited' || github.event.action == 'synchronize' || github.event.action == 'ready_for_review' }}
|
||||
group: ${{ github.event_name == 'push' && format('clawsweeper-dispatch-{0}-{1}', github.repository, github.ref) || format('clawsweeper-dispatch-{0}-{1}', github.repository, github.event.issue.number || github.event.pull_request.number || github.run_id) }}
|
||||
cancel-in-progress: ${{ github.event_name == 'push' || github.event.action == 'edited' || github.event.action == 'synchronize' || github.event.action == 'ready_for_review' }}
|
||||
|
||||
jobs:
|
||||
dispatch:
|
||||
runs-on: ubuntu-latest
|
||||
if: >-
|
||||
${{
|
||||
github.event_name == 'issue_comment' ||
|
||||
(github.event_name != 'issue_comment' ||
|
||||
(github.actor != 'clawsweeper[bot]' && github.actor != 'openclaw-clawsweeper[bot]')) &&
|
||||
!(
|
||||
endsWith(github.actor, '[bot]') &&
|
||||
(github.event.action == 'labeled' || github.event.action == 'unlabeled')
|
||||
@@ -41,6 +42,34 @@ jobs:
|
||||
if: ${{ github.event.action == 'labeled' || github.event.action == 'unlabeled' }}
|
||||
run: sleep 20
|
||||
|
||||
- name: Debounce main push dispatch
|
||||
if: ${{ github.event_name == 'push' }}
|
||||
run: sleep 45
|
||||
|
||||
- name: Install GitHub API backoff helper
|
||||
run: |
|
||||
cat > "$RUNNER_TEMP/github-api-backoff.sh" <<'BASH'
|
||||
gh_api_with_retry() {
|
||||
local attempt output status lower_output
|
||||
for attempt in 1 2 3 4 5; do
|
||||
if output="$(gh api "$@" 2>&1)"; then
|
||||
printf '%s\n' "$output"
|
||||
return 0
|
||||
fi
|
||||
status=$?
|
||||
lower_output="${output,,}"
|
||||
if [[ "$lower_output" != *"rate limit"* && "$output" != *"HTTP 429"* ]]; then
|
||||
printf '%s\n' "$output" >&2
|
||||
return "$status"
|
||||
fi
|
||||
echo "::warning::GitHub API throttled ClawSweeper dispatch on attempt ${attempt}; retrying after backoff." >&2
|
||||
sleep $((attempt * attempt * 5))
|
||||
done
|
||||
printf '%s\n' "$output" >&2
|
||||
return "$status"
|
||||
}
|
||||
BASH
|
||||
|
||||
- name: Create ClawSweeper dispatch token
|
||||
id: token
|
||||
if: ${{ env.HAS_CLAWSWEEPER_APP_PRIVATE_KEY == 'true' }}
|
||||
@@ -52,9 +81,27 @@ jobs:
|
||||
repositories: clawsweeper
|
||||
permission-contents: write
|
||||
|
||||
- name: Pre-filter ClawSweeper comment
|
||||
id: comment_filter
|
||||
if: ${{ github.event_name == 'issue_comment' }}
|
||||
env:
|
||||
COMMENT_BODY: ${{ github.event.comment.body }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if grep -Eiq '(^|[[:space:]])@(clawsweeper|openclaw-clawsweeper)\b(\[bot\])?|(^|[[:space:]])/(clawsweeper|review|autoclose|auto([[:space:]]+|-)?merge)\b' <<< "$COMMENT_BODY"; then
|
||||
echo "is_command=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "is_command=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Create target comment token
|
||||
id: target_token
|
||||
if: ${{ github.event_name == 'issue_comment' && env.HAS_CLAWSWEEPER_APP_PRIVATE_KEY == 'true' }}
|
||||
if: >-
|
||||
${{
|
||||
github.event_name == 'issue_comment' &&
|
||||
steps.comment_filter.outputs.is_command == 'true' &&
|
||||
env.HAS_CLAWSWEEPER_APP_PRIVATE_KEY == 'true'
|
||||
}}
|
||||
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
|
||||
with:
|
||||
client-id: ${{ env.CLAWSWEEPER_APP_CLIENT_ID }}
|
||||
@@ -77,6 +124,7 @@ jobs:
|
||||
echo "::notice::Skipping GitHub activity dispatch because no ClawSweeper app token is configured."
|
||||
exit 0
|
||||
fi
|
||||
. "$RUNNER_TEMP/github-api-backoff.sh"
|
||||
activity="$(jq -c \
|
||||
--arg target_repo "$TARGET_REPO" \
|
||||
--arg event_name "$SOURCE_EVENT" \
|
||||
@@ -143,7 +191,7 @@ jobs:
|
||||
' "$GITHUB_EVENT_PATH")"
|
||||
payload="$(jq -nc --argjson activity "$activity" \
|
||||
'{event_type:"github_activity",client_payload:{activity:$activity}}')"
|
||||
if gh api repos/openclaw/clawsweeper/dispatches \
|
||||
if gh_api_with_retry repos/openclaw/clawsweeper/dispatches \
|
||||
--method POST \
|
||||
--input - <<< "$payload"; then
|
||||
echo "Dispatched GitHub activity to ClawSweeper."
|
||||
@@ -165,6 +213,7 @@ jobs:
|
||||
echo "::notice::Skipping ClawSweeper dispatch because no ClawSweeper app token is configured. Not falling back to a maintainer token."
|
||||
exit 0
|
||||
fi
|
||||
. "$RUNNER_TEMP/github-api-backoff.sh"
|
||||
payload="$(jq -nc \
|
||||
--arg target_repo "$TARGET_REPO" \
|
||||
--argjson item_number "$ITEM_NUMBER" \
|
||||
@@ -173,7 +222,7 @@ jobs:
|
||||
--arg source_action "$SOURCE_ACTION" \
|
||||
--argjson supersedes_in_progress "$SUPERSEDES_IN_PROGRESS" \
|
||||
'{event_type:"clawsweeper_item",client_payload:{target_repo:$target_repo,item_number:$item_number,item_kind:$item_kind,source_event:$source_event,source_action:$source_action,supersedes_in_progress:$supersedes_in_progress}}')"
|
||||
if gh api repos/openclaw/clawsweeper/dispatches \
|
||||
if gh_api_with_retry repos/openclaw/clawsweeper/dispatches \
|
||||
--method POST \
|
||||
--input - <<< "$payload"; then
|
||||
echo "Dispatched ClawSweeper review."
|
||||
@@ -182,7 +231,11 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Acknowledge and dispatch ClawSweeper comment
|
||||
if: ${{ github.event_name == 'issue_comment' }}
|
||||
if: >-
|
||||
${{
|
||||
github.event_name == 'issue_comment' &&
|
||||
steps.comment_filter.outputs.is_command == 'true'
|
||||
}}
|
||||
env:
|
||||
DISPATCH_TOKEN: ${{ steps.token.outputs.token }}
|
||||
TARGET_TOKEN: ${{ steps.target_token.outputs.token }}
|
||||
@@ -198,15 +251,12 @@ jobs:
|
||||
echo "::notice::Skipping ClawSweeper comment dispatch because no ClawSweeper app token is configured."
|
||||
exit 0
|
||||
fi
|
||||
. "$RUNNER_TEMP/github-api-backoff.sh"
|
||||
body_file="$RUNNER_TEMP/clawsweeper-comment-body.txt"
|
||||
printf '%s\n' "$COMMENT_BODY" > "$body_file"
|
||||
if ! grep -Eiq '(^|[[:space:]])@(clawsweeper|openclaw-clawsweeper)\b(\[bot\])?|(^|[[:space:]])/(clawsweeper|review|automerge|autoclose)\b' "$body_file"; then
|
||||
echo "No ClawSweeper command found in comment."
|
||||
exit 0
|
||||
fi
|
||||
if [ -n "$TARGET_TOKEN" ]; then
|
||||
err="$(mktemp)"
|
||||
if GH_TOKEN="$TARGET_TOKEN" gh api -X POST \
|
||||
if GH_TOKEN="$TARGET_TOKEN" gh_api_with_retry -X POST \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
"repos/$TARGET_REPO/issues/comments/$COMMENT_ID/reactions" \
|
||||
-f content="eyes" 2>"$err" >/dev/null; then
|
||||
@@ -233,7 +283,7 @@ jobs:
|
||||
"Command router queued. I will update this comment with the next step.")"
|
||||
status_payload="$(jq -nc --arg body "$status_body" '{body:$body}')"
|
||||
status_err="$(mktemp)"
|
||||
if status_response="$(GH_TOKEN="$TARGET_TOKEN" gh api \
|
||||
if status_response="$(GH_TOKEN="$TARGET_TOKEN" gh_api_with_retry \
|
||||
"repos/$TARGET_REPO/issues/$ITEM_NUMBER/comments" \
|
||||
--method POST \
|
||||
--input - <<< "$status_payload" 2>"$status_err")"; then
|
||||
@@ -254,7 +304,7 @@ jobs:
|
||||
--arg source_event "issue_comment" \
|
||||
--arg source_action "$SOURCE_ACTION" \
|
||||
'{event_type:"clawsweeper_comment",client_payload:({target_repo:$target_repo,item_number:$item_number,comment_id:$comment_id,source_event:$source_event,source_action:$source_action,max_comments:"1"} + (if $status_comment_id != "" then {status_comment_id:($status_comment_id|tonumber)} else {} end))}')"
|
||||
if GH_TOKEN="$DISPATCH_TOKEN" gh api repos/openclaw/clawsweeper/dispatches \
|
||||
if GH_TOKEN="$DISPATCH_TOKEN" gh_api_with_retry repos/openclaw/clawsweeper/dispatches \
|
||||
--method POST \
|
||||
--input - <<< "$payload"; then
|
||||
echo "Dispatched ClawSweeper comment router."
|
||||
@@ -276,6 +326,7 @@ jobs:
|
||||
echo "::notice::Skipping ClawSweeper commit dispatch because no ClawSweeper app token is configured. Not falling back to a maintainer token."
|
||||
exit 0
|
||||
fi
|
||||
. "$RUNNER_TEMP/github-api-backoff.sh"
|
||||
case "$CREATE_CHECKS" in
|
||||
true|TRUE|1|yes|YES|on|ON) create_checks=true ;;
|
||||
*) create_checks=false ;;
|
||||
@@ -287,7 +338,7 @@ jobs:
|
||||
--arg ref "$SOURCE_REF" \
|
||||
--argjson create_checks "$create_checks" \
|
||||
'{event_type:"clawsweeper_commit_review",client_payload:{target_repo:$target_repo,before_sha:$before_sha,after_sha:$after_sha,ref:$ref,enabled:true,create_checks:$create_checks}}')"
|
||||
if gh api repos/openclaw/clawsweeper/dispatches \
|
||||
if gh_api_with_retry repos/openclaw/clawsweeper/dispatches \
|
||||
--method POST \
|
||||
--input - <<< "$payload"; then
|
||||
echo "Dispatched ClawSweeper commit review."
|
||||
|
||||
@@ -6,7 +6,7 @@ on:
|
||||
- cron: "0 7 * * *"
|
||||
|
||||
concurrency:
|
||||
group: codeql-android-critical-security-${{ github.workflow }}-${{ github.event_name == 'workflow_dispatch' && github.run_id || github.sha }}
|
||||
group: codeql-android-critical-security-${{ github.workflow }}-${{ github.event_name == 'workflow_dispatch' && format('manual-{0}', github.run_id) || format('ref-{0}', github.ref) }}
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
|
||||
@@ -96,7 +96,7 @@ on:
|
||||
- "src/auto-reply/reply/post-compaction-context.ts"
|
||||
- "src/auto-reply/reply/queue/**"
|
||||
- "src/auto-reply/reply/startup-context.ts"
|
||||
- "src/commands/doctor-cron-dreaming-payload-migration.ts"
|
||||
- "src/commands/doctor/cron/dreaming-payload-migration.ts"
|
||||
- "src/commands/doctor-memory-search.ts"
|
||||
- "src/commands/doctor-session-*.ts"
|
||||
- "src/commands/session-store-targets.ts"
|
||||
@@ -136,7 +136,7 @@ on:
|
||||
- cron: "30 6 * * *"
|
||||
|
||||
concurrency:
|
||||
group: codeql-critical-quality-${{ github.workflow }}-${{ github.event_name == 'workflow_dispatch' && github.run_id || github.event_name == 'pull_request' && github.event.pull_request.number || github.sha }}
|
||||
group: codeql-critical-quality-${{ github.workflow }}-${{ github.event_name == 'workflow_dispatch' && format('manual-{0}', github.run_id) || github.event_name == 'pull_request' && format('pr-{0}', github.event.pull_request.number) || format('ref-{0}', github.ref) }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
env:
|
||||
@@ -257,7 +257,7 @@ jobs:
|
||||
packages/gateway-protocol/src/*|packages/gateway-protocol/src/**/*|src/gateway/method-scopes.ts|src/gateway/server-methods/*|src/gateway/server-methods.ts|src/gateway/server-methods-list.ts)
|
||||
gateway=true
|
||||
;;
|
||||
packages/memory-host-sdk/*|src/commands/doctor-cron-dreaming-payload-migration.ts|src/commands/doctor-memory-search.ts|src/gateway/server-startup-memory.ts|src/memory/*|src/memory-host-sdk/*)
|
||||
packages/memory-host-sdk/*|src/commands/doctor/cron/dreaming-payload-migration.ts|src/commands/doctor-memory-search.ts|src/gateway/server-startup-memory.ts|src/memory/*|src/memory-host-sdk/*)
|
||||
memory=true
|
||||
;;
|
||||
src/infra/outbound/base-session-key.ts|src/infra/outbound/delivery-queue*.ts|src/infra/outbound/outbound-session.ts|src/infra/outbound/session-binding*.ts|src/infra/outbound/session-context.ts|src/infra/outbound/targets-session.ts)
|
||||
@@ -295,7 +295,7 @@ jobs:
|
||||
src/model-catalog/*|src/plugins/*provider*.ts|src/plugins/capability-provider-runtime.ts|src/plugins/compaction-provider.ts|src/plugins/memory-embedding-provider*.ts|src/plugins/memory-embedding-providers*.ts|src/plugins/migration-provider-runtime.ts|src/plugins/synthetic-auth.runtime.ts|src/plugins/web-fetch-providers*.ts|src/plugins/web-search-providers*.ts)
|
||||
provider=true
|
||||
;;
|
||||
src/plugins/activation-planner.ts|src/plugins/api-builder.ts|src/plugins/bundled-*.ts|src/plugins/captured-registration.ts|src/plugins/config-*.ts|src/plugins/discovery.ts|src/plugins/effective-plugin-ids.ts|src/plugins/externalized-bundled-plugins.ts|src/plugins/installed-plugin-index*.ts|src/plugins/loader*.ts|src/plugins/manifest*.ts|src/plugins/module-export.ts|src/plugins/package-entrypoints.ts|src/plugins/plugin-registry*.ts|src/plugins/public-surface*.ts|src/plugins/registry.ts|src/plugins/registry-types.ts|src/plugins/runtime|src/plugins/runtime/*|src/plugins/runtime-state.ts|src/plugins/runtime.ts|src/plugins/sdk-alias.ts|src/plugins/source-loader.ts|src/plugins/types.ts|src/plugins/validation-diagnostics.ts)
|
||||
src/plugins/activation-planner.ts|src/plugins/api-builder.ts|src/plugins/bundled-*.ts|src/plugins/captured-registration.ts|src/plugins/config-*.ts|src/plugins/discovery.ts|src/plugins/effective-plugin-ids.ts|src/plugins/externalized-bundled-plugins.ts|src/plugins/installed-plugin-index*.ts|src/plugins/loader*.ts|src/plugins/manifest*.ts|src/plugins/module-export.ts|src/plugins/package-entrypoints.ts|src/plugins/plugin-registry*.ts|src/plugins/public-surface*.ts|src/plugins/registry.ts|src/plugins/registry-types.ts|src/plugins/runtime|src/plugins/runtime/*|src/plugins/runtime-state.ts|src/plugins/runtime.ts|src/plugins/sdk-alias.ts|src/plugins/types.ts|src/plugins/validation-diagnostics.ts)
|
||||
plugin=true
|
||||
;;
|
||||
packages/plugin-package-contract/*|packages/plugin-sdk/*)
|
||||
|
||||
@@ -6,7 +6,7 @@ on:
|
||||
- cron: "0 8 * * 1"
|
||||
|
||||
concurrency:
|
||||
group: codeql-macos-critical-security-${{ github.workflow }}-${{ github.event_name == 'workflow_dispatch' && github.run_id || github.sha }}
|
||||
group: codeql-macos-critical-security-${{ github.workflow }}-${{ github.event_name == 'workflow_dispatch' && format('manual-{0}', github.run_id) || format('ref-{0}', github.ref) }}
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
|
||||
4
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
@@ -32,8 +32,8 @@ on:
|
||||
- cron: "0 6 * * *"
|
||||
|
||||
concurrency:
|
||||
group: codeql-${{ github.workflow }}-${{ github.event_name == 'workflow_dispatch' && github.run_id || github.event_name == 'pull_request' && github.event.pull_request.number || github.sha }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
group: codeql-${{ github.workflow }}-${{ github.event_name == 'workflow_dispatch' && format('manual-{0}', github.run_id) || github.event_name == 'pull_request' && format('pr-{0}', github.event.pull_request.number) || format('ref-{0}', github.ref) }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' || (github.event_name == 'push' && github.ref == 'refs/heads/main') }}
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||
|
||||
@@ -23,8 +23,8 @@ permissions:
|
||||
contents: write
|
||||
|
||||
concurrency:
|
||||
group: control-ui-locale-refresh
|
||||
cancel-in-progress: false
|
||||
group: control-ui-locale-refresh-${{ github.event_name == 'push' && github.ref || github.event_name == 'workflow_dispatch' && format('manual-{0}', github.run_id) || github.event_name == 'release' && format('release-{0}', github.event.release.tag_name) || format('{0}-{1}', github.event_name, github.run_id) }}
|
||||
cancel-in-progress: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
|
||||
|
||||
jobs:
|
||||
plan:
|
||||
|
||||
24
.github/workflows/crabbox-hydrate.yml
vendored
24
.github/workflows/crabbox-hydrate.yml
vendored
@@ -663,8 +663,10 @@ jobs:
|
||||
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 }}
|
||||
DEEPINFRA_API_KEY: ${{ secrets.DEEPINFRA_API_KEY }}
|
||||
DASHSCOPE_API_KEY: ${{ secrets.DASHSCOPE_API_KEY }}
|
||||
FACTORY_API_KEY: ${{ secrets.FACTORY_API_KEY }}
|
||||
FIREWORKS_API_KEY: ${{ secrets.FIREWORKS_API_KEY }}
|
||||
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
|
||||
@@ -672,16 +674,38 @@ jobs:
|
||||
GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
|
||||
KIMI_API_KEY: ${{ secrets.KIMI_API_KEY }}
|
||||
MINIMAX_API_KEY: ${{ secrets.MINIMAX_API_KEY }}
|
||||
MODELSTUDIO_API_KEY: ${{ secrets.MODELSTUDIO_API_KEY }}
|
||||
MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }}
|
||||
MOONSHOT_API_KEY: ${{ secrets.MOONSHOT_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 }}
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
|
||||
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
|
||||
QWEN_API_KEY: ${{ secrets.QWEN_API_KEY }}
|
||||
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 }}
|
||||
run: bash scripts/ci-hydrate-testbox-env.sh
|
||||
|
||||
- name: Mark Crabbox ready
|
||||
|
||||
4
.github/workflows/docs-sync-publish.yml
vendored
4
.github/workflows/docs-sync-publish.yml
vendored
@@ -13,6 +13,10 @@ on:
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: docs-sync-publish-${{ github.event_name == 'workflow_dispatch' && format('manual-{0}', github.run_id) || github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
|
||||
|
||||
jobs:
|
||||
sync-publish-repo:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
111
.github/workflows/full-release-validation.yml
vendored
111
.github/workflows/full-release-validation.yml
vendored
@@ -70,7 +70,7 @@ on:
|
||||
default: ""
|
||||
type: string
|
||||
npm_telegram_package_spec:
|
||||
description: Optional published package spec for the package Telegram E2E lane
|
||||
description: Optional published package spec for the focused package Telegram E2E rerun
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
@@ -95,7 +95,7 @@ on:
|
||||
default: ""
|
||||
type: string
|
||||
npm_telegram_provider_mode:
|
||||
description: Provider mode for the package Telegram E2E lane
|
||||
description: Provider mode for the focused package Telegram E2E rerun
|
||||
required: false
|
||||
default: mock-openai
|
||||
type: choice
|
||||
@@ -103,7 +103,7 @@ on:
|
||||
- mock-openai
|
||||
- live-frontier
|
||||
npm_telegram_scenario:
|
||||
description: Optional comma-separated Telegram scenario ids for the package Telegram lane
|
||||
description: Optional comma-separated Telegram scenario ids for the focused package Telegram E2E rerun
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
@@ -200,14 +200,16 @@ jobs:
|
||||
if [[ -n "${RELEASE_PACKAGE_SPEC// }" ]]; then
|
||||
echo "- Published release package: \`${RELEASE_PACKAGE_SPEC}\`"
|
||||
fi
|
||||
if [[ -n "${NPM_TELEGRAM_PACKAGE_SPEC// }" ]]; then
|
||||
if [[ "$RERUN_GROUP" == "npm-telegram" && -n "${NPM_TELEGRAM_PACKAGE_SPEC// }" ]]; then
|
||||
echo "- Published-package Telegram E2E: \`${NPM_TELEGRAM_PACKAGE_SPEC}\`"
|
||||
elif [[ -n "${RELEASE_PACKAGE_SPEC// }" ]]; then
|
||||
elif [[ "$RERUN_GROUP" == "npm-telegram" && -n "${RELEASE_PACKAGE_SPEC// }" ]]; then
|
||||
echo "- Published-package Telegram E2E: \`${RELEASE_PACKAGE_SPEC}\`"
|
||||
elif [[ "$RERUN_GROUP" == "all" && "$RELEASE_PROFILE" == "full" ]]; then
|
||||
echo "- Package Telegram E2E: parent \`release-package-under-test\` artifact"
|
||||
elif [[ "$RERUN_GROUP" == "npm-telegram" ]]; then
|
||||
echo "- Package Telegram E2E: focused rerun requires \`release_package_spec\` or \`npm_telegram_package_spec\`"
|
||||
elif [[ "$RERUN_GROUP" == "all" || "$RERUN_GROUP" == "release-checks" || "$RERUN_GROUP" == "package" ]]; then
|
||||
echo "- Package Telegram E2E: OpenClaw Release Checks Package Acceptance"
|
||||
else
|
||||
echo "- Package Telegram E2E: skipped unless \`release_profile=full\`, \`release_package_spec\`, or \`npm_telegram_package_spec\` is provided"
|
||||
echo "- Package Telegram E2E: skipped by rerun group"
|
||||
fi
|
||||
if [[ -n "${EVIDENCE_PACKAGE_SPEC// }" ]]; then
|
||||
echo "- Private evidence package proof: \`${EVIDENCE_PACKAGE_SPEC}\`"
|
||||
@@ -764,83 +766,13 @@ jobs:
|
||||
|
||||
dispatch_and_wait openclaw-release-checks.yml "${args[@]}"
|
||||
|
||||
prepare_release_package:
|
||||
name: Prepare release package artifact
|
||||
needs: [resolve_target, docker_runtime_assets_preflight]
|
||||
if: ${{ always() && needs.resolve_target.result == 'success' && inputs.npm_telegram_package_spec == '' && inputs.release_package_spec == '' && inputs.rerun_group == 'all' && inputs.release_profile == 'full' && needs.docker_runtime_assets_preflight.result == 'success' }}
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 15
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
outputs:
|
||||
artifact_name: ${{ steps.artifact.outputs.name }}
|
||||
package_sha256: ${{ steps.package.outputs.sha256 }}
|
||||
package_version: ${{ steps.package.outputs.package_version }}
|
||||
source_sha: ${{ steps.package.outputs.source_sha }}
|
||||
steps:
|
||||
- name: Checkout trusted workflow ref
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
||||
with:
|
||||
persist-credentials: true
|
||||
ref: ${{ github.ref_name }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set artifact metadata
|
||||
id: artifact
|
||||
run: echo "name=release-package-under-test" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Setup Node environment
|
||||
uses: ./.github/actions/setup-node-env
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
install-bun: "true"
|
||||
install-deps: "false"
|
||||
|
||||
- name: Resolve release package artifact
|
||||
id: package
|
||||
shell: bash
|
||||
env:
|
||||
PACKAGE_REF: ${{ needs.resolve_target.outputs.sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
node scripts/resolve-openclaw-package-candidate.mjs \
|
||||
--source ref \
|
||||
--package-ref "$PACKAGE_REF" \
|
||||
--output-dir .artifacts/docker-e2e-package \
|
||||
--output-name openclaw-current.tgz \
|
||||
--metadata .artifacts/docker-e2e-package/package-candidate.json \
|
||||
--github-output "$GITHUB_OUTPUT"
|
||||
digest="$(node -p "JSON.parse(require('fs').readFileSync('.artifacts/docker-e2e-package/package-candidate.json', 'utf8')).sha256")"
|
||||
version="$(node -p "JSON.parse(require('fs').readFileSync('.artifacts/docker-e2e-package/package-candidate.json', 'utf8')).version")"
|
||||
source_sha="$(node -p "JSON.parse(require('fs').readFileSync('.artifacts/docker-e2e-package/package-candidate.json', 'utf8')).packageSourceSha")"
|
||||
echo "source_sha=$source_sha" >> "$GITHUB_OUTPUT"
|
||||
{
|
||||
echo "## Release package artifact"
|
||||
echo
|
||||
echo "- Artifact: \`release-package-under-test\`"
|
||||
echo "- Package ref: \`$PACKAGE_REF\`"
|
||||
echo "- SHA-256: \`$digest\`"
|
||||
echo "- Version: \`$version\`"
|
||||
echo "- Source SHA: \`$source_sha\`"
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Upload release package artifact
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: release-package-under-test
|
||||
path: |
|
||||
.artifacts/docker-e2e-package/openclaw-current.tgz
|
||||
.artifacts/docker-e2e-package/package-candidate.json
|
||||
if-no-files-found: error
|
||||
|
||||
npm_telegram:
|
||||
name: Run package Telegram E2E
|
||||
needs: [resolve_target, prepare_release_package]
|
||||
if: ${{ always() && contains(fromJSON('["all","npm-telegram"]'), inputs.rerun_group) && (inputs.npm_telegram_package_spec != '' || inputs.release_package_spec != '' || (inputs.rerun_group == 'all' && inputs.release_profile == 'full')) }}
|
||||
needs: [resolve_target]
|
||||
if: ${{ always() && needs.resolve_target.result == 'success' && inputs.rerun_group == 'npm-telegram' && (inputs.npm_telegram_package_spec != '' || inputs.release_package_spec != '') }}
|
||||
continue-on-error: ${{ startsWith(github.ref, 'refs/heads/tideclaw/alpha/') }}
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: ${{ inputs.release_profile == 'full' && 120 || 60 }}
|
||||
timeout-minutes: ${{ inputs.release_profile == 'full' && 360 || 60 }}
|
||||
outputs:
|
||||
run_id: ${{ steps.dispatch.outputs.run_id }}
|
||||
url: ${{ steps.dispatch.outputs.url }}
|
||||
@@ -853,8 +785,6 @@ jobs:
|
||||
CHILD_WORKFLOW_REF: ${{ github.ref_name }}
|
||||
TARGET_SHA: ${{ needs.resolve_target.outputs.sha }}
|
||||
PACKAGE_SPEC: ${{ inputs.npm_telegram_package_spec || inputs.release_package_spec }}
|
||||
PACKAGE_ARTIFACT_NAME: ${{ needs.prepare_release_package.outputs.artifact_name }}
|
||||
PREPARE_PACKAGE_RESULT: ${{ needs.prepare_release_package.result }}
|
||||
PROVIDER_MODE: ${{ inputs.npm_telegram_provider_mode }}
|
||||
SCENARIO: ${{ inputs.npm_telegram_scenario }}
|
||||
run: |
|
||||
@@ -883,18 +813,7 @@ jobs:
|
||||
return "$status"
|
||||
}
|
||||
|
||||
args=(-f package_spec="${PACKAGE_SPEC:-openclaw@beta}" -f harness_ref="$TARGET_SHA" -f provider_mode="$PROVIDER_MODE")
|
||||
if [[ -z "${PACKAGE_SPEC// }" ]]; then
|
||||
if [[ "$PREPARE_PACKAGE_RESULT" != "success" || -z "${PACKAGE_ARTIFACT_NAME// }" ]]; then
|
||||
echo "Full release Telegram requires either npm_telegram_package_spec or a prepared release-package-under-test artifact." >&2
|
||||
exit 1
|
||||
fi
|
||||
args+=(
|
||||
-f package_artifact_name="$PACKAGE_ARTIFACT_NAME"
|
||||
-f package_artifact_run_id="${GITHUB_RUN_ID}"
|
||||
-f package_label="full-release-${TARGET_SHA:0:12}"
|
||||
)
|
||||
fi
|
||||
args=(-f package_spec="$PACKAGE_SPEC" -f harness_ref="$TARGET_SHA" -f provider_mode="$PROVIDER_MODE")
|
||||
if [[ -n "${SCENARIO// }" ]]; then
|
||||
args+=(-f scenario="$SCENARIO")
|
||||
fi
|
||||
@@ -971,7 +890,7 @@ jobs:
|
||||
needs: [resolve_target, docker_runtime_assets_preflight]
|
||||
if: ${{ always() && needs.resolve_target.result == 'success' && contains(fromJSON('["all","performance"]'), inputs.rerun_group) && (inputs.rerun_group != 'all' || needs.docker_runtime_assets_preflight.result == 'success') }}
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 120
|
||||
timeout-minutes: ${{ inputs.release_profile == 'full' && 360 || 120 }}
|
||||
outputs:
|
||||
run_id: ${{ steps.dispatch.outputs.run_id }}
|
||||
url: ${{ steps.dispatch.outputs.url }}
|
||||
|
||||
@@ -717,7 +717,6 @@ jobs:
|
||||
published_upgrade_survivor_baselines: ${{ needs.resolve_target.outputs.run_release_soak == 'true' && 'last-stable-4 2026.4.23 2026.5.2 2026.4.15' || '' }}
|
||||
published_upgrade_survivor_scenarios: ${{ needs.resolve_target.outputs.run_release_soak == 'true' && 'reported-issues' || '' }}
|
||||
telegram_mode: mock-openai
|
||||
telegram_scenarios: telegram-help-command,telegram-commands-command,telegram-tools-compact-command,telegram-whoami-command,telegram-status-command,telegram-other-bot-command-gating,telegram-context-command,telegram-mentioned-message-reply,telegram-long-final-reuses-preview,telegram-mention-gating
|
||||
secrets:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
|
||||
|
||||
24
.github/workflows/openclaw-release-publish.yml
vendored
24
.github/workflows/openclaw-release-publish.yml
vendored
@@ -519,12 +519,7 @@ jobs:
|
||||
local workflow="$1"
|
||||
shift
|
||||
|
||||
local before_json dispatch_output run_id
|
||||
before_json="$(gh api -X GET "repos/${GITHUB_REPOSITORY}/actions/workflows/${workflow}/runs" \
|
||||
-F event=workflow_dispatch \
|
||||
-F per_page=100 \
|
||||
--jq '[.workflow_runs[].id]')"
|
||||
|
||||
local dispatch_output run_id
|
||||
dispatch_output="$(gh workflow run --repo "$GITHUB_REPOSITORY" "$workflow" --ref "$workflow_ref" "$@" 2>&1)"
|
||||
printf '%s\n' "$dispatch_output" >&2
|
||||
run_id="$(
|
||||
@@ -534,22 +529,7 @@ jobs:
|
||||
)"
|
||||
|
||||
if [[ -z "$run_id" ]]; then
|
||||
for _ in $(seq 1 60); do
|
||||
run_id="$(
|
||||
BEFORE_IDS="$before_json" gh api -X GET "repos/${GITHUB_REPOSITORY}/actions/workflows/${workflow}/runs" \
|
||||
-F event=workflow_dispatch \
|
||||
-F per_page=50 \
|
||||
--jq '.workflow_runs | map({databaseId:.id, createdAt:.created_at}) | map(select(.databaseId as $id | (env.BEFORE_IDS | fromjson | index($id) | not))) | sort_by(.createdAt) | reverse | .[0].databaseId // empty'
|
||||
)"
|
||||
if [[ -n "$run_id" ]]; then
|
||||
break
|
||||
fi
|
||||
sleep 5
|
||||
done
|
||||
fi
|
||||
|
||||
if [[ -z "${run_id:-}" ]]; then
|
||||
echo "Could not find dispatched run for ${workflow}." >&2
|
||||
echo "gh workflow run ${workflow} did not return an Actions run URL; refusing to guess from recent workflow_dispatch runs." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
109
.github/workflows/openclaw-stable-main-closeout.yml
vendored
109
.github/workflows/openclaw-stable-main-closeout.yml
vendored
@@ -23,8 +23,8 @@ permissions:
|
||||
contents: write
|
||||
|
||||
concurrency:
|
||||
group: openclaw-stable-main-closeout
|
||||
cancel-in-progress: false
|
||||
group: openclaw-stable-main-closeout-${{ github.event_name == 'workflow_dispatch' && (inputs.tag || github.run_id) || github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
|
||||
|
||||
jobs:
|
||||
resolve:
|
||||
@@ -43,6 +43,30 @@ jobs:
|
||||
should_closeout: ${{ steps.inputs.outputs.should_closeout }}
|
||||
tag: ${{ steps.inputs.outputs.tag }}
|
||||
steps:
|
||||
- name: Install GitHub API backoff helper
|
||||
run: |
|
||||
cat > "$RUNNER_TEMP/github-api-backoff.sh" <<'BASH'
|
||||
gh_with_retry() {
|
||||
local attempt output status lower_output
|
||||
for attempt in 1 2 3 4 5; do
|
||||
if output="$(gh "$@" 2>&1)"; then
|
||||
printf '%s\n' "$output"
|
||||
return 0
|
||||
fi
|
||||
status=$?
|
||||
lower_output="${output,,}"
|
||||
if [[ "$lower_output" != *"rate limit"* && "$output" != *"HTTP 429"* ]]; then
|
||||
printf '%s\n' "$output" >&2
|
||||
return "$status"
|
||||
fi
|
||||
echo "::warning::GitHub API throttled stable closeout on attempt ${attempt}; retrying after backoff." >&2
|
||||
sleep $((attempt * attempt * 5))
|
||||
done
|
||||
printf '%s\n' "$output" >&2
|
||||
return "$status"
|
||||
}
|
||||
BASH
|
||||
|
||||
- name: Checkout pushed main
|
||||
if: ${{ github.event_name == 'push' }}
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
||||
@@ -62,9 +86,13 @@ jobs:
|
||||
TRIGGER_SHA: ${{ github.sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [[ "$EVENT_NAME" == "push" ]]; then
|
||||
sleep 45
|
||||
fi
|
||||
. "$RUNNER_TEMP/github-api-backoff.sh"
|
||||
if [[ "$EVENT_NAME" == "push" ]]; then
|
||||
main_ref="$TRIGGER_SHA"
|
||||
tag="$(gh release list --repo "$GITHUB_REPOSITORY" --exclude-drafts --limit 100 \
|
||||
tag="$(gh_with_retry release list --repo "$GITHUB_REPOSITORY" --exclude-drafts --limit 100 \
|
||||
--json tagName,isPrerelease,publishedAt \
|
||||
--jq '[.[] | select(.isPrerelease | not) | select(.tagName | test("^v[0-9]{4}\\.[0-9]+\\.[0-9]+(-[0-9]+)?$"))] | sort_by(.publishedAt) | last | .tagName // empty')"
|
||||
if [[ -z "$tag" ]]; then
|
||||
@@ -88,8 +116,27 @@ jobs:
|
||||
if [[ "$release_package_version" =~ ^(.+)-[0-9]+$ ]]; then
|
||||
fallback_package_version="${BASH_REMATCH[1]}"
|
||||
fi
|
||||
tag_package_version="$(gh api "repos/$GITHUB_REPOSITORY/contents/package.json?ref=$tag" \
|
||||
--jq '.content' | tr -d '\n' | base64 --decode | jq -r '.version // empty')"
|
||||
tag_package_content="$RUNNER_TEMP/tag-package-content.b64"
|
||||
tag_package_read=false
|
||||
for attempt in 1 2 3; do
|
||||
if gh_with_retry api "repos/$GITHUB_REPOSITORY/contents/package.json?ref=$tag" \
|
||||
--jq '.content' > "$tag_package_content"; then
|
||||
tag_package_read=true
|
||||
break
|
||||
fi
|
||||
if [[ "$attempt" != "3" ]]; then
|
||||
sleep $((attempt * 5))
|
||||
fi
|
||||
done
|
||||
if [[ "$tag_package_read" != "true" ]]; then
|
||||
echo "Stable closeout could not read package.json for $tag from GitHub API." >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! tag_package_json="$(tr -d '\n' < "$tag_package_content" | base64 --decode)"; then
|
||||
echo "Stable closeout package.json content for $tag was not valid base64." >&2
|
||||
exit 1
|
||||
fi
|
||||
tag_package_version="$(jq -r '.version // empty' <<<"$tag_package_json")"
|
||||
fallback_correction=false
|
||||
evidence_source_tag="$tag"
|
||||
if [[ "$release_package_version" != "$fallback_package_version" &&
|
||||
@@ -107,7 +154,7 @@ jobs:
|
||||
closeout_checksum_asset="${closeout_asset}.sha256"
|
||||
closeout_dir="$RUNNER_TEMP/release-closeout-evidence"
|
||||
mkdir -p "$closeout_dir"
|
||||
gh release download "$tag" --repo "$GITHUB_REPOSITORY" \
|
||||
gh_with_retry release download "$tag" --repo "$GITHUB_REPOSITORY" \
|
||||
--pattern "$closeout_asset" --pattern "$closeout_checksum_asset" --dir "$closeout_dir" || true
|
||||
closeout_json_path="$closeout_dir/$closeout_asset"
|
||||
closeout_checksum_path="$closeout_dir/$closeout_checksum_asset"
|
||||
@@ -163,8 +210,11 @@ jobs:
|
||||
fi
|
||||
evidence_dir="$RUNNER_TEMP/release-postpublish-evidence"
|
||||
mkdir -p "$evidence_dir"
|
||||
if ! gh release download "$evidence_source_tag" --repo "$GITHUB_REPOSITORY" \
|
||||
--pattern "$evidence_asset" --pattern "$evidence_checksum_asset" --dir "$evidence_dir"; then
|
||||
gh_with_retry release download "$evidence_source_tag" --repo "$GITHUB_REPOSITORY" \
|
||||
--pattern "$evidence_asset" --pattern "$evidence_checksum_asset" --dir "$evidence_dir" || true
|
||||
evidence_path="$evidence_dir/$evidence_asset"
|
||||
evidence_checksum_path="$evidence_dir/$evidence_checksum_asset"
|
||||
if [[ ! -f "$evidence_path" || ! -f "$evidence_checksum_path" ]]; then
|
||||
if [[ "$EVENT_NAME" == "push" ]]; then
|
||||
echo "Stable closeout skipped: $evidence_source_tag predates immutable postpublish evidence." >&2
|
||||
echo "should_closeout=false" >> "$GITHUB_OUTPUT"
|
||||
@@ -173,7 +223,6 @@ jobs:
|
||||
echo "Stable closeout is required for $tag, but immutable postpublish evidence from $evidence_source_tag is missing." >&2
|
||||
exit 1
|
||||
fi
|
||||
evidence_path="$evidence_dir/$evidence_asset"
|
||||
if ! (
|
||||
cd "$evidence_dir"
|
||||
sha256sum --strict --status -c "$evidence_checksum_asset"
|
||||
@@ -253,6 +302,30 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Install GitHub API backoff helper
|
||||
run: |
|
||||
cat > "$RUNNER_TEMP/github-api-backoff.sh" <<'BASH'
|
||||
gh_with_retry() {
|
||||
local attempt output status lower_output
|
||||
for attempt in 1 2 3 4 5; do
|
||||
if output="$(gh "$@" 2>&1)"; then
|
||||
printf '%s\n' "$output"
|
||||
return 0
|
||||
fi
|
||||
status=$?
|
||||
lower_output="${output,,}"
|
||||
if [[ "$lower_output" != *"rate limit"* && "$output" != *"HTTP 429"* ]]; then
|
||||
printf '%s\n' "$output" >&2
|
||||
return "$status"
|
||||
fi
|
||||
echo "::warning::GitHub API throttled stable closeout on attempt ${attempt}; retrying after backoff." >&2
|
||||
sleep $((attempt * attempt * 5))
|
||||
done
|
||||
printf '%s\n' "$output" >&2
|
||||
return "$status"
|
||||
}
|
||||
BASH
|
||||
|
||||
- name: Verify release workflow evidence
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
@@ -260,7 +333,8 @@ jobs:
|
||||
RELEASE_PUBLISH_RUN_ID: ${{ needs.resolve.outputs.release_publish_run_id }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
gh run view "$FULL_RELEASE_VALIDATION_RUN_ID" --repo "$GITHUB_REPOSITORY" \
|
||||
. "$RUNNER_TEMP/github-api-backoff.sh"
|
||||
gh_with_retry run view "$FULL_RELEASE_VALIDATION_RUN_ID" --repo "$GITHUB_REPOSITORY" \
|
||||
--json workflowName,event,status,conclusion \
|
||||
> "$RUNNER_TEMP/full-release-validation-run.json"
|
||||
node --input-type=module - "$RUNNER_TEMP/full-release-validation-run.json" <<'NODE'
|
||||
@@ -277,7 +351,7 @@ jobs:
|
||||
}
|
||||
}
|
||||
NODE
|
||||
gh run view "$RELEASE_PUBLISH_RUN_ID" --repo "$GITHUB_REPOSITORY" \
|
||||
gh_with_retry run view "$RELEASE_PUBLISH_RUN_ID" --repo "$GITHUB_REPOSITORY" \
|
||||
--json workflowName,event,status,conclusion \
|
||||
> "$RUNNER_TEMP/release-publish-run.json"
|
||||
node --input-type=module - "$RUNNER_TEMP/release-publish-run.json" <<'NODE'
|
||||
@@ -298,7 +372,7 @@ jobs:
|
||||
manifest_dir="$RUNNER_TEMP/full-release-validation-manifest"
|
||||
rm -rf "$manifest_dir"
|
||||
mkdir -p "$manifest_dir"
|
||||
gh run download "$FULL_RELEASE_VALIDATION_RUN_ID" --repo "$GITHUB_REPOSITORY" \
|
||||
gh_with_retry run download "$FULL_RELEASE_VALIDATION_RUN_ID" --repo "$GITHUB_REPOSITORY" \
|
||||
--name "full-release-validation-${FULL_RELEASE_VALIDATION_RUN_ID}" \
|
||||
--dir "$manifest_dir"
|
||||
tag_sha="$(git -C "$GITHUB_WORKSPACE/release-tag" rev-parse HEAD)"
|
||||
@@ -327,7 +401,8 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p "$CLOSEOUT_DIR"
|
||||
gh release view "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" \
|
||||
. "$RUNNER_TEMP/github-api-backoff.sh"
|
||||
gh_with_retry release view "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" \
|
||||
--json tagName,isDraft,isPrerelease,assets \
|
||||
> "$CLOSEOUT_DIR/github-release.json"
|
||||
node scripts/verify-stable-main-closeout.mjs \
|
||||
@@ -353,21 +428,23 @@ jobs:
|
||||
CLOSEOUT_DIR: ${{ runner.temp }}/openclaw-stable-main-closeout
|
||||
run: |
|
||||
set -euo pipefail
|
||||
. "$RUNNER_TEMP/github-api-backoff.sh"
|
||||
release_version="${RELEASE_TAG#v}"
|
||||
attach_or_verify() {
|
||||
local source_path="$1"
|
||||
local asset_name="$2"
|
||||
local existing_dir="$CLOSEOUT_DIR/existing-${asset_name}"
|
||||
mkdir -p "$existing_dir"
|
||||
if gh release download "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" \
|
||||
--pattern "$asset_name" --dir "$existing_dir"; then
|
||||
gh_with_retry release download "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" \
|
||||
--pattern "$asset_name" --dir "$existing_dir" || true
|
||||
if [[ -f "$existing_dir/$asset_name" ]]; then
|
||||
cmp --silent "$source_path" "$existing_dir/$asset_name" || {
|
||||
echo "Existing release asset $asset_name differs from closeout evidence." >&2
|
||||
exit 1
|
||||
}
|
||||
return
|
||||
fi
|
||||
gh release upload "$RELEASE_TAG" "$source_path#$asset_name" --repo "$GITHUB_REPOSITORY"
|
||||
gh_with_retry release upload "$RELEASE_TAG" "$source_path#$asset_name" --repo "$GITHUB_REPOSITORY"
|
||||
}
|
||||
attach_or_verify \
|
||||
"$CLOSEOUT_DIR/stable-main-closeout.json" \
|
||||
|
||||
4
.github/workflows/plugin-npm-release.yml
vendored
4
.github/workflows/plugin-npm-release.yml
vendored
@@ -38,8 +38,8 @@ on:
|
||||
type: string
|
||||
|
||||
concurrency:
|
||||
group: plugin-npm-release-${{ github.event_name == 'workflow_dispatch' && inputs.ref || github.sha }}
|
||||
cancel-in-progress: false
|
||||
group: plugin-npm-release-${{ github.event_name == 'workflow_dispatch' && inputs.ref || github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||
|
||||
2
.github/workflows/sandbox-common-smoke.yml
vendored
2
.github/workflows/sandbox-common-smoke.yml
vendored
@@ -19,7 +19,7 @@ permissions:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' || (github.event_name == 'push' && github.ref == 'refs/heads/main') }}
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||
|
||||
69
.github/workflows/windows-blacksmith-testbox.yml
vendored
69
.github/workflows/windows-blacksmith-testbox.yml
vendored
@@ -57,6 +57,10 @@ jobs:
|
||||
echo "could not read required Blacksmith metadata" >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! jq -e 'type == "number"' <<<"$installation_model_id" >/dev/null; then
|
||||
echo "invalid Blacksmith installation model id: ${installation_model_id}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -n "${BLACKSMITH_HOSTNAME:-}" ]; then
|
||||
runner_host="$BLACKSMITH_HOSTNAME"
|
||||
@@ -65,21 +69,32 @@ jobs:
|
||||
fi
|
||||
runner_ssh_port="${BLACKSMITH_SSH_PORT:-22}"
|
||||
|
||||
hydrating_body="$RUNNER_TEMP/testbox-hydrating.json"
|
||||
hydrating_response="$RUNNER_TEMP/testbox-hydrating.response"
|
||||
jq -n \
|
||||
--arg testbox_id "$TESTBOX_ID" \
|
||||
--argjson installation_model_id "$installation_model_id" \
|
||||
--arg status "hydrating" \
|
||||
--arg ip_address "$runner_host" \
|
||||
--arg ssh_port "$runner_ssh_port" \
|
||||
--arg working_directory "$GITHUB_WORKSPACE" \
|
||||
--arg adopted_run_id "$GITHUB_RUN_ID" \
|
||||
'{
|
||||
testbox_id: $testbox_id,
|
||||
installation_model_id: $installation_model_id,
|
||||
status: $status,
|
||||
ip_address: $ip_address,
|
||||
ssh_port: $ssh_port,
|
||||
working_directory: $working_directory,
|
||||
adopted_run_id: $adopted_run_id,
|
||||
metadata: {}
|
||||
}' > "$hydrating_body"
|
||||
|
||||
hydrating_http_code="$(curl -sS -L --post302 --post303 -o "$hydrating_response" -w '%{http_code}' \
|
||||
-X POST "${api_url}/api/testbox/phone-home" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer ${auth_token}" \
|
||||
-d "{
|
||||
\"testbox_id\": \"${TESTBOX_ID}\",
|
||||
\"installation_model_id\": ${installation_model_id},
|
||||
\"status\": \"hydrating\",
|
||||
\"ip_address\": \"${runner_host}\",
|
||||
\"ssh_port\": \"${runner_ssh_port}\",
|
||||
\"working_directory\": \"${GITHUB_WORKSPACE}\",
|
||||
\"adopted_run_id\": \"${GITHUB_RUN_ID}\",
|
||||
\"metadata\": {}
|
||||
}" || true)"
|
||||
--data-binary @"$hydrating_body" || true)"
|
||||
|
||||
echo "phone_home_hydrating_http=${hydrating_http_code}"
|
||||
if [[ ! "$hydrating_http_code" =~ ^2 ]]; then
|
||||
@@ -152,20 +167,30 @@ jobs:
|
||||
runner_ssh_port="$(cat "$state/runner_ssh_port")"
|
||||
working_directory="$(cat "$state/working_directory")"
|
||||
adopted_run_id="$(cat "$state/adopted_run_id")"
|
||||
if ! jq -e 'type == "number"' <<<"$installation_model_id" >/dev/null; then
|
||||
echo "invalid Blacksmith installation model id: ${installation_model_id}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ready_body="$RUNNER_TEMP/testbox-ready.json"
|
||||
cat > "$ready_body" <<JSON
|
||||
{
|
||||
"testbox_id": "${testbox_id}",
|
||||
"installation_model_id": ${installation_model_id},
|
||||
"status": "ready",
|
||||
"ip_address": "${runner_host}",
|
||||
"ssh_port": "${runner_ssh_port}",
|
||||
"working_directory": "${working_directory}",
|
||||
"adopted_run_id": "${adopted_run_id}",
|
||||
"metadata": {}
|
||||
}
|
||||
JSON
|
||||
jq -n \
|
||||
--arg testbox_id "$testbox_id" \
|
||||
--argjson installation_model_id "$installation_model_id" \
|
||||
--arg status "ready" \
|
||||
--arg ip_address "$runner_host" \
|
||||
--arg ssh_port "$runner_ssh_port" \
|
||||
--arg working_directory "$working_directory" \
|
||||
--arg adopted_run_id "$adopted_run_id" \
|
||||
'{
|
||||
testbox_id: $testbox_id,
|
||||
installation_model_id: $installation_model_id,
|
||||
status: $status,
|
||||
ip_address: $ip_address,
|
||||
ssh_port: $ssh_port,
|
||||
working_directory: $working_directory,
|
||||
adopted_run_id: $adopted_run_id,
|
||||
metadata: {}
|
||||
}' > "$ready_body"
|
||||
|
||||
http_code="$(curl -sS -L --post302 --post303 -o "$RUNNER_TEMP/testbox-ready.response" -w '%{http_code}' \
|
||||
-X POST "${api_url}/api/testbox/phone-home" \
|
||||
|
||||
25
.github/workflows/workflow-sanity.yml
vendored
25
.github/workflows/workflow-sanity.yml
vendored
@@ -129,11 +129,28 @@ jobs:
|
||||
trusted_config="$RUNNER_TEMP/pre-commit-base.yaml"
|
||||
trusted_zizmor_config="$RUNNER_TEMP/zizmor-base.yml"
|
||||
|
||||
if ! git cat-file -e "${BASE_SHA}^{commit}" 2>/dev/null; then
|
||||
timeout --signal=TERM --kill-after=10s 30s git fetch --no-tags --depth=1 origin \
|
||||
"+${BASE_SHA}:refs/remotes/origin/security-base" ||
|
||||
fetch_base_ref() {
|
||||
local ref="$1"
|
||||
local target="$2"
|
||||
local fetch_status
|
||||
for attempt in 1 2 3; do
|
||||
timeout --signal=TERM --kill-after=10s 30s git fetch --no-tags --depth=1 origin \
|
||||
"+refs/heads/${BASE_REF}:refs/remotes/origin/${BASE_REF}"
|
||||
"+${ref}:${target}" && return 0
|
||||
fetch_status="$?"
|
||||
if [ "$fetch_status" != "124" ] && [ "$fetch_status" != "137" ]; then
|
||||
return "$fetch_status"
|
||||
fi
|
||||
if [ "$attempt" = "3" ]; then
|
||||
return "$fetch_status"
|
||||
fi
|
||||
echo "::warning::trusted base fetch for '$ref' timed out on attempt $attempt; retrying"
|
||||
sleep 5
|
||||
done
|
||||
}
|
||||
|
||||
if ! git cat-file -e "${BASE_SHA}^{commit}" 2>/dev/null; then
|
||||
fetch_base_ref "$BASE_SHA" "refs/remotes/origin/security-base" ||
|
||||
fetch_base_ref "refs/heads/${BASE_REF}" "refs/remotes/origin/${BASE_REF}"
|
||||
fi
|
||||
|
||||
if git cat-file -e "${BASE_SHA}:.pre-commit-config.yaml" 2>/dev/null; then
|
||||
|
||||
@@ -35,7 +35,7 @@ Skills own workflows; root owns hard policy and routing.
|
||||
- One-sided fixes need sibling-surface proof, an explanation for why siblings are unaffected, or explicit follow-up work.
|
||||
- Changelog findings: see Docs / Changelog.
|
||||
- Public ClawSweeper comments prefer `https://docs.openclaw.ai/...` when a public docs page exists; structured evidence still cites repo files, lines, SHAs.
|
||||
- Findings need current source, shipped/current behavior, tests/CI evidence, and dependency contract proof when dependency-backed behavior is involved. Validation is judged against touched and sibling surfaces plus this file's commands; real behavior proof matters for user-visible changes, with Telegram/Desktop proof for Telegram-visible behavior when feasible.
|
||||
- Findings need current source, shipped/current behavior, tests/CI evidence, and dependency contract proof when dependency-backed behavior is involved. Validation is judged against touched and sibling surfaces plus this file's commands; clear evidence matters for user-visible changes, with Telegram/Desktop proof for Telegram-visible behavior when feasible.
|
||||
- Prefer findings for concrete behavior regressions, missing changed-surface proof, owner-boundary violations, security/API contract issues, or docs/config mismatches.
|
||||
- Do not file findings for repo policy preference when changed code follows the relevant scoped guide and no user-visible, runtime, security, or maintainer-risk impact is shown.
|
||||
|
||||
@@ -165,13 +165,12 @@ Skills own workflows; root owns hard policy and routing.
|
||||
- Representing user: if user already has a comment/thread for the point, update/reply there when possible; avoid duplicate PR/issue comments.
|
||||
- No surprise GH writes: chat must mention every posted/updated public comment with URL.
|
||||
- GH comments with backticks, `$`, or shell snippets: use heredoc/body file, not inline double-quoted `--body`.
|
||||
- PR create: real body required. Include Summary + Verification; mention refs, behavior, and proof.
|
||||
- PR create: real body required. Use the current template: `What Problem This Solves`, `Why This Change Was Made`, `User Impact`, and `Evidence`; include visible refs, behavior, and validation.
|
||||
- PR create/refresh: keep PR branches takeover-ready. Use a branch maintainers can push to, or for fork PRs ensure `maintainer_can_modify` / GitHub's `Allow edits by maintainers` is enabled unless explicitly told otherwise or GitHub's Actions/secrets warning makes that unsafe.
|
||||
- GitHub issue/PR create: read `$agent-transcript`; ask about sanitized transcript logs when available.
|
||||
- Contributor PRs: parsed `Real behavior proof` uses exact `field: value` labels: `Behavior addressed`, `Real environment tested`, `Exact steps or command run after this patch`, `Evidence after fix`, `Observed result after fix`, `What was not tested`.
|
||||
- Contributor PRs: parsed context requires authored `What Problem This Solves` and `Evidence` sections. Do not require field-level proof forms; reviewers inspect code, tests, and CI for correctness.
|
||||
- PR artifacts/screenshots: attach to PR/comment/external artifact store. Never push screenshots, videos, proof images, or proof assets to OpenClaw or any product repo branch, including temp artifact branches. Use Crabbox artifact publishing plus the manifest URL. Do not commit `.github/pr-assets`.
|
||||
- CI polling: exact SHA, relevant checks only, minimal fields. Skip routine noise (`Auto response`, `Labeler`, docs agents, performance/stale). Logs only after failure/completion or concrete need.
|
||||
- OpenClaw write-access maintainers may skip `Real behavior proof` when local tests or Crabbox verified behavior; record proof in PR verification.
|
||||
- Agent PR landing to `main`: use only the repo-native `scripts/pr` wrapper: run `scripts/pr review-init <PR>`, follow its emitted checkout/guard guidance, initialize and complete review artifacts with `scripts/pr review-artifacts-init <PR>`, validate them with `scripts/pr review-validate-artifacts <PR>`, then run `scripts/pr prepare-run <PR>` and `scripts/pr merge-run <PR>`; do not idle on `auto-response` or `check-docs`.
|
||||
|
||||
## Code
|
||||
|
||||
68
CHANGELOG.md
68
CHANGELOG.md
@@ -6,34 +6,34 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Highlights
|
||||
|
||||
- **Richer Telegram delivery:** Telegram now sends rich HTML, preserves rich markdown and sticker paths, renders progress drafts and command output more faithfully, and keeps mentions and spooled handlers on the right delivery path. (#93286, #93164, #93124, #93364, #93130, #93088, #93281) Thanks @obviyus, @vincentkoc, @goutamadwant, @kesslerio, @NianJiuZst, @SweetSophia, @Marvinthebored, and @aaajiao.
|
||||
- **Richer Telegram delivery:** Telegram now sends rich HTML, preserves rich markdown and sticker paths, renders progress drafts and command output more faithfully, normalizes HTML tables safely, and keeps mentions and spooled handlers on the right delivery path. (#93286, #93164, #93124, #93364, #93130, #93002, #93088, #93281, #94891, #94856) Thanks @obviyus, @vincentkoc, @goutamadwant, @kesslerio, @NianJiuZst, @SweetSophia, @Marvinthebored, @aaajiao, @zhangguiping-xydt, @zhangqueping, and @jairrab.
|
||||
- **More dependable agent recovery:** retries, terminal outcomes, usage after compaction, session history repair, and reply reconciliation now keep more interrupted or partial turns moving toward a visible final result. (#92191, #93073, #93228, #93084, #93469, #93291, #90943) Thanks @ai-hpc, @lml2468, @fuller-stack-dev, @Hollychou924, @leno23, @de1tydev, @425072024, @wuwahe3, @drvoss, @yetval, @sandieman2, and @vincentkoc.
|
||||
- **A stronger Codex integration:** Codex gains automatic plugin approvals, GPT-5.3 Spark OAuth routing, remote-node `exec` as a dynamic tool, and more reliable app-server teardown and terminal outcomes. (#92625, #89133, #93654, #91767, #93287) Thanks @kevinslin, @VACInc, @vincentkoc, @JPKay-AI, and @aliahnaf2013-max.
|
||||
- **Standalone official provider plugins:** external provider packages are now first-class npm releases, externally installed channel plugins load at Gateway startup, and StepFun is intentionally npm-only because its ClawHub package name is unavailable. (#93470) Thanks @sunlit-deng, @cxdnicole, and @vincentkoc.
|
||||
- **Standalone official provider plugins:** external provider packages are now first-class npm releases, externally installed channel plugins load at Gateway startup, and StepFun is available from npm and ClawHub. (#93470) Thanks @sunlit-deng, @cxdnicole, and @vincentkoc.
|
||||
- **More capable web and native clients:** the Control UI adds a session workspace rail and extension health, iOS adds Watch controls, and Android shows chat context. (#92856, #91952, #93387, #92837) Thanks @Solvely-Colin, @jalehman, @joshavant, and @Tosko4.
|
||||
- **More useful search and skills:** Codex Hosted Search is available, key-free search providers remain deliberate opt-ins, and ClawHub skill installs retain verified source provenance. (#93446, #93616, #93283, #93506) Thanks @fuller-stack-dev, @davemorin, @momothemage, @nmccready-tars, and @vincentkoc.
|
||||
|
||||
### Changes
|
||||
|
||||
- Providers and auth: add Codex Hosted Search, improve Gemini CLI OAuth behind proxies, and keep external provider onboarding on current choices and package metadata. (#93446, #92815) Thanks @fuller-stack-dev, @yetval, @EvetteYoung, and @vincentkoc.
|
||||
- Plugins and installs: externalized official providers publish as independent npm packages, Gateway discovers installed channel plugins at startup, and StepFun installs exclusively from npm. (#93470) Thanks @sunlit-deng, @cxdnicole, and @vincentkoc.
|
||||
- Plugins and installs: externalized official providers publish as independent npm packages, Gateway discovers installed channel plugins at startup, and StepFun installs from npm or ClawHub. (#93470) Thanks @sunlit-deng, @cxdnicole, and @vincentkoc.
|
||||
- Dashboard and mobile: add a session workspace rail, plugin health in status, compact cron lists, and iOS Watch controls. (#92856, #91952, #93395, #93387) Thanks @Solvely-Colin, @jalehman, @yu-xin-c, @centralpc, @joshavant, and @vincentkoc.
|
||||
- Codex and skills: add automatic plugin approvals, preserve ClawHub skill provenance, and expose remote-node execution to Codex when a node is connected. (#92625, #93283, #93654) Thanks @kevinslin, @momothemage, @nmccready-tars, @vincentkoc, and @JPKay-AI.
|
||||
- QA and release engineering: QA scenarios now use YAML, with broader profile evidence and release coverage for the plugin and channel matrix.
|
||||
- Codex, observability, and skills: add automatic plugin approvals and SecretRefs, preserve ClawHub skill provenance, add OpenTelemetry log export, and expose remote-node execution to Codex when a node is connected. (#92625, #94324, #93283, #94561, #93654) Thanks @kevinslin, @kevinlin-openai, @momothemage, @nmccready-tars, @jesse-merhi, @vincentkoc, and @JPKay-AI.
|
||||
- QA and release engineering: QA scenarios now use YAML, with broader profile evidence and release coverage for the plugin and channel matrix. Thanks @vincentkoc.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Security and privacy: redact secrets from debug/config output, block internal HTTP session overrides, audit open-DM tool exposure, and retain plugin write ownership checks. (#93333, #88496, #93443, #92883, #93353) Thanks @Alix-007, @jason-allen-oneal, @coygeek, @RichardCao, @yu-xin-c, @cjg20ss, @eleqtrizit, and @vincentkoc.
|
||||
- Agent and session runtime: retry thinking-only and empty post-tool turns, prevent duplicate hook execution, preserve fresh usage through compaction, and repair partial JSON/history artifacts. (#92191, #93073, #93009, #93084, #93469) Thanks @ai-hpc, @lml2468, @fuller-stack-dev, @zenglingbiao, @dertbv, @Hollychou924, @leno23, @de1tydev, @425072024, @wuwahe3, @drvoss, and @vincentkoc.
|
||||
- Channels and replies: fix Telegram rich delivery and ingress recovery, preserve WhatsApp auth and media error reporting, keep Mattermost thread replies intact, and harden Discord action handling. (#93286, #93364, #93281, #93076, #93334, #93424, #93488) Thanks @obviyus, @NianJiuZst, @mcaxtr, @rushindrasinha, @amknight, @lzyyzznl, @darealgege, and @vincentkoc.
|
||||
- Agent and session runtime: retry thinking-only and empty post-tool turns, prevent duplicate hook execution, preserve pending subagent delivery, preserve fresh usage through compaction, and repair partial JSON/history artifacts. (#92191, #93073, #93009, #93084, #93469, #94349, #92383, #94257) Thanks @ai-hpc, @lml2468, @fuller-stack-dev, @zenglingbiao, @dertbv, @Hollychou924, @leno23, @de1tydev, @425072024, @wuwahe3, @drvoss, @vincentkoc, @sallyom, @oiGaDio, @Hidetsugu55, and @Nas01010101.
|
||||
- Channels and replies: fix Telegram rich delivery, table rendering, action-error handling, progress draft cleanup before visible tool output, and ingress recovery; preserve command progress detail across channel adapters; retain WhatsApp opening text after a media failure; keep Mattermost thread replies intact; and harden Discord action handling. (#93286, #93364, #93281, #93002, #93076, #93334, #93424, #93488, #94868, #94891, #94856, #94810, #93823) Thanks @obviyus, @NianJiuZst, @mcaxtr, @zhangguiping-xydt, @rushindrasinha, @amknight, @lzyyzznl, @darealgege, @vincentkoc, @zhangqueping, @jairrab, @ZOOWH, @parveshsaini, and @yetval.
|
||||
- Storage and migrations: avoid SQLite WAL on network filesystems, clean reindex artifacts, keep setup state out of workspace dot-directories, and import default-agent auth profiles into SQLite. (#93454, #92891, #93182, #93295, #93520, #93156) Thanks @vincentkoc, @ZengWen-DT, @Zeng-wen, @potterdigital, @Alix-007, @Pick-cat, @sallyom, @1qh, and @Tazio7.
|
||||
- Provider and model behavior: fix Gemini CLI proxy OAuth, restore Codex Spark OAuth routing, correct Bedrock embedding model IDs, and preserve configured defaults in embedded runs. (#92815, #89133, #93452, #93428) Thanks @yetval, @EvetteYoung, @VACInc, @LiuwqGit, @aleck31, @zenglingbiao, @danielgerlag, and @vincentkoc.
|
||||
- CLI, TUI, and apps: accept global flags after subcommands, keep terminal output and activity indicators visible, preserve CJK IME composition, and refresh stale UI state. (#93455, #93460, #93006, #93427, #93498, #93606) Thanks @ooiuuii, @Alix-007, @ZengWen-DT, @Zeng-wen, @AlethiaQuizForge, @Zhaoqj2016, @liuhao1024, @BrianClaw1955, @vincentkoc, and @NicoBoom13.
|
||||
- Operations and updates: harden official plugin recovery, restart managed Gateways after failed update handoff, avoid Node-specific npm prefixes, and keep package validation paths reliable. (#93325, #92111, #93650) Thanks @vincentkoc, @yetval, @ofan, and @yaanfpv.
|
||||
- Operations and updates: harden official plugin recovery, restart managed Gateways after failed update handoff, keep safe cron delivery defaults, avoid Node-specific npm prefixes, and keep package validation paths reliable. (#93325, #92111, #93650, #94453, #91685) Thanks @vincentkoc, @yetval, @ofan, @yaanfpv, @jincheng-xydt, @sallyom, @davectr, and @nxmxbbd.
|
||||
|
||||
### Complete contribution record
|
||||
|
||||
This audited record covers the complete v2026.6.8..HEAD~1 history: 375 merged PRs. The generation manifest also supplies direct commits as editorial input; the grouped notes above prioritize user impact.
|
||||
This audited record covers the complete v2026.6.8..HEAD history: 423 merged PRs. The generation manifest also supplies direct commits as editorial input; the grouped notes above prioritize user impact.
|
||||
|
||||
#### Pull requests
|
||||
|
||||
@@ -57,6 +57,7 @@ This audited record covers the complete v2026.6.8..HEAD~1 history: 375 merged PR
|
||||
- **PR #88792** fix(state): harden sqlite path caching. Thanks @vincentkoc.
|
||||
- **PR #93022** fix(gateway): repair usage cost aggregation across agents. Thanks @luke-skywalker-open-claw and @stablegenius49.
|
||||
- **PR #93020** fix(telegram): cool down transient sendChatAction failures. Related #56096. Thanks @Boulea7 and @sumaiazaman and @Pick-cat and @cal-rufus.
|
||||
- **PR #93002** fix(telegram): clear progress drafts before visible tool output. Thanks @zhangguiping-xydt.
|
||||
- **PR #89160** fix(agents): detect truncated API responses to prevent silent session hang. Related #89051. Thanks @joelnishanth and @ArthurusDent.
|
||||
- **PR #93009** fix(agents): make wrapToolWithBeforeToolCallHook idempotent to prevent double hook execution (fixes #92973). Thanks @zenglingbiao and @dertbv.
|
||||
- **PR #92991** fix(agents): tolerate missing attribution baseUrl. Related #92974. Thanks @samrusani and @Haderach-Ram.
|
||||
@@ -175,7 +176,7 @@ This audited record covers the complete v2026.6.8..HEAD~1 history: 375 merged PR
|
||||
- **PR #90003** feat(policy): cover exec approvals artifact. Thanks @giodl73-repo.
|
||||
- **PR #93448** fix(guards): allow auth profile sqlite reader. Thanks @amknight.
|
||||
- **PR #93424** fix(mattermost): keep message tool replies in threads. Thanks @amknight and @vincentkoc.
|
||||
- **PR #93418** fix(telegram): forward Bot API 10.1 rich_message content to agent. Related #93410. Thanks @xzh-xydt and @vincentkoc and @0pen7ech.
|
||||
- **PR #93418** fix(telegram): forward Bot API 10.1 rich_message content to agent. Related #93410. Thanks @xzh-icenter and @vincentkoc and @0pen7ech.
|
||||
- **PR #93175** test(qa): taxonomy profiles: includeAllCategories for release profile, update some coverage. Thanks @RomneyDa.
|
||||
- **PR #93456** fix(agents): handle string assistant message content. Thanks @vincentkoc.
|
||||
- **PR #93441** fix(outbound): ignore schema-padded poll metadata on send. Related #43015. Thanks @weichengdeng and @charzhou.
|
||||
@@ -412,6 +413,53 @@ This audited record covers the complete v2026.6.8..HEAD~1 history: 375 merged PR
|
||||
- **PR #94658** test(sqlite): use shared temp directory helper. Thanks @vincentkoc.
|
||||
- **PR #92135** fix(openai-embedding): preserve openai/ prefix for non-native base URLs. Related #92124. Thanks @xialonglee and @Kambrian.
|
||||
- **PR #93737** refactor: add session maintenance transaction seam. Thanks @jalehman.
|
||||
- **PR #93685** refactor(auto-reply): add lifecycle storage seams. Thanks @jalehman.
|
||||
- **PR #94349** fix(agents): preserve pending subagent completion announces. Related #93323. Thanks @sallyom and @oiGaDio.
|
||||
- **PR #93174** test: fold channel message flows into qa e2e. Thanks @RomneyDa.
|
||||
- **PR #94093** Prevent Codex thread rotation from losing next-step context. Thanks @VACInc.
|
||||
- **PR #53920** fix(scripts): avoid mutating tracked auth-monitor template during setup. Thanks @JackWuGlobal.
|
||||
- **PR #94702** Standardize QA coverage IDs on dotted names. Thanks @RomneyDa.
|
||||
- **PR #81825** fix(skills/1password): stop forcing tmux for desktop app auth (#52540). Thanks @koshaji and @tylerbittner.
|
||||
- **PR #94725** fix(doctor): warn on volatile SQLite state. Thanks @vincentkoc.
|
||||
- **PR #88551** fix(agents): skip auth gate for CLI-owned transport. Thanks @yu-xin-c.
|
||||
- **PR #88581** feat(commands): add /name to rename the current session from chat. Thanks @BSG2000.
|
||||
- **PR #94324** feat(codex): support app-server SecretRefs. Thanks @kevinlin-openai and @kevinslin.
|
||||
- **PR #90882** fix: add self-knowledge docs rule to system prompt. Related #90713. Thanks @SutraHsing.
|
||||
- **PR #94684** fix: #80507 show dry-run output for message send/poll. Thanks @lzyyzznl and @YB0y.
|
||||
- **PR #93823** fix(whatsapp): keep opening text chunk when first media fails on multi-chunk reply. Thanks @yetval.
|
||||
- **PR #89203** refactor: route SDK session compatibility through seam. Thanks @jalehman.
|
||||
- **PR #94453** fix: default cron runMode to "due" instead of "force" (#94270). Thanks @jincheng-xydt and @sallyom and @davectr.
|
||||
- **PR #94746** fix(note): prevent clack from re-breaking copy-sensitive tokens. Related #94730. Thanks @xzh-icenter and @berkgungor.
|
||||
- **PR #89904** refactor: route sdk session compatibility through accessor. Thanks @jalehman.
|
||||
- **PR #86719** fix(skills): retarget stale plugin skill symlinks. Related #85925. Thanks @stevenepalmer and @shakkernerd.
|
||||
- **PR #94337** fix(tui): show 0 not ? for fresh-session context tokens in footer. Thanks @mushuiyu886.
|
||||
- **PR #94539** fix(android): group settings by intent. Thanks @Tosko4.
|
||||
- **PR #92383** fix(gateway): never return an empty chat.history transcript. Thanks @Hidetsugu55.
|
||||
- **PR #92574** test(browser): cover action-input CLI request bodies. Related #83877. Thanks @yu-xin-c and @davinci282828.
|
||||
- **PR #92873** test(diffs): add viewerState, toolbar toggle, shadow root, and hydrateProps tests (fixes #83915). Thanks @liuhao1024 and @davinci282828.
|
||||
- **PR #94257** fix(sessions): preserve Media\* index alignment when reading user-turn fields. Thanks @Nas01010101.
|
||||
- **PR #94756** fix(codex): bound turn/start text when context budget is non-positive. Related #94748. Thanks @Nas01010101.
|
||||
- **PR #94729** fix(skills/trello): add curl to requires.bins to match body examples (fixes #94727). Thanks @liuhao1024 and @berkgungor.
|
||||
- **PR #94790** feat(slack): log INFO receipt for inbound app_mention events. Related #94691. Thanks @ZengWen-DT and @BryceMurray.
|
||||
- **PR #81696** fix: guard tool event callbacks (AI-assisted). Thanks @enjoylife1243.
|
||||
- **PR #94809** chore: forward-port alpha release fixes.
|
||||
- **PR #94612** fix(macos): open NSOpenPanel for embedded Control UI file inputs (#94468). Thanks @bbblending and @DINGDANGMAOUP.
|
||||
- **PR #89806** fix(feishu): avoid axios interceptor internals. Related #83913. Thanks @sweetcornna and @davinci282828.
|
||||
- **PR #91923** fix(ios): clean up notification settings state. Thanks @zats.
|
||||
- **PR #91345** fix: suggest close CLI commands. Related #83999. Thanks @glenn-agent and @HannesOberreiter.
|
||||
- **PR #94561** Add stdout diagnostics OTEL log exporter. Thanks @jesse-merhi.
|
||||
- **PR #91013** fix(gateway): ignore stale abort markers for fresh chat events. Related #91012. Thanks @nxmxbbd.
|
||||
- **PR #89279** fix(tasks): deliver ACP completions to bound Discord threads. Related #84022. Thanks @anyech and @h-mascot.
|
||||
- **PR #91656** test(cron): expand parseAbsoluteTimeMs test coverage to 39 cases. Related #91654. Thanks @SpecialLeon.
|
||||
- **PR #94810** fix(telegram): classify sendChatAction 401 by structured error_code, not bare substring match. Related #94787. Thanks @ZOOWH and @parveshsaini.
|
||||
- **PR #94737** fix(reply): clarify provider internal error copy. Thanks @snowzlmbot.
|
||||
- **PR #94868** fix(channels): preserve command progress detail. Thanks @vincentkoc.
|
||||
- **PR #94891** fix(telegram): send progress previews as html text. Thanks @obviyus.
|
||||
- **PR #94683** fix(outbound): keep direct-only targets out of group sessions. Related #92384. Thanks @scotthuang and @haiwei01.
|
||||
- **PR #92477** fix: migrate watch app to single-target app (Xcode 27+ compat). Thanks @zats and @joshavant.
|
||||
- **PR #94812** test(perf): compare saved CLI startup benchmarks. Thanks @FelixIsaac.
|
||||
- **PR #94856** fix(telegram): normalize all HTML tables before entity-escaping in rich messages. Related #94317. Thanks @zhangqueping and @jairrab.
|
||||
- **PR #91685** fix(cron): refuse keyless implicit isolated cron delivery inherited from shared agent-main bucket. Thanks @nxmxbbd.
|
||||
|
||||
## 2026.6.8
|
||||
|
||||
|
||||
@@ -106,7 +106,8 @@ For coordinated change sets that genuinely need more than 20 PRs, join the **#cl
|
||||
## Before You PR
|
||||
|
||||
- Test locally with your OpenClaw instance
|
||||
- External PRs must include a filled **Real behavior proof** section in the PR body. Show the real setup you tested, the exact command or steps you ran after the patch, after-fix evidence, the observed result, and anything you did not test. Screenshots, recordings, terminal screenshots, console output, copied live output, linked artifacts, and redacted runtime logs all count. Unit tests, mocks, snapshots, lint, typechecks, and CI are useful but do not satisfy this requirement by themselves. Maintainers may apply `proof: override` only when the proof gate should not apply.
|
||||
- External PRs must describe the user, product, or operational problem in **What Problem This Solves** and include useful validation in **Evidence**. Focused tests, CI results, screenshots, recordings, terminal output, live observations, redacted logs, and artifact links all count. Reviewers will inspect the code, tests, and CI; use the PR body to explain intent and make validation easy to understand.
|
||||
- When ClawSweeper, Codex, Barnacle, or a maintainer asks for more context or evidence, edit the PR description instead of only replying in a new comment. Keep **What Problem This Solves**, **Why This Change Was Made**, **User Impact**, and **Evidence** current; a short comment can point reviewers to the update, but the PR body should remain the durable explanation for maintainers and bots.
|
||||
- Keep PRs takeover-ready: open them from a branch maintainers can push to. For fork PRs, leave GitHub's **Allow edits by maintainers** option enabled so maintainers can finish urgent fixes, changelog entries, or merge prep when needed. If GitHub shows **Allow edits and access to secrets by maintainers**, enable it only when that workflow/secrets access is acceptable and say so in the PR.
|
||||
- Do not edit `CHANGELOG.md` in contributor PRs. Maintainers or ClawSweeper add the changelog entry when landing user-facing changes.
|
||||
- Run tests: `pnpm build && pnpm check && pnpm test`
|
||||
@@ -169,7 +170,7 @@ Built with Codex, Claude, or other AI tools? **Awesome - just mark it!**
|
||||
Please include in your PR:
|
||||
|
||||
- [ ] Mark as AI-assisted in the PR title or description
|
||||
- [ ] Include human-run real behavior proof from your own setup. AI-generated tests, mocks, lint, typechecks, and CI output are supplemental only; they do not prove the fix works for users.
|
||||
- [ ] Include a concise **Evidence** section with the most useful validation. Reviewers will inspect the code, tests, and CI rather than relying on the PR body alone.
|
||||
- [ ] Include prompts or session logs if possible (super helpful!)
|
||||
- [ ] Confirm you understand what the code does
|
||||
- [ ] If you have access to Codex, run `codex review --base origin/main` locally and address the findings before asking for review
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# Source of truth: apps/android/version.json
|
||||
# Generated by scripts/android-sync-versioning.ts.
|
||||
|
||||
OPENCLAW_ANDROID_VERSION_NAME=2026.6.2
|
||||
OPENCLAW_ANDROID_VERSION_CODE=2026060201
|
||||
OPENCLAW_ANDROID_VERSION_NAME=2026.6.9
|
||||
OPENCLAW_ANDROID_VERSION_CODE=2026060901
|
||||
|
||||
@@ -1,3 +1 @@
|
||||
OpenClaw is now available on Android.
|
||||
|
||||
Connect to your OpenClaw Gateway to chat with your assistant, use realtime Talk mode, review approvals, and bring Android device capabilities like camera, location, screen, and notifications into your private automation workflows.
|
||||
Maintenance update for the current OpenClaw Android release.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"version": "2026.6.2",
|
||||
"versionCode": 2026060201
|
||||
"version": "2026.6.9",
|
||||
"versionCode": 2026060901
|
||||
}
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# OpenClaw iOS Changelog
|
||||
|
||||
## 2026.6.9 - 2026-06-20
|
||||
|
||||
Maintenance update for the current OpenClaw release.
|
||||
|
||||
- Added Apple Watch controls for common agent actions.
|
||||
- Improved Gateway setup, notification settings, and share-extension identity handling.
|
||||
- Updated the Watch app integration for current Xcode compatibility.
|
||||
|
||||
## 2026.6.2 - 2026-06-02
|
||||
|
||||
OpenClaw is now available on iPhone.
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
// Source of truth: apps/ios/version.json
|
||||
// Generated by scripts/ios-sync-versioning.ts.
|
||||
|
||||
OPENCLAW_IOS_VERSION = 2026.6.2
|
||||
OPENCLAW_MARKETING_VERSION = 2026.6.2
|
||||
OPENCLAW_IOS_VERSION = 2026.6.9
|
||||
OPENCLAW_MARKETING_VERSION = 2026.6.9
|
||||
OPENCLAW_BUILD_VERSION = 1
|
||||
|
||||
#include? "../build/Version.xcconfig"
|
||||
|
||||
@@ -54,6 +54,8 @@ struct SettingsProTab: View {
|
||||
@State var locationStatusText: String?
|
||||
@State var previousLocationModeRaw: String = OpenClawLocationMode.off.rawValue
|
||||
@State var notificationStatus: SettingsNotificationStatus = .checking
|
||||
@State var isRequestingNotificationAuthorization = false
|
||||
@State var showNotificationRelayDisclosure = false
|
||||
@State var diagnosticsLastRunText = "Not run"
|
||||
@State var diagnosticsIssueCount: Int?
|
||||
@State var showTalkIssueDetails = false
|
||||
@@ -61,15 +63,18 @@ struct SettingsProTab: View {
|
||||
let initialRoute: SettingsRoute?
|
||||
let directRoute: SettingsRoute?
|
||||
let headerLeadingAction: OpenClawSidebarHeaderAction?
|
||||
let onRouteChange: ((SettingsRoute?) -> Void)?
|
||||
|
||||
init(
|
||||
initialRoute: SettingsRoute? = nil,
|
||||
directRoute: SettingsRoute? = nil,
|
||||
headerLeadingAction: OpenClawSidebarHeaderAction? = nil)
|
||||
headerLeadingAction: OpenClawSidebarHeaderAction? = nil,
|
||||
onRouteChange: ((SettingsRoute?) -> Void)? = nil)
|
||||
{
|
||||
self.initialRoute = initialRoute
|
||||
self.directRoute = directRoute
|
||||
self.headerLeadingAction = headerLeadingAction
|
||||
self.onRouteChange = onRouteChange
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
@@ -117,6 +122,7 @@ struct SettingsProTab: View {
|
||||
self.refreshNotificationSettings()
|
||||
self.applyPendingGatewaySetupLinkIfNeeded()
|
||||
self.applyInitialRouteIfNeeded()
|
||||
self.notifyRouteChange()
|
||||
}
|
||||
.onChange(of: self.scenePhase) { _, phase in
|
||||
if phase == .active {
|
||||
@@ -153,6 +159,9 @@ struct SettingsProTab: View {
|
||||
.onChange(of: self.appModel.gatewaySetupRequestID) { _, _ in
|
||||
self.applyPendingGatewaySetupLinkIfNeeded()
|
||||
}
|
||||
.onChange(of: self.navigationPath) { _, _ in
|
||||
self.notifyRouteChange()
|
||||
}
|
||||
}
|
||||
|
||||
private func settingsModalPresentation(_ content: some View) -> some View {
|
||||
@@ -217,6 +226,19 @@ struct SettingsProTab: View {
|
||||
} message: {
|
||||
Text(self.scannerError ?? "")
|
||||
}
|
||||
.alert("Enable OpenClaw Hosted Push Relay?", isPresented: self.$showNotificationRelayDisclosure) {
|
||||
Button("Continue") {
|
||||
self.requestNotificationAuthorizationFromSettings()
|
||||
}
|
||||
Button("Not Now", role: .cancel) {}
|
||||
} message: {
|
||||
Text(self.notificationRelayDisclosureMessage)
|
||||
}
|
||||
}
|
||||
|
||||
func openNotificationsRouteFromApprovals() {
|
||||
guard self.directRoute == nil else { return }
|
||||
self.navigationPath = [.notifications]
|
||||
}
|
||||
|
||||
private func applyInitialRouteIfNeeded() {
|
||||
@@ -225,4 +247,12 @@ struct SettingsProTab: View {
|
||||
guard self.navigationPath != [initialRoute] else { return }
|
||||
self.navigationPath = [initialRoute]
|
||||
}
|
||||
|
||||
private func notifyRouteChange() {
|
||||
if let directRoute {
|
||||
self.onRouteChange?(directRoute)
|
||||
return
|
||||
}
|
||||
self.onRouteChange?(self.navigationPath.last)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -426,15 +426,30 @@ extension SettingsProTab {
|
||||
self.openNotificationSettings()
|
||||
return
|
||||
}
|
||||
guard self.notificationStatus == .notSet else { return }
|
||||
|
||||
if PushBuildConfig.current.usesOpenClawHostedRelay {
|
||||
self.showNotificationRelayDisclosure = true
|
||||
return
|
||||
}
|
||||
self.requestNotificationAuthorizationFromSettings()
|
||||
}
|
||||
|
||||
func requestNotificationAuthorizationFromSettings() {
|
||||
guard !self.isRequestingNotificationAuthorization else { return }
|
||||
self.isRequestingNotificationAuthorization = true
|
||||
Task {
|
||||
let granted = await (try? UNUserNotificationCenter.current().requestAuthorization(options: [
|
||||
.alert,
|
||||
.badge,
|
||||
.sound,
|
||||
])) ?? false
|
||||
let settings = await UNUserNotificationCenter.current().notificationSettings()
|
||||
await MainActor.run {
|
||||
self.notificationStatus = granted ? .allowed : .notAllowed
|
||||
self.isRequestingNotificationAuthorization = false
|
||||
self.notificationStatus = SettingsNotificationStatus(settings.authorizationStatus)
|
||||
guard granted, self.notificationStatus.allowsNotifications else { return }
|
||||
UIApplication.shared.registerForRemoteNotifications()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -661,6 +676,9 @@ extension SettingsProTab {
|
||||
if self.appModel.isAppleReviewDemoModeEnabled {
|
||||
return "Live gateway requests are disabled in demo mode."
|
||||
}
|
||||
if self.notificationsNeedAttention {
|
||||
return "Foreground approvals still appear while OpenClaw is connected."
|
||||
}
|
||||
return self.gatewayConnected ? "Gateway requests will appear here." : "Connect to the gateway."
|
||||
}
|
||||
|
||||
@@ -700,7 +718,19 @@ extension SettingsProTab {
|
||||
}
|
||||
|
||||
var approvalsDetail: String {
|
||||
self.pendingApproval == nil ? "No approvals waiting" : "1 request waiting"
|
||||
if self.notificationsNeedAttention {
|
||||
return self.pendingApproval == nil ? "Notifications off" : "1 waiting, notifications off"
|
||||
}
|
||||
return self.pendingApproval == nil ? "No approvals waiting" : "1 request waiting"
|
||||
}
|
||||
|
||||
var notificationsNeedAttention: Bool {
|
||||
switch self.notificationStatus {
|
||||
case .allowed, .checking:
|
||||
false
|
||||
case .notAllowed, .notSet, .unknown:
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
var approvalItems: [SettingsApprovalItem] {
|
||||
@@ -771,4 +801,33 @@ extension SettingsProTab {
|
||||
var notificationActionText: String {
|
||||
self.notificationStatus.actionTitle
|
||||
}
|
||||
|
||||
var notificationStatusDetail: String {
|
||||
switch self.notificationStatus {
|
||||
case .checking:
|
||||
"Checking iOS notification permission."
|
||||
case .allowed:
|
||||
"OpenClaw can show approval prompts and event alerts when the app is not active."
|
||||
case .notAllowed:
|
||||
"Notifications have been denied. Enable them in iOS Settings."
|
||||
case .notSet:
|
||||
"Enable notifications to receive approval prompts and event alerts outside the app."
|
||||
case .unknown:
|
||||
"OpenClaw cannot determine the current notification permission state."
|
||||
}
|
||||
}
|
||||
|
||||
var notificationRelayDetail: String {
|
||||
if PushBuildConfig.current.usesOpenClawHostedRelay {
|
||||
return """
|
||||
This build uses OpenClaw's hosted push relay at ios-push-relay.openclaw.ai for notification \
|
||||
delivery data.
|
||||
"""
|
||||
}
|
||||
return "This build is not configured to use OpenClaw's hosted push relay."
|
||||
}
|
||||
|
||||
var notificationRelayDisclosureMessage: String {
|
||||
"Enabling this sends delivery data through OpenClaw's hosted push relay."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -308,15 +308,57 @@ extension SettingsProTab {
|
||||
self.detailStatusCard(
|
||||
icon: "checkmark.shield.fill",
|
||||
title: "Approvals",
|
||||
detail: self.pendingApproval == nil ? "No gateway actions are waiting for review." :
|
||||
"Review the pending gateway action.",
|
||||
value: self.pendingApproval == nil ? "clear" : "1 waiting",
|
||||
color: self.pendingApproval == nil ? OpenClawBrand.ok : OpenClawBrand.warn)
|
||||
detail: self.notificationsNeedAttention
|
||||
? "Out-of-app approval alerts need notification permission."
|
||||
: (self.pendingApproval == nil ? "No gateway actions are waiting for review." :
|
||||
"Review the pending gateway action."),
|
||||
value: self.notificationsNeedAttention
|
||||
? "Alerts Off"
|
||||
: (self.pendingApproval == nil ? "clear" : "1 waiting"),
|
||||
color: self.notificationsNeedAttention ? OpenClawBrand.warn :
|
||||
(self.pendingApproval == nil ? OpenClawBrand.ok : OpenClawBrand.warn))
|
||||
|
||||
if self.notificationsNeedAttention {
|
||||
self.approvalNotificationsWarningCard
|
||||
}
|
||||
|
||||
self.approvalsReviewCard
|
||||
}
|
||||
}
|
||||
|
||||
var approvalNotificationsWarningCard: some View {
|
||||
ProCard(radius: SettingsLayout.cardRadius) {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
HStack(alignment: .top, spacing: 12) {
|
||||
ProIconBadge(systemName: "bell.slash.fill", color: OpenClawBrand.warn)
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text("Notifications are off")
|
||||
.font(.subheadline.weight(.semibold))
|
||||
Text(
|
||||
"""
|
||||
Enable Notifications to receive approval notifications while OpenClaw is not open.
|
||||
""")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
}
|
||||
|
||||
if self.directRoute == nil {
|
||||
Button {
|
||||
self.openNotificationsRouteFromApprovals()
|
||||
} label: {
|
||||
Label("Open Notifications", systemImage: "bell.badge")
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.controlSize(.small)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, OpenClawProMetric.pagePadding)
|
||||
}
|
||||
|
||||
var approvalsReviewCard: some View {
|
||||
ProCard(radius: SettingsLayout.cardRadius) {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
@@ -490,7 +532,7 @@ extension SettingsProTab {
|
||||
self.detailStatusCard(
|
||||
icon: "bell",
|
||||
title: "Notifications",
|
||||
detail: "Approvals and event alerts from OpenClaw.",
|
||||
detail: self.notificationStatusDetail,
|
||||
value: self.notificationStatusText,
|
||||
color: self.notificationStatus.color)
|
||||
|
||||
@@ -506,10 +548,25 @@ extension SettingsProTab {
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.controlSize(.small)
|
||||
.disabled(self.notificationStatus == .checking || self.isRequestingNotificationAuthorization)
|
||||
|
||||
Text("OpenClaw uses notifications for approval prompts and mirrored event alerts.")
|
||||
Text(self.notificationStatusDetail)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
||||
Divider()
|
||||
|
||||
HStack(alignment: .top, spacing: 10) {
|
||||
Image(systemName: "network")
|
||||
.font(.caption.weight(.semibold))
|
||||
.foregroundStyle(OpenClawBrand.accent)
|
||||
.frame(width: 22, height: 22)
|
||||
Text(self.notificationRelayDetail)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, OpenClawProMetric.pagePadding)
|
||||
|
||||
@@ -89,28 +89,48 @@ enum SettingsNotificationStatus: Equatable {
|
||||
var text: String {
|
||||
switch self {
|
||||
case .checking: "Checking"
|
||||
case .allowed: "Allowed"
|
||||
case .notAllowed: "Not Allowed"
|
||||
case .notSet: "Not Set"
|
||||
case .allowed: "Enabled"
|
||||
case .notAllowed: "Denied"
|
||||
case .notSet: "Not Enabled"
|
||||
case .unknown: "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
var actionTitle: String {
|
||||
switch self {
|
||||
case .notSet, .checking:
|
||||
"Request Access"
|
||||
case .allowed, .notAllowed, .unknown:
|
||||
"Open System Settings"
|
||||
case .notSet:
|
||||
"Enable Notifications"
|
||||
case .checking:
|
||||
"Checking"
|
||||
case .allowed:
|
||||
"Manage in iOS Settings"
|
||||
case .notAllowed, .unknown:
|
||||
"Open iOS Settings"
|
||||
}
|
||||
}
|
||||
|
||||
var actionIcon: String {
|
||||
self == .allowed ? "gear" : "bell.badge"
|
||||
switch self {
|
||||
case .allowed:
|
||||
"gear"
|
||||
case .notAllowed, .unknown:
|
||||
"gear.badge"
|
||||
case .checking:
|
||||
"hourglass"
|
||||
case .notSet:
|
||||
"bell.badge"
|
||||
}
|
||||
}
|
||||
|
||||
var color: Color {
|
||||
self == .allowed ? OpenClawBrand.ok : .secondary
|
||||
switch self {
|
||||
case .allowed:
|
||||
OpenClawBrand.ok
|
||||
case .notAllowed, .unknown:
|
||||
OpenClawBrand.warn
|
||||
case .checking, .notSet:
|
||||
.secondary
|
||||
}
|
||||
}
|
||||
|
||||
var shouldOpenNotificationSettings: Bool {
|
||||
@@ -121,6 +141,10 @@ enum SettingsNotificationStatus: Equatable {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
var allowsNotifications: Bool {
|
||||
self == .allowed
|
||||
}
|
||||
}
|
||||
|
||||
enum SettingsDiagnosticIssue: String, Equatable, CaseIterable {
|
||||
|
||||
@@ -2,11 +2,14 @@ import SwiftUI
|
||||
|
||||
private struct ExecApprovalPromptDialogModifier: ViewModifier {
|
||||
@Environment(NodeAppModel.self) private var appModel: NodeAppModel
|
||||
let suppressedApprovalID: String?
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.overlay {
|
||||
if let prompt = self.appModel.pendingExecApprovalPrompt {
|
||||
if let prompt = self.appModel.pendingExecApprovalPrompt,
|
||||
prompt.id != self.suppressedApprovalID
|
||||
{
|
||||
ZStack {
|
||||
Color.black.opacity(0.38)
|
||||
.ignoresSafeArea()
|
||||
@@ -58,7 +61,7 @@ private struct ExecApprovalPromptCard: View {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text("Exec approval required")
|
||||
.font(.headline)
|
||||
Text("OpenClaw opened from a notification. Review this exec request before continuing.")
|
||||
Text("Review this exec request before continuing. Your decision will be sent back to the gateway.")
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
@@ -188,7 +191,7 @@ private struct ExecApprovalPromptMetadataRow: View {
|
||||
}
|
||||
|
||||
extension View {
|
||||
func execApprovalPromptDialog() -> some View {
|
||||
self.modifier(ExecApprovalPromptDialogModifier())
|
||||
func execApprovalPromptDialog(suppressedApprovalID: String? = nil) -> some View {
|
||||
self.modifier(ExecApprovalPromptDialogModifier(suppressedApprovalID: suppressedApprovalID))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
import SwiftUI
|
||||
|
||||
private struct NotificationPermissionGuidanceDialogModifier: ViewModifier {
|
||||
@Environment(NodeAppModel.self) private var appModel: NodeAppModel
|
||||
let openNotifications: (String) -> Void
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.overlay {
|
||||
if let prompt = self.appModel.pendingNotificationPermissionGuidancePrompt {
|
||||
ZStack {
|
||||
Color.black.opacity(0.38)
|
||||
.ignoresSafeArea()
|
||||
|
||||
NotificationPermissionGuidanceCard(
|
||||
onOpenNotifications: {
|
||||
let approvalId = prompt.approvalId
|
||||
self.appModel.dismissNotificationPermissionGuidancePrompt(
|
||||
suppressFuture: false)
|
||||
self.openNotifications(approvalId)
|
||||
},
|
||||
onDismiss: {
|
||||
self.appModel.dismissNotificationPermissionGuidancePrompt(
|
||||
suppressFuture: false)
|
||||
},
|
||||
onSuppressFuture: {
|
||||
self.appModel.dismissNotificationPermissionGuidancePrompt(
|
||||
suppressFuture: true)
|
||||
})
|
||||
.padding(.horizontal, 20)
|
||||
.frame(maxWidth: 460)
|
||||
.transition(.scale(scale: 0.98).combined(with: .opacity))
|
||||
}
|
||||
.zIndex(2)
|
||||
.id(prompt.id)
|
||||
}
|
||||
}
|
||||
.animation(
|
||||
.easeInOut(duration: 0.18),
|
||||
value: self.appModel.pendingNotificationPermissionGuidancePrompt?.id)
|
||||
}
|
||||
}
|
||||
|
||||
private struct NotificationPermissionGuidanceCard: View {
|
||||
let onOpenNotifications: () -> Void
|
||||
let onDismiss: () -> Void
|
||||
let onSuppressFuture: () -> Void
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 14) {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text("Notifications are off")
|
||||
.font(.headline)
|
||||
Text(
|
||||
"""
|
||||
Exec approvals can only be reviewed while OpenClaw is open and connected.
|
||||
|
||||
Enable Notifications to receive approval notifications while OpenClaw is not open.
|
||||
""")
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
|
||||
VStack(spacing: 10) {
|
||||
Button {
|
||||
self.onOpenNotifications()
|
||||
} label: {
|
||||
Text("Open Notifications Settings")
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
|
||||
Button(role: .cancel) {
|
||||
self.onDismiss()
|
||||
} label: {
|
||||
Text("Not Now")
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
|
||||
Button {
|
||||
self.onSuppressFuture()
|
||||
} label: {
|
||||
Text("Don't show again")
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
}
|
||||
.controlSize(.large)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.padding(18)
|
||||
.proPanelSurface(tint: OpenClawBrand.warn, radius: 20, isProminent: true)
|
||||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
func notificationPermissionGuidanceDialog(openNotifications: @escaping (String) -> Void) -> some View {
|
||||
self.modifier(NotificationPermissionGuidanceDialogModifier(openNotifications: openNotifications))
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,10 @@ private struct WatchChatPreview {
|
||||
var statusText: String?
|
||||
}
|
||||
|
||||
private struct ExecApprovalGatewayEventPayload: Decodable {
|
||||
var id: String
|
||||
}
|
||||
|
||||
/// Ensures notification requests return promptly even if the system prompt blocks.
|
||||
private final class NotificationInvokeLatch<T: Sendable>: @unchecked Sendable {
|
||||
private let lock = NSLock()
|
||||
@@ -83,6 +87,11 @@ final class NodeAppModel {
|
||||
}
|
||||
}
|
||||
|
||||
struct NotificationPermissionGuidancePrompt: Identifiable, Equatable {
|
||||
let id = UUID()
|
||||
let approvalId: String
|
||||
}
|
||||
|
||||
private enum ExecApprovalResolutionOutcome {
|
||||
case resolved
|
||||
case stale
|
||||
@@ -96,6 +105,8 @@ final class NodeAppModel {
|
||||
}
|
||||
|
||||
private let deepLinkLogger = Logger(subsystem: "ai.openclawfoundation.app", category: "DeepLink")
|
||||
private nonisolated static let execApprovalNotificationGuidanceSuppressedKey =
|
||||
"notifications.execApprovalGuidance.suppressed"
|
||||
private let pushWakeLogger = Logger(subsystem: "ai.openclawfoundation.app", category: "PushWake")
|
||||
private let pendingActionLogger = Logger(subsystem: "ai.openclawfoundation.app", category: "PendingAction")
|
||||
private let locationWakeLogger = Logger(subsystem: "ai.openclawfoundation.app", category: "LocationWake")
|
||||
@@ -156,6 +167,7 @@ final class NodeAppModel {
|
||||
private(set) var pendingExecApprovalPromptResolving: Bool = false
|
||||
private(set) var pendingExecApprovalPromptErrorText: String?
|
||||
private var pendingExecApprovalPromptRequestGeneration: Int = 0
|
||||
private(set) var pendingNotificationPermissionGuidancePrompt: NotificationPermissionGuidancePrompt?
|
||||
private var queuedAgentDeepLinkPrompt: AgentDeepLinkPrompt?
|
||||
private var lastAgentDeepLinkPromptAt: Date = .distantPast
|
||||
@ObservationIgnored private var queuedAgentDeepLinkPromptTask: Task<Void, Never>?
|
||||
@@ -895,26 +907,50 @@ final class NodeAppModel {
|
||||
for await evt in stream {
|
||||
if Task.isCancelled { return }
|
||||
guard let payload = evt.payload else { continue }
|
||||
switch evt.event {
|
||||
case "voicewake.changed":
|
||||
struct Payload: Decodable { var triggers: [String] }
|
||||
guard let decoded = try? GatewayPayloadDecoding.decode(payload, as: Payload.self) else { continue }
|
||||
let triggers = VoiceWakePreferences.sanitizeTriggerWords(decoded.triggers)
|
||||
VoiceWakePreferences.saveTriggerWords(triggers)
|
||||
case "talk.mode":
|
||||
struct Payload: Decodable {
|
||||
var enabled: Bool
|
||||
var phase: String?
|
||||
}
|
||||
guard let decoded = try? GatewayPayloadDecoding.decode(payload, as: Payload.self) else { continue }
|
||||
self.applyTalkModeSync(enabled: decoded.enabled, phase: decoded.phase)
|
||||
default:
|
||||
continue
|
||||
}
|
||||
await self.handleOperatorGatewayServerEvent(evt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func handleOperatorGatewayServerEvent(_ evt: EventFrame) async {
|
||||
guard let payload = evt.payload else { return }
|
||||
switch evt.event {
|
||||
case "voicewake.changed":
|
||||
struct Payload: Decodable { var triggers: [String] }
|
||||
guard let decoded = try? GatewayPayloadDecoding.decode(payload, as: Payload.self) else { return }
|
||||
let triggers = VoiceWakePreferences.sanitizeTriggerWords(decoded.triggers)
|
||||
VoiceWakePreferences.saveTriggerWords(triggers)
|
||||
case "talk.mode":
|
||||
struct Payload: Decodable {
|
||||
var enabled: Bool
|
||||
var phase: String?
|
||||
}
|
||||
guard let decoded = try? GatewayPayloadDecoding.decode(payload, as: Payload.self) else { return }
|
||||
self.applyTalkModeSync(enabled: decoded.enabled, phase: decoded.phase)
|
||||
case ExecApprovalNotificationBridge.requestedKind:
|
||||
guard let approvalId = Self.execApprovalEventID(from: payload) else { return }
|
||||
await self.presentNotificationPermissionGuidanceForExecApprovalIfNeeded(approvalId: approvalId)
|
||||
await self.presentExecApprovalNotificationPrompt(
|
||||
ExecApprovalNotificationPrompt(approvalId: approvalId))
|
||||
case ExecApprovalNotificationBridge.resolvedKind:
|
||||
guard let approvalId = Self.execApprovalEventID(from: payload) else { return }
|
||||
await self.handleExecApprovalResolvedRemotePush(approvalId: approvalId)
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
private nonisolated static func execApprovalEventID(from payload: AnyCodable) -> String? {
|
||||
guard let decoded = try? GatewayPayloadDecoding.decode(
|
||||
payload,
|
||||
as: ExecApprovalGatewayEventPayload.self)
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
let approvalId = decoded.id.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
return approvalId.isEmpty ? nil : approvalId
|
||||
}
|
||||
|
||||
private func applyTalkModeSync(enabled: Bool, phase: String?) {
|
||||
_ = phase
|
||||
guard self.talkMode.isEnabled != enabled else { return }
|
||||
@@ -1332,8 +1368,8 @@ final class NodeAppModel {
|
||||
error: OpenClawNodeError(code: .invalidRequest, message: "INVALID_REQUEST: empty notification"))
|
||||
}
|
||||
|
||||
let finalStatus = await self.requestNotificationAuthorizationIfNeeded()
|
||||
guard finalStatus == .authorized || finalStatus == .provisional || finalStatus == .ephemeral else {
|
||||
let status = await self.notificationAuthorizationStatus()
|
||||
guard Self.isNotificationAuthorizationAllowed(status) else {
|
||||
return BridgeInvokeResponse(
|
||||
id: req.id,
|
||||
ok: false,
|
||||
@@ -1385,9 +1421,18 @@ final class NodeAppModel {
|
||||
error: OpenClawNodeError(code: .invalidRequest, message: "INVALID_REQUEST: empty chat.push text"))
|
||||
}
|
||||
|
||||
let finalStatus = await self.requestNotificationAuthorizationIfNeeded()
|
||||
let shouldSpeak = params.speak ?? true
|
||||
let status = await self.notificationAuthorizationStatus()
|
||||
let notificationsAllowed = Self.isNotificationAuthorizationAllowed(status)
|
||||
if !notificationsAllowed, !shouldSpeak {
|
||||
return BridgeInvokeResponse(
|
||||
id: req.id,
|
||||
ok: false,
|
||||
error: OpenClawNodeError(code: .unavailable, message: "NOT_AUTHORIZED: notifications"))
|
||||
}
|
||||
|
||||
let messageId = UUID().uuidString
|
||||
if finalStatus == .authorized || finalStatus == .provisional || finalStatus == .ephemeral {
|
||||
if notificationsAllowed {
|
||||
let addResult = await self.runNotificationCall(timeoutSeconds: 2.0) { [notificationCenter] in
|
||||
let content = UNMutableNotificationContent()
|
||||
content.title = "OpenClaw"
|
||||
@@ -1408,7 +1453,7 @@ final class NodeAppModel {
|
||||
}
|
||||
}
|
||||
|
||||
if params.speak ?? true {
|
||||
if shouldSpeak {
|
||||
let toSpeak = text
|
||||
Task { @MainActor in
|
||||
try? await TalkSystemSpeechSynthesizer.shared.speak(text: toSpeak)
|
||||
@@ -1420,26 +1465,6 @@ final class NodeAppModel {
|
||||
return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: json)
|
||||
}
|
||||
|
||||
private func requestNotificationAuthorizationIfNeeded() async -> NotificationAuthorizationStatus {
|
||||
let status = await self.notificationAuthorizationStatus()
|
||||
guard status == .notDetermined else { return status }
|
||||
|
||||
// Avoid hanging invoke requests if the permission prompt is never answered.
|
||||
_ = await self.runNotificationCall(timeoutSeconds: 2.0) { [notificationCenter] in
|
||||
_ = try await notificationCenter.requestAuthorization(options: [.alert, .sound, .badge])
|
||||
}
|
||||
|
||||
let updatedStatus = await self.notificationAuthorizationStatus()
|
||||
if Self.isNotificationAuthorizationAllowed(updatedStatus) {
|
||||
// Refresh APNs registration immediately after the first permission grant so the
|
||||
// gateway can receive a push registration without requiring an app relaunch.
|
||||
await MainActor.run {
|
||||
UIApplication.shared.registerForRemoteNotifications()
|
||||
}
|
||||
}
|
||||
return updatedStatus
|
||||
}
|
||||
|
||||
private func notificationAuthorizationStatus() async -> NotificationAuthorizationStatus {
|
||||
let result = await self.runNotificationCall(timeoutSeconds: 1.5) { [notificationCenter] in
|
||||
await notificationCenter.authorizationStatus()
|
||||
@@ -1463,6 +1488,29 @@ final class NodeAppModel {
|
||||
}
|
||||
}
|
||||
|
||||
private func presentNotificationPermissionGuidanceForExecApprovalIfNeeded(approvalId: String) async {
|
||||
guard !self.execApprovalNotificationGuidanceSuppressed else { return }
|
||||
let status = await self.notificationAuthorizationStatus()
|
||||
guard !Self.isNotificationAuthorizationAllowed(status) else { return }
|
||||
self.pendingNotificationPermissionGuidancePrompt =
|
||||
NotificationPermissionGuidancePrompt(approvalId: approvalId)
|
||||
}
|
||||
|
||||
var execApprovalNotificationGuidanceSuppressed: Bool {
|
||||
UserDefaults.standard.bool(forKey: Self.execApprovalNotificationGuidanceSuppressedKey)
|
||||
}
|
||||
|
||||
func dismissNotificationPermissionGuidancePrompt(suppressFuture: Bool) {
|
||||
if suppressFuture {
|
||||
UserDefaults.standard.set(true, forKey: Self.execApprovalNotificationGuidanceSuppressedKey)
|
||||
}
|
||||
self.pendingNotificationPermissionGuidancePrompt = nil
|
||||
}
|
||||
|
||||
func resetExecApprovalNotificationGuidanceSuppression() {
|
||||
UserDefaults.standard.removeObject(forKey: Self.execApprovalNotificationGuidanceSuppressedKey)
|
||||
}
|
||||
|
||||
private func runNotificationCall<T: Sendable>(
|
||||
timeoutSeconds: Double,
|
||||
operation: @escaping @Sendable () async throws -> T) async -> Result<T, NotificationCallError>
|
||||
@@ -2335,10 +2383,6 @@ extension NodeAppModel {
|
||||
nodeOptions: nodeOptions,
|
||||
sessionBox: sessionBox)
|
||||
}
|
||||
|
||||
// QR bootstrap onboarding should surface the system notification permission
|
||||
// prompt immediately so visible APNs alerts work without a second manual step.
|
||||
_ = await self.requestNotificationAuthorizationIfNeeded()
|
||||
}
|
||||
|
||||
private func refreshBackgroundReconnectSuppressionIfNeeded(source: String) {
|
||||
@@ -3916,11 +3960,15 @@ extension NodeAppModel {
|
||||
let hadWatchPrompt = self.watchExecApprovalPromptsByID[normalizedApprovalID] != nil
|
||||
let hadPendingPrompt = self.pendingExecApprovalPrompt?.id == normalizedApprovalID
|
||||
let hadPendingRecoveryID = self.pendingWatchExecApprovalRecoveryIDs.contains(normalizedApprovalID)
|
||||
guard hadWatchPrompt || hadPendingPrompt || hadPendingRecoveryID else {
|
||||
let hadGuidancePrompt = self.pendingNotificationPermissionGuidancePrompt?.approvalId == normalizedApprovalID
|
||||
let hadApprovalSurface = hadWatchPrompt || hadPendingPrompt || hadPendingRecoveryID
|
||||
guard hadApprovalSurface || hadGuidancePrompt else {
|
||||
return
|
||||
}
|
||||
|
||||
await self.publishWatchExecApprovalExpired(approvalId: normalizedApprovalID, reason: .resolved)
|
||||
if hadApprovalSurface {
|
||||
await self.publishWatchExecApprovalExpired(approvalId: normalizedApprovalID, reason: .resolved)
|
||||
}
|
||||
self.clearPendingExecApprovalPromptIfMatches(normalizedApprovalID)
|
||||
}
|
||||
|
||||
@@ -4401,10 +4449,17 @@ extension NodeAppModel {
|
||||
|
||||
private func clearPendingExecApprovalPromptIfMatches(_ approvalId: String) {
|
||||
let normalizedApprovalID = approvalId.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
self.clearNotificationPermissionGuidancePromptIfMatches(normalizedApprovalID)
|
||||
guard self.pendingExecApprovalPrompt?.id == normalizedApprovalID else { return }
|
||||
self.dismissPendingExecApprovalPrompt()
|
||||
}
|
||||
|
||||
private func clearNotificationPermissionGuidancePromptIfMatches(_ approvalId: String) {
|
||||
let normalizedApprovalID = approvalId.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard self.pendingNotificationPermissionGuidancePrompt?.approvalId == normalizedApprovalID else { return }
|
||||
self.pendingNotificationPermissionGuidancePrompt = nil
|
||||
}
|
||||
|
||||
private nonisolated static func isApprovalNotificationStaleError(_ error: Error) -> Bool {
|
||||
guard let gatewayError = error as? GatewayResponseError else { return false }
|
||||
if gatewayError.code != "INVALID_REQUEST" {
|
||||
@@ -5110,6 +5165,20 @@ extension NodeAppModel {
|
||||
self.pendingExecApprovalPrompt
|
||||
}
|
||||
|
||||
func _test_pendingNotificationPermissionGuidancePrompt() -> NotificationPermissionGuidancePrompt? {
|
||||
self.pendingNotificationPermissionGuidancePrompt
|
||||
}
|
||||
|
||||
func _debug_presentNotificationPermissionGuidancePromptForScreenshot() {
|
||||
self.resetExecApprovalNotificationGuidanceSuppression()
|
||||
self.pendingNotificationPermissionGuidancePrompt =
|
||||
NotificationPermissionGuidancePrompt(approvalId: "screenshot-exec-approval")
|
||||
}
|
||||
|
||||
func _test_resetExecApprovalNotificationGuidanceSuppression() {
|
||||
self.resetExecApprovalNotificationGuidanceSuppression()
|
||||
}
|
||||
|
||||
func _test_recordPendingWatchExecApprovalRecoveryID(_ approvalId: String) {
|
||||
self.appendPendingWatchExecApprovalRecoveryID(approvalId)
|
||||
}
|
||||
@@ -5139,6 +5208,14 @@ extension NodeAppModel {
|
||||
isBackgrounded: isBackgrounded)
|
||||
}
|
||||
|
||||
nonisolated static func _test_execApprovalEventID(from payload: AnyCodable) -> String? {
|
||||
self.execApprovalEventID(from: payload)
|
||||
}
|
||||
|
||||
func _test_handleOperatorGatewayServerEvent(_ event: EventFrame) async {
|
||||
await self.handleOperatorGatewayServerEvent(event)
|
||||
}
|
||||
|
||||
nonisolated static func _test_watchExecApprovalIDsNeedingFetch(
|
||||
candidateIDs: [String],
|
||||
cachedApprovalIDs: [String]) -> [String]
|
||||
@@ -5235,24 +5312,6 @@ extension NodeAppModel {
|
||||
func _test_restartGatewaySessionsAfterForegroundStaleConnection() async {
|
||||
await self.restartGatewaySessionsAfterForegroundStaleConnection()
|
||||
}
|
||||
|
||||
func _test_handleSuccessfulBootstrapGatewayOnboarding() async {
|
||||
await self.handleSuccessfulBootstrapGatewayOnboarding(
|
||||
url: URL(string: "wss://gateway.example")!,
|
||||
stableID: "test-gateway",
|
||||
token: nil,
|
||||
password: nil,
|
||||
nodeOptions: GatewayConnectOptions(
|
||||
role: "node",
|
||||
scopes: [],
|
||||
caps: [],
|
||||
commands: [],
|
||||
permissions: [:],
|
||||
clientId: "openclaw-ios",
|
||||
clientMode: "node",
|
||||
clientDisplayName: nil),
|
||||
sessionBox: nil)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
// swiftlint:enable type_body_length file_length
|
||||
|
||||
@@ -409,7 +409,7 @@ enum WatchPromptNotificationBridge {
|
||||
let title = params.title.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let body = params.body.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !title.isEmpty || !body.isEmpty else { return }
|
||||
guard await self.requestNotificationAuthorizationIfNeeded() else { return }
|
||||
guard await self.isNotificationAuthorizationAllowed() else { return }
|
||||
|
||||
let normalizedActions = (params.actions ?? []).compactMap { action -> OpenClawWatchAction? in
|
||||
let id = action.id.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
@@ -516,29 +516,10 @@ enum WatchPromptNotificationBridge {
|
||||
}
|
||||
}
|
||||
|
||||
private static func requestNotificationAuthorizationIfNeeded() async -> Bool {
|
||||
private static func isNotificationAuthorizationAllowed() async -> Bool {
|
||||
let center = UNUserNotificationCenter.current()
|
||||
let status = await self.notificationAuthorizationStatus(center: center)
|
||||
switch status {
|
||||
case .authorized, .provisional, .ephemeral:
|
||||
return true
|
||||
case .notDetermined:
|
||||
let granted = await (try? center.requestAuthorization(options: [.alert, .sound, .badge])) ?? false
|
||||
if !granted { return false }
|
||||
let updatedStatus = await self.notificationAuthorizationStatus(center: center)
|
||||
if self.isAuthorizationStatusAllowed(updatedStatus) {
|
||||
// Refresh APNs registration immediately after the first permission grant so the
|
||||
// gateway can receive a push registration without requiring an app relaunch.
|
||||
await MainActor.run {
|
||||
UIApplication.shared.registerForRemoteNotifications()
|
||||
}
|
||||
}
|
||||
return self.isAuthorizationStatusAllowed(updatedStatus)
|
||||
case .denied:
|
||||
return false
|
||||
@unknown default:
|
||||
return false
|
||||
}
|
||||
return self.isAuthorizationStatusAllowed(status)
|
||||
}
|
||||
|
||||
private static func isAuthorizationStatusAllowed(_ status: UNAuthorizationStatus) -> Bool {
|
||||
@@ -635,6 +616,9 @@ struct OpenClawApp: App {
|
||||
UserDefaults.standard.set(true, forKey: "gateway.hasConnectedOnce")
|
||||
UserDefaults.standard.set(true, forKey: "onboarding.quickSetupDismissed")
|
||||
appModel.enterScreenshotFixtureMode()
|
||||
if Self.screenshotNotificationGuidanceEnabled {
|
||||
appModel._debug_presentNotificationPermissionGuidancePromptForScreenshot()
|
||||
}
|
||||
}
|
||||
#endif
|
||||
OpenClawAppModelRegistry.appModel = appModel
|
||||
@@ -686,6 +670,14 @@ struct OpenClawApp: App {
|
||||
#endif
|
||||
}
|
||||
|
||||
private static var screenshotNotificationGuidanceEnabled: Bool {
|
||||
#if DEBUG
|
||||
ProcessInfo.processInfo.arguments.contains("--openclaw-screenshot-notification-guidance")
|
||||
#else
|
||||
false
|
||||
#endif
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func applyAppearancePreference() {
|
||||
let style = self.appearancePreference.userInterfaceStyle
|
||||
|
||||
@@ -22,6 +22,20 @@ struct PushBuildConfig {
|
||||
let apnsEnvironment: PushAPNsEnvironment
|
||||
|
||||
static let current = PushBuildConfig()
|
||||
static let openClawHostedRelayHost = "ios-push-relay.openclaw.ai"
|
||||
|
||||
var usesOpenClawHostedRelay: Bool {
|
||||
guard self.transport == .relay, self.distribution == .official else { return false }
|
||||
guard let relayBaseURL = self.relayBaseURL,
|
||||
let components = URLComponents(url: relayBaseURL, resolvingAgainstBaseURL: false)
|
||||
else {
|
||||
return false
|
||||
}
|
||||
return components.scheme?.lowercased() == "https"
|
||||
&& components.host?.lowercased() == Self.openClawHostedRelayHost
|
||||
&& components.user == nil
|
||||
&& components.password == nil
|
||||
}
|
||||
|
||||
init(bundle: Bundle = .main) {
|
||||
self.transport = Self.readEnum(
|
||||
|
||||
@@ -24,6 +24,8 @@ struct RootTabs: View {
|
||||
AppAppearancePreference.system.rawValue
|
||||
@State private var selectedTab: AppTab = Self.initialTab
|
||||
@State private var selectedSidebarDestination: SidebarDestination = Self.initialSidebarDestination
|
||||
@State private var selectedSettingsRoute: SettingsRoute? = Self.initialSidebarDestination.settingsRoute
|
||||
@State private var selectedSettingsRouteRequestID: Int = 0
|
||||
@State private var isSidebarVisible: Bool = Self.initialSidebarVisibility ?? false
|
||||
@State private var sidebarVisibilityUserOverridden: Bool = Self.initialSidebarVisibility != nil
|
||||
@State private var isSidebarDrawerLayout: Bool = false
|
||||
@@ -39,6 +41,7 @@ struct RootTabs: View {
|
||||
@State private var didApplyInitialAppearance: Bool = false
|
||||
@State private var didApplyInitialChatSession: Bool = false
|
||||
@State private var handledGatewaySetupRequestID: Int = 0
|
||||
@State private var suppressedExecApprovalPromptIDForNotificationSettings: String?
|
||||
|
||||
private static var initialTab: AppTab {
|
||||
let arguments = ProcessInfo.processInfo.arguments
|
||||
@@ -161,8 +164,10 @@ struct RootTabs: View {
|
||||
.tabItem { Label("Agent", systemImage: "person.2.fill") }
|
||||
.tag(AppTab.agent)
|
||||
|
||||
SettingsProTab(initialRoute: self.selectedSidebarDestination.settingsRoute)
|
||||
.id(self.selectedSidebarDestination.settingsRoute.map { "\($0)" } ?? "settings")
|
||||
SettingsProTab(
|
||||
initialRoute: self.selectedSettingsRoute,
|
||||
onRouteChange: self.handleSettingsRouteChange)
|
||||
.id(self.settingsTabViewID)
|
||||
.tabItem { Label("Settings", systemImage: "gearshape.fill") }
|
||||
.tag(AppTab.settings)
|
||||
}
|
||||
@@ -235,7 +240,7 @@ struct RootTabs: View {
|
||||
|
||||
private var sidebarDetailShell: some View {
|
||||
self.sidebarDetail
|
||||
.id(self.selectedSidebarDestination.id)
|
||||
.id(self.sidebarDetailShellID)
|
||||
}
|
||||
|
||||
private var sidebarColumn: some View {
|
||||
@@ -463,11 +468,21 @@ struct RootTabs: View {
|
||||
headerLeadingAction: self.sidebarHeaderLeadingAction,
|
||||
gatewayAction: { self.selectSidebarDestination(.gateway) })
|
||||
case .settings:
|
||||
SettingsProTab(headerLeadingAction: self.sidebarHeaderLeadingAction)
|
||||
if let selectedSettingsRoute {
|
||||
SettingsProTab(
|
||||
directRoute: selectedSettingsRoute,
|
||||
headerLeadingAction: self.sidebarHeaderLeadingAction,
|
||||
onRouteChange: self.handleSettingsRouteChange)
|
||||
} else {
|
||||
SettingsProTab(
|
||||
headerLeadingAction: self.sidebarHeaderLeadingAction,
|
||||
onRouteChange: self.handleSettingsRouteChange)
|
||||
}
|
||||
case .gateway:
|
||||
SettingsProTab(
|
||||
directRoute: self.selectedSidebarDestination.settingsRoute ?? .gateway,
|
||||
headerLeadingAction: self.sidebarHeaderLeadingAction)
|
||||
directRoute: self.selectedSettingsRoute ?? self.selectedSidebarDestination.settingsRoute ?? .gateway,
|
||||
headerLeadingAction: self.sidebarHeaderLeadingAction,
|
||||
onRouteChange: self.handleSettingsRouteChange)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -492,6 +507,21 @@ struct RootTabs: View {
|
||||
return UIDevice.current.userInterfaceIdiom
|
||||
}
|
||||
|
||||
private var sidebarDetailShellID: String {
|
||||
let routeID = self.selectedSettingsRoute.map { "\($0)" } ?? "root"
|
||||
return "\(self.selectedSidebarDestination.id):\(routeID):\(self.selectedSettingsRouteRequestID)"
|
||||
}
|
||||
|
||||
private var settingsTabViewID: String {
|
||||
let routeID = self.selectedSettingsRoute.map { "\($0)" } ?? "settings"
|
||||
return "\(routeID):\(self.selectedSettingsRouteRequestID)"
|
||||
}
|
||||
|
||||
private var activeExecApprovalPromptSuppressionID: String? {
|
||||
guard self.selectedTab == .settings, self.selectedSettingsRoute == .notifications else { return nil }
|
||||
return self.suppressedExecApprovalPromptIDForNotificationSettings
|
||||
}
|
||||
|
||||
private var shouldCollapseSidebarAfterSelection: Bool {
|
||||
Self.shouldCollapseSidebarAfterSelection(
|
||||
layoutMode: self.isSidebarDrawerLayout ? .drawer : .split)
|
||||
@@ -705,6 +735,11 @@ struct RootTabs: View {
|
||||
.onChange(of: self.appModel.gatewaySetupRequestID) { _, _ in
|
||||
self.maybeOpenSettingsForGatewaySetup()
|
||||
}
|
||||
.onChange(of: self.appModel.pendingExecApprovalPrompt?.id) { _, newValue in
|
||||
if newValue != self.suppressedExecApprovalPromptIDForNotificationSettings {
|
||||
self.suppressedExecApprovalPromptIDForNotificationSettings = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func rootPresentation(_ content: some View) -> some View {
|
||||
@@ -742,7 +777,12 @@ struct RootTabs: View {
|
||||
}
|
||||
.gatewayTrustPromptAlert()
|
||||
.deepLinkAgentPromptAlert()
|
||||
.execApprovalPromptDialog()
|
||||
.execApprovalPromptDialog(
|
||||
suppressedApprovalID: self.activeExecApprovalPromptSuppressionID)
|
||||
.notificationPermissionGuidanceDialog(openNotifications: { approvalId in
|
||||
self.suppressedExecApprovalPromptIDForNotificationSettings = approvalId
|
||||
self.selectSettingsRoute(.notifications)
|
||||
})
|
||||
}
|
||||
|
||||
private var appearancePreference: AppAppearancePreference {
|
||||
@@ -874,9 +914,15 @@ struct RootTabs: View {
|
||||
private func homeCanvasName(for agent: AgentSummary) -> String {
|
||||
self.normalized(agent.name) ?? agent.id
|
||||
}
|
||||
}
|
||||
|
||||
extension RootTabs {
|
||||
private func selectSidebarDestination(_ destination: SidebarDestination) {
|
||||
if destination.settingsRoute != .notifications {
|
||||
self.suppressedExecApprovalPromptIDForNotificationSettings = nil
|
||||
}
|
||||
self.selectedSidebarDestination = destination
|
||||
self.selectedSettingsRoute = destination.settingsRoute
|
||||
self.selectedTab = destination.appTab
|
||||
guard self.usesSidebarTabs, self.shouldCollapseSidebarAfterSelection else { return }
|
||||
withAnimation(.easeInOut(duration: 0.22)) {
|
||||
@@ -884,6 +930,31 @@ struct RootTabs: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func selectSettingsRoute(_ route: SettingsRoute) {
|
||||
if route != .notifications {
|
||||
self.suppressedExecApprovalPromptIDForNotificationSettings = nil
|
||||
}
|
||||
self.selectedSettingsRoute = route
|
||||
self.selectedSettingsRouteRequestID &+= 1
|
||||
self.selectedSidebarDestination = .settings
|
||||
self.selectedTab = .settings
|
||||
guard self.usesSidebarTabs, self.shouldCollapseSidebarAfterSelection else { return }
|
||||
withAnimation(.easeInOut(duration: 0.22)) {
|
||||
self.setSidebarVisible(false)
|
||||
}
|
||||
}
|
||||
|
||||
private func handleSettingsRouteChange(_ route: SettingsRoute?) {
|
||||
guard route != .notifications else { return }
|
||||
if route == nil {
|
||||
self.selectedSettingsRoute = nil
|
||||
if self.selectedTab == .settings {
|
||||
self.selectedSidebarDestination = .settings
|
||||
}
|
||||
}
|
||||
self.suppressedExecApprovalPromptIDForNotificationSettings = nil
|
||||
}
|
||||
|
||||
private func showSidebar() {
|
||||
self.sidebarVisibilityUserOverridden = true
|
||||
withAnimation(.easeInOut(duration: 0.22)) {
|
||||
|
||||
@@ -16,7 +16,6 @@ enum NotificationAuthorizationStatus {
|
||||
|
||||
protocol NotificationCentering: Sendable {
|
||||
func authorizationStatus() async -> NotificationAuthorizationStatus
|
||||
func requestAuthorization(options: UNAuthorizationOptions) async throws -> Bool
|
||||
func add(_ request: UNNotificationRequest) async throws
|
||||
func removePendingNotificationRequests(withIdentifiers identifiers: [String]) async
|
||||
func removeDeliveredNotifications(withIdentifiers identifiers: [String]) async
|
||||
@@ -48,10 +47,6 @@ struct LiveNotificationCenter: NotificationCentering, @unchecked Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
func requestAuthorization(options: UNAuthorizationOptions) async throws -> Bool {
|
||||
try await self.center.requestAuthorization(options: options)
|
||||
}
|
||||
|
||||
func add(_ request: UNNotificationRequest) async throws {
|
||||
try await withCheckedThrowingContinuation { (cont: CheckedContinuation<Void, Error>) in
|
||||
self.center.add(request) { error in
|
||||
|
||||
@@ -50,6 +50,7 @@ Sources/Gateway/GatewayDiscoveryModel.swift
|
||||
Sources/Gateway/GatewayHealthMonitor.swift
|
||||
Sources/Gateway/GatewayProblemView.swift
|
||||
Sources/Gateway/GatewayQuickSetupSheet.swift
|
||||
Sources/Gateway/NotificationPermissionGuidanceDialog.swift
|
||||
Sources/Gateway/GatewayServiceResolver.swift
|
||||
Sources/Gateway/GatewaySettingsStore.swift
|
||||
Sources/Gateway/GatewayTrustPromptAlert.swift
|
||||
|
||||
@@ -14,10 +14,6 @@ private final class MockNotificationCenter: NotificationCentering, @unchecked Se
|
||||
self.authorization
|
||||
}
|
||||
|
||||
func requestAuthorization(options _: UNAuthorizationOptions) async throws -> Bool {
|
||||
true
|
||||
}
|
||||
|
||||
func add(_ request: UNNotificationRequest) async throws {
|
||||
self.addedRequests.append(request)
|
||||
}
|
||||
|
||||
@@ -200,25 +200,16 @@ private final class MockWatchMessagingService: @preconcurrency WatchMessagingSer
|
||||
|
||||
private final class MockBootstrapNotificationCenter: NotificationCentering, @unchecked Sendable {
|
||||
var status: NotificationAuthorizationStatus = .notDetermined
|
||||
var requestAuthorizationResult = false
|
||||
var requestAuthorizationCalls = 0
|
||||
var addCalls = 0
|
||||
|
||||
func authorizationStatus() async -> NotificationAuthorizationStatus {
|
||||
self.status
|
||||
}
|
||||
|
||||
func requestAuthorization(options _: UNAuthorizationOptions) async throws -> Bool {
|
||||
self.requestAuthorizationCalls += 1
|
||||
if self.requestAuthorizationResult {
|
||||
self.status = .authorized
|
||||
} else {
|
||||
self.status = .denied
|
||||
}
|
||||
return self.requestAuthorizationResult
|
||||
func add(_: UNNotificationRequest) async throws {
|
||||
self.addCalls += 1
|
||||
}
|
||||
|
||||
func add(_: UNNotificationRequest) async throws {}
|
||||
|
||||
func removePendingNotificationRequests(withIdentifiers _: [String]) async {}
|
||||
|
||||
func removeDeliveredNotifications(withIdentifiers _: [String]) async {}
|
||||
@@ -1160,6 +1151,35 @@ private final class MockBootstrapNotificationCenter: NotificationCentering, @unc
|
||||
isBackgrounded: false))
|
||||
}
|
||||
|
||||
@Test func execApprovalEventIDDecodesGatewayPayload() {
|
||||
#expect(NodeAppModel._test_execApprovalEventID(from: AnyCodable(["id": " approval-1 "])) == "approval-1")
|
||||
#expect(NodeAppModel._test_execApprovalEventID(from: AnyCodable(["id": " "])) == nil)
|
||||
#expect(NodeAppModel._test_execApprovalEventID(from: AnyCodable(["other": "approval-1"])) == nil)
|
||||
}
|
||||
|
||||
@Test @MainActor func operatorGatewayResolvedEventClearsPendingApprovalPrompt() async throws {
|
||||
let appModel = NodeAppModel()
|
||||
try appModel._test_presentExecApprovalPrompt(
|
||||
#require(
|
||||
NodeAppModel._test_makeExecApprovalPrompt(
|
||||
id: "approval-event-resolved",
|
||||
commandText: "echo clear",
|
||||
allowedDecisions: ["allow-once", "deny"],
|
||||
host: "gateway",
|
||||
nodeId: nil,
|
||||
agentId: nil,
|
||||
expiresAtMs: Int(Date().timeIntervalSince1970 * 1000) + 60000)))
|
||||
|
||||
await appModel._test_handleOperatorGatewayServerEvent(EventFrame(
|
||||
type: "event",
|
||||
event: ExecApprovalNotificationBridge.resolvedKind,
|
||||
payload: AnyCodable(["id": "approval-event-resolved"]),
|
||||
seq: nil,
|
||||
stateversion: nil))
|
||||
|
||||
#expect(appModel._test_pendingExecApprovalPrompt() == nil)
|
||||
}
|
||||
|
||||
@Test func watchExecApprovalHydrateFetchesOnlyMissingIDs() {
|
||||
let idsToFetch = NodeAppModel._test_watchExecApprovalIDsNeedingFetch(
|
||||
candidateIDs: ["cached", "pending", "cached", "other", "", " pending "],
|
||||
@@ -1201,13 +1221,65 @@ private final class MockBootstrapNotificationCenter: NotificationCentering, @unc
|
||||
hasStoredOperatorToken: false))
|
||||
}
|
||||
|
||||
@Test @MainActor func successfulBootstrapOnboardingRequestsNotificationAuthorization() async {
|
||||
@Test @MainActor func operatorGatewayRequestedEventShowsNotificationGuidanceWhenNotificationsOff() async throws {
|
||||
let center = MockBootstrapNotificationCenter()
|
||||
center.status = .notDetermined
|
||||
let appModel = NodeAppModel(notificationCenter: center)
|
||||
appModel._test_resetExecApprovalNotificationGuidanceSuppression()
|
||||
defer { appModel._test_resetExecApprovalNotificationGuidanceSuppression() }
|
||||
|
||||
await appModel._test_handleSuccessfulBootstrapGatewayOnboarding()
|
||||
await appModel._test_handleOperatorGatewayServerEvent(EventFrame(
|
||||
type: "event",
|
||||
event: ExecApprovalNotificationBridge.requestedKind,
|
||||
payload: AnyCodable(["id": "approval-notifications-off"]),
|
||||
seq: nil,
|
||||
stateversion: nil))
|
||||
|
||||
#expect(center.requestAuthorizationCalls == 1)
|
||||
let prompt = try #require(appModel._test_pendingNotificationPermissionGuidancePrompt())
|
||||
#expect(prompt.approvalId == "approval-notifications-off")
|
||||
}
|
||||
|
||||
@Test @MainActor func suppressedOperatorGatewayRequestedEventDoesNotShowNotificationGuidance() async {
|
||||
let center = MockBootstrapNotificationCenter()
|
||||
center.status = .denied
|
||||
let appModel = NodeAppModel(notificationCenter: center)
|
||||
appModel._test_resetExecApprovalNotificationGuidanceSuppression()
|
||||
defer { appModel._test_resetExecApprovalNotificationGuidanceSuppression() }
|
||||
appModel.dismissNotificationPermissionGuidancePrompt(suppressFuture: true)
|
||||
|
||||
await appModel._test_handleOperatorGatewayServerEvent(EventFrame(
|
||||
type: "event",
|
||||
event: ExecApprovalNotificationBridge.requestedKind,
|
||||
payload: AnyCodable(["id": "approval-suppressed"]),
|
||||
seq: nil,
|
||||
stateversion: nil))
|
||||
|
||||
#expect(appModel._test_pendingNotificationPermissionGuidancePrompt() == nil)
|
||||
}
|
||||
|
||||
@Test @MainActor func operatorGatewayResolvedEventClearsNotificationGuidancePrompt() async throws {
|
||||
let center = MockBootstrapNotificationCenter()
|
||||
center.status = .denied
|
||||
let appModel = NodeAppModel(notificationCenter: center)
|
||||
appModel._test_resetExecApprovalNotificationGuidanceSuppression()
|
||||
defer { appModel._test_resetExecApprovalNotificationGuidanceSuppression() }
|
||||
|
||||
await appModel._test_handleOperatorGatewayServerEvent(EventFrame(
|
||||
type: "event",
|
||||
event: ExecApprovalNotificationBridge.requestedKind,
|
||||
payload: AnyCodable(["id": "approval-guidance-resolved"]),
|
||||
seq: nil,
|
||||
stateversion: nil))
|
||||
_ = try #require(appModel._test_pendingNotificationPermissionGuidancePrompt())
|
||||
|
||||
await appModel._test_handleOperatorGatewayServerEvent(EventFrame(
|
||||
type: "event",
|
||||
event: ExecApprovalNotificationBridge.resolvedKind,
|
||||
payload: AnyCodable(["id": "approval-guidance-resolved"]),
|
||||
seq: nil,
|
||||
stateversion: nil))
|
||||
|
||||
#expect(appModel._test_pendingNotificationPermissionGuidancePrompt() == nil)
|
||||
}
|
||||
|
||||
@Test func clearingBootstrapTokenStripsReconnectConfigEvenWithoutPersistence() throws {
|
||||
@@ -1269,6 +1341,78 @@ private final class MockBootstrapNotificationCenter: NotificationCentering, @unc
|
||||
#expect(res.error?.message.contains("CAMERA_DISABLED") == true)
|
||||
}
|
||||
|
||||
@Test @MainActor func systemNotifyReturnsUnavailableWhenNotificationsOff() async throws {
|
||||
let center = MockBootstrapNotificationCenter()
|
||||
center.status = .notDetermined
|
||||
let appModel = NodeAppModel(notificationCenter: center)
|
||||
let params = OpenClawSystemNotifyParams(title: "Approval", body: "Review request")
|
||||
let paramsData = try JSONEncoder().encode(params)
|
||||
let req = BridgeInvokeRequest(
|
||||
id: "notify-off",
|
||||
command: OpenClawSystemCommand.notify.rawValue,
|
||||
paramsJSON: String(decoding: paramsData, as: UTF8.self))
|
||||
|
||||
let res = await appModel._test_handleInvoke(req)
|
||||
|
||||
#expect(res.ok == false)
|
||||
#expect(res.error?.code == .unavailable)
|
||||
#expect(res.error?.message == "NOT_AUTHORIZED: notifications")
|
||||
#expect(center.addCalls == 0)
|
||||
}
|
||||
|
||||
@Test @MainActor func systemNotifySchedulesWhenNotificationsAreAlreadyAllowed() async throws {
|
||||
let center = MockBootstrapNotificationCenter()
|
||||
center.status = .authorized
|
||||
let appModel = NodeAppModel(notificationCenter: center)
|
||||
let params = OpenClawSystemNotifyParams(title: "Approval", body: "Review request")
|
||||
let paramsData = try JSONEncoder().encode(params)
|
||||
let req = BridgeInvokeRequest(
|
||||
id: "notify-on",
|
||||
command: OpenClawSystemCommand.notify.rawValue,
|
||||
paramsJSON: String(decoding: paramsData, as: UTF8.self))
|
||||
|
||||
let res = await appModel._test_handleInvoke(req)
|
||||
|
||||
#expect(res.ok)
|
||||
#expect(center.addCalls == 1)
|
||||
}
|
||||
|
||||
@Test @MainActor func chatPushWithoutSpeechReturnsUnavailableWhenNotificationsOff() async throws {
|
||||
let center = MockBootstrapNotificationCenter()
|
||||
center.status = .notDetermined
|
||||
let appModel = NodeAppModel(notificationCenter: center)
|
||||
let params = OpenClawChatPushParams(text: "Build finished", speak: false)
|
||||
let paramsData = try JSONEncoder().encode(params)
|
||||
let req = BridgeInvokeRequest(
|
||||
id: "chat-push-off",
|
||||
command: OpenClawChatCommand.push.rawValue,
|
||||
paramsJSON: String(decoding: paramsData, as: UTF8.self))
|
||||
|
||||
let res = await appModel._test_handleInvoke(req)
|
||||
|
||||
#expect(res.ok == false)
|
||||
#expect(res.error?.code == .unavailable)
|
||||
#expect(res.error?.message == "NOT_AUTHORIZED: notifications")
|
||||
#expect(center.addCalls == 0)
|
||||
}
|
||||
|
||||
@Test @MainActor func chatPushSchedulesWhenNotificationsAreAlreadyAllowed() async throws {
|
||||
let center = MockBootstrapNotificationCenter()
|
||||
center.status = .authorized
|
||||
let appModel = NodeAppModel(notificationCenter: center)
|
||||
let params = OpenClawChatPushParams(text: "Build finished", speak: false)
|
||||
let paramsData = try JSONEncoder().encode(params)
|
||||
let req = BridgeInvokeRequest(
|
||||
id: "chat-push-on",
|
||||
command: OpenClawChatCommand.push.rawValue,
|
||||
paramsJSON: String(decoding: paramsData, as: UTF8.self))
|
||||
|
||||
let res = await appModel._test_handleInvoke(req)
|
||||
|
||||
#expect(res.ok)
|
||||
#expect(center.addCalls == 1)
|
||||
}
|
||||
|
||||
@Test @MainActor func handleInvokeRejectsInvalidScreenFormat() async {
|
||||
let appModel = NodeAppModel()
|
||||
let params = OpenClawScreenRecordParams(format: "gif")
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import Foundation
|
||||
import Testing
|
||||
|
||||
@Suite struct RootTabsSourceGuardTests {
|
||||
@Test func hiddenSidebarRevealUsesDestinationHeaderWithoutReservedRail() throws {
|
||||
struct RootTabsSourceGuardTests {
|
||||
@Test func `hidden sidebar reveal uses destination header without reserved rail`() throws {
|
||||
let source = try String(contentsOf: Self.rootTabsSourceURL(), encoding: .utf8)
|
||||
let componentSource = try String(contentsOf: Self.proComponentsSourceURL(), encoding: .utf8)
|
||||
|
||||
@@ -38,7 +38,7 @@ import Testing
|
||||
#expect(!source.contains("shouldShowOverviewHeaderSidebarReveal"))
|
||||
}
|
||||
|
||||
@Test func iPadSplitUsesSlidingSidebarWhilePortraitKeepsDrawerOverlay() throws {
|
||||
@Test func `i pad split uses sliding sidebar while portrait keeps drawer overlay`() throws {
|
||||
let source = try String(contentsOf: Self.rootTabsSourceURL(), encoding: .utf8)
|
||||
let splitContent = try Self.extract(
|
||||
source,
|
||||
@@ -67,7 +67,7 @@ import Testing
|
||||
#expect(!drawerContent.contains("NavigationSplitView"))
|
||||
}
|
||||
|
||||
@Test func sidebarKeepsNavigationModelDestinationOnly() throws {
|
||||
@Test func `sidebar keeps navigation model destination only`() throws {
|
||||
let source = try String(contentsOf: Self.rootTabsSourceURL(), encoding: .utf8)
|
||||
let navigationSource = try String(contentsOf: Self.rootTabsNavigationSourceURL(), encoding: .utf8)
|
||||
let sidebarColumn = try Self.extract(
|
||||
@@ -114,7 +114,7 @@ import Testing
|
||||
#expect(navigationSource.contains("SidebarGroup(title: \"REFERENCE\", destinations: [.docs])"))
|
||||
}
|
||||
|
||||
@Test func sidebarRoutesUseDestinationHeadersInsteadOfRepeatedProductBranding() throws {
|
||||
@Test func `sidebar routes use destination headers instead of repeated product branding`() throws {
|
||||
let rootSource = try String(contentsOf: Self.rootTabsSourceURL(), encoding: .utf8)
|
||||
let agentOverviewSource = try String(contentsOf: Self.agentProTabOverviewSourceURL(), encoding: .utf8)
|
||||
let docsSource = try String(contentsOf: Self.docsSourceURL(), encoding: .utf8)
|
||||
@@ -148,7 +148,7 @@ import Testing
|
||||
#expect(!docsSource.contains("Text(\"OpenClaw Docs\")"))
|
||||
}
|
||||
|
||||
@Test func agentsDirectRouteKeepsSingleSidebarControl() throws {
|
||||
@Test func `agents direct route keeps single sidebar control`() throws {
|
||||
let source = try String(contentsOf: Self.agentProTabSourceURL(), encoding: .utf8)
|
||||
let destinationsSource = try String(contentsOf: Self.agentProTabDestinationsSourceURL(), encoding: .utf8)
|
||||
let nodesSource = try String(contentsOf: Self.agentProNodesDestinationSourceURL(), encoding: .utf8)
|
||||
@@ -165,7 +165,7 @@ import Testing
|
||||
#expect(dreamingSource.contains("OpenClawSidebarHeaderLeadingSlot(action: headerLeadingAction)"))
|
||||
}
|
||||
|
||||
@Test func routedHeadersUseSharedAdaptiveLayout() throws {
|
||||
@Test func `routed headers use shared adaptive layout`() throws {
|
||||
let componentsSource = try String(contentsOf: Self.proComponentsSourceURL(), encoding: .utf8)
|
||||
let featureChromeSource = try String(contentsOf: Self.iPadSidebarScreenChromeSourceURL(), encoding: .utf8)
|
||||
let docsSource = try String(contentsOf: Self.docsSourceURL(), encoding: .utf8)
|
||||
@@ -187,7 +187,7 @@ import Testing
|
||||
#expect(settingsSource.contains("OpenClawAdaptiveHeaderRow("))
|
||||
}
|
||||
|
||||
@Test func phoneHubKeepsDocsAsDestinationOnly() throws {
|
||||
@Test func `phone hub keeps docs as destination only`() throws {
|
||||
let source = try String(contentsOf: Self.phoneHubSourceURL(), encoding: .utf8)
|
||||
|
||||
#expect(source.contains("case .docs:"))
|
||||
@@ -198,7 +198,7 @@ import Testing
|
||||
#expect(!source.contains("https://docs.openclaw.ai"))
|
||||
}
|
||||
|
||||
@Test func rootShellPreviewMatrixCoversPhoneAndIPadStates() throws {
|
||||
@Test func `root shell preview matrix covers phone and I pad states`() throws {
|
||||
let source = try String(contentsOf: Self.rootTabsSourceURL(), encoding: .utf8)
|
||||
|
||||
#expect(source.contains("#Preview(\n \"Shell iPhone portrait\""))
|
||||
@@ -211,7 +211,7 @@ import Testing
|
||||
#expect(source.contains("#Preview(\n \"Shell iPad gateway error\""))
|
||||
}
|
||||
|
||||
@Test func sharedChatPreviewMatrixCoversConnectionStates() throws {
|
||||
@Test func `shared chat preview matrix covers connection states`() throws {
|
||||
let source = try String(contentsOf: Self.sharedChatPreviewSourceURL(), encoding: .utf8)
|
||||
|
||||
#expect(source.contains("#Preview(\"Chat connected\")"))
|
||||
@@ -226,7 +226,7 @@ import Testing
|
||||
#expect(source.contains("Gateway not connected. Check Tailscale and retry."))
|
||||
}
|
||||
|
||||
@Test func phoneHubKeepsContentAboveFloatingTabBar() throws {
|
||||
@Test func `phone hub keeps content above floating tab bar`() throws {
|
||||
let source = try String(contentsOf: Self.phoneHubSourceURL(), encoding: .utf8)
|
||||
|
||||
#expect(source.contains(".safeAreaPadding(.bottom, self.bottomScrollInset)"))
|
||||
@@ -235,7 +235,7 @@ import Testing
|
||||
#expect(!source.contains("bottomTabBarClearance"))
|
||||
}
|
||||
|
||||
@Test func phoneHubHeaderStaysTaskFirst() throws {
|
||||
@Test func `phone hub header stays task first`() throws {
|
||||
let source = try String(contentsOf: Self.phoneHubSourceURL(), encoding: .utf8)
|
||||
|
||||
#expect(source.contains("private var gatewayActionRow: some View"))
|
||||
@@ -253,7 +253,7 @@ import Testing
|
||||
#expect(!source.contains("private func metric(label:"))
|
||||
}
|
||||
|
||||
@Test func workboardUsesRealGatewayMethods() throws {
|
||||
@Test func `workboard uses real gateway methods`() throws {
|
||||
let source = try String(contentsOf: Self.iPadWorkboardScreenSourceURL(), encoding: .utf8)
|
||||
|
||||
#expect(source.contains("workboard.cards.list"))
|
||||
@@ -268,7 +268,7 @@ import Testing
|
||||
#expect(!source.contains("Multi-column queue control"))
|
||||
}
|
||||
|
||||
@Test func workboardCreateActionSurfacesUnavailableReasons() throws {
|
||||
@Test func `workboard create action surfaces unavailable reasons`() throws {
|
||||
let source = try String(contentsOf: Self.iPadWorkboardScreenSourceURL(), encoding: .utf8)
|
||||
let createFunction = try Self.extract(
|
||||
source,
|
||||
@@ -295,7 +295,7 @@ import Testing
|
||||
#expect(createFunction.contains("return true"))
|
||||
}
|
||||
|
||||
@Test func taskScopeControlsSendRealGatewayParams() throws {
|
||||
@Test func `task scope controls send real gateway params`() throws {
|
||||
let source = try Self.iPadTaskFeatureScreensSource()
|
||||
|
||||
#expect(source.contains("private var boardScopeMenu: some View"))
|
||||
@@ -314,7 +314,7 @@ import Testing
|
||||
"params: EmptyParams(),\n timeoutSeconds: 20)\n let response = try JSONDecoder().decode(IPadSkillProposalManifest.self"))
|
||||
}
|
||||
|
||||
@Test func compactTaskRowsKeepPhoneNativeActions() throws {
|
||||
@Test func `compact task rows keep phone native actions`() throws {
|
||||
let source = try Self.iPadTaskFeatureScreensSource()
|
||||
let compactControls = try Self.extract(
|
||||
source,
|
||||
@@ -348,7 +348,7 @@ import Testing
|
||||
#expect(compactControls.contains("Label(\"Dispatch\""))
|
||||
}
|
||||
|
||||
@Test func skillWorkshopUsesKanbanLanesOnWideIPad() throws {
|
||||
@Test func `skill workshop uses kanban lanes on wide I pad`() throws {
|
||||
let source = try String(contentsOf: Self.iPadSkillWorkshopScreenSourceURL(), encoding: .utf8)
|
||||
let previewSource = try String(contentsOf: Self.iPadSidebarFeaturePreviewsSourceURL(), encoding: .utf8)
|
||||
let content = try Self.extract(
|
||||
@@ -376,7 +376,7 @@ import Testing
|
||||
#expect(previewSource.contains("status: \"manual_QA\""))
|
||||
}
|
||||
|
||||
@Test func compactTaskRowsHavePopulatedPhonePreviews() throws {
|
||||
@Test func `compact task rows have populated phone previews`() throws {
|
||||
let source = try String(contentsOf: Self.iPadSidebarFeaturePreviewsSourceURL(), encoding: .utf8)
|
||||
|
||||
#expect(source.contains("#Preview(\"Workboard phone queue rows\")"))
|
||||
@@ -387,7 +387,7 @@ import Testing
|
||||
#expect(source.contains("IPadSkillWorkshopPreviewFixtures.proposals"))
|
||||
}
|
||||
|
||||
@Test func taskScreenPreviewMatricesCoverPrimaryStates() throws {
|
||||
@Test func `task screen preview matrices cover primary states`() throws {
|
||||
let source = try String(contentsOf: Self.iPadSidebarFeaturePreviewsSourceURL(), encoding: .utf8)
|
||||
|
||||
#expect(source.contains("#Preview(\"Workboard states\")"))
|
||||
@@ -412,7 +412,7 @@ import Testing
|
||||
#expect(source.contains("\"manual_QA\""))
|
||||
}
|
||||
|
||||
@Test func activityPreviewMatrixCoversConnectionStates() throws {
|
||||
@Test func `activity preview matrix covers connection states`() throws {
|
||||
let source = try String(contentsOf: Self.iPadSidebarFeaturePreviewsSourceURL(), encoding: .utf8)
|
||||
|
||||
#expect(source.contains("#Preview(\"Activity states\")"))
|
||||
@@ -426,7 +426,7 @@ import Testing
|
||||
#expect(source.contains("title: \"Loading sessions\""))
|
||||
}
|
||||
|
||||
@Test func routedFeatureScreensReuseSharedProComponents() throws {
|
||||
@Test func `routed feature screens reuse shared pro components`() throws {
|
||||
let source = try Self.iPadTaskFeatureScreensSource()
|
||||
let componentsSource = try String(contentsOf: Self.proComponentsSourceURL(), encoding: .utf8)
|
||||
let channelsSource = try String(contentsOf: Self.channelsSourceURL(), encoding: .utf8)
|
||||
@@ -445,20 +445,22 @@ import Testing
|
||||
#expect(componentsSource.contains("struct ProStatusRow"))
|
||||
}
|
||||
|
||||
@Test func activityScreenStaysSplitFromTaskFeatureScreens() throws {
|
||||
@Test func `activity screen stays split from task feature screens`() throws {
|
||||
let taskSource = try Self.iPadTaskFeatureScreensSource()
|
||||
let activitySource = try String(contentsOf: Self.iPadActivityScreenSourceURL(), encoding: .utf8)
|
||||
let appModelSource = try String(contentsOf: Self.nodeAppModelSourceURL(), encoding: .utf8)
|
||||
let projectSource = try String(contentsOf: Self.xcodeProjectSourceURL(), encoding: .utf8)
|
||||
|
||||
#expect(activitySource.contains("struct IPadActivityScreen: View"))
|
||||
#expect(activitySource.contains("IOSGatewayChatTransport(gateway: self.appModel.operatorSession)"))
|
||||
#expect(activitySource.contains("self.appModel.makeChatTransport()"))
|
||||
#expect(appModelSource.contains("return IOSGatewayChatTransport(gateway: self.operatorSession)"))
|
||||
#expect(activitySource.contains("IPadSidebarScreenChrome("))
|
||||
#expect(!taskSource.contains("struct IPadActivityScreen"))
|
||||
#expect(!taskSource.contains("import OpenClawChatUI"))
|
||||
#expect(projectSource.contains("IPadActivityScreen.swift in Sources"))
|
||||
}
|
||||
|
||||
@Test func routedFeatureChromeStaysSplitFromTaskFeatureScreens() throws {
|
||||
@Test func `routed feature chrome stays split from task feature screens`() throws {
|
||||
let taskSource = try Self.iPadTaskFeatureScreensSource()
|
||||
let chromeSource = try String(contentsOf: Self.iPadSidebarScreenChromeSourceURL(), encoding: .utf8)
|
||||
let projectSource = try String(contentsOf: Self.xcodeProjectSourceURL(), encoding: .utf8)
|
||||
@@ -470,7 +472,7 @@ import Testing
|
||||
#expect(projectSource.contains("IPadSidebarScreenChrome.swift in Sources"))
|
||||
}
|
||||
|
||||
@Test func routedFeatureChromeKeepsGatewayPillActionable() throws {
|
||||
@Test func `routed feature chrome keeps gateway pill actionable`() throws {
|
||||
let chromeSource = try String(contentsOf: Self.iPadSidebarScreenChromeSourceURL(), encoding: .utf8)
|
||||
let featureSource = try Self.iPadTaskFeatureScreensSource()
|
||||
let rootSource = try String(contentsOf: Self.rootTabsSourceURL(), encoding: .utf8)
|
||||
@@ -486,14 +488,18 @@ import Testing
|
||||
.count == 1)
|
||||
}
|
||||
|
||||
@Test func routedGatewayPillsOpenGatewaySettings() throws {
|
||||
@Test func `routed gateway pills open gateway settings`() throws {
|
||||
let rootSource = try String(contentsOf: Self.rootTabsSourceURL(), encoding: .utf8)
|
||||
let agentSource = try String(contentsOf: Self.agentProTabSourceURL(), encoding: .utf8)
|
||||
let agentOverviewSource = try String(contentsOf: Self.agentProTabOverviewSourceURL(), encoding: .utf8)
|
||||
let overviewSource = try String(contentsOf: Self.commandCenterSourceURL(), encoding: .utf8)
|
||||
let chatSource = try String(contentsOf: Self.chatProTabSourceURL(), encoding: .utf8)
|
||||
let docsSource = try String(contentsOf: Self.docsSourceURL(), encoding: .utf8)
|
||||
let settingsTabSource = try String(contentsOf: Self.settingsProTabSourceURL(), encoding: .utf8)
|
||||
let settingsSource = try String(contentsOf: Self.settingsProTabSectionsSourceURL(), encoding: .utf8)
|
||||
let notificationGuidanceSource = try String(
|
||||
contentsOf: Self.notificationPermissionGuidanceDialogSourceURL(),
|
||||
encoding: .utf8)
|
||||
|
||||
#expect(rootSource.matches(of: /openSettings: \{ self\.selectSidebarDestination\(\.gateway\) \}/).count >= 2)
|
||||
#expect(rootSource.matches(of: /gatewayAction: \{ self\.selectSidebarDestination\(\.gateway\) \}/).count == 1)
|
||||
@@ -512,15 +518,39 @@ import Testing
|
||||
#expect(docsSource.contains("let gatewayAction: (() -> Void)?"))
|
||||
#expect(settingsSource.contains("NavigationLink(value: SettingsRoute.gateway)"))
|
||||
#expect(rootSource.contains("case .settings:"))
|
||||
#expect(rootSource.contains("SettingsProTab(headerLeadingAction: self.sidebarHeaderLeadingAction)"))
|
||||
#expect(rootSource.contains("directRoute: self.selectedSidebarDestination.settingsRoute ?? .gateway"))
|
||||
#expect(rootSource.contains("SettingsProTab(initialRoute: self.selectedSidebarDestination.settingsRoute)"))
|
||||
#expect(rootSource
|
||||
.matches(of: /SettingsProTab\(\s*headerLeadingAction: self\.sidebarHeaderLeadingAction,/)
|
||||
.count >= 1)
|
||||
#expect(rootSource
|
||||
.contains(
|
||||
"directRoute: self.selectedSettingsRoute ?? self.selectedSidebarDestination.settingsRoute ?? .gateway"))
|
||||
#expect(rootSource.matches(of: /SettingsProTab\(\s*initialRoute: self\.selectedSettingsRoute,/).count == 1)
|
||||
#expect(rootSource.contains(".id(self.settingsTabViewID)"))
|
||||
#expect(rootSource.contains("@State private var selectedSettingsRouteRequestID: Int = 0"))
|
||||
#expect(rootSource.contains("self.selectedSettingsRouteRequestID &+= 1"))
|
||||
#expect(rootSource.contains("@State private var suppressedExecApprovalPromptIDForNotificationSettings"))
|
||||
#expect(rootSource.contains("private var activeExecApprovalPromptSuppressionID: String?"))
|
||||
#expect(rootSource.contains("suppressedApprovalID: self.activeExecApprovalPromptSuppressionID"))
|
||||
#expect(rootSource.contains("if destination.settingsRoute != .notifications"))
|
||||
#expect(rootSource.contains("if route != .notifications"))
|
||||
#expect(rootSource.contains("if route == nil"))
|
||||
#expect(rootSource.contains("self.selectedSettingsRoute = nil"))
|
||||
#expect(rootSource.contains("self.selectedSidebarDestination = .settings"))
|
||||
#expect(rootSource.contains("self.suppressedExecApprovalPromptIDForNotificationSettings = approvalId"))
|
||||
#expect(rootSource.contains("onRouteChange: self.handleSettingsRouteChange"))
|
||||
#expect(rootSource.contains("private func handleSettingsRouteChange(_ route: SettingsRoute?)"))
|
||||
#expect(settingsTabSource.contains("let onRouteChange: ((SettingsRoute?) -> Void)?"))
|
||||
#expect(settingsTabSource.contains("self.onRouteChange?(self.navigationPath.last)"))
|
||||
#expect(notificationGuidanceSource.contains("onSuppressFuture"))
|
||||
#expect(notificationGuidanceSource.contains("suppressFuture: true"))
|
||||
#expect(notificationGuidanceSource.contains("Text(\"Don't show again\")"))
|
||||
#expect(rootSource.contains("private func selectSettingsRoute(_ route: SettingsRoute)"))
|
||||
#expect(settingsSource.contains("title: \"Channels / Integrations\""))
|
||||
#expect(settingsSource.contains("route: .channels"))
|
||||
#expect(docsSource.contains(".accessibilityHint(\"Opens Settings / Gateway\")"))
|
||||
}
|
||||
|
||||
@Test func gatewaySettingsKeepsPairingTrustDiagnosticsAndTailscaleActions() throws {
|
||||
@Test func `gateway settings keeps pairing trust diagnostics and tailscale actions`() throws {
|
||||
let settingsSource = try String(contentsOf: Self.settingsProTabSourceURL(), encoding: .utf8)
|
||||
let sectionsSource = try String(contentsOf: Self.settingsProTabSectionsSourceURL(), encoding: .utf8)
|
||||
let actionsSource = try String(contentsOf: Self.settingsProTabActionsSourceURL(), encoding: .utf8)
|
||||
@@ -566,7 +596,7 @@ import Testing
|
||||
#expect(controllerSource.contains("trustRotatedGatewayCertificate(from problem: GatewayConnectionProblem)"))
|
||||
}
|
||||
|
||||
@Test func gatewaySettingsPreviewMatrixCoversPrimaryStates() throws {
|
||||
@Test func `gateway settings preview matrix covers primary states`() throws {
|
||||
let supportSource = try String(contentsOf: Self.settingsProTabSupportSourceURL(), encoding: .utf8)
|
||||
|
||||
#expect(supportSource.contains("#Preview(\"Gateway settings states\")"))
|
||||
@@ -585,12 +615,15 @@ import Testing
|
||||
#expect(supportSource.contains("self.previewButton(\"Diagnose\""))
|
||||
}
|
||||
|
||||
@Test func nativeChatUsesGatewayTransport() throws {
|
||||
@Test func `native chat uses gateway transport`() throws {
|
||||
let chatSource = try String(contentsOf: Self.chatProTabSourceURL(), encoding: .utf8)
|
||||
let channelsSource = try String(contentsOf: Self.channelsSourceURL(), encoding: .utf8)
|
||||
let settingsSectionsSource = try String(contentsOf: Self.settingsProTabSectionsSourceURL(), encoding: .utf8)
|
||||
let appModelSource = try String(contentsOf: Self.nodeAppModelSourceURL(), encoding: .utf8)
|
||||
|
||||
#expect(chatSource.contains("IOSGatewayChatTransport(gateway: self.appModel.operatorSession)"))
|
||||
#expect(channelsSource.contains("Message routing and external channel clients."))
|
||||
#expect(chatSource.matches(of: /self\.appModel\.makeChatTransport\(\)/).count == 2)
|
||||
#expect(appModelSource.contains("return IOSGatewayChatTransport(gateway: self.operatorSession)"))
|
||||
#expect(settingsSectionsSource.contains("Message routing and external channel clients."))
|
||||
#expect(channelsSource.contains("\"clickclack\": SettingsChannelFallbackMetadata"))
|
||||
#expect(channelsSource.contains("label: \"ClickClack\""))
|
||||
#expect(channelsSource.contains("Self-hosted chat bot routing."))
|
||||
@@ -603,6 +636,13 @@ import Testing
|
||||
.appendingPathComponent("Sources/RootTabs.swift")
|
||||
}
|
||||
|
||||
private static func nodeAppModelSourceURL() -> URL {
|
||||
URL(fileURLWithPath: #filePath)
|
||||
.deletingLastPathComponent()
|
||||
.deletingLastPathComponent()
|
||||
.appendingPathComponent("Sources/Model/NodeAppModel.swift")
|
||||
}
|
||||
|
||||
private static func phoneHubSourceURL() -> URL {
|
||||
URL(fileURLWithPath: #filePath)
|
||||
.deletingLastPathComponent()
|
||||
@@ -746,6 +786,13 @@ import Testing
|
||||
.appendingPathComponent("Sources/Design/SettingsProTab.swift")
|
||||
}
|
||||
|
||||
private static func notificationPermissionGuidanceDialogSourceURL() -> URL {
|
||||
URL(fileURLWithPath: #filePath)
|
||||
.deletingLastPathComponent()
|
||||
.deletingLastPathComponent()
|
||||
.appendingPathComponent("Sources/Gateway/NotificationPermissionGuidanceDialog.swift")
|
||||
}
|
||||
|
||||
private static func settingsProTabActionsSourceURL() -> URL {
|
||||
URL(fileURLWithPath: #filePath)
|
||||
.deletingLastPathComponent()
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
OpenClaw is now available on iPhone.
|
||||
Maintenance update for the current OpenClaw release.
|
||||
|
||||
Connect to your OpenClaw Gateway to chat with your assistant, use realtime Talk mode, review approvals, share content from iOS, and bring device capabilities like camera, location, screen, and notifications into your private automation workflows.
|
||||
- Added Apple Watch controls for common agent actions.
|
||||
- Improved Gateway setup, notification settings, and share-extension identity handling.
|
||||
- Updated the Watch app integration for current Xcode compatibility.
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"version": "2026.6.2"
|
||||
"version": "2026.6.9"
|
||||
}
|
||||
|
||||
@@ -15,9 +15,9 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>2026.6.2</string>
|
||||
<string>2026.6.9</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>2026060200</string>
|
||||
<string>2026060900</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>OpenClaw</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
|
||||
@@ -69,6 +69,17 @@
|
||||
"fileName"
|
||||
]
|
||||
},
|
||||
"api": {
|
||||
"emoji": "🌐",
|
||||
"title": "API",
|
||||
"detailKeys": [
|
||||
"url",
|
||||
"endpoint",
|
||||
"path",
|
||||
"method",
|
||||
"name"
|
||||
]
|
||||
},
|
||||
"browser": {
|
||||
"emoji": "🌐",
|
||||
"title": "Browser",
|
||||
|
||||
@@ -6592,6 +6592,7 @@ public struct ExecApprovalRequestParams: Codable, Sendable {
|
||||
public let turnsourceto: AnyCodable?
|
||||
public let turnsourceaccountid: AnyCodable?
|
||||
public let turnsourcethreadid: AnyCodable?
|
||||
public let approvalreviewerdeviceids: [String]?
|
||||
public let requiredeliveryroute: Bool?
|
||||
public let suppressdelivery: Bool?
|
||||
public let timeoutms: Int?
|
||||
@@ -6618,6 +6619,7 @@ public struct ExecApprovalRequestParams: Codable, Sendable {
|
||||
turnsourceto: AnyCodable?,
|
||||
turnsourceaccountid: AnyCodable?,
|
||||
turnsourcethreadid: AnyCodable?,
|
||||
approvalreviewerdeviceids: [String]?,
|
||||
requiredeliveryroute: Bool? = nil,
|
||||
suppressdelivery: Bool? = nil,
|
||||
timeoutms: Int?,
|
||||
@@ -6643,6 +6645,7 @@ public struct ExecApprovalRequestParams: Codable, Sendable {
|
||||
self.turnsourceto = turnsourceto
|
||||
self.turnsourceaccountid = turnsourceaccountid
|
||||
self.turnsourcethreadid = turnsourcethreadid
|
||||
self.approvalreviewerdeviceids = approvalreviewerdeviceids
|
||||
self.requiredeliveryroute = requiredeliveryroute
|
||||
self.suppressdelivery = suppressdelivery
|
||||
self.timeoutms = timeoutms
|
||||
@@ -6670,6 +6673,7 @@ public struct ExecApprovalRequestParams: Codable, Sendable {
|
||||
case turnsourceto = "turnSourceTo"
|
||||
case turnsourceaccountid = "turnSourceAccountId"
|
||||
case turnsourcethreadid = "turnSourceThreadId"
|
||||
case approvalreviewerdeviceids = "approvalReviewerDeviceIds"
|
||||
case requiredeliveryroute = "requireDeliveryRoute"
|
||||
case suppressdelivery = "suppressDelivery"
|
||||
case timeoutms = "timeoutMs"
|
||||
@@ -7273,7 +7277,9 @@ public struct ChatSendParams: Codable, Sendable {
|
||||
public let sessionid: String?
|
||||
public let message: String
|
||||
public let thinking: String?
|
||||
public let fastmode: Bool?
|
||||
public let fastmodevalue: AnyCodable?
|
||||
public var fastmode: Bool? { fastmodevalue?.value as? Bool }
|
||||
public let fastautoonseconds: Int?
|
||||
public let deliver: Bool?
|
||||
public let originatingchannel: String?
|
||||
public let originatingto: String?
|
||||
@@ -7286,6 +7292,46 @@ public struct ChatSendParams: Codable, Sendable {
|
||||
public let suppresscommandinterpretation: Bool?
|
||||
public let idempotencykey: String
|
||||
|
||||
public init(
|
||||
sessionkey: String,
|
||||
agentid: String? = nil,
|
||||
sessionid: String?,
|
||||
message: String,
|
||||
thinking: String?,
|
||||
fastmodevalue: AnyCodable?,
|
||||
fastautoonseconds: Int?,
|
||||
deliver: Bool?,
|
||||
originatingchannel: String?,
|
||||
originatingto: String?,
|
||||
originatingaccountid: String?,
|
||||
originatingthreadid: String?,
|
||||
attachments: [AnyCodable]?,
|
||||
timeoutms: Int?,
|
||||
systeminputprovenance: [String: AnyCodable]?,
|
||||
systemprovenancereceipt: String?,
|
||||
suppresscommandinterpretation: Bool?,
|
||||
idempotencykey: String)
|
||||
{
|
||||
self.sessionkey = sessionkey
|
||||
self.agentid = agentid
|
||||
self.sessionid = sessionid
|
||||
self.message = message
|
||||
self.thinking = thinking
|
||||
self.fastmodevalue = fastmodevalue
|
||||
self.fastautoonseconds = fastautoonseconds
|
||||
self.deliver = deliver
|
||||
self.originatingchannel = originatingchannel
|
||||
self.originatingto = originatingto
|
||||
self.originatingaccountid = originatingaccountid
|
||||
self.originatingthreadid = originatingthreadid
|
||||
self.attachments = attachments
|
||||
self.timeoutms = timeoutms
|
||||
self.systeminputprovenance = systeminputprovenance
|
||||
self.systemprovenancereceipt = systemprovenancereceipt
|
||||
self.suppresscommandinterpretation = suppresscommandinterpretation
|
||||
self.idempotencykey = idempotencykey
|
||||
}
|
||||
|
||||
public init(
|
||||
sessionkey: String,
|
||||
agentid: String? = nil,
|
||||
@@ -7305,23 +7351,25 @@ public struct ChatSendParams: Codable, Sendable {
|
||||
suppresscommandinterpretation: Bool?,
|
||||
idempotencykey: String)
|
||||
{
|
||||
self.sessionkey = sessionkey
|
||||
self.agentid = agentid
|
||||
self.sessionid = sessionid
|
||||
self.message = message
|
||||
self.thinking = thinking
|
||||
self.fastmode = fastmode
|
||||
self.deliver = deliver
|
||||
self.originatingchannel = originatingchannel
|
||||
self.originatingto = originatingto
|
||||
self.originatingaccountid = originatingaccountid
|
||||
self.originatingthreadid = originatingthreadid
|
||||
self.attachments = attachments
|
||||
self.timeoutms = timeoutms
|
||||
self.systeminputprovenance = systeminputprovenance
|
||||
self.systemprovenancereceipt = systemprovenancereceipt
|
||||
self.suppresscommandinterpretation = suppresscommandinterpretation
|
||||
self.idempotencykey = idempotencykey
|
||||
self.init(
|
||||
sessionkey: sessionkey,
|
||||
agentid: agentid,
|
||||
sessionid: sessionid,
|
||||
message: message,
|
||||
thinking: thinking,
|
||||
fastmodevalue: fastmode.map { AnyCodable($0) },
|
||||
fastautoonseconds: nil,
|
||||
deliver: deliver,
|
||||
originatingchannel: originatingchannel,
|
||||
originatingto: originatingto,
|
||||
originatingaccountid: originatingaccountid,
|
||||
originatingthreadid: originatingthreadid,
|
||||
attachments: attachments,
|
||||
timeoutms: timeoutms,
|
||||
systeminputprovenance: systeminputprovenance,
|
||||
systemprovenancereceipt: systemprovenancereceipt,
|
||||
suppresscommandinterpretation: suppresscommandinterpretation,
|
||||
idempotencykey: idempotencykey)
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
@@ -7330,7 +7378,8 @@ public struct ChatSendParams: Codable, Sendable {
|
||||
case sessionid = "sessionId"
|
||||
case message
|
||||
case thinking
|
||||
case fastmode = "fastMode"
|
||||
case fastmodevalue = "fastMode"
|
||||
case fastautoonseconds = "fastAutoOnSeconds"
|
||||
case deliver
|
||||
case originatingchannel = "originatingChannel"
|
||||
case originatingto = "originatingTo"
|
||||
|
||||
@@ -128,18 +128,9 @@ const config = {
|
||||
"**/*.test-utils.ts",
|
||||
"test/helpers/live-image-probe.ts",
|
||||
"src/secrets/credential-matrix.ts",
|
||||
"src/agents/claude-cli-runner.ts",
|
||||
"src/agents/agent-auth-json.ts",
|
||||
"src/agents/tool-policy.conformance.ts",
|
||||
"src/auto-reply/reply/audio-tags.ts",
|
||||
"src/gateway/live-tool-probe-utils.ts",
|
||||
"src/gateway/server.auth.shared.ts",
|
||||
"src/shared/text/assistant-visible-text.ts",
|
||||
bundledPluginFile("telegram", "src/bot/reply-threading.ts"),
|
||||
bundledPluginFile("telegram", "src/draft-chunking.ts"),
|
||||
bundledPluginFile("msteams", "src/conversation-store-memory.ts"),
|
||||
bundledPluginFile("msteams", "src/polls-store-memory.ts"),
|
||||
bundledPluginFile("voice-call", "src/providers/index.ts"),
|
||||
],
|
||||
ignore: ["packages/*/dist/**"],
|
||||
workspaces: {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
ac06b6c20a93a8543ec1bd3748ef4f7bdae5006839dd93b3fff874d0da4244aa config-baseline.json
|
||||
e7965566fdaedef445bcd562141f4f3ea1a499cf8ea5956418af7c98049bf242 config-baseline.core.json
|
||||
2d735389858305509528e74329b6f8c65d311e1471c3b4e91dc17aaab8e63a80 config-baseline.channel.json
|
||||
0039da0cf2ba2845b37db52c4cf3a0f25e367cf3d2d507c5d6f8a5e5bdfdc4d4 config-baseline.plugin.json
|
||||
3ac3be8b7e201eb577854806a9806ba90acbfb2616e14b3ffd1169f188620303 config-baseline.json
|
||||
2923c1120c0369aeca6646cd67f7264590c6a1f4e5bc3157a04d7661324c6868 config-baseline.core.json
|
||||
769899651e2769833ae7e9c8fbf402e55f3d5e32da6bfe21a9659cc35d1f07bb config-baseline.channel.json
|
||||
d2e2114f1cd43dc894fe1a4836677b42a2a5af825537d6c4a932da832d58a590 config-baseline.plugin.json
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
b29fdf14b8b6bd3f8f61699754bd3269e54a6452f0430784f0e42c0bbf6d2be3 plugin-sdk-api-baseline.json
|
||||
d3a9400a6eb7b9e22ff7264dfe5afdda5bd694a6f8fa6427d146a4c4b1506d3e plugin-sdk-api-baseline.jsonl
|
||||
172fe4e143964c0a20525428ff3e6c7631856a7d51c6ad48959a35c72363a410 plugin-sdk-api-baseline.json
|
||||
a4c18ea9f0b0d2c22183bf8c082e757b7f9852b4c518c8b8cb62a21a9dd766e9 plugin-sdk-api-baseline.jsonl
|
||||
|
||||
@@ -183,7 +183,7 @@ Model-selection precedence for isolated jobs is:
|
||||
3. User-selected stored cron session model override
|
||||
4. Agent/default model selection
|
||||
|
||||
Fast mode follows the resolved live selection too. If the selected model config has `params.fastMode`, isolated cron uses that by default. A stored session `fastMode` override still wins over config in either direction.
|
||||
Fast mode follows the resolved live selection too. If the selected model config has `params.fastMode`, isolated cron uses that by default. A stored session `fastMode` override still wins over config in either direction. Auto mode uses the selected model's `params.fastAutoOnSeconds` cutoff when present, defaulting to 60 seconds.
|
||||
|
||||
If an isolated run hits a live model-switch handoff, cron retries with the switched provider/model and persists that live selection for the active run before retrying. When the switch also carries a new auth profile, cron persists that auth profile override for the active run too. Retries are bounded: after the initial attempt plus 2 switch retries, cron aborts instead of looping forever.
|
||||
|
||||
|
||||
@@ -63,9 +63,9 @@ If `plugins.allow` is a non-empty restrictive list, explicitly selecting
|
||||
ClickClack in channel setup or running `openclaw plugins enable clickclack`
|
||||
appends `clickclack` to that list. Onboarding installation uses the same
|
||||
explicit-selection behavior. These paths do not override `plugins.deny` or a
|
||||
global `plugins.enabled: false` setting. Direct `openclaw plugins install
|
||||
clickclack` follows the normal plugin-install policy and also records ClickClack
|
||||
in an existing allowlist.
|
||||
global `plugins.enabled: false` setting. Direct
|
||||
`openclaw plugins install @openclaw/clickclack` follows the normal
|
||||
plugin-install policy and also records ClickClack in an existing allowlist.
|
||||
|
||||
## Multiple bots
|
||||
|
||||
|
||||
@@ -94,28 +94,28 @@ Use this checklist when you already know your old BlueBubbles config and want th
|
||||
|
||||
iMessage and BlueBubbles share a lot of channel-level config. The keys that change are mostly transport (REST server vs local CLI). Behavior keys (`dmPolicy`, `groupPolicy`, `allowFrom`, etc.) keep the same meaning.
|
||||
|
||||
| BlueBubbles | bundled iMessage | Notes |
|
||||
| ---------------------------------------------------------- | ----------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `channels.bluebubbles.enabled` | `channels.imessage.enabled` | Same semantics. |
|
||||
| `channels.bluebubbles.serverUrl` | _(removed)_ | No REST server — the plugin spawns `imsg rpc` over stdio. |
|
||||
| `channels.bluebubbles.password` | _(removed)_ | No webhook authentication needed. |
|
||||
| _(implicit)_ | `channels.imessage.cliPath` | Path to `imsg` (default `imsg`); use a wrapper script for SSH. |
|
||||
| _(implicit)_ | `channels.imessage.dbPath` | Optional Messages.app `chat.db` override; auto-detected when omitted. |
|
||||
| _(implicit)_ | `channels.imessage.remoteHost` | `host` or `user@host` — only needed when `cliPath` is an SSH wrapper and you want SCP attachment fetches. |
|
||||
| `channels.bluebubbles.dmPolicy` | `channels.imessage.dmPolicy` | Same values (`pairing` / `allowlist` / `open` / `disabled`). |
|
||||
| `channels.bluebubbles.allowFrom` | `channels.imessage.allowFrom` | Pairing approvals carry over by handle, not by token. |
|
||||
| `channels.bluebubbles.groupPolicy` | `channels.imessage.groupPolicy` | Same values (`allowlist` / `open` / `disabled`). |
|
||||
| `channels.bluebubbles.groupAllowFrom` | `channels.imessage.groupAllowFrom` | Same. |
|
||||
| `channels.bluebubbles.groups` | `channels.imessage.groups` | **Copy this verbatim, including any `groups: { "*": { ... } }` wildcard entry.** Per-group `requireMention`, `tools`, `toolsBySender` carry over. With `groupPolicy: "allowlist"`, an empty or missing `groups` block silently drops every group message — see "Group registry footgun" below. |
|
||||
| `channels.bluebubbles.sendReadReceipts` | `channels.imessage.sendReadReceipts` | Default `true`. With the bundled plugin this only fires when the private API probe is up. |
|
||||
| `channels.bluebubbles.includeAttachments` | `channels.imessage.includeAttachments` | Same shape, **same off-by-default**. If you had attachments flowing on BlueBubbles you must re-set this explicitly on the iMessage block — it does not carry over implicitly, and inbound photos/media will be silently dropped with no `Inbound message` log line until you do. |
|
||||
| `channels.bluebubbles.attachmentRoots` | `channels.imessage.attachmentRoots` | Local roots; same wildcard rules. |
|
||||
| _(N/A)_ | `channels.imessage.remoteAttachmentRoots` | Only used when `remoteHost` is set for SCP fetches. |
|
||||
| `channels.bluebubbles.mediaMaxMb` | `channels.imessage.mediaMaxMb` | Default 16 MB on iMessage (BlueBubbles default was 8 MB). Set explicitly if you want to keep the lower cap. |
|
||||
| `channels.bluebubbles.textChunkLimit` | `channels.imessage.textChunkLimit` | Default 4000 on both. |
|
||||
| `channels.bluebubbles.coalesceSameSenderDms` | `channels.imessage.coalesceSameSenderDms` | Same opt-in. DM-only — group chats keep instant per-message dispatch on both channels. Widens the default inbound debounce to 2500 ms when enabled without an explicit `messages.inbound.byChannel.imessage`. See [iMessage docs § Coalescing split-send DMs](/channels/imessage#coalescing-split-send-dms-command--url-in-one-composition). |
|
||||
| `channels.bluebubbles.enrichGroupParticipantsFromContacts` | _(N/A)_ | iMessage already reads sender display names from `chat.db`. |
|
||||
| `channels.bluebubbles.actions.*` | `channels.imessage.actions.*` | Per-action toggles: `reactions`, `edit`, `unsend`, `reply`, `sendWithEffect`, `renameGroup`, `setGroupIcon`, `addParticipant`, `removeParticipant`, `leaveGroup`, `sendAttachment`. |
|
||||
| BlueBubbles | bundled iMessage | Notes |
|
||||
| ---------------------------------------------------------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `channels.bluebubbles.enabled` | `channels.imessage.enabled` | Same semantics. |
|
||||
| `channels.bluebubbles.serverUrl` | _(removed)_ | No REST server — the plugin spawns `imsg rpc` over stdio. |
|
||||
| `channels.bluebubbles.password` | _(removed)_ | No webhook authentication needed. |
|
||||
| _(implicit)_ | `channels.imessage.cliPath` | Path to `imsg` (default `imsg`); use a wrapper script for SSH. |
|
||||
| _(implicit)_ | `channels.imessage.dbPath` | Optional Messages.app `chat.db` override; auto-detected when omitted. |
|
||||
| _(implicit)_ | `channels.imessage.remoteHost` | `host` or `user@host` — only needed when `cliPath` is an SSH wrapper and you want SCP attachment fetches. |
|
||||
| `channels.bluebubbles.dmPolicy` | `channels.imessage.dmPolicy` | Same values (`pairing` / `allowlist` / `open` / `disabled`). |
|
||||
| `channels.bluebubbles.allowFrom` | `channels.imessage.allowFrom` | Pairing approvals carry over by handle, not by token. |
|
||||
| `channels.bluebubbles.groupPolicy` | `channels.imessage.groupPolicy` | Same values (`allowlist` / `open` / `disabled`). |
|
||||
| `channels.bluebubbles.groupAllowFrom` | `channels.imessage.groupAllowFrom` | Same. |
|
||||
| `channels.bluebubbles.groups` | `channels.imessage.groups` | **Copy this verbatim, including any `groups: { "*": { ... } }` wildcard entry.** Per-group `requireMention`, `tools`, `toolsBySender` carry over. With `groupPolicy: "allowlist"`, an empty or missing `groups` block silently drops every group message — see "Group registry footgun" below. |
|
||||
| `channels.bluebubbles.sendReadReceipts` | `channels.imessage.sendReadReceipts` | Default `true`. With the bundled plugin this only fires when the private API probe is up. |
|
||||
| `channels.bluebubbles.includeAttachments` | `channels.imessage.includeAttachments` | Same shape, **same off-by-default**. If you had attachments flowing on BlueBubbles you must re-set this explicitly on the iMessage block — it does not carry over implicitly, and inbound photos/media will be silently dropped with no `Inbound message` log line until you do. |
|
||||
| `channels.bluebubbles.attachmentRoots` | `channels.imessage.attachmentRoots` | Local roots; same wildcard rules. |
|
||||
| _(N/A)_ | `channels.imessage.remoteAttachmentRoots` | Only used when `remoteHost` is set for SCP fetches. |
|
||||
| `channels.bluebubbles.mediaMaxMb` | `channels.imessage.mediaMaxMb` | Default 16 MB on iMessage (BlueBubbles default was 8 MB). Set explicitly if you want to keep the lower cap. |
|
||||
| `channels.bluebubbles.textChunkLimit` | `channels.imessage.textChunkLimit` | Default 4000 on both. |
|
||||
| `channels.bluebubbles.coalesceSameSenderDms` | `channels.imessage.coalesceSameSenderDms` | Same opt-in. DM-only — group chats keep instant per-message dispatch on both channels. Widens the default inbound debounce to 7000 ms when enabled without an explicit `messages.inbound.byChannel.imessage` or global `messages.inbound.debounceMs`. See [iMessage docs § Coalescing split-send DMs](/channels/imessage#coalescing-split-send-dms-command--url-in-one-composition). |
|
||||
| `channels.bluebubbles.enrichGroupParticipantsFromContacts` | _(N/A)_ | iMessage already reads sender display names from `chat.db`. |
|
||||
| `channels.bluebubbles.actions.*` | `channels.imessage.actions.*` | Per-action toggles: `reactions`, `edit`, `unsend`, `reply`, `sendWithEffect`, `renameGroup`, `setGroupIcon`, `addParticipant`, `removeParticipant`, `leaveGroup`, `sendAttachment`. |
|
||||
|
||||
Multi-account configs (`channels.bluebubbles.accounts.*`) translate one-to-one to `channels.imessage.accounts.*`.
|
||||
|
||||
|
||||
@@ -681,7 +681,7 @@ The two rows arrive at OpenClaw ~0.8-2.0 s apart on most setups. Without coalesc
|
||||
}
|
||||
```
|
||||
|
||||
With the flag on and no explicit `messages.inbound.byChannel.imessage`, the debounce window widens to **2500 ms** (the legacy default is 0 ms — no debouncing). The wider window is required because Apple's split-send cadence of 0.8-2.0 s does not fit in a tighter default.
|
||||
With the flag on and no explicit `messages.inbound.byChannel.imessage` or global `messages.inbound.debounceMs`, the debounce window widens to **7000 ms** (the legacy default is 0 ms — no debouncing). The wider window is required because Apple's URL-preview split-send cadence can stretch to several seconds while Messages.app emits the preview row.
|
||||
|
||||
To tune the window yourself:
|
||||
|
||||
@@ -690,10 +690,8 @@ The two rows arrive at OpenClaw ~0.8-2.0 s apart on most setups. Without coalesc
|
||||
messages: {
|
||||
inbound: {
|
||||
byChannel: {
|
||||
// 2500 ms works for most setups; raise to 4000 ms if your Mac is
|
||||
// slow or under memory pressure (observed gap can stretch past 2 s
|
||||
// then).
|
||||
imessage: 2500,
|
||||
// 7000 ms covers observed Messages.app URL-preview delays.
|
||||
imessage: 7000,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -715,15 +713,15 @@ The two rows arrive at OpenClaw ~0.8-2.0 s apart on most setups. Without coalesc
|
||||
|
||||
The "Flag on" column shows behavior on an `imsg` build that emits `balloon_bundle_id`. On older `imsg` builds that emit no balloon metadata at all, the rows below marked "Two turns" / "N turns" instead fall back to a legacy merge (one turn): OpenClaw cannot structurally tell a split-send from separate sends, so it preserves the pre-metadata merge. Precise separation activates once the build emits balloon metadata.
|
||||
|
||||
| User composes | `chat.db` produces | Flag off (default) | Flag on + window (imsg emits balloon metadata) |
|
||||
| ------------------------------------------------------------------ | ----------------------------------- | --------------------------------------- | ------------------------------------------------ |
|
||||
| `Dump https://example.com` (one send) | 2 rows ~1 s apart | Two agent turns: "Dump" alone, then URL | One turn: merged text `Dump https://example.com` |
|
||||
| `Save this 📎image.jpg caption` (attachment + text) | 2 rows without URL balloon metadata | Two turns | Two turns (legacy merge on metadata-less builds) |
|
||||
| `/status` (standalone command) | 1 row | Instant dispatch | **Wait up to window, then dispatch** |
|
||||
| URL pasted alone | 1 row | Instant dispatch | Wait up to window, then dispatch |
|
||||
| Text + URL sent as two deliberate separate messages, minutes apart | 2 rows outside window | Two turns | Two turns (window expires between them) |
|
||||
| Rapid flood (>10 small DMs inside window) | N rows without URL balloon metadata | N turns | N turns (legacy merge on metadata-less builds) |
|
||||
| Two people typing in a group chat | N rows from M senders | M+ turns (one per sender bucket) | M+ turns — group chats are not coalesced |
|
||||
| User composes | `chat.db` produces | Flag off (default) | Flag on + window (imsg emits balloon metadata) |
|
||||
| ------------------------------------------------------------------ | ----------------------------------- | --------------------------------------- | --------------------------------------------------------------------------------------------------- |
|
||||
| `Dump https://example.com` (one send) | 2 rows ~1 s apart | Two agent turns: "Dump" alone, then URL | One turn: merged text `Dump https://example.com` |
|
||||
| `Save this 📎image.jpg caption` (attachment + text) | 2 rows without URL balloon metadata | Two turns | Two turns after metadata is observed; one merged turn on old/pre-latch metadata-less sessions |
|
||||
| `/status` (standalone command) | 1 row | Instant dispatch | **Wait up to window, then dispatch** |
|
||||
| URL pasted alone | 1 row | Instant dispatch | Wait up to window, then dispatch |
|
||||
| Text + URL sent as two deliberate separate messages, minutes apart | 2 rows outside window | Two turns | Two turns (window expires between them) |
|
||||
| Rapid flood (>10 small DMs inside window) | N rows without URL balloon metadata | N turns | N turns after metadata is observed; one bounded merged turn on old/pre-latch metadata-less sessions |
|
||||
| Two people typing in a group chat | N rows from M senders | M+ turns (one per sender bucket) | M+ turns — group chats are not coalesced |
|
||||
|
||||
## Inbound recovery after a bridge or gateway restart
|
||||
|
||||
|
||||
@@ -39,9 +39,10 @@ Text is supported everywhere; media and reactions vary by channel.
|
||||
- [Nextcloud Talk](/channels/nextcloud-talk) - Self-hosted chat via Nextcloud Talk (bundled plugin).
|
||||
- [Nostr](/channels/nostr) - Decentralized DMs via NIP-04 (bundled plugin).
|
||||
- [QQ Bot](/channels/qqbot) - QQ Bot API; private chat, group chat, and rich media (bundled plugin).
|
||||
- [Raft](/channels/raft) - Raft CLI wake bridge for human and agent collaboration (external plugin).
|
||||
- [Signal](/channels/signal) - signal-cli; privacy-focused.
|
||||
- [Slack](/channels/slack) - Bolt SDK; workspace apps.
|
||||
- [SMS](/channels/sms) - Twilio-backed SMS through the Gateway webhook (bundled plugin).
|
||||
- [SMS](/channels/sms) - Twilio-backed SMS through the Gateway webhook (official plugin).
|
||||
- [Synology Chat](/channels/synology-chat) - Synology NAS Chat via outgoing+incoming webhooks (bundled plugin).
|
||||
- [Telegram](/channels/telegram) - Bot API via grammY; supports groups.
|
||||
- [Tlon](/channels/tlon) - Urbit-based messenger (bundled plugin).
|
||||
|
||||
@@ -7,12 +7,18 @@ read_when:
|
||||
---
|
||||
|
||||
Use IRC when you want OpenClaw in classic channels (`#room`) and direct messages.
|
||||
IRC ships as a bundled plugin, but it is configured in the main config under `channels.irc`.
|
||||
Install the official IRC plugin, then configure it under `channels.irc`.
|
||||
|
||||
## Quick start
|
||||
|
||||
1. Enable IRC config in `~/.openclaw/openclaw.json`.
|
||||
2. Set at least:
|
||||
1. Install the plugin:
|
||||
|
||||
```bash
|
||||
openclaw plugins install @openclaw/irc
|
||||
```
|
||||
|
||||
2. Enable IRC config in `~/.openclaw/openclaw.json`.
|
||||
3. Set at least:
|
||||
|
||||
```json5
|
||||
{
|
||||
@@ -31,7 +37,7 @@ IRC ships as a bundled plugin, but it is configured in the main config under `ch
|
||||
|
||||
Prefer a private IRC server for bot coordination. If you intentionally use a public IRC network, common choices include Libera.Chat, OFTC, and Snoonet. Avoid predictable public channels for bot or swarm backchannel traffic.
|
||||
|
||||
3. Start/restart gateway:
|
||||
4. Start/restart gateway:
|
||||
|
||||
```bash
|
||||
openclaw gateway run
|
||||
|
||||
@@ -32,7 +32,7 @@ Details: [Plugins](/tools/plugin)
|
||||
|
||||
<Steps>
|
||||
<Step title="Ensure plugin is available">
|
||||
Current packaged OpenClaw releases already bundle it. Older/custom installs can add it manually with the commands above.
|
||||
Install `@openclaw/mattermost` with the command above, then restart the Gateway if it is already running.
|
||||
</Step>
|
||||
<Step title="Create a Mattermost bot">
|
||||
Create a Mattermost bot account and copy the **bot token**.
|
||||
|
||||
147
docs/channels/raft.md
Normal file
147
docs/channels/raft.md
Normal file
@@ -0,0 +1,147 @@
|
||||
---
|
||||
summary: "Raft External Agent support through the Raft CLI wake bridge"
|
||||
read_when:
|
||||
- You want to connect OpenClaw to a Raft workspace
|
||||
- You are configuring a Raft External Agent
|
||||
- You are debugging Raft wake delivery
|
||||
title: "Raft"
|
||||
sidebarTitle: "Raft"
|
||||
---
|
||||
|
||||
Raft support connects an OpenClaw agent to a Raft External Agent through the local
|
||||
Raft CLI. Raft sends authenticated wake hints to the Gateway. The agent then uses
|
||||
the Raft CLI to check and send messages.
|
||||
|
||||
## Install
|
||||
|
||||
Raft is an official external plugin. Install it on the Gateway host:
|
||||
|
||||
```bash
|
||||
openclaw plugins install @openclaw/raft
|
||||
openclaw gateway restart
|
||||
```
|
||||
|
||||
Details: [Plugins](/tools/plugin)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- A Raft workspace with an External Agent.
|
||||
- The Raft CLI installed on the same host as the OpenClaw Gateway.
|
||||
- A Raft CLI profile that is already signed in and associated with that External Agent.
|
||||
|
||||
The plugin does not store Raft credentials. The Raft CLI keeps that authentication
|
||||
in its own profile.
|
||||
|
||||
## Configure
|
||||
|
||||
Set the profile in config:
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
raft: {
|
||||
enabled: true,
|
||||
profile: "openclaw",
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
For the default account, you can instead set `RAFT_PROFILE` in the Gateway
|
||||
environment:
|
||||
|
||||
```bash
|
||||
RAFT_PROFILE=openclaw
|
||||
```
|
||||
|
||||
Use a named account when one Gateway connects to more than one Raft External Agent:
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
raft: {
|
||||
accounts: {
|
||||
support: {
|
||||
profile: "support-agent",
|
||||
},
|
||||
engineering: {
|
||||
profile: "engineering-agent",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
The interactive setup flow records the same profile:
|
||||
|
||||
```bash
|
||||
openclaw channels setup raft
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
When the Gateway starts, the plugin:
|
||||
|
||||
1. Opens a loopback-only HTTP wake endpoint on an ephemeral port.
|
||||
2. Starts `raft --profile <profile> agent bridge` with that endpoint and a
|
||||
per-process token.
|
||||
3. Accepts only authenticated, content-free wake hints with a replay identity from the local bridge.
|
||||
4. Requires one of `eventId`, `attemptId`, `messageId`, `delivery_id`, `wake_id`, or `id`.
|
||||
5. Deduplicates recent retried wake deliveries by bridge event id, including across Gateway restarts.
|
||||
6. Returns a stable runtime session for the current bridge and an empty activity-drain batch for the Raft CLI protocol.
|
||||
7. Starts one serialized OpenClaw agent turn for each accepted wake.
|
||||
|
||||
The bridge owns Raft delivery retries and reconnects. The OpenClaw turn receives
|
||||
only a wake notice, not a copied Raft message body. It uses the CLI to read
|
||||
pending messages and to send its response:
|
||||
|
||||
```bash
|
||||
raft --profile openclaw message check
|
||||
raft --profile openclaw message send
|
||||
```
|
||||
|
||||
<Note>
|
||||
Raft is not a normal push-message transport. OpenClaw does not automatically
|
||||
send the model's final text back through the bridge, so the agent must use the
|
||||
Raft CLI after processing a wake.
|
||||
</Note>
|
||||
|
||||
## Verify
|
||||
|
||||
Check that OpenClaw can find the CLI and has a configured profile:
|
||||
|
||||
```bash
|
||||
openclaw channels status --probe
|
||||
openclaw plugins inspect raft --runtime --json
|
||||
```
|
||||
|
||||
Then send a message to the Raft External Agent. The Gateway log should show the
|
||||
Raft bridge starting, followed by an inbound wake. The agent should use the
|
||||
configured Raft profile to check its pending messages.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Raft CLI is missing">
|
||||
Install the Raft CLI on the Gateway host and make `raft` available on the
|
||||
service's `PATH`. Verify it with `raft --help`, then restart the Gateway.
|
||||
</Accordion>
|
||||
<Accordion title="The bridge exits immediately">
|
||||
Verify the configured profile is signed in and belongs to the intended
|
||||
Raft External Agent. Run `raft --profile <profile> agent bridge` directly
|
||||
to see the CLI diagnostic.
|
||||
</Accordion>
|
||||
<Accordion title="A wake arrives but no Raft response is sent">
|
||||
This is expected when the agent does not invoke the Raft CLI. The wake
|
||||
bridge does not carry message bodies or automatic final replies. Check the
|
||||
agent's tool policy and ensure it can run `raft --profile <profile> message
|
||||
check` and `message send`.
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
## References
|
||||
|
||||
- [Raft](https://raft.build/)
|
||||
- [Raft documentation](https://docs.raft.build/welcome/)
|
||||
- [Hermes Raft integration](https://hermes-agent.nousresearch.com/docs/user-guide/messaging/raft)
|
||||
@@ -20,12 +20,18 @@ Status: external CLI integration. Gateway talks to `signal-cli` over HTTP — ei
|
||||
## Quick setup (beginner)
|
||||
|
||||
1. Use a **separate Signal number** for the bot (recommended).
|
||||
2. Install `signal-cli` (Java required if you use the JVM build).
|
||||
3. Choose one setup path:
|
||||
2. Install the OpenClaw plugin:
|
||||
|
||||
```bash
|
||||
openclaw plugins install @openclaw/signal
|
||||
```
|
||||
|
||||
3. Install `signal-cli` (Java required if you use the JVM build).
|
||||
4. Choose one setup path:
|
||||
- **Path A (QR link):** `signal-cli link -n "OpenClaw"` and scan with Signal.
|
||||
- **Path B (SMS register):** register a dedicated number with captcha + SMS verification.
|
||||
4. Configure OpenClaw and restart the gateway.
|
||||
5. Send a first DM and approve pairing (`openclaw pairing approve signal <CODE>`).
|
||||
5. Configure OpenClaw and restart the gateway.
|
||||
6. Send a first DM and approve pairing (`openclaw pairing approve signal <CODE>`).
|
||||
|
||||
Minimal config:
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
---
|
||||
summary: "Slack setup and runtime behavior (Socket Mode + HTTP Request URLs)"
|
||||
summary: "Slack setup and runtime behavior (Socket Mode, HTTP Request URLs, and relay mode)"
|
||||
read_when:
|
||||
- Setting up Slack or debugging Slack socket/HTTP mode
|
||||
- Setting up Slack or debugging Slack socket, HTTP, or relay mode
|
||||
title: "Slack"
|
||||
---
|
||||
|
||||
Production-ready for DMs and channels via Slack app integrations. Default mode is Socket Mode; HTTP Request URLs are also supported.
|
||||
Production-ready for DMs and channels via Slack app integrations. Default mode is Socket Mode; HTTP Request URLs are also supported. Relay mode is intended for managed deployments where a trusted router owns Slack ingress.
|
||||
|
||||
<CardGroup cols={3}>
|
||||
<Card title="Pairing" icon="link" href="/channels/pairing">
|
||||
@@ -41,6 +41,37 @@ Both transports are production-ready and reach feature parity for messaging, sla
|
||||
**Pick HTTP Request URLs** when running multiple Gateway replicas behind a load balancer, when outbound WSS is blocked but inbound HTTPS is allowed, or when you already terminate Slack webhooks at a reverse proxy.
|
||||
</Note>
|
||||
|
||||
### Relay mode
|
||||
|
||||
Relay mode separates Slack ingress from the OpenClaw gateway. A trusted router owns the
|
||||
single Slack Socket Mode connection, chooses a destination gateway, and forwards a typed
|
||||
event over an authenticated websocket. The gateway continues to use its bot token for
|
||||
outbound Slack Web API calls.
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
slack: {
|
||||
mode: "relay",
|
||||
botToken: { source: "env", provider: "default", id: "SLACK_BOT_TOKEN" },
|
||||
relay: {
|
||||
url: "wss://router.example.com/gateway/ws",
|
||||
authToken: { source: "env", provider: "default", id: "SLACK_RELAY_AUTH_TOKEN" },
|
||||
gatewayId: "team-gateway",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
The relay URL must use `wss://` unless it targets localhost. Treat the bearer token and
|
||||
router route table as part of the Slack authorization boundary: routed events enter the
|
||||
normal Slack message handler as authorized activations. A router-provided `slack_identity`
|
||||
in the websocket `hello` frame can set the default outbound username and icon; an explicit
|
||||
identity supplied by the caller still wins. The relay connection reconnects with the same
|
||||
bounded backoff timing used by Socket Mode and clears the router-provided identity whenever
|
||||
it disconnects.
|
||||
|
||||
## Install
|
||||
|
||||
Install Slack before configuring the channel:
|
||||
@@ -863,7 +894,8 @@ The default manifest enables the Slack App Home **Home** tab and subscribes to `
|
||||
|
||||
- `botToken` + `appToken` are required for Socket Mode.
|
||||
- HTTP mode requires `botToken` + `signingSecret`.
|
||||
- `botToken`, `appToken`, `signingSecret`, and `userToken` accept plaintext
|
||||
- Relay mode requires `botToken` plus `relay.url`, `relay.authToken`, and `relay.gatewayId`; it does not use an app token or signing secret.
|
||||
- `botToken`, `appToken`, `signingSecret`, `relay.authToken`, and `userToken` accept plaintext
|
||||
strings or SecretRef objects.
|
||||
- Config tokens override env fallback.
|
||||
- `SLACK_BOT_TOKEN` / `SLACK_APP_TOKEN` env fallback applies only to the default account.
|
||||
|
||||
@@ -24,6 +24,7 @@ OpenClaw can receive and send SMS through a Twilio phone number or Messaging Ser
|
||||
|
||||
You need:
|
||||
|
||||
- The official SMS plugin installed with `openclaw plugins install @openclaw/sms`.
|
||||
- A Twilio account with an SMS-capable phone number, or a Twilio Messaging Service.
|
||||
- The Twilio Account SID and Auth Token.
|
||||
- A public HTTPS URL that reaches your OpenClaw Gateway.
|
||||
@@ -34,6 +35,11 @@ Use one Twilio number for both SMS and Voice Call if the number has both capabil
|
||||
## Quick Setup
|
||||
|
||||
<Steps>
|
||||
<Step title="Install the plugin">
|
||||
```bash
|
||||
openclaw plugins install @openclaw/sms
|
||||
```
|
||||
</Step>
|
||||
<Step title="Create or choose a Twilio sender">
|
||||
In Twilio, open **Phone Numbers > Manage > Active numbers** and choose an SMS-capable number. Save:
|
||||
|
||||
|
||||
@@ -336,7 +336,8 @@ curl "https://api.telegram.org/bot<bot_token>/getUpdates"
|
||||
Requirement:
|
||||
|
||||
- `channels.telegram.streaming` is `off | partial | block | progress` (default: `partial`)
|
||||
- `progress` keeps one editable status draft for tool progress, clears it at completion, and sends the final answer as a normal message
|
||||
- short initial answer previews are debounced, then materialized after a bounded delay if the run is still active
|
||||
- `progress` keeps one editable status draft for tool progress, shows the stable status label when answer activity arrives before tool progress, clears it at completion, and sends the final answer as a normal message
|
||||
- `streaming.preview.toolProgress` controls whether tool/progress updates reuse the same edited preview message (default: `true` when preview streaming is active)
|
||||
- `streaming.preview.commandText` controls command/exec detail inside those tool-progress lines: `raw` (default, preserves released behavior) or `status` (tool label only)
|
||||
- `streaming.progress.commentary` (default: `false`) opts into assistant commentary/preamble text in the temporary progress draft
|
||||
|
||||
48
docs/ci.md
48
docs/ci.md
@@ -47,33 +47,21 @@ Use `pnpm ci:timings`, `pnpm ci:timings:recent`, or `node scripts/ci-run-timings
|
||||
|
||||
For pull request runs, the terminal timing-summary job runs the helper from the trusted base revision before passing `GH_TOKEN` to `gh run view`. That keeps the tokened query out of branch-controlled code while still summarizing the pull request's current CI run.
|
||||
|
||||
## Real behavior proof
|
||||
## PR context and evidence
|
||||
|
||||
External contributor PRs run a `Real behavior proof` gate from
|
||||
External contributor PRs run a PR context and evidence gate from
|
||||
`.github/workflows/real-behavior-proof.yml`. The workflow checks out the trusted
|
||||
base commit and evaluates the PR body only; it does not execute code from the
|
||||
contributor branch.
|
||||
|
||||
The gate applies to PR authors who are not repository owners, members,
|
||||
collaborators, or bots. It passes when the PR body contains a
|
||||
`Real behavior proof` section with filled values for:
|
||||
|
||||
- `Behavior or issue addressed`
|
||||
- `Real environment tested`
|
||||
- `Exact steps or command run after this patch`
|
||||
- `Evidence after fix`
|
||||
- `Observed result after fix`
|
||||
- `What was not tested`
|
||||
|
||||
The evidence must show the changed behavior after the patch in a real OpenClaw
|
||||
setup. Screenshots, recordings, terminal captures, console output, copied live
|
||||
output, redacted runtime logs, and linked artifacts all count. Unit tests, mocks,
|
||||
snapshots, lint, typechecks, and CI results are useful supporting verification,
|
||||
but they do not satisfy this gate by themselves.
|
||||
collaborators, or bots. It passes when the PR body contains authored
|
||||
`What Problem This Solves` and `Evidence` sections. Evidence can be a focused
|
||||
test, CI result, screenshot, recording, terminal output, live observation,
|
||||
redacted log, or artifact link. The body provides intent and useful validation;
|
||||
reviewers inspect the code, tests, and CI to assess correctness.
|
||||
|
||||
When the check fails, update the PR body instead of pushing another code commit.
|
||||
Maintainers can apply `proof: override` only when the proof gate should not
|
||||
apply to that PR.
|
||||
|
||||
## Scope and routing
|
||||
|
||||
@@ -86,7 +74,7 @@ Scope logic lives in `scripts/ci-changed-scope.mjs` and is covered by unit tests
|
||||
- **CI routing-only edits, selected cheap core-test fixture edits, and narrow plugin contract helper/test-routing edits** use a fast Node-only manifest path: `preflight`, security, and a single `checks-fast-core` task. That path skips build artifacts, Node 22 compatibility, channel contracts, full core shards, bundled-plugin shards, and additional guard matrices when the change is limited to the routing or helper surfaces the fast task exercises directly.
|
||||
- **Windows Node checks** are scoped to Windows-specific process/path wrappers, npm/pnpm/UI runner helpers, package manager config, and the CI workflow surfaces that execute that lane; unrelated source, plugin, install-smoke, and test-only changes stay on the Linux Node lanes.
|
||||
|
||||
The slowest Node test families are split or balanced so each job stays small without over-reserving runners: plugin contracts and channel contracts each run as two weighted Blacksmith-backed shards with the standard GitHub runner fallback, core unit fast/support lanes run separately, core runtime infra is split between state, process/config, shared, and three cron domain shards, auto-reply runs as balanced workers (with the reply subtree split into agent-runner, dispatch, and commands/state-routing shards), and agentic gateway/server configs are split across chat/auth/model/http-plugin/runtime/startup lanes instead of waiting on built artifacts. Broad browser, QA, media, and miscellaneous plugin tests use their dedicated Vitest configs instead of the shared plugin catch-all. Include-pattern shards record timing entries using the CI shard name, so `.artifacts/vitest-shard-timings.json` can distinguish a whole config from a filtered shard. `check-additional-*` keeps package-boundary compile/canary work together and separates runtime topology architecture from gateway watch coverage; the boundary guard list is striped into one prompt-heavy shard and one combined shard for the remaining guard stripes, each running selected independent guards concurrently and printing per-check timings. The expensive Codex happy-path prompt snapshot drift check runs as its own additional job for manual CI and for prompt-affecting changes only, so normal unrelated Node changes do not wait behind cold prompt snapshot generation and the boundary shards stay balanced while prompt drift is still pinned to the PR that caused it; the same flag skips prompt snapshot Vitest generation inside the built-artifact core support-boundary shard. Gateway watch, channel tests, and the core support-boundary shard run concurrently inside `build-artifacts` after `dist/` and `dist-runtime/` are already built.
|
||||
The slowest Node test families are split or balanced so each job stays small without over-reserving runners: plugin contracts and channel contracts each run as two weighted Blacksmith-backed shards with the standard GitHub runner fallback, core unit fast/support lanes run separately, core runtime infra is split between state, process/config, shared, and three cron domain shards, auto-reply runs as balanced workers (with the reply subtree split into agent-runner, dispatch, and commands/state-routing shards), and agentic gateway/server configs are split across chat/auth/model/http-plugin/runtime/startup lanes instead of waiting on built artifacts. Normal CI then packs only isolated infra include-pattern shards into deterministic bundles of at most 64 test files, reducing the Node matrix without merging non-isolated command/cron, stateful agents-core, or gateway/server suites; heavy fixed suites stay on 8 vCPU while the bundled and lower-weight lanes use 4 vCPU. Broad browser, QA, media, and miscellaneous plugin tests use their dedicated Vitest configs instead of the shared plugin catch-all. Include-pattern shards record timing entries using the CI shard name, so `.artifacts/vitest-shard-timings.json` can distinguish a whole config from a filtered shard. `check-additional-*` keeps package-boundary compile/canary work together and separates runtime topology architecture from gateway watch coverage; the boundary guard list is striped into one prompt-heavy shard and one combined shard for the remaining guard stripes, each running selected independent guards concurrently and printing per-check timings. The expensive Codex happy-path prompt snapshot drift check runs as its own additional job for manual CI and for prompt-affecting changes only, so normal unrelated Node changes do not wait behind cold prompt snapshot generation and the boundary shards stay balanced while prompt drift is still pinned to the PR that caused it; the same flag skips prompt snapshot Vitest generation inside the built-artifact core support-boundary shard. Gateway watch, channel tests, and the core support-boundary shard run concurrently inside `build-artifacts` after `dist/` and `dist-runtime/` are already built.
|
||||
|
||||
Android CI runs both `testPlayDebugUnitTest` and `testThirdPartyDebugUnitTest` and then builds the Play debug APK. The third-party flavor has no separate source set or manifest; its unit-test lane still compiles the flavor with the SMS/call-log BuildConfig flags, while avoiding a duplicate debug APK packaging job on every Android-relevant push.
|
||||
|
||||
@@ -123,15 +111,15 @@ gh workflow run full-release-validation.yml --ref main -f ref=<branch-or-sha>
|
||||
|
||||
## Runners
|
||||
|
||||
| Runner | Jobs |
|
||||
| -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `ubuntu-24.04` | Manual CI dispatch and non-canonical repository fallbacks, workflow-sanity, labeler, auto-response, docs workflows outside CI, and install-smoke preflight so the Blacksmith matrix can queue earlier |
|
||||
| `blacksmith-4vcpu-ubuntu-2404` | `CodeQL Critical Quality`, `preflight`, `security-fast`, lower-weight extension shards, `checks-fast-core`, plugin/channel contract shards, `checks-node-compat-node22`, `check-guards`, `check-prod-types`, and `check-test-types` |
|
||||
| `blacksmith-8vcpu-ubuntu-2404` | Linux Node test shards, bundled plugin test shards, `check-additional-*` shards, `check-dependencies`, and `android` |
|
||||
| `blacksmith-16vcpu-ubuntu-2404` | `build-artifacts`, `check-lint` (CPU-sensitive enough that 8 vCPU cost more than they saved); install-smoke Docker builds (32-vCPU queue time cost more than it saved) |
|
||||
| `blacksmith-16vcpu-windows-2025` | `checks-windows` |
|
||||
| `blacksmith-6vcpu-macos-15` | `macos-node` on `openclaw/openclaw`; forks fall back to `macos-15` |
|
||||
| `blacksmith-12vcpu-macos-26` | `macos-swift` on `openclaw/openclaw`; forks fall back to `macos-26` |
|
||||
| Runner | Jobs |
|
||||
| ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `ubuntu-24.04` | Manual CI dispatch and non-canonical repository fallbacks, workflow-sanity, labeler, auto-response, docs workflows outside CI, and install-smoke preflight so the Blacksmith matrix can queue earlier |
|
||||
| `blacksmith-4vcpu-ubuntu-2404` | `CodeQL Critical Quality`, `preflight`, `security-fast`, lower-weight extension shards, `checks-fast-core`, plugin/channel contract shards, most bundled/lower-weight Linux Node shards, `check-guards`, `check-prod-types`, `check-test-types`, selected `check-additional-*` shards, and `check-dependencies` |
|
||||
| `blacksmith-8vcpu-ubuntu-2404` | Retained heavy Linux Node suites, boundary/extension-heavy `check-additional-*` shards, and `android` |
|
||||
| `blacksmith-16vcpu-ubuntu-2404` | `build-artifacts`, `check-lint` (CPU-sensitive enough that 8 vCPU cost more than they saved); install-smoke Docker builds (32-vCPU queue time cost more than it saved) |
|
||||
| `blacksmith-8vcpu-windows-2025` | `checks-windows` |
|
||||
| `blacksmith-6vcpu-macos-15` | `macos-node` on `openclaw/openclaw`; forks fall back to `macos-15` |
|
||||
| `blacksmith-12vcpu-macos-26` | `macos-swift` on `openclaw/openclaw`; forks fall back to `macos-26` |
|
||||
|
||||
Canonical-repo CI keeps Blacksmith as the default runner path for normal push and pull-request runs. `workflow_dispatch` and non-canonical repository runs use GitHub-hosted runners, but normal canonical runs do not currently probe Blacksmith queue health or automatically fall back to GitHub-hosted labels when Blacksmith is unavailable.
|
||||
|
||||
@@ -189,7 +177,7 @@ Every lane uploads GitHub artifacts. When `CLAWGRIT_REPORTS_TOKEN` is configured
|
||||
|
||||
## Full Release Validation
|
||||
|
||||
`Full Release Validation` is the manual umbrella workflow for "run everything before release." It accepts a branch, tag, or full commit SHA, dispatches the manual `CI` workflow with that target, dispatches `Plugin Prerelease` for release-only plugin/package/static/Docker proof, and dispatches `OpenClaw Release Checks` for install smoke, package acceptance, cross-OS package checks, QA Lab parity, Matrix, and Telegram lanes. Stable and full profiles always include exhaustive live/E2E and Docker release-path soak coverage; the beta profile can opt in with `run_release_soak=true`. With `rerun_group=all` and `release_profile=full`, it also runs `NPM Telegram Beta E2E` against the `release-package-under-test` artifact from release checks. After publishing, pass `release_package_spec` to reuse the shipped npm package across release checks, Package Acceptance, Docker, cross-OS, and Telegram without rebuilding. Use `npm_telegram_package_spec` only when Telegram must prove a different package. The Codex plugin live package lane uses the same selected state by default: published `release_package_spec=openclaw@<tag>` derives `codex_plugin_spec=npm:@openclaw/codex@<tag>`, while SHA/artifact runs pack `extensions/codex` from the selected ref. Set `codex_plugin_spec` explicitly for custom plugin sources such as `npm:`, `npm-pack:`, or `git:` specs.
|
||||
`Full Release Validation` is the manual umbrella workflow for "run everything before release." It accepts a branch, tag, or full commit SHA, dispatches the manual `CI` workflow with that target, dispatches `Plugin Prerelease` for release-only plugin/package/static/Docker proof, and dispatches `OpenClaw Release Checks` for install smoke, package acceptance, cross-OS package checks, QA Lab parity, Matrix, and Telegram lanes. Stable and full profiles always include exhaustive live/E2E and Docker release-path soak coverage; the beta profile can opt in with `run_release_soak=true`. The canonical package Telegram E2E runs inside Package Acceptance, so a full candidate does not start a duplicate live poller. After publishing, pass `release_package_spec` to reuse the shipped npm package across release checks, Package Acceptance, Docker, cross-OS, and Telegram without rebuilding. Use `npm_telegram_package_spec` only for a focused published-package Telegram rerun. The Codex plugin live package lane uses the same selected state by default: published `release_package_spec=openclaw@<tag>` derives `codex_plugin_spec=npm:@openclaw/codex@<tag>`, while SHA/artifact runs pack `extensions/codex` from the selected ref. Set `codex_plugin_spec` explicitly for custom plugin sources such as `npm:`, `npm-pack:`, or `git:` specs.
|
||||
|
||||
See [Full release validation](/reference/full-release-validation) for the
|
||||
stage matrix, exact workflow job names, profile differences, artifacts, and
|
||||
|
||||
@@ -197,7 +197,7 @@ Isolated cron resolves the active model in this order:
|
||||
|
||||
### Fast mode
|
||||
|
||||
Isolated cron fast mode follows the resolved live model selection. Model config `params.fastMode` applies by default, but a stored session `fastMode` override still wins over config.
|
||||
Isolated cron fast mode follows the resolved live model selection. Model config `params.fastMode` applies by default, but a stored session `fastMode` override still wins over config. When the resolved mode is `auto`, the cutoff uses the selected model's `params.fastAutoOnSeconds` value, defaulting to 60 seconds.
|
||||
|
||||
### Live model switch retries
|
||||
|
||||
|
||||
@@ -172,10 +172,12 @@ A finding includes:
|
||||
| `ocPath` | Precise `oc://` address when a check can point to one. |
|
||||
| `fixHint` | Suggested operator action or repair summary. |
|
||||
|
||||
This release registers the modernized core doctor checks on the structured
|
||||
health path. The `openclaw/plugin-sdk/health` subpath exposes the same
|
||||
contract for bundled follow-up consumers, but plugin-backed checks only run
|
||||
after their owning package registers them in the active command path.
|
||||
Modernized core doctor checks stay attached to the ordered doctor contribution
|
||||
that owns their human `doctor` / `doctor --fix` behavior. The shared structured
|
||||
health registry is the extension point: bundled and plugin-backed checks run
|
||||
after core doctor checks once their owning package registers them in the active
|
||||
command path. The `openclaw/plugin-sdk/health` subpath exposes the same
|
||||
contract for those extension consumers.
|
||||
|
||||
## Check Selection
|
||||
|
||||
|
||||
@@ -165,10 +165,15 @@ When you set `--url`, the CLI does not fall back to config or environment creden
|
||||
|
||||
```bash
|
||||
openclaw gateway health --url ws://127.0.0.1:18789
|
||||
openclaw gateway health --port 18789
|
||||
```
|
||||
|
||||
The HTTP `/healthz` endpoint is a liveness probe: it returns once the server can answer HTTP. The HTTP `/readyz` endpoint is stricter and stays red while startup plugin sidecars, channels, or configured hooks are still settling. Local or authenticated detailed readiness responses include an `eventLoop` diagnostic block with event-loop delay, event-loop utilization, CPU core ratio, and a `degraded` flag.
|
||||
|
||||
<ParamField path="--port <port>" type="number">
|
||||
Target a local loopback Gateway on this port. This overrides `OPENCLAW_GATEWAY_URL` and `OPENCLAW_GATEWAY_PORT` for the health call.
|
||||
</ParamField>
|
||||
|
||||
### `gateway usage-cost`
|
||||
|
||||
Fetch usage-cost summaries from session logs.
|
||||
@@ -340,8 +345,13 @@ If multiple probe targets are reachable, it prints all of them. An SSH tunnel, T
|
||||
```bash
|
||||
openclaw gateway probe
|
||||
openclaw gateway probe --json
|
||||
openclaw gateway probe --port 18789
|
||||
```
|
||||
|
||||
<ParamField path="--port <port>" type="number">
|
||||
Use this port for the local loopback probe target and SSH tunnel remote port. Without `--url`, this selects the local loopback target instead of configured gateway environment URL, environment port, or remote targets.
|
||||
</ParamField>
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Interpretation">
|
||||
- `Reachable: yes` means at least one target accepted a WebSocket connect.
|
||||
|
||||
@@ -39,7 +39,13 @@ openclaw nodes status --last-connected 24h
|
||||
`nodes list` prints pending/paired tables. Paired rows include the most recent connect age (Last Connect).
|
||||
Use `--connected` to only show currently-connected nodes. Use `--last-connected <duration>` to
|
||||
filter to nodes that connected within a duration (e.g. `24h`, `7d`).
|
||||
Use `nodes remove --node <id|name|ip>` to delete a stale gateway-owned node pairing record.
|
||||
Use `nodes remove --node <id|name|ip>` to remove a node pairing. For a
|
||||
device-backed node this revokes the device's `node` role in `devices/paired.json`
|
||||
and disconnects its node-role sessions (a mixed-role device keeps its row and
|
||||
only loses the `node` role; a node-only device is deleted); it also clears any
|
||||
matching legacy gateway-owned node pairing record. `operator.pairing` can remove
|
||||
non-operator node rows; a device-token caller revoking its own node role on a
|
||||
mixed-role device additionally needs `operator.admin`.
|
||||
|
||||
Approval note:
|
||||
|
||||
|
||||
@@ -168,11 +168,62 @@ traffic. Use `--store <path>` for explicit offline repair of a store file.
|
||||
}
|
||||
```
|
||||
|
||||
Related:
|
||||
## Compact a session
|
||||
|
||||
- Session config: [Configuration reference](/gateway/config-agents#session)
|
||||
Reclaim context budget for a wedged or oversized session. `openclaw sessions compact <key>` is the first-class wrapper around the `sessions.compact` gateway RPC and requires a running gateway.
|
||||
|
||||
```bash
|
||||
openclaw sessions compact "agent:main:main"
|
||||
openclaw sessions compact "agent:main:main" --max-lines 200
|
||||
openclaw sessions compact "agent:work:main" --agent work --json
|
||||
```
|
||||
|
||||
- Without `--max-lines`, the gateway LLM-summarizes the transcript. This can be slow, so the default `--timeout` is `180000` ms.
|
||||
- With `--max-lines <n>`, it truncates to the last `n` transcript lines and archives the prior transcript as a `.bak` sidecar.
|
||||
- `--agent <id>`: agent that owns the session; required for `global` keys.
|
||||
- `--url` / `--token` / `--password`: gateway connection overrides.
|
||||
- `--timeout <ms>`: RPC timeout in milliseconds.
|
||||
- `--json`: print the raw RPC payload.
|
||||
|
||||
The command exits non-zero when the gateway reports a failed compaction or is unreachable, so crons and scripts never mistake a silent no-op for success.
|
||||
|
||||
> Note: `openclaw agent --message '/compact ...'` is **not** a compaction path. Slash commands from the CLI are rejected by the authorized-sender check; that invocation exits non-zero with guidance pointing here instead of silently no-opping.
|
||||
|
||||
### sessions.compact RPC
|
||||
|
||||
`openclaw gateway call sessions.compact --params '<json>'` accepts:
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
| ---------- | ----------- | -------- | ---------------------------------------------------------- |
|
||||
| `key` | string | yes | Session key to compact (for example `agent:main:main`). |
|
||||
| `agentId` | string | no | Agent id that owns the session (for `global` keys). |
|
||||
| `maxLines` | integer ≥ 1 | no | Truncate to the last N lines instead of LLM summarization. |
|
||||
|
||||
Example LLM-summarize response:
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"key": "agent:main:main",
|
||||
"compacted": true,
|
||||
"result": { "tokensBefore": 243868, "tokensAfter": 34941 }
|
||||
}
|
||||
```
|
||||
|
||||
Example truncate response (`--max-lines 200`):
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"key": "agent:main:main",
|
||||
"compacted": true,
|
||||
"archived": "/home/user/.openclaw/agents/main/sessions/transcripts/<id>.jsonl.bak",
|
||||
"kept": 200
|
||||
}
|
||||
```
|
||||
|
||||
## Related
|
||||
|
||||
- Session config: [Configuration reference](/gateway/config-agents#session)
|
||||
- [CLI reference](/cli)
|
||||
- [Session management](/concepts/session)
|
||||
|
||||
@@ -230,8 +230,8 @@ canonical subscription `github-copilot` provider and is **never** selected by
|
||||
The harness claims its provider, runtime, CLI session key, and auth profile
|
||||
prefix in `extensions/copilot/doctor-contract-api.ts`, which
|
||||
`openclaw doctor` auto-loads. For configuration, auth, transcript mirroring,
|
||||
compaction, the doctor probe surface, and the broader PI vs Codex vs Copilot
|
||||
SDK decision, see [GitHub Copilot agent runtime](/plugins/copilot).
|
||||
compaction, the declarative doctor contract, and the broader PI vs Codex vs
|
||||
Copilot SDK decision, see [GitHub Copilot agent runtime](/plugins/copilot).
|
||||
|
||||
## Compatibility contract
|
||||
|
||||
|
||||
@@ -333,7 +333,7 @@ Gateway model capability checks also read explicit `models.providers.<id>.models
|
||||
|
||||
### Moonshot AI (Kimi)
|
||||
|
||||
Moonshot ships as a bundled provider plugin. Use the built-in provider by default, and add an explicit `models.providers.moonshot` entry only when you need to override the base URL or model metadata:
|
||||
Install `@openclaw/moonshot-provider` before onboarding. Add an explicit `models.providers.moonshot` entry only when you need to override the base URL or model metadata:
|
||||
|
||||
- Provider: `moonshot`
|
||||
- Auth: `MOONSHOT_API_KEY`
|
||||
|
||||
@@ -37,7 +37,7 @@ that agent; if you copy credentials manually, copy only portable static
|
||||
`api_key` or `token` profiles.
|
||||
</Warning>
|
||||
|
||||
Skills are loaded from each agent workspace plus shared roots such as `~/.openclaw/skills`, then filtered by the effective agent skill allowlist when configured. Use `agents.defaults.skills` for a shared baseline and `agents.list[].skills` for per-agent replacement. See [Skills: per-agent vs shared](/tools/skills#per-agent-vs-shared-skills) and [Skills: agent skill allowlists](/tools/skills#agent-skill-allowlists).
|
||||
Skills are loaded from each agent workspace plus shared roots such as `~/.openclaw/skills`, then filtered by the effective agent skill allowlist when configured. Use `agents.defaults.skills` for a shared baseline and `agents.list[].skills` for per-agent replacement. See [Skills: per-agent vs shared](/tools/skills#per-agent-vs-shared-skills) and [Skills: agent skill allowlists](/tools/skills#agent-allowlists).
|
||||
|
||||
The Gateway can host **one agent** (default) or **many agents** side-by-side.
|
||||
|
||||
|
||||
@@ -302,13 +302,13 @@ Live transport runners should import the shared scenario ids, baseline
|
||||
coverage helpers, and scenario-selection helper from
|
||||
`openclaw/plugin-sdk/qa-live-transport-scenarios`.
|
||||
|
||||
| Lane | Canary | Mention gating | Bot-to-bot | Allowlist block | Top-level reply | Restart resume | Thread follow-up | Thread isolation | Reaction observation | Help command | Native command registration |
|
||||
| -------- | ------ | -------------- | ---------- | --------------- | --------------- | -------------- | ---------------- | ---------------- | -------------------- | ------------ | --------------------------- |
|
||||
| Matrix | x | x | x | x | x | x | x | x | x | | |
|
||||
| Telegram | x | x | x | | | | | | | x | |
|
||||
| Discord | x | x | x | | | | | | | | x |
|
||||
| Slack | x | x | x | x | x | x | x | x | | | |
|
||||
| WhatsApp | x | x | | x | x | x | | | x | x | |
|
||||
| Lane | Canary | Mention gating | Bot-to-bot | Allowlist block | Top-level reply | Quote reply | Restart resume | Thread follow-up | Thread isolation | Reaction observation | Help command | Native command registration |
|
||||
| -------- | ------ | -------------- | ---------- | --------------- | --------------- | ----------- | -------------- | ---------------- | ---------------- | -------------------- | ------------ | --------------------------- |
|
||||
| Matrix | x | x | x | x | x | | x | x | x | x | | |
|
||||
| Telegram | x | x | x | | | | | | | | x | |
|
||||
| Discord | x | x | x | | | | | | | | | x |
|
||||
| Slack | x | x | x | x | x | | x | x | x | | | |
|
||||
| WhatsApp | x | x | | x | x | x | x | | | x | x | |
|
||||
|
||||
This keeps `qa-channel` as the broad product-behavior suite while Matrix,
|
||||
Telegram, and other live transports share one explicit transport-contract checklist.
|
||||
@@ -731,8 +731,9 @@ Scenario catalog (`extensions/qa-lab/src/live-transports/whatsapp/whatsapp-live.
|
||||
`whatsapp-whoami-command`, `whatsapp-context-command`,
|
||||
`whatsapp-native-new-command`.
|
||||
- Reply and final-output behavior: `whatsapp-tool-only-usage-footer`,
|
||||
`whatsapp-reply-to-message`, `whatsapp-reply-context-isolation`,
|
||||
`whatsapp-reply-delivery-shape`, `whatsapp-stream-final-message-accounting`.
|
||||
`whatsapp-reply-to-message`, `whatsapp-group-reply-to-message`,
|
||||
`whatsapp-reply-context-isolation`, `whatsapp-reply-delivery-shape`,
|
||||
`whatsapp-stream-final-message-accounting`.
|
||||
- Inbound media and structured messages: `whatsapp-inbound-image-caption`,
|
||||
`whatsapp-audio-preflight`, `whatsapp-inbound-structured-messages`,
|
||||
`whatsapp-group-audio-gating`. These send real WhatsApp image, audio,
|
||||
@@ -749,9 +750,9 @@ Scenario catalog (`extensions/qa-lab/src/live-transports/whatsapp/whatsapp-live.
|
||||
`whatsapp-approval-plugin-native`.
|
||||
- Status reactions: `whatsapp-status-reactions`.
|
||||
|
||||
The catalog currently contains 35 scenarios. The `live-frontier` default lane is
|
||||
kept small at 8 scenarios for fast smoke coverage. The `mock-openai` default
|
||||
lane runs 29 deterministic scenarios through the real WhatsApp transport while
|
||||
The catalog currently contains 36 scenarios. The `live-frontier` default lane is
|
||||
kept small at 10 scenarios for fast smoke coverage. The `mock-openai` default
|
||||
lane runs 31 deterministic scenarios through the real WhatsApp transport while
|
||||
mocking only model output. Approval scenarios and a few heavier/blocking checks
|
||||
remain explicit by scenario id.
|
||||
|
||||
|
||||
@@ -160,9 +160,10 @@ Legacy key migration:
|
||||
Telegram:
|
||||
|
||||
- Uses `sendMessage` + `editMessageText` preview updates across DMs and group/topics.
|
||||
- Short initial previews are still debounced for push-notification UX, but Telegram now materializes them after a bounded delay so active runs do not stay visually silent.
|
||||
- Final text edits the active preview in place; long finals reuse that message for the first chunk and send only the remaining chunks.
|
||||
- `block` mode rotates the preview into a new message at `streaming.preview.chunk.maxChars` (default 800, capped at Telegram's 4096 edit limit); other modes grow one preview up to 4096 characters.
|
||||
- `progress` mode keeps tool progress in an editable status draft, clears that draft at completion, and sends the final answer through normal delivery.
|
||||
- `progress` mode keeps tool progress in an editable status draft, materializes the status label when answer streaming is active but no tool line is available yet, clears that draft at completion, and sends the final answer through normal delivery.
|
||||
- If the final edit fails before the completed text is confirmed, OpenClaw uses normal final delivery and cleans up the stale preview.
|
||||
- Preview streaming is skipped when Telegram block streaming is explicitly enabled (to avoid double-streaming).
|
||||
- `/reasoning stream` can write reasoning to a transient preview that is deleted after final delivery.
|
||||
|
||||
@@ -1122,6 +1122,7 @@
|
||||
"channels/mattermost",
|
||||
"channels/nextcloud-talk",
|
||||
"channels/nostr",
|
||||
"channels/raft",
|
||||
"channels/tlon",
|
||||
"channels/synology-chat",
|
||||
"channels/twitch"
|
||||
|
||||
@@ -249,9 +249,10 @@ Shared defaults for bounded runtime context surfaces.
|
||||
- `toolResultMaxChars`: advanced live tool-result ceiling used for persisted
|
||||
results and overflow recovery. Leave unset for the model-context auto cap:
|
||||
`16000` chars below 100K tokens, `32000` chars at 100K+ tokens, and `64000`
|
||||
chars at 200K+ tokens. The effective cap is still limited to about 30% of the
|
||||
model context window. `openclaw doctor --deep` prints the effective cap, and
|
||||
doctor warns only when an explicit override is stale or has no effect.
|
||||
chars at 200K+ tokens. Explicit values up to `1000000` are accepted for
|
||||
long-context models, but the effective cap is still limited to about 30% of
|
||||
the model context window. `openclaw doctor --deep` prints the effective cap,
|
||||
and doctor warns only when an explicit override is stale or has no effect.
|
||||
- `postCompactionMaxChars`: AGENTS.md excerpt cap used during post-compaction
|
||||
refresh injection.
|
||||
|
||||
@@ -1098,7 +1099,7 @@ for provider examples and precedence.
|
||||
- `skills`: optional per-agent skill allowlist. If omitted, the agent inherits `agents.defaults.skills` when set; an explicit list replaces defaults instead of merging, and `[]` means no skills.
|
||||
- `thinkingDefault`: optional per-agent default thinking level (`off | minimal | low | medium | high | xhigh | adaptive | max`). Overrides `agents.defaults.thinkingDefault` for this agent when no per-message or session override is set. The selected provider/model profile controls which values are valid; for Google Gemini, `adaptive` keeps provider-owned dynamic thinking (`thinkingLevel` omitted on Gemini 3/3.1, `thinkingBudget: -1` on Gemini 2.5).
|
||||
- `reasoningDefault`: optional per-agent default reasoning visibility (`on | off | stream`). Overrides `agents.defaults.reasoningDefault` for this agent when no per-message or session reasoning override is set.
|
||||
- `fastModeDefault`: optional per-agent default for fast mode (`true | false`). Applies when no per-message or session fast-mode override is set.
|
||||
- `fastModeDefault`: optional per-agent default for fast mode (`"auto" | true | false`). Applies when no per-message or session fast-mode override is set.
|
||||
- `models`: optional per-agent model catalog/runtime overrides keyed by full `provider/model` ids. Use `models["provider/model"].agentRuntime` for per-agent runtime exceptions.
|
||||
- `runtime`: optional per-agent runtime descriptor. Use `type: "acp"` with `runtime.acp` defaults (`agent`, `backend`, `mode`, `cwd`) when the agent should default to ACP harness sessions.
|
||||
- `identity.avatar`: workspace-relative path, `http(s)` URL, or `data:` URI.
|
||||
|
||||
@@ -160,7 +160,6 @@ must be paired with `--lint`; regular doctor and repair runs reject them.
|
||||
- State integrity and permissions checks (sessions, transcripts, state dir).
|
||||
- Config file permission checks (chmod 600) when running locally.
|
||||
- Model auth health: checks OAuth expiry, can refresh expiring tokens, and reports auth-profile cooldown/disabled states.
|
||||
- Extra workspace dir detection (`~/openclaw`).
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="Gateway, services, and supervisors">
|
||||
@@ -469,14 +468,14 @@ That stages grounded durable candidates into the short-term dreaming store while
|
||||
<Accordion title="10. systemd linger (Linux)">
|
||||
If running as a systemd user service, doctor ensures lingering is enabled so the gateway stays alive after logout.
|
||||
</Accordion>
|
||||
<Accordion title="11. Workspace status (skills, plugins, and legacy dirs)">
|
||||
<Accordion title="11. Workspace status (skills, plugins, and TaskFlows)">
|
||||
Doctor prints a summary of the workspace state for the default agent:
|
||||
|
||||
- **Skills status**: counts eligible, missing-requirements, and allowlist-blocked skills.
|
||||
- **Legacy workspace dirs**: warns when `~/openclaw` or other legacy workspace directories exist alongside the current workspace.
|
||||
- **Plugin status**: counts enabled/disabled/errored plugins; lists plugin IDs for any errors; reports bundle plugin capabilities.
|
||||
- **Plugin compatibility warnings**: flags plugins that have compatibility issues with the current runtime.
|
||||
- **Plugin diagnostics**: surfaces any load-time warnings or errors emitted by the plugin registry.
|
||||
- **TaskFlow recovery**: surfaces suspicious managed TaskFlows that need manual inspection or cancellation.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="11b. Bootstrap file size">
|
||||
|
||||
@@ -58,7 +58,14 @@ Methods:
|
||||
- `node.pair.list` - list pending + paired nodes (`operator.pairing`).
|
||||
- `node.pair.approve` - approve a pending request (issues token).
|
||||
- `node.pair.reject` - reject a pending request.
|
||||
- `node.pair.remove` - remove a stale paired node entry.
|
||||
- `node.pair.remove` - remove a paired node. For device-backed pairings this
|
||||
revokes the device's `node` role: it mutates `devices/paired.json` and
|
||||
invalidates/disconnects that device's node-role sessions. A **mixed-role**
|
||||
device (e.g. it also holds `operator`) keeps its row and only loses the `node`
|
||||
role; a node-only device row is deleted. It also removes any matching legacy
|
||||
gateway-owned node pairing entry. Authz: `operator.pairing` may remove
|
||||
non-operator node rows; a device-token caller revoking its **own** node role on
|
||||
a mixed-role device additionally needs `operator.admin`.
|
||||
- `node.pair.verify` - verify `{ nodeId, token }`.
|
||||
|
||||
Notes:
|
||||
|
||||
@@ -445,6 +445,7 @@ enumeration of `src/gateway/server-methods/*.ts`.
|
||||
- `sessions.get` returns the full stored session row.
|
||||
- Chat execution still uses `chat.history`, `chat.send`, `chat.abort`, and `chat.inject`. `chat.history` is display-normalized for UI clients: inline directive tags are stripped from visible text, plain-text tool-call XML payloads (including `<tool_call>...</tool_call>`, `<function_call>...</function_call>`, `<tool_calls>...</tool_calls>`, `<function_calls>...</function_calls>`, and truncated tool-call blocks) and leaked ASCII/full-width model control tokens are stripped, pure silent-token assistant rows such as exact `NO_REPLY` / `no_reply` are omitted, and oversized rows can be replaced with placeholders.
|
||||
- `chat.message.get` is the additive bounded full-message reader for a single visible transcript entry. Clients pass `sessionKey`, optional `agentId` when the session selection is agent-scoped, plus a transcript `messageId` previously surfaced through `chat.history`, and the Gateway returns the same display-normalized projection without the lightweight history truncation cap when the stored entry is still available and not oversized.
|
||||
- `chat.send` accepts one-turn `fastMode: "auto"` to use fast mode for model calls started before the auto cutoff, then start later retry, fallback, tool-result, or continuation calls without fast mode. The cutoff defaults to 60 seconds and can be configured per model with `agents.defaults.models["<provider>/<model>"].params.fastAutoOnSeconds`. A `chat.send` caller can pass one-turn `fastAutoOnSeconds` to override the cutoff for that request.
|
||||
|
||||
</Accordion>
|
||||
|
||||
|
||||
@@ -160,7 +160,7 @@ it disabled for read-only shared skill roots.
|
||||
|
||||
Related:
|
||||
|
||||
- [Skills config](/tools/skills-config#symlinked-sibling-repos)
|
||||
- [Skills config](/tools/skills-config#symlinked-skill-roots)
|
||||
- [Configuration examples](/gateway/configuration-examples#symlinked-sibling-skill-repo)
|
||||
|
||||
## Anthropic 429 extra usage required for long context
|
||||
|
||||
@@ -174,6 +174,7 @@ troubleshooting, see the main [FAQ](/help/faq).
|
||||
|
||||
- **Per session:** send `/fast on` while the session is using `openai/gpt-5.5`.
|
||||
- **Per model default:** set `agents.defaults.models["openai/gpt-5.5"].params.fastMode` to `true`.
|
||||
- **Automatic cutoff:** use `/fast auto` or `params.fastMode: "auto"` to start new model calls fast until the auto cutoff, then start later retry, fallback, tool-result, or continuation calls without fast mode. The cutoff defaults to 60 seconds; set `params.fastAutoOnSeconds` on the active model to change it.
|
||||
|
||||
Example:
|
||||
|
||||
@@ -184,7 +185,8 @@ troubleshooting, see the main [FAQ](/help/faq).
|
||||
models: {
|
||||
"openai/gpt-5.5": {
|
||||
params: {
|
||||
fastMode: true,
|
||||
fastMode: "auto",
|
||||
fastAutoOnSeconds: 30,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -193,7 +195,7 @@ troubleshooting, see the main [FAQ](/help/faq).
|
||||
}
|
||||
```
|
||||
|
||||
For OpenAI, fast mode maps to `service_tier = "priority"` on supported native Responses requests. Session `/fast` overrides beat config defaults.
|
||||
For OpenAI, fast mode maps to `service_tier = "priority"` on supported native Responses requests. Session `/fast` overrides beat config defaults. Codex app-server turns can only receive the tier at turn start, so `auto` applies on the next OpenClaw-started model turn rather than inside one already-running app-server turn.
|
||||
|
||||
See [Thinking and fast mode](/tools/thinking) and [OpenAI fast mode](/providers/openai#fast-mode).
|
||||
|
||||
|
||||
@@ -51,8 +51,14 @@ Notes:
|
||||
different role that pairing approval never granted.
|
||||
- `node.pair.*` (CLI: `openclaw nodes pending/approve/reject/remove/rename`) is a separate gateway-owned
|
||||
node pairing store; it does **not** gate the WS `connect` handshake.
|
||||
- `openclaw nodes remove --node <id|name|ip>` deletes stale entries from that
|
||||
separate gateway-owned node pairing store.
|
||||
- `openclaw nodes remove --node <id|name|ip>` removes a node pairing. For a
|
||||
device-backed node it revokes the device's `node` role in `devices/paired.json`
|
||||
and disconnects that device's node-role sessions — a mixed-role device keeps
|
||||
its row and only loses the `node` role, while a node-only device row is
|
||||
deleted. It also clears any matching entry from the separate gateway-owned node
|
||||
pairing store. `operator.pairing` may remove non-operator node rows; a
|
||||
device-token caller revoking its own node role on a mixed-role device
|
||||
additionally needs `operator.admin`.
|
||||
- Approval scope follows the pending request's declared commands:
|
||||
- commandless request: `operator.pairing`
|
||||
- non-exec node commands: `operator.pairing` + `operator.write`
|
||||
|
||||
@@ -15,15 +15,18 @@ OpenClaw treats **wake words as a single global list** owned by the **Gateway**.
|
||||
|
||||
## Storage (Gateway host)
|
||||
|
||||
Wake words are stored on the gateway machine at:
|
||||
Wake words and routing rules are stored in the gateway state database:
|
||||
|
||||
- `~/.openclaw/settings/voicewake.json`
|
||||
- `~/.openclaw/state/openclaw.sqlite`
|
||||
|
||||
Shape:
|
||||
The active tables are:
|
||||
|
||||
```json
|
||||
{ "triggers": ["openclaw", "claude", "computer"], "updatedAtMs": 1730000000000 }
|
||||
```
|
||||
- `voicewake_triggers`
|
||||
- `voicewake_routing_config`
|
||||
- `voicewake_routing_routes`
|
||||
|
||||
Legacy `settings/voicewake.json` and `settings/voicewake-routing.json` files are
|
||||
doctor migration inputs only; runtime reads and writes the SQLite tables.
|
||||
|
||||
## Protocol
|
||||
|
||||
|
||||
@@ -33,15 +33,12 @@ For the broader model/provider/runtime split, start with
|
||||
- A GitHub Copilot subscription that can drive the Copilot CLI (or a
|
||||
`gitHubToken` env / auth-profile entry for headless / cron runs).
|
||||
- A writable `copilotHome` directory. The harness defaults to
|
||||
`~/.openclaw/agents/<agentId>/copilot` for full per-agent isolation. The
|
||||
platform default (`%APPDATA%\copilot` on Windows, `$XDG_CONFIG_HOME/copilot`
|
||||
or `~/.config/copilot` elsewhere) is used as the doctor probe fallback when
|
||||
no explicit home is set.
|
||||
`<agentDir>/copilot` when OpenClaw provides an agent directory, otherwise
|
||||
`~/.openclaw/agents/<agentId>/copilot` for full per-agent isolation.
|
||||
|
||||
`openclaw doctor` runs the plugin
|
||||
[doctor contract](#doctor-and-probes) for the extension; failures there are
|
||||
the canonical way to confirm the environment is ready before opting an agent
|
||||
in.
|
||||
[doctor contract](#doctor) for declarative session-state ownership and future
|
||||
compatibility migrations. It does not run Copilot CLI environment probes.
|
||||
|
||||
## Plugin install
|
||||
|
||||
@@ -79,9 +76,9 @@ Pin one model (or one provider) to the harness:
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
model: "github-copilot/gpt-5.5",
|
||||
model: "github-copilot/auto",
|
||||
models: {
|
||||
"github-copilot/gpt-5.5": {
|
||||
"github-copilot/auto": {
|
||||
agentRuntime: { id: "copilot" },
|
||||
},
|
||||
},
|
||||
@@ -95,6 +92,10 @@ when only that model should be routed through the harness; set
|
||||
`agentRuntime.id` on a provider when every model under that provider should
|
||||
use it.
|
||||
|
||||
`github-copilot/auto` is the portable starting point. Named Copilot models are
|
||||
account- and organization-policy-dependent, so only pin one after confirming
|
||||
that the authenticated Copilot CLI exposes it.
|
||||
|
||||
## Supported providers
|
||||
|
||||
The harness advertises support for the canonical `github-copilot` provider
|
||||
@@ -149,10 +150,6 @@ the same directory), or `~/.openclaw/agents/<agentId>/copilot` otherwise.
|
||||
Override with `copilotHome: <path>` on the attempt input when you need a
|
||||
custom location (for example, a shared mount for migration).
|
||||
|
||||
`probeCopilotAuthShape` (see [Doctor and probes](#doctor-and-probes)) is the
|
||||
pure shape check that validates which of the modes above will be used.
|
||||
It does not perform a live SDK handshake.
|
||||
|
||||
## Configuration surface
|
||||
|
||||
The harness reads its config from per-attempt input
|
||||
@@ -169,8 +166,9 @@ The harness reads its config from per-attempt input
|
||||
- `infiniteSessionConfig` — optional override for the SDK
|
||||
`infiniteSessions` block driven by `harness.compact`. Defaults are safe to
|
||||
leave as-is.
|
||||
- `hooksConfig` — optional bridge config exposing OpenClaw
|
||||
before/after-message-write hooks to the SDK loop.
|
||||
- `hooksConfig` — optional native Copilot SDK `SessionHooks` compatibility
|
||||
config for tool/MCP, user-prompt, session, and error callbacks.
|
||||
It is separate from OpenClaw's portable lifecycle hooks.
|
||||
- `permissionPolicy` — optional override for the SDK's
|
||||
`onPermissionRequest` handler used for built-in SDK tool kinds
|
||||
(`shell`, `write`, `read`, `url`, `mcp`, `memory`, `hook`). Defaults
|
||||
@@ -181,6 +179,14 @@ The harness reads its config from per-attempt input
|
||||
wrapped `execute()`. See [Permissions and ask_user](#permissions-and-ask_user).
|
||||
- `enableSessionTelemetry` — optional SDK session telemetry flag.
|
||||
|
||||
OpenClaw plugin hooks do not need Copilot-specific attempt configuration. The
|
||||
harness runs `before_prompt_build` (and the legacy `before_agent_start`
|
||||
compatibility hook), `llm_input`, `llm_output`, and `agent_end` through the
|
||||
standard harness helpers. Successful SDK compactions also run
|
||||
`before_compaction` and `after_compaction`. Bridged OpenClaw tools continue to
|
||||
run `before_tool_call` and report `after_tool_call`; `hooksConfig` remains for
|
||||
native SDK-only callbacks that have no portable equivalent.
|
||||
|
||||
Nothing in the rest of OpenClaw needs to know about these fields. Other
|
||||
plugins, channels, and core code only see the standard
|
||||
`AgentHarnessAttemptParams` / `AgentHarnessAttemptResult` shape.
|
||||
@@ -226,7 +232,7 @@ asserted in
|
||||
[`extensions/copilot/harness.test.ts`](https://github.com/openclaw/openclaw/blob/main/extensions/copilot/harness.test.ts)
|
||||
under `describe("runSideQuestion")`.
|
||||
|
||||
## Doctor and probes
|
||||
## Doctor
|
||||
|
||||
`extensions/copilot/doctor-contract-api.ts` is auto-loaded by
|
||||
`src/plugins/doctor-contract-registry.ts`. It contributes:
|
||||
@@ -238,18 +244,6 @@ under `describe("runSideQuestion")`.
|
||||
runtime `copilot`; CLI session key `copilot`; auth profile
|
||||
prefix `github-copilot:`.
|
||||
|
||||
`extensions/copilot/src/doctor-probes.ts` exports three imperative probes
|
||||
that hosts (including `openclaw doctor`) can call to verify the environment:
|
||||
|
||||
| Probe | What it checks | Reasons it can fail |
|
||||
| -------------------------- | --------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
|
||||
| `probeCopilotCliVersion` | `copilot --version` exits 0 with a non-empty version string | `non-zero-exit`, `empty-version`, `spawn-failed`, `spawn-error`, `probe-timeout` |
|
||||
| `probeCopilotHomeWritable` | `mkdir -p copilotHome` + write + rm a marker file | `copilothome-not-writable` (with the underlying fs error in `details.rawError`) |
|
||||
| `probeCopilotAuthShape` | At least one of `useLoggedInUser`, `gitHubToken`, or `profileId`+`profileVersion` | `no-auth-source` |
|
||||
|
||||
Each probe accepts a DI seam (`spawnFn`, `fsApi`) so tests do not spawn the
|
||||
real Copilot CLI or touch the host fs.
|
||||
|
||||
## Limitations
|
||||
|
||||
- The harness only claims the canonical `github-copilot` provider at MVP.
|
||||
|
||||
@@ -146,6 +146,7 @@ observation-only.
|
||||
- `subagent_delivery_target` - compatibility hook for completion delivery when no core session binding can project a route.
|
||||
- `subagent_spawning` - deprecated compatibility hook. Core now prepares `thread: true` subagent bindings through channel session-binding adapters before `subagent_spawned` fires.
|
||||
- `subagent_spawned` includes `resolvedModel` and `resolvedProvider` when OpenClaw has resolved the child session's native model before launch.
|
||||
- `subagent_ended` carries `targetSessionKey` (identity — this matches `subagent_spawned.childSessionKey`), `targetKind` (`"subagent"` or `"acp"`), `reason`, optional `outcome` (`"ok"`, `"error"`, `"timeout"`, `"killed"`, `"reset"`, or `"deleted"`), optional `error`, `runId`, `endedAt`, `accountId`, and `sendFarewell`. It does **not** include `agentId` or `childSessionKey`; use `targetSessionKey` to correlate with the corresponding `subagent_spawned` event.
|
||||
|
||||
**Lifecycle**
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ Each entry lists the package, distribution route, and description.
|
||||
|
||||
## Core npm package
|
||||
|
||||
73 plugins
|
||||
59 plugins
|
||||
|
||||
- **[admin-http-rpc](/plugins/reference/admin-http-rpc)** (`@openclaw/admin-http-rpc`) - included in OpenClaw. OpenClaw admin HTTP RPC endpoint.
|
||||
|
||||
@@ -69,8 +69,6 @@ Each entry lists the package, distribution route, and description.
|
||||
|
||||
- **[canvas](/plugins/reference/canvas)** (`@openclaw/canvas-plugin`) - included in OpenClaw. Experimental Canvas control and A2UI rendering surfaces for paired nodes.
|
||||
|
||||
- **[clickclack](/plugins/reference/clickclack)** (`@openclaw/clickclack`) - included in OpenClaw. Adds the Clickclack channel surface for sending and receiving OpenClaw messages.
|
||||
|
||||
- **[codex-supervisor](/plugins/reference/codex-supervisor)** (`@openclaw/codex-supervisor`) - included in OpenClaw. Supervise Codex app-server sessions from OpenClaw.
|
||||
|
||||
- **[cohere](/plugins/reference/cohere)** (`@openclaw/cohere-provider`) - included in OpenClaw; npm; ClawHub: `clawhub:@openclaw/cohere-provider`. OpenClaw Cohere provider plugin.
|
||||
@@ -89,12 +87,8 @@ Each entry lists the package, distribution route, and description.
|
||||
|
||||
- **[fal](/plugins/reference/fal)** (`@openclaw/fal-provider`) - included in OpenClaw. Adds fal model provider support to OpenClaw.
|
||||
|
||||
- **[feeds](/plugins/reference/feeds)** (`@openclaw/feeds`) - included in OpenClaw. Adds configured catalog feed source validation for skills and plugins.
|
||||
|
||||
- **[file-transfer](/plugins/reference/file-transfer)** (`@openclaw/file-transfer`) - included in OpenClaw. Fetch, list, and write files on paired nodes via dedicated node commands. Bypasses bash stdout truncation by using base64 over node.invoke for binaries up to 16 MB.
|
||||
|
||||
- **[fireworks](/plugins/reference/fireworks)** (`@openclaw/fireworks-provider`) - included in OpenClaw. Adds Fireworks model provider support to OpenClaw.
|
||||
|
||||
- **[github-copilot](/plugins/reference/github-copilot)** (`@openclaw/github-copilot-provider`) - included in OpenClaw. Adds GitHub Copilot model provider support to OpenClaw.
|
||||
|
||||
- **[google](/plugins/reference/google)** (`@openclaw/google-plugin`) - included in OpenClaw. Adds Google, Google Gemini CLI, Google Vertex model provider support to OpenClaw.
|
||||
@@ -103,16 +97,12 @@ Each entry lists the package, distribution route, and description.
|
||||
|
||||
- **[imessage](/plugins/reference/imessage)** (`@openclaw/imessage`) - included in OpenClaw. Adds the iMessage channel surface for sending and receiving OpenClaw messages.
|
||||
|
||||
- **[irc](/plugins/reference/irc)** (`@openclaw/irc`) - included in OpenClaw. Adds the IRC channel surface for sending and receiving OpenClaw messages.
|
||||
|
||||
- **[litellm](/plugins/reference/litellm)** (`@openclaw/litellm-provider`) - included in OpenClaw. Adds LiteLLM model provider support to OpenClaw.
|
||||
|
||||
- **[llm-task](/plugins/reference/llm-task)** (`@openclaw/llm-task`) - included in OpenClaw. Generic JSON-only LLM tool for structured tasks callable from workflows.
|
||||
|
||||
- **[lmstudio](/plugins/reference/lmstudio)** (`@openclaw/lmstudio-provider`) - included in OpenClaw. Adds LM Studio model provider support to OpenClaw.
|
||||
|
||||
- **[mattermost](/plugins/reference/mattermost)** (`@openclaw/mattermost`) - included in OpenClaw. Adds the Mattermost channel surface for sending and receiving OpenClaw messages.
|
||||
|
||||
- **[memory-core](/plugins/reference/memory-core)** (`@openclaw/memory-core`) - included in OpenClaw. Adds agent-callable tools.
|
||||
|
||||
- **[memory-wiki](/plugins/reference/memory-wiki)** (`@openclaw/memory-wiki`) - included in OpenClaw. Persistent wiki compiler and Obsidian-friendly knowledge vault for OpenClaw.
|
||||
@@ -129,8 +119,6 @@ Each entry lists the package, distribution route, and description.
|
||||
|
||||
- **[mistral](/plugins/reference/mistral)** (`@openclaw/mistral-provider`) - included in OpenClaw. Adds Mistral model provider support to OpenClaw.
|
||||
|
||||
- **[moonshot](/plugins/reference/moonshot)** (`@openclaw/moonshot-provider`) - included in OpenClaw. Adds Moonshot model provider support to OpenClaw.
|
||||
|
||||
- **[novita](/plugins/reference/novita)** (`@openclaw/novita-provider`) - included in OpenClaw. Adds Novita, Novita AI, Novitaai model provider support to OpenClaw.
|
||||
|
||||
- **[nvidia](/plugins/reference/nvidia)** (`@openclaw/nvidia-provider`) - included in OpenClaw. Adds NVIDIA model provider support to OpenClaw.
|
||||
@@ -153,32 +141,18 @@ Each entry lists the package, distribution route, and description.
|
||||
|
||||
- **[runway](/plugins/reference/runway)** (`@openclaw/runway-provider`) - included in OpenClaw. Adds video generation provider support.
|
||||
|
||||
- **[searxng](/plugins/reference/searxng)** (`@openclaw/searxng-plugin`) - included in OpenClaw. Adds web search provider support.
|
||||
|
||||
- **[senseaudio](/plugins/reference/senseaudio)** (`@openclaw/senseaudio-provider`) - included in OpenClaw. Adds media understanding provider support.
|
||||
|
||||
- **[sglang](/plugins/reference/sglang)** (`@openclaw/sglang-provider`) - included in OpenClaw. Adds SGLang model provider support to OpenClaw.
|
||||
|
||||
- **[signal](/plugins/reference/signal)** (`@openclaw/signal`) - included in OpenClaw. Adds the Signal channel surface for sending and receiving OpenClaw messages.
|
||||
|
||||
- **[sms](/plugins/reference/sms)** (`@openclaw/sms`) - included in OpenClaw. Twilio SMS channel plugin for OpenClaw text messages.
|
||||
|
||||
- **[synthetic](/plugins/reference/synthetic)** (`@openclaw/synthetic-provider`) - included in OpenClaw. Adds Synthetic model provider support to OpenClaw.
|
||||
|
||||
- **[tavily](/plugins/reference/tavily)** (`@openclaw/tavily-plugin`) - included in OpenClaw. Adds agent-callable tools. Adds web search provider support.
|
||||
|
||||
- **[telegram](/plugins/reference/telegram)** (`@openclaw/telegram`) - included in OpenClaw. Adds the Telegram channel surface for sending and receiving OpenClaw messages.
|
||||
|
||||
- **[tencent](/plugins/reference/tencent)** (`@openclaw/tencent-provider`) - included in OpenClaw. Adds Tencent TokenHub model provider support to OpenClaw.
|
||||
|
||||
- **[together](/plugins/reference/together)** (`@openclaw/together-provider`) - included in OpenClaw. Adds Together model provider support to OpenClaw.
|
||||
|
||||
- **[tts-local-cli](/plugins/reference/tts-local-cli)** (`@openclaw/tts-local-cli`) - included in OpenClaw. Adds text-to-speech provider support.
|
||||
|
||||
- **[venice](/plugins/reference/venice)** (`@openclaw/venice-provider`) - included in OpenClaw. Adds Venice model provider support to OpenClaw.
|
||||
|
||||
- **[vercel-ai-gateway](/plugins/reference/vercel-ai-gateway)** (`@openclaw/vercel-ai-gateway-provider`) - included in OpenClaw. Adds Vercel AI Gateway model provider support to OpenClaw.
|
||||
|
||||
- **[vllm](/plugins/reference/vllm)** (`@openclaw/vllm-provider`) - included in OpenClaw. Adds vLLM model provider support to OpenClaw.
|
||||
|
||||
- **[volcengine](/plugins/reference/volcengine)** (`@openclaw/volcengine-provider`) - included in OpenClaw. Adds Volcengine, Volcengine Plan model provider support to OpenClaw.
|
||||
@@ -197,11 +171,9 @@ Each entry lists the package, distribution route, and description.
|
||||
|
||||
- **[xiaomi](/plugins/reference/xiaomi)** (`@openclaw/xiaomi-provider`) - included in OpenClaw. Adds Xiaomi, Xiaomi Token Plan model provider support to OpenClaw.
|
||||
|
||||
- **[zai](/plugins/reference/zai)** (`@openclaw/zai-provider`) - included in OpenClaw. Adds Z.AI model provider support to OpenClaw.
|
||||
|
||||
## Official external packages
|
||||
|
||||
54 plugins
|
||||
68 plugins
|
||||
|
||||
- **[acpx](/plugins/reference/acpx)** (`@openclaw/acpx`) - npm; ClawHub. OpenClaw ACP runtime backend with plugin-owned session and transport management.
|
||||
|
||||
@@ -219,6 +191,8 @@ Each entry lists the package, distribution route, and description.
|
||||
|
||||
- **[chutes](/plugins/reference/chutes)** (`@openclaw/chutes-provider`) - npm; ClawHub: `clawhub:@openclaw/chutes-provider`. Adds Chutes model provider support to OpenClaw.
|
||||
|
||||
- **[clickclack](/plugins/reference/clickclack)** (`@openclaw/clickclack`) - npm; ClawHub: `clawhub:@openclaw/clickclack`. Adds the Clickclack channel surface for sending and receiving OpenClaw messages.
|
||||
|
||||
- **[cloudflare-ai-gateway](/plugins/reference/cloudflare-ai-gateway)** (`@openclaw/cloudflare-ai-gateway-provider`) - npm; ClawHub: `clawhub:@openclaw/cloudflare-ai-gateway-provider`. Adds Cloudflare AI Gateway model provider support to OpenClaw.
|
||||
|
||||
- **[codex](/plugins/reference/codex)** (`@openclaw/codex`) - npm; ClawHub. OpenClaw Codex app-server harness and model provider plugin with a Codex-managed GPT catalog.
|
||||
@@ -245,6 +219,8 @@ Each entry lists the package, distribution route, and description.
|
||||
|
||||
- **[firecrawl](/plugins/reference/firecrawl)** (`@openclaw/firecrawl-plugin`) - npm; ClawHub: `clawhub:@openclaw/firecrawl-plugin`. Adds agent-callable tools. Adds web fetch provider support. Adds web search provider support.
|
||||
|
||||
- **[fireworks](/plugins/reference/fireworks)** (`@openclaw/fireworks-provider`) - npm; ClawHub: `clawhub:@openclaw/fireworks-provider`. Adds Fireworks model provider support to OpenClaw.
|
||||
|
||||
- **[gmi](/plugins/reference/gmi)** (`@openclaw/gmi-provider`) - npm; ClawHub: `clawhub:@openclaw/gmi-provider`. OpenClaw GMI Cloud provider plugin.
|
||||
|
||||
- **[google-meet](/plugins/reference/google-meet)** (`@openclaw/google-meet`) - npm; ClawHub. OpenClaw Google Meet participant plugin for joining calls through Chrome or Twilio transports.
|
||||
@@ -257,6 +233,8 @@ Each entry lists the package, distribution route, and description.
|
||||
|
||||
- **[inworld](/plugins/reference/inworld)** (`@openclaw/inworld-speech`) - npm; ClawHub: `clawhub:@openclaw/inworld-speech`. Inworld streaming text-to-speech (MP3, OGG_OPUS, PCM telephony).
|
||||
|
||||
- **[irc](/plugins/reference/irc)** (`@openclaw/irc`) - npm; ClawHub: `clawhub:@openclaw/irc`. Adds the IRC channel surface for sending and receiving OpenClaw messages.
|
||||
|
||||
- **[kilocode](/plugins/reference/kilocode)** (`@openclaw/kilocode-provider`) - npm; ClawHub: `clawhub:@openclaw/kilocode-provider`. Adds Kilocode model provider support to OpenClaw.
|
||||
|
||||
- **[kimi](/plugins/reference/kimi)** (`@openclaw/kimi-provider`) - npm; ClawHub: `clawhub:@openclaw/kimi-provider`. Adds Kimi, Kimi Coding model provider support to OpenClaw.
|
||||
@@ -269,8 +247,12 @@ Each entry lists the package, distribution route, and description.
|
||||
|
||||
- **[matrix](/plugins/reference/matrix)** (`@openclaw/matrix`) - ClawHub: `clawhub:@openclaw/matrix`; npm. OpenClaw Matrix channel plugin for rooms and direct messages.
|
||||
|
||||
- **[mattermost](/plugins/reference/mattermost)** (`@openclaw/mattermost`) - npm; ClawHub: `clawhub:@openclaw/mattermost`. Adds the Mattermost channel surface for sending and receiving OpenClaw messages.
|
||||
|
||||
- **[memory-lancedb](/plugins/reference/memory-lancedb)** (`@openclaw/memory-lancedb`) - npm; ClawHub. OpenClaw LanceDB-backed long-term memory plugin with auto-recall, auto-capture, and vector search.
|
||||
|
||||
- **[moonshot](/plugins/reference/moonshot)** (`@openclaw/moonshot-provider`) - npm; ClawHub: `clawhub:@openclaw/moonshot-provider`. Adds Moonshot model provider support to OpenClaw.
|
||||
|
||||
- **[msteams](/plugins/reference/msteams)** (`@openclaw/msteams`) - npm; ClawHub. OpenClaw Microsoft Teams channel plugin for bot conversations.
|
||||
|
||||
- **[nextcloud-talk](/plugins/reference/nextcloud-talk)** (`@openclaw/nextcloud-talk`) - npm; ClawHub. OpenClaw Nextcloud Talk channel plugin for conversations.
|
||||
@@ -291,22 +273,40 @@ Each entry lists the package, distribution route, and description.
|
||||
|
||||
- **[qwen](/plugins/reference/qwen)** (`@openclaw/qwen-provider`) - npm; ClawHub: `clawhub:@openclaw/qwen-provider`. Adds Qwen, Qwen Cloud, Model Studio, DashScope, Qwen Oauth, Qwen Portal, Qwen CLI model provider support to OpenClaw.
|
||||
|
||||
- **[raft](/plugins/reference/raft)** (`@openclaw/raft`) - npm; ClawHub. OpenClaw Raft channel plugin for secure CLI wake bridges.
|
||||
|
||||
- **[searxng](/plugins/reference/searxng)** (`@openclaw/searxng-plugin`) - npm; ClawHub: `clawhub:@openclaw/searxng-plugin`. Adds web search provider support.
|
||||
|
||||
- **[signal](/plugins/reference/signal)** (`@openclaw/signal`) - npm; ClawHub: `clawhub:@openclaw/signal`. Adds the Signal channel surface for sending and receiving OpenClaw messages.
|
||||
|
||||
- **[slack](/plugins/reference/slack)** (`@openclaw/slack`) - npm; ClawHub. OpenClaw Slack channel plugin for channels, DMs, commands, and app events.
|
||||
|
||||
- **[stepfun](/plugins/reference/stepfun)** (`@openclaw/stepfun-provider`) - npm. Adds StepFun, StepFun Plan model provider support to OpenClaw.
|
||||
- **[sms](/plugins/reference/sms)** (`@openclaw/sms`) - npm; ClawHub: `clawhub:@openclaw/sms`. Twilio SMS channel plugin for OpenClaw text messages.
|
||||
|
||||
- **[stepfun](/plugins/reference/stepfun)** (`@openclaw/stepfun-provider`) - npm; ClawHub: `clawhub:@openclaw/stepfun-provider`. Adds StepFun, StepFun Plan model provider support to OpenClaw.
|
||||
|
||||
- **[synology-chat](/plugins/reference/synology-chat)** (`@openclaw/synology-chat`) - npm; ClawHub. Synology Chat channel plugin for OpenClaw channels and direct messages.
|
||||
|
||||
- **[tavily](/plugins/reference/tavily)** (`@openclaw/tavily-plugin`) - npm; ClawHub: `clawhub:@openclaw/tavily-plugin`. Adds agent-callable tools. Adds web search provider support.
|
||||
|
||||
- **[tencent](/plugins/reference/tencent)** (`@openclaw/tencent-provider`) - npm; ClawHub: `clawhub:@openclaw/tencent-provider`. Adds Tencent TokenHub model provider support to OpenClaw.
|
||||
|
||||
- **[tlon](/plugins/reference/tlon)** (`@openclaw/tlon`) - npm; ClawHub. OpenClaw Tlon/Urbit channel plugin for chat workflows.
|
||||
|
||||
- **[tokenjuice](/plugins/reference/tokenjuice)** (`@openclaw/tokenjuice`) - npm; ClawHub: `clawhub:@openclaw/tokenjuice`. Compacts exec and bash tool results with tokenjuice reducers.
|
||||
|
||||
- **[twitch](/plugins/reference/twitch)** (`@openclaw/twitch`) - npm; ClawHub. OpenClaw Twitch channel plugin for chat and moderation workflows.
|
||||
|
||||
- **[venice](/plugins/reference/venice)** (`@openclaw/venice-provider`) - npm; ClawHub: `clawhub:@openclaw/venice-provider`. Adds Venice model provider support to OpenClaw.
|
||||
|
||||
- **[vercel-ai-gateway](/plugins/reference/vercel-ai-gateway)** (`@openclaw/vercel-ai-gateway-provider`) - npm; ClawHub: `clawhub:@openclaw/vercel-ai-gateway-provider`. Adds Vercel AI Gateway model provider support to OpenClaw.
|
||||
|
||||
- **[voice-call](/plugins/reference/voice-call)** (`@openclaw/voice-call`) - npm; ClawHub. OpenClaw voice-call plugin for Twilio, Telnyx, and Plivo phone calls.
|
||||
|
||||
- **[whatsapp](/plugins/reference/whatsapp)** (`@openclaw/whatsapp`) - ClawHub: `clawhub:@openclaw/whatsapp`; npm. OpenClaw WhatsApp channel plugin for WhatsApp Web chats.
|
||||
|
||||
- **[zai](/plugins/reference/zai)** (`@openclaw/zai-provider`) - npm; ClawHub: `clawhub:@openclaw/zai-provider`. Adds Z.AI model provider support to OpenClaw.
|
||||
|
||||
- **[zalo](/plugins/reference/zalo)** (`@openclaw/zalo`) - npm; ClawHub. OpenClaw Zalo channel plugin for bot and webhook chats.
|
||||
|
||||
- **[zalouser](/plugins/reference/zalouser)** (`@openclaw/zalouser`) - npm; ClawHub. OpenClaw Zalo Personal Account plugin via native zca-js integration.
|
||||
|
||||
@@ -16,4 +16,4 @@ Experimental Canvas control and A2UI rendering surfaces for paired nodes.
|
||||
|
||||
## Surface
|
||||
|
||||
contracts: tools
|
||||
contracts: tools; skills
|
||||
|
||||
@@ -12,7 +12,7 @@ Adds the Clickclack channel surface for sending and receiving OpenClaw messages.
|
||||
## Distribution
|
||||
|
||||
- Package: `@openclaw/clickclack`
|
||||
- Install route: included in OpenClaw
|
||||
- Install route: npm; ClawHub: `clawhub:@openclaw/clickclack`
|
||||
|
||||
## Surface
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ OpenClaw Discord channel plugin for channels, DMs, commands, and app events.
|
||||
|
||||
## Surface
|
||||
|
||||
channels: discord; contracts: transcriptSourceProviders
|
||||
channels: discord; contracts: transcriptSourceProviders; skills
|
||||
|
||||
## Related docs
|
||||
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
---
|
||||
summary: "Adds configured catalog feed source validation for skills and plugins."
|
||||
read_when:
|
||||
- You are installing, configuring, or auditing the feeds plugin
|
||||
title: "Feeds plugin"
|
||||
---
|
||||
|
||||
# Feeds plugin
|
||||
|
||||
Adds configured catalog feed source validation for skills and plugins.
|
||||
|
||||
## Distribution
|
||||
|
||||
- Package: `@openclaw/feeds`
|
||||
- Install route: included in OpenClaw
|
||||
|
||||
## Surface
|
||||
|
||||
plugin
|
||||
@@ -12,7 +12,7 @@ Adds Fireworks model provider support to OpenClaw.
|
||||
## Distribution
|
||||
|
||||
- Package: `@openclaw/fireworks-provider`
|
||||
- Install route: included in OpenClaw
|
||||
- Install route: npm; ClawHub: `clawhub:@openclaw/fireworks-provider`
|
||||
|
||||
## Surface
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ Adds the IRC channel surface for sending and receiving OpenClaw messages.
|
||||
## Distribution
|
||||
|
||||
- Package: `@openclaw/irc`
|
||||
- Install route: included in OpenClaw
|
||||
- Install route: npm; ClawHub: `clawhub:@openclaw/irc`
|
||||
|
||||
## Surface
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ Adds the Mattermost channel surface for sending and receiving OpenClaw messages.
|
||||
## Distribution
|
||||
|
||||
- Package: `@openclaw/mattermost`
|
||||
- Install route: included in OpenClaw
|
||||
- Install route: npm; ClawHub: `clawhub:@openclaw/mattermost`
|
||||
|
||||
## Surface
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user