mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-08 06:51:49 +08:00
Compare commits
820 Commits
fix/sqlite
...
feat/node-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bfd9fcac18 | ||
|
|
4f7b5d8f44 | ||
|
|
32caafd4ed | ||
|
|
60becfb941 | ||
|
|
3f4ea59779 | ||
|
|
cde2b5f718 | ||
|
|
2af75a93c2 | ||
|
|
571179c80b | ||
|
|
f0e5fdc064 | ||
|
|
f24ae91842 | ||
|
|
ec22756340 | ||
|
|
e1b5fd2716 | ||
|
|
437a5a71ae | ||
|
|
a8154f425c | ||
|
|
a6ecc4bd89 | ||
|
|
8c94131c0d | ||
|
|
e825301393 | ||
|
|
a84910be91 | ||
|
|
390a35d441 | ||
|
|
f4c448f65b | ||
|
|
c5d6764f56 | ||
|
|
7f6af117f2 | ||
|
|
f0cb1a93e5 | ||
|
|
d89d352971 | ||
|
|
3f6268ebd7 | ||
|
|
f3128f92d0 | ||
|
|
ec56a94ba3 | ||
|
|
f0256be48d | ||
|
|
9de73ab6d2 | ||
|
|
6bb91b2971 | ||
|
|
840eaf9c19 | ||
|
|
2f6d4b811c | ||
|
|
509fa621de | ||
|
|
6a95c8724a | ||
|
|
82de5903d7 | ||
|
|
64b288be64 | ||
|
|
768143af06 | ||
|
|
1e311058bc | ||
|
|
7d216c2945 | ||
|
|
fd8c789d42 | ||
|
|
deb9f11897 | ||
|
|
41d5e685ef | ||
|
|
ca72d2706e | ||
|
|
81d9c2f41f | ||
|
|
a0a115d466 | ||
|
|
501adb2524 | ||
|
|
1a4732410a | ||
|
|
8779bc49e0 | ||
|
|
15afc1d34c | ||
|
|
8b4d12e161 | ||
|
|
4c5b423fb8 | ||
|
|
bd76296c21 | ||
|
|
360b2c9699 | ||
|
|
aa9cc80060 | ||
|
|
861bf541c2 | ||
|
|
c8ac4c8aea | ||
|
|
8e371cfea1 | ||
|
|
dc23e924ef | ||
|
|
a3f495eb09 | ||
|
|
1e438739bc | ||
|
|
4d8502804d | ||
|
|
d72184d3e0 | ||
|
|
7e0ee6d5c8 | ||
|
|
2da49ef4ac | ||
|
|
fba99cddc1 | ||
|
|
d76301e0ab | ||
|
|
043929e76d | ||
|
|
c9c8125941 | ||
|
|
b4a63886af | ||
|
|
5a6eddf5d0 | ||
|
|
ba72fb5b43 | ||
|
|
fc1848a28b | ||
|
|
aa12e7cda9 | ||
|
|
9093556647 | ||
|
|
ffc6bc0be0 | ||
|
|
1f52854c0d | ||
|
|
170df6612e | ||
|
|
eb6be3cf62 | ||
|
|
53aa5232bc | ||
|
|
ba82257e37 | ||
|
|
5f7095f8be | ||
|
|
d6e4c879e8 | ||
|
|
ef6f4c1544 | ||
|
|
9b42f399a1 | ||
|
|
347ed87a96 | ||
|
|
3d168074b4 | ||
|
|
cc296f3a46 | ||
|
|
f39aff1558 | ||
|
|
ea9f791a68 | ||
|
|
d5ce1edf7e | ||
|
|
5c71f2190b | ||
|
|
c70e2bd2b3 | ||
|
|
ba02f12464 | ||
|
|
4e6fbf73a2 | ||
|
|
a0fa579cdc | ||
|
|
a98f292a11 | ||
|
|
56ae6d3c1a | ||
|
|
0e427e6cdc | ||
|
|
caa9078e70 | ||
|
|
099584676b | ||
|
|
1ad9109a6c | ||
|
|
1e8609af5d | ||
|
|
a3f21d03e8 | ||
|
|
d045deb79d | ||
|
|
c65eacae17 | ||
|
|
6c259af759 | ||
|
|
266dcf33f2 | ||
|
|
b380cdc84e | ||
|
|
0f73e09769 | ||
|
|
2d3b378876 | ||
|
|
3ac506a887 | ||
|
|
22f21ed7e6 | ||
|
|
bca7d18c60 | ||
|
|
9932ba7359 | ||
|
|
53978b358a | ||
|
|
3f16f2e9a5 | ||
|
|
4a6dc1b830 | ||
|
|
7766d2b65b | ||
|
|
01eefa7f96 | ||
|
|
ff254a44c9 | ||
|
|
22f2a91c2d | ||
|
|
1f57a946ca | ||
|
|
a09594b4ac | ||
|
|
e7de27f8b0 | ||
|
|
ec4a871f91 | ||
|
|
7af2673965 | ||
|
|
a9224f6f5d | ||
|
|
ca24dd7793 | ||
|
|
f4ac968577 | ||
|
|
939fe702a6 | ||
|
|
dca200ade5 | ||
|
|
58f00707ed | ||
|
|
3da0803ab4 | ||
|
|
40661e9d19 | ||
|
|
57ec0b236f | ||
|
|
88a0fc69f0 | ||
|
|
4a93974a90 | ||
|
|
d3e5959669 | ||
|
|
f8f7ba8f01 | ||
|
|
1a8d237369 | ||
|
|
b491058e88 | ||
|
|
1df9bca8e2 | ||
|
|
48d67e88d0 | ||
|
|
8605076a6f | ||
|
|
c1b54fe01e | ||
|
|
117bb3c61c | ||
|
|
c31877464c | ||
|
|
4653454c91 | ||
|
|
287a62c2fd | ||
|
|
a0cdd4e305 | ||
|
|
3140bb695d | ||
|
|
ab0a633ab9 | ||
|
|
d4523cba74 | ||
|
|
0d2a9073f5 | ||
|
|
4c5b2cf2e2 | ||
|
|
829847292e | ||
|
|
8048ceca71 | ||
|
|
a1c6a6e36f | ||
|
|
bc2294b413 | ||
|
|
b4e47ae395 | ||
|
|
51f9082873 | ||
|
|
85b4bd6c7b | ||
|
|
023427b1d5 | ||
|
|
5864669b3b | ||
|
|
e0fe08ccce | ||
|
|
a5880a3747 | ||
|
|
60cb5d633f | ||
|
|
fc7f96c826 | ||
|
|
6cde30a77c | ||
|
|
82d4d989d0 | ||
|
|
69df4c9136 | ||
|
|
689bafd16f | ||
|
|
d91f645d28 | ||
|
|
c7c67fc790 | ||
|
|
9dcf42472b | ||
|
|
0f53d0000c | ||
|
|
6365951160 | ||
|
|
838bc724ec | ||
|
|
ff39de4806 | ||
|
|
dfde0ce1a6 | ||
|
|
dd8f491040 | ||
|
|
a31d3355cd | ||
|
|
cd26595d6f | ||
|
|
810f29b5f6 | ||
|
|
0b6aad58f2 | ||
|
|
e78ef6fbad | ||
|
|
315cdd42fb | ||
|
|
85df2e1f85 | ||
|
|
94555a5898 | ||
|
|
998adc707f | ||
|
|
77d0792e02 | ||
|
|
0241665795 | ||
|
|
100be0e55a | ||
|
|
31b4575172 | ||
|
|
17fc1c430f | ||
|
|
a767c6d1df | ||
|
|
8fb70a90bd | ||
|
|
429bf9fe84 | ||
|
|
a44c5ee3f7 | ||
|
|
bb6c3ce262 | ||
|
|
ddc832ead1 | ||
|
|
d216322640 | ||
|
|
2761e8cc3b | ||
|
|
407f4777d2 | ||
|
|
a3c44d53d1 | ||
|
|
feeaff20ab | ||
|
|
975d40d474 | ||
|
|
0d35da9cc4 | ||
|
|
d1bf769dbd | ||
|
|
77f09f2575 | ||
|
|
f51126f0fa | ||
|
|
31ce6dfc4c | ||
|
|
875c9fd96d | ||
|
|
03a2f6f89d | ||
|
|
93e75f646f | ||
|
|
9d2c7bcb66 | ||
|
|
a10dfb7185 | ||
|
|
a0e19507e3 | ||
|
|
c74fd6f015 | ||
|
|
fbac4a2ec7 | ||
|
|
3f16b96ddc | ||
|
|
0e3f7a82fd | ||
|
|
8b29ff5f16 | ||
|
|
fff04af46d | ||
|
|
c87c1569d5 | ||
|
|
d00d10f172 | ||
|
|
8d65e78a07 | ||
|
|
726bc2b6c7 | ||
|
|
cb4f6af504 | ||
|
|
81c8f525eb | ||
|
|
1794efbba1 | ||
|
|
9b7ad2441f | ||
|
|
976ea3ff50 | ||
|
|
f0b3c4164f | ||
|
|
ea6d3232ca | ||
|
|
abb09b93cb | ||
|
|
e11e4e8935 | ||
|
|
2939ac6b72 | ||
|
|
ae948fa429 | ||
|
|
d09e0740e5 | ||
|
|
09467b1b87 | ||
|
|
97cdf8e7ac | ||
|
|
7dead6537a | ||
|
|
d3cabb0fc6 | ||
|
|
c100ae1f36 | ||
|
|
3bae50af7f | ||
|
|
fe70a2f5a6 | ||
|
|
6c89ef9c3a | ||
|
|
0812b7e3a8 | ||
|
|
d4ed8964d3 | ||
|
|
12efbcaa7e | ||
|
|
47f0af0d2d | ||
|
|
9d7f83b175 | ||
|
|
73752f07f2 | ||
|
|
feb6dc6bb6 | ||
|
|
865bd10bda | ||
|
|
945a7fdb36 | ||
|
|
c6a6f56699 | ||
|
|
09df56ee1f | ||
|
|
473f651e09 | ||
|
|
4912342dd7 | ||
|
|
d4ac91d8f0 | ||
|
|
c0b3c8cdb9 | ||
|
|
0913b6989c | ||
|
|
9d6e8b872a | ||
|
|
f1e6177331 | ||
|
|
3b914ca40b | ||
|
|
688777ca79 | ||
|
|
d2ff1c31d6 | ||
|
|
c0e9797644 | ||
|
|
3e2d56469b | ||
|
|
3e6978770a | ||
|
|
51f7844c43 | ||
|
|
2016d32187 | ||
|
|
90f9f2c2e4 | ||
|
|
8dc5b9afcd | ||
|
|
4fcc7537ff | ||
|
|
7beeedbe73 | ||
|
|
6a6da54062 | ||
|
|
efda5918b5 | ||
|
|
5e8190b779 | ||
|
|
98df83079d | ||
|
|
0c4ea11b06 | ||
|
|
695e181179 | ||
|
|
4eb3d1fae9 | ||
|
|
118060157d | ||
|
|
14962b2825 | ||
|
|
7385c611fc | ||
|
|
b9aade4b12 | ||
|
|
3a335c6df1 | ||
|
|
0a351cdf7f | ||
|
|
52b07b4a46 | ||
|
|
fa3901e665 | ||
|
|
4c32553875 | ||
|
|
63dfa848a6 | ||
|
|
03490ba1b9 | ||
|
|
367be94676 | ||
|
|
4af066b013 | ||
|
|
20c4e9475a | ||
|
|
98187f3277 | ||
|
|
f7e54acec1 | ||
|
|
169a4159de | ||
|
|
a94a939626 | ||
|
|
69c27677f6 | ||
|
|
cb5d43ba95 | ||
|
|
8946648ace | ||
|
|
09cee22249 | ||
|
|
5522268b24 | ||
|
|
e2a5823c83 | ||
|
|
dd0dd662a1 | ||
|
|
ca9249e357 | ||
|
|
cc73ef8ba5 | ||
|
|
67ddda2a21 | ||
|
|
15f3903b6f | ||
|
|
ecb30fece4 | ||
|
|
71bda851d1 | ||
|
|
08fd123906 | ||
|
|
7d3f1963d3 | ||
|
|
6aed185ccb | ||
|
|
041fab7b72 | ||
|
|
1ce11fbf42 | ||
|
|
2cbaacda43 | ||
|
|
bf1634b17a | ||
|
|
3894fe11ca | ||
|
|
589b1f6aec | ||
|
|
600a57e60f | ||
|
|
f84460e625 | ||
|
|
735587dde0 | ||
|
|
8fdfb2d7e3 | ||
|
|
88f78190ee | ||
|
|
15361bfe07 | ||
|
|
e8895f0a99 | ||
|
|
8e9a4e99f5 | ||
|
|
b031913031 | ||
|
|
585e89adbe | ||
|
|
46b826944c | ||
|
|
33bda2629a | ||
|
|
e95e51a24f | ||
|
|
fa6be505ef | ||
|
|
20577f0b3b | ||
|
|
f5ccfb7319 | ||
|
|
6719528316 | ||
|
|
dea0be4f11 | ||
|
|
e59a7680e6 | ||
|
|
448e67bd8b | ||
|
|
1f4b08ad2a | ||
|
|
afb8d80ce7 | ||
|
|
bd065c1154 | ||
|
|
4c9b724987 | ||
|
|
2bf886b7dd | ||
|
|
9ac94568f3 | ||
|
|
ca0789ee8f | ||
|
|
0d44d970a9 | ||
|
|
0c272958cf | ||
|
|
d07cce7bd1 | ||
|
|
dbfe5a252c | ||
|
|
550f707565 | ||
|
|
2173d1bf47 | ||
|
|
e12776037f | ||
|
|
a4550c5769 | ||
|
|
e996956c29 | ||
|
|
31a1034cb5 | ||
|
|
087fcf4085 | ||
|
|
1d7d8a1658 | ||
|
|
f178c31305 | ||
|
|
e6ec78ede4 | ||
|
|
34f7d78449 | ||
|
|
258373717a | ||
|
|
383d214c7c | ||
|
|
59777971d2 | ||
|
|
f187bec815 | ||
|
|
0dea7eab37 | ||
|
|
b53c6eae62 | ||
|
|
67ff2f8c95 | ||
|
|
1e1a966651 | ||
|
|
9b9d4883c3 | ||
|
|
796ed1b501 | ||
|
|
ff867fcb7f | ||
|
|
e72447de40 | ||
|
|
bd94eda53a | ||
|
|
d99268ae51 | ||
|
|
22efdfa904 | ||
|
|
b91ed087c8 | ||
|
|
e4a775567c | ||
|
|
e1c7f228d6 | ||
|
|
226f5ac17f | ||
|
|
29e9625b18 | ||
|
|
b1c47dabd9 | ||
|
|
2ff83d3023 | ||
|
|
121ee3f555 | ||
|
|
7a2aa68960 | ||
|
|
c67491cbaf | ||
|
|
7139f47333 | ||
|
|
381a51b2d4 | ||
|
|
0dc1d6a989 | ||
|
|
0b5298d24e | ||
|
|
d249e25a64 | ||
|
|
0050f6b165 | ||
|
|
23258c86be | ||
|
|
f60943717e | ||
|
|
8b477d2887 | ||
|
|
802cdc7783 | ||
|
|
a4a27517ff | ||
|
|
4726aaa08c | ||
|
|
18ecb82034 | ||
|
|
e900428a47 | ||
|
|
f07ee23d23 | ||
|
|
f750029c72 | ||
|
|
0d7f8051d0 | ||
|
|
5ab430fa11 | ||
|
|
29ddb9d926 | ||
|
|
383531da96 | ||
|
|
44ceccd2be | ||
|
|
3720ecaf52 | ||
|
|
e8e57f9395 | ||
|
|
3dcdfee1e1 | ||
|
|
b24979cc30 | ||
|
|
c32748bc28 | ||
|
|
a3af426353 | ||
|
|
7fe6c16f03 | ||
|
|
ce56fc176a | ||
|
|
5dcb072f7f | ||
|
|
a982f798ca | ||
|
|
83e4cfba30 | ||
|
|
2ad6314d72 | ||
|
|
caf930e65e | ||
|
|
d89ad16124 | ||
|
|
c46610472f | ||
|
|
8cfc09238f | ||
|
|
8c02521c47 | ||
|
|
bac84c5858 | ||
|
|
198d0b36a2 | ||
|
|
33c284ca0d | ||
|
|
1cbbfe8ed2 | ||
|
|
7ef836812b | ||
|
|
6ca104d129 | ||
|
|
1a8263c2f5 | ||
|
|
18ed27bf5f | ||
|
|
8edd7e84ad | ||
|
|
7fb74310f0 | ||
|
|
eb48b6bd06 | ||
|
|
511f114138 | ||
|
|
7913b6cd27 | ||
|
|
f3a2488ab0 | ||
|
|
f5f046a736 | ||
|
|
e533ff4c4a | ||
|
|
fbf3e009d4 | ||
|
|
21031c2243 | ||
|
|
5181a93391 | ||
|
|
a8f6e7601b | ||
|
|
76f2a12ad7 | ||
|
|
88eb405491 | ||
|
|
36ae3dd235 | ||
|
|
53d08d4aef | ||
|
|
d2d2dfd9f2 | ||
|
|
ac7ef5b8c6 | ||
|
|
bc88f735cd | ||
|
|
2feb81249f | ||
|
|
045145c700 | ||
|
|
ec6cf6a2ac | ||
|
|
6537080674 | ||
|
|
8cd4d74d94 | ||
|
|
e5f3bf99cc | ||
|
|
11eb9ac1b9 | ||
|
|
41cefdff8f | ||
|
|
7b8da19302 | ||
|
|
db7a228e6c | ||
|
|
f9613ff01e | ||
|
|
9ed9af4f39 | ||
|
|
01cc68ee0d | ||
|
|
2454952544 | ||
|
|
77c383d1e0 | ||
|
|
ca9ab97427 | ||
|
|
e1da5a36d4 | ||
|
|
d581d9d733 | ||
|
|
9d20ad261a | ||
|
|
81516ca1a4 | ||
|
|
474d6e520a | ||
|
|
2cba10a49f | ||
|
|
f7ef52e66d | ||
|
|
479df18caf | ||
|
|
523537a627 | ||
|
|
c25800ccc1 | ||
|
|
60e0d2a7b9 | ||
|
|
634174f050 | ||
|
|
6c113837b8 | ||
|
|
f2d8facb48 | ||
|
|
b851ba2f98 | ||
|
|
e112fb939a | ||
|
|
61fdc7bf34 | ||
|
|
fc64494b03 | ||
|
|
05289f1aa0 | ||
|
|
d88b06cb75 | ||
|
|
c782e8e44f | ||
|
|
5a350aeaf5 | ||
|
|
053fbf0209 | ||
|
|
3c1e9984e0 | ||
|
|
bea35d0902 | ||
|
|
d28ac4dbdb | ||
|
|
a720a1f9de | ||
|
|
5a869eea5a | ||
|
|
0135a0a780 | ||
|
|
f4d2748ca5 | ||
|
|
72bb5cd692 | ||
|
|
7c1deea5fa | ||
|
|
f875b519e5 | ||
|
|
040ebadfc5 | ||
|
|
f91fab8b18 | ||
|
|
a77f20a6d6 | ||
|
|
4d54d196c9 | ||
|
|
fffd72f36d | ||
|
|
21572415c8 | ||
|
|
f6049db20f | ||
|
|
d77d231507 | ||
|
|
6de517cbcb | ||
|
|
507e237d8c | ||
|
|
6082f01b97 | ||
|
|
d33664aef0 | ||
|
|
8975f75c8b | ||
|
|
463e9f2704 | ||
|
|
10b9df6d8a | ||
|
|
ee282c6de5 | ||
|
|
6d6aba2be5 | ||
|
|
a6d084113a | ||
|
|
6a2b1b2198 | ||
|
|
e8e6c684bb | ||
|
|
4ed2fb75f2 | ||
|
|
bced79b63d | ||
|
|
d522e02fe4 | ||
|
|
961759c08b | ||
|
|
0e8c5fd85d | ||
|
|
8408c16da4 | ||
|
|
40c8ed0dff | ||
|
|
838644b989 | ||
|
|
0796e992e4 | ||
|
|
1f1ce8a1fe | ||
|
|
9572267f64 | ||
|
|
edc6042c65 | ||
|
|
186e966483 | ||
|
|
51474b6f15 | ||
|
|
54fe5dc842 | ||
|
|
6989d6283a | ||
|
|
43190f5248 | ||
|
|
6f358fd8e0 | ||
|
|
bff849b874 | ||
|
|
606e3d7866 | ||
|
|
cca24cc78b | ||
|
|
4930766711 | ||
|
|
2e8b444da8 | ||
|
|
d13a431860 | ||
|
|
117aca7f4e | ||
|
|
ec3aa5def4 | ||
|
|
73b6de1011 | ||
|
|
5780aa1cd6 | ||
|
|
cfe31ca3b2 | ||
|
|
25eb63885d | ||
|
|
79dc565825 | ||
|
|
5dcb2ab40e | ||
|
|
d3b38311b0 | ||
|
|
59fca2d738 | ||
|
|
1275368151 | ||
|
|
a881181fd8 | ||
|
|
f542e23a2f | ||
|
|
3c6259ebb7 | ||
|
|
6851dc9505 | ||
|
|
cce1a14795 | ||
|
|
2f814c6c92 | ||
|
|
dc3f2bd1d9 | ||
|
|
d819ef3e32 | ||
|
|
334a1dd716 | ||
|
|
5c08fb225a | ||
|
|
0d109750ae | ||
|
|
a9a386dee1 | ||
|
|
4295329ec3 | ||
|
|
b4f16c7bcb | ||
|
|
e17bfc4938 | ||
|
|
2db057423b | ||
|
|
c1aa424d6b | ||
|
|
b18a05ae3e | ||
|
|
29f057b242 | ||
|
|
119bb57627 | ||
|
|
21c3d6993b | ||
|
|
e71e585969 | ||
|
|
ea6d3a35ff | ||
|
|
26355cc35d | ||
|
|
25e9097af0 | ||
|
|
d0f05d98d2 | ||
|
|
88b27c378d | ||
|
|
a66462b583 | ||
|
|
e61fb145fc | ||
|
|
afeab32780 | ||
|
|
4e5752631c | ||
|
|
08b1b06aab | ||
|
|
0289b046da | ||
|
|
1053a76dd8 | ||
|
|
82e5dd4da7 | ||
|
|
a70e618b20 | ||
|
|
20d7c7ae02 | ||
|
|
1c0fb5768b | ||
|
|
25c0699fe9 | ||
|
|
ce0d5117bf | ||
|
|
826cdd884c | ||
|
|
4503560084 | ||
|
|
bf1056c554 | ||
|
|
344417c0de | ||
|
|
86150a3e51 | ||
|
|
d8b5e22e8b | ||
|
|
5dd026f3f7 | ||
|
|
ae5376a599 | ||
|
|
cc122956df | ||
|
|
eaf803b223 | ||
|
|
d14fe163b5 | ||
|
|
5b98f03c64 | ||
|
|
eecec7495f | ||
|
|
c40dd6ff5c | ||
|
|
8b6bed9c9c | ||
|
|
5a10f46c56 | ||
|
|
bdfeece562 | ||
|
|
aafdf67d39 | ||
|
|
55bde6750f | ||
|
|
546f44f395 | ||
|
|
9877f31fdd | ||
|
|
9bc7712c40 | ||
|
|
ba445e0e3f | ||
|
|
f1ec2605b7 | ||
|
|
81f359ec5b | ||
|
|
d8a67ef39a | ||
|
|
cf36b9456d | ||
|
|
376bf65d8e | ||
|
|
b7b069c4d6 | ||
|
|
340fca0a45 | ||
|
|
c768a9e6ca | ||
|
|
ce1ef04efe | ||
|
|
79f6c5a8ad | ||
|
|
3a5baf1229 | ||
|
|
fe52654d2e | ||
|
|
45144ce2e8 | ||
|
|
5b36bbf83e | ||
|
|
9b4e2fa8a8 | ||
|
|
cc191e8021 | ||
|
|
64b9b60d94 | ||
|
|
68307afb5b | ||
|
|
5a557b5e10 | ||
|
|
7e85ba6139 | ||
|
|
edd3870d53 | ||
|
|
a590fd24a9 | ||
|
|
8f14a1c59a | ||
|
|
e1db0f01fe | ||
|
|
b1053ef9e9 | ||
|
|
35d801a1e5 | ||
|
|
d901f85abb | ||
|
|
61d16dd173 | ||
|
|
bb8e0ab5dc | ||
|
|
1c640622dd | ||
|
|
918d5afd67 | ||
|
|
5820d105c9 | ||
|
|
3f1e0ebb86 | ||
|
|
52f96fab51 | ||
|
|
9c10ef2ffa | ||
|
|
4cd8b5eb78 | ||
|
|
07676fbb44 | ||
|
|
bb1f3e8eaf | ||
|
|
fd3cc7d224 | ||
|
|
5a62a896b2 | ||
|
|
85b26bd206 | ||
|
|
6f08a1a3dd | ||
|
|
20c3736dae | ||
|
|
3c21fdad3c | ||
|
|
5960549816 | ||
|
|
2baa9d550e | ||
|
|
b90fb1ef71 | ||
|
|
301c6d0043 | ||
|
|
ed4c133c2c | ||
|
|
f4369d225a | ||
|
|
b77c272fb9 | ||
|
|
46f3efe7ce | ||
|
|
87b5796649 | ||
|
|
2bb3132a5c | ||
|
|
54c3f53de5 | ||
|
|
73a81d1d6a | ||
|
|
7b02080fa1 | ||
|
|
c90f42dbae | ||
|
|
32282418eb | ||
|
|
3eaab8632e | ||
|
|
ff43ede887 | ||
|
|
e4f6dd3440 | ||
|
|
48557cecff | ||
|
|
6439b64c90 | ||
|
|
2fb968a425 | ||
|
|
ddaa2c5dc8 | ||
|
|
fba1e49083 | ||
|
|
059277f83b | ||
|
|
ae8b868342 | ||
|
|
48d6c75111 | ||
|
|
d966486242 | ||
|
|
e98b864752 | ||
|
|
88dc177afc | ||
|
|
06fee678e1 | ||
|
|
1d2e91e20d | ||
|
|
5169d19ce8 | ||
|
|
86c071035d | ||
|
|
c635716297 | ||
|
|
0df6292ab3 | ||
|
|
dd555073d0 | ||
|
|
8c74fd4e23 | ||
|
|
59768909ba | ||
|
|
1b35d46257 | ||
|
|
20e443b965 | ||
|
|
d714803e6d | ||
|
|
18d036326c | ||
|
|
8b47fa5a76 | ||
|
|
e168a82367 | ||
|
|
83eab79d15 | ||
|
|
c7a8114f54 | ||
|
|
ef17cecca9 | ||
|
|
8835787ed6 | ||
|
|
b12114e45c | ||
|
|
32e51f250f | ||
|
|
8f7808d1e6 | ||
|
|
3788a2fd3d | ||
|
|
b6d6ed34ed | ||
|
|
44bcaf00b7 | ||
|
|
546aa5770a | ||
|
|
658f90f845 | ||
|
|
155260eb04 | ||
|
|
7ce1487f33 | ||
|
|
ac2dbfcfca | ||
|
|
d6ab1fdfe4 | ||
|
|
50c3995894 | ||
|
|
ad958fd97a | ||
|
|
0451dcdc56 | ||
|
|
003bb8546d | ||
|
|
a2a4924679 | ||
|
|
2ff2ed4099 | ||
|
|
fc5cb461c9 | ||
|
|
c86eb20dc5 | ||
|
|
0d0632d88d | ||
|
|
2e89655a03 | ||
|
|
233666366f | ||
|
|
474be452a7 | ||
|
|
076178adc6 | ||
|
|
eda170f328 | ||
|
|
d4867ec20d | ||
|
|
d26cef4249 | ||
|
|
e1e095d020 | ||
|
|
7e5ea598c5 | ||
|
|
0328f29707 | ||
|
|
9dce23f295 | ||
|
|
caa6102144 | ||
|
|
ca3250a3c1 | ||
|
|
dcfd033746 | ||
|
|
45f4875613 | ||
|
|
51e279153f | ||
|
|
d54addcd28 | ||
|
|
9eb525de0e | ||
|
|
d0bf656a3f | ||
|
|
604597d825 | ||
|
|
c286f56167 | ||
|
|
036b730321 | ||
|
|
deea78da72 | ||
|
|
eb5d6c7294 | ||
|
|
009d7335b5 | ||
|
|
25f3d2d714 | ||
|
|
0416117168 | ||
|
|
4cb34f3999 | ||
|
|
0059f5c24a | ||
|
|
4bcae169e2 | ||
|
|
3da05d01a7 | ||
|
|
f7e44ac6b5 | ||
|
|
25e3162cce | ||
|
|
7150c3c957 | ||
|
|
bf08234ee3 | ||
|
|
179ff9b423 | ||
|
|
9b6cd2ea75 | ||
|
|
4fbc318e30 | ||
|
|
7b5f75eb98 | ||
|
|
d1fef1d50d | ||
|
|
392af2e612 | ||
|
|
b4234d4028 | ||
|
|
69c8097dd1 | ||
|
|
60104fe254 | ||
|
|
fd5dc5bb3a | ||
|
|
f6aa2c02d1 | ||
|
|
7b4d14f786 | ||
|
|
822ebb4c94 | ||
|
|
58f7d7e5f8 | ||
|
|
0ad13b714e | ||
|
|
bb8192ff7c | ||
|
|
c5d52bf2a7 | ||
|
|
06ad1d0d74 | ||
|
|
27b1d05a1d | ||
|
|
02c6630f11 | ||
|
|
c821ef274b | ||
|
|
6d84fb35c7 | ||
|
|
335f045393 | ||
|
|
371777ad14 | ||
|
|
ca7c2714f6 | ||
|
|
b1d434b666 | ||
|
|
a7f442ffd8 | ||
|
|
bbff951880 | ||
|
|
9a4d28695b | ||
|
|
96136e6d71 | ||
|
|
e993e1c334 | ||
|
|
99e627b283 | ||
|
|
edc9be1b7f | ||
|
|
01d69041a2 | ||
|
|
6baa5ca5b1 | ||
|
|
d5d3e9983e | ||
|
|
0b6fff44f5 | ||
|
|
e07dbb27d9 | ||
|
|
d9d4514c00 | ||
|
|
05d92d8761 | ||
|
|
90b1ab1c70 | ||
|
|
93917413de | ||
|
|
9a1e896c96 | ||
|
|
208fec6ddc | ||
|
|
6d4d313d44 | ||
|
|
8129fc0f3a | ||
|
|
e16ac04330 | ||
|
|
116bc2a0f0 |
@@ -1,6 +1,8 @@
|
||||
#!/usr/bin/env node
|
||||
// Secret scanning alert handler for OpenClaw maintainers.
|
||||
// Usage: node secret-scanning.mjs <command> [options]
|
||||
/**
|
||||
* Secret scanning alert handler for OpenClaw maintainers.
|
||||
* Usage: node secret-scanning.mjs <command> [options]
|
||||
*/
|
||||
|
||||
import { spawnSync } from "node:child_process";
|
||||
import crypto from "node:crypto";
|
||||
@@ -57,6 +59,7 @@ function isBodyLocationType(locationType) {
|
||||
return locationType === "issue_body" || locationType === "pull_request_body";
|
||||
}
|
||||
|
||||
/** Decides whether redacting an issue/PR body requires notifying the reporter. */
|
||||
export function decideBodyRedaction(currentBody, redactedBody) {
|
||||
const bodyChanged = String(currentBody) !== String(redactedBody);
|
||||
return {
|
||||
@@ -65,6 +68,7 @@ export function decideBodyRedaction(currentBody, redactedBody) {
|
||||
};
|
||||
}
|
||||
|
||||
/** Loads redaction-result metadata for issue/PR body secret locations. */
|
||||
export function loadBodyRedactionResult(locationType, resultFile) {
|
||||
if (!isBodyLocationType(locationType)) {
|
||||
return { notify_required: true };
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Heap snapshot diff utility for OpenClaw test memory leak investigations.
|
||||
*/
|
||||
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Release CI summary helper that prints parent and child workflow status for a
|
||||
* full release run.
|
||||
*/
|
||||
import { execFileSync } from "node:child_process";
|
||||
import process from "node:process";
|
||||
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Release preflight helper that verifies required provider API keys can reach
|
||||
* their model-list endpoints without printing secret values.
|
||||
*/
|
||||
import process from "node:process";
|
||||
|
||||
const args = new Map();
|
||||
|
||||
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
@@ -92,7 +92,7 @@ jobs:
|
||||
for attempt in 1 2 3; do
|
||||
timeout --signal=TERM --kill-after=10s 30s git -C "$GITHUB_WORKSPACE" \
|
||||
-c protocol.version=2 \
|
||||
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
|
||||
fetch --no-tags --prune --no-recurse-submodules --depth=2 origin \
|
||||
"+${ref}:refs/remotes/origin/checkout" && return 0
|
||||
fetch_status="$?"
|
||||
if [ "$fetch_status" != "124" ] && [ "$fetch_status" != "137" ]; then
|
||||
@@ -146,12 +146,12 @@ jobs:
|
||||
|
||||
if [ "${{ github.event_name }}" = "push" ]; then
|
||||
BASE="${{ github.event.before }}"
|
||||
node scripts/ci-changed-scope.mjs --base "$BASE" --head HEAD
|
||||
else
|
||||
BASE="${{ github.event.pull_request.base.sha }}"
|
||||
node scripts/ci-changed-scope.mjs --base "$BASE" --head HEAD --merge-head-first-parent
|
||||
fi
|
||||
|
||||
node scripts/ci-changed-scope.mjs --base "$BASE" --head HEAD
|
||||
|
||||
- name: Build CI manifest
|
||||
id: manifest
|
||||
env:
|
||||
|
||||
24
.github/workflows/crabbox-hydrate.yml
vendored
24
.github/workflows/crabbox-hydrate.yml
vendored
@@ -34,7 +34,7 @@ env:
|
||||
PNPM_CONFIG_CHILD_CONCURRENCY: "1"
|
||||
PNPM_CONFIG_MODULES_DIR: "/var/tmp/openclaw-pnpm/node_modules"
|
||||
PNPM_CONFIG_NETWORK_CONCURRENCY: "1"
|
||||
PNPM_CONFIG_STORE_DIR: "/var/tmp/openclaw-pnpm/store"
|
||||
PNPM_CONFIG_STORE_DIR: "/var/cache/crabbox/pnpm/store"
|
||||
PNPM_CONFIG_VERIFY_DEPS_BEFORE_RUN: "false"
|
||||
PNPM_CONFIG_VIRTUAL_STORE_DIR: "/var/tmp/openclaw-pnpm/virtual-store"
|
||||
|
||||
@@ -120,16 +120,24 @@ jobs:
|
||||
append_pnpm_option_arg PNPM_CONFIG_MODULES_DIR modules-dir
|
||||
append_pnpm_option_arg PNPM_CONFIG_NETWORK_CONCURRENCY network-concurrency
|
||||
append_pnpm_option_arg PNPM_CONFIG_VIRTUAL_STORE_DIR virtual-store-dir
|
||||
reset_crabbox_pnpm_root() {
|
||||
local root="/var/tmp/openclaw-pnpm"
|
||||
rm -rf -- "$root"
|
||||
mkdir -p "$root"
|
||||
if [ -L "$root" ] || [ ! -d "$root" ] || [ ! -O "$root" ]; then
|
||||
echo "::error::Refusing unsafe pnpm cache root: $root"
|
||||
require_safe_writable_dir() {
|
||||
local dir="$1"
|
||||
if [ -L "$dir" ] || [ ! -d "$dir" ] || [ ! -w "$dir" ]; then
|
||||
echo "::error::Refusing unsafe pnpm directory: $dir"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
reset_crabbox_pnpm_root
|
||||
prepare_crabbox_pnpm_dirs() {
|
||||
local volatile_root="/var/tmp/openclaw-pnpm"
|
||||
case "${PNPM_CONFIG_MODULES_DIR:?}" in "$volatile_root"/*) ;; *) echo "::error::PNPM_CONFIG_MODULES_DIR must stay under $volatile_root"; exit 1 ;; esac
|
||||
case "${PNPM_CONFIG_VIRTUAL_STORE_DIR:?}" in "$volatile_root"/*) ;; *) echo "::error::PNPM_CONFIG_VIRTUAL_STORE_DIR must stay under $volatile_root"; exit 1 ;; esac
|
||||
rm -rf -- "$volatile_root"
|
||||
mkdir -p "$volatile_root" "$PNPM_CONFIG_STORE_DIR"
|
||||
require_safe_writable_dir "$volatile_root"
|
||||
require_safe_writable_dir "$PNPM_CONFIG_STORE_DIR"
|
||||
mkdir -p "$PNPM_CONFIG_MODULES_DIR" "$PNPM_CONFIG_VIRTUAL_STORE_DIR"
|
||||
}
|
||||
prepare_crabbox_pnpm_dirs
|
||||
if [ -L node_modules ] && [ "$(readlink node_modules)" = "${PNPM_CONFIG_MODULES_DIR:-}" ]; then
|
||||
rm -f node_modules
|
||||
fi
|
||||
|
||||
@@ -563,7 +563,7 @@ jobs:
|
||||
needs: validate_selected_ref
|
||||
if: inputs.include_repo_e2e && inputs.live_suite_filter == ''
|
||||
continue-on-error: ${{ inputs.advisory }}
|
||||
runs-on: ${{ inputs.use_github_hosted_runners && 'ubuntu-24.04' || 'blacksmith-8vcpu-ubuntu-2404' }}
|
||||
runs-on: ${{ inputs.use_github_hosted_runners && 'ubuntu-24.04' || 'blacksmith-32vcpu-ubuntu-2404' }}
|
||||
timeout-minutes: ${{ inputs.release_test_profile == 'full' && 90 || 60 }}
|
||||
env:
|
||||
OPENCLAW_VITEST_MAX_WORKERS: "2"
|
||||
@@ -595,7 +595,7 @@ jobs:
|
||||
needs: validate_selected_ref
|
||||
if: inputs.include_repo_e2e && (inputs.live_suite_filter == '' || inputs.live_suite_filter == 'openshell-e2e')
|
||||
continue-on-error: ${{ inputs.advisory }}
|
||||
runs-on: ${{ inputs.use_github_hosted_runners && 'ubuntu-24.04' || 'blacksmith-8vcpu-ubuntu-2404' }}
|
||||
runs-on: ${{ inputs.use_github_hosted_runners && 'ubuntu-24.04' || 'blacksmith-32vcpu-ubuntu-2404' }}
|
||||
timeout-minutes: ${{ matrix.timeout_minutes }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
||||
3
.github/workflows/opengrep-precise.yml
vendored
3
.github/workflows/opengrep-precise.yml
vendored
@@ -44,7 +44,7 @@ jobs:
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ github.sha }}
|
||||
fetch-depth: 1
|
||||
fetch-depth: 2
|
||||
fetch-tags: false
|
||||
persist-credentials: false
|
||||
submodules: false
|
||||
@@ -74,6 +74,7 @@ jobs:
|
||||
- name: Run opengrep on PR diff
|
||||
env:
|
||||
OPENCLAW_OPENGREP_BASE_REF: ${{ github.event.pull_request.base.sha }}...HEAD
|
||||
OPENCLAW_OPENGREP_MERGE_HEAD_FIRST_PARENT: "1"
|
||||
# Findings from precise rules block this workflow. Pull requests scan
|
||||
# changed first-party source paths only so findings stay attributable to
|
||||
# the PR diff. Test/fixture/QA path exclusions live in `.semgrepignore`
|
||||
|
||||
6
.github/workflows/tui-pty.yml
vendored
6
.github/workflows/tui-pty.yml
vendored
@@ -27,7 +27,9 @@ env:
|
||||
jobs:
|
||||
tui-pty:
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 5
|
||||
timeout-minutes: 8
|
||||
env:
|
||||
OPENCLAW_TUI_PTY_INCLUDE_LOCAL: "1"
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
@@ -38,4 +40,4 @@ jobs:
|
||||
install-bun: "false"
|
||||
|
||||
- name: Run TUI PTY tests
|
||||
run: timeout --kill-after=30s 120s node scripts/run-vitest.mjs run --config test/vitest/vitest.tui-pty.config.ts
|
||||
run: timeout --kill-after=30s 240s node scripts/run-vitest.mjs run --config test/vitest/vitest.tui-pty.config.ts
|
||||
|
||||
12
CHANGELOG.md
12
CHANGELOG.md
@@ -27,11 +27,23 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
- Channels/outbound: keep channel sends durable when transcript mirroring fails, stop schema-padded poll modifiers from blocking normal sends, preserve WebChat `sessions_send` handoffs, preserve Discord channel-label suppression while hiding internal agent failure traces, match Discord libopus error shapes, and sanitize Discord tool progress scaffolding. (#89626, #89812, #89601) Thanks @Petru2224, @codezz, and @takhoffman.
|
||||
- Telegram/Feishu: require admin rights for Telegram target writeback, keep Telegram DM exec approval allowlists working with `ask:off`, prevent Telegram preview duplication across streaming modes, isolate verbose status after streamed finals, cancel clean restart stop timers, slow polling restart storms, and wire Feishu setup runtime setters. (#88973, #89035, #89813, #89814) Thanks @pgondhi987, @zhangguiping-xydt, and @takhoffman.
|
||||
- Feishu: preserve full streaming card content by sending the merged text on each update instead of only the latest delta, so card readers see complete output when intermediate frames are missed. (#90181) Thanks @mushuiyu886.
|
||||
- Chat/UI/Gateway: preserve visible chat stream text, clear stale stream buffers before terminal commits, reconcile completed sends, scroll pending sends into view, harden Workboard dialog accessibility, stabilize WebChat prompt-cache affinity, overlap chat catalog startup, render chat history incrementally, lazy-load usage dashboard, and report gateway health auth diagnostics. (#89337) Thanks @RomneyDa.
|
||||
- Agents/Codex/providers/models: release session write locks when prompt-release fence reads fail, retire abandoned Codex app-server startups, keep stream-to-parent ACP spawns registered, close Codex startup clients on timeout, recover bundled provider aliases, avoid custom-provider runtime fanout, preserve provider prompt-cache boundaries, forward Gemini stop sequences, and strip Kimi-incompatible Anthropic cache markers. (#89811) Thanks @takhoffman.
|
||||
- Memory/build/update: warn after startup watcher pressure checks, externalize optional Baileys image backends, restore and pin Canvas A2UI compatibility assets, keep plugin repair fetch failures nonblocking, restore Skill Workshop view switching, and keep the current chat toggle active after awaited session switches. (#89244) Thanks @RomneyDa.
|
||||
- Plugins/auth: keep Hermes migration reports pointed at SQLite auth-profile stores and keep plugin auth-profile reuse tests on the current store path.
|
||||
- Plugins/CLI: avoid importing the runtime plugin loader only to clear in-process caches after short-lived plugin install, enable, disable, update, and uninstall commands refresh registry metadata.
|
||||
- Security/config/tooling: reject corrupt shell snapshots, suspicious gateway startup configs, malformed release/test/tooling/Docker/perf numeric limits, oversized audit responses, unsafe exec precheck env, and invalid pending-agent SQLite scaffold denials. (#89701, #89705, #89480, #81488) Thanks @RomneyDa and @mmaps.
|
||||
- Release/CI/E2E: restore package changelog extraction after the post-2026.6.1 version bump, keep hydrated pnpm modules under `node_modules` for ARM/Linux package lifecycle scripts, keep OpenAI live-cache prerequisites advisory while Anthropic prerequisites stay blocking, retry Windows Parallels background log appends on transient file-lock errors, bound candidate GitHub and cross-OS Discord fetches, harden ARM smoke/browser checks, show Docker build heartbeats, reset Crabbox pnpm hydrate state, and isolate Testbox/Docker/release journey artifacts.
|
||||
- Release/CI/E2E: keep Crabbox hydrate pnpm stores on the persistent cache volume while still resetting volatile modules, reducing cold installs and runner memory churn.
|
||||
- Release/CI/E2E: fail secret-provider proof startup immediately when the gateway exits by signal instead of waiting for the readiness timeout.
|
||||
- Release/CI/E2E: report plugin gateway gauntlet command-log write failures as failed rows instead of crashing the harness from child-process callbacks.
|
||||
- Release/CI/E2E: abort stalled Kitchen Sink RPC readiness probes as soon as the gateway exits so proof failures return promptly.
|
||||
- Release/CI/E2E: keep Parallels JSON-mode progress on stderr so macOS, Linux, Windows, and aggregate update smoke summaries stay parseable on stdout.
|
||||
- Release/CI/E2E: fail Crabbox sparse-sync runs clearly when their temporary full checkout disappears while the child process is running, instead of pretending the child's deleted cwd can be repaired.
|
||||
- Release/CI/E2E: fail PTY-backed E2E commands when transcript logs cannot be written instead of letting missing proof capture crash around a live child process.
|
||||
- Release/CI/E2E: fail mock OpenAI request-log write errors with clear HTTP responses instead of leaving provider proof clients waiting on a broken socket.
|
||||
- Release/CI/E2E: fail Parallels host-command log write errors through the command result path instead of leaving streaming smoke phases unresolved.
|
||||
|
||||
## 2026.6.1
|
||||
|
||||
|
||||
12
Dockerfile
12
Dockerfile
@@ -9,18 +9,18 @@
|
||||
# Build stages use full bookworm; the runtime image is always bookworm-slim.
|
||||
ARG OPENCLAW_EXTENSIONS=""
|
||||
ARG OPENCLAW_BUNDLED_PLUGIN_DIR=extensions
|
||||
ARG OPENCLAW_NODE_BOOKWORM_IMAGE="node:24-bookworm@sha256:8530f76a96d88820d288761f022e318970dda93d01536919fbc16076b7983e63"
|
||||
ARG OPENCLAW_NODE_BOOKWORM_SLIM_IMAGE="node:24-bookworm-slim@sha256:242549cd46785b480c832479a730f4f2a20865d61ea2e404fdb2a5c3d3b73ecf"
|
||||
ARG OPENCLAW_NODE_BOOKWORM_IMAGE="docker.io/library/node:24-bookworm@sha256:8530f76a96d88820d288761f022e318970dda93d01536919fbc16076b7983e63"
|
||||
ARG OPENCLAW_NODE_BOOKWORM_SLIM_IMAGE="docker.io/library/node:24-bookworm-slim@sha256:242549cd46785b480c832479a730f4f2a20865d61ea2e404fdb2a5c3d3b73ecf"
|
||||
ARG OPENCLAW_NODE_BOOKWORM_SLIM_DIGEST="sha256:242549cd46785b480c832479a730f4f2a20865d61ea2e404fdb2a5c3d3b73ecf"
|
||||
# Keep in sync with .github/actions/setup-node-env/action.yml bun-version.
|
||||
# To update: docker buildx imagetools inspect oven/bun:<version> and use the manifest-list digest.
|
||||
ARG OPENCLAW_BUN_IMAGE="oven/bun:1.3.13@sha256:87416c977a612a204eb54ab9f3927023c2a3c971f4f345a01da08ea6262ae30e"
|
||||
# To update: docker buildx imagetools inspect docker.io/oven/bun:<version> and use the manifest-list digest.
|
||||
ARG OPENCLAW_BUN_IMAGE="docker.io/oven/bun:1.3.13@sha256:87416c977a612a204eb54ab9f3927023c2a3c971f4f345a01da08ea6262ae30e"
|
||||
|
||||
# Base images are pinned to SHA256 digests for reproducible builds.
|
||||
# Dependabot refreshes these blessed digests; release builds consume the
|
||||
# reviewed base snapshot instead of mutating distro state on every build.
|
||||
# To update, run: docker buildx imagetools inspect node:24-bookworm and
|
||||
# node:24-bookworm-slim (or podman) and replace the digests below with the
|
||||
# To update, run: docker buildx imagetools inspect docker.io/library/node:24-bookworm and
|
||||
# docker.io/library/node:24-bookworm-slim (or podman) and replace the digests below with the
|
||||
# current multi-arch manifest list entries.
|
||||
|
||||
FROM ${OPENCLAW_NODE_BOOKWORM_IMAGE} AS workspace-deps
|
||||
|
||||
@@ -30,7 +30,8 @@ Supported channels include: WhatsApp, Telegram, Slack, Discord, Google Chat, Sig
|
||||
New install? Start here: [Getting started](https://docs.openclaw.ai/start/getting-started)
|
||||
|
||||
Preferred setup: run `openclaw onboard` in your terminal.
|
||||
OpenClaw Onboard guides you step by step through setting up the gateway, workspace, channels, and skills. It is the recommended CLI setup path and works on **macOS, Linux, and Windows (via WSL2; strongly recommended)**.
|
||||
OpenClaw Onboard guides you step by step through setting up the gateway, workspace, channels, and skills. It is the recommended CLI setup path and works on **macOS, Linux, and Windows**.
|
||||
Windows desktop users can start with the native [Windows Hub](https://docs.openclaw.ai/platforms/windows) companion app for setup, tray status, chat, node mode, and local MCP mode.
|
||||
Works with npm, pnpm, or bun.
|
||||
|
||||
## Sponsors
|
||||
@@ -164,7 +165,7 @@ Run `openclaw doctor` to surface risky/misconfigured DM policies.
|
||||
- **[Voice Wake](https://docs.openclaw.ai/nodes/voicewake) + [Talk Mode](https://docs.openclaw.ai/nodes/talk)** — wake words on macOS/iOS and continuous voice on Android (ElevenLabs + system TTS fallback).
|
||||
- **[Live Canvas](https://docs.openclaw.ai/platforms/mac/canvas)** — agent-driven visual workspace with [A2UI](https://docs.openclaw.ai/platforms/mac/canvas#canvas-a2ui).
|
||||
- **[First-class tools](https://docs.openclaw.ai/tools)** — browser, canvas, nodes, cron, sessions, and Discord/Slack actions.
|
||||
- **[Companion apps](https://docs.openclaw.ai/platforms/macos)** — macOS menu bar app + iOS/Android [nodes](https://docs.openclaw.ai/nodes).
|
||||
- **[Companion apps](https://docs.openclaw.ai/platforms)** — Windows Hub, macOS menu bar app, and iOS/Android [nodes](https://docs.openclaw.ai/nodes).
|
||||
- **[Onboarding](https://docs.openclaw.ai/start/wizard) + [skills](https://docs.openclaw.ai/tools/skills)** — onboarding-driven setup with bundled/managed/workspace skills.
|
||||
|
||||
## Security model (important)
|
||||
@@ -185,7 +186,7 @@ Run `openclaw doctor` to surface risky/misconfigured DM policies.
|
||||
|
||||
- New here: [Getting started](https://docs.openclaw.ai/start/getting-started), [Onboarding](https://docs.openclaw.ai/start/wizard), [Updating](https://docs.openclaw.ai/install/updating)
|
||||
- Channel setup: [Channels index](https://docs.openclaw.ai/channels), [WhatsApp](https://docs.openclaw.ai/channels/whatsapp), [Telegram](https://docs.openclaw.ai/channels/telegram), [Discord](https://docs.openclaw.ai/channels/discord), [Slack](https://docs.openclaw.ai/channels/slack)
|
||||
- Apps + nodes: [macOS](https://docs.openclaw.ai/platforms/macos), [iOS](https://docs.openclaw.ai/platforms/ios), [Android](https://docs.openclaw.ai/platforms/android), [Nodes](https://docs.openclaw.ai/nodes)
|
||||
- Apps + nodes: [Windows Hub](https://docs.openclaw.ai/platforms/windows), [macOS](https://docs.openclaw.ai/platforms/macos), [iOS](https://docs.openclaw.ai/platforms/ios), [Android](https://docs.openclaw.ai/platforms/android), [Nodes](https://docs.openclaw.ai/nodes)
|
||||
- Config + security: [Configuration](https://docs.openclaw.ai/gateway/configuration), [Security](https://docs.openclaw.ai/gateway/security), [Exposure runbook](https://docs.openclaw.ai/gateway/security/exposure-runbook), [Sandboxing](https://docs.openclaw.ai/gateway/sandboxing)
|
||||
- Remote + web: [Gateway](https://docs.openclaw.ai/gateway), [Remote access](https://docs.openclaw.ai/gateway/remote), [Tailscale](https://docs.openclaw.ai/gateway/tailscale), [Web surfaces](https://docs.openclaw.ai/web)
|
||||
- Tools + automation: [Tools](https://docs.openclaw.ai/tools), [Skills](https://docs.openclaw.ai/tools/skills), [Cron jobs](https://docs.openclaw.ai/automation/cron-jobs), [Webhooks](https://docs.openclaw.ai/automation/webhook), [Gmail Pub/Sub](https://docs.openclaw.ai/automation/gmail-pubsub)
|
||||
|
||||
@@ -253,9 +253,9 @@ Pre-req checklist:
|
||||
4) Open the app **Screen** tab and keep it active during the run (canvas/A2UI commands require the canvas WebView attached there).
|
||||
5) Grant runtime permissions for capabilities you expect to pass (camera/mic/location/notification listener/location, etc.).
|
||||
6) No interactive system dialogs should be pending before test start.
|
||||
7) Canvas host is enabled and reachable from the device (do not run gateway with `OPENCLAW_SKIP_CANVAS_HOST=1`; startup logs should include `canvas host mounted at .../__openclaw__/`).
|
||||
7) Canvas host is enabled and reachable from the device for remote Canvas checks (do not run gateway with `OPENCLAW_SKIP_CANVAS_HOST=1`; startup logs should include `canvas host mounted at .../__openclaw__/`).
|
||||
8) Local operator test client pairing is approved. If first run fails with `pairing required`, preview the latest pending request, approve the printed request ID, then rerun:
|
||||
9) For A2UI checks, keep the app on **Screen** tab; the node now auto-refreshes canvas capability once on first A2UI reachability failure (TTL-safe retry).
|
||||
9) For A2UI checks, keep the app on **Screen** tab; the node uses its bundled app-owned A2UI page for message application.
|
||||
|
||||
```bash
|
||||
openclaw devices list
|
||||
@@ -287,8 +287,8 @@ Common failure quick-fixes:
|
||||
|
||||
- `pairing required` before tests start:
|
||||
- list pending requests (`openclaw devices list`), then approve with the exact ID (`openclaw devices approve <requestId>`) and rerun.
|
||||
- `A2UI host not reachable` / `A2UI_HOST_NOT_CONFIGURED`:
|
||||
- ensure the Canvas plugin host is running and reachable, keep the app on the **Screen** tab. The app refreshes the Canvas plugin surface URL once before failing; if it still fails, reconnect app and rerun.
|
||||
- `A2UI host not reachable` / `A2UI_HOST_UNAVAILABLE`:
|
||||
- keep the app foregrounded on the **Screen** tab and rerun. A2UI commands use the bundled app-owned A2UI page; the Gateway Canvas host is still needed for remote Canvas checks, but not for A2UI message application.
|
||||
- `NODE_BACKGROUND_UNAVAILABLE: canvas unavailable`:
|
||||
- app is not effectively ready for canvas commands; keep app foregrounded and **Screen** tab active.
|
||||
|
||||
|
||||
@@ -189,8 +189,6 @@ class NodeRuntime(
|
||||
A2UIHandler(
|
||||
canvas = canvas,
|
||||
json = json,
|
||||
getNodeCanvasHostUrl = { nodeSession.currentCanvasHostUrl() },
|
||||
getOperatorCanvasHostUrl = { operatorSession.currentCanvasHostUrl() },
|
||||
)
|
||||
|
||||
private val connectionManager: ConnectionManager =
|
||||
@@ -254,7 +252,6 @@ class NodeRuntime(
|
||||
_canvasRehydrateErrorText.value = null
|
||||
},
|
||||
onCanvasA2uiReset = { _canvasA2uiHydrated.value = false },
|
||||
refreshCanvasHostUrl = { nodeSession.refreshCanvasHostUrl() },
|
||||
motionActivityAvailable = { motionHandler.isActivityAvailable() },
|
||||
motionPedometerAvailable = { motionHandler.isPedometerAvailable() },
|
||||
)
|
||||
|
||||
@@ -12,47 +12,30 @@ import kotlinx.serialization.json.JsonPrimitive
|
||||
class A2UIHandler(
|
||||
private val canvas: CanvasController,
|
||||
private val json: Json,
|
||||
private val getNodeCanvasHostUrl: () -> String?,
|
||||
private val getOperatorCanvasHostUrl: () -> String?,
|
||||
) {
|
||||
fun isTrustedCanvasActionUrl(rawUrl: String?): Boolean =
|
||||
CanvasActionTrust.isTrustedCanvasActionUrl(
|
||||
rawUrl = rawUrl,
|
||||
trustedA2uiUrls = listOfNotNull(resolveA2uiHostUrl()),
|
||||
)
|
||||
fun isTrustedCanvasActionUrl(rawUrl: String?): Boolean = CanvasActionTrust.isTrustedCanvasActionUrl(rawUrl)
|
||||
|
||||
fun resolveA2uiHostUrl(): String? {
|
||||
val nodeRaw = getNodeCanvasHostUrl()?.trim().orEmpty()
|
||||
val operatorRaw = getOperatorCanvasHostUrl()?.trim().orEmpty()
|
||||
// Prefer node-advertised canvas host; operator URL is a fallback for older hello payloads.
|
||||
val raw = if (nodeRaw.isNotBlank()) nodeRaw else operatorRaw
|
||||
if (raw.isBlank()) return null
|
||||
val base = raw.trimEnd('/')
|
||||
return "$base/__openclaw__/a2ui/?platform=android"
|
||||
}
|
||||
|
||||
suspend fun ensureA2uiReady(a2uiUrl: String): Boolean {
|
||||
try {
|
||||
val already = canvas.eval(a2uiReadyCheckJS)
|
||||
if (already == "true") return true
|
||||
} catch (_: Throwable) {
|
||||
// ignore
|
||||
suspend fun ensureA2uiReady(): Boolean {
|
||||
if (canvas.currentUrl()?.trim() == CanvasActionTrust.localA2uiAssetUrl && isA2uiReady()) {
|
||||
return true
|
||||
}
|
||||
|
||||
canvas.navigate(a2uiUrl)
|
||||
// A2UI host bootstraps asynchronously after navigation; poll briefly before failing the command.
|
||||
canvas.showLocalA2ui()
|
||||
// The bundled A2UI host bootstraps asynchronously after navigation; poll briefly before failing the command.
|
||||
repeat(50) {
|
||||
try {
|
||||
val ready = canvas.eval(a2uiReadyCheckJS)
|
||||
if (ready == "true") return true
|
||||
} catch (_: Throwable) {
|
||||
// ignore
|
||||
}
|
||||
if (isA2uiReady()) return true
|
||||
delay(120)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private suspend fun isA2uiReady(): Boolean =
|
||||
try {
|
||||
canvas.eval(a2uiReadyCheckJS) == "true"
|
||||
} catch (_: Throwable) {
|
||||
false
|
||||
}
|
||||
|
||||
fun decodeA2uiMessages(
|
||||
command: String,
|
||||
paramsJson: String?,
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package ai.openclaw.app.node
|
||||
|
||||
import java.net.URI
|
||||
|
||||
/**
|
||||
* Trust helper for WebView-originated canvas/A2UI actions.
|
||||
*/
|
||||
@@ -9,62 +7,15 @@ object CanvasActionTrust {
|
||||
/** Local canvas scaffold is the only trusted file URL. */
|
||||
const val scaffoldAssetUrl: String = "file:///android_asset/CanvasScaffold/scaffold.html"
|
||||
|
||||
/** Accepts local scaffold or exact remote A2UI URLs advertised by the gateway. */
|
||||
fun isTrustedCanvasActionUrl(
|
||||
rawUrl: String?,
|
||||
trustedA2uiUrls: List<String>,
|
||||
): Boolean {
|
||||
/** Local bundled A2UI is the only action-capable A2UI host. */
|
||||
const val localA2uiAssetUrl: String = "file:///android_asset/CanvasA2UI/index.html"
|
||||
|
||||
/** Accepts only app-owned bundled pages. Remote WebView content is render-only. */
|
||||
fun isTrustedCanvasActionUrl(rawUrl: String?): Boolean {
|
||||
val candidate = rawUrl?.trim().orEmpty()
|
||||
if (candidate.isEmpty()) return false
|
||||
if (candidate == scaffoldAssetUrl) return true
|
||||
|
||||
val candidateUri = parseUri(candidate) ?: return false
|
||||
if (candidateUri.scheme.equals("file", ignoreCase = true)) {
|
||||
return false
|
||||
}
|
||||
val normalizedCandidate = normalizeTrustedRemoteA2uiUri(candidateUri) ?: return false
|
||||
|
||||
return trustedA2uiUrls.any { trusted ->
|
||||
matchesTrustedRemoteA2uiUrlExact(normalizedCandidate, trusted)
|
||||
}
|
||||
if (candidate == localA2uiAssetUrl) return true
|
||||
return false
|
||||
}
|
||||
|
||||
private fun matchesTrustedRemoteA2uiUrlExact(
|
||||
candidateUri: URI,
|
||||
trustedUrl: String,
|
||||
): Boolean {
|
||||
// Gateway-advertised URLs are capabilities. Treat malformed entries as
|
||||
// absent instead of broadening trust to same-origin or prefix matches.
|
||||
val trustedUri = parseUri(trustedUrl) ?: return false
|
||||
val normalizedTrusted = normalizeTrustedRemoteA2uiUri(trustedUri) ?: return false
|
||||
return candidateUri == normalizedTrusted
|
||||
}
|
||||
|
||||
/** Normalizes only the URL parts allowed to vary across trusted remote A2UI URLs. */
|
||||
private fun normalizeTrustedRemoteA2uiUri(uri: URI): URI? {
|
||||
// Keep Android trust normalization aligned with iOS ScreenController:
|
||||
// exact remote URL match, scheme/host normalized, fragment ignored.
|
||||
val scheme = uri.scheme?.lowercase() ?: return null
|
||||
if (scheme != "http" && scheme != "https") return null
|
||||
|
||||
val host =
|
||||
uri.host
|
||||
?.trim()
|
||||
?.takeIf { it.isNotEmpty() }
|
||||
?.lowercase() ?: return null
|
||||
|
||||
return try {
|
||||
URI(scheme, uri.userInfo, host, uri.port, uri.rawPath, uri.rawQuery, null)
|
||||
} catch (_: Throwable) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/** Parses untrusted WebView/gateway URL text without throwing into UI event handlers. */
|
||||
private fun parseUri(raw: String): URI? =
|
||||
try {
|
||||
URI(raw)
|
||||
} catch (_: Throwable) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,7 +48,8 @@ class CanvasController {
|
||||
private val _currentUrl = MutableStateFlow<String?>(null)
|
||||
val currentUrl: StateFlow<String?> = _currentUrl.asStateFlow()
|
||||
|
||||
private val scaffoldAssetUrl = "file:///android_asset/CanvasScaffold/scaffold.html"
|
||||
private val scaffoldAssetUrl = CanvasActionTrust.scaffoldAssetUrl
|
||||
private val localA2uiAssetUrl = CanvasActionTrust.localA2uiAssetUrl
|
||||
|
||||
private fun clampJpegQuality(quality: Double?): Int {
|
||||
val q = (quality ?: 0.82).coerceIn(0.1, 1.0)
|
||||
@@ -87,6 +88,13 @@ class CanvasController {
|
||||
reload()
|
||||
}
|
||||
|
||||
/** Shows the app-owned A2UI renderer that is allowed to dispatch native actions. */
|
||||
fun showLocalA2ui() {
|
||||
this.url = localA2uiAssetUrl
|
||||
_currentUrl.value = localA2uiAssetUrl
|
||||
reload()
|
||||
}
|
||||
|
||||
fun currentUrl(): String? = url
|
||||
|
||||
fun isDefaultCanvas(): Boolean = url == null
|
||||
|
||||
@@ -89,7 +89,6 @@ class InvokeDispatcher(
|
||||
private val debugBuild: () -> Boolean,
|
||||
private val onCanvasA2uiPush: () -> Unit,
|
||||
private val onCanvasA2uiReset: () -> Unit,
|
||||
private val refreshCanvasHostUrl: suspend () -> String?,
|
||||
private val motionActivityAvailable: () -> Boolean,
|
||||
private val motionPedometerAvailable: () -> Boolean,
|
||||
) {
|
||||
@@ -242,24 +241,11 @@ class InvokeDispatcher(
|
||||
}
|
||||
|
||||
private suspend fun withReadyA2ui(block: suspend () -> GatewaySession.InvokeResult): GatewaySession.InvokeResult {
|
||||
var a2uiUrl =
|
||||
a2uiHandler.resolveA2uiHostUrl()
|
||||
?: refreshCanvasHostUrl().let { a2uiHandler.resolveA2uiHostUrl() }
|
||||
?: return GatewaySession.InvokeResult.error(
|
||||
code = "A2UI_HOST_NOT_CONFIGURED",
|
||||
message = "A2UI_HOST_NOT_CONFIGURED: gateway did not advertise canvas host",
|
||||
)
|
||||
val readyOnFirstCheck = a2uiHandler.ensureA2uiReady(a2uiUrl)
|
||||
if (!readyOnFirstCheck) {
|
||||
// Gateway canvas host metadata can lag reconnects; refresh once before failing the command.
|
||||
refreshCanvasHostUrl()
|
||||
a2uiUrl = a2uiHandler.resolveA2uiHostUrl() ?: a2uiUrl
|
||||
if (!a2uiHandler.ensureA2uiReady(a2uiUrl)) {
|
||||
return GatewaySession.InvokeResult.error(
|
||||
code = "A2UI_HOST_UNAVAILABLE",
|
||||
message = "A2UI_HOST_UNAVAILABLE: A2UI host not reachable",
|
||||
)
|
||||
}
|
||||
if (!a2uiHandler.ensureA2uiReady()) {
|
||||
return GatewaySession.InvokeResult.error(
|
||||
code = "A2UI_HOST_UNAVAILABLE",
|
||||
message = "A2UI_HOST_UNAVAILABLE: bundled A2UI host not reachable",
|
||||
)
|
||||
}
|
||||
return block()
|
||||
}
|
||||
|
||||
@@ -152,9 +152,8 @@ fun CanvasScreen(
|
||||
}
|
||||
}
|
||||
|
||||
// The listener accepts any WebView origin at registration time because
|
||||
// gateway A2UI URLs are dynamic; CanvasActionTrust validates the live URL
|
||||
// before forwarding each message.
|
||||
// The listener accepts any WebView origin at registration time; native
|
||||
// dispatch still requires the live URL to be an app-owned bundled page.
|
||||
val bridge =
|
||||
CanvasA2UIActionBridge(
|
||||
isTrustedPage = { viewModel.isTrustedCanvasActionUrl(currentPageUrlRef.get()) },
|
||||
|
||||
@@ -7,66 +7,57 @@ import org.junit.Test
|
||||
class CanvasActionTrustTest {
|
||||
@Test
|
||||
fun acceptsBundledScaffoldAsset() {
|
||||
assertTrue(CanvasActionTrust.isTrustedCanvasActionUrl(CanvasActionTrust.scaffoldAssetUrl, emptyList()))
|
||||
assertTrue(CanvasActionTrust.isTrustedCanvasActionUrl(CanvasActionTrust.scaffoldAssetUrl))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun acceptsTrustedA2uiPageOnAdvertisedCanvasHost() {
|
||||
assertTrue(
|
||||
fun acceptsBundledA2uiAsset() {
|
||||
assertTrue(CanvasActionTrust.isTrustedCanvasActionUrl(CanvasActionTrust.localA2uiAssetUrl))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun rejectsRemoteHttpA2uiPageEvenWhenGatewayAdvertised() {
|
||||
assertFalse(
|
||||
CanvasActionTrust.isTrustedCanvasActionUrl(
|
||||
rawUrl = "http://canvas.example.com:9443/__openclaw__/cap/token/__openclaw__/a2ui/?platform=android",
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun rejectsRemoteHttpsA2uiPageEvenWhenGatewayAdvertised() {
|
||||
assertFalse(
|
||||
CanvasActionTrust.isTrustedCanvasActionUrl(
|
||||
rawUrl = "https://canvas.example.com:9443/__openclaw__/cap/token/__openclaw__/a2ui/?platform=android",
|
||||
trustedA2uiUrls = listOf("https://canvas.example.com:9443/__openclaw__/cap/token/__openclaw__/a2ui/?platform=android"),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun rejectsDifferentOriginEvenIfPathMatches() {
|
||||
fun rejectsRemoteCanvasPage() {
|
||||
assertFalse(
|
||||
CanvasActionTrust.isTrustedCanvasActionUrl(
|
||||
rawUrl = "https://evil.example.com:9443/__openclaw__/cap/token/__openclaw__/a2ui/?platform=android",
|
||||
trustedA2uiUrls = listOf("https://canvas.example.com:9443/__openclaw__/cap/token/__openclaw__/a2ui/?platform=android"),
|
||||
rawUrl = "https://canvas.example.com:9443/__openclaw__/canvas/",
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun rejectsUntrustedCanvasPagePathOnTrustedOrigin() {
|
||||
fun rejectsDescendantPathUnderBundledA2uiRoot() {
|
||||
assertFalse(
|
||||
CanvasActionTrust.isTrustedCanvasActionUrl(
|
||||
rawUrl = "https://canvas.example.com:9443/untrusted/index.html",
|
||||
trustedA2uiUrls = listOf("https://canvas.example.com:9443/__openclaw__/cap/token/__openclaw__/a2ui/?platform=android"),
|
||||
rawUrl = "file:///android_asset/CanvasA2UI/child/index.html",
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun acceptsFragmentOnlyDifferenceForTrustedA2uiPage() {
|
||||
assertTrue(
|
||||
CanvasActionTrust.isTrustedCanvasActionUrl(
|
||||
rawUrl = "https://canvas.example.com:9443/__openclaw__/cap/token/__openclaw__/a2ui/?platform=android#step2",
|
||||
trustedA2uiUrls = listOf("https://canvas.example.com:9443/__openclaw__/cap/token/__openclaw__/a2ui/?platform=android"),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun rejectsQueryMismatchOnTrustedOriginAndPath() {
|
||||
fun rejectsQueryOrFragmentChangesToBundledA2uiAsset() {
|
||||
assertFalse(
|
||||
CanvasActionTrust.isTrustedCanvasActionUrl(
|
||||
rawUrl = "https://canvas.example.com:9443/__openclaw__/cap/token/__openclaw__/a2ui/?platform=ios",
|
||||
trustedA2uiUrls = listOf("https://canvas.example.com:9443/__openclaw__/cap/token/__openclaw__/a2ui/?platform=android"),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun rejectsDescendantPathUnderTrustedA2uiRoot() {
|
||||
assertFalse(
|
||||
CanvasActionTrust.isTrustedCanvasActionUrl(
|
||||
rawUrl = "https://canvas.example.com:9443/__openclaw__/cap/token/__openclaw__/a2ui/child/index.html?platform=android",
|
||||
trustedA2uiUrls = listOf("https://canvas.example.com:9443/__openclaw__/cap/token/__openclaw__/a2ui/?platform=android"),
|
||||
rawUrl = "${CanvasActionTrust.localA2uiAssetUrl}?platform=android",
|
||||
),
|
||||
)
|
||||
assertFalse(CanvasActionTrust.isTrustedCanvasActionUrl("${CanvasActionTrust.localA2uiAssetUrl}#step2"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -299,8 +299,6 @@ class InvokeDispatcherTest {
|
||||
A2UIHandler(
|
||||
canvas = canvas,
|
||||
json = Json { ignoreUnknownKeys = true },
|
||||
getNodeCanvasHostUrl = { null },
|
||||
getOperatorCanvasHostUrl = { null },
|
||||
),
|
||||
debugHandler = DebugHandler(appContext, DeviceIdentityStore(appContext)),
|
||||
callLogHandler = CallLogHandler.forTesting(appContext, InvokeDispatcherFakeCallLogDataSource()),
|
||||
@@ -317,7 +315,6 @@ class InvokeDispatcherTest {
|
||||
debugBuild = { debugBuild },
|
||||
onCanvasA2uiPush = {},
|
||||
onCanvasA2uiReset = {},
|
||||
refreshCanvasHostUrl = { null },
|
||||
motionActivityAvailable = { motionActivityAvailable },
|
||||
motionPedometerAvailable = { motionPedometerAvailable },
|
||||
)
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
#!/usr/bin/env bun
|
||||
/**
|
||||
* Android release helper that bumps version fields, builds release AAB variants,
|
||||
* verifies signatures, and prints SHA-256 checksums.
|
||||
*/
|
||||
|
||||
import { $ } from "bun";
|
||||
import { dirname, join } from "node:path";
|
||||
|
||||
@@ -702,6 +702,9 @@ final class GatewayConnectionController {
|
||||
appModel.gatewayStatusText = "Connecting…"
|
||||
Task { [weak self, weak appModel] in
|
||||
guard let self, let appModel else { return }
|
||||
if forceReconnect {
|
||||
await appModel.resetGatewaySessionsForForcedReconnect()
|
||||
}
|
||||
let nodeOptions = await self.makeConnectOptions(stableID: gatewayStableID)
|
||||
let cfg = GatewayConnectConfig(
|
||||
url: url,
|
||||
@@ -990,7 +993,10 @@ extension GatewayConnectionController {
|
||||
}
|
||||
|
||||
private func currentCaps() -> [String] {
|
||||
var caps = [OpenClawCapability.canvas.rawValue, OpenClawCapability.screen.rawValue]
|
||||
var caps = [
|
||||
OpenClawCapability.canvas.rawValue,
|
||||
OpenClawCapability.screen.rawValue,
|
||||
]
|
||||
|
||||
// Default-on: if the key doesn't exist yet, treat it as enabled.
|
||||
let cameraEnabled =
|
||||
|
||||
@@ -1,106 +1,35 @@
|
||||
import Foundation
|
||||
import Network
|
||||
import OpenClawKit
|
||||
|
||||
enum A2UIReadyState {
|
||||
case ready(String)
|
||||
case hostNotConfigured
|
||||
case ready
|
||||
case hostUnavailable
|
||||
}
|
||||
|
||||
extension NodeAppModel {
|
||||
func resolveCanvasHostURL() async -> String? {
|
||||
guard let raw = await self.gatewaySession.currentCanvasHostUrl() else { return nil }
|
||||
let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !trimmed.isEmpty, let base = URL(string: trimmed) else { return nil }
|
||||
if let host = base.host, LoopbackHost.isLoopback(host) {
|
||||
return nil
|
||||
}
|
||||
return base.appendingPathComponent("__openclaw__/canvas/").absoluteString
|
||||
}
|
||||
|
||||
func _test_resolveA2UIHostURL() async -> String? {
|
||||
await self.resolveA2UIHostURL()
|
||||
}
|
||||
|
||||
func resolveA2UIHostURL() async -> String? {
|
||||
guard let raw = await self.gatewaySession.currentCanvasHostUrl() else { return nil }
|
||||
let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !trimmed.isEmpty, let base = URL(string: trimmed) else { return nil }
|
||||
if let host = base.host, LoopbackHost.isLoopback(host) {
|
||||
return nil
|
||||
}
|
||||
return base.appendingPathComponent("__openclaw__/a2ui/").absoluteString + "?platform=ios"
|
||||
}
|
||||
|
||||
/// Normalize a URL string for trust comparison: lowercase scheme/host and strip fragment.
|
||||
/// This matches the normalization applied by ScreenController.isTrustedCanvasUIURL so that
|
||||
/// SPA hash-routing fragments and scheme/host casing do not silently prevent trust being set.
|
||||
static func normalizeURLForTrustComparison(_ raw: String) -> String {
|
||||
guard let url = URL(string: raw),
|
||||
var components = URLComponents(url: url, resolvingAgainstBaseURL: false)
|
||||
else { return raw }
|
||||
components.fragment = nil
|
||||
components.scheme = components.scheme?.lowercased()
|
||||
components.host = components.host?.lowercased()
|
||||
return components.url?.absoluteString ?? raw
|
||||
}
|
||||
|
||||
func showA2UIOnConnectIfNeeded() async {
|
||||
await MainActor.run {
|
||||
// Keep the bundled home canvas as the default connected view.
|
||||
// Agents can still explicitly present a remote or local canvas later.
|
||||
self.lastAutoA2uiURL = nil
|
||||
self.screen.showDefaultCanvas()
|
||||
}
|
||||
}
|
||||
|
||||
func ensureA2UIReadyWithCapabilityRefresh(timeoutMs: Int = 5000) async -> A2UIReadyState {
|
||||
guard let initialUrl = await self.resolveA2UIHostURLWithCapabilityRefresh() else {
|
||||
return .hostNotConfigured
|
||||
if self.screen.isShowingLocalA2UI(),
|
||||
await self.screen.waitForA2UIReady(timeoutMs: timeoutMs)
|
||||
{
|
||||
return .ready
|
||||
}
|
||||
self.screen.navigate(to: initialUrl, trustA2UIActions: true)
|
||||
|
||||
self.screen.showLocalA2UI()
|
||||
if await self.screen.waitForA2UIReady(timeoutMs: timeoutMs) {
|
||||
return .ready(initialUrl)
|
||||
}
|
||||
guard let refreshedUrl = await self.resolveA2UIHostURLWithCapabilityRefresh(forceRefresh: true) else {
|
||||
return .hostUnavailable
|
||||
}
|
||||
self.screen.navigate(to: refreshedUrl, trustA2UIActions: true)
|
||||
if await self.screen.waitForA2UIReady(timeoutMs: timeoutMs) {
|
||||
return .ready(refreshedUrl)
|
||||
return .ready
|
||||
}
|
||||
return .hostUnavailable
|
||||
}
|
||||
|
||||
func showLocalCanvasOnDisconnect() {
|
||||
self.lastAutoA2uiURL = nil
|
||||
self.screen.showDefaultCanvas()
|
||||
}
|
||||
|
||||
private func resolveA2UIHostURLWithCapabilityRefresh(forceRefresh: Bool = false) async -> String? {
|
||||
if !forceRefresh, let current = await self.resolveA2UIHostURL() {
|
||||
return current
|
||||
}
|
||||
_ = await self.gatewaySession.refreshCanvasHostUrl()
|
||||
return await self.resolveA2UIHostURL()
|
||||
}
|
||||
|
||||
private func resolveCanvasHostURLWithCapabilityRefresh(forceRefresh: Bool = false) async -> String? {
|
||||
if !forceRefresh, let current = await self.resolveCanvasHostURL() {
|
||||
return current
|
||||
}
|
||||
_ = await self.gatewaySession.refreshCanvasHostUrl()
|
||||
return await self.resolveCanvasHostURL()
|
||||
}
|
||||
|
||||
private static func probeTCP(url: URL, timeoutSeconds: Double) async -> Bool {
|
||||
guard let host = url.host, !host.isEmpty else { return false }
|
||||
let portInt = url.port ?? ((url.scheme ?? "").lowercased() == "wss" ? 443 : 80)
|
||||
return await TCPProbe.probe(
|
||||
host: host,
|
||||
port: portInt,
|
||||
timeoutSeconds: timeoutSeconds,
|
||||
queueLabel: "a2ui.preflight")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,7 +173,6 @@ final class NodeAppModel {
|
||||
private let remindersService: any RemindersServicing
|
||||
private let motionService: any MotionServicing
|
||||
private let watchMessagingService: any WatchMessagingServicing
|
||||
var lastAutoA2uiURL: String?
|
||||
private var pttVoiceWakeSuspended = false
|
||||
private var talkVoiceWakeSuspended = false
|
||||
private var backgroundVoiceWakeSuspended = false
|
||||
@@ -1035,24 +1034,18 @@ final class NodeAppModel {
|
||||
OpenClawCanvasPresentParams()
|
||||
let url = params.url?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
if url.isEmpty {
|
||||
self.screen.showDefaultCanvas()
|
||||
self.screen.presentDefaultCanvas()
|
||||
} else {
|
||||
let trustedA2UIURL = await self.resolveA2UIHostURL()
|
||||
self.screen.navigate(
|
||||
to: url,
|
||||
trustA2UIActions: trustedA2UIURL == Self.normalizeURLForTrustComparison(url))
|
||||
self.screen.present(urlString: url)
|
||||
}
|
||||
return BridgeInvokeResponse(id: req.id, ok: true)
|
||||
case OpenClawCanvasCommand.hide.rawValue:
|
||||
self.screen.showDefaultCanvas()
|
||||
self.screen.hideCanvas()
|
||||
return BridgeInvokeResponse(id: req.id, ok: true)
|
||||
case OpenClawCanvasCommand.navigate.rawValue:
|
||||
let params = try Self.decodeParams(OpenClawCanvasNavigateParams.self, from: req.paramsJSON)
|
||||
let trimmedURL = params.url.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let trustedA2UIURL = await self.resolveA2UIHostURL()
|
||||
self.screen.navigate(
|
||||
to: trimmedURL,
|
||||
trustA2UIActions: trustedA2UIURL == Self.normalizeURLForTrustComparison(trimmedURL))
|
||||
self.screen.present(urlString: trimmedURL)
|
||||
return BridgeInvokeResponse(id: req.id, ok: true)
|
||||
case OpenClawCanvasCommand.evalJS.rawValue:
|
||||
let params = try Self.decodeParams(OpenClawCanvasEvalParams.self, from: req.paramsJSON)
|
||||
@@ -1095,20 +1088,13 @@ final class NodeAppModel {
|
||||
switch await self.ensureA2UIReadyWithCapabilityRefresh(timeoutMs: 5000) {
|
||||
case .ready:
|
||||
break
|
||||
case .hostNotConfigured:
|
||||
return BridgeInvokeResponse(
|
||||
id: req.id,
|
||||
ok: false,
|
||||
error: OpenClawNodeError(
|
||||
code: .unavailable,
|
||||
message: "A2UI_HOST_NOT_CONFIGURED: gateway did not advertise canvas host"))
|
||||
case .hostUnavailable:
|
||||
return BridgeInvokeResponse(
|
||||
id: req.id,
|
||||
ok: false,
|
||||
error: OpenClawNodeError(
|
||||
code: .unavailable,
|
||||
message: "A2UI_HOST_UNAVAILABLE: A2UI host not reachable"))
|
||||
message: "A2UI_HOST_UNAVAILABLE: bundled A2UI host not reachable"))
|
||||
}
|
||||
let json = try await self.screen.eval(javaScript: """
|
||||
(() => {
|
||||
@@ -1138,20 +1124,13 @@ final class NodeAppModel {
|
||||
switch await self.ensureA2UIReadyWithCapabilityRefresh(timeoutMs: 5000) {
|
||||
case .ready:
|
||||
break
|
||||
case .hostNotConfigured:
|
||||
return BridgeInvokeResponse(
|
||||
id: req.id,
|
||||
ok: false,
|
||||
error: OpenClawNodeError(
|
||||
code: .unavailable,
|
||||
message: "A2UI_HOST_NOT_CONFIGURED: gateway did not advertise canvas host"))
|
||||
case .hostUnavailable:
|
||||
return BridgeInvokeResponse(
|
||||
id: req.id,
|
||||
ok: false,
|
||||
error: OpenClawNodeError(
|
||||
code: .unavailable,
|
||||
message: "A2UI_HOST_UNAVAILABLE: A2UI host not reachable"))
|
||||
message: "A2UI_HOST_UNAVAILABLE: bundled A2UI host not reachable"))
|
||||
}
|
||||
|
||||
let messagesJSON = try OpenClawCanvasA2UIJSONL.encodeMessagesJSONArray(messages)
|
||||
@@ -1960,6 +1939,15 @@ extension NodeAppModel {
|
||||
forceReconnect: forceReconnect)
|
||||
}
|
||||
|
||||
func resetGatewaySessionsForForcedReconnect() async {
|
||||
self.nodeGatewayTask?.cancel()
|
||||
self.nodeGatewayTask = nil
|
||||
self.operatorGatewayTask?.cancel()
|
||||
self.operatorGatewayTask = nil
|
||||
await self.operatorGateway.disconnect()
|
||||
await self.nodeGateway.disconnect()
|
||||
}
|
||||
|
||||
func disconnectGateway() {
|
||||
self.gatewayAutoReconnectEnabled = false
|
||||
self.gatewayPairingPaused = false
|
||||
@@ -4578,6 +4566,10 @@ extension NodeAppModel {
|
||||
self.clearingBootstrapToken(in: config)
|
||||
}
|
||||
|
||||
func _test_hasGatewayLoopTasks() -> (node: Bool, operator: Bool) {
|
||||
(self.nodeGatewayTask != nil, self.operatorGatewayTask != nil)
|
||||
}
|
||||
|
||||
func _test_handleSuccessfulBootstrapGatewayOnboarding() async {
|
||||
await self.handleSuccessfulBootstrapGatewayOnboarding(
|
||||
url: URL(string: "wss://gateway.example")!,
|
||||
|
||||
@@ -200,6 +200,36 @@ struct RootTabs: View {
|
||||
RootCameraFlashOverlay(nonce: self.appModel.cameraFlashNonce)
|
||||
}
|
||||
}
|
||||
.overlay {
|
||||
if self.appModel.screen.isCanvasPresented {
|
||||
self.canvasPresentationOverlay
|
||||
.transition(.opacity)
|
||||
.zIndex(20)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var canvasPresentationOverlay: some View {
|
||||
ZStack(alignment: .topTrailing) {
|
||||
Color.black.ignoresSafeArea()
|
||||
ScreenWebView(controller: self.appModel.screen)
|
||||
.ignoresSafeArea()
|
||||
Button {
|
||||
self.appModel.screen.hideCanvas()
|
||||
} label: {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
.font(.system(size: 30, weight: .semibold))
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.foregroundStyle(.white)
|
||||
.shadow(color: .black.opacity(0.32), radius: 8, y: 2)
|
||||
.frame(width: 48, height: 48)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityLabel("Close canvas")
|
||||
.safeAreaPadding(.top, 8)
|
||||
.padding(.trailing, 12)
|
||||
}
|
||||
}
|
||||
|
||||
private func rootLifecycle(_ content: some View) -> some View {
|
||||
|
||||
@@ -7,10 +7,10 @@ import WebKit
|
||||
@Observable
|
||||
final class ScreenController {
|
||||
private weak var activeWebView: WKWebView?
|
||||
private var trustedRemoteA2UIURL: URL?
|
||||
|
||||
var urlString: String = ""
|
||||
var errorText: String?
|
||||
var isCanvasPresented: Bool = false
|
||||
|
||||
/// Callback invoked when an openclaw:// deep link is tapped in the canvas
|
||||
var onDeepLink: ((URL) -> Void)?
|
||||
@@ -27,11 +27,10 @@ final class ScreenController {
|
||||
self.reload()
|
||||
}
|
||||
|
||||
func navigate(to urlString: String, trustA2UIActions: Bool = false) {
|
||||
func navigate(to urlString: String, trustA2UIActions _: Bool = false) {
|
||||
let trimmed = urlString.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if trimmed.isEmpty {
|
||||
self.urlString = ""
|
||||
self.trustedRemoteA2UIURL = nil
|
||||
self.reload()
|
||||
return
|
||||
}
|
||||
@@ -45,7 +44,6 @@ final class ScreenController {
|
||||
return
|
||||
}
|
||||
self.urlString = (trimmed == "/" ? "" : trimmed)
|
||||
self.trustedRemoteA2UIURL = trustA2UIActions ? Self.normalizeTrustedRemoteA2UIURL(from: trimmed) : nil
|
||||
self.reload()
|
||||
}
|
||||
|
||||
@@ -75,10 +73,42 @@ final class ScreenController {
|
||||
|
||||
func showDefaultCanvas() {
|
||||
self.urlString = ""
|
||||
self.trustedRemoteA2UIURL = nil
|
||||
self.reload()
|
||||
}
|
||||
|
||||
func presentDefaultCanvas() {
|
||||
self.isCanvasPresented = true
|
||||
self.showDefaultCanvas()
|
||||
}
|
||||
|
||||
func present(urlString: String) {
|
||||
self.isCanvasPresented = true
|
||||
self.navigate(to: urlString)
|
||||
}
|
||||
|
||||
func hideCanvas() {
|
||||
self.isCanvasPresented = false
|
||||
self.showDefaultCanvas()
|
||||
}
|
||||
|
||||
func showLocalA2UI() {
|
||||
self.isCanvasPresented = true
|
||||
guard let url = Self.localA2UIURL else {
|
||||
self.showDefaultCanvas()
|
||||
return
|
||||
}
|
||||
self.urlString = url.absoluteString
|
||||
self.reload()
|
||||
}
|
||||
|
||||
func isShowingLocalA2UI() -> Bool {
|
||||
guard let url = URL(string: self.urlString),
|
||||
url.isFileURL,
|
||||
let expected = Self.localA2UIURL
|
||||
else { return false }
|
||||
return url.standardizedFileURL == expected.standardizedFileURL
|
||||
}
|
||||
|
||||
func setDebugStatusEnabled(_ enabled: Bool) {
|
||||
self.debugStatusEnabled = enabled
|
||||
self.applyDebugStatusIfNeeded()
|
||||
@@ -239,6 +269,11 @@ final class ScreenController {
|
||||
ext: "html",
|
||||
subdirectory: "CanvasScaffold")
|
||||
|
||||
private static let localA2UIURL: URL? = ScreenController.bundledResourceURL(
|
||||
name: "index",
|
||||
ext: "html",
|
||||
subdirectory: "CanvasA2UI")
|
||||
|
||||
func isTrustedCanvasUIURL(_ url: URL) -> Bool {
|
||||
if url.isFileURL {
|
||||
let std = url.standardizedFileURL
|
||||
@@ -247,10 +282,14 @@ final class ScreenController {
|
||||
{
|
||||
return true
|
||||
}
|
||||
if let expected = Self.localA2UIURL,
|
||||
std == expected.standardizedFileURL
|
||||
{
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
guard let trusted = self.trustedRemoteA2UIURL else { return false }
|
||||
return Self.normalizeTrustedRemoteA2UIURL(from: url) == trusted
|
||||
return false
|
||||
}
|
||||
|
||||
nonisolated static func parseA2UIActionBody(_ body: Any) -> [String: Any]? {
|
||||
@@ -280,26 +319,6 @@ final class ScreenController {
|
||||
scrollView.isScrollEnabled = allowScroll
|
||||
scrollView.bounces = allowScroll
|
||||
}
|
||||
|
||||
private static func normalizeTrustedRemoteA2UIURL(from raw: String) -> URL? {
|
||||
guard let url = URL(string: raw) else { return nil }
|
||||
return self.normalizeTrustedRemoteA2UIURL(from: url)
|
||||
}
|
||||
|
||||
private static func normalizeTrustedRemoteA2UIURL(from url: URL) -> URL? {
|
||||
guard !url.isFileURL else { return nil }
|
||||
guard let scheme = url.scheme?.lowercased(), scheme == "http" || scheme == "https" else {
|
||||
return nil
|
||||
}
|
||||
guard let host = url.host?.trimmingCharacters(in: .whitespacesAndNewlines), !host.isEmpty else {
|
||||
return nil
|
||||
}
|
||||
var components = URLComponents(url: url, resolvingAgainstBaseURL: false)
|
||||
components?.scheme = scheme
|
||||
components?.host = host.lowercased()
|
||||
components?.fragment = nil
|
||||
return components?.url
|
||||
}
|
||||
}
|
||||
|
||||
extension Double {
|
||||
|
||||
@@ -235,6 +235,20 @@ import UIKit
|
||||
#expect(appModel.connectedGatewayID == second.stableID)
|
||||
}
|
||||
|
||||
@Test @MainActor func forcedReconnectResetClearsActiveGatewayLoopTasks() async {
|
||||
let appModel = NodeAppModel()
|
||||
defer { appModel.disconnectGateway() }
|
||||
|
||||
appModel.applyGatewayConnectConfig(Self.makeGatewayConnectConfig())
|
||||
#expect(appModel._test_hasGatewayLoopTasks().node)
|
||||
#expect(appModel._test_hasGatewayLoopTasks().operator)
|
||||
|
||||
await appModel.resetGatewaySessionsForForcedReconnect()
|
||||
|
||||
#expect(!appModel._test_hasGatewayLoopTasks().node)
|
||||
#expect(!appModel._test_hasGatewayLoopTasks().operator)
|
||||
}
|
||||
|
||||
@Test @MainActor func loadLastConnectionReadsSavedValues() {
|
||||
let prior = KeychainStore.loadString(service: "ai.openclaw.gateway", account: "lastConnection")
|
||||
defer {
|
||||
|
||||
@@ -623,13 +623,13 @@ private final class MockBootstrapNotificationCenter: NotificationCentering, @unc
|
||||
#expect(appModel.screen.urlString.isEmpty)
|
||||
}
|
||||
|
||||
@Test @MainActor func handleInvokeA2UICommandsFailWhenHostMissing() async throws {
|
||||
@Test @MainActor func handleInvokeA2UICommandsFailWhenLocalHostUnavailable() async throws {
|
||||
let appModel = NodeAppModel()
|
||||
|
||||
let reset = BridgeInvokeRequest(id: "reset", command: OpenClawCanvasA2UICommand.reset.rawValue)
|
||||
let resetRes = await appModel._test_handleInvoke(reset)
|
||||
#expect(resetRes.ok == false)
|
||||
#expect(resetRes.error?.message.contains("A2UI_HOST_NOT_CONFIGURED") == true)
|
||||
#expect(resetRes.error?.message.contains("A2UI_HOST_UNAVAILABLE") == true)
|
||||
|
||||
let jsonl = "{\"beginRendering\":{}}"
|
||||
let pushParams = OpenClawCanvasA2UIPushJSONLParams(jsonl: jsonl)
|
||||
@@ -641,7 +641,7 @@ private final class MockBootstrapNotificationCenter: NotificationCentering, @unc
|
||||
paramsJSON: pushJSON)
|
||||
let pushRes = await appModel._test_handleInvoke(push)
|
||||
#expect(pushRes.ok == false)
|
||||
#expect(pushRes.error?.message.contains("A2UI_HOST_NOT_CONFIGURED") == true)
|
||||
#expect(pushRes.error?.message.contains("A2UI_HOST_UNAVAILABLE") == true)
|
||||
}
|
||||
|
||||
@Test @MainActor func handleInvokeUnknownCommandReturnsInvalidRequest() async {
|
||||
|
||||
@@ -45,6 +45,23 @@ private func mountScreen(_ screen: ScreenController) throws -> (ScreenWebViewCoo
|
||||
#expect(screen.urlString.isEmpty)
|
||||
}
|
||||
|
||||
@Test @MainActor func canvasPresentationTracksExplicitPresentAndHide() {
|
||||
let screen = ScreenController()
|
||||
|
||||
#expect(screen.isCanvasPresented == false)
|
||||
|
||||
screen.showDefaultCanvas()
|
||||
#expect(screen.isCanvasPresented == false)
|
||||
|
||||
screen.presentDefaultCanvas()
|
||||
#expect(screen.isCanvasPresented == true)
|
||||
#expect(screen.urlString.isEmpty)
|
||||
|
||||
screen.hideCanvas()
|
||||
#expect(screen.isCanvasPresented == false)
|
||||
#expect(screen.urlString.isEmpty)
|
||||
}
|
||||
|
||||
@Test @MainActor func evalExecutesJavaScript() async throws {
|
||||
let screen = ScreenController()
|
||||
let (coordinator, _) = try mountScreen(screen)
|
||||
@@ -66,26 +83,37 @@ private func mountScreen(_ screen: ScreenController) throws -> (ScreenWebViewCoo
|
||||
}
|
||||
}
|
||||
|
||||
@Test @MainActor func trustedRemoteA2UIURLMustMatchExactly() {
|
||||
@Test("remote A2UI URL is not trusted for native actions")
|
||||
@MainActor func remoteA2UIURLIsNotTrustedForNativeActions() throws {
|
||||
let screen = ScreenController()
|
||||
let trusted = "https://node.ts.net:18789/__openclaw__/a2ui/?platform=ios"
|
||||
screen.navigate(to: trusted, trustA2UIActions: true)
|
||||
|
||||
#expect(screen.isTrustedCanvasUIURL(URL(string: trusted)!) == true)
|
||||
// Fragment differences must not affect trust (SPA hash routing).
|
||||
#expect(screen.isTrustedCanvasUIURL(URL(string: "https://node.ts.net:18789/__openclaw__/a2ui/?platform=ios#step2")!) == true)
|
||||
#expect(screen.isTrustedCanvasUIURL(URL(string: "https://node.ts.net:18789/__openclaw__/a2ui/?platform=android")!) == false)
|
||||
#expect(screen.isTrustedCanvasUIURL(URL(string: "https://node.ts.net:18789/__openclaw__/canvas/")!) == false)
|
||||
#expect(screen.isTrustedCanvasUIURL(URL(string: "https://evil.ts.net:18789/__openclaw__/a2ui/?platform=ios")!) == false)
|
||||
#expect(screen.isTrustedCanvasUIURL(URL(string: "http://192.168.0.10:18789/")!) == false)
|
||||
#expect(screen.isShowingLocalA2UI() == false)
|
||||
|
||||
let urls = try [
|
||||
trusted,
|
||||
"https://node.ts.net:18789/__openclaw__/a2ui/?platform=ios#step2",
|
||||
"http://192.168.0.10:18789/__openclaw__/a2ui/?platform=ios",
|
||||
"https://node.ts.net:18789/__openclaw__/a2ui/?platform=android",
|
||||
"https://node.ts.net:18789/__openclaw__/canvas/",
|
||||
"https://evil.ts.net:18789/__openclaw__/a2ui/?platform=ios",
|
||||
].map { try #require(URL(string: $0)) }
|
||||
|
||||
for url in urls {
|
||||
#expect(screen.isTrustedCanvasUIURL(url) == false)
|
||||
}
|
||||
}
|
||||
|
||||
@Test @MainActor func genericNavigationClearsTrustedRemoteA2UIURL() {
|
||||
@Test("local A2UI URL is trusted for native actions")
|
||||
@MainActor func localA2UIURLIsTrustedForNativeActions() throws {
|
||||
let screen = ScreenController()
|
||||
screen.navigate(to: "https://node.ts.net:18789/__openclaw__/a2ui/?platform=ios", trustA2UIActions: true)
|
||||
screen.navigate(to: "https://evil.ts.net:18789/")
|
||||
screen.showLocalA2UI()
|
||||
|
||||
#expect(screen.isTrustedCanvasUIURL(URL(string: "https://node.ts.net:18789/__openclaw__/a2ui/?platform=ios")!) == false)
|
||||
let url = try #require(URL(string: screen.urlString))
|
||||
#expect(url.isFileURL)
|
||||
#expect(screen.isShowingLocalA2UI() == true)
|
||||
#expect(screen.isTrustedCanvasUIURL(url) == true)
|
||||
}
|
||||
|
||||
@Test func parseA2UIActionBodyAcceptsJSONString() throws {
|
||||
|
||||
@@ -139,7 +139,10 @@ final class MacNodeModeCoordinator {
|
||||
locationMode: OpenClawLocationMode,
|
||||
connectionMode: AppState.ConnectionMode) -> [String]
|
||||
{
|
||||
var caps: [String] = [OpenClawCapability.canvas.rawValue, OpenClawCapability.screen.rawValue]
|
||||
var caps: [String] = [
|
||||
OpenClawCapability.canvas.rawValue,
|
||||
OpenClawCapability.screen.rawValue,
|
||||
]
|
||||
if browserControlEnabled, connectionMode == .local {
|
||||
caps.append(OpenClawCapability.browser.rawValue)
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
After Width: | Height: | Size: 86 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 258 KiB |
@@ -0,0 +1,311 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>OpenClaw Canvas</title>
|
||||
<script>
|
||||
(() => {
|
||||
const normalizeLower = (value) => {
|
||||
const trimmed = String(value || "").trim();
|
||||
return trimmed.toLocaleLowerCase();
|
||||
};
|
||||
try {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const platform = normalizeLower(params.get("platform"));
|
||||
if (platform) {
|
||||
document.documentElement.dataset.platform = platform;
|
||||
return;
|
||||
}
|
||||
if (/android/i.test(navigator.userAgent || "")) {
|
||||
document.documentElement.dataset.platform = "android";
|
||||
}
|
||||
} catch (_) {}
|
||||
})();
|
||||
</script>
|
||||
<style>
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
body::before,
|
||||
body::after {
|
||||
animation: none !important;
|
||||
}
|
||||
}
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
body {
|
||||
font:
|
||||
14px system-ui,
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
"Roboto",
|
||||
sans-serif;
|
||||
background:
|
||||
radial-gradient(1200px 900px at 15% 20%, rgba(42, 113, 255, 0.18), rgba(0, 0, 0, 0) 55%),
|
||||
radial-gradient(900px 700px at 85% 30%, rgba(255, 0, 138, 0.14), rgba(0, 0, 0, 0) 60%),
|
||||
radial-gradient(1000px 900px at 60% 90%, rgba(0, 209, 255, 0.1), rgba(0, 0, 0, 0) 60%),
|
||||
#000;
|
||||
color: #e5e7eb;
|
||||
overflow: hidden;
|
||||
}
|
||||
:root[data-platform="android"] body {
|
||||
background:
|
||||
radial-gradient(1200px 900px at 15% 20%, rgba(42, 113, 255, 0.62), rgba(0, 0, 0, 0) 55%),
|
||||
radial-gradient(900px 700px at 85% 30%, rgba(255, 0, 138, 0.52), rgba(0, 0, 0, 0) 60%),
|
||||
radial-gradient(1000px 900px at 60% 90%, rgba(0, 209, 255, 0.48), rgba(0, 0, 0, 0) 60%),
|
||||
#0b1328;
|
||||
}
|
||||
body::before {
|
||||
content: "";
|
||||
position: fixed;
|
||||
inset: -20%;
|
||||
background:
|
||||
repeating-linear-gradient(
|
||||
0deg,
|
||||
rgba(255, 255, 255, 0.03) 0,
|
||||
rgba(255, 255, 255, 0.03) 1px,
|
||||
transparent 1px,
|
||||
transparent 48px
|
||||
),
|
||||
repeating-linear-gradient(
|
||||
90deg,
|
||||
rgba(255, 255, 255, 0.03) 0,
|
||||
rgba(255, 255, 255, 0.03) 1px,
|
||||
transparent 1px,
|
||||
transparent 48px
|
||||
);
|
||||
transform: translate3d(0, 0, 0) rotate(-7deg);
|
||||
will-change: transform, opacity;
|
||||
-webkit-backface-visibility: hidden;
|
||||
backface-visibility: hidden;
|
||||
opacity: 0.45;
|
||||
pointer-events: none;
|
||||
animation: openclaw-grid-drift 140s ease-in-out infinite alternate;
|
||||
}
|
||||
:root[data-platform="android"] body::before {
|
||||
opacity: 0.8;
|
||||
}
|
||||
body::after {
|
||||
content: "";
|
||||
position: fixed;
|
||||
inset: -35%;
|
||||
background:
|
||||
radial-gradient(900px 700px at 30% 30%, rgba(42, 113, 255, 0.16), rgba(0, 0, 0, 0) 60%),
|
||||
radial-gradient(800px 650px at 70% 35%, rgba(255, 0, 138, 0.12), rgba(0, 0, 0, 0) 62%),
|
||||
radial-gradient(900px 800px at 55% 75%, rgba(0, 209, 255, 0.1), rgba(0, 0, 0, 0) 62%);
|
||||
filter: blur(28px);
|
||||
opacity: 0.52;
|
||||
will-change: transform, opacity;
|
||||
-webkit-backface-visibility: hidden;
|
||||
backface-visibility: hidden;
|
||||
transform: translate3d(0, 0, 0);
|
||||
pointer-events: none;
|
||||
animation: openclaw-glow-drift 110s ease-in-out infinite alternate;
|
||||
}
|
||||
:root[data-platform="android"] body::after {
|
||||
opacity: 0.85;
|
||||
}
|
||||
@supports (mix-blend-mode: screen) {
|
||||
body::after {
|
||||
mix-blend-mode: screen;
|
||||
}
|
||||
}
|
||||
@supports not (mix-blend-mode: screen) {
|
||||
body::after {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
@keyframes openclaw-grid-drift {
|
||||
0% {
|
||||
transform: translate3d(-12px, 8px, 0) rotate(-7deg);
|
||||
opacity: 0.4;
|
||||
}
|
||||
50% {
|
||||
transform: translate3d(10px, -7px, 0) rotate(-6.6deg);
|
||||
opacity: 0.56;
|
||||
}
|
||||
100% {
|
||||
transform: translate3d(-8px, 6px, 0) rotate(-7.2deg);
|
||||
opacity: 0.42;
|
||||
}
|
||||
}
|
||||
@keyframes openclaw-glow-drift {
|
||||
0% {
|
||||
transform: translate3d(-18px, 12px, 0) scale(1.02);
|
||||
opacity: 0.4;
|
||||
}
|
||||
50% {
|
||||
transform: translate3d(14px, -10px, 0) scale(1.05);
|
||||
opacity: 0.52;
|
||||
}
|
||||
100% {
|
||||
transform: translate3d(-10px, 8px, 0) scale(1.03);
|
||||
opacity: 0.43;
|
||||
}
|
||||
}
|
||||
canvas {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
display: block;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
touch-action: none;
|
||||
z-index: 1;
|
||||
}
|
||||
:root[data-platform="android"] #openclaw-canvas {
|
||||
background:
|
||||
radial-gradient(1100px 800px at 20% 15%, rgba(42, 113, 255, 0.78), rgba(0, 0, 0, 0) 58%),
|
||||
radial-gradient(900px 650px at 82% 28%, rgba(255, 0, 138, 0.66), rgba(0, 0, 0, 0) 62%),
|
||||
radial-gradient(1000px 900px at 60% 88%, rgba(0, 209, 255, 0.58), rgba(0, 0, 0, 0) 62%),
|
||||
#141c33;
|
||||
}
|
||||
#openclaw-status {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
padding: 24px;
|
||||
box-sizing: border-box;
|
||||
pointer-events: none;
|
||||
z-index: 3;
|
||||
}
|
||||
#openclaw-status .card {
|
||||
width: min(560px, 88vw);
|
||||
text-align: left;
|
||||
padding: 14px 16px 12px;
|
||||
border-radius: 16px;
|
||||
background: linear-gradient(140deg, rgba(23, 24, 35, 0.78), rgba(18, 19, 28, 0.55));
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
box-shadow:
|
||||
0 16px 46px rgba(0, 0, 0, 0.52),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.06);
|
||||
-webkit-backdrop-filter: blur(18px) saturate(140%);
|
||||
backdrop-filter: blur(18px) saturate(140%);
|
||||
}
|
||||
#openclaw-status .title {
|
||||
font:
|
||||
600 12px/1.2 -apple-system,
|
||||
BlinkMacSystemFont,
|
||||
"SF Pro Text",
|
||||
system-ui,
|
||||
sans-serif;
|
||||
letter-spacing: 0.45px;
|
||||
text-transform: uppercase;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
#openclaw-status .subtitle {
|
||||
margin-top: 8px;
|
||||
font:
|
||||
500 13px/1.45 -apple-system,
|
||||
BlinkMacSystemFont,
|
||||
"SF Pro Text",
|
||||
system-ui,
|
||||
sans-serif;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
openclaw-a2ui-host {
|
||||
display: block;
|
||||
height: 100%;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 4;
|
||||
--openclaw-a2ui-inset-top: 28px;
|
||||
--openclaw-a2ui-inset-right: 0px;
|
||||
--openclaw-a2ui-inset-bottom: 0px;
|
||||
--openclaw-a2ui-inset-left: 0px;
|
||||
--openclaw-a2ui-scroll-pad-bottom: 0px;
|
||||
--openclaw-a2ui-status-top: calc(50% - 18px);
|
||||
--openclaw-a2ui-empty-top: 18px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<canvas id="openclaw-canvas"></canvas>
|
||||
<div id="openclaw-status" role="status" aria-live="polite">
|
||||
<section class="card">
|
||||
<div class="title" id="openclaw-status-title">Ready</div>
|
||||
<div class="subtitle" id="openclaw-status-subtitle">Waiting for agent</div>
|
||||
</section>
|
||||
</div>
|
||||
<openclaw-a2ui-host></openclaw-a2ui-host>
|
||||
<script src="a2ui.bundle.js"></script>
|
||||
<script>
|
||||
(() => {
|
||||
const canvas = document.getElementById("openclaw-canvas");
|
||||
const ctx = canvas.getContext("2d");
|
||||
const statusEl = document.getElementById("openclaw-status");
|
||||
const titleEl = document.getElementById("openclaw-status-title");
|
||||
const subtitleEl = document.getElementById("openclaw-status-subtitle");
|
||||
const debugStatusEnabledByQuery = (() => {
|
||||
try {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const raw = params.get("debugStatus") ?? params.get("debug");
|
||||
if (!raw) return false;
|
||||
const normalized = normalizeLower(raw);
|
||||
return normalized === "1" || normalized === "true" || normalized === "yes";
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
})();
|
||||
let debugStatusEnabled = debugStatusEnabledByQuery;
|
||||
|
||||
function resize() {
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
const w = Math.max(1, Math.floor(window.innerWidth * dpr));
|
||||
const h = Math.max(1, Math.floor(window.innerHeight * dpr));
|
||||
canvas.width = w;
|
||||
canvas.height = h;
|
||||
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||
}
|
||||
|
||||
window.addEventListener("resize", resize);
|
||||
resize();
|
||||
|
||||
const setDebugStatusEnabled = (enabled) => {
|
||||
debugStatusEnabled = !!enabled;
|
||||
if (!statusEl) return;
|
||||
if (!debugStatusEnabled) {
|
||||
statusEl.style.display = "none";
|
||||
}
|
||||
};
|
||||
|
||||
if (statusEl && !debugStatusEnabled) {
|
||||
statusEl.style.display = "none";
|
||||
}
|
||||
|
||||
window.__openclaw = {
|
||||
canvas,
|
||||
ctx,
|
||||
setDebugStatusEnabled,
|
||||
setStatus: (title, subtitle) => {
|
||||
if (!statusEl || !debugStatusEnabled) return;
|
||||
if (!title && !subtitle) {
|
||||
statusEl.style.display = "none";
|
||||
return;
|
||||
}
|
||||
statusEl.style.display = "flex";
|
||||
if (titleEl && typeof title === "string") titleEl.textContent = title;
|
||||
if (subtitleEl && typeof subtitle === "string") subtitleEl.textContent = subtitle;
|
||||
if (!debugStatusEnabled) {
|
||||
clearTimeout(window.__statusTimeout);
|
||||
window.__statusTimeout = setTimeout(() => {
|
||||
statusEl.style.display = "none";
|
||||
}, 3000);
|
||||
} else {
|
||||
clearTimeout(window.__statusTimeout);
|
||||
}
|
||||
},
|
||||
};
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -52,6 +52,7 @@ public struct ConnectParams: Codable, Sendable {
|
||||
public let client: [String: AnyCodable]
|
||||
public let caps: [String]?
|
||||
public let commands: [String]?
|
||||
public let nodeplugintools: [NodePluginToolDescriptor]?
|
||||
public let permissions: [String: AnyCodable]?
|
||||
public let pathenv: String?
|
||||
public let role: String?
|
||||
@@ -67,6 +68,7 @@ public struct ConnectParams: Codable, Sendable {
|
||||
client: [String: AnyCodable],
|
||||
caps: [String]?,
|
||||
commands: [String]?,
|
||||
nodeplugintools: [NodePluginToolDescriptor]?,
|
||||
permissions: [String: AnyCodable]?,
|
||||
pathenv: String?,
|
||||
role: String?,
|
||||
@@ -81,6 +83,7 @@ public struct ConnectParams: Codable, Sendable {
|
||||
self.client = client
|
||||
self.caps = caps
|
||||
self.commands = commands
|
||||
self.nodeplugintools = nodeplugintools
|
||||
self.permissions = permissions
|
||||
self.pathenv = pathenv
|
||||
self.role = role
|
||||
@@ -97,6 +100,7 @@ public struct ConnectParams: Codable, Sendable {
|
||||
case client
|
||||
case caps
|
||||
case commands
|
||||
case nodeplugintools = "nodePluginTools"
|
||||
case permissions
|
||||
case pathenv = "pathEnv"
|
||||
case role
|
||||
@@ -1128,6 +1132,54 @@ public struct NodeRenameParams: Codable, Sendable {
|
||||
|
||||
public struct NodeListParams: Codable, Sendable {}
|
||||
|
||||
public struct NodePluginToolDescriptor: Codable, Sendable {
|
||||
public let pluginid: String
|
||||
public let name: String
|
||||
public let description: String
|
||||
public let parameters: [String: AnyCodable]?
|
||||
public let command: String?
|
||||
public let mcp: [String: AnyCodable]?
|
||||
|
||||
public init(
|
||||
pluginid: String,
|
||||
name: String,
|
||||
description: String,
|
||||
parameters: [String: AnyCodable]?,
|
||||
command: String?,
|
||||
mcp: [String: AnyCodable]?)
|
||||
{
|
||||
self.pluginid = pluginid
|
||||
self.name = name
|
||||
self.description = description
|
||||
self.parameters = parameters
|
||||
self.command = command
|
||||
self.mcp = mcp
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case pluginid = "pluginId"
|
||||
case name
|
||||
case description
|
||||
case parameters
|
||||
case command
|
||||
case mcp
|
||||
}
|
||||
}
|
||||
|
||||
public struct NodePluginToolsUpdateParams: Codable, Sendable {
|
||||
public let tools: [NodePluginToolDescriptor]
|
||||
|
||||
public init(
|
||||
tools: [NodePluginToolDescriptor])
|
||||
{
|
||||
self.tools = tools
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case tools
|
||||
}
|
||||
}
|
||||
|
||||
public struct NodePendingAckParams: Codable, Sendable {
|
||||
public let ids: [String]
|
||||
|
||||
@@ -5478,6 +5530,62 @@ public struct SkillsProposalReviseParams: Codable, Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
public struct SkillsProposalRequestRevisionParams: Codable, Sendable {
|
||||
public let agentid: String?
|
||||
public let targetagentid: String?
|
||||
public let proposalid: String
|
||||
public let instructions: String
|
||||
public let sessionkey: String
|
||||
public let sessionid: String?
|
||||
public let idempotencykey: String
|
||||
|
||||
public init(
|
||||
agentid: String? = nil,
|
||||
targetagentid: String?,
|
||||
proposalid: String,
|
||||
instructions: String,
|
||||
sessionkey: String,
|
||||
sessionid: String?,
|
||||
idempotencykey: String)
|
||||
{
|
||||
self.agentid = agentid
|
||||
self.targetagentid = targetagentid
|
||||
self.proposalid = proposalid
|
||||
self.instructions = instructions
|
||||
self.sessionkey = sessionkey
|
||||
self.sessionid = sessionid
|
||||
self.idempotencykey = idempotencykey
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case agentid = "agentId"
|
||||
case targetagentid = "targetAgentId"
|
||||
case proposalid = "proposalId"
|
||||
case instructions
|
||||
case sessionkey = "sessionKey"
|
||||
case sessionid = "sessionId"
|
||||
case idempotencykey = "idempotencyKey"
|
||||
}
|
||||
}
|
||||
|
||||
public struct SkillsProposalRequestRevisionResult: Codable, Sendable {
|
||||
public let runid: String
|
||||
public let status: AnyCodable
|
||||
|
||||
public init(
|
||||
runid: String,
|
||||
status: AnyCodable)
|
||||
{
|
||||
self.runid = runid
|
||||
self.status = status
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case runid = "runId"
|
||||
case status
|
||||
}
|
||||
}
|
||||
|
||||
public struct SkillsProposalActionParams: Codable, Sendable {
|
||||
public let agentid: String?
|
||||
public let proposalid: String
|
||||
@@ -6974,6 +7082,7 @@ public struct ChatSendParams: Codable, Sendable {
|
||||
public let timeoutms: Int?
|
||||
public let systeminputprovenance: [String: AnyCodable]?
|
||||
public let systemprovenancereceipt: String?
|
||||
public let suppresscommandinterpretation: Bool?
|
||||
public let idempotencykey: String
|
||||
|
||||
public init(
|
||||
@@ -6992,6 +7101,7 @@ public struct ChatSendParams: Codable, Sendable {
|
||||
timeoutms: Int?,
|
||||
systeminputprovenance: [String: AnyCodable]?,
|
||||
systemprovenancereceipt: String?,
|
||||
suppresscommandinterpretation: Bool?,
|
||||
idempotencykey: String)
|
||||
{
|
||||
self.sessionkey = sessionkey
|
||||
@@ -7009,6 +7119,7 @@ public struct ChatSendParams: Codable, Sendable {
|
||||
self.timeoutms = timeoutms
|
||||
self.systeminputprovenance = systeminputprovenance
|
||||
self.systemprovenancereceipt = systemprovenancereceipt
|
||||
self.suppresscommandinterpretation = suppresscommandinterpretation
|
||||
self.idempotencykey = idempotencykey
|
||||
}
|
||||
|
||||
@@ -7028,6 +7139,7 @@ public struct ChatSendParams: Codable, Sendable {
|
||||
case timeoutms = "timeoutMs"
|
||||
case systeminputprovenance = "systemInputProvenance"
|
||||
case systemprovenancereceipt = "systemProvenanceReceipt"
|
||||
case suppresscommandinterpretation = "suppressCommandInterpretation"
|
||||
case idempotencykey = "idempotencyKey"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
/**
|
||||
* Knip configuration for OpenClaw root and bundled plugin dependency hygiene.
|
||||
*/
|
||||
const BUNDLED_PLUGIN_ROOT_DIR = "extensions";
|
||||
|
||||
function bundledPluginFile(pluginId: string, relativePath: string, suffix = ""): string {
|
||||
|
||||
@@ -916,7 +916,7 @@ OpenClaw sends Teams polls as Adaptive Cards (there is no native Teams poll API)
|
||||
|
||||
- CLI: `openclaw message poll --channel msteams --target conversation:<id> ...`
|
||||
- Votes are recorded by the gateway in OpenClaw plugin-state SQLite under `state/openclaw.sqlite`.
|
||||
- Existing `msteams-polls.json` files are imported once when the MSTeams plugin starts.
|
||||
- Existing `msteams-polls.json` files are imported by `openclaw doctor --fix`, not by the running plugin.
|
||||
- The gateway must stay online to record votes.
|
||||
- Polls do not auto-post result summaries yet, and there is no supported poll-results CLI yet.
|
||||
|
||||
|
||||
@@ -397,7 +397,7 @@ Docker lane definitions live in `scripts/lib/docker-e2e-scenarios.mjs`, planner
|
||||
| `OPENCLAW_DOCKER_ALL_PARALLELISM` | 10 | Main-pool slot count for normal lanes. |
|
||||
| `OPENCLAW_DOCKER_ALL_TAIL_PARALLELISM` | 10 | Provider-sensitive tail-pool slot count. |
|
||||
| `OPENCLAW_DOCKER_ALL_LIVE_LIMIT` | 9 | Concurrent live lane cap so providers do not throttle. |
|
||||
| `OPENCLAW_DOCKER_ALL_NPM_LIMIT` | 10 | Concurrent npm install lane cap. |
|
||||
| `OPENCLAW_DOCKER_ALL_NPM_LIMIT` | 5 | Concurrent npm install lane cap. |
|
||||
| `OPENCLAW_DOCKER_ALL_SERVICE_LIMIT` | 7 | Concurrent multi-service lane cap. |
|
||||
| `OPENCLAW_DOCKER_ALL_START_STAGGER_MS` | 2000 | Stagger between lane starts to avoid Docker daemon create storms; set `0` for no stagger. |
|
||||
| `OPENCLAW_DOCKER_ALL_LANE_TIMEOUT_MS` | 7200000 | Per-lane fallback timeout (120 minutes); selected live/tail lanes use tighter caps. |
|
||||
|
||||
@@ -25,6 +25,13 @@ Common use cases:
|
||||
Execution is still guarded by **exec approvals** and per-agent allowlists on the
|
||||
node host, so you can keep command access scoped and explicit.
|
||||
|
||||
Gateway-loaded plugins can also register node-host commands. When a registered
|
||||
command includes `agentTool` metadata, `openclaw node run` advertises that
|
||||
plugin or MCP-backed tool to the Gateway while the node is connected. The agent
|
||||
sees it as a normal plugin tool, but execution still goes through `node.invoke`
|
||||
and the node command allowlist, so disconnecting the node removes the tool from
|
||||
new agent runs.
|
||||
|
||||
## Browser proxy (zero-config)
|
||||
|
||||
Node hosts automatically advertise a browser proxy if `browser.enabled` is not
|
||||
|
||||
@@ -20,8 +20,8 @@ title: "Features"
|
||||
<Card title="Media" icon="image" href="/nodes/images">
|
||||
Images, audio, video, documents, and image/video generation.
|
||||
</Card>
|
||||
<Card title="Apps and UI" icon="monitor" href="/web/control-ui">
|
||||
Web Control UI and macOS companion app.
|
||||
<Card title="Apps and UI" icon="monitor" href="/platforms">
|
||||
Windows Hub, Web Control UI, macOS app, and mobile nodes.
|
||||
</Card>
|
||||
<Card title="Mobile nodes" icon="smartphone" href="/nodes">
|
||||
iOS and Android nodes with pairing, voice/chat, and rich device commands.
|
||||
|
||||
@@ -108,10 +108,10 @@ These notices are operational messages, not assistant content. They are delivere
|
||||
|
||||
OpenClaw uses **auth profiles** for both API keys and OAuth tokens.
|
||||
|
||||
- Secrets live in `~/.openclaw/agents/<agentId>/agent/auth-profiles.json` (legacy: `~/.openclaw/agent/auth-profiles.json`).
|
||||
- Runtime auth-routing state lives in `~/.openclaw/agents/<agentId>/agent/auth-state.json`.
|
||||
- Secrets and runtime auth-routing state live in `~/.openclaw/agents/<agentId>/agent/openclaw-agent.sqlite`.
|
||||
- Config `auth.profiles` / `auth.order` are **metadata + routing only** (no secrets).
|
||||
- Legacy import-only OAuth file: `~/.openclaw/credentials/oauth.json` (imported into `auth-profiles.json` on first use).
|
||||
- Legacy import-only OAuth file: `~/.openclaw/credentials/oauth.json` (imported into the per-agent auth store on first use).
|
||||
- Legacy `auth-profiles.json`, `auth-state.json`, and per-agent `auth.json` files are imported by `openclaw doctor --fix`.
|
||||
|
||||
More detail: [OAuth](/concepts/oauth)
|
||||
|
||||
@@ -127,7 +127,7 @@ OAuth logins create distinct profiles so multiple accounts can coexist.
|
||||
- Default: `provider:default` when no email is available.
|
||||
- OAuth with email: `provider:<email>` (for example `google-antigravity:user@gmail.com`).
|
||||
|
||||
Profiles live in `~/.openclaw/agents/<agentId>/agent/auth-profiles.json` under `profiles`.
|
||||
Profiles live in the per-agent `openclaw-agent.sqlite` auth profile store.
|
||||
|
||||
## Rotation order
|
||||
|
||||
@@ -141,7 +141,7 @@ When a provider has multiple profiles, OpenClaw chooses an order like this:
|
||||
`auth.profiles` filtered by provider.
|
||||
</Step>
|
||||
<Step title="Stored profiles">
|
||||
Entries in `auth-profiles.json` for the provider.
|
||||
Per-agent SQLite auth profile entries for the provider.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
@@ -229,7 +229,7 @@ Cooldowns use exponential backoff:
|
||||
- 25 minutes
|
||||
- 1 hour (cap)
|
||||
|
||||
State is stored in `auth-state.json` under `usageStats`:
|
||||
State is stored in the per-agent SQLite auth state under `usageStats`:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -253,7 +253,7 @@ Not every billing-shaped response is `402`, and not every HTTP `402` lands here.
|
||||
Meanwhile temporary `402` usage-window and organization/workspace spend-limit errors are classified as `rate_limit` when the message looks retryable (for example `weekly usage limit exhausted`, `daily limit reached, resets tomorrow`, or `organization spending limit exceeded`). Those stay on the short cooldown/failover path instead of the long billing-disable path.
|
||||
</Note>
|
||||
|
||||
State is stored in `auth-state.json`:
|
||||
State is stored in the per-agent SQLite auth state:
|
||||
|
||||
```json
|
||||
{
|
||||
|
||||
@@ -306,7 +306,7 @@ See [/providers/kilocode](/providers/kilocode) for setup details.
|
||||
| MiniMax | `minimax` / `minimax-portal` | `MINIMAX_API_KEY` / `MINIMAX_OAUTH_TOKEN` | `minimax/MiniMax-M3` |
|
||||
| Mistral | `mistral` | `MISTRAL_API_KEY` | `mistral/mistral-large-latest` |
|
||||
| Moonshot | `moonshot` | `MOONSHOT_API_KEY` | `moonshot/kimi-k2.6` |
|
||||
| NVIDIA | `nvidia` | `NVIDIA_API_KEY` | `nvidia/nvidia/nemotron-3-super-120b-a12b` |
|
||||
| NVIDIA | `nvidia` | `NVIDIA_API_KEY` | `nvidia/nvidia/nemotron-3-ultra-550b-a55b` |
|
||||
| NovitaAI | `novita` | `NOVITA_API_KEY` | `novita/deepseek/deepseek-v3-0324` |
|
||||
| [Ollama Cloud](/providers/ollama-cloud) | `ollama-cloud` | `OLLAMA_API_KEY` | `ollama-cloud/kimi-k2.6` |
|
||||
| OpenRouter | `openrouter` | `OPENROUTER_API_KEY` | `openrouter/auto` |
|
||||
|
||||
@@ -17,10 +17,18 @@ is now:
|
||||
told us this usage is allowed again
|
||||
|
||||
OpenAI Codex OAuth is explicitly supported for use in external tools like
|
||||
OpenClaw. This page explains:
|
||||
OpenClaw.
|
||||
|
||||
OpenClaw stores both OpenAI API-key auth and ChatGPT/Codex OAuth under the
|
||||
canonical provider id `openai`. Older `openai-codex:*` profile ids and
|
||||
`auth.order.openai-codex` entries are legacy state repaired by
|
||||
`openclaw doctor --fix`; use `openai:*` profile ids and `auth.order.openai` for
|
||||
new config.
|
||||
|
||||
For Anthropic in production, API key auth is the safer recommended path.
|
||||
|
||||
This page explains:
|
||||
|
||||
- how the OAuth **token exchange** works (PKCE)
|
||||
- where tokens are **stored** (and why)
|
||||
- how to handle **multiple accounts** (profiles + per-session overrides)
|
||||
@@ -122,6 +130,18 @@ Flow shape:
|
||||
|
||||
OpenAI Codex OAuth is explicitly supported for use outside the Codex CLI, including OpenClaw workflows.
|
||||
|
||||
The login command still uses the canonical OpenAI provider id:
|
||||
|
||||
```bash
|
||||
openclaw models auth login --provider openai
|
||||
```
|
||||
|
||||
Use `--profile-id openai:<name>` for multiple ChatGPT/Codex OAuth accounts in
|
||||
one agent. Do not use `openai-codex:<name>` for new profiles. Doctor migrates
|
||||
that older prefix to a collision-free `openai:*` profile id; run
|
||||
`openclaw models auth list --provider openai` after repair before copying
|
||||
profile ids into `auth.order` or `/model ...@<profileId>`.
|
||||
|
||||
Flow shape (PKCE):
|
||||
|
||||
1. generate PKCE verifier/challenge + random `state`
|
||||
|
||||
@@ -87,13 +87,13 @@ This is a two-step setup:
|
||||
If `claude` is not on `PATH`, either install Claude Code first or set
|
||||
`agents.defaults.cliBackends.claude-cli.command` to the real binary path.
|
||||
|
||||
Manual token entry (any provider; writes `auth-profiles.json` + updates config):
|
||||
Manual token entry (any provider; writes the per-agent SQLite auth store + updates config):
|
||||
|
||||
```bash
|
||||
openclaw models auth paste-token --provider openrouter
|
||||
```
|
||||
|
||||
`auth-profiles.json` stores credentials only. The canonical shape is:
|
||||
The auth profile store keeps credentials only. Legacy `auth-profiles.json` files used this canonical shape:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -108,9 +108,9 @@ openclaw models auth paste-token --provider openrouter
|
||||
}
|
||||
```
|
||||
|
||||
OpenClaw expects the canonical `version` + `profiles` shape at runtime. If an older install still has a flat file such as `{ "openrouter": { "apiKey": "..." } }`, run `openclaw doctor --fix` to rewrite it as an `openrouter:default` API-key profile; doctor keeps a `.legacy-flat.*.bak` copy beside the original. Endpoint details such as `baseUrl`, `api`, model ids, headers, and timeouts belong under `models.providers.<id>` in `openclaw.json` or `models.json`, not in `auth-profiles.json`.
|
||||
OpenClaw now reads auth profiles from each agent's `openclaw-agent.sqlite`. If an older install still has `auth-profiles.json`, `auth-state.json`, or a flat auth profile file such as `{ "openrouter": { "apiKey": "..." } }`, run `openclaw doctor --fix` to import it into SQLite; doctor keeps timestamped backups beside the original JSON files. Endpoint details such as `baseUrl`, `api`, model ids, headers, and timeouts belong under `models.providers.<id>` in `openclaw.json` or `models.json`, not in auth profiles.
|
||||
|
||||
External auth routes such as Bedrock `auth: "aws-sdk"` are also not credentials. If you want a named Bedrock route, put `auth.profiles.<id>.mode: "aws-sdk"` in `openclaw.json`; do not write `type: "aws-sdk"` into `auth-profiles.json`. `openclaw doctor --fix` moves legacy AWS SDK markers from the credential store into config metadata.
|
||||
External auth routes such as Bedrock `auth: "aws-sdk"` are also not credentials. If you want a named Bedrock route, put `auth.profiles.<id>.mode: "aws-sdk"` in `openclaw.json`; do not write `type: "aws-sdk"` into the auth profile store. `openclaw doctor --fix` moves legacy AWS SDK markers from the credential store into config metadata.
|
||||
|
||||
Auth profile refs are also supported for static credentials:
|
||||
|
||||
@@ -193,6 +193,25 @@ key in the provider dashboard when you need provider-side invalidation.
|
||||
|
||||
## Controlling which credential is used
|
||||
|
||||
### OpenAI and legacy `openai-codex` ids
|
||||
|
||||
OpenAI API-key profiles and ChatGPT/Codex OAuth profiles both use the canonical
|
||||
provider id `openai`. New config should use `openai:*` profile ids and
|
||||
`auth.order.openai`.
|
||||
|
||||
If you see `openai-codex` in older config, auth profile ids, or
|
||||
`auth.order.openai-codex`, treat it as legacy migration input. Do not create new
|
||||
`openai-codex` profiles. Run:
|
||||
|
||||
```bash
|
||||
openclaw doctor --fix
|
||||
openclaw models auth list --provider openai
|
||||
```
|
||||
|
||||
Doctor rewrites legacy `openai-codex:*` profile ids and
|
||||
`auth.order.openai-codex` entries to the canonical `openai` auth route. For
|
||||
OpenAI-specific model/runtime routing, see [OpenAI](/providers/openai).
|
||||
|
||||
### During login (CLI)
|
||||
|
||||
Use `openclaw models auth login --provider <id> --profile-id <profileId>` for
|
||||
@@ -225,7 +244,7 @@ Use `/model` (or `/model list`) for a compact picker; use `/model status` for th
|
||||
|
||||
### Per-agent (CLI override)
|
||||
|
||||
Set an explicit auth profile order override for an agent (stored in that agent's `auth-state.json`):
|
||||
Set an explicit auth profile order override for an agent (stored in that agent's SQLite auth state):
|
||||
|
||||
```bash
|
||||
openclaw models auth order get --provider anthropic
|
||||
|
||||
@@ -270,6 +270,13 @@ Nodes declare capability claims at connect time:
|
||||
- `permissions`: granular toggles (e.g. `screen.record`, `camera.capture`).
|
||||
|
||||
The Gateway treats these as **claims** and enforces server-side allowlists.
|
||||
Connected nodes can publish optional agent-visible plugin or MCP tool
|
||||
descriptors with `node.pluginTools.update` after a successful connect, after
|
||||
reconnect, or after a local plugin/MCP inventory change. Each descriptor must
|
||||
use a provider-safe tool `name` and name a `command` in the node's current
|
||||
command allowlist. The Gateway filters descriptors outside the approved command
|
||||
surface, removes them when the node disconnects, and rejects operator attempts
|
||||
to mutate another node's catalog.
|
||||
|
||||
## Presence
|
||||
|
||||
@@ -461,6 +468,7 @@ enumeration of `src/gateway/server-methods/*.ts`.
|
||||
- `node.invoke` forwards a command to a connected node.
|
||||
- `node.invoke.result` returns the result for an invoke request.
|
||||
- `node.event` carries node-originated events back into the gateway.
|
||||
- `node.pluginTools.update` replaces the connected node's agent-visible plugin/MCP tool descriptors.
|
||||
- `node.pending.pull` and `node.pending.ack` are the connected-node queue APIs.
|
||||
- `node.pending.enqueue` and `node.pending.drain` manage durable pending work for offline/disconnected nodes.
|
||||
|
||||
|
||||
@@ -512,9 +512,10 @@ The agent-facing `gateway` runtime tool still refuses to rewrite
|
||||
`tools.exec.ask` or `tools.exec.security`; legacy `tools.bash.*` aliases are
|
||||
normalized to the same protected exec paths before the write.
|
||||
Agent-driven `gateway config.apply` and `gateway config.patch` edits are
|
||||
fail-closed by default: only a narrow set of prompt, model, and mention-gating
|
||||
paths are agent-tunable. New sensitive config trees are therefore protected
|
||||
unless they are deliberately added to the allowlist.
|
||||
fail-closed by default: only a narrow set of low-risk runtime tuning,
|
||||
mention-gating, and visible-reply paths are agent-tunable. Global model defaults
|
||||
and prompt overlays stay operator-controlled. New sensitive config trees are
|
||||
therefore protected unless they are deliberately added to the allowlist.
|
||||
|
||||
For any agent/surface that handles untrusted content, deny these by default:
|
||||
|
||||
|
||||
@@ -856,7 +856,8 @@ and troubleshooting see the main [FAQ](/help/faq).
|
||||
- **Recommended:** 2GB RAM or more if you run multiple channels, browser automation, or media tools.
|
||||
- **OS:** Ubuntu LTS or another modern Debian/Ubuntu.
|
||||
|
||||
If you are on Windows, **WSL2 is the easiest VM style setup** and has the best tooling
|
||||
If you are on Windows, use **Windows Hub** for desktop setup, or WSL2 when
|
||||
you specifically want a Linux-style Gateway VM with broad tooling
|
||||
compatibility. See [Windows](/platforms/windows), [VPS hosting](/vps).
|
||||
If you are running macOS in a VM, see [macOS VM](/install/macos-vm).
|
||||
|
||||
|
||||
@@ -1652,9 +1652,14 @@ lives on the [Models FAQ](/help/faq-models).
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="I closed my terminal on Windows - how do I restart OpenClaw?">
|
||||
There are **two Windows install modes**:
|
||||
There are **three Windows install modes**:
|
||||
|
||||
**1) WSL2 (recommended):** the Gateway runs inside Linux.
|
||||
**1) Windows Hub local setup:** the native app manages a local app-owned WSL Gateway.
|
||||
|
||||
Open **OpenClaw Companion** from the Start menu or tray, then use
|
||||
**Gateway Setup** or the Connections tab.
|
||||
|
||||
**2) Manual WSL2 Gateway:** the Gateway runs inside Linux.
|
||||
|
||||
Open PowerShell, enter WSL, then restart:
|
||||
|
||||
@@ -1670,7 +1675,7 @@ lives on the [Models FAQ](/help/faq-models).
|
||||
openclaw gateway run
|
||||
```
|
||||
|
||||
**2) Native Windows (not recommended):** the Gateway runs directly in Windows.
|
||||
**3) Native Windows CLI/Gateway:** the Gateway runs directly in Windows.
|
||||
|
||||
Open PowerShell and run:
|
||||
|
||||
@@ -1685,7 +1690,7 @@ lives on the [Models FAQ](/help/faq-models).
|
||||
openclaw gateway run
|
||||
```
|
||||
|
||||
Docs: [Windows (WSL2)](/platforms/windows), [Gateway service runbook](/gateway).
|
||||
Docs: [Windows](/platforms/windows), [Gateway service runbook](/gateway).
|
||||
|
||||
</Accordion>
|
||||
|
||||
|
||||
@@ -746,7 +746,7 @@ These Docker runners split into two buckets:
|
||||
`OPENCLAW_LIVE_GATEWAY_STEP_TIMEOUT_MS=45000`, and
|
||||
`OPENCLAW_LIVE_GATEWAY_MODEL_TIMEOUT_MS=90000`. Set `OPENCLAW_LIVE_MAX_MODELS`
|
||||
or the gateway env vars when you explicitly want a smaller cap or larger scan.
|
||||
- `test:docker:all` builds the live Docker image once via `test:docker:live-build`, packs OpenClaw once as an npm tarball through `scripts/package-openclaw-for-docker.mjs`, then builds/reuses two `scripts/e2e/Dockerfile` images. The bare image is only the Node/Git runner for install/update/plugin-dependency lanes; those lanes mount the prebuilt tarball. The functional image installs the same tarball into `/app` for built-app functionality lanes. Docker lane definitions live in `scripts/lib/docker-e2e-scenarios.mjs`; planner logic lives in `scripts/lib/docker-e2e-plan.mjs`; `scripts/test-docker-all.mjs` executes the selected plan. The aggregate uses a weighted local scheduler: `OPENCLAW_DOCKER_ALL_PARALLELISM` controls process slots, while resource caps keep heavy live, npm-install, and multi-service lanes from all starting at once. If a single lane is heavier than the active caps, the scheduler can still start it when the pool is empty and then keeps it running alone until capacity is available again. Defaults are 10 slots, `OPENCLAW_DOCKER_ALL_LIVE_LIMIT=9`, `OPENCLAW_DOCKER_ALL_NPM_LIMIT=10`, and `OPENCLAW_DOCKER_ALL_SERVICE_LIMIT=7`; tune `OPENCLAW_DOCKER_ALL_WEIGHT_LIMIT` or `OPENCLAW_DOCKER_ALL_DOCKER_LIMIT` only when the Docker host has more headroom. The runner performs a Docker preflight by default, removes stale OpenClaw E2E containers, prints status every 30 seconds, stores successful lane timings in `.artifacts/docker-tests/lane-timings.json`, and uses those timings to start longer lanes first on later runs. Use `OPENCLAW_DOCKER_ALL_DRY_RUN=1` to print the weighted lane manifest without building or running Docker, or `node scripts/test-docker-all.mjs --plan-json` to print the CI plan for selected lanes, package/image needs, and credentials.
|
||||
- `test:docker:all` builds the live Docker image once via `test:docker:live-build`, packs OpenClaw once as an npm tarball through `scripts/package-openclaw-for-docker.mjs`, then builds/reuses two `scripts/e2e/Dockerfile` images. The bare image is only the Node/Git runner for install/update/plugin-dependency lanes; those lanes mount the prebuilt tarball. The functional image installs the same tarball into `/app` for built-app functionality lanes. Docker lane definitions live in `scripts/lib/docker-e2e-scenarios.mjs`; planner logic lives in `scripts/lib/docker-e2e-plan.mjs`; `scripts/test-docker-all.mjs` executes the selected plan. The aggregate uses a weighted local scheduler: `OPENCLAW_DOCKER_ALL_PARALLELISM` controls process slots, while resource caps keep heavy live, npm-install, and multi-service lanes from all starting at once. If a single lane is heavier than the active caps, the scheduler can still start it when the pool is empty and then keeps it running alone until capacity is available again. Defaults are 10 slots, `OPENCLAW_DOCKER_ALL_LIVE_LIMIT=9`, `OPENCLAW_DOCKER_ALL_NPM_LIMIT=5`, and `OPENCLAW_DOCKER_ALL_SERVICE_LIMIT=7`; tune `OPENCLAW_DOCKER_ALL_WEIGHT_LIMIT` or `OPENCLAW_DOCKER_ALL_DOCKER_LIMIT` only when the Docker host has more headroom. The runner performs a Docker preflight by default, removes stale OpenClaw E2E containers, prints status every 30 seconds, stores successful lane timings in `.artifacts/docker-tests/lane-timings.json`, and uses those timings to start longer lanes first on later runs. Use `OPENCLAW_DOCKER_ALL_DRY_RUN=1` to print the weighted lane manifest without building or running Docker, or `node scripts/test-docker-all.mjs --plan-json` to print the CI plan for selected lanes, package/image needs, and credentials.
|
||||
- `Package Acceptance` is the GitHub-native package gate for "does this installable tarball work as a product?" It resolves one candidate package from `source=npm`, `source=ref`, `source=url`, or `source=artifact`, uploads it as `package-under-test`, then runs the reusable Docker E2E lanes against that exact tarball instead of repacking the selected ref. Profiles are ordered by breadth: `smoke`, `package`, `product`, and `full`. See [Testing updates and plugins](/help/testing-updates-plugins) for the package/update/plugin contract, published-upgrade survivor matrix, release defaults, and failure triage.
|
||||
- Build and release checks run `scripts/check-cli-bootstrap-imports.mjs` after tsdown. The guard walks the static built graph from `dist/entry.js` and `dist/cli/run-main.js` and fails if pre-dispatch startup imports package dependencies such as Commander, prompt UI, undici, or logging before command dispatch; it also keeps the bundled gateway run chunk under budget and rejects static imports of known cold gateway paths. Packaged CLI smoke also covers root help, onboard help, doctor help, status, config schema, and a model-list command.
|
||||
- Package Acceptance legacy compatibility is capped at `2026.4.25` (`2026.4.25-beta.*` included). Through that cutoff, the harness tolerates only shipped-package metadata gaps: omitted private QA inventory entries, missing `gateway install --wrapper`, missing patch files in the tarball-derived git fixture, missing persisted `update.channel`, legacy plugin install-record locations, missing marketplace install-record persistence, and config metadata migration during `plugins update`. For packages after `2026.4.25`, those paths are strict failures.
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
/**
|
||||
* Docs UI enhancement that mirrors the active nav tab underline with a stable
|
||||
* animated underline element.
|
||||
*/
|
||||
(() => {
|
||||
const NAV_TABS_SELECTOR = ".nav-tabs";
|
||||
const ACTIVE_UNDERLINE_SELECTOR = ".nav-tabs-item > div.bg-primary";
|
||||
|
||||
@@ -249,7 +249,9 @@ openclaw nodes canvas a2ui reset --node <idOrNameOrIp>
|
||||
|
||||
Notes:
|
||||
|
||||
- Mobile nodes use a bundled app-owned A2UI page for action-capable rendering.
|
||||
- Only A2UI v0.8 JSONL is supported (v0.9/createSurface is rejected).
|
||||
- iOS and Android render remote Gateway Canvas pages, but A2UI button actions are dispatched only from the bundled app-owned A2UI page. Gateway-hosted HTTP/HTTPS A2UI pages are render-only on those mobile clients.
|
||||
|
||||
## Photos + videos (node camera)
|
||||
|
||||
|
||||
@@ -198,12 +198,12 @@ openclaw nodes invoke --node "<Android Node>" --command canvas.navigate --params
|
||||
Tailnet (optional): if both devices are on Tailscale, use a MagicDNS name or tailnet IP instead of `.local`, e.g. `http://<gateway-magicdns>:18789/__openclaw__/canvas/`.
|
||||
|
||||
This server injects a live-reload client into HTML and reloads on file changes.
|
||||
The A2UI host lives at `http://<gateway-host>:18789/__openclaw__/a2ui/`.
|
||||
The Gateway also serves `/__openclaw__/a2ui/`, but the Android app treats remote A2UI pages as render-only. Action-capable A2UI commands use the bundled app-owned A2UI page before applying messages.
|
||||
|
||||
Canvas commands (foreground only):
|
||||
|
||||
- `canvas.eval`, `canvas.snapshot`, `canvas.navigate` (use `{"url":""}` or `{"url":"/"}` to return to the default scaffold). `canvas.snapshot` returns `{ format, base64 }` (default `format="jpeg"`).
|
||||
- A2UI: `canvas.a2ui.push`, `canvas.a2ui.reset` (`canvas.a2ui.pushJSONL` legacy alias)
|
||||
- A2UI: `canvas.a2ui.push`, `canvas.a2ui.reset` (`canvas.a2ui.pushJSONL` legacy alias). These commands use the bundled app-owned A2UI page for action-capable rendering.
|
||||
|
||||
Camera commands (foreground only; permission-gated):
|
||||
|
||||
|
||||
@@ -10,9 +10,11 @@ OpenClaw core is written in TypeScript. **Node is the recommended runtime**.
|
||||
Bun is not recommended for the Gateway — known issues with WhatsApp and
|
||||
Telegram channels; see [Bun (experimental)](/install/bun) for details.
|
||||
|
||||
Companion apps exist for macOS (menu bar app) and mobile nodes (iOS/Android). Windows and
|
||||
Linux companion apps are planned, but the Gateway is fully supported today.
|
||||
Native companion apps for Windows are also planned; the Gateway is recommended via WSL2.
|
||||
Companion apps exist for Windows Hub, macOS (menu bar app), and mobile nodes
|
||||
(iOS/Android). Linux companion apps are planned, but the Gateway is fully
|
||||
supported today. On Windows, choose Windows Hub for the desktop app, native
|
||||
PowerShell install for terminal-first use, or WSL2 for the most
|
||||
Linux-compatible Gateway runtime.
|
||||
|
||||
## Choose your OS
|
||||
|
||||
@@ -35,6 +37,7 @@ Native companion apps for Windows are also planned; the Gateway is recommended v
|
||||
## Common links
|
||||
|
||||
- Install guide: [Getting Started](/start/getting-started)
|
||||
- Windows Hub: [Windows](/platforms/windows)
|
||||
- Gateway runbook: [Gateway](/gateway)
|
||||
- Gateway configuration: [Configuration](/gateway/configuration)
|
||||
- Service status: `openclaw gateway status`
|
||||
@@ -57,5 +60,6 @@ The service target depends on OS:
|
||||
## Related
|
||||
|
||||
- [Install overview](/install)
|
||||
- [Windows Hub](/platforms/windows)
|
||||
- [macOS app](/platforms/macos)
|
||||
- [iOS app](/platforms/ios)
|
||||
|
||||
@@ -238,7 +238,8 @@ Notes:
|
||||
|
||||
- The Gateway canvas host serves `/__openclaw__/canvas/` and `/__openclaw__/a2ui/`.
|
||||
- It is served from the Gateway HTTP server (same port as `gateway.port`, default `18789`).
|
||||
- The iOS node auto-navigates to A2UI on connect when a canvas host URL is advertised.
|
||||
- The iOS node keeps the built-in scaffold as the connected default view. `canvas.a2ui.push` and `canvas.a2ui.reset` use the bundled app-owned A2UI page.
|
||||
- Remote Gateway A2UI pages are render-only on iOS; native A2UI button actions are accepted only from bundled app-owned pages.
|
||||
- Return to the built-in scaffold with `canvas.navigate` and `{"url":""}`.
|
||||
|
||||
## Computer Use relationship
|
||||
@@ -275,7 +276,7 @@ openclaw nodes invoke --node "iOS Node" --command canvas.snapshot --params '{"ma
|
||||
## Common errors
|
||||
|
||||
- `NODE_BACKGROUND_UNAVAILABLE`: bring the iOS app to the foreground (canvas/camera/screen commands require it).
|
||||
- `A2UI_HOST_NOT_CONFIGURED`: the Gateway did not advertise the Canvas plugin surface URL; check `plugins.entries.canvas.config.host` in [Gateway configuration](/gateway/configuration).
|
||||
- `A2UI_HOST_UNAVAILABLE`: the bundled A2UI page was not reachable in the app WebView; keep the app foregrounded on the Screen tab and retry.
|
||||
- Pairing prompt never appears: run `openclaw devices list` and approve manually.
|
||||
- Reconnect fails after reinstall: the Keychain pairing token was cleared; re-pair the node.
|
||||
|
||||
|
||||
@@ -120,10 +120,11 @@ Use [`defineToolPlugin`](/plugins/tool-plugins) for simple tool-only plugins
|
||||
with fixed tool names. Use `api.registerTool(...)` directly for mixed plugins
|
||||
or fully dynamic tool registration.
|
||||
|
||||
| Method | What it registers |
|
||||
| ------------------------------- | --------------------------------------------- |
|
||||
| `api.registerTool(tool, opts?)` | Agent tool (required or `{ optional: true }`) |
|
||||
| `api.registerCommand(def)` | Custom command (bypasses the LLM) |
|
||||
| Method | What it registers |
|
||||
| -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `api.registerTool(tool, opts?)` | Agent tool (required or `{ optional: true }`) |
|
||||
| `api.registerCommand(def)` | Custom command (bypasses the LLM) |
|
||||
| `api.registerNodeHostCommand(command)` | Command handled by `openclaw node run`; optional `agentTool` metadata can expose it as an agent-visible tool while the node is connected |
|
||||
|
||||
Plugin commands can set `agentPromptGuidance` when the agent needs a short,
|
||||
command-owned routing hint. Keep that text about the command itself; do not add
|
||||
@@ -150,6 +151,19 @@ surfaces: only guidance explicitly scoped to `codex_app_server` is promoted into
|
||||
that higher-priority lane. Legacy string guidance and unscoped structured
|
||||
guidance remain available to non-Codex prompt surfaces for compatibility.
|
||||
|
||||
Node-host commands run on the connected node host, not inside the Gateway
|
||||
process. If `agentTool` is present, the node publishes a descriptor after a
|
||||
successful Gateway connect; the Gateway exposes it to agent runs only while that
|
||||
node is connected and only if the descriptor's `command` is in the node's
|
||||
approved command surface. Set `agentTool.defaultPlatforms` to opt a
|
||||
non-dangerous command into the default node command allowlist; otherwise require
|
||||
explicit `gateway.nodes.allowCommands` or a node-invoke policy. `agentTool.name`
|
||||
must be provider-safe: start with a letter, use only letters, digits,
|
||||
underscores, or hyphens, and stay within 64 characters. MCP-backed node tools
|
||||
can set `agentTool.mcp` metadata so catalog and tool-search surfaces can show
|
||||
the remote MCP server/tool identity, but execution still goes through the
|
||||
advertised node command.
|
||||
|
||||
### Infrastructure
|
||||
|
||||
| Method | What it registers |
|
||||
|
||||
@@ -251,9 +251,15 @@ two-party event loops that do not go through the shared inbound reply runner.
|
||||
});
|
||||
```
|
||||
|
||||
`nodes.list(...)` includes each connected node's advertised
|
||||
`nodePluginTools` descriptors when that node exposes plugin or MCP-backed
|
||||
tools to the agent. Those descriptors are live connection state: the Gateway
|
||||
drops them when the node disconnects, and a node can replace them with
|
||||
`node.pluginTools.update` after local plugin/MCP inventory changes.
|
||||
|
||||
Inside the Gateway this runtime is in-process. In plugin CLI commands it calls the configured Gateway over RPC, so commands such as `openclaw googlemeet recover-tab` can inspect paired nodes from the terminal. Node commands still go through normal Gateway node pairing, command allowlists, plugin node-invoke policies, and node-local command handling.
|
||||
|
||||
Plugins that expose dangerous node-host commands should register a node-invoke policy with `api.registerNodeInvokePolicy(...)`. The policy runs in the Gateway after command allowlist checks and before the command is forwarded to the node, so direct `node.invoke` calls and higher-level plugin tools share the same enforcement path.
|
||||
Plugins that expose node-hosted agent tools can set `agentTool.defaultPlatforms` for non-dangerous commands that should be allowlisted by default. Omit it when operators must opt in with `gateway.nodes.allowCommands`. Dangerous node-host commands should register a node-invoke policy with `api.registerNodeInvokePolicy(...)`; the policy runs in the Gateway after command allowlist checks and before the command is forwarded to the node, so direct `node.invoke` calls, node-hosted plugin tools, and higher-level plugin tools share the same enforcement path.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="api.runtime.tasks.managedFlows">
|
||||
|
||||
@@ -3,12 +3,15 @@ summary: "Use NVIDIA's OpenAI-compatible API in OpenClaw"
|
||||
read_when:
|
||||
- You want to use open models in OpenClaw for free
|
||||
- You need NVIDIA_API_KEY setup
|
||||
- You want to use Nemotron 3 Ultra through NVIDIA
|
||||
title: "NVIDIA"
|
||||
---
|
||||
|
||||
NVIDIA provides an OpenAI-compatible API at `https://integrate.api.nvidia.com/v1` for
|
||||
open models for free. Authenticate with an API key from
|
||||
[build.nvidia.com](https://build.nvidia.com/settings/api-keys).
|
||||
[build.nvidia.com](https://build.nvidia.com/settings/api-keys). OpenClaw
|
||||
defaults the NVIDIA provider to Nemotron 3 Ultra, NVIDIA's 550B total / 55B
|
||||
active reasoning model for long-context agentic work.
|
||||
|
||||
## Getting started
|
||||
|
||||
@@ -24,7 +27,7 @@ open models for free. Authenticate with an API key from
|
||||
</Step>
|
||||
<Step title="Set an NVIDIA model">
|
||||
```bash
|
||||
openclaw models set nvidia/nvidia/nemotron-3-super-120b-a12b
|
||||
openclaw models set nvidia/nvidia/nemotron-3-ultra-550b-a55b
|
||||
```
|
||||
</Step>
|
||||
</Steps>
|
||||
@@ -56,7 +59,7 @@ openclaw onboard --auth-choice nvidia-api-key --nvidia-api-key "nvapi-..."
|
||||
},
|
||||
agents: {
|
||||
defaults: {
|
||||
model: { primary: "nvidia/nvidia/nemotron-3-super-120b-a12b" },
|
||||
model: { primary: "nvidia/nvidia/nemotron-3-ultra-550b-a55b" },
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -69,22 +72,39 @@ try NVIDIA's public featured-model catalog from
|
||||
`https://assets.ngc.nvidia.com/products/api-catalog/featured-models.json` and
|
||||
caches the ranked result for 24 hours. New featured models from build.nvidia.com
|
||||
therefore appear in setup and model-selection surfaces without waiting for an
|
||||
OpenClaw release.
|
||||
OpenClaw release. When the live feed is available, the first returned model is
|
||||
the default option shown during NVIDIA setup.
|
||||
|
||||
The fetch uses a fixed HTTPS host policy for `assets.ngc.nvidia.com`. If no
|
||||
NVIDIA API key is configured, or if that public catalog is unavailable or
|
||||
malformed, OpenClaw falls back to the bundled catalog below.
|
||||
malformed, OpenClaw falls back to the bundled catalog and bundled default below.
|
||||
|
||||
## Nemotron 3 Ultra
|
||||
|
||||
Nemotron 3 Ultra is the default NVIDIA model in OpenClaw. NVIDIA's build page for
|
||||
[`nvidia/nemotron-3-ultra-550b-a55b`](https://build.nvidia.com/nvidia/nemotron-3-ultra-550b-a55b)
|
||||
lists it as an available free endpoint with a 1M-token context specification.
|
||||
The bundled catalog records a 16,384-token max output to match NVIDIA's current
|
||||
OpenAI-compatible sample request for the hosted endpoint.
|
||||
|
||||
Use Ultra for the highest-capability NVIDIA default. Keep Super selected when
|
||||
you want the smaller Nemotron 3 option, or choose one of the third-party models
|
||||
hosted in NVIDIA's catalog when their context, latency, or behavior fits better.
|
||||
The bundled Ultra row sends `chat_template_kwargs.enable_thinking: false` and
|
||||
`force_nonempty_content: true` by default so normal chat output stays in the
|
||||
visible answer instead of exposing reasoning text.
|
||||
|
||||
## Bundled fallback catalog
|
||||
|
||||
| Model ref | Name | Context | Max output | Notes |
|
||||
| ------------------------------------------ | ---------------------------- | ------- | ---------- | --------------------------------- |
|
||||
| `nvidia/nvidia/nemotron-3-super-120b-a12b` | NVIDIA Nemotron 3 Super 120B | 262,144 | 8,192 | Featured fallback |
|
||||
| `nvidia/moonshotai/kimi-k2.5` | Kimi K2.5 | 262,144 | 8,192 | Featured fallback |
|
||||
| `nvidia/minimaxai/minimax-m2.7` | Minimax M2.7 | 196,608 | 8,192 | Featured fallback |
|
||||
| `nvidia/z-ai/glm-5.1` | GLM 5.1 | 202,752 | 8,192 | Featured fallback |
|
||||
| `nvidia/minimaxai/minimax-m2.5` | MiniMax M2.5 | 196,608 | 8,192 | Deprecated, upgrade compatibility |
|
||||
| `nvidia/z-ai/glm5` | GLM-5 | 202,752 | 8,192 | Deprecated, upgrade compatibility |
|
||||
| Model ref | Name | Context | Max output | Notes |
|
||||
| ------------------------------------------ | ---------------------------- | --------- | ---------- | --------------------------------- |
|
||||
| `nvidia/nvidia/nemotron-3-ultra-550b-a55b` | NVIDIA Nemotron 3 Ultra 550B | 1,000,000 | 16,384 | Default |
|
||||
| `nvidia/nvidia/nemotron-3-super-120b-a12b` | NVIDIA Nemotron 3 Super 120B | 262,144 | 8,192 | Featured fallback |
|
||||
| `nvidia/moonshotai/kimi-k2.5` | Kimi K2.5 | 262,144 | 8,192 | Featured fallback |
|
||||
| `nvidia/minimaxai/minimax-m2.7` | Minimax M2.7 | 196,608 | 8,192 | Featured fallback |
|
||||
| `nvidia/z-ai/glm-5.1` | GLM 5.1 | 202,752 | 8,192 | Featured fallback |
|
||||
| `nvidia/minimaxai/minimax-m2.5` | MiniMax M2.5 | 196,608 | 8,192 | Deprecated, upgrade compatibility |
|
||||
| `nvidia/z-ai/glm5` | GLM-5 | 202,752 | 8,192 | Deprecated, upgrade compatibility |
|
||||
|
||||
## Advanced configuration
|
||||
|
||||
@@ -97,9 +117,9 @@ malformed, OpenClaw falls back to the bundled catalog below.
|
||||
<Accordion title="Catalog and pricing">
|
||||
OpenClaw prefers NVIDIA's public featured-model catalog when NVIDIA auth is
|
||||
configured and caches it for 24 hours. The bundled fallback catalog is static
|
||||
and keeps deprecated shipped refs for upgrade compatibility. Costs default to
|
||||
`0` in source since NVIDIA currently offers free API access for the listed
|
||||
models.
|
||||
and keeps deprecated shipped refs for upgrade compatibility. Costs default
|
||||
to `0` in source since NVIDIA currently offers free API access for the
|
||||
listed models.
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="OpenAI-compatible endpoint">
|
||||
@@ -107,6 +127,36 @@ malformed, OpenClaw falls back to the bundled catalog below.
|
||||
tooling should work out of the box with the NVIDIA base URL.
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Nemotron 3 Ultra reasoning params">
|
||||
NVIDIA's Ultra sample request uses `chat_template_kwargs.enable_thinking`
|
||||
and `reasoning_budget` for reasoning output. OpenClaw's bundled Ultra row
|
||||
disables template thinking by default for normal chat use. If you need to
|
||||
opt into NVIDIA reasoning output or force other NVIDIA-specific request
|
||||
fields, set per-model params and keep provider-specific overrides scoped to
|
||||
the NVIDIA model:
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
models: {
|
||||
"nvidia/nvidia/nemotron-3-ultra-550b-a55b": {
|
||||
params: {
|
||||
chat_template_kwargs: { enable_thinking: true },
|
||||
extra_body: { reasoning_budget: 16384 },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
`params.extra_body` is the final OpenAI-compatible request-body override, so
|
||||
use it only for fields NVIDIA documents for the selected endpoint.
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Slow custom provider responses">
|
||||
Some NVIDIA-hosted custom models can take longer than the default model idle
|
||||
watchdog before they emit a first response chunk. For custom NVIDIA provider
|
||||
|
||||
@@ -1002,10 +1002,10 @@ sessionId})`; create, branch, continue, list, and fork flows live in their
|
||||
- The generic plugin SDK persistent-dedupe helper no longer exposes file-shaped
|
||||
options. Callers provide SQLite scope keys and durable dedupe rows live in
|
||||
shared plugin state.
|
||||
- Microsoft Teams SSO and delegated OAuth tokens moved from locked JSON files
|
||||
to SQLite plugin state. Doctor imports `msteams-sso-tokens.json` and
|
||||
`msteams-delegated.json`, rebuilds canonical SSO token keys from payloads,
|
||||
and removes the source files.
|
||||
- Microsoft Teams SSO tokens moved from locked JSON files to SQLite plugin
|
||||
state. Doctor imports `msteams-sso-tokens.json`, rebuilds canonical SSO token
|
||||
keys from payloads, and removes the source file. Delegated OAuth tokens stay
|
||||
on their existing private credential-file boundary.
|
||||
- Matrix sync cache state moved from `bot-storage.json` to SQLite plugin
|
||||
state. Doctor imports legacy raw or wrapped sync payloads and removes the
|
||||
source file. Active Matrix and QA Matrix clients pass a SQLite sync-store root
|
||||
@@ -1613,13 +1613,13 @@ Move these into the global database:
|
||||
`reply-cache`, `sent-echoes`) instead of `imessage/catchup/*.json`,
|
||||
`imessage/reply-cache.jsonl`, and `imessage/sent-echoes.jsonl`; the iMessage
|
||||
doctor/setup migration imports and removes the legacy files.
|
||||
- Microsoft Teams conversations, polls, delegated tokens, pending uploads, and
|
||||
feedback learnings now use SQLite plugin state/blob namespaces
|
||||
(`conversations`, `polls`, `delegated-tokens`, `pending-uploads`,
|
||||
- Microsoft Teams conversations, polls, SSO tokens, and feedback learnings now
|
||||
use SQLite plugin state namespaces (`conversations`, `polls`, `sso-tokens`,
|
||||
`feedback-learnings`) instead of `msteams-conversations.json`,
|
||||
`msteams-polls.json`, `msteams-delegated.json`,
|
||||
`msteams-pending-uploads.json`, and `*.learnings.json`; the Microsoft Teams
|
||||
doctor/setup migration imports and removes the legacy files.
|
||||
`msteams-polls.json`, `msteams-sso-tokens.json`, and `*.learnings.json`; the
|
||||
Microsoft Teams doctor/setup migration imports and archives the legacy files.
|
||||
Pending uploads are a short-lived SQLite cache and old JSON cache files are
|
||||
not migrated.
|
||||
- Matrix sync cache, storage metadata, thread bindings, inbound dedupe markers,
|
||||
startup verification cooldown state, credentials, recovery keys, and SDK
|
||||
IndexedDB crypto snapshots now use SQLite plugin state/blob namespaces under
|
||||
@@ -2191,8 +2191,6 @@ Add a repo check that fails new runtime writes to legacy state paths:
|
||||
- Microsoft Teams `msteams-conversations.json`
|
||||
- Microsoft Teams `msteams-polls.json`
|
||||
- Microsoft Teams `msteams-sso-tokens.json`
|
||||
- Microsoft Teams `msteams-delegated.json`
|
||||
- Microsoft Teams `msteams-pending-uploads.json`
|
||||
- Microsoft Teams `*.learnings.json`
|
||||
- Matrix `bot-storage.json`
|
||||
- Matrix `sync-store.json`
|
||||
|
||||
@@ -24,6 +24,7 @@ title: "Tests"
|
||||
- `pnpm test`: routes explicit file/directory targets through scoped Vitest lanes. Untargeted runs are full-suite proof: they use fixed shard groups, expand to leaf configs for local parallel execution, and print the expected local shard fanout before starting. The extension group always expands to the per-extension shard configs instead of one giant root-project process.
|
||||
- Test wrapper runs end with a short `[test] passed|failed|skipped ... in ...` summary. Vitest's own duration line stays the per-shard detail.
|
||||
- Shared OpenClaw test state: use `src/test-utils/openclaw-test-state.ts` from Vitest when a test needs an isolated `HOME`, `OPENCLAW_STATE_DIR`, `OPENCLAW_CONFIG_PATH`, config fixture, workspace, agent dir, or auth-profile store.
|
||||
- `pnpm test:env-mutations:report`: non-blocking report of tests and harnesses that mutate `HOME`, `OPENCLAW_STATE_DIR`, `OPENCLAW_CONFIG_PATH`, `OPENCLAW_WORKSPACE_DIR`, or related OpenClaw env keys directly. Use it to find candidates for migration to the shared test-state helper.
|
||||
- Control UI mocked E2E: use `pnpm test:ui:e2e` for the Vitest + Playwright lane that starts the Vite Control UI and drives a real Chromium page against a mocked Gateway WebSocket. Tests live in `ui/src/**/*.e2e.test.ts`; shared mocks and controls live in `ui/src/test-helpers/control-ui-e2e.ts`. `pnpm test:e2e` includes this lane. In Codex worktrees, prefer `node scripts/run-vitest.mjs run --config test/vitest/vitest.ui-e2e.config.ts --configLoader runner ui/src/ui/e2e/chat-flow.e2e.test.ts` for tiny targeted proof after dependencies are installed, or Testbox/Crabbox for broader GUI proof.
|
||||
- Process E2E helpers: use `test/helpers/openclaw-test-instance.ts` when a Vitest process-level E2E test needs a running Gateway, CLI env, log capture, and cleanup in one place.
|
||||
- TUI PTY tests: use `node scripts/run-vitest.mjs run --config test/vitest/vitest.tui-pty.config.ts` for the fast fake-backend PTY lane. Use `OPENCLAW_TUI_PTY_INCLUDE_LOCAL=1` or `pnpm tui:pty:test:watch --mode local` for the slower `tui --local` smoke, which mocks only the external model endpoint. Assert stable visible text or fixture calls, not raw ANSI snapshots.
|
||||
@@ -48,7 +49,7 @@ title: "Tests"
|
||||
- `pnpm test:e2e`: Runs the repo E2E aggregate: gateway end-to-end smoke tests plus the Control UI mocked browser E2E lane.
|
||||
- `pnpm test:e2e:gateway`: Runs gateway end-to-end smoke tests (multi-instance WS/HTTP/node pairing). Defaults to `threads` + `isolate: false` with adaptive workers in `vitest.e2e.config.ts`; tune with `OPENCLAW_E2E_WORKERS=<n>` and set `OPENCLAW_E2E_VERBOSE=1` for verbose logs.
|
||||
- `pnpm test:live`: Runs provider live tests (minimax/zai). Requires API keys and `LIVE=1` (or provider-specific `*_LIVE_TEST=1`) to unskip.
|
||||
- `pnpm test:docker:all`: Builds the shared live-test image, packs OpenClaw once as an npm tarball, builds/reuses a bare Node/Git runner image plus a functional image that installs that tarball into `/app`, then runs Docker smoke lanes with `OPENCLAW_SKIP_DOCKER_BUILD=1` through a weighted scheduler. The bare image (`OPENCLAW_DOCKER_E2E_BARE_IMAGE`) is used for installer/update/plugin-dependency lanes; those lanes mount the prebuilt tarball instead of using copied repo sources. The functional image (`OPENCLAW_DOCKER_E2E_FUNCTIONAL_IMAGE`) is used for normal built-app functionality lanes. `scripts/package-openclaw-for-docker.mjs` is the single local/CI package packer and validates the tarball plus `dist/postinstall-inventory.json` before Docker consumes it. Docker lane definitions live in `scripts/lib/docker-e2e-scenarios.mjs`; planner logic lives in `scripts/lib/docker-e2e-plan.mjs`; `scripts/test-docker-all.mjs` executes the selected plan. `node scripts/test-docker-all.mjs --plan-json` emits the scheduler-owned CI plan for selected lanes, image kinds, package/live-image needs, state scenarios, and credential checks without building or running Docker. `OPENCLAW_DOCKER_ALL_PARALLELISM=<n>` controls process slots and defaults to 10; `OPENCLAW_DOCKER_ALL_TAIL_PARALLELISM=<n>` controls the provider-sensitive tail pool and defaults to 10. Heavy lane caps default to `OPENCLAW_DOCKER_ALL_LIVE_LIMIT=9`, `OPENCLAW_DOCKER_ALL_NPM_LIMIT=10`, and `OPENCLAW_DOCKER_ALL_SERVICE_LIMIT=7`; provider caps default to one heavy lane per provider via `OPENCLAW_DOCKER_ALL_LIVE_CLAUDE_LIMIT=4`, `OPENCLAW_DOCKER_ALL_LIVE_CODEX_LIMIT=4`, and `OPENCLAW_DOCKER_ALL_LIVE_GEMINI_LIMIT=4`. Use `OPENCLAW_DOCKER_ALL_WEIGHT_LIMIT` or `OPENCLAW_DOCKER_ALL_DOCKER_LIMIT` for larger hosts. If one lane exceeds the effective weight or resource cap on a low-parallelism host, it can still start from an empty pool and will run alone until it releases capacity. Lane starts are staggered by 2 seconds by default to avoid local Docker daemon create storms; override with `OPENCLAW_DOCKER_ALL_START_STAGGER_MS=<ms>`. The runner preflights Docker by default, cleans stale OpenClaw E2E containers, emits active-lane status every 30 seconds, shares provider CLI tool caches between compatible lanes, retries transient live-provider failures once by default (`OPENCLAW_DOCKER_ALL_LIVE_RETRIES=<n>`), and stores lane timings in `.artifacts/docker-tests/lane-timings.json` for longest-first ordering on later runs. Use `OPENCLAW_DOCKER_ALL_DRY_RUN=1` to print the lane manifest without running Docker, `OPENCLAW_DOCKER_ALL_STATUS_INTERVAL_MS=<ms>` to tune status output, or `OPENCLAW_DOCKER_ALL_TIMINGS=0` to disable timing reuse. Use `OPENCLAW_DOCKER_ALL_LIVE_MODE=skip` for deterministic/local lanes only or `OPENCLAW_DOCKER_ALL_LIVE_MODE=only` for live-provider lanes only; package aliases are `pnpm test:docker:local:all` and `pnpm test:docker:live:all`. Live-only mode merges main and tail live lanes into one longest-first pool so provider buckets can pack Claude, Codex, and Gemini work together. The runner stops scheduling new pooled lanes after the first failure unless `OPENCLAW_DOCKER_ALL_FAIL_FAST=0` is set, and each lane has a 120-minute fallback timeout overridable with `OPENCLAW_DOCKER_ALL_LANE_TIMEOUT_MS`; selected live/tail lanes use tighter per-lane caps. CLI backend Docker setup commands have their own timeout via `OPENCLAW_LIVE_CLI_BACKEND_SETUP_TIMEOUT_SECONDS` (default 180). Per-lane logs, `summary.json`, `failures.json`, and phase timings are written under `.artifacts/docker-tests/<run-id>/`; use `pnpm test:docker:timings <summary.json>` to inspect slow lanes and `pnpm test:docker:rerun <run-id|summary.json|failures.json>` to print cheap targeted rerun commands.
|
||||
- `pnpm test:docker:all`: Builds the shared live-test image, packs OpenClaw once as an npm tarball, builds/reuses a bare Node/Git runner image plus a functional image that installs that tarball into `/app`, then runs Docker smoke lanes with `OPENCLAW_SKIP_DOCKER_BUILD=1` through a weighted scheduler. The bare image (`OPENCLAW_DOCKER_E2E_BARE_IMAGE`) is used for installer/update/plugin-dependency lanes; those lanes mount the prebuilt tarball instead of using copied repo sources. The functional image (`OPENCLAW_DOCKER_E2E_FUNCTIONAL_IMAGE`) is used for normal built-app functionality lanes. `scripts/package-openclaw-for-docker.mjs` is the single local/CI package packer and validates the tarball plus `dist/postinstall-inventory.json` before Docker consumes it. Docker lane definitions live in `scripts/lib/docker-e2e-scenarios.mjs`; planner logic lives in `scripts/lib/docker-e2e-plan.mjs`; `scripts/test-docker-all.mjs` executes the selected plan. `node scripts/test-docker-all.mjs --plan-json` emits the scheduler-owned CI plan for selected lanes, image kinds, package/live-image needs, state scenarios, and credential checks without building or running Docker. `OPENCLAW_DOCKER_ALL_PARALLELISM=<n>` controls process slots and defaults to 10; `OPENCLAW_DOCKER_ALL_TAIL_PARALLELISM=<n>` controls the provider-sensitive tail pool and defaults to 10. Heavy lane caps default to `OPENCLAW_DOCKER_ALL_LIVE_LIMIT=9`, `OPENCLAW_DOCKER_ALL_NPM_LIMIT=5`, and `OPENCLAW_DOCKER_ALL_SERVICE_LIMIT=7`; provider caps default to one heavy lane per provider via `OPENCLAW_DOCKER_ALL_LIVE_CLAUDE_LIMIT=4`, `OPENCLAW_DOCKER_ALL_LIVE_CODEX_LIMIT=4`, and `OPENCLAW_DOCKER_ALL_LIVE_GEMINI_LIMIT=4`. Use `OPENCLAW_DOCKER_ALL_WEIGHT_LIMIT` or `OPENCLAW_DOCKER_ALL_DOCKER_LIMIT` for larger hosts. If one lane exceeds the effective weight or resource cap on a low-parallelism host, it can still start from an empty pool and will run alone until it releases capacity. Lane starts are staggered by 2 seconds by default to avoid local Docker daemon create storms; override with `OPENCLAW_DOCKER_ALL_START_STAGGER_MS=<ms>`. The runner preflights Docker by default, cleans stale OpenClaw E2E containers, emits active-lane status every 30 seconds, shares provider CLI tool caches between compatible lanes, retries transient live-provider failures once by default (`OPENCLAW_DOCKER_ALL_LIVE_RETRIES=<n>`), and stores lane timings in `.artifacts/docker-tests/lane-timings.json` for longest-first ordering on later runs. Use `OPENCLAW_DOCKER_ALL_DRY_RUN=1` to print the lane manifest without running Docker, `OPENCLAW_DOCKER_ALL_STATUS_INTERVAL_MS=<ms>` to tune status output, or `OPENCLAW_DOCKER_ALL_TIMINGS=0` to disable timing reuse. Use `OPENCLAW_DOCKER_ALL_LIVE_MODE=skip` for deterministic/local lanes only or `OPENCLAW_DOCKER_ALL_LIVE_MODE=only` for live-provider lanes only; package aliases are `pnpm test:docker:local:all` and `pnpm test:docker:live:all`. Live-only mode merges main and tail live lanes into one longest-first pool so provider buckets can pack Claude, Codex, and Gemini work together. The runner stops scheduling new pooled lanes after the first failure unless `OPENCLAW_DOCKER_ALL_FAIL_FAST=0` is set, and each lane has a 120-minute fallback timeout overridable with `OPENCLAW_DOCKER_ALL_LANE_TIMEOUT_MS`; selected live/tail lanes use tighter per-lane caps. CLI backend Docker setup commands have their own timeout via `OPENCLAW_LIVE_CLI_BACKEND_SETUP_TIMEOUT_SECONDS` (default 180). Per-lane logs, `summary.json`, `failures.json`, and phase timings are written under `.artifacts/docker-tests/<run-id>/`; use `pnpm test:docker:timings <summary.json>` to inspect slow lanes and `pnpm test:docker:rerun <run-id|summary.json|failures.json>` to print cheap targeted rerun commands.
|
||||
- `pnpm test:docker:browser-cdp-snapshot`: Builds a Chromium-backed source E2E container, starts raw CDP plus an isolated Gateway, runs `browser doctor --deep`, and verifies CDP role snapshots include link URLs, cursor-promoted clickables, iframe refs, and frame metadata.
|
||||
- `pnpm test:docker:skill-install`: Installs the packed OpenClaw tarball in a bare Docker runner, disables `skills.install.allowUploadedArchives`, resolves a current skill slug from live ClawHub search, installs it through `openclaw skills install`, and verifies `SKILL.md`, `.clawhub/origin.json`, `.clawhub/lock.json`, and `skills info --json`.
|
||||
- CLI backend live Docker probes can be run as focused lanes, for example `pnpm test:docker:live-cli-backend:claude`, `pnpm test:docker:live-cli-backend:claude:resume`, or `pnpm test:docker:live-cli-backend:claude:mcp`. Gemini has matching `:resume` and `:mcp` aliases.
|
||||
|
||||
@@ -150,6 +150,13 @@ inter-session user turns that only have provenance metadata.
|
||||
- Turn validation (merge consecutive user turns to satisfy strict alternation).
|
||||
- Trailing assistant prefill turns are stripped from outgoing Anthropic Messages
|
||||
payloads when thinking is enabled, including Cloudflare AI Gateway routes.
|
||||
- Pre-compaction assistant thinking signatures are stripped before provider
|
||||
replay when a session has been compacted. Thinking signatures are
|
||||
cryptographically bound to the conversation prefix at generation time; after
|
||||
compaction the prefix changes (summarized content is replaced by a compaction
|
||||
summary), so replaying the original signatures causes Anthropic to reject the
|
||||
request with "Invalid signature in thinking block". The thinking text is
|
||||
preserved as an unsigned block and is then handled by the rule below.
|
||||
- Thinking blocks with missing, empty, or blank replay signatures are stripped
|
||||
before provider conversion. If that empties an assistant turn, OpenClaw keeps
|
||||
turn shape with non-empty omitted-reasoning text.
|
||||
@@ -165,6 +172,9 @@ inter-session user turns that only have provenance metadata.
|
||||
repaired on disk before load.
|
||||
- Assistant stream-error turns that contain only blank text blocks are dropped
|
||||
from the in-memory replay copy instead of replaying an invalid blank block.
|
||||
- Pre-compaction assistant thinking signatures are stripped before Converse
|
||||
replay when a session has been compacted, for the same reason as Anthropic
|
||||
above.
|
||||
- Claude thinking blocks with missing, empty, or blank replay signatures are
|
||||
stripped before Converse replay. If that empties an assistant turn, OpenClaw
|
||||
keeps turn shape with non-empty omitted-reasoning text.
|
||||
|
||||
@@ -51,7 +51,7 @@ For a complete map of the docs, see [Docs hubs](/start/hubs).
|
||||
- [macOS app](/platforms/macos)
|
||||
- [iOS app](/platforms/ios)
|
||||
- [Android app](/platforms/android)
|
||||
- [Windows (WSL2)](/platforms/windows)
|
||||
- [Windows Hub](/platforms/windows)
|
||||
- [Linux app](/platforms/linux)
|
||||
|
||||
## Operations and safety
|
||||
|
||||
@@ -135,7 +135,7 @@ Use these hubs to discover every page, including deep dives and reference docs t
|
||||
- [macOS](/platforms/macos)
|
||||
- [iOS](/platforms/ios)
|
||||
- [Android](/platforms/android)
|
||||
- [Windows (WSL2)](/platforms/windows)
|
||||
- [Windows Hub](/platforms/windows)
|
||||
- [Linux](/platforms/linux)
|
||||
- [Web surfaces](/web)
|
||||
|
||||
|
||||
@@ -235,7 +235,7 @@ Logs live under `/tmp/openclaw/` (default: `openclaw-YYYY-MM-DD.log`).
|
||||
- macOS menu bar companion: [OpenClaw macOS app](/platforms/macos)
|
||||
- iOS node app: [iOS app](/platforms/ios)
|
||||
- Android node app: [Android app](/platforms/android)
|
||||
- Windows status: [Windows (WSL2)](/platforms/windows)
|
||||
- Windows Hub: [Windows](/platforms/windows)
|
||||
- Linux status: [Linux app](/platforms/linux)
|
||||
- Security: [Security](/gateway/security)
|
||||
|
||||
|
||||
@@ -7,8 +7,9 @@ title: "Onboarding (CLI)"
|
||||
sidebarTitle: "Onboarding: CLI"
|
||||
---
|
||||
|
||||
CLI onboarding is the **recommended** way to set up OpenClaw on macOS,
|
||||
Linux, or Windows (via WSL2; strongly recommended).
|
||||
CLI onboarding is the **recommended** terminal setup path for OpenClaw on
|
||||
macOS, Linux, or Windows. Windows desktop users can also start with
|
||||
[Windows Hub](/platforms/windows).
|
||||
It configures a local Gateway or a remote Gateway connection, plus channels, skills,
|
||||
and workspace defaults in one guided flow.
|
||||
|
||||
|
||||
@@ -548,6 +548,11 @@ Two ways to start an ACP session:
|
||||
requester session as system events. Accepted responses include
|
||||
`streamLogPath` pointing to a session-scoped JSONL log
|
||||
(`<sessionId>.acp-stream.jsonl`) you can tail for full relay history.
|
||||
Parent progress streams show assistant commentary and ACP status progress by
|
||||
default unless `streaming.progress.commentary=false`. Discord also defaults
|
||||
parent previews to progress mode when no stream mode is configured. Status
|
||||
progress still honors `acp.stream.tagVisibility`, so tags such as `plan`
|
||||
remain hidden unless explicitly enabled.
|
||||
</ParamField>
|
||||
|
||||
ACP `sessions_spawn` runs use `agents.defaults.subagents.runTimeoutSeconds` for
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
/**
|
||||
* ACPX runtime plugin entry. It registers the embedded ACP backend service and
|
||||
* wires reply-dispatch hooks into the plugin SDK runtime.
|
||||
*/
|
||||
import { tryDispatchAcpReplyHook } from "openclaw/plugin-sdk/acp-runtime-backend";
|
||||
import { createAcpxRuntimeService } from "./register.runtime.js";
|
||||
import type { OpenClawPluginApi } from "./runtime-api.js";
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
/**
|
||||
* Lazy ACPX runtime service registration. The plugin exposes an ACP backend
|
||||
* immediately, then imports the heavier service only when a session needs it.
|
||||
*/
|
||||
import {
|
||||
getAcpRuntimeBackend,
|
||||
registerAcpRuntimeBackend,
|
||||
@@ -62,6 +66,7 @@ function createDeferredRuntime(state: DeferredServiceState): AcpRuntime {
|
||||
return createLazyAcpRuntimeProxy(resolveRuntime);
|
||||
}
|
||||
|
||||
/** Creates the plugin service that registers ACPX as an ACP runtime backend. */
|
||||
export function createAcpxRuntimeService(
|
||||
params: CreateAcpxRuntimeServiceParams = {},
|
||||
): OpenClawPluginService {
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
/**
|
||||
* Public runtime API barrel for ACPX. Core and plugin consumers import these
|
||||
* SDK-facing ACP runtime contracts instead of reaching into ACPX internals.
|
||||
*/
|
||||
export type { AcpRuntimeErrorCode } from "openclaw/plugin-sdk/acp-runtime-backend";
|
||||
export {
|
||||
AcpRuntimeError,
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
/**
|
||||
* ACPX setup plugin entry. It auto-enables setup when ACP config already points
|
||||
* at the embedded ACPX runtime backend.
|
||||
*/
|
||||
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
|
||||
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
/**
|
||||
* Prepares isolated Codex and Claude ACP wrapper commands for ACPX. The bridge
|
||||
* copies safe auth/config state into plugin-owned homes and redacts diagnostics.
|
||||
*/
|
||||
import fsSync from "node:fs";
|
||||
import fs from "node:fs/promises";
|
||||
import { createRequire } from "node:module";
|
||||
@@ -724,6 +728,7 @@ function buildClaudeAcpWrapperCommand(wrapperPath: string, configuredCommand?: s
|
||||
return configuredCommand?.trim() || buildWrapperCommand(wrapperPath);
|
||||
}
|
||||
|
||||
/** Prepare ACPX agent commands and isolated auth homes for Codex/Claude adapters. */
|
||||
export async function prepareAcpxCodexAuthConfig(params: {
|
||||
pluginConfig: ResolvedAcpxPluginConfig;
|
||||
stateDir: string;
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
/**
|
||||
* Builds isolated Codex config for ACPX sessions. It preserves safe inherited
|
||||
* runtime options while rendering only trusted project entries for the session.
|
||||
*/
|
||||
import path from "node:path";
|
||||
|
||||
function stripTomlComment(line: string): string {
|
||||
@@ -114,6 +118,7 @@ function parseTrustedInlineProjectEntries(value: string): string[] {
|
||||
return trusted;
|
||||
}
|
||||
|
||||
/** Extract trusted project paths from Codex TOML config. */
|
||||
export function extractTrustedCodexProjectPaths(configToml: string): string[] {
|
||||
const trusted = new Set<string>();
|
||||
let currentProjectPath: string | undefined;
|
||||
@@ -261,6 +266,7 @@ function extractInheritedCodexRuntimeConfig(configToml: string): string {
|
||||
return inheritedLines.join("\n");
|
||||
}
|
||||
|
||||
/** Render a session-local Codex config with inherited runtime settings and trust entries. */
|
||||
export function renderIsolatedCodexConfig(params: {
|
||||
sourceConfigToml?: string;
|
||||
projectPaths: string[];
|
||||
@@ -292,6 +298,7 @@ export function renderIsolatedCodexConfig(params: {
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
/** Render only the project trust section for a session-local Codex config. */
|
||||
export function renderIsolatedCodexProjectTrustConfig(projectPaths: string[]): string {
|
||||
return renderIsolatedCodexConfig({ projectPaths });
|
||||
}
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
/**
|
||||
* Small shell-command helpers for ACPX-launched processes. Splitting supports
|
||||
* simple quoted command strings from config without invoking a shell parser.
|
||||
*/
|
||||
/** Quote one command argument for display or config serialization. */
|
||||
export function quoteCommandPart(value: string): string {
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
|
||||
/** Split a command string into argv-like parts using simple quote/backslash rules. */
|
||||
export function splitCommandParts(value: string): string[] {
|
||||
const parts: string[] = [];
|
||||
let current = "";
|
||||
|
||||
@@ -1,19 +1,28 @@
|
||||
/**
|
||||
* ACPX plugin configuration schema and public config types. Runtime setup uses
|
||||
* this file as the single source of truth for validation and defaulting.
|
||||
*/
|
||||
import { z } from "zod";
|
||||
|
||||
const ACPX_PERMISSION_MODES = ["approve-all", "approve-reads", "deny-all"] as const;
|
||||
/** Permission policy applied to interactive ACPX tool requests. */
|
||||
export type AcpxPermissionMode = (typeof ACPX_PERMISSION_MODES)[number];
|
||||
|
||||
const ACPX_NON_INTERACTIVE_POLICIES = ["deny", "fail"] as const;
|
||||
/** Permission policy applied when ACPX cannot ask a human for approval. */
|
||||
export type AcpxNonInteractivePermissionPolicy = (typeof ACPX_NON_INTERACTIVE_POLICIES)[number];
|
||||
|
||||
/** Default session timeout for ACPX runtime turns. */
|
||||
export const DEFAULT_ACPX_TIMEOUT_SECONDS = 120;
|
||||
|
||||
/** Raw MCP server command config accepted from plugin configuration. */
|
||||
export type McpServerConfig = {
|
||||
command: string;
|
||||
args?: string[];
|
||||
env?: Record<string, string>;
|
||||
};
|
||||
|
||||
/** Normalized MCP server config emitted to the ACPX runtime process. */
|
||||
export type AcpxMcpServer = {
|
||||
name: string;
|
||||
command: string;
|
||||
@@ -21,6 +30,7 @@ export type AcpxMcpServer = {
|
||||
env: Array<{ name: string; value: string }>;
|
||||
};
|
||||
|
||||
/** User-provided ACPX plugin configuration before defaults are resolved. */
|
||||
export type AcpxPluginConfig = {
|
||||
cwd?: string;
|
||||
stateDir?: string;
|
||||
@@ -36,6 +46,7 @@ export type AcpxPluginConfig = {
|
||||
agents?: Record<string, { command: string; args?: string[] }>;
|
||||
};
|
||||
|
||||
/** Fully resolved ACPX config consumed by the runtime service. */
|
||||
export type ResolvedAcpxPluginConfig = {
|
||||
cwd: string;
|
||||
stateDir: string;
|
||||
@@ -76,6 +87,7 @@ const McpServerConfigSchema = z.object({
|
||||
.describe("Environment variables for the MCP server"),
|
||||
});
|
||||
|
||||
/** Zod schema for validating raw ACPX plugin config from OpenClaw config. */
|
||||
export const AcpxPluginConfigSchema = z.strictObject({
|
||||
cwd: nonEmptyTrimmedString("cwd must be a non-empty string").optional(),
|
||||
stateDir: nonEmptyTrimmedString("stateDir must be a non-empty string").optional(),
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
/**
|
||||
* Resolves ACPX plugin config from raw user configuration. It locates the
|
||||
* plugin root, injects optional MCP bridge servers, and applies runtime defaults.
|
||||
*/
|
||||
import fs from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
@@ -80,6 +84,7 @@ function resolveAcpxPluginRootFromOpenClawLayout(moduleUrl: string): string | nu
|
||||
}
|
||||
return null;
|
||||
}
|
||||
/** Resolve the ACPX plugin root across source, dist, and dist-runtime layouts. */
|
||||
export function resolveAcpxPluginRoot(moduleUrl: string = import.meta.url): string {
|
||||
const resolvedRoot = resolveNearestAcpxPluginRoot(moduleUrl);
|
||||
// In a live repo checkout, dist/ can be rebuilt out from under the running gateway.
|
||||
@@ -210,6 +215,7 @@ function resolveConfiguredMcpServers(params: {
|
||||
return resolved;
|
||||
}
|
||||
|
||||
/** Convert OpenClaw MCP server config into ACPX runtime MCP server entries. */
|
||||
export function toAcpMcpServers(mcpServers: Record<string, McpServerConfig>): AcpxMcpServer[] {
|
||||
return Object.entries(mcpServers).map(([name, server]) => ({
|
||||
name,
|
||||
@@ -222,6 +228,7 @@ export function toAcpMcpServers(mcpServers: Record<string, McpServerConfig>): Ac
|
||||
}));
|
||||
}
|
||||
|
||||
/** Validate and normalize raw ACPX plugin config for runtime startup. */
|
||||
export function resolveAcpxPluginConfig(params: {
|
||||
rawConfig: unknown;
|
||||
workspaceDir?: string;
|
||||
|
||||
@@ -1,15 +1,25 @@
|
||||
/**
|
||||
* Persistent lease store for ACPX wrapper processes. Leases let OpenClaw attach
|
||||
* gateway/session identity to spawned ACP processes and clean them up later.
|
||||
*/
|
||||
import { randomUUID, createHash } from "node:crypto";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { readJsonFileWithFallback, writeJsonFileAtomically } from "openclaw/plugin-sdk/json-store";
|
||||
|
||||
/** Environment variable carrying the ACPX process lease id. */
|
||||
export const OPENCLAW_ACPX_LEASE_ID_ENV = "OPENCLAW_ACPX_LEASE_ID";
|
||||
/** Environment variable carrying the owning gateway instance id. */
|
||||
export const OPENCLAW_GATEWAY_INSTANCE_ID_ENV = "OPENCLAW_GATEWAY_INSTANCE_ID";
|
||||
/** CLI argument carrying the ACPX process lease id for platforms without env wrapping. */
|
||||
export const OPENCLAW_ACPX_LEASE_ID_ARG = "--openclaw-acpx-lease-id";
|
||||
/** CLI argument carrying the owning gateway instance id. */
|
||||
export const OPENCLAW_GATEWAY_INSTANCE_ID_ARG = "--openclaw-gateway-instance-id";
|
||||
|
||||
/** Lifecycle state for a tracked ACPX wrapper process. */
|
||||
export type AcpxProcessLeaseState = "open" | "closing" | "closed" | "lost";
|
||||
|
||||
/** Persisted identity and command metadata for one ACPX wrapper process. */
|
||||
export type AcpxProcessLease = {
|
||||
leaseId: string;
|
||||
gatewayInstanceId: string;
|
||||
@@ -23,6 +33,7 @@ export type AcpxProcessLease = {
|
||||
state: AcpxProcessLeaseState;
|
||||
};
|
||||
|
||||
/** Async lease store used by runtime sessions and cleanup routines. */
|
||||
export type AcpxProcessLeaseStore = {
|
||||
load(leaseId: string): Promise<AcpxProcessLease | undefined>;
|
||||
listOpen(gatewayInstanceId?: string): Promise<AcpxProcessLease[]>;
|
||||
@@ -84,6 +95,7 @@ function writeLeaseFile(filePath: string, value: LeaseFile): Promise<void> {
|
||||
return writeJsonFileAtomically(filePath, value);
|
||||
}
|
||||
|
||||
/** Create a serialized JSON-backed ACPX process lease store. */
|
||||
export function createAcpxProcessLeaseStore(params: { stateDir: string }): AcpxProcessLeaseStore {
|
||||
const filePath = path.join(params.stateDir, LEASE_FILE);
|
||||
let updateQueue: Promise<void> = Promise.resolve();
|
||||
@@ -135,10 +147,12 @@ export function createAcpxProcessLeaseStore(params: { stateDir: string }): AcpxP
|
||||
};
|
||||
}
|
||||
|
||||
/** Create a unique lease id for one ACPX wrapper process. */
|
||||
export function createAcpxProcessLeaseId(): string {
|
||||
return randomUUID();
|
||||
}
|
||||
|
||||
/** Hash a wrapper command so process leases can detect command drift. */
|
||||
export function hashAcpxProcessCommand(command: string): string {
|
||||
return createHash("sha256").update(command).digest("hex");
|
||||
}
|
||||
@@ -161,6 +175,7 @@ function appendAcpxLeaseArgs(params: {
|
||||
].join(" ");
|
||||
}
|
||||
|
||||
/** Add ACPX lease identity to a command through env vars and portable args. */
|
||||
export function withAcpxLeaseEnvironment(params: {
|
||||
command: string;
|
||||
leaseId: string;
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
/**
|
||||
* ACPX process ownership checks and cleanup. The reaper only terminates
|
||||
* OpenClaw-owned wrapper trees after validating paths, packages, and lease ids.
|
||||
*/
|
||||
import { execFile } from "node:child_process";
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
@@ -29,24 +33,28 @@ const ACP_PACKAGE_MARKERS = [
|
||||
"/acpx/dist/",
|
||||
];
|
||||
|
||||
/** Minimal process-table row used by ACPX cleanup. */
|
||||
export type AcpxProcessInfo = {
|
||||
pid: number;
|
||||
ppid: number;
|
||||
command: string;
|
||||
};
|
||||
|
||||
/** Injectable process-listing and termination hooks for tests. */
|
||||
export type AcpxProcessCleanupDeps = {
|
||||
listProcesses?: () => Promise<AcpxProcessInfo[]>;
|
||||
killProcess?: (pid: number, signal: NodeJS.Signals) => void;
|
||||
sleep?: (ms: number) => Promise<void>;
|
||||
};
|
||||
|
||||
/** Result from cleaning up a single ACPX process tree. */
|
||||
export type AcpxProcessCleanupResult = {
|
||||
inspectedPids: number[];
|
||||
terminatedPids: number[];
|
||||
skippedReason?: "missing-root" | "not-openclaw-owned" | "unverified-root";
|
||||
};
|
||||
|
||||
/** Result from startup orphan reaping. */
|
||||
export type AcpxStartupReapResult = {
|
||||
inspectedPids: number[];
|
||||
terminatedPids: number[];
|
||||
@@ -109,6 +117,7 @@ function commandWrapperBelongsToRoot(command: string, wrapperRoot: string | unde
|
||||
);
|
||||
}
|
||||
|
||||
/** Check whether a command references an OpenClaw-generated ACPX wrapper path. */
|
||||
export function isOpenClawLeaseAwareAcpxProcessCommand(params: {
|
||||
command: string | undefined;
|
||||
wrapperRoot?: string;
|
||||
@@ -158,6 +167,7 @@ function liveCommandMatchesLeaseIdentity(params: {
|
||||
);
|
||||
}
|
||||
|
||||
/** Check whether a command is owned by OpenClaw ACPX runtime packages or wrappers. */
|
||||
export function isOpenClawOwnedAcpxProcessCommand(params: {
|
||||
command: string | undefined;
|
||||
wrapperRoot?: string;
|
||||
@@ -200,6 +210,7 @@ function parseProcessList(stdout: string): AcpxProcessInfo[] {
|
||||
return processes;
|
||||
}
|
||||
|
||||
/** List host processes in the compact shape needed by ACPX cleanup. */
|
||||
export async function listPlatformProcesses(): Promise<AcpxProcessInfo[]> {
|
||||
if (process.platform === "win32") {
|
||||
return [];
|
||||
@@ -294,6 +305,7 @@ async function terminatePids(
|
||||
return terminated;
|
||||
}
|
||||
|
||||
/** Terminate one validated OpenClaw-owned ACPX wrapper process tree. */
|
||||
export async function cleanupOpenClawOwnedAcpxProcessTree(params: {
|
||||
rootPid?: number;
|
||||
rootCommand?: string;
|
||||
@@ -378,6 +390,7 @@ export async function cleanupOpenClawOwnedAcpxProcessTree(params: {
|
||||
};
|
||||
}
|
||||
|
||||
/** Reap orphaned OpenClaw-owned ACPX wrapper trees during runtime startup. */
|
||||
export async function reapStaleOpenClawOwnedAcpxOrphans(params: {
|
||||
wrapperRoot: string;
|
||||
deps?: AcpxProcessCleanupDeps;
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
/**
|
||||
* Command-line parser for ACPX MCP proxy targets. It handles simple quoting and
|
||||
* Windows executable paths before spawning the configured MCP target.
|
||||
*/
|
||||
const WINDOWS_DIRECT_EXECUTABLE_PATH_RE =
|
||||
/^(?<command>(?:[A-Za-z]:[\\/]|\\\\[^\\/]+[\\/][^\\/]+[\\/]).*?\.(?:exe|com))(?=\s|$)(?:\s+(?<rest>.*))?$/i;
|
||||
|
||||
@@ -106,6 +110,7 @@ function assertSupportedWindowsCommand(command, platform = process.platform) {
|
||||
);
|
||||
}
|
||||
|
||||
/** Split a configured command string into `{ command, args }` for child_process.spawn. */
|
||||
export function splitCommandLine(value, platform = process.platform) {
|
||||
const windowsCommand = splitWindowsExecutableCommand(value, platform);
|
||||
const parts = windowsCommand ?? splitCommandParts(value, platform);
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Stdio MCP proxy used by ACPX wrappers. It injects OpenClaw-provided MCP
|
||||
* servers into session creation/load/fork requests before forwarding to target.
|
||||
*/
|
||||
import { spawn } from "node:child_process";
|
||||
import path from "node:path";
|
||||
import { createInterface } from "node:readline";
|
||||
@@ -70,6 +74,7 @@ function rewriteLine(line, mcpServers) {
|
||||
}
|
||||
}
|
||||
|
||||
/** Build spawn options for the proxied MCP target process. */
|
||||
export function createTargetSpawnOptions(platform = process.platform) {
|
||||
const options = {
|
||||
stdio: ["pipe", "pipe", "inherit"],
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
/**
|
||||
* Lazy ACP runtime proxy for ACPX. It defers resolving the real runtime until
|
||||
* the first ACP call while preserving the SDK runtime shape.
|
||||
*/
|
||||
import type { AcpRuntime } from "../runtime-api.js";
|
||||
import { lazyStartRuntimeTurn } from "./runtime-turn.js";
|
||||
|
||||
/** Create an ACP runtime facade backed by an async runtime resolver. */
|
||||
export function createLazyAcpRuntimeProxy<T extends AcpRuntime>(
|
||||
resolveRuntime: () => Promise<T>,
|
||||
): AcpRuntime {
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
/**
|
||||
* ACPX turn adapters. Modern runtimes can expose startTurn directly; legacy
|
||||
* runtimes that only stream runTurn events are adapted to the newer contract.
|
||||
*/
|
||||
import type {
|
||||
AcpRuntime,
|
||||
AcpRuntimeEvent,
|
||||
@@ -153,10 +157,12 @@ function legacyRunTurnAsStartTurn(runtime: AcpRuntime, input: AcpRuntimeTurnInpu
|
||||
};
|
||||
}
|
||||
|
||||
/** Start an ACP turn, adapting legacy runTurn-only runtimes when needed. */
|
||||
export function startRuntimeTurn(runtime: AcpRuntime, input: AcpRuntimeTurnInput): AcpRuntimeTurn {
|
||||
return runtime.startTurn?.(input) ?? legacyRunTurnAsStartTurn(runtime, input);
|
||||
}
|
||||
|
||||
/** Start an ACP turn through a lazy runtime resolver. */
|
||||
export function lazyStartRuntimeTurn(
|
||||
resolveRuntime: () => Promise<AcpRuntime>,
|
||||
input: AcpRuntimeTurnInput,
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
/**
|
||||
* OpenClaw ACPX runtime adapter. It wraps the upstream acpx runtime with
|
||||
* OpenClaw session metadata, lease tracking, model scoping, and cleanup policy.
|
||||
*/
|
||||
import { AsyncLocalStorage } from "node:async_hooks";
|
||||
import fs from "node:fs/promises";
|
||||
import path, { resolve as resolvePath } from "node:path";
|
||||
@@ -635,6 +639,7 @@ function shouldUseDistinctBridgeDelegate(options: AcpRuntimeOptions): boolean {
|
||||
return Array.isArray(mcpServers) && mcpServers.length > 0;
|
||||
}
|
||||
|
||||
/** OpenClaw-managed ACP runtime implementation backed by the upstream acpx runtime. */
|
||||
export class AcpxRuntime implements AcpRuntime {
|
||||
private readonly sessionStore: ResetAwareSessionStore;
|
||||
private readonly agentRegistry: AcpAgentRegistry;
|
||||
@@ -1235,6 +1240,7 @@ export {
|
||||
encodeAcpxRuntimeHandleState,
|
||||
};
|
||||
|
||||
/** Test-only hooks for ACPX runtime behavior that is otherwise private. */
|
||||
export const testing = {
|
||||
appendCodexAcpConfigOverrides,
|
||||
assertSupportedRuntimeSessionMode,
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
/**
|
||||
* ACPX plugin service lifecycle. It resolves config, prepares isolated adapter
|
||||
* wrappers, registers the ACP backend, and manages startup/cleanup probes.
|
||||
*/
|
||||
import { randomUUID } from "node:crypto";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
@@ -61,6 +65,7 @@ function loadRuntimeModule(): Promise<AcpxRuntimeModule> {
|
||||
return runtimeModulePromise;
|
||||
}
|
||||
|
||||
/** Convert ACPX timeout seconds into timer-safe milliseconds. */
|
||||
export function resolveAcpxTimerTimeoutMs(timeoutSeconds: number | undefined): number | undefined {
|
||||
if (timeoutSeconds === undefined) {
|
||||
return undefined;
|
||||
@@ -295,6 +300,7 @@ async function reapOpenAcpxProcessLeases(params: {
|
||||
return { inspectedPids, terminatedPids };
|
||||
}
|
||||
|
||||
/** Create the ACPX plugin service that owns runtime registration and cleanup. */
|
||||
export function createAcpxRuntimeService(
|
||||
params: CreateAcpxRuntimeServiceParams = {},
|
||||
): OpenClawPluginService {
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
/**
|
||||
* Doctor migration contract for Active Memory state. It moves legacy per-session
|
||||
* toggle JSON into the plugin state keyed store used by current runtimes.
|
||||
*/
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
@@ -81,6 +85,7 @@ async function archiveLegacySource(params: {
|
||||
}
|
||||
}
|
||||
|
||||
/** State migrations exposed to OpenClaw doctor for Active Memory. */
|
||||
export const stateMigrations: PluginDoctorStateMigration[] = [
|
||||
{
|
||||
id: "active-memory-session-toggles-json-to-plugin-state",
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
/**
|
||||
* Active Memory plugin entry and runtime implementation. It recalls recent
|
||||
* memory context through configured agents and injects bounded context snippets.
|
||||
*/
|
||||
import crypto from "node:crypto";
|
||||
import fsSync from "node:fs";
|
||||
import fs from "node:fs/promises";
|
||||
@@ -2861,6 +2865,7 @@ async function maybeResolveActiveRecall(params: {
|
||||
}
|
||||
}
|
||||
|
||||
/** Plugin entry registering Active Memory hooks, tools, config schema, and doctor cleanup. */
|
||||
export default definePluginEntry({
|
||||
id: "active-memory",
|
||||
name: "Active Memory",
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
/**
|
||||
* Admin HTTP RPC plugin entry. It exposes a trusted gateway-authenticated HTTP
|
||||
* endpoint for the explicit admin method allowlist.
|
||||
*/
|
||||
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
|
||||
import { handleAdminHttpRpcRequest } from "./src/handler.js";
|
||||
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
/**
|
||||
* HTTP handler for the Admin RPC endpoint. It validates JSON requests, enforces
|
||||
* the method allowlist, dispatches gateway methods, and maps errors to HTTP.
|
||||
*/
|
||||
import { randomUUID } from "node:crypto";
|
||||
import type { IncomingMessage, ServerResponse } from "node:http";
|
||||
import { dispatchGatewayMethod } from "openclaw/plugin-sdk/gateway-method-runtime";
|
||||
@@ -184,6 +188,7 @@ async function dispatchAdminRpc(request: ParsedRequest): Promise<RpcResponse> {
|
||||
}
|
||||
}
|
||||
|
||||
/** Handle one gateway-authenticated Admin HTTP RPC request. */
|
||||
export async function handleAdminHttpRpcRequest(
|
||||
req: IncomingMessage,
|
||||
res: ServerResponse,
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
/**
|
||||
* Method allowlist for Admin HTTP RPC. Only methods listed here can cross the
|
||||
* trusted operator HTTP surface.
|
||||
*/
|
||||
const ADMIN_HTTP_RPC_ALLOWED_METHOD_GROUPS = {
|
||||
gateway: [
|
||||
"health",
|
||||
@@ -54,10 +58,12 @@ const ADMIN_HTTP_RPC_ALLOWED_METHODS: ReadonlySet<string> = new Set(
|
||||
Object.values(ADMIN_HTTP_RPC_ALLOWED_METHOD_GROUPS).flat(),
|
||||
);
|
||||
|
||||
/** Return whether an admin RPC method is exposed over HTTP. */
|
||||
export function isAdminHttpRpcAllowedMethod(method: string): boolean {
|
||||
return ADMIN_HTTP_RPC_ALLOWED_METHODS.has(method);
|
||||
}
|
||||
|
||||
/** List all admin RPC methods exposed over HTTP. */
|
||||
export function listAdminHttpRpcAllowedMethods(): string[] {
|
||||
return Array.from(ADMIN_HTTP_RPC_ALLOWED_METHODS);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
/**
|
||||
* Alibaba Model Studio plugin entry. Registers the DashScope-backed video
|
||||
* generation provider.
|
||||
*/
|
||||
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
|
||||
import { buildAlibabaVideoGenerationProvider } from "./video-generation-provider.js";
|
||||
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
/**
|
||||
* Alibaba Model Studio video provider adapter. It resolves DashScope auth and
|
||||
* HTTP policy before delegating task polling to the shared video helper.
|
||||
*/
|
||||
import { isProviderApiKeyConfigured } from "openclaw/plugin-sdk/provider-auth";
|
||||
import { resolveApiKeyForProvider } from "openclaw/plugin-sdk/provider-auth-runtime";
|
||||
import { resolveProviderHttpRequestConfig } from "openclaw/plugin-sdk/provider-http";
|
||||
@@ -25,6 +29,7 @@ function resolveDashscopeAigcApiBaseUrl(baseUrl: string): string {
|
||||
return baseUrl.replace(/\/+$/u, "");
|
||||
}
|
||||
|
||||
/** Build the Alibaba/DashScope video generation provider descriptor. */
|
||||
export function buildAlibabaVideoGenerationProvider(): VideoGenerationProvider {
|
||||
return {
|
||||
id: "alibaba",
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
/**
|
||||
* Public Amazon Bedrock Mantle API barrel for discovery and bearer-token
|
||||
* helpers shared by config, runtime, and tests.
|
||||
*/
|
||||
export {
|
||||
discoverMantleModels,
|
||||
generateBearerTokenFromIam,
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
/**
|
||||
* Amazon Bedrock Mantle discovery and bearer-token handling. It resolves
|
||||
* explicit tokens, IAM-generated tokens, model catalogs, and implicit provider config.
|
||||
*/
|
||||
import { createSubsystemLogger } from "openclaw/plugin-sdk/core";
|
||||
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
||||
import {
|
||||
@@ -22,6 +26,7 @@ const DEFAULT_COST = {
|
||||
const DEFAULT_CONTEXT_WINDOW = 32000;
|
||||
const DEFAULT_MAX_TOKENS = 4096;
|
||||
const DEFAULT_REFRESH_INTERVAL_SECONDS = 3600; // 1 hour
|
||||
/** Config auth marker meaning Mantle should mint runtime bearer tokens from IAM. */
|
||||
export const MANTLE_IAM_TOKEN_MARKER = "__amazon_bedrock_mantle_iam__";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -152,6 +157,7 @@ export function getCachedIamToken(region: string): string | undefined {
|
||||
return getCachedIamTokenEntry(region)?.token;
|
||||
}
|
||||
|
||||
/** Resolve the actual runtime bearer token for Mantle, generating IAM tokens when needed. */
|
||||
export async function resolveMantleRuntimeBearerToken(params: {
|
||||
apiKey: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
@@ -186,7 +192,7 @@ export async function resolveMantleRuntimeBearerToken(params: {
|
||||
...(expiresAt === undefined ? {} : { expiresAt }),
|
||||
};
|
||||
}
|
||||
/** Reset the IAM token cache (for testing). */
|
||||
/** Clear the IAM token cache for tests. */
|
||||
export function resetIamTokenCacheForTest(): void {
|
||||
iamTokenCache.clear();
|
||||
}
|
||||
@@ -241,7 +247,7 @@ type MantleDiscoveryConfig = {
|
||||
|
||||
const discoveryCache = new Map<string, MantleCacheEntry>();
|
||||
|
||||
/** Clear the discovery cache (for testing). */
|
||||
/** Clear the Mantle discovery cache for tests. */
|
||||
export function resetMantleDiscoveryCacheForTest(): void {
|
||||
discoveryCache.clear();
|
||||
}
|
||||
@@ -261,6 +267,7 @@ export function resetMantleDiscoveryCacheForTest(): void {
|
||||
* Results are cached per region for `DEFAULT_REFRESH_INTERVAL_SECONDS`.
|
||||
* Returns an empty array if the request fails (no permission, network error, etc.).
|
||||
*/
|
||||
/** Discover Mantle models for one region/config. */
|
||||
export async function discoverMantleModels(params: {
|
||||
region: string;
|
||||
bearerToken: string;
|
||||
@@ -334,6 +341,7 @@ export async function discoverMantleModels(params: {
|
||||
* - Region from AWS_REGION / AWS_DEFAULT_REGION / default us-east-1
|
||||
* - Models discovered from `/v1/models`
|
||||
*/
|
||||
/** Resolve implicit Mantle provider config from env, IAM token support, and discovery. */
|
||||
export async function resolveImplicitMantleProvider(params: {
|
||||
env?: NodeJS.ProcessEnv;
|
||||
pluginConfig?: { discovery?: MantleDiscoveryConfig };
|
||||
@@ -408,6 +416,7 @@ export async function resolveImplicitMantleProvider(params: {
|
||||
};
|
||||
}
|
||||
|
||||
/** Merge an implicit Mantle provider catalog with explicit user config. */
|
||||
export function mergeImplicitMantleProvider(params: {
|
||||
existing: ModelProviderConfig | undefined;
|
||||
implicit: ModelProviderConfig;
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
/**
|
||||
* Amazon Bedrock Mantle plugin entry. Registers the OpenAI-compatible Mantle
|
||||
* provider plus Anthropic stream compatibility hooks.
|
||||
*/
|
||||
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
|
||||
import { registerBedrockMantlePlugin } from "./register.sync.runtime.js";
|
||||
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
/**
|
||||
* Anthropic Messages stream adapter for Bedrock Mantle. It rewrites Mantle
|
||||
* endpoints to Anthropic-compatible URLs and adjusts thinking-token budgets.
|
||||
*/
|
||||
import Anthropic from "@anthropic-ai/sdk";
|
||||
import type { StreamFn } from "openclaw/plugin-sdk/agent-core";
|
||||
import { stream, type Model, type SimpleStreamOptions } from "openclaw/plugin-sdk/llm";
|
||||
@@ -7,6 +11,7 @@ type AnthropicOptions = ConstructorParameters<typeof Anthropic>[0];
|
||||
type MantleAnthropicStream = typeof stream;
|
||||
type AnthropicStreamClient = Anthropic;
|
||||
|
||||
/** Resolve the Anthropic-compatible Mantle base URL from a provider base URL. */
|
||||
export function resolveMantleAnthropicBaseUrl(baseUrl: string): string {
|
||||
const trimmed = baseUrl.replace(/\/+$/, "");
|
||||
if (trimmed.endsWith("/anthropic")) {
|
||||
@@ -76,6 +81,7 @@ function adjustMaxTokensForThinking(
|
||||
return { maxTokens, thinkingBudget };
|
||||
}
|
||||
|
||||
/** Create the Mantle Anthropic Messages stream function. */
|
||||
export function createMantleAnthropicStreamFn(deps?: {
|
||||
createClient?: (options: AnthropicOptions) => Anthropic;
|
||||
stream?: MantleAnthropicStream;
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
/**
|
||||
* Synchronous Amazon Bedrock Mantle provider registration. It wires discovery,
|
||||
* runtime bearer-token preparation, stream wrappers, and failover classifiers.
|
||||
*/
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
import { resolvePluginConfigObject } from "openclaw/plugin-sdk/plugin-config-runtime";
|
||||
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry";
|
||||
@@ -15,6 +19,7 @@ type BedrockMantlePluginConfig = {
|
||||
};
|
||||
};
|
||||
|
||||
/** Register the Amazon Bedrock Mantle provider with OpenClaw. */
|
||||
export function registerBedrockMantlePlugin(api: OpenClawPluginApi): void {
|
||||
const providerId = "amazon-bedrock-mantle";
|
||||
const startupPluginConfig = (api.pluginConfig ?? {}) as BedrockMantlePluginConfig;
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
/**
|
||||
* Lightweight Amazon Bedrock API barrel for config and discovery consumers.
|
||||
* Keep runtime streaming exports out of this path so metadata flows stay cheap.
|
||||
*/
|
||||
export { mergeImplicitBedrockProvider, resolveBedrockConfigApiKey } from "./discovery-shared.js";
|
||||
export {
|
||||
discoverBedrockModels,
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
/**
|
||||
* AWS shared config cache refresh helpers for Bedrock. They nudge the AWS SDK
|
||||
* to re-read profile/SSO config when no static credentials are present.
|
||||
*/
|
||||
type SharedIniFileLoader = {
|
||||
loadSharedConfigFiles(init?: { ignoreCache?: boolean }): Promise<unknown>;
|
||||
};
|
||||
@@ -8,6 +12,7 @@ function hasStaticAwsCredentialEnv(env: NodeJS.ProcessEnv): boolean {
|
||||
return Boolean(env.AWS_ACCESS_KEY_ID && env.AWS_SECRET_ACCESS_KEY);
|
||||
}
|
||||
|
||||
/** Return whether Bedrock should refresh the AWS shared config cache before discovery. */
|
||||
export function shouldRefreshAwsSharedConfigCacheForBedrock(env: NodeJS.ProcessEnv): boolean {
|
||||
if (env.AWS_BEDROCK_SKIP_AUTH === "1" || env.AWS_BEARER_TOKEN_BEDROCK) {
|
||||
return false;
|
||||
@@ -25,6 +30,7 @@ async function loadSharedIniFileLoader(): Promise<SharedIniFileLoader> {
|
||||
return (await import("@smithy/shared-ini-file-loader")) as SharedIniFileLoader;
|
||||
}
|
||||
|
||||
/** Refresh Smithy shared config files when Bedrock needs default-chain credentials. */
|
||||
export async function refreshAwsSharedConfigCacheForBedrock(
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): Promise<void> {
|
||||
@@ -35,6 +41,7 @@ export async function refreshAwsSharedConfigCacheForBedrock(
|
||||
await loader.loadSharedConfigFiles({ ignoreCache: true });
|
||||
}
|
||||
|
||||
/** Override the shared INI loader for Bedrock credential-refresh tests. */
|
||||
export function setAwsSharedIniFileLoaderForTest(
|
||||
loader: SharedIniFileLoader | null | undefined,
|
||||
): void {
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
/**
|
||||
* Stream option extensions and prompt-cache policy for Amazon Bedrock models.
|
||||
* Provider registration and runtime streaming share these contracts.
|
||||
*/
|
||||
import type { StreamOptions, ThinkingBudgets, ThinkingLevel } from "openclaw/plugin-sdk/llm";
|
||||
|
||||
/** How Bedrock thinking output should be displayed to users. */
|
||||
export type BedrockThinkingDisplay = "summarized" | "omitted";
|
||||
|
||||
/** Extra Bedrock-specific stream options accepted by the provider runtime. */
|
||||
export interface BedrockOptions extends StreamOptions {
|
||||
region?: string;
|
||||
profile?: string;
|
||||
@@ -22,6 +28,7 @@ function getModelMatchCandidates(modelId: string, modelName?: string): string[]
|
||||
});
|
||||
}
|
||||
|
||||
/** Return whether a Bedrock model is known to support Anthropic prompt caching. */
|
||||
export function supportsBedrockPromptCaching(modelId: string, modelName?: string): boolean {
|
||||
const candidates = getModelMatchCandidates(modelId, modelName);
|
||||
const hasClaudeRef = candidates.some((s) => s.includes("claude"));
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// Narrow barrel for config compatibility helpers consumed outside the plugin.
|
||||
// Keep this separate from runtime exports so doctor/config code stays lightweight.
|
||||
|
||||
/**
|
||||
* Narrow config compatibility barrel for Amazon Bedrock. Doctor/config code can
|
||||
* import this without loading runtime provider dependencies.
|
||||
*/
|
||||
export { migrateAmazonBedrockLegacyConfig } from "./config-compat.js";
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user