mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-17 03:28:57 +08:00
Compare commits
990 Commits
vincentkoc
...
codex/llm-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fff82599fc | ||
|
|
7b8b268079 | ||
|
|
13f396b395 | ||
|
|
86e9dcfc1b | ||
|
|
79c6158ac6 | ||
|
|
4157bcd024 | ||
|
|
d41c9ad4cb | ||
|
|
089a43f5e8 | ||
|
|
f58e0f5592 | ||
|
|
06832112ee | ||
|
|
0e9b899aee | ||
|
|
f2655e1e92 | ||
|
|
b9e08a6839 | ||
|
|
238c036b0d | ||
|
|
f96ee99bbc | ||
|
|
93a31b69de | ||
|
|
afad0697aa | ||
|
|
d8a1ad0f0d | ||
|
|
1890089f49 | ||
|
|
1040ae56b5 | ||
|
|
2f3bc89f4f | ||
|
|
61a19107e1 | ||
|
|
4ac9024de9 | ||
|
|
7ac23ae7c2 | ||
|
|
5625cf4724 | ||
|
|
3cecbcf8b6 | ||
|
|
d1ef7d64e9 | ||
|
|
25011bdb1e | ||
|
|
0567f111ac | ||
|
|
d9e776eb47 | ||
|
|
9b6859e5db | ||
|
|
2afa556746 | ||
|
|
da2289869d | ||
|
|
0ae3e70a5c | ||
|
|
bde4c7995f | ||
|
|
fbd88e2c8f | ||
|
|
e6c6aaa11b | ||
|
|
80e681a60c | ||
|
|
8193af6d4e | ||
|
|
466510b6d8 | ||
|
|
6802a768cf | ||
|
|
4e265fe7d6 | ||
|
|
3a28bc7d8f | ||
|
|
198ed08a38 | ||
|
|
6538c87673 | ||
|
|
4ebd3d11aa | ||
|
|
50a81c8731 | ||
|
|
c99c4b1e27 | ||
|
|
e17d10f7cd | ||
|
|
21c2ba480a | ||
|
|
79f2173cd2 | ||
|
|
1cbfd53ed1 | ||
|
|
0dda3e66b5 | ||
|
|
3d31ba7830 | ||
|
|
8ac4b09fa4 | ||
|
|
bd444435c9 | ||
|
|
5eea523f39 | ||
|
|
0385553918 | ||
|
|
98fbbebf6a | ||
|
|
a5fa75cdb3 | ||
|
|
d341d68180 | ||
|
|
d1fe30b35f | ||
|
|
fe84354a33 | ||
|
|
c36a493e80 | ||
|
|
ad185dd4a8 | ||
|
|
732e075e92 | ||
|
|
b333eb137b | ||
|
|
100d7b0227 | ||
|
|
b48413e252 | ||
|
|
b9b891b614 | ||
|
|
d1d10007a9 | ||
|
|
77dfa73736 | ||
|
|
8af4628a6d | ||
|
|
c81b4a5389 | ||
|
|
6e723dfd69 | ||
|
|
df79113593 | ||
|
|
0bdd17aef2 | ||
|
|
9282d5d09e | ||
|
|
08a0219b1a | ||
|
|
75f98fe19a | ||
|
|
d949a513c5 | ||
|
|
c245c8b39d | ||
|
|
8c436a470e | ||
|
|
1aab71cf5b | ||
|
|
4d551e6f33 | ||
|
|
02826eaa0c | ||
|
|
ed479f96a1 | ||
|
|
0a065bc6c2 | ||
|
|
5642fb2682 | ||
|
|
645c5bda2c | ||
|
|
2ef28a7a3e | ||
|
|
7b27f8a9ae | ||
|
|
7f0f8dd268 | ||
|
|
937f118d8e | ||
|
|
ff849613a4 | ||
|
|
dc20a7cd89 | ||
|
|
cd2752346c | ||
|
|
5f89897df1 | ||
|
|
2c579b6ac1 | ||
|
|
4ca87fa4b0 | ||
|
|
4c160d2c3a | ||
|
|
bfecc58a62 | ||
|
|
5464ad113e | ||
|
|
0354d49a82 | ||
|
|
67ce726bba | ||
|
|
05603e4e6c | ||
|
|
2c5fd8e0c1 | ||
|
|
e1cae60294 | ||
|
|
1ef7e544e9 | ||
|
|
b9dfb6cc23 | ||
|
|
b85d97f22c | ||
|
|
243dabc186 | ||
|
|
23f618d62d | ||
|
|
edcf3e9d32 | ||
|
|
6aaf0d0f24 | ||
|
|
77fb2589b1 | ||
|
|
112d1d3a7c | ||
|
|
2fbf2c0a47 | ||
|
|
9932d2984c | ||
|
|
873ac8bc79 | ||
|
|
aa3739167c | ||
|
|
6710a2be61 | ||
|
|
04eb17bfab | ||
|
|
e5363b0268 | ||
|
|
a8907d80dd | ||
|
|
2b5fa0931d | ||
|
|
b86bc9de95 | ||
|
|
4e94f3aa02 | ||
|
|
e93412b5ce | ||
|
|
0ffcc308f2 | ||
|
|
bf470b711b | ||
|
|
f842de046c | ||
|
|
e5eda19db2 | ||
|
|
1e1bc24f80 | ||
|
|
2d87bc703f | ||
|
|
2b67a3f76e | ||
|
|
4285eb3539 | ||
|
|
0636c6eafa | ||
|
|
2a02337be2 | ||
|
|
c03b0877d0 | ||
|
|
de0285d8ea | ||
|
|
b8dd6548aa | ||
|
|
33bcf11c3f | ||
|
|
6816c76738 | ||
|
|
0f7cd59824 | ||
|
|
d6c13d9dc0 | ||
|
|
028f3c4d15 | ||
|
|
d1d36da700 | ||
|
|
fa896704d2 | ||
|
|
6ba15aadcc | ||
|
|
4079de21ce | ||
|
|
826c592deb | ||
|
|
92a40d324a | ||
|
|
3de973ffff | ||
|
|
df72ca1ece | ||
|
|
1a9114a169 | ||
|
|
1c81b82f48 | ||
|
|
24dc91c6ef | ||
|
|
e691345774 | ||
|
|
326c660775 | ||
|
|
a2518a16ac | ||
|
|
fb5ab95e03 | ||
|
|
a89cb3e10e | ||
|
|
4c9028439c | ||
|
|
2c35faf437 | ||
|
|
d2ef865073 | ||
|
|
1aae93b1fa | ||
|
|
f253f14b0b | ||
|
|
a8f433d611 | ||
|
|
bf8702973f | ||
|
|
4e706da898 | ||
|
|
1f5f3fc2ef | ||
|
|
c29458d407 | ||
|
|
a4b98f95c2 | ||
|
|
9c12b41c52 | ||
|
|
005b25e9d4 | ||
|
|
6556a40330 | ||
|
|
5c4903d3fd | ||
|
|
7ba8dd112f | ||
|
|
a34944c918 | ||
|
|
f8f9e06b58 | ||
|
|
0bfaa36126 | ||
|
|
9350cb19dd | ||
|
|
9e556f75f5 | ||
|
|
889011c08c | ||
|
|
abaa9107c5 | ||
|
|
2f21eeb3cb | ||
|
|
1777b99ccc | ||
|
|
56066dccb0 | ||
|
|
0a90b07f8d | ||
|
|
28b888cbcd | ||
|
|
cd5c2f4cb2 | ||
|
|
3cc83cb81e | ||
|
|
a41840f717 | ||
|
|
53dcafbec3 | ||
|
|
206d1be082 | ||
|
|
27d4fdf3bb | ||
|
|
6b9b32a160 | ||
|
|
682f4d1ca3 | ||
|
|
870f260772 | ||
|
|
25e6cd38b6 | ||
|
|
fa34cb887d | ||
|
|
5b2c5ee2bc | ||
|
|
055632460d | ||
|
|
889bb8a78a | ||
|
|
1313767825 | ||
|
|
b942dacf48 | ||
|
|
44521d6b20 | ||
|
|
6f060d7e6c | ||
|
|
841b1a59d7 | ||
|
|
01ae160108 | ||
|
|
d8b95d2315 | ||
|
|
fa73f5aeb5 | ||
|
|
9e8b9aba1f | ||
|
|
bb803a42ac | ||
|
|
09de192b77 | ||
|
|
8e98019b6a | ||
|
|
fb0d04c834 | ||
|
|
1c6676cd57 | ||
|
|
ed7269518f | ||
|
|
b5c38b1095 | ||
|
|
8165db758b | ||
|
|
b3ae50c71c | ||
|
|
c3386d34d2 | ||
|
|
9df3e9b617 | ||
|
|
4c36436fb4 | ||
|
|
d3fc6c0cc7 | ||
|
|
d073ec42cd | ||
|
|
2d3bcbfe08 | ||
|
|
9a455a8c08 | ||
|
|
50cac39657 | ||
|
|
53df7ff86d | ||
|
|
f2de673130 | ||
|
|
ab62f3b9f4 | ||
|
|
ab1da26f4d | ||
|
|
7dabcf287d | ||
|
|
951f3f992b | ||
|
|
da948a8073 | ||
|
|
cac1c62208 | ||
|
|
28ab5061bf | ||
|
|
60104de428 | ||
|
|
0a0ca804aa | ||
|
|
c9ba985839 | ||
|
|
bb365dba73 | ||
|
|
144b95ffce | ||
|
|
b1c03715fb | ||
|
|
1c08455848 | ||
|
|
5ce3eb3ff3 | ||
|
|
a32c7e16d2 | ||
|
|
df284fec27 | ||
|
|
60d4c5a30b | ||
|
|
d95dc50e0a | ||
|
|
dbc367e50a | ||
|
|
05634eed16 | ||
|
|
4b5e801d1b | ||
|
|
11720510f5 | ||
|
|
a14ad01d66 | ||
|
|
4e912bffd8 | ||
|
|
79f7dbfd6e | ||
|
|
ab5aec137c | ||
|
|
ffe24955c8 | ||
|
|
f118191182 | ||
|
|
0e4c072f37 | ||
|
|
e99963100d | ||
|
|
bd21442f7e | ||
|
|
af63b72901 | ||
|
|
e7422716bb | ||
|
|
2f65ae1b80 | ||
|
|
90a0d50ae9 | ||
|
|
dcdfed995a | ||
|
|
f23a069d37 | ||
|
|
681d16a892 | ||
|
|
77f145f1db | ||
|
|
6981922254 | ||
|
|
45bfe3f44b | ||
|
|
7d5a90e589 | ||
|
|
ba09092a44 | ||
|
|
b31b681088 | ||
|
|
5a2a4abc12 | ||
|
|
3d3f292f66 | ||
|
|
dd7b5dc46f | ||
|
|
de564689da | ||
|
|
025bdc7e8f | ||
|
|
464f3da53f | ||
|
|
8124253cdf | ||
|
|
ff19ae1768 | ||
|
|
0f56b16d47 | ||
|
|
4b2aec622b | ||
|
|
0d80897476 | ||
|
|
3983928958 | ||
|
|
e4825a0f93 | ||
|
|
272d6ed24b | ||
|
|
ccf16cd889 | ||
|
|
6d9bf6de93 | ||
|
|
bdf2c265a7 | ||
|
|
6636ca87f4 | ||
|
|
2145eb5908 | ||
|
|
7b61b025ff | ||
|
|
829ea70519 | ||
|
|
4b125762f6 | ||
|
|
4d8106eece | ||
|
|
a724bbce1a | ||
|
|
ea15819ecf | ||
|
|
8139f83175 | ||
|
|
39a8dab0da | ||
|
|
c94beb03b2 | ||
|
|
0aff1c7630 | ||
|
|
9f8cf7f71a | ||
|
|
647fb9cc3e | ||
|
|
58313fcd05 | ||
|
|
e3d021163c | ||
|
|
31d739fda2 | ||
|
|
c672635413 | ||
|
|
9e29511316 | ||
|
|
4a95e6529f | ||
|
|
6646ca61cc | ||
|
|
63997aec23 | ||
|
|
141d73ddf4 | ||
|
|
58c26ad706 | ||
|
|
ef53926542 | ||
|
|
7866655176 | ||
|
|
9e087f66be | ||
|
|
9b7aafa141 | ||
|
|
23a3211c29 | ||
|
|
c1733d700d | ||
|
|
610d836151 | ||
|
|
8cfcce0849 | ||
|
|
789730d1a3 | ||
|
|
50c8569786 | ||
|
|
c4b866855a | ||
|
|
253ec7452f | ||
|
|
64c1fc098a | ||
|
|
37df574da0 | ||
|
|
59eaeaccfe | ||
|
|
7c24aab954 | ||
|
|
060654e947 | ||
|
|
48a9aa152c | ||
|
|
580e00d91b | ||
|
|
3be44b1044 | ||
|
|
5a5a66d63d | ||
|
|
f9408e57d2 | ||
|
|
c4323db30f | ||
|
|
b7dc23b403 | ||
|
|
fb4b6eef03 | ||
|
|
a24325f40c | ||
|
|
8ab2d886eb | ||
|
|
2cfccf59c7 | ||
|
|
355051f401 | ||
|
|
5311d48c66 | ||
|
|
477cea7709 | ||
|
|
d49c1688f7 | ||
|
|
6372062be4 | ||
|
|
97c481120f | ||
|
|
23d700b090 | ||
|
|
909ec6b416 | ||
|
|
17143ed878 | ||
|
|
c21654e1b9 | ||
|
|
1a3bde81d8 | ||
|
|
588c8be6ff | ||
|
|
2c073e7bcb | ||
|
|
d988e39fc7 | ||
|
|
7efa79121a | ||
|
|
bf22e9461e | ||
|
|
444e3eb9e3 | ||
|
|
790747478e | ||
|
|
85c5ec8065 | ||
|
|
167a6ebed9 | ||
|
|
4fd17021f2 | ||
|
|
3aa76a8ce7 | ||
|
|
7e1bc4677f | ||
|
|
5f0f69b2c7 | ||
|
|
2ef7b13962 | ||
|
|
03b405659b | ||
|
|
0c070ccd53 | ||
|
|
1038990bdd | ||
|
|
9c086f26a0 | ||
|
|
34460f24b8 | ||
|
|
7c3efaeccf | ||
|
|
61a7d856e7 | ||
|
|
d1df3f37a6 | ||
|
|
eef0f5bfbc | ||
|
|
74cc748ff7 | ||
|
|
604c2636b9 | ||
|
|
5f0c466146 | ||
|
|
2f9e2f500f | ||
|
|
47a78a03a3 | ||
|
|
dc3cb9349a | ||
|
|
b8861b4815 | ||
|
|
a53de5ad51 | ||
|
|
91f055c10e | ||
|
|
40f1aad019 | ||
|
|
8a9dee9ac8 | ||
|
|
a3f09d519d | ||
|
|
2b980bfcee | ||
|
|
00b7308396 | ||
|
|
63c5932e84 | ||
|
|
94a48912de | ||
|
|
9b22bd41d8 | ||
|
|
f2107a53cb | ||
|
|
df76e0f44b | ||
|
|
094a0cc412 | ||
|
|
ebee4e2210 | ||
|
|
e1b0e74e78 | ||
|
|
795f1f438b | ||
|
|
4f6955fb11 | ||
|
|
f036ed27f4 | ||
|
|
7cd0acf8af | ||
|
|
f84a41dcb8 | ||
|
|
1399ca5fcb | ||
|
|
1561c6a71c | ||
|
|
8448f48cc5 | ||
|
|
3e8bf845cb | ||
|
|
a413da9cca | ||
|
|
4234d9b42c | ||
|
|
59cd98068f | ||
|
|
f404ff32d5 | ||
|
|
6b6942552d | ||
|
|
7303253427 | ||
|
|
6101c023bb | ||
|
|
6bec21bf00 | ||
|
|
6bf07b5075 | ||
|
|
990d0d7261 | ||
|
|
0ff82497e9 | ||
|
|
3a456678ee | ||
|
|
916db21fe5 | ||
|
|
99c7750c2d | ||
|
|
ce486292a1 | ||
|
|
f9588da3e0 | ||
|
|
527a1919ea | ||
|
|
85e610e4e7 | ||
|
|
774b351982 | ||
|
|
4db3fed299 | ||
|
|
2971c52343 | ||
|
|
bc36ed8e1e | ||
|
|
d46f3bd739 | ||
|
|
e510132f3c | ||
|
|
8c8b0ab224 | ||
|
|
b531af82d5 | ||
|
|
2847ad1f8f | ||
|
|
1373821470 | ||
|
|
93d829b7f6 | ||
|
|
535475e4cb | ||
|
|
ec1b80809d | ||
|
|
9648e7fecb | ||
|
|
449127b474 | ||
|
|
c0e4721712 | ||
|
|
7d90dff8fa | ||
|
|
9c1e9c5263 | ||
|
|
be6716c7aa | ||
|
|
0956de7316 | ||
|
|
68f3e537d3 | ||
|
|
bb13dd0c01 | ||
|
|
58f6362921 | ||
|
|
ef0812beff | ||
|
|
38616c7c95 | ||
|
|
528edce5b9 | ||
|
|
e4287e0938 | ||
|
|
168fa9d433 | ||
|
|
1eb810a5e3 | ||
|
|
9053f551cb | ||
|
|
1843248c69 | ||
|
|
9c047c5423 | ||
|
|
7bb36efd7b | ||
|
|
1b9704df4d | ||
|
|
5699b3dd27 | ||
|
|
d698d8c5a5 | ||
|
|
f8f6ae4673 | ||
|
|
5747700b3c | ||
|
|
201964ce6c | ||
|
|
e5c03ebea7 | ||
|
|
282e336243 | ||
|
|
c08d556ae4 | ||
|
|
88139c4271 | ||
|
|
d08d43fb1a | ||
|
|
276803095d | ||
|
|
e56e4923bd | ||
|
|
52ad686ab5 | ||
|
|
214c7a481c | ||
|
|
769332c1a7 | ||
|
|
e1ca5d9cc4 | ||
|
|
1ff10690e7 | ||
|
|
e184cd97cc | ||
|
|
d28cb8d821 | ||
|
|
cc35627c8f | ||
|
|
ff0481ad65 | ||
|
|
9887311de3 | ||
|
|
315cee96b9 | ||
|
|
228448e6b3 | ||
|
|
6f795fd60e | ||
|
|
be4fdb9222 | ||
|
|
f8d03022cf | ||
|
|
5fb7a1363f | ||
|
|
026d8ea534 | ||
|
|
13505c7392 | ||
|
|
e5919bc524 | ||
|
|
73ca53ee02 | ||
|
|
3dec814fda | ||
|
|
0d776c87c3 | ||
|
|
42c8c3c983 | ||
|
|
c1e5697889 | ||
|
|
f6868b7e42 | ||
|
|
80a2af1d65 | ||
|
|
57204b4fa9 | ||
|
|
38a6415a70 | ||
|
|
e32976f8cf | ||
|
|
2ed5ad36ae | ||
|
|
43838b1b14 | ||
|
|
520d753b27 | ||
|
|
143530407d | ||
|
|
03c6946125 | ||
|
|
4f5e3e1799 | ||
|
|
01c89a7985 | ||
|
|
54419a826b | ||
|
|
45510084cd | ||
|
|
c974adf10d | ||
|
|
e793e3873f | ||
|
|
da9e0b658d | ||
|
|
4b001c7934 | ||
|
|
79078f6a70 | ||
|
|
3486bff7d5 | ||
|
|
55c52b9094 | ||
|
|
60ee5f661f | ||
|
|
c9de17fc20 | ||
|
|
6a57ede661 | ||
|
|
f1df31eeef | ||
|
|
a6bee25247 | ||
|
|
2280fa0022 | ||
|
|
c601dda389 | ||
|
|
618d35f933 | ||
|
|
c1ef5748eb | ||
|
|
14d6b762fb | ||
|
|
efaa4dc5b3 | ||
|
|
be2e6ca0f6 | ||
|
|
2d100157bd | ||
|
|
aa2d5aaa0c | ||
|
|
c79ade10e6 | ||
|
|
cc88b4a72d | ||
|
|
1116ae9766 | ||
|
|
00b57145ff | ||
|
|
78a4d12e9a | ||
|
|
5dd2245094 | ||
|
|
f2bd76cd1a | ||
|
|
357ce71988 | ||
|
|
64c69c3fc9 | ||
|
|
61ccc5bede | ||
|
|
ac4aead8a7 | ||
|
|
0bc9c065f2 | ||
|
|
049bb37c62 | ||
|
|
dd9fce1686 | ||
|
|
6c866b8543 | ||
|
|
0bf11c1d69 | ||
|
|
223ae42c79 | ||
|
|
2bbf33a9ec | ||
|
|
dbe77d0425 | ||
|
|
d2445b5fcd | ||
|
|
6cbff9e7d3 | ||
|
|
ec89357547 | ||
|
|
a1a8b74e9a | ||
|
|
626e301502 | ||
|
|
e36f16e750 | ||
|
|
423f1e994e | ||
|
|
f3da292097 | ||
|
|
21bc5a90ec | ||
|
|
e820c255bc | ||
|
|
7e9c46d7dd | ||
|
|
503932919f | ||
|
|
1dc3104dbf | ||
|
|
23deb3da98 | ||
|
|
7ab074631b | ||
|
|
5ce2ed3bd2 | ||
|
|
63d82a6299 | ||
|
|
06ae5e9d21 | ||
|
|
b0dd757ec8 | ||
|
|
10660fe47d | ||
|
|
966b8656d2 | ||
|
|
ed06d21013 | ||
|
|
dd85ff4da7 | ||
|
|
d20363bcc9 | ||
|
|
7f042758b0 | ||
|
|
880bc969f9 | ||
|
|
da34f81ce2 | ||
|
|
50c3321d2e | ||
|
|
7fa3825e80 | ||
|
|
21f5675f03 | ||
|
|
68d2bd27c9 | ||
|
|
dde89d2a83 | ||
|
|
ad7924b0ac | ||
|
|
06459ca0df | ||
|
|
38bc364aed | ||
|
|
5572e6965a | ||
|
|
87b9a063ce | ||
|
|
0cfc80b81c | ||
|
|
73703d977c | ||
|
|
631f6f47cf | ||
|
|
4bba2888e7 | ||
|
|
6d6825ea18 | ||
|
|
9183081bf1 | ||
|
|
529272d338 | ||
|
|
70da383a61 | ||
|
|
9ebe38b6e3 | ||
|
|
afc0172cb1 | ||
|
|
f4fa84aea7 | ||
|
|
45cb02b1dd | ||
|
|
08d120e706 | ||
|
|
39183746ba | ||
|
|
0a6140acfa | ||
|
|
a20b64cd92 | ||
|
|
8357372cc7 | ||
|
|
6a27db0cd7 | ||
|
|
233ef31190 | ||
|
|
4ae71485e9 | ||
|
|
c51842660f | ||
|
|
78869f1517 | ||
|
|
5ddbba1c70 | ||
|
|
387d9fa7c4 | ||
|
|
4fd75e5fc8 | ||
|
|
81ef52a81e | ||
|
|
7fc134d74e | ||
|
|
9c48321176 | ||
|
|
a0e7e3c3cd | ||
|
|
b058077b16 | ||
|
|
a3474dda33 | ||
|
|
4f7ee60a8f | ||
|
|
6d6e08b147 | ||
|
|
7758873d7e | ||
|
|
c3571d982d | ||
|
|
31a8225951 | ||
|
|
a8853d23ef | ||
|
|
3cc1c7ba83 | ||
|
|
ba79d90313 | ||
|
|
75b8117f83 | ||
|
|
f90d432de3 | ||
|
|
095a9f6e1d | ||
|
|
71a79bdf5c | ||
|
|
c081dc52b7 | ||
|
|
e064c1198e | ||
|
|
c64f6adc83 | ||
|
|
3566e88c08 | ||
|
|
3e010e280a | ||
|
|
14907d3de0 | ||
|
|
57f1ab1fca | ||
|
|
5f5b409fe9 | ||
|
|
5602973b5d | ||
|
|
622f13253b | ||
|
|
a71c61122d | ||
|
|
2497b8147e | ||
|
|
77d6274624 | ||
|
|
03f50365d7 | ||
|
|
763eff8b32 | ||
|
|
2182137bde | ||
|
|
f6d3aaa442 | ||
|
|
7df0ced8ac | ||
|
|
5a763ac57b | ||
|
|
683be73d54 | ||
|
|
fe4368cbca | ||
|
|
1ffe8fde84 | ||
|
|
ed248c76c7 | ||
|
|
85781353ec | ||
|
|
7c2c20a62f | ||
|
|
3aa4199ef0 | ||
|
|
76500c7a78 | ||
|
|
6da9ba3267 | ||
|
|
662031a88e | ||
|
|
ad05cd9ab2 | ||
|
|
029f5d6427 | ||
|
|
b230e524a5 | ||
|
|
1c0db5b8e4 | ||
|
|
e88c6d8486 | ||
|
|
9c80d717bc | ||
|
|
7959be4336 | ||
|
|
6805a80da2 | ||
|
|
8a10903cf7 | ||
|
|
e554eee541 | ||
|
|
6c1433a3c0 | ||
|
|
0a93e22b37 | ||
|
|
4194bba575 | ||
|
|
8b2f0cbb6c | ||
|
|
02df22a495 | ||
|
|
0f013575f8 | ||
|
|
750ce393bc | ||
|
|
94c27f34a1 | ||
|
|
4863b651c6 | ||
|
|
6ba4d0ddc3 | ||
|
|
313e5bb58b | ||
|
|
a53030a7f2 | ||
|
|
10ef58dd69 | ||
|
|
2ab25babce | ||
|
|
04985dab23 | ||
|
|
eeb140b4f0 | ||
|
|
abce640772 | ||
|
|
2de28379dd | ||
|
|
412811ec19 | ||
|
|
df3a19051d | ||
|
|
546e4d940a | ||
|
|
09df232f39 | ||
|
|
7e2658908d | ||
|
|
97a7dcf48e | ||
|
|
2c3c48fd8d | ||
|
|
4649f82b77 | ||
|
|
c28a52263b | ||
|
|
8a226fffb4 | ||
|
|
13894ec5aa | ||
|
|
d352be8e99 | ||
|
|
ce1d95454f | ||
|
|
771fbeae79 | ||
|
|
f8bcfb9d73 | ||
|
|
1f1a93a1dc | ||
|
|
96ed010a37 | ||
|
|
1b234b910b | ||
|
|
541e697554 | ||
|
|
4337b1eba5 | ||
|
|
64e412e57e | ||
|
|
ac66d383e7 | ||
|
|
e2b8ef369d | ||
|
|
7178a0d3cb | ||
|
|
0b055303f5 | ||
|
|
7deb543624 | ||
|
|
3e360ec8cb | ||
|
|
a41be2585f | ||
|
|
56e23a887f | ||
|
|
3009e689bc | ||
|
|
5f78057ffa | ||
|
|
1b31ede435 | ||
|
|
55253e2a9d | ||
|
|
8ad8069854 | ||
|
|
80bef826f8 | ||
|
|
7d4ccee717 | ||
|
|
841025da66 | ||
|
|
77566a1448 | ||
|
|
c186176ca3 | ||
|
|
ad18866bcc | ||
|
|
467dae53cf | ||
|
|
e5282e6bda | ||
|
|
b7f99a57bf | ||
|
|
c08f2aa21a | ||
|
|
9fc6c1929a | ||
|
|
e78b51baea | ||
|
|
55f6d2d1ad | ||
|
|
092afc850d | ||
|
|
4c8853122a | ||
|
|
5e4851ae2b | ||
|
|
d6aa9b516e | ||
|
|
ccba943738 | ||
|
|
8b438a308b | ||
|
|
fba394c56b | ||
|
|
6a8f5bc12f | ||
|
|
fdfa98cda8 | ||
|
|
d61c08efbb | ||
|
|
6e65066616 | ||
|
|
8cd1bdd345 | ||
|
|
1cf544ffbc | ||
|
|
296083a49a | ||
|
|
92700940d9 | ||
|
|
e1f759f4f1 | ||
|
|
5336c4e945 | ||
|
|
303f690dd9 | ||
|
|
2ee20a6072 | ||
|
|
d68645d47f | ||
|
|
898d6840dc | ||
|
|
1447e2e384 | ||
|
|
3832f938fd | ||
|
|
abb21d9163 | ||
|
|
d572188f61 | ||
|
|
65f05d7c09 | ||
|
|
a8970963cd | ||
|
|
70aa9204c0 | ||
|
|
79a8905fa4 | ||
|
|
4aae0d4c9d | ||
|
|
429144d9f1 | ||
|
|
5cd206f780 | ||
|
|
d896d8e0cd | ||
|
|
2a85fa7db1 | ||
|
|
6f5369c7e8 | ||
|
|
43c156e43b | ||
|
|
c9423dce1e | ||
|
|
c06101b8ad | ||
|
|
30c31d4efd | ||
|
|
ff2e864c98 | ||
|
|
9ee0fb52e9 | ||
|
|
776e5d8a08 | ||
|
|
77b1f240fd | ||
|
|
09e8d1e96f | ||
|
|
f49fc633ac | ||
|
|
4c8678c0b4 | ||
|
|
7e74adef91 | ||
|
|
94a01c9789 | ||
|
|
1aabce78e7 | ||
|
|
e575f419a5 | ||
|
|
7cc5789202 | ||
|
|
a73d6620b3 | ||
|
|
f11589b311 | ||
|
|
7a09255361 | ||
|
|
7c2863d401 | ||
|
|
83ddb0fb4c | ||
|
|
ced20e7997 | ||
|
|
3a2c24e598 | ||
|
|
0ed64f124d | ||
|
|
78f24dcaa2 | ||
|
|
4f8c066680 | ||
|
|
8fe08df2eb | ||
|
|
74d0c39b32 | ||
|
|
49251def61 | ||
|
|
67b886b725 | ||
|
|
045a879acf | ||
|
|
a6eda07316 | ||
|
|
209f1a08d7 | ||
|
|
bbf3b4acf2 | ||
|
|
b3025e6d8e | ||
|
|
7964563299 | ||
|
|
e90c1d9add | ||
|
|
320b4bcb07 | ||
|
|
cec10703dc | ||
|
|
99c501a9a7 | ||
|
|
3c62ab5c89 | ||
|
|
79a67a5e08 | ||
|
|
95b761a2e1 | ||
|
|
947b548870 | ||
|
|
6644783052 | ||
|
|
36f0f216ce | ||
|
|
e3ab0e174c | ||
|
|
0ca1b18517 | ||
|
|
e7eb410dd1 | ||
|
|
7dab66c89e | ||
|
|
182a00cc49 | ||
|
|
62de7e02ea | ||
|
|
25535b571a | ||
|
|
a9a9cf4257 | ||
|
|
3fe3a53dd9 | ||
|
|
85b7bc7edf | ||
|
|
5ca26bcae0 | ||
|
|
c59e2dde47 | ||
|
|
00ef214d59 | ||
|
|
edab939f4d | ||
|
|
3c6a49b27e | ||
|
|
476d948732 | ||
|
|
10cd276641 | ||
|
|
d7ab1a6c7c | ||
|
|
a8367bb0ec | ||
|
|
9b73673313 | ||
|
|
0f502726e1 | ||
|
|
a8878be0fd | ||
|
|
d410debd01 | ||
|
|
5ece9afa8b | ||
|
|
7cdd8a84a6 | ||
|
|
2054cb9431 | ||
|
|
ae60094fb5 | ||
|
|
f5ef936615 | ||
|
|
9df7e8bec4 | ||
|
|
acf7e83ac4 | ||
|
|
c5d61b9677 | ||
|
|
910d039ea7 | ||
|
|
6043e733a6 | ||
|
|
3105a1284a | ||
|
|
fb47777d38 | ||
|
|
623ba14031 | ||
|
|
3838ef9b2a | ||
|
|
4fc3492da5 | ||
|
|
13090da3ac | ||
|
|
4ae80407a6 | ||
|
|
c01515672f | ||
|
|
bd67f33364 | ||
|
|
c7137270d1 | ||
|
|
d163278e9c | ||
|
|
d5b12f505c | ||
|
|
a608d09552 | ||
|
|
4ab016a9bd | ||
|
|
130b575c21 | ||
|
|
7b2a7da549 | ||
|
|
853d8c0d8e | ||
|
|
81d3c6c909 | ||
|
|
ed82c7e57b | ||
|
|
f0f934556e | ||
|
|
fa62231afc | ||
|
|
aa97368f7d | ||
|
|
ddd34b6cc3 | ||
|
|
c4b18ab3c9 | ||
|
|
d47fc009de | ||
|
|
5f42389d8d | ||
|
|
a2119efe1c | ||
|
|
4cb46f223c | ||
|
|
ebfd32efc3 | ||
|
|
0a6f22a694 | ||
|
|
465567b1eb | ||
|
|
2852eab323 | ||
|
|
ecaafb6a4f | ||
|
|
ff558862f0 | ||
|
|
7bea559166 | ||
|
|
3963408871 | ||
|
|
9cd9c7a488 | ||
|
|
2580b81bd2 | ||
|
|
f9e185887f | ||
|
|
2acbea0da7 | ||
|
|
55cbfb6e6a | ||
|
|
0b58a1cc13 | ||
|
|
ad97c581e2 | ||
|
|
680eff63fb | ||
|
|
98f6ec50aa | ||
|
|
c780b6a6ab | ||
|
|
44304ba24a | ||
|
|
0311ff05d7 | ||
|
|
304703f165 | ||
|
|
e627a5069f | ||
|
|
abe7ea4373 | ||
|
|
3e8bc9f16a | ||
|
|
69c12c2b11 | ||
|
|
d937b61fb3 | ||
|
|
823039c000 | ||
|
|
f6f0045e0f | ||
|
|
5c120cb36c | ||
|
|
98877dc413 | ||
|
|
0277aa0159 | ||
|
|
c7d31bae8a | ||
|
|
92bea9704e | ||
|
|
69a85325c3 | ||
|
|
e77aa26af6 | ||
|
|
6ed8ad1844 | ||
|
|
833a19f756 | ||
|
|
d607d2e6d4 | ||
|
|
52c90524c9 | ||
|
|
eb51ba5c1d | ||
|
|
c66b994965 | ||
|
|
3a08f70151 | ||
|
|
0feb939cb3 | ||
|
|
8f41001edf | ||
|
|
576ea84195 | ||
|
|
14b7187c33 | ||
|
|
38f61564ac | ||
|
|
2d048980af | ||
|
|
bdc91130fe | ||
|
|
474368d746 | ||
|
|
2eb2b0995d | ||
|
|
04081d349e | ||
|
|
c1846000dd | ||
|
|
f6d8a1129d | ||
|
|
59bcc9ee46 | ||
|
|
d5a7880de2 | ||
|
|
1e54a4a6a3 | ||
|
|
8b6806ab5c | ||
|
|
298832d170 | ||
|
|
6fd11f5496 | ||
|
|
f889219955 | ||
|
|
59d355bc48 | ||
|
|
f327408fad | ||
|
|
e50545d767 | ||
|
|
b1243bf15b | ||
|
|
82f587fc82 | ||
|
|
5e093639d7 | ||
|
|
f3f0bdcb07 | ||
|
|
7018412102 | ||
|
|
12f4dd9a05 | ||
|
|
df2a6b1672 | ||
|
|
082383b40d | ||
|
|
cc6f03ec6c | ||
|
|
553cbccd40 | ||
|
|
f70d2624dc | ||
|
|
1c2a609d03 | ||
|
|
28de97356d | ||
|
|
a69f6190ab | ||
|
|
99a4594bde | ||
|
|
0c2ae71366 | ||
|
|
7a6be3d531 | ||
|
|
3d8c29cc53 | ||
|
|
922ce15c65 | ||
|
|
09f607fa82 | ||
|
|
5287ae3c06 | ||
|
|
656848dcd7 | ||
|
|
07d71d2b27 | ||
|
|
1beea52d8d | ||
|
|
0a2f95916b | ||
|
|
b8bb8510a2 | ||
|
|
33edb57e74 | ||
|
|
7781f62d33 | ||
|
|
cb4a298961 | ||
|
|
7e8f5ca71b | ||
|
|
093e51f2b3 | ||
|
|
c4a5fd8465 | ||
|
|
0f43dc4680 | ||
|
|
53ccc78c63 | ||
|
|
350b42d342 | ||
|
|
0218045818 | ||
|
|
522dda1971 | ||
|
|
270ba54c47 | ||
|
|
7d5e26b4a2 | ||
|
|
31e6cb0df6 | ||
|
|
d9fb50e777 | ||
|
|
01456f95bc | ||
|
|
a33caab280 | ||
|
|
ca2f046668 | ||
|
|
1f50fed3b2 | ||
|
|
92d5307074 |
62
.agents/skills/parallels-discord-roundtrip/SKILL.md
Normal file
62
.agents/skills/parallels-discord-roundtrip/SKILL.md
Normal file
@@ -0,0 +1,62 @@
|
||||
---
|
||||
name: parallels-discord-roundtrip
|
||||
description: Run the macOS Parallels smoke harness with Discord end-to-end roundtrip verification, including guest send, host verification, host reply, and guest readback.
|
||||
---
|
||||
|
||||
# Parallels Discord Roundtrip
|
||||
|
||||
Use when macOS Parallels smoke must prove Discord two-way delivery end to end.
|
||||
|
||||
## Goal
|
||||
|
||||
Cover:
|
||||
|
||||
- install on fresh macOS snapshot
|
||||
- onboard + gateway health
|
||||
- guest `message send` to Discord
|
||||
- host sees that message on Discord
|
||||
- host posts a new Discord message
|
||||
- guest `message read` sees that new message
|
||||
|
||||
## Inputs
|
||||
|
||||
- host env var with Discord bot token
|
||||
- Discord guild ID
|
||||
- Discord channel ID
|
||||
- `OPENAI_API_KEY`
|
||||
|
||||
## Preferred run
|
||||
|
||||
```bash
|
||||
export OPENCLAW_PARALLELS_DISCORD_TOKEN="$(
|
||||
ssh peters-mac-studio-1 'jq -r ".channels.discord.token" ~/.openclaw/openclaw.json' | tr -d '\n'
|
||||
)"
|
||||
|
||||
pnpm test:parallels:macos \
|
||||
--discord-token-env OPENCLAW_PARALLELS_DISCORD_TOKEN \
|
||||
--discord-guild-id 1456350064065904867 \
|
||||
--discord-channel-id 1456744319972282449 \
|
||||
--json
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- Snapshot target: closest to `macOS 26.3.1 fresh`.
|
||||
- Snapshot resolver now prefers matching `*-poweroff*` clones when the base hint also matches. That lets the harness reuse disk-only recovery snapshots without passing a longer hint.
|
||||
- If Windows/Linux snapshot restore logs show `PET_QUESTION_SNAPSHOT_STATE_INCOMPATIBLE_CPU`, drop the suspended state once, create a `*-poweroff*` replacement snapshot, and rerun. The smoke scripts now auto-start restored power-off snapshots.
|
||||
- Harness configures Discord inside the guest; no checked-in token/config.
|
||||
- Use the `openclaw` wrapper for guest `message send/read`; `node openclaw.mjs message ...` does not expose the lazy message subcommands the same way.
|
||||
- Write `channels.discord.guilds` in one JSON object (`--strict-json`), not dotted `config set channels.discord.guilds.<snowflake>...` paths; numeric snowflakes get treated like array indexes.
|
||||
- Avoid `prlctl enter` / expect for long Discord setup scripts; it line-wraps/corrupts long commands. Use `prlctl exec --current-user /bin/sh -lc ...` for the Discord config phase.
|
||||
- Full 3-OS sweeps: the shared build lock is safe in parallel, but snapshot restore is still a Parallels bottleneck. Prefer serialized Windows/Linux restore-heavy reruns if the host is already under load.
|
||||
- Harness cleanup deletes the temporary Discord smoke messages at exit.
|
||||
- Per-phase logs: `/tmp/openclaw-parallels-smoke.*`
|
||||
- Machine summary: pass `--json`
|
||||
- If roundtrip flakes, inspect `fresh.discord-roundtrip.log` and `discord-last-readback.json` in the run dir first.
|
||||
|
||||
## Pass criteria
|
||||
|
||||
- fresh lane or upgrade lane requested passes
|
||||
- summary reports `discord=pass` for that lane
|
||||
- guest outbound nonce appears in channel history
|
||||
- host inbound nonce appears in `openclaw message read` output
|
||||
4
.github/labeler.yml
vendored
4
.github/labeler.yml
vendored
@@ -314,3 +314,7 @@
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "extensions/xiaomi/**"
|
||||
"extensions: fal":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "extensions/fal/**"
|
||||
|
||||
212
.github/workflows/ci.yml
vendored
212
.github/workflows/ci.yml
vendored
@@ -78,6 +78,50 @@ jobs:
|
||||
|
||||
node scripts/ci-changed-scope.mjs --base "$BASE" --head HEAD
|
||||
|
||||
changed-extensions:
|
||||
needs: [docs-scope, changed-scope]
|
||||
if: needs.docs-scope.outputs.docs_only != 'true' && needs.changed-scope.outputs.run_node == 'true'
|
||||
runs-on: blacksmith-16vcpu-ubuntu-2404
|
||||
outputs:
|
||||
has_changed_extensions: ${{ steps.changed.outputs.has_changed_extensions }}
|
||||
changed_extensions_matrix: ${{ steps.changed.outputs.changed_extensions_matrix }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 1
|
||||
fetch-tags: false
|
||||
submodules: false
|
||||
|
||||
- name: Ensure changed-extensions base commit
|
||||
uses: ./.github/actions/ensure-base-commit
|
||||
with:
|
||||
base-sha: ${{ github.event_name == 'push' && github.event.before || github.event.pull_request.base.sha }}
|
||||
fetch-ref: ${{ github.event_name == 'push' && github.ref_name || github.event.pull_request.base.ref }}
|
||||
|
||||
- name: Setup Node environment
|
||||
uses: ./.github/actions/setup-node-env
|
||||
with:
|
||||
install-bun: "false"
|
||||
install-deps: "false"
|
||||
use-sticky-disk: "false"
|
||||
|
||||
- name: Detect changed extensions
|
||||
id: changed
|
||||
env:
|
||||
BASE_SHA: ${{ github.event_name == 'push' && github.event.before || github.event.pull_request.base.sha }}
|
||||
run: |
|
||||
node --input-type=module <<'EOF'
|
||||
import { appendFileSync } from "node:fs";
|
||||
import { listChangedExtensionIds } from "./scripts/test-extension.mjs";
|
||||
|
||||
const extensionIds = listChangedExtensionIds({ base: process.env.BASE_SHA, head: "HEAD" });
|
||||
const matrix = JSON.stringify({ include: extensionIds.map((extension) => ({ extension })) });
|
||||
|
||||
appendFileSync(process.env.GITHUB_OUTPUT, `has_changed_extensions=${extensionIds.length > 0}\n`, "utf8");
|
||||
appendFileSync(process.env.GITHUB_OUTPUT, `changed_extensions_matrix=${matrix}\n`, "utf8");
|
||||
EOF
|
||||
|
||||
# Build dist once for Node-relevant changes and share it with downstream jobs.
|
||||
build-artifacts:
|
||||
needs: [docs-scope, changed-scope]
|
||||
@@ -162,6 +206,9 @@ jobs:
|
||||
- runtime: node
|
||||
task: channels
|
||||
command: pnpm test:channels
|
||||
- runtime: node
|
||||
task: contracts
|
||||
command: pnpm test:contracts
|
||||
- runtime: node
|
||||
task: protocol
|
||||
command: pnpm protocol:check
|
||||
@@ -205,6 +252,31 @@ jobs:
|
||||
if: matrix.runtime != 'bun' || github.event_name != 'pull_request'
|
||||
run: ${{ matrix.command }}
|
||||
|
||||
extension-fast:
|
||||
name: "extension-fast (${{ matrix.extension }})"
|
||||
needs: [docs-scope, changed-scope, changed-extensions]
|
||||
if: needs.docs-scope.outputs.docs_only != 'true' && needs.changed-scope.outputs.run_node == 'true' && needs.changed-extensions.outputs.has_changed_extensions == 'true'
|
||||
runs-on: blacksmith-16vcpu-ubuntu-2404
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix: ${{ fromJson(needs.changed-extensions.outputs.changed_extensions_matrix) }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: false
|
||||
|
||||
- name: Setup Node environment
|
||||
uses: ./.github/actions/setup-node-env
|
||||
with:
|
||||
install-bun: "false"
|
||||
use-sticky-disk: "false"
|
||||
|
||||
- name: Run changed extension tests
|
||||
env:
|
||||
OPENCLAW_CHANGED_EXTENSION: ${{ matrix.extension }}
|
||||
run: pnpm test:extension "$OPENCLAW_CHANGED_EXTENSION"
|
||||
|
||||
# Types, lint, and format check.
|
||||
check:
|
||||
name: "check"
|
||||
@@ -232,8 +304,88 @@ jobs:
|
||||
- name: Enforce safe external URL opening policy
|
||||
run: pnpm lint:ui:no-raw-window-open
|
||||
|
||||
startup-memory:
|
||||
name: "startup-memory"
|
||||
plugin-extension-boundary:
|
||||
name: "plugin-extension-boundary"
|
||||
needs: [docs-scope, changed-scope]
|
||||
if: needs.docs-scope.outputs.docs_only != 'true' && needs.changed-scope.outputs.run_node == 'true'
|
||||
runs-on: blacksmith-16vcpu-ubuntu-2404
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: false
|
||||
|
||||
- name: Setup Node environment
|
||||
uses: ./.github/actions/setup-node-env
|
||||
with:
|
||||
install-bun: "false"
|
||||
use-sticky-disk: "false"
|
||||
|
||||
- name: Run plugin extension boundary guard
|
||||
run: pnpm run lint:plugins:no-extension-imports
|
||||
|
||||
web-search-provider-boundary:
|
||||
name: "web-search-provider-boundary"
|
||||
needs: [docs-scope, changed-scope]
|
||||
if: needs.docs-scope.outputs.docs_only != 'true' && needs.changed-scope.outputs.run_node == 'true'
|
||||
runs-on: blacksmith-16vcpu-ubuntu-2404
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: false
|
||||
|
||||
- name: Setup Node environment
|
||||
uses: ./.github/actions/setup-node-env
|
||||
with:
|
||||
install-bun: "false"
|
||||
use-sticky-disk: "false"
|
||||
|
||||
- name: Run web search provider boundary guard
|
||||
run: pnpm run lint:web-search-provider-boundaries
|
||||
|
||||
extension-src-outside-plugin-sdk-boundary:
|
||||
name: "extension-src-outside-plugin-sdk-boundary"
|
||||
needs: [docs-scope, changed-scope]
|
||||
if: needs.docs-scope.outputs.docs_only != 'true' && needs.changed-scope.outputs.run_node == 'true'
|
||||
runs-on: blacksmith-16vcpu-ubuntu-2404
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: false
|
||||
|
||||
- name: Setup Node environment
|
||||
uses: ./.github/actions/setup-node-env
|
||||
with:
|
||||
install-bun: "false"
|
||||
use-sticky-disk: "false"
|
||||
|
||||
- name: Run extension src boundary guard
|
||||
run: pnpm run lint:extensions:no-src-outside-plugin-sdk
|
||||
|
||||
extension-plugin-sdk-internal-boundary:
|
||||
name: "extension-plugin-sdk-internal-boundary"
|
||||
needs: [docs-scope, changed-scope]
|
||||
if: needs.docs-scope.outputs.docs_only != 'true' && needs.changed-scope.outputs.run_node == 'true'
|
||||
runs-on: blacksmith-16vcpu-ubuntu-2404
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: false
|
||||
|
||||
- name: Setup Node environment
|
||||
uses: ./.github/actions/setup-node-env
|
||||
with:
|
||||
install-bun: "false"
|
||||
use-sticky-disk: "false"
|
||||
|
||||
- name: Run extension plugin-sdk-internal guard
|
||||
run: pnpm run lint:extensions:no-plugin-sdk-internal
|
||||
|
||||
build-smoke:
|
||||
name: "build-smoke"
|
||||
needs: [docs-scope, changed-scope]
|
||||
if: needs.docs-scope.outputs.docs_only != 'true' && needs.changed-scope.outputs.run_node == 'true'
|
||||
runs-on: blacksmith-16vcpu-ubuntu-2404
|
||||
@@ -252,9 +404,46 @@ jobs:
|
||||
- name: Build dist
|
||||
run: pnpm build
|
||||
|
||||
- name: Smoke test CLI launcher help
|
||||
run: node openclaw.mjs --help
|
||||
|
||||
- name: Smoke test CLI launcher status json
|
||||
run: node openclaw.mjs status --json --timeout 1
|
||||
|
||||
- name: Smoke test built bundled plugin singleton
|
||||
run: pnpm test:build:singleton
|
||||
|
||||
- name: Check CLI startup memory
|
||||
run: pnpm test:startup:memory
|
||||
|
||||
gateway-watch-regression:
|
||||
name: "gateway-watch-regression"
|
||||
needs: [docs-scope, changed-scope]
|
||||
if: needs.docs-scope.outputs.docs_only != 'true' && needs.changed-scope.outputs.run_node == 'true'
|
||||
runs-on: blacksmith-16vcpu-ubuntu-2404
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: false
|
||||
|
||||
- name: Setup Node environment
|
||||
uses: ./.github/actions/setup-node-env
|
||||
with:
|
||||
install-bun: "false"
|
||||
use-sticky-disk: "false"
|
||||
|
||||
- name: Run gateway watch regression harness
|
||||
run: pnpm test:gateway:watch-regression
|
||||
|
||||
- name: Upload gateway watch regression artifacts
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: gateway-watch-regression
|
||||
path: .local/gateway-watch-regression/
|
||||
retention-days: 7
|
||||
|
||||
# Validate docs (format, lint, broken links) only when docs files changed.
|
||||
check-docs:
|
||||
needs: [docs-scope]
|
||||
@@ -382,21 +571,30 @@ jobs:
|
||||
run: pre-commit run --all-files detect-private-key
|
||||
|
||||
- name: Audit changed GitHub workflows with zizmor
|
||||
env:
|
||||
BASE_SHA: ${{ github.event_name == 'push' && github.event.before || github.event.pull_request.base.sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
if [ "${{ github.event_name }}" = "push" ]; then
|
||||
BASE="${{ github.event.before }}"
|
||||
else
|
||||
BASE="${{ github.event.pull_request.base.sha }}"
|
||||
if [ -z "${BASE_SHA:-}" ] || [ "${BASE_SHA}" = "0000000000000000000000000000000000000000" ]; then
|
||||
echo "No usable base SHA detected; skipping zizmor."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
mapfile -t workflow_files < <(git diff --name-only "$BASE" HEAD -- '.github/workflows/*.yml' '.github/workflows/*.yaml')
|
||||
if ! git cat-file -e "${BASE_SHA}^{commit}" 2>/dev/null; then
|
||||
echo "Base SHA ${BASE_SHA} is unavailable; skipping zizmor."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
mapfile -t workflow_files < <(
|
||||
git diff --name-only "${BASE_SHA}" HEAD -- '.github/workflows/*.yml' '.github/workflows/*.yaml'
|
||||
)
|
||||
if [ "${#workflow_files[@]}" -eq 0 ]; then
|
||||
echo "No workflow changes detected; skipping zizmor."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
printf 'Auditing workflow files:\n%s\n' "${workflow_files[@]}"
|
||||
pre-commit run zizmor --files "${workflow_files[@]}"
|
||||
|
||||
- name: Audit production dependencies
|
||||
|
||||
214
.github/workflows/plugin-npm-release.yml
vendored
Normal file
214
.github/workflows/plugin-npm-release.yml
vendored
Normal file
@@ -0,0 +1,214 @@
|
||||
name: Plugin NPM Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- ".github/workflows/plugin-npm-release.yml"
|
||||
- "extensions/**"
|
||||
- "package.json"
|
||||
- "scripts/lib/plugin-npm-release.ts"
|
||||
- "scripts/plugin-npm-publish.sh"
|
||||
- "scripts/plugin-npm-release-check.ts"
|
||||
- "scripts/plugin-npm-release-plan.ts"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
publish_scope:
|
||||
description: Publish the selected plugins or all publishable plugins from the ref
|
||||
required: true
|
||||
default: selected
|
||||
type: choice
|
||||
options:
|
||||
- selected
|
||||
- all-publishable
|
||||
ref:
|
||||
description: Commit SHA on main to publish from (copy from the preview run)
|
||||
required: true
|
||||
type: string
|
||||
plugins:
|
||||
description: Comma-separated plugin package names to publish when publish_scope=selected
|
||||
required: false
|
||||
type: string
|
||||
|
||||
concurrency:
|
||||
group: plugin-npm-release-${{ github.event_name == 'workflow_dispatch' && inputs.ref || github.sha }}
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||
NODE_VERSION: "24.x"
|
||||
PNPM_VERSION: "10.23.0"
|
||||
|
||||
jobs:
|
||||
preview_plugins_npm:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
outputs:
|
||||
ref_sha: ${{ steps.ref.outputs.sha }}
|
||||
has_candidates: ${{ steps.plan.outputs.has_candidates }}
|
||||
candidate_count: ${{ steps.plan.outputs.candidate_count }}
|
||||
matrix: ${{ steps.plan.outputs.matrix }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || github.sha }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Node environment
|
||||
uses: ./.github/actions/setup-node-env
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
pnpm-version: ${{ env.PNPM_VERSION }}
|
||||
install-bun: "false"
|
||||
use-sticky-disk: "false"
|
||||
|
||||
- name: Resolve checked-out ref
|
||||
id: ref
|
||||
run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Validate ref is on main
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main
|
||||
git merge-base --is-ancestor HEAD origin/main
|
||||
|
||||
- name: Validate publishable plugin metadata
|
||||
env:
|
||||
PUBLISH_SCOPE: ${{ github.event_name == 'workflow_dispatch' && inputs.publish_scope || '' }}
|
||||
RELEASE_PLUGINS: ${{ github.event_name == 'workflow_dispatch' && inputs.plugins || '' }}
|
||||
BASE_REF: ${{ github.event_name != 'workflow_dispatch' && github.event.before || '' }}
|
||||
HEAD_REF: ${{ steps.ref.outputs.sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [[ -n "${PUBLISH_SCOPE}" ]]; then
|
||||
release_args=(--selection-mode "${PUBLISH_SCOPE}")
|
||||
if [[ -n "${RELEASE_PLUGINS}" ]]; then
|
||||
release_args+=(--plugins "${RELEASE_PLUGINS}")
|
||||
fi
|
||||
pnpm release:plugins:npm:check -- "${release_args[@]}"
|
||||
elif [[ -n "${BASE_REF}" ]]; then
|
||||
pnpm release:plugins:npm:check -- --base-ref "${BASE_REF}" --head-ref "${HEAD_REF}"
|
||||
else
|
||||
pnpm release:plugins:npm:check
|
||||
fi
|
||||
|
||||
- name: Resolve plugin release plan
|
||||
id: plan
|
||||
env:
|
||||
PUBLISH_SCOPE: ${{ github.event_name == 'workflow_dispatch' && inputs.publish_scope || '' }}
|
||||
RELEASE_PLUGINS: ${{ github.event_name == 'workflow_dispatch' && inputs.plugins || '' }}
|
||||
BASE_REF: ${{ github.event_name != 'workflow_dispatch' && github.event.before || '' }}
|
||||
HEAD_REF: ${{ steps.ref.outputs.sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p .local
|
||||
if [[ -n "${PUBLISH_SCOPE}" ]]; then
|
||||
plan_args=(--selection-mode "${PUBLISH_SCOPE}")
|
||||
if [[ -n "${RELEASE_PLUGINS}" ]]; then
|
||||
plan_args+=(--plugins "${RELEASE_PLUGINS}")
|
||||
fi
|
||||
node --import tsx scripts/plugin-npm-release-plan.ts "${plan_args[@]}" > .local/plugin-npm-release-plan.json
|
||||
elif [[ -n "${BASE_REF}" ]]; then
|
||||
node --import tsx scripts/plugin-npm-release-plan.ts --base-ref "${BASE_REF}" --head-ref "${HEAD_REF}" > .local/plugin-npm-release-plan.json
|
||||
else
|
||||
node --import tsx scripts/plugin-npm-release-plan.ts > .local/plugin-npm-release-plan.json
|
||||
fi
|
||||
|
||||
cat .local/plugin-npm-release-plan.json
|
||||
|
||||
candidate_count="$(jq -r '.candidates | length' .local/plugin-npm-release-plan.json)"
|
||||
has_candidates="false"
|
||||
if [[ "${candidate_count}" != "0" ]]; then
|
||||
has_candidates="true"
|
||||
fi
|
||||
matrix_json="$(jq -c '.candidates' .local/plugin-npm-release-plan.json)"
|
||||
|
||||
{
|
||||
echo "candidate_count=${candidate_count}"
|
||||
echo "has_candidates=${has_candidates}"
|
||||
echo "matrix=${matrix_json}"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
echo "Plugin release candidates:"
|
||||
jq -r '.candidates[]? | "- \(.packageName)@\(.version) [\(.publishTag)] from \(.packageDir)"' .local/plugin-npm-release-plan.json
|
||||
|
||||
echo "Already published / skipped:"
|
||||
jq -r '.skippedPublished[]? | "- \(.packageName)@\(.version)"' .local/plugin-npm-release-plan.json
|
||||
|
||||
preview_plugin_pack:
|
||||
needs: preview_plugins_npm
|
||||
if: needs.preview_plugins_npm.outputs.has_candidates == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
plugin: ${{ fromJson(needs.preview_plugins_npm.outputs.matrix) }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ needs.preview_plugins_npm.outputs.ref_sha }}
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Setup Node environment
|
||||
uses: ./.github/actions/setup-node-env
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
pnpm-version: ${{ env.PNPM_VERSION }}
|
||||
install-bun: "false"
|
||||
use-sticky-disk: "false"
|
||||
install-deps: "false"
|
||||
|
||||
- name: Preview publish command
|
||||
run: bash scripts/plugin-npm-publish.sh --dry-run "${{ matrix.plugin.packageDir }}"
|
||||
|
||||
- name: Preview npm pack contents
|
||||
working-directory: ${{ matrix.plugin.packageDir }}
|
||||
run: npm pack --dry-run --json --ignore-scripts
|
||||
|
||||
publish_plugins_npm:
|
||||
needs: [preview_plugins_npm, preview_plugin_pack]
|
||||
if: github.event_name == 'workflow_dispatch' && needs.preview_plugins_npm.outputs.has_candidates == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
environment: npm-release
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
plugin: ${{ fromJson(needs.preview_plugins_npm.outputs.matrix) }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ needs.preview_plugins_npm.outputs.ref_sha }}
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Setup Node environment
|
||||
uses: ./.github/actions/setup-node-env
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
pnpm-version: ${{ env.PNPM_VERSION }}
|
||||
install-bun: "false"
|
||||
use-sticky-disk: "false"
|
||||
install-deps: "false"
|
||||
|
||||
- name: Ensure version is not already published
|
||||
env:
|
||||
PACKAGE_NAME: ${{ matrix.plugin.packageName }}
|
||||
PACKAGE_VERSION: ${{ matrix.plugin.version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if npm view "${PACKAGE_NAME}@${PACKAGE_VERSION}" version >/dev/null 2>&1; then
|
||||
echo "${PACKAGE_NAME}@${PACKAGE_VERSION} is already published on npm."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Publish
|
||||
run: bash scripts/plugin-npm-publish.sh --publish "${{ matrix.plugin.packageDir }}"
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -4,10 +4,12 @@ node_modules
|
||||
docker-compose.override.yml
|
||||
docker-compose.extra.yml
|
||||
dist
|
||||
dist-runtime
|
||||
pnpm-lock.yaml
|
||||
bun.lock
|
||||
bun.lockb
|
||||
coverage
|
||||
__openclaw_vitest__/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.tsbuildinfo
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
**/node_modules/
|
||||
docs/.generated/
|
||||
|
||||
10
AGENTS.md
10
AGENTS.md
@@ -114,6 +114,7 @@
|
||||
- Never add `@ts-nocheck` and do not disable `no-explicit-any`; fix root causes and update Oxlint/Oxfmt config only when required.
|
||||
- Dynamic import guardrail: do not mix `await import("x")` and static `import ... from "x"` for the same module in production code paths. If you need lazy loading, create a dedicated `*.runtime.ts` boundary (that re-exports from `x`) and dynamically import that boundary from lazy callers only.
|
||||
- Dynamic import verification: after refactors that touch lazy-loading/module boundaries, run `pnpm build` and check for `[INEFFECTIVE_DYNAMIC_IMPORT]` warnings before submitting.
|
||||
- Extension SDK self-import guardrail: inside an extension package, do not import that same extension via `openclaw/plugin-sdk/<extension>` from production files. Route internal imports through a local barrel such as `./api.ts` or `./runtime-api.ts`, and keep the `plugin-sdk/<extension>` path as the external contract only.
|
||||
- Never share class behavior via prototype mutation (`applyPrototypeMixins`, `Object.defineProperty` on `.prototype`, or exporting `Class.prototype` for merges). Use explicit inheritance/composition (`A extends B extends C`) or helper composition so TypeScript can typecheck.
|
||||
- If this pattern is needed, stop and get explicit approval before shipping; default behavior is to split/refactor into an explicit class hierarchy and keep members strongly typed.
|
||||
- In tests, prefer per-instance stubs over prototype mutation (`SomeClass.prototype.method = ...`) unless a test explicitly documents why prototype-level patching is required.
|
||||
@@ -139,7 +140,7 @@
|
||||
- Do not set test workers above 16; tried already.
|
||||
- If local Vitest runs cause memory pressure (common on non-Mac-Studio hosts), use `OPENCLAW_TEST_PROFILE=low OPENCLAW_TEST_SERIAL_GATEWAY=1 pnpm test` for land/gate runs.
|
||||
- Live tests (real keys): `CLAWDBOT_LIVE_TEST=1 pnpm test:live` (OpenClaw-only) or `LIVE=1 pnpm test:live` (includes provider live tests). Docker: `pnpm test:docker:live-models`, `pnpm test:docker:live-gateway`. Onboarding Docker E2E: `pnpm test:docker:onboard`.
|
||||
- Full kit + what’s covered: `docs/testing.md`.
|
||||
- Full kit + what’s covered: `docs/help/testing.md`.
|
||||
- Changelog: user-facing changes only; no internal/meta notes (version alignment, appcast reminders, release process).
|
||||
- Changelog placement: in the active version block, append new entries to the end of the target section (`### Changes` or `### Fixes`); do not insert new entries at the top of a section.
|
||||
- Changelog attribution: use at most one contributor mention per line; prefer `Thanks @author` and do not also add `by @author` on the same entry.
|
||||
@@ -212,6 +213,11 @@
|
||||
- `prlctl exec` is fine for deterministic repo commands, but it can misrepresent interactive shell behavior (`PATH`, `HOME`, `curl | bash`, shebang resolution). For installer parity or shell-sensitive repros, prefer the guest Terminal or `prlctl enter`.
|
||||
- Fresh Tahoe snapshot current reality: `brew` exists, `node` may not be on `PATH` in noninteractive guest exec. Use absolute `/opt/homebrew/bin/node` for repo/CLI runs when needed.
|
||||
- Preferred automation entrypoint: `pnpm test:parallels:macos`. It restores the snapshot most closely matching `macOS 26.3.1 fresh`, serves the current `main` tarball from the host, then runs fresh-install and latest-release-to-main smoke lanes.
|
||||
- Discord roundtrip smoke is opt-in. Pass `--discord-token-env <VAR> --discord-guild-id <guild> --discord-channel-id <channel>`; the harness will configure Discord in-guest, post a guest message, verify host-side visibility via the Discord REST API, post a fresh host-side message back into the channel, then verify `openclaw message read` sees it in-guest.
|
||||
- Keep the Discord token in a host env var only. For Peter’s Mac Studio bot, fetch it into a temp env var from `~/.openclaw/openclaw.json` over SSH instead of hardcoding it in repo files/shell history.
|
||||
- For Discord smoke on this snapshot: use `openclaw message send/read` via the installed wrapper, not `node openclaw.mjs message ...`; lazy `message` subcommands do not resolve the same way through the direct module entrypoint.
|
||||
- For Discord guild allowlists: set `channels.discord.guilds` as one JSON object. Do not use dotted `config set channels.discord.guilds.<snowflake>...` paths; numeric snowflakes get treated as array indexes.
|
||||
- Avoid `prlctl enter` / expect for the Discord config phase; long lines get mangled. Use `prlctl exec --current-user /bin/sh -lc ...` with short commands or temp files.
|
||||
- Gateway verification in smoke runs should use `openclaw gateway status --deep --require-rpc`, not plain `--deep`, so probe failures go non-zero.
|
||||
- Latest-release pre-upgrade diagnostics still need compatibility fallback: stable `2026.3.12` does not know `--require-rpc`, so precheck status dumps should fall back to plain `gateway status --deep` until the guest is upgraded.
|
||||
- Harness output: pass `--json` for machine-readable summary; per-phase logs land under `/tmp/openclaw-parallels-smoke.*`.
|
||||
@@ -275,7 +281,7 @@
|
||||
- If staged+unstaged diffs are formatting-only, auto-resolve without asking.
|
||||
- If commit/push already requested, auto-stage and include formatting-only follow-ups in the same commit (or a tiny follow-up commit if needed), no extra confirmation.
|
||||
- Only ask when changes are semantic (logic/data/behavior).
|
||||
- Lobster seam: use the shared CLI palette in `src/terminal/palette.ts` (no hardcoded colors); apply palette to onboarding/config prompts and other TTY UI output as needed.
|
||||
- Lobster palette: use the shared CLI palette in `src/terminal/palette.ts` (no hardcoded colors); apply palette to onboarding/config prompts and other TTY UI output as needed.
|
||||
- **Multi-agent safety:** focus reports on your edits; avoid guard-rail disclaimers unless truly blocked; when multiple agents touch the same file, continue if safe; end with a brief “other files present” note only if relevant.
|
||||
- Bug investigations: read source code of relevant npm dependencies and all related local code before concluding; aim for high-confidence root cause.
|
||||
- Code style: add brief comments for tricky logic; keep files under ~500 LOC when feasible (split/refactor as needed).
|
||||
|
||||
460
CHANGELOG.md
460
CHANGELOG.md
@@ -6,79 +6,160 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Changes
|
||||
|
||||
- Android/mobile: add a system-aware dark theme across onboarding and post-onboarding screens so the app follows the device theme through setup, chat, and voice flows. (#46249) Thanks @sibbl.
|
||||
- Commands/btw: add `/btw` side questions for quick tool-less answers about the current session without changing future session context, with dismissible in-session TUI answers and explicit BTW replies on external channels. (#45444) Thanks @ngutman.
|
||||
- Gateway/health monitor: add configurable stale-event thresholds and restart limits, plus per-channel and per-account `healthMonitor.enabled` overrides, while keeping the existing global disable path on `gateway.channelHealthCheckMinutes=0`. (#42107) Thanks @rstar327.
|
||||
- Feishu/cards: add identity-aware structured card headers and note footers for Feishu replies and direct sends, while keeping that presentation wired through the shared outbound identity path. (#29938) Thanks @nszhsl.
|
||||
- Feishu/streaming: add `onReasoningStream` and `onReasoningEnd` support to streaming cards, so `/reasoning stream` renders thinking tokens as markdown blockquotes in the same card — matching the Telegram channel's reasoning lane behavior. (#46029)
|
||||
- Web tools/Firecrawl: add Firecrawl as an `onboard`/configure search provider via a bundled plugin, expose explicit `firecrawl_search` and `firecrawl_scrape` tools, and align core `web_fetch` fallback behavior with Firecrawl base-URL/env fallback plus guarded endpoint fetches.
|
||||
- Refactor/channels: remove the legacy channel shim directories and point channel-specific imports directly at the extension-owned implementations. (#45967) thanks @scoootscooob.
|
||||
- Android/nodes: add `callLog.search` plus shared Call Log permission wiring so Android nodes can search recent call history through the gateway. (#44073) Thanks @lxk7280.
|
||||
- Docs/Zalo: clarify the Marketplace-bot support matrix and config guidance so the Zalo channel docs match current Bot Creator behavior more closely. (#47552) Thanks @No898.
|
||||
- Install/update: allow package-manager installs from GitHub `main` via `openclaw update --tag main`, installer `--version main`, or direct npm/pnpm git specs.
|
||||
- Plugins/providers: move OpenRouter, GitHub Copilot, and OpenAI Codex provider/runtime logic into bundled plugins, including dynamic model fallback, runtime auth exchange, stream wrappers, capability hints, and cache-TTL policy.
|
||||
- Plugins/MiniMax: merge the bundled MiniMax API and MiniMax OAuth plugin surfaces into a single default-on `minimax` plugin, while keeping legacy `minimax-portal-auth` config ids aliased for compatibility.
|
||||
- Plugins/bundles: add compatible Codex, Claude, and Cursor bundle discovery/install support, map bundle skills into OpenClaw skills, and apply Claude bundle `settings.json` defaults to embedded Pi with shell overrides sanitized.
|
||||
- Plugins/agent integrations: broaden the plugin surface for app-server integrations with channel-aware commands, interactive callbacks, inbound claims, and Discord/Telegram conversation binding support. (#45318) Thanks @huntharo and @vincentkoc.
|
||||
- Telegram/actions: add `topic-edit` for forum-topic renames and icon updates while sharing the same Telegram topic-edit transport used by the plugin runtime. (#47798) Thanks @obviyus.
|
||||
- secrets: harden read-only SecretRef command paths and diagnostics. (#47794) Thanks @joshavant.
|
||||
- Gateway/docs: clarify that empty URL input allowlists are treated as unset, document `allowUrl: false` as the deny-all switch, and add regression coverage for the normalization path.
|
||||
- Sandbox/runtime: add pluggable sandbox backends, ship an OpenShell backend with `mirror` and `remote` workspace modes, and make sandbox list/recreate/prune backend-aware instead of Docker-only.
|
||||
- Sandbox/SSH: add a core SSH sandbox backend with secret-backed key, certificate, and known_hosts inputs, move shared remote exec/filesystem tooling into core, and keep OpenShell focused on sandbox lifecycle plus optional `mirror` mode.
|
||||
- Web tools/Firecrawl: add Firecrawl as an `onboard`/configure search provider via a bundled plugin, expose explicit `firecrawl_search` and `firecrawl_scrape` tools, and align core `web_fetch` fallback behavior with Firecrawl base-URL/env fallback plus guarded endpoint fetches.
|
||||
- Plugins/bundles: add compatible Codex, Claude, and Cursor bundle discovery/install support, map bundle skills into OpenClaw skills, and apply Claude bundle `settings.json` defaults to embedded Pi with shell overrides sanitized.
|
||||
- Plugins/providers: move OpenRouter, GitHub Copilot, and OpenAI Codex provider/runtime logic into bundled plugins, including dynamic model fallback, runtime auth exchange, stream wrappers, capability hints, and cache-TTL policy.
|
||||
- Plugins/agent integrations: broaden the plugin surface for app-server integrations with channel-aware commands, interactive callbacks, inbound claims, and Discord/Telegram conversation binding support. (#45318) Thanks @huntharo and @vincentkoc.
|
||||
- Install/update: allow package-manager installs from GitHub `main` via `openclaw update --tag main`, installer `--version main`, or direct npm/pnpm git specs. (#47630) Thanks @vincentkoc.
|
||||
- Gateway/health monitor: add configurable stale-event thresholds and restart limits, plus per-channel and per-account `healthMonitor.enabled` overrides, while keeping the existing global disable path on `gateway.channelHealthCheckMinutes=0`. (#42107) Thanks @rstar327.
|
||||
- Android/mobile: add a system-aware dark theme across onboarding and post-onboarding screens so the app follows the device theme through setup, chat, and voice flows. (#46249) Thanks @sibbl.
|
||||
- Feishu/ACP: add current-conversation ACP and subagent session binding for supported DMs and topic conversations, including completion delivery back to the originating Feishu conversation. (#46819) Thanks @Takhoffman.
|
||||
- Plugins/marketplaces: add Claude marketplace registry resolution, `plugin@marketplace` installs, marketplace listing, and update support, plus Docker E2E coverage for local and official marketplace flows. (#48058) Thanks @vincentkoc.
|
||||
- Commands/plugins: add owner-gated `/plugins` and `/plugin` chat commands for plugin list/show and enable/disable flows, alongside explicit `commands.plugins` config gating. Thanks @vincentkoc.
|
||||
- Feishu/cards: add structured interactive approval and quick-action launcher cards, preserve callback user and conversation context through routing, and keep legacy card-action fallback behavior so common actions can run without typing raw commands. (#47873) Thanks @Takhoffman.
|
||||
- Feishu/streaming: add `onReasoningStream` and `onReasoningEnd` support to streaming cards, so `/reasoning stream` renders thinking tokens as markdown blockquotes in the same card — matching the Telegram channel's reasoning lane behavior. (#46029) Thanks @day253.
|
||||
- Feishu/cards: add identity-aware structured card headers and note footers for Feishu replies and direct sends, while keeping that presentation wired through the shared outbound identity path. (#29938) Thanks @nszhsl.
|
||||
- Android/nodes: add `callLog.search` plus shared Call Log permission wiring so Android nodes can search recent call history through the gateway. (#44073) Thanks @lxk7280.
|
||||
- Plugins/MiniMax: merge the bundled MiniMax API and MiniMax OAuth plugin surfaces into a single default-on `minimax` plugin, while keeping legacy `minimax-portal-auth` config ids aliased for compatibility.
|
||||
- Telegram/actions: add `topic-edit` for forum-topic renames and icon updates while sharing the same Telegram topic-edit transport used by the plugin runtime. (#47798) Thanks @obviyus.
|
||||
- Telegram/error replies: add a default-off `channels.telegram.silentErrorReplies` setting so bot error replies can be delivered silently across regular replies, native commands, and fallback sends. (#19776) Thanks @ImLukeF.
|
||||
- Refactor/channels: remove the legacy channel shim directories and point channel-specific imports directly at the extension-owned implementations. (#45967) Thanks @scoootscooob.
|
||||
- Docs/Zalo: clarify the Marketplace-bot support matrix and config guidance so the Zalo channel docs match current Bot Creator behavior more closely. (#47552) Thanks @No898.
|
||||
- secrets: harden read-only SecretRef command paths and diagnostics. (#47794) Thanks @joshavant.
|
||||
- Browser/existing-session: support `browser.profiles.<name>.userDataDir` so Chrome DevTools MCP can attach to Brave, Edge, and other Chromium-based browsers through their own user data directories. (#48170) Thanks @velvet-shark.
|
||||
- Skills/prompt budget: preserve all registered skills via a compact catalog fallback before dropping entries when the full prompt format exceeds `maxSkillsPromptChars`. (#47553) Thanks @snese.
|
||||
- Models/OpenAI: add native forward-compat support for `gpt-5.4-mini` and `gpt-5.4-nano` in the OpenAI provider catalog, runtime resolution, and reasoning capability gates. Thanks @vincentkoc.
|
||||
- Plugins/bundles: make enabled bundle MCP servers expose runnable tools in embedded Pi, and default relative bundle MCP launches to the bundle root so marketplace bundles like Context7 work through Pi instead of stopping at config import.
|
||||
- Scope message SecretRef resolution and harden doctor/status paths. (#48728) Thanks @joshavant.
|
||||
- Plugins/testing: add a public `openclaw/plugin-sdk/testing` surface for plugin-author test helpers, and move bundled-extension-only test bridges out of `extensions/` into private repo test helpers.
|
||||
- Plugins/Chutes: add a bundled Chutes provider with plugin-owned OAuth/API-key auth, dynamic model discovery, and default-on extension wiring. (#41416) Thanks @Veightor.
|
||||
- Plugins/binding: add `onConversationBindingResolved(...)` so plugins can react immediately after bind approvals or denies without blocking channel interaction acknowledgements. (#48678) Thanks @huntharo.
|
||||
- CLI/config: expand `config set` with SecretRef and provider builder modes, JSON/batch assignment support, and `--dry-run` validation with structured JSON output. (#49296) Thanks @joshavant.
|
||||
- Control UI/appearance: unify theme border radii across Claw, Knot, and Dash, and add a Roundness slider to the Appearance settings so users can adjust corner radius from sharp to fully rounded. Thanks @BunsDev.
|
||||
- Control UI/chat: add an expand-to-canvas button on assistant chat bubbles and in-app session navigation from Sessions and Cron views. Thanks @BunsDev.
|
||||
- Plugins/context engines: expose `delegateCompactionToRuntime(...)` on the public plugin SDK, refactor the legacy engine to use the shared helper, and clarify `ownsCompaction` delegation semantics for non-owning engines. (#49061) Thanks @jalehman.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Group mention gating: reject invalid and unsafe nested-repetition `mentionPatterns`, reuse the shared safe config-regex compiler across mention stripping and detection, and cache strip-time regex compilation so noisy groups avoid repeated recompiles.
|
||||
- Control UI/chat sessions: show human-readable labels in the grouped session dropdown again, keep unique scoped fallbacks when metadata is missing, and disambiguate duplicate labels only when needed. (#45130) thanks @luzhidong.
|
||||
- Control UI: scope persisted session selection per gateway, prevent stale session bleed across tokenized gateway opens, and cap stored gateway session history. (#47453) Thanks @sallyom.
|
||||
- Slack/interactive replies: preserve `channelData.slack.blocks` through live DM delivery and preview-finalized edits so Block Kit button and select directives render instead of falling back to raw text. (#45890) Thanks @vincentkoc.
|
||||
- Feishu/topic threads: fetch full thread context, including prior bot replies, when starting a topic-thread session so follow-up turns in Feishu topics keep the right conversation state. (#45254) Thanks @Coobiw.
|
||||
- Configure/startup: move outbound send-deps resolution into a lightweight helper so `openclaw configure` no longer stalls after the banner while eagerly loading channel plugins. (#46301) thanks @scoootscooob.
|
||||
- Control UI/dashboard: preserve structured gateway shutdown reasons across restart disconnects so config-triggered restarts no longer fall back to `disconnected (1006): no reason`. (#46532) Thanks @vincentkoc.
|
||||
- Android/chat: theme the thinking dropdown and TLS trust dialogs explicitly so popup surfaces match the active app theme instead of falling back to mismatched Material defaults.
|
||||
- Z.AI/onboarding: detect a working default model even for explicit `zai-coding-*` endpoint choices, so Coding Plan setup can keep the selected endpoint while defaulting to `glm-5` when available or `glm-4.7` as fallback. (#45969)
|
||||
- Models/OpenRouter runtime capabilities: fetch uncatalogued OpenRouter model metadata on first use so newly added vision models keep image input instead of silently degrading to text-only, with top-level capability field fallbacks for `/api/v1/models`. (#45824) Thanks @DJjjjhao.
|
||||
- Z.AI/onboarding: add `glm-5-turbo` to the default Z.AI provider catalog so onboarding-generated configs expose the new model alongside the existing GLM defaults. (#46670) Thanks @tomsun28.
|
||||
- Zalo Personal/group gating: stop reapplying `dmPolicy.allowFrom` as a sender gate for already-allowlisted groups when `groupAllowFrom` is unset, so any member of an allowed group can trigger replies while DMs stay restricted. (#40146)
|
||||
- Channels/plugins: keep shared interactive payloads merge-ready by fixing Slack custom callback routing and repeat-click dedupe, allowing interactive-only sends, and preserving ordered Discord shared text blocks. (#47715) Thanks @vincentkoc.
|
||||
- Plugins/bundler TDZ: fix `RESERVED_COMMANDS` temporal dead zone error that prevented device-pair, phone-control, and talk-voice plugins from registering when the bundler placed the commands module after call sites in the same output chunk. Thanks @BunsDev.
|
||||
- Plugins/imports: fix stale googlechat runtime-api import paths and signal SDK circular re-exports broken by recent plugin-sdk refactors. Thanks @BunsDev.
|
||||
- Google auth/Node 25: patch `gaxios` to use native fetch without injecting `globalThis.window`, while translating proxy and mTLS transport settings so Google Vertex and Google Chat auth keep working on Node 25. (#47914) Thanks @pdd-cli.
|
||||
- Gateway/startup: load bundled channel plugins from compiled `dist/extensions` entries in built installs, so gateway boot no longer recompiles bundled extension TypeScript on every startup and WhatsApp-class cold starts drop back to seconds instead of tens of seconds or worse. (#47560) Thanks @ngutman.
|
||||
- Plugins/context engines: enforce owner-aware context-engine registration on both loader and public SDK paths so plugins cannot spoof privileged ownership, claim the core `legacy` engine id, or overwrite an existing engine id through direct SDK imports. (#47595) Thanks @vincentkoc.
|
||||
- Browser/remote CDP: honor strict browser SSRF policy during remote CDP reachability and `/json/version` discovery checks, redact sensitive `cdpUrl` tokens from status output, and warn when remote CDP targets private/internal hosts.
|
||||
- Plugins/install precedence: keep bundled plugins ahead of auto-discovered globals by default, but let an explicitly installed plugin record win its own duplicate-id tie so installed channel plugins load from `~/.openclaw/extensions` after `openclaw plugins install`.
|
||||
- ACP/acpx: resolve the bundled plugin root from the actual plugin directory so plugin-local installs stay under `dist/extensions/acpx` instead of escaping to `dist/extensions` and failing runtime setup.
|
||||
- Gateway/auth: ignore spoofed loopback hops in trusted forwarding chains and block device approvals that request scopes above the caller session. Thanks @vincentkoc.
|
||||
- Gateway/config views: strip embedded credentials from URL-based endpoint fields before returning read-only account and config snapshots. Thanks @vincentkoc.
|
||||
- Tools/apply-patch: revalidate workspace-only delete and directory targets immediately before mutating host paths. Thanks @vincentkoc.
|
||||
- Webhooks/runtime: move auth earlier and tighten pre-auth body limits and timeouts across bundled webhook handlers, including slow-body handling for Mattermost slash commands. Thanks @vincentkoc.
|
||||
- Subagents/follow-ups: require the same controller ownership checks for `/subagents send` as other control actions, so leaf sessions cannot message nested child runs they do not control. Thanks @vincentkoc.
|
||||
- Inbound policy hardening: tighten callback and webhook sender checks across Mattermost and Google Chat, match Nextcloud Talk rooms by stable room token, and treat explicit empty Twitch allowlists as deny-all. (#46787) Thanks @zpbrent, @ijxpwastaken and @vincentkoc.
|
||||
- macOS/canvas actions: keep unattended local agent actions on trusted in-app canvas surfaces only, and stop exposing the deep-link fallback key to arbitrary page scripts. (#46790) Thanks @vincentkoc.
|
||||
- Agents/compaction: extend the enclosing run deadline once while compaction is actively in flight, and abort the underlying SDK compaction on timeout/cancel so large-session compactions stop freezing mid-run. (#46889) Thanks @asyncjason.
|
||||
- Models/openai-completions: default non-native OpenAI-compatible providers to omit tool-definition `strict` fields unless users explicitly opt back in, so tool calling keeps working on providers that reject that option. (#45497) Thanks @sahancava.
|
||||
- WhatsApp/reconnect: restore the append recency filter in the extension inbox monitor and handle protobuf `Long` timestamps correctly, so fresh post-reconnect append messages are processed while stale history sync stays suppressed. (#42588) thanks @MonkeyLeeT.
|
||||
- WhatsApp/login: wait for pending creds writes before reopening after Baileys `515` pairing restarts in both QR login and `channels login` flows, and keep the restart coverage pinned to the real wrapped error shape plus per-account creds queues. (#27910) Thanks @asyncjason.
|
||||
- Agents/openai-compatible tool calls: deduplicate repeated tool call ids across live assistant messages and replayed history so OpenAI-compatible backends no longer reject duplicate `tool_call_id` values with HTTP 400. (#40996) Thanks @xaeon2026.
|
||||
- Security/device pairing: harden `device.token.rotate` deny handling by keeping public failures generic while logging internal deny reasons and preserving approved-baseline enforcement. (`GHSA-7jrw-x62h-64p8`)
|
||||
- Slack/interactive replies: preserve `channelData.slack.blocks` through live DM delivery and preview-finalized edits so Block Kit button and select directives render instead of falling back to raw text. (#45890) Thanks @vincentkoc.
|
||||
- Zalo/plugin runtime: export `resolveClientIp` from `openclaw/plugin-sdk/zalo` so installed builds no longer crash on startup when the webhook monitor loads from the packaged extension instead of the monorepo source tree. (#46549) Thanks @No898.
|
||||
- CI/channel test routing: move the built-in channel suites into `test:channels` and keep them out of `test:extensions`, so extension CI no longer fails after the channel migration while targeted test routing still sends Slack, Signal, and iMessage suites to the right lane. (#46066) Thanks @scoootscooob.
|
||||
- Browser/profiles: drop the auto-created `chrome-relay` browser profile; users who need the Chrome extension relay must now create their own profile via `openclaw browser create-profile`. (#45777) Thanks @odysseus0.
|
||||
- Docs/Mintlify: fix MDX marker syntax on Perplexity, Model Providers, Moonshot, and exec approvals pages so local docs preview no longer breaks rendering or leaves stale pages unpublished. (#46695) Thanks @velvet-shark.
|
||||
- Email/webhook wrapping: sanitize sender and subject metadata before external-content wrapping so metadata fields cannot break the wrapper structure. Thanks @vincentkoc.
|
||||
- Node/startup: remove leftover debug `console.log("node host PATH: ...")` that printed the resolved PATH on every `openclaw node run` invocation. (#46411)
|
||||
- Nodes/pending actions: re-check queued foreground actions against the current node command policy before returning them to the node. (#46815) Thanks @zpbrent and @vincentkoc.
|
||||
- ACP/approvals: use canonical tool identity for prompting decisions and fail closed when conflicting tool identity hints are present. (#46817) Thanks @zpbrent and @vincentkoc.
|
||||
- Telegram/message send: forward `--force-document` through the `sendPayload` path as well as `sendMedia`, so Telegram payload sends with `channelData` keep uploading images as documents instead of silently falling back to compressed photo sends. (#47119) Thanks @thepagent.
|
||||
- Telegram/message chunking: preserve spaces, paragraph separators, and word boundaries when HTML overflow rechunking splits formatted replies. (#47274)
|
||||
- Plugins/scoped ids: preserve scoped plugin ids during install and config keying, and keep bundled plugins ahead of discovered duplicate ids by default so `@scope/name` plugins no longer collide with unscoped installs. Thanks @vincentkoc.
|
||||
- CLI: avoid loading provider discovery during startup model normalization. (#46522) Thanks @ItsAditya-xyz and @vincentkoc.
|
||||
- Tlon: honor explicit empty allowlists and defer cite expansion. (#46788) Thanks @zpbrent and @vincentkoc.
|
||||
- ACP: require admin scope for mutating internal actions. (#46789) Thanks @tdjackey and @vincentkoc.
|
||||
- Gateway/config validation: stop treating the implicit default memory slot as a required explicit plugin config, so startup no longer fails with `plugins.slots.memory: plugin not found: memory-core` when `memory-core` was only inferred. (#47494) Thanks @ngutman.
|
||||
- Gateway/plugins: pin runtime webhook routes to the gateway startup registry so channel webhooks keep working across plugin-registry churn, and make plugin auth + dispatch resolve routes from the same live HTTP-route registry. (#47902) Fixes #46924 and #47041. Thanks @steipete.
|
||||
- Gateway/auth: ignore spoofed loopback hops in trusted forwarding chains and block device approvals that request scopes above the caller session. (#46800) Thanks @vincentkoc.
|
||||
- Gateway/restart: defer externally signaled unmanaged restarts through the in-process idle drain, and preserve the restored subagent run as remap fallback during orphan recovery so resumed sessions do not duplicate work. (#47719) Thanks @joeykrug.
|
||||
- Control UI/session routing: preserve established external delivery routes when webchat views or sends in externally originated sessions, so subagent completions still return to the original channel instead of the dashboard. (#47797) Thanks @brokemac79.
|
||||
- Configure/startup: move outbound send-deps resolution into a lightweight helper so `openclaw configure` no longer stalls after the banner while eagerly loading channel plugins. (#46301) Thanks @scoootscooob.
|
||||
- CLI/startup: lazy-load channel add and root help startup paths to trim avoidable RSS and help latency on constrained hosts. (#46784) Thanks @vincentkoc.
|
||||
- CLI/onboarding: import static provider definitions directly for onboarding model/config helpers so those paths no longer pull provider discovery just for built-in defaults. (#47467) Thanks @vincentkoc.
|
||||
- CLI/auth choice: lazy-load plugin/provider fallback resolution so mapped auth choices stay on the static path and only unknown choices pay the heavy provider load. (#47495) Thanks @vincentkoc.
|
||||
- CLI/completion: reduce recursive completion-script string churn and fix nested PowerShell command-path matching so generated nested completions resolve on PowerShell too. (#45537) Thanks @yiShanXin and @vincentkoc.
|
||||
- Gateway/startup: load bundled channel plugins from compiled `dist/extensions` entries in built installs, so gateway boot no longer recompiles bundled extension TypeScript on every startup and WhatsApp-class cold starts drop back to seconds instead of tens of seconds or worse.
|
||||
- Gateway/watch mode: restart on bundled-plugin package and manifest metadata changes, rebuild `dist` for extension source and `tsdown.config.ts` changes, and still ignore extension docs. (#47571) thanks @gumadeiras.
|
||||
- CLI: avoid loading provider discovery during startup model normalization. (#46522) Thanks @ItsAditya-xyz and @vincentkoc.
|
||||
- Security/device pairing: harden `device.token.rotate` deny handling by keeping public failures generic while logging internal deny reasons and preserving approved-baseline enforcement. (`GHSA-7jrw-x62h-64p8`)
|
||||
- Inbound policy hardening: tighten callback and webhook sender checks across Mattermost and Google Chat, match Nextcloud Talk rooms by stable room token, and treat explicit empty Twitch allowlists as deny-all. (#46787) Thanks @zpbrent, @ijxpwastaken and @vincentkoc.
|
||||
- Webhooks/runtime: move auth earlier and tighten pre-auth body limits and timeouts across bundled webhook handlers, including slow-body handling for Mattermost slash commands. (#46802) Thanks @vincentkoc.
|
||||
- Email/webhook wrapping: sanitize sender and subject metadata before external-content wrapping so metadata fields cannot break the wrapper structure. (#46816) Thanks @vincentkoc.
|
||||
- Tools/apply-patch: revalidate workspace-only delete and directory targets immediately before mutating host paths. (#46803) Thanks @vincentkoc.
|
||||
- Gateway/config views: strip embedded credentials from URL-based endpoint fields before returning read-only account and config snapshots. (#46799) Thanks @vincentkoc.
|
||||
- ACP/approvals: use canonical tool identity for prompting decisions and fail closed when conflicting tool identity hints are present. (#46817) Thanks @zpbrent and @vincentkoc.
|
||||
- ACP: require admin scope for mutating internal actions. (#46789) Thanks @tdjackey and @vincentkoc.
|
||||
- Subagents/follow-ups: require the same controller ownership checks for `/subagents send` as other control actions, so leaf sessions cannot message nested child runs they do not control. (#46801) Thanks @vincentkoc.
|
||||
- macOS/canvas actions: keep unattended local agent actions on trusted in-app canvas surfaces only, and stop exposing the deep-link fallback key to arbitrary page scripts. (#46790) Thanks @vincentkoc.
|
||||
- Agents/compaction: extend the enclosing run deadline once while compaction is actively in flight, and abort the underlying SDK compaction on timeout/cancel so large-session compactions stop freezing mid-run. (#46889) Thanks @asyncjason.
|
||||
- Agents/openai-compatible tool calls: deduplicate repeated tool call ids across live assistant messages and replayed history so OpenAI-compatible backends no longer reject duplicate `tool_call_id` values with HTTP 400. (#40996) Thanks @xaeon2026.
|
||||
- Models/openai-completions: default non-native OpenAI-compatible providers to omit tool-definition `strict` fields unless users explicitly opt back in, so tool calling keeps working on providers that reject that option. (#45497) Thanks @sahancava.
|
||||
- Models/OpenRouter runtime capabilities: fetch uncatalogued OpenRouter model metadata on first use so newly added vision models keep image input instead of silently degrading to text-only, with top-level capability field fallbacks for `/api/v1/models`. (#45824) Thanks @DJjjjhao.
|
||||
- Channels/plugins: keep shared interactive payloads merge-ready by fixing Slack custom callback routing and repeat-click dedupe, allowing interactive-only sends, and preserving ordered Discord shared text blocks. (#47715) Thanks @vincentkoc.
|
||||
- Slack/interactive replies: preserve `channelData.slack.blocks` through live DM delivery and preview-finalized edits so Block Kit button and select directives render instead of falling back to raw text. (#45890) Thanks @vincentkoc.
|
||||
- Feishu/actions: expand the runtime action surface with message read/edit, explicit thread replies, pinning, and operator-facing chat/member inspection so Feishu can operate more of the workspace directly. (#47968) Thanks @Takhoffman.
|
||||
- Feishu/topic threads: fetch full thread context, including prior bot replies, when starting a topic-thread session so follow-up turns in Feishu topics keep the right conversation state. (#45254) Thanks @Coobiw.
|
||||
- Feishu/media: keep native image, file, audio, and video/media handling aligned across outbound sends, inbound downloads, thread replies, directory/action aliases, and capability docs so unsupported areas are explicit instead of implied. (#47968) Thanks @Takhoffman.
|
||||
- Feishu/webhooks: harden signed webhook verification to use constant-time signature comparison and keep malformed short signatures fail-closed in webhook E2E coverage.
|
||||
- WhatsApp/reconnect: restore the append recency filter in the extension inbox monitor and handle protobuf `Long` timestamps correctly, so fresh post-reconnect append messages are processed while stale history sync stays suppressed. (#42588) Thanks @MonkeyLeeT.
|
||||
- WhatsApp/login: wait for pending creds writes before reopening after Baileys `515` pairing restarts in both QR login and `channels login` flows, and keep the restart coverage pinned to the real wrapped error shape plus per-account creds queues. (#27910) Thanks @asyncjason.
|
||||
- Telegram/message send: forward `--force-document` through the `sendPayload` path as well as `sendMedia`, so Telegram payload sends with `channelData` keep uploading images as documents instead of silently falling back to compressed photo sends. (#47119) Thanks @thepagent.
|
||||
- Telegram/message chunking: preserve spaces, paragraph separators, and word boundaries when HTML overflow rechunking splits formatted replies. (#47274) Thanks @obviyus.
|
||||
- Z.AI/onboarding: detect a working default model even for explicit `zai-coding-*` endpoint choices, so Coding Plan setup can keep the selected endpoint while defaulting to `glm-5` when available or `glm-4.7` as fallback. (#45969) Thanks @obviyus.
|
||||
- Z.AI/onboarding: add `glm-5-turbo` to the default Z.AI provider catalog so onboarding-generated configs expose the new model alongside the existing GLM defaults. (#46670) Thanks @tomsun28.
|
||||
- Zalo Personal/group gating: stop reapplying `dmPolicy.allowFrom` as a sender gate for already-allowlisted groups when `groupAllowFrom` is unset, so any member of an allowed group can trigger replies while DMs stay restricted. (#46663) Fixes #40146. Thanks @Takhoffman.
|
||||
- Zalo/plugin runtime: export `resolveClientIp` from `openclaw/plugin-sdk/zalo` so installed builds no longer crash on startup when the webhook monitor loads from the packaged extension instead of the monorepo source tree. (#46549) Thanks @No898.
|
||||
- Docker/live tests: mount external CLI auth homes into writable container copies, derive Codex OAuth expiry from JWT `exp`, refresh synced CLI creds instead of trusting stale cached expiry, and make gateway live probes wait on transcript output so `pnpm test:docker:all` stays green in Linux.
|
||||
- Plugins/install precedence: keep bundled plugins ahead of auto-discovered globals by default, but let an explicitly installed plugin record win its own duplicate-id tie so installed channel plugins load from `~/.openclaw/extensions` after `openclaw plugins install`. (#46722) Thanks @Takhoffman.
|
||||
- Control UI/logging: make browser-safe logger imports avoid eager temp-dir resolution so the bundled Control UI no longer crashes to a blank screen when logging reaches `tmp-openclaw-dir`. (#48469) Fixes #48062. Thanks @7inspire.
|
||||
- Plugins/scoped ids: preserve scoped plugin ids during install and config keying, and keep bundled plugins ahead of discovered duplicate ids by default so `@scope/name` plugins no longer collide with unscoped installs. (#47413) Thanks @vincentkoc.
|
||||
- Gateway/watch mode: restart on bundled-plugin package and manifest metadata changes, rebuild `dist` for extension source and `tsdown.config.ts` changes, and still ignore extension docs. (#47571) Thanks @gumadeiras.
|
||||
- Gateway/watch mode: recreate bundled plugin runtime metadata after clean or stale `dist` states, so `pnpm gateway:watch` no longer fails on missing `dist/extensions/*/openclaw.plugin.json` manifests after a rebuild. Thanks @gumadeiras.
|
||||
- Plugins/context engines: enforce owner-aware context-engine registration on both loader and public SDK paths so plugins cannot spoof privileged ownership, claim the core `legacy` engine id, or overwrite an existing engine id through direct SDK imports. (#47595) Thanks @vincentkoc.
|
||||
- Control UI/chat sessions: show human-readable labels in the grouped session dropdown again, keep unique scoped fallbacks when metadata is missing, and disambiguate duplicate labels only when needed. (#45130) Thanks @luzhidong.
|
||||
- Control UI: scope persisted session selection per gateway, prevent stale session bleed across tokenized gateway opens, and cap stored gateway session history. (#47453) Thanks @sallyom.
|
||||
- Control UI/dashboard: preserve structured gateway shutdown reasons across restart disconnects so config-triggered restarts no longer fall back to `disconnected (1006): no reason`. (#46580) Fixes #46532. Thanks @vincentkoc.
|
||||
- Android/chat: theme the thinking dropdown and TLS trust dialogs explicitly so popup surfaces match the active app theme instead of falling back to mismatched Material defaults.
|
||||
- Group mention gating: reject invalid and unsafe nested-repetition `mentionPatterns`, reuse the shared safe config-regex compiler across mention stripping and detection, and cache strip-time regex compilation so noisy groups avoid repeated recompiles.
|
||||
- Browser/profiles: drop the auto-created `chrome-relay` browser profile; users who need the Chrome extension relay must now create their own profile via `openclaw browser create-profile`. (#46596) Fixes #45777. Thanks @odysseus0.
|
||||
- CI/channel test routing: move the built-in channel suites into `test:channels` and keep them out of `test:extensions`, so extension CI no longer fails after the channel migration while targeted test routing still sends Slack, Signal, and iMessage suites to the right lane. (#46066) Thanks @scoootscooob.
|
||||
- Docs/Mintlify: fix MDX marker syntax on Perplexity, Model Providers, Moonshot, and exec approvals pages so local docs preview no longer breaks rendering or leaves stale pages unpublished. (#46695) Thanks @velvet-shark.
|
||||
- Gateway/config validation: stop treating the implicit default memory slot as a required explicit plugin config, so startup no longer fails with `plugins.slots.memory: plugin not found: memory-core` when `memory-core` was only inferred. (#47494) Thanks @ngutman.
|
||||
- Tlon: honor explicit empty allowlists and defer cite expansion. (#46788) Thanks @zpbrent and @vincentkoc.
|
||||
- Tlon/DM auth: defer cited-message expansion until after DM authorization and owner command handling, so unauthorized DMs and owner approval/admin commands no longer trigger cross-channel cite fetches before the deny or command path.
|
||||
- Docs/security audit: spell out that `gateway.controlUi.allowedOrigins: ["*"]` is an explicit allow-all browser-origin policy and should be avoided outside tightly controlled local testing.
|
||||
- Gateway/auth: clear self-declared scopes for device-less trusted-proxy Control UI sessions so proxy-authenticated connects cannot claim admin or secrets scopes without a bound device identity.
|
||||
- Nodes/pending actions: re-check queued foreground actions against the current node command policy before returning them to the node. (#46815) Thanks @zpbrent and @vincentkoc.
|
||||
- Node/startup: remove leftover debug `console.log("node host PATH: ...")` that printed the resolved PATH on every `openclaw node run` invocation. (#46515) Fixes #46411. Thanks @ademczuk.
|
||||
- CLI/completion: reduce recursive completion-script string churn and fix nested PowerShell command-path matching so generated nested completions resolve on PowerShell too. (#45537) Thanks @yiShanXin and @vincentkoc.
|
||||
- Slack/startup: harden `@slack/bolt` import interop across current bundled runtime shapes so Slack monitors no longer crash with `App is not a constructor` after plugin-sdk bundling changes. (#45953) Thanks @merc1305.
|
||||
- Windows/gateway status: accept `schtasks` `Last Result` output as an alias for `Last Run Result`, so running scheduled-task installs no longer show `Runtime: unknown`. (#47844) Thanks @MoerAI.
|
||||
- ACP/acpx: resolve the bundled plugin root from the actual plugin directory so plugin-local installs stay under `dist/extensions/acpx` instead of escaping to `dist/extensions` and failing runtime setup. (#47601) Thanks @ngutman.
|
||||
- Gateway/websocket pairing bypass for disabled auth: skip device-pairing enforcement for Control UI operator sessions when `gateway.auth.mode=none`, so reverse-proxied dashboards no longer get stuck on `pairing required` despite auth being explicitly disabled. (#47148) Thanks @ademczuk.
|
||||
- Control UI/model switching: preserve the selected provider prefix when switching models from the chat dropdown, so multi-provider setups no longer send `anthropic/gpt-5.2`-style mismatches when the user picked `openai/gpt-5.2`. (#47581) Thanks @chrishham.
|
||||
- Control UI/storage: scope persisted settings keys by gateway base path, with migration from the legacy shared key, so multiple gateways under one domain stop overwriting each other's dashboard preferences. (#47932) Thanks @bobBot-claw.
|
||||
- Agents/usage tracking: stop forcing `supportsUsageInStreaming: false` on non-native OpenAI-completions providers so compatible backends report token usage and cost again instead of showing all zeros. (#46500) Fixes #46142. Thanks @ademczuk.
|
||||
- ACP/acpx: keep plugin-local backend installs under `extensions/acpx` in live repo checkouts so rebuilds no longer delete the runtime binary, and avoid package-lock churn during runtime repair.
|
||||
- Plugins/subagents: preserve gateway-owned plugin subagent access across runtime, tool, and embedded-runner load paths so gateway plugin tools and context engines can still spawn and manage subagents after the loader cache split. (#46648) Thanks @jalehman.
|
||||
- Control UI/overview: keep the language dropdown aligned with the persisted locale during dashboard startup so refreshing the page does not fall back to English before locale hydration completes. (#48019) Thanks @git-jxj.
|
||||
- Agents/compaction: rerun transcript repair after `session.compact()` so orphaned `tool_result` blocks cannot survive compaction and break later Anthropic requests. (#16095) thanks @claw-sylphx.
|
||||
- Agents/compaction: trigger overflow recovery from the tool-result guard once post-compaction context still exceeds the safe threshold, so long tool loops compact before the next model call hard-fails. (#29371) thanks @keshav55.
|
||||
- macOS/exec approvals: harden exec-host request HMAC verification to use a timing-safe compare and keep malformed or truncated signatures fail-closed in focused IPC auth coverage.
|
||||
- Gateway/exec approvals: surface requested env override keys in gateway-host approval prompts so operators can review surviving env context without inheriting noisy base host env.
|
||||
- Telegram/network: preserve sticky IPv4 fallback state across polling restarts so hosts with unstable IPv6 to `api.telegram.org` stop re-triggering repeated Telegram timeouts after each restart. (#48282) Thanks @yassinebkr.
|
||||
- Plugins/subagents: forward per-run provider and model overrides through gateway plugin subagent dispatch so plugin-launched agent delegations honor explicit model selection again. (#48277) Thanks @jalehman.
|
||||
- Agents/compaction: write minimal boundary summaries for empty preparations while keeping split-turn prefixes on the normal path, so no-summarizable-message sessions stop retriggering the safeguard loop. (#42215) thanks @lml2468.
|
||||
- Models/chat commands: keep `/model ...@YYYYMMDD` version suffixes intact by default, but still honor matching stored numeric auth-profile overrides for the same provider. (#48896) Thanks @Alix-007.
|
||||
- Gateway/channels: serialize per-account channel startup so overlapping starts do not boot the same provider twice, preventing MS Teams `EADDRINUSE` crash loops during startup and restart. (#49583) Thanks @sudie-codes.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Agents/bootstrap warnings: move bootstrap truncation warnings out of the system prompt and into the per-turn prompt body so prompt-cache reuse stays stable when truncation warnings appear or disappear. (#48753) Thanks @scoootscooob and @obviyus.
|
||||
- Telegram/DM topic session keys: route named-account DM topics through the same per-account base session key across inbound messages, native commands, and session-state lookups so `/status` and thread recovery stop creating phantom `agent:main:main:thread:...` sessions. (#48204) Thanks @vincentkoc.
|
||||
- macOS/node service startup: use `openclaw node start/stop --json` from the Mac app instead of the removed `openclaw service node ...` command shape, so current CLI installs expose the full node exec surface again. (#46843) Fixes #43171. Thanks @Br1an67.
|
||||
- macOS/launch at login: stop emitting `KeepAlive` for the desktop app launch agent so OpenClaw no longer relaunches immediately after a manual quit while launch at login remains enabled. (#40213) Thanks @stablegenius49.
|
||||
- ACP/gateway startup: use direct Telegram and Discord startup/status helpers instead of routing probes through the plugin runtime, and prepend the selected daemon Node bin dir to service PATH so plugin-local installs can still find `npm` and `pnpm`.
|
||||
- ACP/configured bindings: reinitialize configured ACP sessions that are stuck in `error` state instead of reusing the failed runtime.
|
||||
- Mattermost/DM send: retry transient direct-channel creation failures for DM deliveries, with configurable backoff and per-request timeout. (#42398) Thanks @JonathanJing.
|
||||
- Telegram/network: unify API and media fetches under the same sticky IPv4 and pinned-IP fallback chain, and re-validate pinned override addresses against SSRF policy. (#49148) Thanks @obviyus.
|
||||
- Agents/prompt composition: append bootstrap truncation warnings to the current-turn prompt and add regression coverage for stable system-prompt cache invariants. (#49237) Thanks @scoootscooob.
|
||||
- Gateway/auth: add regression coverage that keeps device-less trusted-proxy Control UI sessions off privileged pairing approval RPCs. Thanks @vincentkoc.
|
||||
- Plugins/runtime-api: pin extension runtime-api export surfaces with explicit guardrail coverage so future surface creep becomes a deliberate diff. Thanks @vincentkoc.
|
||||
- Telegram/security: add regression coverage proving pinned fallback host overrides stay bound to Telegram and delegate non-matching hostnames back to the original lookup path. Thanks @vincentkoc.
|
||||
- Secrets/exec refs: require explicit `--allow-exec` for `secrets apply` write plans that contain exec SecretRefs/providers, and align audit/configure/apply dry-run behavior to skip exec checks unless opted in to prevent unexpected command side effects. (#49417) Thanks @restriction and @joshavant.
|
||||
- Tools/image generation: add bundled fal image generation support so `image_generate` can target `fal/*` models with `FAL_KEY`, including single-image edit flows via FLUX image-to-image. Thanks @vincentkoc.
|
||||
- xAI/web search: add missing Grok credential metadata so the bundled provider registration type-checks again. (#49472) thanks @scoootscooob.
|
||||
- Signal/runtime API: re-export `SignalAccountConfig` so Signal account resolution type-checks again. (#49470) Thanks @scoootscooob.
|
||||
- Google Chat/runtime API: thin the private runtime barrel onto the curated public SDK surface while keeping public Google Chat exports intact. (#49504) Thanks @scoootscooob.
|
||||
|
||||
### Breaking
|
||||
|
||||
- Skills/image generation: remove the bundled `nano-banana-pro` skill wrapper. Use `agents.defaults.imageGenerationModel.primary: "google/gemini-3-pro-image-preview"` for the native Nano Banana-style path instead.
|
||||
|
||||
- Browser/Chrome MCP: remove the legacy Chrome extension relay path, bundled extension assets, `driver: "extension"`, and `browser.relayBindHost`. Run `openclaw doctor --fix` to migrate host-local browser config to `existing-session` / `user`; Docker, headless, sandbox, and remote browser flows still use raw CDP. (#47893) Thanks @vincentkoc.
|
||||
- Plugins/runtime: remove the public `openclaw/extension-api` surface with no compatibility shim. Bundled plugins must use injected runtime for host-side operations (for example `api.runtime.agent.runEmbeddedPiAgent`) and any remaining direct imports must come from narrow `openclaw/plugin-sdk/*` subpaths instead of the monolithic SDK root.
|
||||
- Tools/image generation: standardize the stock image create/edit path on the core `image_generate` tool. The old `nano-banana-pro` docs/examples are gone; if you previously copied that sample-skill config, switch to `agents.defaults.imageGenerationModel` for built-in image generation or install a separate third-party skill explicitly.
|
||||
- Skills/image generation: remove the bundled `nano-banana-pro` skill wrapper. Use `agents.defaults.imageGenerationModel.primary: "google/gemini-3-pro-image-preview"` for the native Nano Banana-style path instead.
|
||||
- Plugins/message discovery: require `ChannelMessageActionAdapter.describeMessageTool(...)` for shared `message` tool discovery. The legacy `listActions`, `getCapabilities`, and `getToolSchema` adapter methods are removed. Plugin authors should migrate message discovery to `describeMessageTool(...)` and keep channel-specific action runtime code inside the owning plugin package. Thanks @gumadeiras.
|
||||
- Exec/env sandbox: block build-tool JVM injection (`MAVEN_OPTS`, `SBT_OPTS`, `GRADLE_OPTS`, `ANT_OPTS`), glibc tunable exploitation (`GLIBC_TUNABLES`), and .NET dependency resolution hijack (`DOTNET_ADDITIONAL_DEPS`) from the host exec environment, and restrict Gradle init script redirect (`GRADLE_USER_HOME`) as an override-only block so user-configured Gradle homes still propagate. (#49702)
|
||||
|
||||
## 2026.3.13
|
||||
|
||||
@@ -94,10 +175,6 @@ Docs: https://docs.openclaw.ai
|
||||
- Cron/sessions: add `sessionTarget: "current"` and `session:<id>` support so cron jobs can bind to the creating session or a persistent named session instead of only `main` or `isolated`. Thanks @kkhomej33-netizen and @ImLukeF.
|
||||
- Telegram/message send: add `--force-document` so Telegram image and GIF sends can upload as documents without compression. (#45111) Thanks @thepagent.
|
||||
|
||||
### Breaking
|
||||
|
||||
- **BREAKING:** Agents now load at most one root memory bootstrap file. `MEMORY.md` wins; `memory.md` is only used when `MEMORY.md` is absent. If you intentionally kept both files and depended on both being injected, merge them before upgrade. This also fixes duplicate memory injection on case-insensitive Docker mounts. (#26054) Thanks @Lanfei.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Dashboard/chat UI: stop reloading full chat history on every live tool result in dashboard v2 so tool-heavy runs no longer trigger UI freeze/re-render storms while the final event still refreshes persisted history. (#45541) Thanks @BunsDev.
|
||||
@@ -157,6 +234,12 @@ Docs: https://docs.openclaw.ai
|
||||
- Auth/login lockout recovery: clear stale `auth_permanent` and `billing` disabled state for all profiles matching the target provider when `openclaw models auth login` is invoked, so users locked out by expired or revoked OAuth tokens can recover by re-authenticating instead of waiting for the cooldown timer to expire. (#43057)
|
||||
- Auto-reply/context-engine compaction: persist the exact embedded-run metadata compaction count for main and followup runner session accounting, so metadata-only auto-compactions no longer undercount multi-compaction runs. (#42629) thanks @uf-hy.
|
||||
- Auth/Codex CLI reuse: sync reused Codex CLI credentials into the supported `openai-codex:default` OAuth profile instead of reviving the deprecated `openai-codex:codex-cli` slot, so doctor cleanup no longer loops. (#45353) thanks @Gugu-sugar.
|
||||
- Deps/audit: bump the pinned `fast-xml-parser` override to the first patched release so `pnpm audit --prod --audit-level=high` no longer fails on the AWS Bedrock XML builder path. Thanks @vincentkoc.
|
||||
- Hooks/after_compaction: forward `sessionFile` for direct/manual compaction events and add `sessionFile` plus `sessionKey` to wired auto-compaction hook context so plugins receive the session metadata already declared in the hook types. (#40781) Thanks @jarimustonen.
|
||||
|
||||
### Breaking
|
||||
|
||||
- **BREAKING:** Agents now load at most one root memory bootstrap file. `MEMORY.md` wins; `memory.md` is only used when `MEMORY.md` is absent. If you intentionally kept both files and depended on both being injected, merge them before upgrade. This also fixes duplicate memory injection on case-insensitive Docker mounts. (#26054) Thanks @Lanfei.
|
||||
|
||||
## 2026.3.12
|
||||
|
||||
@@ -248,13 +331,16 @@ Docs: https://docs.openclaw.ai
|
||||
- Agents/Anthropic replay: drop replayed assistant thinking blocks for native Anthropic and Bedrock Claude providers so persisted follow-up turns no longer fail on stored thinking blocks. (#44843) Thanks @jmcte.
|
||||
- Docs/Brave pricing: escape literal dollar signs in Brave Search cost text so the docs render the free credit and per-request pricing correctly. (#44989) Thanks @keelanfh.
|
||||
- Feishu/file uploads: preserve literal UTF-8 filenames in `im.file.create` so Chinese and other non-ASCII filenames no longer appear percent-encoded in chat. (#34262) Thanks @fabiaodemianyang and @KangShuaiFu.
|
||||
- Agents/compaction safeguard: trim large kept `toolResult` payloads consistently for budgeting, pruning, and identifier seeding, then restore preserved payloads after prune so oversized safeguard summaries stay stable. (#44133) thanks @SayrWolfridge.
|
||||
- Agents/compaction: compare post-compaction token sanity checks against full-session pre-compaction totals and skip the check when token estimation fails, so sessions with large bootstrap context keep real token counts instead of falling back to unknown. (#28347) thanks @efe-arv.
|
||||
- Discord/gateway startup: treat plain-text and transient `/gateway/bot` metadata fetch failures as transient startup errors so Discord gateway boot no longer crashes on unhandled rejections. (#44397) Thanks @jalehman.
|
||||
- Agents/Ollama overflow: rewrite Ollama `prompt too long` API payloads through the normal context-overflow sanitizer so embedded sessions keep the friendly overflow copy and auto-compaction trigger. (#34019) thanks @lishuaigit.
|
||||
|
||||
- Control UI/auth: restore one-time legacy `?token=` imports for shared Control UI links while keeping `#token=` preferred, and carry pending query tokens through gateway URL confirmation so compatibility links still authenticate after confirmation. (#43979) Thanks @stim64045-spec.
|
||||
- Plugins/context engines: retry legacy lifecycle calls once without `sessionKey` when older plugins reject that field, memoize legacy mode after the first strict-schema fallback, and preserve non-compat runtime errors without retry. (#44779) thanks @hhhhao28.
|
||||
|
||||
## 2026.3.11
|
||||
|
||||
### Security
|
||||
|
||||
- Gateway/WebSocket: enforce browser origin validation for all browser-originated connections regardless of whether proxy headers are present, closing a cross-site WebSocket hijacking path in `trusted-proxy` mode that could grant untrusted origins `operator.admin` access. (GHSA-5wcw-8jjv-m286)
|
||||
|
||||
### Changes
|
||||
|
||||
- OpenRouter/models: add temporary Hunter Alpha and Healer Alpha entries to the built-in catalog so OpenRouter users can try the new free stealth models during their roughly one-week availability window. (#43642) Thanks @ping-Toven.
|
||||
@@ -276,10 +362,6 @@ Docs: https://docs.openclaw.ai
|
||||
- Mattermost/reply threading: add `channels.mattermost.replyToMode` for channel and group messages so top-level posts can start thread-scoped sessions without the manual reply-then-thread workaround. (#29587) Thanks @teconomix.
|
||||
- iOS/push relay: add relay-backed official-build push delivery with App Attest + receipt verification, gateway-bound send delegation, and config-based relay URL setup on the gateway. (#43369) Thanks @ngutman.
|
||||
|
||||
### Breaking
|
||||
|
||||
- Cron/doctor: tighten isolated cron delivery so cron jobs can no longer notify through ad hoc agent sends or fallback main-session summaries, and add `openclaw doctor --fix` migration for legacy cron storage and legacy notify/webhook delivery metadata. (#40998) Thanks @mbelinky.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Windows/install: stop auto-installing `node-llama-cpp` during normal npm CLI installs so `openclaw@latest` no longer fails on Windows while building optional local-embedding dependencies.
|
||||
@@ -390,6 +472,15 @@ Docs: https://docs.openclaw.ai
|
||||
- Memory/QMD Windows: fail closed when `qmd.cmd` or `mcporter.cmd` wrappers cannot be resolved to a direct entrypoint, so memory search no longer falls back to shell execution on Windows.
|
||||
- macOS/remote gateway: stop PortGuardian from killing Docker Desktop and other external listeners on the gateway port in remote mode, so containerized and tunneled gateway setups no longer lose their port-forward owner on app startup. (#6755) Thanks @teslamint.
|
||||
- Feishu/streaming recovery: clear stale `streamingStartPromise` when card creation fails (HTTP 400) so subsequent messages can retry streaming instead of silently dropping all future replies. Fixes #43322.
|
||||
- Exec/env sandbox: block JVM agent injection (`JAVA_TOOL_OPTIONS`, `_JAVA_OPTIONS`, `JDK_JAVA_OPTIONS`), Python breakpoint hijack (`PYTHONBREAKPOINT`), and .NET startup hooks (`DOTNET_STARTUP_HOOKS`) from the host exec environment. (#49025)
|
||||
|
||||
### Security
|
||||
|
||||
- Gateway/WebSocket: enforce browser origin validation for all browser-originated connections regardless of whether proxy headers are present, closing a cross-site WebSocket hijacking path in `trusted-proxy` mode that could grant untrusted origins `operator.admin` access. (GHSA-5wcw-8jjv-m286)
|
||||
|
||||
### Breaking
|
||||
|
||||
- Cron/doctor: tighten isolated cron delivery so cron jobs can no longer notify through ad hoc agent sends or fallback main-session summaries, and add `openclaw doctor --fix` migration for legacy cron storage and legacy notify/webhook delivery metadata. (#40998) Thanks @mbelinky.
|
||||
|
||||
## 2026.3.8
|
||||
|
||||
@@ -503,10 +594,6 @@ Docs: https://docs.openclaw.ai
|
||||
- Google/Gemini 3.1 Flash-Lite: add first-class `google/gemini-3.1-flash-lite-preview` support across model-id normalization, default aliases, media-understanding image lookups, Google Gemini CLI forward-compat fallback, and docs.
|
||||
- Agents/compaction model override: allow `agents.defaults.compaction.model` to route compaction summarization through a different model than the main session, and document the override across config help/reference surfaces. (#38753) thanks @starbuck100.
|
||||
|
||||
### Breaking
|
||||
|
||||
- **BREAKING:** Gateway auth now requires explicit `gateway.auth.mode` when both `gateway.auth.token` and `gateway.auth.password` are configured (including SecretRefs). Set `gateway.auth.mode` to `token` or `password` before upgrade to avoid startup/pairing/TUI failures. (#35094) Thanks @joshavant.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Models/MiniMax: stop advertising removed `MiniMax-M2.5-Lightning` in built-in provider catalogs, onboarding metadata, and docs; keep the supported fast-tier model as `MiniMax-M2.5-highspeed`.
|
||||
@@ -577,6 +664,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Control UI/markdown fallback regression coverage: add explicit regression assertions for parser-error fallback behavior so malformed markdown no longer risks reintroducing hard-crash rendering paths in future markdown/parser upgrades. (#36445) Thanks @BinHPdev.
|
||||
- Web UI/config form: treat `additionalProperties: true` object schemas as editable map entries instead of unsupported fields so Accounts-style maps stay editable in form mode. (#35380, supersedes #32072) Thanks @stakeswky and @liuxiaopai-ai.
|
||||
- Feishu/streaming card delivery synthesis: unify snapshot and delta streaming merge semantics, apply overlap-aware final merge, suppress duplicate final text delivery (including text+media final packets), prefer topic-thread `message.reply` routing when a reply target exists, and tune card print cadence to avoid duplicate incremental rendering. (from #33245, #32896, #33840) Thanks @rexl2018, @kcinzgg, and @aerelune.
|
||||
- macOS/tray menu: keep injected sessions and device rows below the controls section so toggles and action buttons stay visible even when many sessions are active. (#38079) Thanks @bernesto.
|
||||
- Feishu/group mention detection: carry startup-probed bot display names through monitor dispatch so `requireMention` checks compare against current bot identity instead of stale config names, fixing missed `@bot` handling in groups while preserving multi-bot false-positive guards. (#36317, #34271) Thanks @liuxiaopai-ai.
|
||||
- Security/dependency audit: patch transitive Hono vulnerabilities by pinning `hono` to `4.12.5` and `@hono/node-server` to `1.19.10` in production resolution paths. Thanks @shakkernerd.
|
||||
- Security/dependency audit: bump `tar` to `7.5.10` (from `7.5.9`) to address the high-severity hardlink path traversal advisory (`GHSA-qffp-2rhf-9h96`). Thanks @shakkernerd.
|
||||
@@ -831,6 +919,10 @@ Docs: https://docs.openclaw.ai
|
||||
- Mattermost/DM media uploads: resolve bare 26-character Mattermost IDs user-first for direct messages so media sends no longer fail with `403 Forbidden` when targets are configured as unprefixed user IDs. (#29925) Thanks @teconomix.
|
||||
- Voice-call/OpenAI TTS config parity: add missing `speed`, `instructions`, and `baseUrl` fields to the OpenAI TTS config schema and gate `instructions` to supported models so voice-call overrides validate and route cleanly through core TTS. (#39226) Thanks @ademczuk.
|
||||
|
||||
### Breaking
|
||||
|
||||
- **BREAKING:** Gateway auth now requires explicit `gateway.auth.mode` when both `gateway.auth.token` and `gateway.auth.password` are configured (including SecretRefs). Set `gateway.auth.mode` to `token` or `password` before upgrade to avoid startup/pairing/TUI failures. (#35094) Thanks @joshavant.
|
||||
|
||||
## 2026.3.2
|
||||
|
||||
### Changes
|
||||
@@ -859,13 +951,6 @@ Docs: https://docs.openclaw.ai
|
||||
- Gateway/input_image MIME validation: sniff uploaded image bytes before MIME allowlist enforcement again so declared image types cannot mask concrete non-image payloads, while keeping HEIC/HEIF normalization behavior scoped to actual HEIC inputs. Thanks @vincentkoc.
|
||||
- Zalo Personal plugin (`@openclaw/zalouser`): keep canonical DM routing while preserving legacy DM session continuity on upgrade, and preserve provider-native `g-`/`u-` target ids in outbound send and directory flows so #33992 lands without breaking existing sessions or stored targets. (#33992) Thanks @darkamenosa.
|
||||
|
||||
### Breaking
|
||||
|
||||
- **BREAKING:** Onboarding now defaults `tools.profile` to `messaging` for new local installs (interactive + non-interactive). New setups no longer start with broad coding/system tools unless explicitly configured.
|
||||
- **BREAKING:** ACP dispatch now defaults to enabled unless explicitly disabled (`acp.dispatch.enabled=false`). If you need to pause ACP turn routing while keeping `/acp` controls, set `acp.dispatch.enabled=false`. Docs: https://docs.openclaw.ai/tools/acp-agents
|
||||
- **BREAKING:** Plugin SDK removed `api.registerHttpHandler(...)`. Plugins must register explicit HTTP routes via `api.registerHttpRoute({ path, auth, match, handler })`, and dynamic webhook lifecycles should use `registerPluginHttpRoute(...)`.
|
||||
- **BREAKING:** Zalo Personal plugin (`@openclaw/zalouser`) no longer depends on external `zca`-compatible CLI binaries (`openzca`, `zca-cli`) for runtime send/listen/login; operators should use `openclaw channels login --channel zalouser` after upgrade to refresh sessions in the new JS-native path.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Feishu/Outbound render mode: respect Feishu account `renderMode` in outbound sends so card mode (and auto-detected markdown tables/code blocks) uses markdown card delivery instead of always sending plain text. (#31562) Thanks @arkyu2077.
|
||||
@@ -1052,6 +1137,13 @@ Docs: https://docs.openclaw.ai
|
||||
- Tests/Subagent announce: set `OPENCLAW_TEST_FAST=1` before importing `subagent-announce` format suites so module-level fast-mode constants are captured deterministically on Windows CI, preventing timeout flakes in nested completion announce coverage. (#31370) Thanks @zwffff.
|
||||
- Control UI/markdown recursion fallback: catch markdown parser failures and safely render escaped plain-text fallback instead of crashing the Control UI on pathological markdown history payloads. (#36445, fixes #36213) Thanks @BinHPdev.
|
||||
|
||||
### Breaking
|
||||
|
||||
- **BREAKING:** Onboarding now defaults `tools.profile` to `messaging` for new local installs (interactive + non-interactive). New setups no longer start with broad coding/system tools unless explicitly configured.
|
||||
- **BREAKING:** ACP dispatch now defaults to enabled unless explicitly disabled (`acp.dispatch.enabled=false`). If you need to pause ACP turn routing while keeping `/acp` controls, set `acp.dispatch.enabled=false`. Docs: https://docs.openclaw.ai/tools/acp-agents
|
||||
- **BREAKING:** Plugin SDK removed `api.registerHttpHandler(...)`. Plugins must register explicit HTTP routes via `api.registerHttpRoute({ path, auth, match, handler })`, and dynamic webhook lifecycles should use `registerPluginHttpRoute(...)`.
|
||||
- **BREAKING:** Zalo Personal plugin (`@openclaw/zalouser`) no longer depends on external `zca`-compatible CLI binaries (`openzca`, `zca-cli`) for runtime send/listen/login; operators should use `openclaw channels login --channel zalouser` after upgrade to refresh sessions in the new JS-native path.
|
||||
|
||||
## 2026.3.1
|
||||
|
||||
### Changes
|
||||
@@ -1079,11 +1171,6 @@ Docs: https://docs.openclaw.ai
|
||||
- OpenAI/WebSocket warm-up: add optional OpenAI Responses WebSocket warm-up (`response.create` with `generate:false`), enable it by default for `openai/*`, and expose `params.openaiWsWarmup` for per-model enable/disable control.
|
||||
- Agents/Subagents runtime events: replace ad-hoc subagent completion system-message handoff with typed internal completion events (`task_completion`) that are rendered consistently across direct and queued announce paths, with gateway/CLI plumbing for structured `internalEvents`.
|
||||
|
||||
### Breaking
|
||||
|
||||
- **BREAKING:** Node exec approval payloads now require `systemRunPlan`. `host=node` approval requests without that plan are rejected.
|
||||
- **BREAKING:** Node `system.run` execution now pins path-token commands to the canonical executable path (`realpath`) in both allowlist and approval execution flows. Integrations/tests that asserted token-form argv (for example `tr`) must now accept canonical paths (for example `/usr/bin/tr`).
|
||||
|
||||
### Fixes
|
||||
|
||||
- Feishu/Streaming card text fidelity: merge throttled/fragmented partial updates without dropping content and avoid newline injection when stitching chunk-style deltas so card-stream output matches final reply text. (#29616) Thanks @HaoHuaqing.
|
||||
@@ -1178,7 +1265,12 @@ Docs: https://docs.openclaw.ai
|
||||
- Signal/Sync message null-handling: treat `syncMessage` presence (including `null`) as sync envelope traffic so replayed sentTranscript payloads cannot bypass loop guards after daemon restart. Landed from contributor PR #31138 by @Sid-Qin. Thanks @Sid-Qin.
|
||||
- Infra/fs-safe: sanitize directory-read failures so raw `EISDIR` text never leaks to messaging surfaces, with regression tests for both root-scoped and direct safe reads. Landed from contributor PR #31205 by @polooooo. Thanks @polooooo.
|
||||
|
||||
## Unreleased
|
||||
### Breaking
|
||||
|
||||
- **BREAKING:** Node exec approval payloads now require `systemRunPlan`. `host=node` approval requests without that plan are rejected.
|
||||
- **BREAKING:** Node `system.run` execution now pins path-token commands to the canonical executable path (`realpath`) in both allowlist and approval execution flows. Integrations/tests that asserted token-form argv (for example `tr`) must now accept canonical paths (for example `/usr/bin/tr`).
|
||||
|
||||
## 2026.2.27
|
||||
|
||||
### Changes
|
||||
|
||||
@@ -1453,10 +1545,6 @@ Docs: https://docs.openclaw.ai
|
||||
- Agents/Config: remind agents to call `config.schema` before config edits or config-field questions to avoid guessing. Thanks @thewilloftheshadow.
|
||||
- Dependencies: update workspace dependency pins and lockfile (Bedrock SDK `3.998.0`, `@mariozechner/pi-*` `0.55.1`, TypeScript native preview `7.0.0-dev.20260225.1`) while keeping `@buape/carbon` pinned.
|
||||
|
||||
### Breaking
|
||||
|
||||
- **BREAKING:** Heartbeat direct/DM delivery default is now `allow` again. To keep DM-blocked behavior from `2026.2.24`, set `agents.defaults.heartbeat.directPolicy: "block"` (or per-agent override).
|
||||
|
||||
### Fixes
|
||||
|
||||
- Slack/Identity: thread agent outbound identity (`chat:write.customize` overrides) through the channel reply delivery path so per-agent username, icon URL, and icon emoji are applied to all Slack replies including media messages. (#27134) Thanks @hou-rong.
|
||||
@@ -1520,6 +1608,10 @@ Docs: https://docs.openclaw.ai
|
||||
- Tests/Low-memory stability: disable Vitest `vmForks` by default on low-memory local hosts (`<64 GiB`), keep low-profile extension lane parallelism at 4 workers, and align cron isolated-agent tests with `setSessionRuntimeModel` usage to avoid deterministic suite failures. (#26324) Thanks @ngutman.
|
||||
- Feishu/WebSocket proxy: pass a proxy agent to Feishu WS clients from standard proxy environment variables and include plugin-local runtime dependency wiring so websocket mode works in proxy-constrained installs. (#26397) Thanks @colin719.
|
||||
|
||||
### Breaking
|
||||
|
||||
- **BREAKING:** Heartbeat direct/DM delivery default is now `allow` again. To keep DM-blocked behavior from `2026.2.24`, set `agents.defaults.heartbeat.directPolicy: "block"` (or per-agent override).
|
||||
|
||||
## 2026.2.24
|
||||
|
||||
### Changes
|
||||
@@ -1530,11 +1622,6 @@ Docs: https://docs.openclaw.ai
|
||||
- Security/Audit: add `security.trust_model.multi_user_heuristic` to flag likely shared-user ingress and clarify the personal-assistant trust model, with hardening guidance for intentional multi-user setups (`sandbox.mode="all"`, workspace-scoped FS, reduced tool surface, no personal/private identities on shared runtimes).
|
||||
- Dependencies: refresh key runtime and tooling packages across the workspace (Bedrock SDK, pi runtime stack, OpenAI, Google auth, and oxlint/oxfmt), while intentionally keeping `@buape/carbon` pinned.
|
||||
|
||||
### Breaking
|
||||
|
||||
- **BREAKING:** Heartbeat delivery now blocks direct/DM targets when destination parsing identifies a direct chat (for example `user:<id>`, Telegram user chat IDs, or WhatsApp direct numbers/JIDs). Heartbeat runs still execute, but direct-message delivery is skipped and only non-DM destinations (for example channel/group targets) can receive outbound heartbeat messages.
|
||||
- **BREAKING:** Security/Sandbox: block Docker `network: "container:<id>"` namespace-join mode by default for sandbox and sandbox-browser containers. To keep that behavior intentionally, set `agents.defaults.sandbox.docker.dangerouslyAllowContainerNamespaceJoin: true` (break-glass). Thanks @tdjackey for reporting.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Routing/Session isolation: harden followup routing so explicit cross-channel origin replies never fall back to the active dispatcher on route failure, preserve queued overflow summary routing metadata (`channel`/`to`/`thread`) across followup drain, and prefer originating channel context over internal provider tags for embedded followup runs. This prevents webchat/control-ui context from hijacking Discord-targeted replies in shared sessions. (#25864) Thanks @Gamedesigner.
|
||||
@@ -1614,6 +1701,11 @@ Docs: https://docs.openclaw.ai
|
||||
- Agents/Compaction: harden summarization prompts to preserve opaque identifiers verbatim (UUIDs, IDs, tokens, host/IP/port, URLs), reducing post-compaction identifier drift and hallucinated identifier reconstruction.
|
||||
- Security/Sandbox: canonicalize bind-mount source paths via existing-ancestor realpath so symlink-parent + non-existent-leaf paths cannot bypass allowed-source-roots or blocked-path checks. Thanks @tdjackey.
|
||||
|
||||
### Breaking
|
||||
|
||||
- **BREAKING:** Heartbeat delivery now blocks direct/DM targets when destination parsing identifies a direct chat (for example `user:<id>`, Telegram user chat IDs, or WhatsApp direct numbers/JIDs). Heartbeat runs still execute, but direct-message delivery is skipped and only non-DM destinations (for example channel/group targets) can receive outbound heartbeat messages.
|
||||
- **BREAKING:** Security/Sandbox: block Docker `network: "container:<id>"` namespace-join mode by default for sandbox and sandbox-browser containers. To keep that behavior intentionally, set `agents.defaults.sandbox.docker.dangerouslyAllowContainerNamespaceJoin: true` (break-glass). Thanks @tdjackey for reporting.
|
||||
|
||||
## 2026.2.23
|
||||
|
||||
### Changes
|
||||
@@ -1628,10 +1720,6 @@ Docs: https://docs.openclaw.ai
|
||||
- Agents/Config: support per-agent `params` overrides merged on top of model defaults (including `cacheRetention`) so mixed-traffic agents can tune cache behavior independently. (#17470, #17112) Thanks @rrenamed.
|
||||
- Agents/Bootstrap: cache bootstrap file snapshots per session key and clear them on session reset/delete, reducing prompt-cache invalidations from in-session `AGENTS.md`/`MEMORY.md` writes. (#22220) Thanks @anisoptera.
|
||||
|
||||
### Breaking
|
||||
|
||||
- **BREAKING:** browser SSRF policy now defaults to trusted-network mode (`browser.ssrfPolicy.dangerouslyAllowPrivateNetwork=true` when unset), and canonical config uses `browser.ssrfPolicy.dangerouslyAllowPrivateNetwork` instead of `browser.ssrfPolicy.allowPrivateNetwork`. `openclaw doctor --fix` migrates the legacy key automatically.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Security/Config: redact sensitive-looking dynamic catchall keys in `config.get` snapshots (for example `env.*` and `skills.entries.*.env.*`) and preserve round-trip restore behavior for those redacted sentinels. Thanks @merc1305.
|
||||
@@ -1677,6 +1765,10 @@ Docs: https://docs.openclaw.ai
|
||||
- Skills/Python: harden skill script packaging and validation edge cases (self-including `.skill` outputs, CRLF frontmatter parsing, strict `--days` validation, and safer image file loading), with expanded Python regression coverage. Thanks @vincentkoc.
|
||||
- Skills/Python: add CI + pre-commit linting (`ruff`) and pytest discovery coverage for Python scripts/tests under `skills/`, including package test execution from repo root. Thanks @vincentkoc.
|
||||
|
||||
### Breaking
|
||||
|
||||
- **BREAKING:** browser SSRF policy now defaults to trusted-network mode (`browser.ssrfPolicy.dangerouslyAllowPrivateNetwork=true` when unset), and canonical config uses `browser.ssrfPolicy.dangerouslyAllowPrivateNetwork` instead of `browser.ssrfPolicy.allowPrivateNetwork`. `openclaw doctor --fix` migrates the legacy key automatically.
|
||||
|
||||
## 2026.2.22
|
||||
|
||||
### Changes
|
||||
@@ -1701,14 +1793,6 @@ Docs: https://docs.openclaw.ai
|
||||
- Skills: remove bundled `food-order` skill from this repo; manage/install it from ClawHub instead.
|
||||
- Docs/Subagents: make thread-bound session guidance channel-first instead of Discord-specific, and list thread-supporting channels explicitly. (#23589) Thanks @osolmaz.
|
||||
|
||||
### Breaking
|
||||
|
||||
- **BREAKING:** removed Google Antigravity provider support and the bundled `google-antigravity-auth` plugin. Existing `google-antigravity/*` model/profile configs no longer work; migrate to `google-gemini-cli` or other supported providers.
|
||||
- **BREAKING:** tool-failure replies now hide raw error details by default. OpenClaw still sends a failure summary, but detailed error suffixes (for example provider/runtime messages and local path fragments) now require `/verbose on` or `/verbose full`.
|
||||
- **BREAKING:** CLI local onboarding now sets `session.dmScope` to `per-channel-peer` by default for new/implicit DM scope configuration. If you depend on shared DM continuity across senders, explicitly set `session.dmScope` to `main`. (#23468) Thanks @bmendonca3.
|
||||
- **BREAKING:** unify channel preview-streaming config to `channels.<channel>.streaming` with enum values `off | partial | block | progress`, and move Slack native stream toggle to `channels.slack.nativeStreaming`. Legacy keys (`streamMode`, Slack boolean `streaming`) are still read and migrated by `openclaw doctor --fix`, but canonical saved config/docs now use the unified names.
|
||||
- **BREAKING:** remove legacy Gateway device-auth signature `v1`. Device-auth clients must now sign `v2` payloads with the per-connection `connect.challenge` nonce and send `device.nonce`; nonce-less connects are rejected.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Sessions/Resilience: ignore invalid persisted `sessionFile` metadata and fall back to the derived safe transcript path instead of aborting session resolution for handlers and tooling. (#16061) Thanks @haoyifan and @vincentkoc.
|
||||
@@ -1935,6 +2019,14 @@ Docs: https://docs.openclaw.ai
|
||||
- Gateway/Daemon: verify gateway health after daemon restart.
|
||||
- Agents/UI text: stop rewriting normal assistant billing/payment language outside explicit error contexts. (#17834) Thanks @niceysam.
|
||||
|
||||
### Breaking
|
||||
|
||||
- **BREAKING:** removed Google Antigravity provider support and the bundled `google-antigravity-auth` plugin. Existing `google-antigravity/*` model/profile configs no longer work; migrate to `google-gemini-cli` or other supported providers.
|
||||
- **BREAKING:** tool-failure replies now hide raw error details by default. OpenClaw still sends a failure summary, but detailed error suffixes (for example provider/runtime messages and local path fragments) now require `/verbose on` or `/verbose full`.
|
||||
- **BREAKING:** CLI local onboarding now sets `session.dmScope` to `per-channel-peer` by default for new/implicit DM scope configuration. If you depend on shared DM continuity across senders, explicitly set `session.dmScope` to `main`. (#23468) Thanks @bmendonca3.
|
||||
- **BREAKING:** unify channel preview-streaming config to `channels.<channel>.streaming` with enum values `off | partial | block | progress`, and move Slack native stream toggle to `channels.slack.nativeStreaming`. Legacy keys (`streamMode`, Slack boolean `streaming`) are still read and migrated by `openclaw doctor --fix`, but canonical saved config/docs now use the unified names.
|
||||
- **BREAKING:** remove legacy Gateway device-auth signature `v1`. Device-auth clients must now sign `v2` payloads with the per-connection `connect.challenge` nonce and send `device.nonce`; nonce-less connects are rejected.
|
||||
|
||||
## 2026.2.21
|
||||
|
||||
### Changes
|
||||
@@ -2583,10 +2675,6 @@ Docs: https://docs.openclaw.ai
|
||||
- Onboarding/Providers: add first-class Hugging Face Inference provider support (provider wiring, onboarding auth choice/API key flow, and default-model selection), and preserve Hugging Face auth intent in auth-choice remapping (`tokenProvider=huggingface` with `authChoice=apiKey`) while skipping env-override prompts when an explicit token is provided. (#13472) Thanks @Josephrp.
|
||||
- Onboarding/Providers: add `minimax-api-key-cn` auth choice for the MiniMax China API endpoint. (#15191) Thanks @liuy.
|
||||
|
||||
### Breaking
|
||||
|
||||
- Config/State: removed legacy `.moltbot` auto-detection/migration and `moltbot.json` config candidates. If you still have state/config under `~/.moltbot`, move it to `~/.openclaw` (recommended) or set `OPENCLAW_STATE_DIR` / `OPENCLAW_CONFIG_PATH` explicitly.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Gateway/Auth: add trusted-proxy mode hardening follow-ups by keeping `OPENCLAW_GATEWAY_*` env compatibility, auto-normalizing invalid setup combinations in interactive `gateway configure` (trusted-proxy forces `bind=lan` and disables Tailscale serve/funnel), and suppressing shared-secret/rate-limit audit findings that do not apply to trusted-proxy deployments. (#15940) Thanks @nickytonline.
|
||||
@@ -2689,6 +2777,10 @@ Docs: https://docs.openclaw.ai
|
||||
- Docs/Mermaid: remove hardcoded Mermaid init theme blocks from four docs diagrams so dark mode inherits readable theme defaults. (#15157) Thanks @heytulsiprasad.
|
||||
- Security/Pairing: generate 256-bit base64url device and node pairing tokens and use byte-safe constant-time verification to avoid token-compare edge-case failures. (#16535) Thanks @FaizanKolega, @gumadeiras.
|
||||
|
||||
### Breaking
|
||||
|
||||
- Config/State: removed legacy `.moltbot` auto-detection/migration and `moltbot.json` config candidates. If you still have state/config under `~/.moltbot`, move it to `~/.openclaw` (recommended) or set `OPENCLAW_STATE_DIR` / `OPENCLAW_CONFIG_PATH` explicitly.
|
||||
|
||||
## 2026.2.12
|
||||
|
||||
### Changes
|
||||
@@ -2700,10 +2792,6 @@ Docs: https://docs.openclaw.ai
|
||||
- Discord: add role-based allowlists and role-based agent routing. (#10650) Thanks @Minidoracat.
|
||||
- Config: avoid redacting `maxTokens`-like fields during config snapshot redaction, preventing round-trip validation failures in `/config`. (#14006) Thanks @constansino.
|
||||
|
||||
### Breaking
|
||||
|
||||
- Hooks: `POST /hooks/agent` now rejects payload `sessionKey` overrides by default. To keep fixed hook context, set `hooks.defaultSessionKey` (recommended with `hooks.allowedSessionKeyPrefixes: ["hook:"]`). If you need legacy behavior, explicitly set `hooks.allowRequestSessionKey: true`. Thanks @alpernae for reporting.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Gateway/OpenResponses: harden URL-based `input_file`/`input_image` handling with explicit SSRF deny policy, hostname allowlists (`files.urlAllowlist` / `images.urlAllowlist`), per-request URL input caps (`maxUrlParts`), blocked-fetch audit logging, and regression coverage/docs updates.
|
||||
@@ -2786,6 +2874,10 @@ Docs: https://docs.openclaw.ai
|
||||
- Tests: update thread ID handling in Slack message collection tests. (#14108) Thanks @swizzmagik.
|
||||
- Update/Daemon: fix post-update restart compatibility by generating `dist/cli/daemon-cli.js` with alias-aware exports from hashed daemon bundles, preventing `registerDaemonCli` import failures during `openclaw update`.
|
||||
|
||||
### Breaking
|
||||
|
||||
- Hooks: `POST /hooks/agent` now rejects payload `sessionKey` overrides by default. To keep fixed hook context, set `hooks.defaultSessionKey` (recommended with `hooks.allowedSessionKeyPrefixes: ["hook:"]`). If you need legacy behavior, explicitly set `hooks.allowRequestSessionKey: true`. Thanks @alpernae for reporting.
|
||||
|
||||
## 2026.2.9
|
||||
|
||||
### Added
|
||||
@@ -2875,6 +2967,12 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
## 2026.2.6
|
||||
|
||||
### Added
|
||||
|
||||
- Cron: run history deep-links to session chat from the dashboard. (#10776) Thanks @tyler6204.
|
||||
- Cron: per-run session keys in run log entries and default labels for cron sessions. (#10776) Thanks @tyler6204.
|
||||
- Cron: legacy payload field compatibility (`deliver`, `channel`, `to`, `bestEffortDeliver`) in schema. (#10776) Thanks @tyler6204.
|
||||
|
||||
### Changes
|
||||
|
||||
- Cron: default `wakeMode` is now `"now"` for new jobs (was `"next-heartbeat"`). (#10776) Thanks @tyler6204.
|
||||
@@ -2890,12 +2988,6 @@ Docs: https://docs.openclaw.ai
|
||||
- CI: optimize pipeline throughput (macOS consolidation, Windows perf, workflow concurrency). (#10784) Thanks @mcaxtr.
|
||||
- Agents: bump pi-mono to 0.52.7; add embedded forward-compat fallback for Opus 4.6 model ids.
|
||||
|
||||
### Added
|
||||
|
||||
- Cron: run history deep-links to session chat from the dashboard. (#10776) Thanks @tyler6204.
|
||||
- Cron: per-run session keys in run log entries and default labels for cron sessions. (#10776) Thanks @tyler6204.
|
||||
- Cron: legacy payload field compatibility (`deliver`, `channel`, `to`, `bestEffortDeliver`) in schema. (#10776) Thanks @tyler6204.
|
||||
|
||||
### Fixes
|
||||
|
||||
- TTS: add missing OpenAI voices (ballad, cedar, juniper, marin, verse) to the allowlist so they are recognized instead of silently falling back to Edge TTS. (#2393)
|
||||
@@ -3194,10 +3286,6 @@ Docs: https://docs.openclaw.ai
|
||||
- Docs: keep docs header sticky so navbar stays visible while scrolling. (#2445) Thanks @chenyuan99.
|
||||
- Docs: update exe.dev install instructions. (#https://github.com/openclaw/openclaw/pull/3047) Thanks @zackerthescar.
|
||||
|
||||
### Breaking
|
||||
|
||||
- **BREAKING:** Gateway auth mode "none" is removed; gateway now requires token/password (Tailscale Serve identity still allowed).
|
||||
|
||||
### Fixes
|
||||
|
||||
- Skills: update session-logs paths to use ~/.openclaw. (#4502) Thanks @bonald.
|
||||
@@ -3250,6 +3338,10 @@ Docs: https://docs.openclaw.ai
|
||||
- Gateway: treat loopback + non-local Host connections as remote unless trusted proxy headers are present.
|
||||
- Onboarding: remove unsupported gateway auth "off" choice from onboarding/configure flows and CLI flags.
|
||||
|
||||
### Breaking
|
||||
|
||||
- **BREAKING:** Gateway auth mode "none" is removed; gateway now requires token/password (Tailscale Serve identity still allowed).
|
||||
|
||||
## 2026.1.24-3
|
||||
|
||||
### Fixes
|
||||
@@ -3481,11 +3573,6 @@ Docs: https://docs.openclaw.ai
|
||||
- Docs: add /model allowlist troubleshooting note. (#1405)
|
||||
- Docs: add per-message Gmail search example for gog. (#1220) Thanks @mbelinky.
|
||||
|
||||
### Breaking
|
||||
|
||||
- **BREAKING:** Control UI now rejects insecure HTTP without device identity by default. Use HTTPS (Tailscale Serve) or set `gateway.controlUi.allowInsecureAuth: true` to allow token-only auth. https://docs.openclaw.ai/web/control-ui#insecure-http
|
||||
- **BREAKING:** Envelope and system event timestamps now default to host-local time (was UTC) so agents don’t have to constantly convert.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Nodes/macOS: prompt on allowlist miss for node exec approvals, persist allowlist decisions, and flatten node invoke errors. (#1394) Thanks @ngutman.
|
||||
@@ -3508,6 +3595,11 @@ Docs: https://docs.openclaw.ai
|
||||
- macOS: default distribution packaging to universal binaries. (#1396) Thanks @JustYannicc.
|
||||
- Embedded runner: forward sender identity into attempt execution so Feishu doc auto-grant receives requester context again. (#32915) Thanks @cszhouwei.
|
||||
|
||||
### Breaking
|
||||
|
||||
- **BREAKING:** Control UI now rejects insecure HTTP without device identity by default. Use HTTPS (Tailscale Serve) or set `gateway.controlUi.allowInsecureAuth: true` to allow token-only auth. https://docs.openclaw.ai/web/control-ui#insecure-http
|
||||
- **BREAKING:** Envelope and system event timestamps now default to host-local time (was UTC) so agents don’t have to constantly convert.
|
||||
|
||||
## 2026.1.20
|
||||
|
||||
### Changes
|
||||
@@ -3589,10 +3681,6 @@ Docs: https://docs.openclaw.ai
|
||||
- macOS: stop syncing Peekaboo in postinstall.
|
||||
- Swabble: use the tagged Commander Swift package release.
|
||||
|
||||
### Breaking
|
||||
|
||||
- **BREAKING:** Reject invalid/unknown config entries and refuse to start the gateway for safety. Run `openclaw doctor --fix` to repair, then update plugins (`openclaw plugins update`) if you use any.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Discovery: shorten Bonjour DNS-SD service type to `_moltbot-gw._tcp` and update discovery clients/docs.
|
||||
@@ -3691,6 +3779,10 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @NicholaiVogel, @RyanLisse, @ThePickle31, @VACInc, @Whoaa512, @YuriNachos, @aaronveklabs, @abdaraxus, @alauppe, @ameno-, @artuskg, @austinm911, @bradleypriest, @cheeeee, @dougvk, @fogboots, @gnarco, @gumadeiras, @jdrhyne, @joelklabo, @longmaba, @mukhtharcm, @odysseus0, @oscargavin, @rhjoh, @sebslight, @sibbl, @sleontenko, @steipete, @suminhthanh, @thewilloftheshadow, @tyler6204, @vignesh07, @visionik, @ysqander, @zerone0x.
|
||||
|
||||
### Breaking
|
||||
|
||||
- **BREAKING:** Reject invalid/unknown config entries and refuse to start the gateway for safety. Run `openclaw doctor --fix` to repair, then update plugins (`openclaw plugins update`) if you use any.
|
||||
|
||||
## 2026.1.16-2
|
||||
|
||||
### Changes
|
||||
@@ -3709,15 +3801,6 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
|
||||
- Sessions: add `session.identityLinks` for cross-platform DM session li nking. (#1033) — thanks @thewilloftheshadow. https://docs.openclaw.ai/concepts/session
|
||||
- Web search: add `country`/`language` parameters (schema + Brave API) and docs. (#1046) — thanks @YuriNachos. https://docs.openclaw.ai/tools/web
|
||||
|
||||
### Breaking
|
||||
|
||||
- **BREAKING:** `openclaw message` and message tool now require `target` (dropping `to`/`channelId` for destinations). (#1034) — thanks @tobalsan.
|
||||
- **BREAKING:** Channel auth now prefers config over env for Discord/Telegram/Matrix (env is fallback only). (#1040) — thanks @thewilloftheshadow.
|
||||
- **BREAKING:** Drop legacy `chatType: "room"` support; use `chatType: "channel"`.
|
||||
- **BREAKING:** remove legacy provider-specific target resolution fallbacks; target resolution is centralized with plugin hints + directory lookups.
|
||||
- **BREAKING:** `openclaw hooks` is now `openclaw webhooks`; hooks live under `openclaw hooks`. https://docs.openclaw.ai/cli/webhooks
|
||||
- **BREAKING:** `openclaw plugins install <path>` now copies into `~/.openclaw/extensions` (use `--link` to keep path-based loading).
|
||||
|
||||
### Changes
|
||||
|
||||
- Plugins: ship bundled plugins disabled by default and allow overrides by installed versions. (#1066) — thanks @ItzR3NO.
|
||||
@@ -3809,6 +3892,15 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
|
||||
- Discord: preserve whitespace when chunking long lines so message splits keep spacing intact.
|
||||
- Skills: fix skills watcher ignored list typing (tsc).
|
||||
|
||||
### Breaking
|
||||
|
||||
- **BREAKING:** `openclaw message` and message tool now require `target` (dropping `to`/`channelId` for destinations). (#1034) — thanks @tobalsan.
|
||||
- **BREAKING:** Channel auth now prefers config over env for Discord/Telegram/Matrix (env is fallback only). (#1040) — thanks @thewilloftheshadow.
|
||||
- **BREAKING:** Drop legacy `chatType: "room"` support; use `chatType: "channel"`.
|
||||
- **BREAKING:** remove legacy provider-specific target resolution fallbacks; target resolution is centralized with plugin hints + directory lookups.
|
||||
- **BREAKING:** `openclaw hooks` is now `openclaw webhooks`; hooks live under `openclaw hooks`. https://docs.openclaw.ai/cli/webhooks
|
||||
- **BREAKING:** `openclaw plugins install <path>` now copies into `~/.openclaw/extensions` (use `--link` to keep path-based loading).
|
||||
|
||||
## 2026.1.15
|
||||
|
||||
### Highlights
|
||||
@@ -3818,11 +3910,6 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
|
||||
- Heartbeat: per-agent configuration + 24h duplicate suppression. (#980) — thanks @voidserf.
|
||||
- Security: audit warns on weak model tiers; app nodes store auth tokens encrypted (Keychain/SecurePrefs).
|
||||
|
||||
### Breaking
|
||||
|
||||
- **BREAKING:** iOS minimum version is now 18.0 to support Textual markdown rendering in native chat. (#702)
|
||||
- **BREAKING:** Microsoft Teams is now a plugin; install `@openclaw/msteams` via `openclaw plugins install @openclaw/msteams`.
|
||||
|
||||
### Changes
|
||||
|
||||
- UI/Apps: move channel/config settings to schema-driven forms and rename Connections → Channels. (#1040) — thanks @thewilloftheshadow.
|
||||
@@ -3895,6 +3982,11 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
|
||||
- Fix: allow local Tailscale Serve hostnames without treating tailnet clients as direct. (#885) — thanks @oswalpalash.
|
||||
- Fix: reset sessions after role-ordering conflicts to recover from consecutive user turns. (#998)
|
||||
|
||||
### Breaking
|
||||
|
||||
- **BREAKING:** iOS minimum version is now 18.0 to support Textual markdown rendering in native chat. (#702)
|
||||
- **BREAKING:** Microsoft Teams is now a plugin; install `@openclaw/msteams` via `openclaw plugins install @openclaw/msteams`.
|
||||
|
||||
## 2026.1.14-1
|
||||
|
||||
### Highlights
|
||||
@@ -4031,10 +4123,6 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
|
||||
- Gateway: allow Tailscale Serve identity headers to satisfy token auth; rebuild Control UI assets when protocol schema is newer. (#823) — thanks @roshanasingh4; (#786) — thanks @meaningfool.
|
||||
- Heartbeat: default `ackMaxChars` to 300 so short `HEARTBEAT_OK` replies stay internal.
|
||||
|
||||
### Installer
|
||||
|
||||
- Install: run `openclaw doctor --non-interactive` after git installs/updates and nudge daemon restarts when detected.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Doctor: warn on pnpm workspace mismatches, missing Control UI assets, and missing tsx binaries; offer UI rebuilds.
|
||||
@@ -4060,6 +4148,10 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
|
||||
- Tools/UI: harden tool input schemas for strict providers; drop null-only union variants for Gemini schema cleanup; treat `maxChars: 0` as unlimited; keep TUI last streamed response instead of "(no output)". (#782) — thanks @AbhisekBasu1; (#796) — thanks @gabriel-trigo; (#747) — thanks @thewilloftheshadow.
|
||||
- Connections UI: polish multi-account account cards. (#816) — thanks @steipete.
|
||||
|
||||
### Installer
|
||||
|
||||
- Install: run `openclaw doctor --non-interactive` after git installs/updates and nudge daemon restarts when detected.
|
||||
|
||||
### Maintenance
|
||||
|
||||
- Dependencies: bump Pi packages to 0.45.3 and refresh patched pi-ai.
|
||||
@@ -4111,15 +4203,6 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
|
||||
- Gateway: require `client.id` in WebSocket connect params; use `client.instanceId` for presence de-dupe; update docs/tests.
|
||||
- macOS: remove the attach-only gateway setting; local mode now always manages launchd while still attaching to an existing gateway if present.
|
||||
|
||||
### Installer
|
||||
|
||||
- Postinstall: replace `git apply` with builtin JS patcher (works npm/pnpm/bun; no git dependency) plus regression tests.
|
||||
- Postinstall: skip pnpm patch fallback when the new patcher is active.
|
||||
- Installer tests: add root+non-root docker smokes, CI workflow to fetch openclaw.ai scripts and run install sh/cli with onboarding skipped.
|
||||
- Installer UX: support `CLAWDBOT_NO_ONBOARD=1` for non-interactive installs; fix npm prefix on Linux and auto-install git.
|
||||
- Installer UX: add `install.sh --help` with flags/env and git install hint.
|
||||
- Installer UX: add `--install-method git|npm` and auto-detect source checkouts (prompt to update git checkout vs migrate to npm).
|
||||
|
||||
### Fixes
|
||||
|
||||
- Models/Onboarding: configure MiniMax (minimax.io) via Anthropic-compatible `/anthropic` endpoint by default (keep `minimax-api` as a legacy alias).
|
||||
@@ -4158,6 +4241,15 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
|
||||
- Sandbox/Gateway: treat `agent:<id>:main` as a main-session alias when `session.mainKey` is customized (backwards compatible).
|
||||
- Auto-reply: fast-path allowlisted slash commands (inline `/help`/`/commands`/`/status`/`/whoami` stripped before model).
|
||||
|
||||
### Installer
|
||||
|
||||
- Postinstall: replace `git apply` with builtin JS patcher (works npm/pnpm/bun; no git dependency) plus regression tests.
|
||||
- Postinstall: skip pnpm patch fallback when the new patcher is active.
|
||||
- Installer tests: add root+non-root docker smokes, CI workflow to fetch openclaw.ai scripts and run install sh/cli with onboarding skipped.
|
||||
- Installer UX: support `CLAWDBOT_NO_ONBOARD=1` for non-interactive installs; fix npm prefix on Linux and auto-install git.
|
||||
- Installer UX: add `install.sh --help` with flags/env and git install hint.
|
||||
- Installer UX: add `--install-method git|npm` and auto-detect source checkouts (prompt to update git checkout vs migrate to npm).
|
||||
|
||||
## 2026.1.10
|
||||
|
||||
### Highlights
|
||||
@@ -4266,11 +4358,6 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
|
||||
- Auto-reply + status: block-streaming controls, reasoning handling, usage/cost reporting.
|
||||
- Control UI/TUI: queued messages, session links, reasoning view, mobile polish, logs UX.
|
||||
|
||||
### Breaking
|
||||
|
||||
- CLI: `openclaw message` now subcommands (`message send|poll|...`) and requires `--provider` unless only one provider configured.
|
||||
- Commands/Tools: `/restart` and gateway restart tool disabled by default; enable with `commands.restart=true`.
|
||||
|
||||
### New Features and Changes
|
||||
|
||||
- Models/Auth: OpenCode Zen onboarding (#623) — thanks @magimetal; MiniMax Anthropic-compatible API + hosted onboarding (#590, #495) — thanks @mneves75, @tobiasbischoff.
|
||||
@@ -4312,6 +4399,11 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
|
||||
- Onboarding/Configure: QuickStart single-select provider picker; avoid Codex CLI false-expiry warnings; clarify WhatsApp owner prompt; fix Minimax hosted onboarding (agents.defaults + msteams heartbeat target); remove configure Control UI prompt; honor gateway --dev flag.
|
||||
- Agent loop: guard overflow compaction throws and restore compaction hooks for engine-owned context engines. (#41361) — thanks @davidrudduck
|
||||
|
||||
### Breaking
|
||||
|
||||
- CLI: `openclaw message` now subcommands (`message send|poll|...`) and requires `--provider` unless only one provider configured.
|
||||
- Commands/Tools: `/restart` and gateway restart tool disabled by default; enable with `commands.restart=true`.
|
||||
|
||||
### Maintenance
|
||||
|
||||
- Dependencies: bump pi-\* stack to 0.42.2.
|
||||
@@ -4331,6 +4423,18 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
|
||||
- Control UI: logs tab, streaming stability, focus mode, and large-output rendering fixes.
|
||||
- CLI/Gateway/Doctor: daemon/logs/status, auth migration, and diagnostics significantly expanded.
|
||||
|
||||
### Fixes
|
||||
|
||||
- **CLI/Gateway/Doctor:** daemon runtime selection + improved logs/status/health/errors; auth/password handling for local CLI; richer close/timeout details; auto-migrate legacy config/sessions/state; integrity checks + repair prompts; `--yes`/`--non-interactive`; `--deep` gateway scans; better restart/service hints.
|
||||
- **Agent loop + compaction:** compaction/pruning tuning, overflow handling, safer bootstrap context, and per-provider threading/confirmations; opt-in tool-result pruning + compact tracking.
|
||||
- **Sandbox + tools:** per-agent sandbox overrides, workspaceAccess controls, session tool visibility, tool policy overrides, process isolation, and tool schema/timeout/reaction unification.
|
||||
- **Providers (Telegram/WhatsApp/Discord/Slack/Signal/iMessage):** retry/backoff, threading, reactions, media groups/attachments, mention gating, typing behavior, and error/log stability; long polling + forum topic isolation for Telegram.
|
||||
- **Gateway/CLI UX:** `openclaw logs`, cron list colors/aliases, docs search, agents list/add/delete flows, status usage snapshots, runtime/auth source display, and `/status`/commands auth unification.
|
||||
- **Control UI/Web:** logs tab, focus mode polish, config form resilience, streaming stability, tool output caps, windowed chat history, and reconnect/password URL auth.
|
||||
- **macOS/Android/TUI/Build:** macOS gateway races, QR bundling, JSON5 config safety, Voice Wake hardening; Android EXIF rotation + APK naming/versioning; TUI key handling; tooling/bundling fixes.
|
||||
- **Packaging/compat:** npm dist folder coverage, Node 25 qrcode-terminal import fixes, Bun/Playwright/WebSocket patches, and Docker Bun install.
|
||||
- **Docs:** new FAQ/ClawHub/config examples/showcase entries and clarified auth, sandbox, and systemd docs.
|
||||
|
||||
### Breaking
|
||||
|
||||
- **SECURITY (update ASAP):** inbound DMs are now **locked down by default** on Telegram/WhatsApp/Signal/iMessage/Discord/Slack.
|
||||
@@ -4346,18 +4450,6 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
|
||||
- Auto-reply: removed `autoReply` from Discord/Slack/Telegram channel configs; use `requireMention` instead (Telegram topics now support `requireMention` overrides).
|
||||
- CLI: remove `update`, `gateway-daemon`, `gateway {install|uninstall|start|stop|restart|daemon status|wake|send|agent}`, and `telegram` commands; move `login/logout` to `providers login/logout` (top-level aliases hidden); use `daemon` for service control, `send`/`agent`/`wake` for RPC, and `nodes canvas` for canvas ops.
|
||||
|
||||
### Fixes
|
||||
|
||||
- **CLI/Gateway/Doctor:** daemon runtime selection + improved logs/status/health/errors; auth/password handling for local CLI; richer close/timeout details; auto-migrate legacy config/sessions/state; integrity checks + repair prompts; `--yes`/`--non-interactive`; `--deep` gateway scans; better restart/service hints.
|
||||
- **Agent loop + compaction:** compaction/pruning tuning, overflow handling, safer bootstrap context, and per-provider threading/confirmations; opt-in tool-result pruning + compact tracking.
|
||||
- **Sandbox + tools:** per-agent sandbox overrides, workspaceAccess controls, session tool visibility, tool policy overrides, process isolation, and tool schema/timeout/reaction unification.
|
||||
- **Providers (Telegram/WhatsApp/Discord/Slack/Signal/iMessage):** retry/backoff, threading, reactions, media groups/attachments, mention gating, typing behavior, and error/log stability; long polling + forum topic isolation for Telegram.
|
||||
- **Gateway/CLI UX:** `openclaw logs`, cron list colors/aliases, docs search, agents list/add/delete flows, status usage snapshots, runtime/auth source display, and `/status`/commands auth unification.
|
||||
- **Control UI/Web:** logs tab, focus mode polish, config form resilience, streaming stability, tool output caps, windowed chat history, and reconnect/password URL auth.
|
||||
- **macOS/Android/TUI/Build:** macOS gateway races, QR bundling, JSON5 config safety, Voice Wake hardening; Android EXIF rotation + APK naming/versioning; TUI key handling; tooling/bundling fixes.
|
||||
- **Packaging/compat:** npm dist folder coverage, Node 25 qrcode-terminal import fixes, Bun/Playwright/WebSocket patches, and Docker Bun install.
|
||||
- **Docs:** new FAQ/ClawHub/config examples/showcase entries and clarified auth, sandbox, and systemd docs.
|
||||
|
||||
### Maintenance
|
||||
|
||||
- Skills additions (Himalaya email, CodexBar, 1Password).
|
||||
|
||||
@@ -47,7 +47,7 @@ Welcome to the lobster tank! 🦞
|
||||
- **Christoph Nakazawa** - JS Infra
|
||||
- GitHub: [@cpojer](https://github.com/cpojer) · X: [@cnakazawa](https://x.com/cnakazawa)
|
||||
|
||||
- **Gustavo Madeira Santana** - Multi-agents, CLI, web UI
|
||||
- **Gustavo Madeira Santana** - Multi-agents, CLI, Performance, Plugins, Matrix
|
||||
- GitHub: [@gumadeiras](https://github.com/gumadeiras) · X: [@gumadeiras](https://x.com/gumadeiras)
|
||||
|
||||
- **Onur Solmaz** - Agents, dev workflows, ACP integrations, MS Teams
|
||||
@@ -89,6 +89,12 @@ Welcome to the lobster tank! 🦞
|
||||
|
||||
- Test locally with your OpenClaw instance
|
||||
- Run tests: `pnpm build && pnpm check && pnpm test`
|
||||
- For extension/plugin changes, run the fast local lane first:
|
||||
- `pnpm test:extension <extension-name>`
|
||||
- `pnpm test:extension --list` to see valid extension ids
|
||||
- If you changed shared plugin or channel surfaces, run `pnpm test:contracts`
|
||||
- For targeted shared-surface work, use `pnpm test:contracts:channels` or `pnpm test:contracts:plugins`
|
||||
- If you changed broader runtime behavior, still run the relevant wider lanes (`pnpm test:extensions`, `pnpm test:channels`, or `pnpm test`) before asking for review
|
||||
- If you have access to Codex, run `codex review --base origin/main` locally before opening or updating your PR. Treat this as the current highest standard of AI review, even if GitHub Codex review also runs.
|
||||
- Ensure CI checks pass
|
||||
- Keep PRs focused (one thing per PR; do not mix unrelated concerns)
|
||||
|
||||
18
README.md
18
README.md
@@ -23,10 +23,10 @@ It answers you on the channels you already use (WhatsApp, Telegram, Slack, Disco
|
||||
|
||||
If you want a personal, single-user assistant that feels local, fast, and always-on, this is it.
|
||||
|
||||
[Website](https://openclaw.ai) · [Docs](https://docs.openclaw.ai) · [Vision](VISION.md) · [DeepWiki](https://deepwiki.com/openclaw/openclaw) · [Getting Started](https://docs.openclaw.ai/start/getting-started) · [Updating](https://docs.openclaw.ai/install/updating) · [Showcase](https://docs.openclaw.ai/start/showcase) · [FAQ](https://docs.openclaw.ai/help/faq) · [Wizard](https://docs.openclaw.ai/start/wizard) · [Nix](https://github.com/openclaw/nix-openclaw) · [Docker](https://docs.openclaw.ai/install/docker) · [Discord](https://discord.gg/clawd)
|
||||
[Website](https://openclaw.ai) · [Docs](https://docs.openclaw.ai) · [Vision](VISION.md) · [DeepWiki](https://deepwiki.com/openclaw/openclaw) · [Getting Started](https://docs.openclaw.ai/start/getting-started) · [Updating](https://docs.openclaw.ai/install/updating) · [Showcase](https://docs.openclaw.ai/start/showcase) · [FAQ](https://docs.openclaw.ai/help/faq) · [Onboarding](https://docs.openclaw.ai/start/wizard) · [Nix](https://github.com/openclaw/nix-openclaw) · [Docker](https://docs.openclaw.ai/install/docker) · [Discord](https://discord.gg/clawd)
|
||||
|
||||
Preferred setup: run the onboarding wizard (`openclaw onboard`) in your terminal.
|
||||
The wizard guides you step by step through setting up the gateway, workspace, channels, and skills. The CLI wizard is the recommended path and works on **macOS, Linux, and Windows (via WSL2; strongly recommended)**.
|
||||
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)**.
|
||||
Works with npm, pnpm, or bun.
|
||||
New install? Start here: [Getting started](https://docs.openclaw.ai/start/getting-started)
|
||||
|
||||
@@ -58,7 +58,7 @@ npm install -g openclaw@latest
|
||||
openclaw onboard --install-daemon
|
||||
```
|
||||
|
||||
The wizard installs the Gateway daemon (launchd/systemd user service) so it stays running.
|
||||
OpenClaw Onboard installs the Gateway daemon (launchd/systemd user service) so it stays running.
|
||||
|
||||
## Quick start (TL;DR)
|
||||
|
||||
@@ -132,7 +132,7 @@ Run `openclaw doctor` to surface risky/misconfigured DM policies.
|
||||
- **[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).
|
||||
- **[Onboarding](https://docs.openclaw.ai/start/wizard) + [skills](https://docs.openclaw.ai/tools/skills)** — wizard-driven setup with bundled/managed/workspace skills.
|
||||
- **[Onboarding](https://docs.openclaw.ai/start/wizard) + [skills](https://docs.openclaw.ai/tools/skills)** — onboarding-driven setup with bundled/managed/workspace skills.
|
||||
|
||||
## Star History
|
||||
|
||||
@@ -143,7 +143,7 @@ Run `openclaw doctor` to surface risky/misconfigured DM policies.
|
||||
### Core platform
|
||||
|
||||
- [Gateway WS control plane](https://docs.openclaw.ai/gateway) with sessions, presence, config, cron, webhooks, [Control UI](https://docs.openclaw.ai/web), and [Canvas host](https://docs.openclaw.ai/platforms/mac/canvas#canvas-a2ui).
|
||||
- [CLI surface](https://docs.openclaw.ai/tools/agent-send): gateway, agent, send, [wizard](https://docs.openclaw.ai/start/wizard), and [doctor](https://docs.openclaw.ai/gateway/doctor).
|
||||
- [CLI surface](https://docs.openclaw.ai/tools/agent-send): gateway, agent, send, [onboarding](https://docs.openclaw.ai/start/wizard), and [doctor](https://docs.openclaw.ai/gateway/doctor).
|
||||
- [Pi agent runtime](https://docs.openclaw.ai/concepts/agent) in RPC mode with tool streaming and block streaming.
|
||||
- [Session model](https://docs.openclaw.ai/concepts/session): `main` for direct chats, group isolation, activation modes, queue modes, reply-back. Group rules: [Groups](https://docs.openclaw.ai/channels/groups).
|
||||
- [Media pipeline](https://docs.openclaw.ai/nodes/images): images/audio/video, transcription hooks, size caps, temp file lifecycle. Audio details: [Audio](https://docs.openclaw.ai/nodes/audio).
|
||||
@@ -293,7 +293,7 @@ If you plan to build/run companion apps, follow the platform runbooks below.
|
||||
- WebChat + debug tools.
|
||||
- Remote gateway control over SSH.
|
||||
|
||||
Note: signed builds required for macOS permissions to stick across rebuilds (see `docs/mac/permissions.md`).
|
||||
Note: signed builds required for macOS permissions to stick across rebuilds (see [macOS Permissions](https://docs.openclaw.ai/platforms/mac/permissions)).
|
||||
|
||||
### iOS node (optional)
|
||||
|
||||
@@ -364,7 +364,7 @@ Details: [Security guide](https://docs.openclaw.ai/gateway/security) · [Docker
|
||||
|
||||
### [Discord](https://docs.openclaw.ai/channels/discord)
|
||||
|
||||
- Set `DISCORD_BOT_TOKEN` or `channels.discord.token` (env wins).
|
||||
- Set `DISCORD_BOT_TOKEN` or `channels.discord.token`.
|
||||
- Optional: set `commands.native`, `commands.text`, or `commands.useAccessGroups`, plus `channels.discord.allowFrom`, `channels.discord.guilds`, or `channels.discord.mediaMaxMb` as needed.
|
||||
|
||||
```json5
|
||||
@@ -422,7 +422,7 @@ Use these when you’re past the onboarding flow and want the deeper reference.
|
||||
- [Run the Gateway by the book with the operational runbook.](https://docs.openclaw.ai/gateway)
|
||||
- [Learn how the Control UI/Web surfaces work and how to expose them safely.](https://docs.openclaw.ai/web)
|
||||
- [Understand remote access over SSH tunnels or tailnets.](https://docs.openclaw.ai/gateway/remote)
|
||||
- [Follow the onboarding wizard flow for a guided setup.](https://docs.openclaw.ai/start/wizard)
|
||||
- [Follow OpenClaw Onboard for a guided setup.](https://docs.openclaw.ai/start/wizard)
|
||||
- [Wire external triggers via the webhook surface.](https://docs.openclaw.ai/automation/webhook)
|
||||
- [Set up Gmail Pub/Sub triggers.](https://docs.openclaw.ai/automation/gmail-pubsub)
|
||||
- [Learn the macOS menu bar companion details.](https://docs.openclaw.ai/platforms/mac/menu-bar)
|
||||
|
||||
@@ -18,14 +18,13 @@ import kotlinx.coroutines.launch
|
||||
class MainActivity : ComponentActivity() {
|
||||
private val viewModel: MainViewModel by viewModels()
|
||||
private lateinit var permissionRequester: PermissionRequester
|
||||
private var didAttachRuntimeUi = false
|
||||
private var didStartNodeService = false
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
permissionRequester = PermissionRequester(this)
|
||||
viewModel.camera.attachLifecycleOwner(this)
|
||||
viewModel.camera.attachPermissionRequester(permissionRequester)
|
||||
viewModel.sms.attachPermissionRequester(permissionRequester)
|
||||
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
@@ -39,6 +38,20 @@ class MainActivity : ComponentActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
viewModel.runtimeInitialized.collect { ready ->
|
||||
if (!ready || didAttachRuntimeUi) return@collect
|
||||
viewModel.attachRuntimeUi(owner = this@MainActivity, permissionRequester = permissionRequester)
|
||||
didAttachRuntimeUi = true
|
||||
if (!didStartNodeService) {
|
||||
NodeForegroundService.start(this@MainActivity)
|
||||
didStartNodeService = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setContent {
|
||||
OpenClawTheme {
|
||||
Surface(modifier = Modifier) {
|
||||
@@ -46,9 +59,6 @@ class MainActivity : ComponentActivity() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Keep startup path lean: start foreground service after first frame.
|
||||
window.decorView.post { NodeForegroundService.start(this) }
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
|
||||
@@ -2,209 +2,268 @@ package ai.openclaw.app
|
||||
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import ai.openclaw.app.gateway.GatewayEndpoint
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import ai.openclaw.app.chat.ChatMessage
|
||||
import ai.openclaw.app.chat.ChatPendingToolCall
|
||||
import ai.openclaw.app.chat.ChatSessionEntry
|
||||
import ai.openclaw.app.chat.OutgoingAttachment
|
||||
import ai.openclaw.app.gateway.GatewayEndpoint
|
||||
import ai.openclaw.app.node.CameraCaptureManager
|
||||
import ai.openclaw.app.node.CanvasController
|
||||
import ai.openclaw.app.node.SmsManager
|
||||
import ai.openclaw.app.voice.VoiceConversationEntry
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class MainViewModel(app: Application) : AndroidViewModel(app) {
|
||||
private val runtime: NodeRuntime = (app as NodeApp).runtime
|
||||
private val nodeApp = app as NodeApp
|
||||
private val prefs = nodeApp.prefs
|
||||
private val runtimeRef = MutableStateFlow<NodeRuntime?>(null)
|
||||
private var foreground = true
|
||||
|
||||
val canvas: CanvasController = runtime.canvas
|
||||
val canvasCurrentUrl: StateFlow<String?> = runtime.canvas.currentUrl
|
||||
val canvasA2uiHydrated: StateFlow<Boolean> = runtime.canvasA2uiHydrated
|
||||
val canvasRehydratePending: StateFlow<Boolean> = runtime.canvasRehydratePending
|
||||
val canvasRehydrateErrorText: StateFlow<String?> = runtime.canvasRehydrateErrorText
|
||||
val camera: CameraCaptureManager = runtime.camera
|
||||
val sms: SmsManager = runtime.sms
|
||||
private fun ensureRuntime(): NodeRuntime {
|
||||
runtimeRef.value?.let { return it }
|
||||
val runtime = nodeApp.ensureRuntime()
|
||||
runtime.setForeground(foreground)
|
||||
runtimeRef.value = runtime
|
||||
return runtime
|
||||
}
|
||||
|
||||
val gateways: StateFlow<List<GatewayEndpoint>> = runtime.gateways
|
||||
val discoveryStatusText: StateFlow<String> = runtime.discoveryStatusText
|
||||
private fun <T> runtimeState(
|
||||
initial: T,
|
||||
selector: (NodeRuntime) -> StateFlow<T>,
|
||||
): StateFlow<T> =
|
||||
runtimeRef
|
||||
.flatMapLatest { runtime -> runtime?.let(selector) ?: flowOf(initial) }
|
||||
.stateIn(viewModelScope, SharingStarted.Eagerly, initial)
|
||||
|
||||
val isConnected: StateFlow<Boolean> = runtime.isConnected
|
||||
val isNodeConnected: StateFlow<Boolean> = runtime.nodeConnected
|
||||
val statusText: StateFlow<String> = runtime.statusText
|
||||
val serverName: StateFlow<String?> = runtime.serverName
|
||||
val remoteAddress: StateFlow<String?> = runtime.remoteAddress
|
||||
val pendingGatewayTrust: StateFlow<NodeRuntime.GatewayTrustPrompt?> = runtime.pendingGatewayTrust
|
||||
val isForeground: StateFlow<Boolean> = runtime.isForeground
|
||||
val seamColorArgb: StateFlow<Long> = runtime.seamColorArgb
|
||||
val mainSessionKey: StateFlow<String> = runtime.mainSessionKey
|
||||
val runtimeInitialized: StateFlow<Boolean> =
|
||||
runtimeRef
|
||||
.flatMapLatest { runtime -> flowOf(runtime != null) }
|
||||
.stateIn(viewModelScope, SharingStarted.Eagerly, false)
|
||||
|
||||
val cameraHud: StateFlow<CameraHudState?> = runtime.cameraHud
|
||||
val cameraFlashToken: StateFlow<Long> = runtime.cameraFlashToken
|
||||
val canvasCurrentUrl: StateFlow<String?> = runtimeState(initial = null) { it.canvas.currentUrl }
|
||||
val canvasA2uiHydrated: StateFlow<Boolean> = runtimeState(initial = false) { it.canvasA2uiHydrated }
|
||||
val canvasRehydratePending: StateFlow<Boolean> = runtimeState(initial = false) { it.canvasRehydratePending }
|
||||
val canvasRehydrateErrorText: StateFlow<String?> = runtimeState(initial = null) { it.canvasRehydrateErrorText }
|
||||
|
||||
val instanceId: StateFlow<String> = runtime.instanceId
|
||||
val displayName: StateFlow<String> = runtime.displayName
|
||||
val cameraEnabled: StateFlow<Boolean> = runtime.cameraEnabled
|
||||
val locationMode: StateFlow<LocationMode> = runtime.locationMode
|
||||
val locationPreciseEnabled: StateFlow<Boolean> = runtime.locationPreciseEnabled
|
||||
val preventSleep: StateFlow<Boolean> = runtime.preventSleep
|
||||
val micEnabled: StateFlow<Boolean> = runtime.micEnabled
|
||||
val micCooldown: StateFlow<Boolean> = runtime.micCooldown
|
||||
val micStatusText: StateFlow<String> = runtime.micStatusText
|
||||
val micLiveTranscript: StateFlow<String?> = runtime.micLiveTranscript
|
||||
val micIsListening: StateFlow<Boolean> = runtime.micIsListening
|
||||
val micQueuedMessages: StateFlow<List<String>> = runtime.micQueuedMessages
|
||||
val micConversation: StateFlow<List<VoiceConversationEntry>> = runtime.micConversation
|
||||
val micInputLevel: StateFlow<Float> = runtime.micInputLevel
|
||||
val micIsSending: StateFlow<Boolean> = runtime.micIsSending
|
||||
val speakerEnabled: StateFlow<Boolean> = runtime.speakerEnabled
|
||||
val manualEnabled: StateFlow<Boolean> = runtime.manualEnabled
|
||||
val manualHost: StateFlow<String> = runtime.manualHost
|
||||
val manualPort: StateFlow<Int> = runtime.manualPort
|
||||
val manualTls: StateFlow<Boolean> = runtime.manualTls
|
||||
val gatewayToken: StateFlow<String> = runtime.gatewayToken
|
||||
val onboardingCompleted: StateFlow<Boolean> = runtime.onboardingCompleted
|
||||
val canvasDebugStatusEnabled: StateFlow<Boolean> = runtime.canvasDebugStatusEnabled
|
||||
val gateways: StateFlow<List<GatewayEndpoint>> = runtimeState(initial = emptyList()) { it.gateways }
|
||||
val discoveryStatusText: StateFlow<String> = runtimeState(initial = "Searching…") { it.discoveryStatusText }
|
||||
|
||||
val chatSessionKey: StateFlow<String> = runtime.chatSessionKey
|
||||
val chatSessionId: StateFlow<String?> = runtime.chatSessionId
|
||||
val chatMessages = runtime.chatMessages
|
||||
val chatError: StateFlow<String?> = runtime.chatError
|
||||
val chatHealthOk: StateFlow<Boolean> = runtime.chatHealthOk
|
||||
val chatThinkingLevel: StateFlow<String> = runtime.chatThinkingLevel
|
||||
val chatStreamingAssistantText: StateFlow<String?> = runtime.chatStreamingAssistantText
|
||||
val chatPendingToolCalls = runtime.chatPendingToolCalls
|
||||
val chatSessions = runtime.chatSessions
|
||||
val pendingRunCount: StateFlow<Int> = runtime.pendingRunCount
|
||||
val isConnected: StateFlow<Boolean> = runtimeState(initial = false) { it.isConnected }
|
||||
val isNodeConnected: StateFlow<Boolean> = runtimeState(initial = false) { it.nodeConnected }
|
||||
val statusText: StateFlow<String> = runtimeState(initial = "Offline") { it.statusText }
|
||||
val serverName: StateFlow<String?> = runtimeState(initial = null) { it.serverName }
|
||||
val remoteAddress: StateFlow<String?> = runtimeState(initial = null) { it.remoteAddress }
|
||||
val pendingGatewayTrust: StateFlow<NodeRuntime.GatewayTrustPrompt?> = runtimeState(initial = null) { it.pendingGatewayTrust }
|
||||
val seamColorArgb: StateFlow<Long> = runtimeState(initial = 0xFF0EA5E9) { it.seamColorArgb }
|
||||
val mainSessionKey: StateFlow<String> = runtimeState(initial = "main") { it.mainSessionKey }
|
||||
|
||||
val cameraHud: StateFlow<CameraHudState?> = runtimeState(initial = null) { it.cameraHud }
|
||||
val cameraFlashToken: StateFlow<Long> = runtimeState(initial = 0L) { it.cameraFlashToken }
|
||||
|
||||
val instanceId: StateFlow<String> = prefs.instanceId
|
||||
val displayName: StateFlow<String> = prefs.displayName
|
||||
val cameraEnabled: StateFlow<Boolean> = prefs.cameraEnabled
|
||||
val locationMode: StateFlow<LocationMode> = prefs.locationMode
|
||||
val locationPreciseEnabled: StateFlow<Boolean> = prefs.locationPreciseEnabled
|
||||
val preventSleep: StateFlow<Boolean> = prefs.preventSleep
|
||||
val manualEnabled: StateFlow<Boolean> = prefs.manualEnabled
|
||||
val manualHost: StateFlow<String> = prefs.manualHost
|
||||
val manualPort: StateFlow<Int> = prefs.manualPort
|
||||
val manualTls: StateFlow<Boolean> = prefs.manualTls
|
||||
val gatewayToken: StateFlow<String> = prefs.gatewayToken
|
||||
val onboardingCompleted: StateFlow<Boolean> = prefs.onboardingCompleted
|
||||
val canvasDebugStatusEnabled: StateFlow<Boolean> = prefs.canvasDebugStatusEnabled
|
||||
val speakerEnabled: StateFlow<Boolean> = prefs.speakerEnabled
|
||||
val micEnabled: StateFlow<Boolean> = prefs.talkEnabled
|
||||
|
||||
val micCooldown: StateFlow<Boolean> = runtimeState(initial = false) { it.micCooldown }
|
||||
val micStatusText: StateFlow<String> = runtimeState(initial = "Mic off") { it.micStatusText }
|
||||
val micLiveTranscript: StateFlow<String?> = runtimeState(initial = null) { it.micLiveTranscript }
|
||||
val micIsListening: StateFlow<Boolean> = runtimeState(initial = false) { it.micIsListening }
|
||||
val micQueuedMessages: StateFlow<List<String>> = runtimeState(initial = emptyList()) { it.micQueuedMessages }
|
||||
val micConversation: StateFlow<List<VoiceConversationEntry>> = runtimeState(initial = emptyList()) { it.micConversation }
|
||||
val micInputLevel: StateFlow<Float> = runtimeState(initial = 0f) { it.micInputLevel }
|
||||
val micIsSending: StateFlow<Boolean> = runtimeState(initial = false) { it.micIsSending }
|
||||
|
||||
val chatSessionKey: StateFlow<String> = runtimeState(initial = "main") { it.chatSessionKey }
|
||||
val chatSessionId: StateFlow<String?> = runtimeState(initial = null) { it.chatSessionId }
|
||||
val chatMessages: StateFlow<List<ChatMessage>> = runtimeState(initial = emptyList()) { it.chatMessages }
|
||||
val chatError: StateFlow<String?> = runtimeState(initial = null) { it.chatError }
|
||||
val chatHealthOk: StateFlow<Boolean> = runtimeState(initial = false) { it.chatHealthOk }
|
||||
val chatThinkingLevel: StateFlow<String> = runtimeState(initial = "off") { it.chatThinkingLevel }
|
||||
val chatStreamingAssistantText: StateFlow<String?> = runtimeState(initial = null) { it.chatStreamingAssistantText }
|
||||
val chatPendingToolCalls: StateFlow<List<ChatPendingToolCall>> = runtimeState(initial = emptyList()) { it.chatPendingToolCalls }
|
||||
val chatSessions: StateFlow<List<ChatSessionEntry>> = runtimeState(initial = emptyList()) { it.chatSessions }
|
||||
val pendingRunCount: StateFlow<Int> = runtimeState(initial = 0) { it.pendingRunCount }
|
||||
|
||||
init {
|
||||
if (prefs.onboardingCompleted.value) {
|
||||
ensureRuntime()
|
||||
}
|
||||
}
|
||||
|
||||
val canvas: CanvasController
|
||||
get() = ensureRuntime().canvas
|
||||
|
||||
val camera: CameraCaptureManager
|
||||
get() = ensureRuntime().camera
|
||||
|
||||
val sms: SmsManager
|
||||
get() = ensureRuntime().sms
|
||||
|
||||
fun attachRuntimeUi(owner: LifecycleOwner, permissionRequester: PermissionRequester) {
|
||||
val runtime = runtimeRef.value ?: return
|
||||
runtime.camera.attachLifecycleOwner(owner)
|
||||
runtime.camera.attachPermissionRequester(permissionRequester)
|
||||
runtime.sms.attachPermissionRequester(permissionRequester)
|
||||
}
|
||||
|
||||
fun setForeground(value: Boolean) {
|
||||
runtime.setForeground(value)
|
||||
foreground = value
|
||||
runtimeRef.value?.setForeground(value)
|
||||
}
|
||||
|
||||
fun setDisplayName(value: String) {
|
||||
runtime.setDisplayName(value)
|
||||
prefs.setDisplayName(value)
|
||||
}
|
||||
|
||||
fun setCameraEnabled(value: Boolean) {
|
||||
runtime.setCameraEnabled(value)
|
||||
prefs.setCameraEnabled(value)
|
||||
}
|
||||
|
||||
fun setLocationMode(mode: LocationMode) {
|
||||
runtime.setLocationMode(mode)
|
||||
prefs.setLocationMode(mode)
|
||||
}
|
||||
|
||||
fun setLocationPreciseEnabled(value: Boolean) {
|
||||
runtime.setLocationPreciseEnabled(value)
|
||||
prefs.setLocationPreciseEnabled(value)
|
||||
}
|
||||
|
||||
fun setPreventSleep(value: Boolean) {
|
||||
runtime.setPreventSleep(value)
|
||||
prefs.setPreventSleep(value)
|
||||
}
|
||||
|
||||
fun setManualEnabled(value: Boolean) {
|
||||
runtime.setManualEnabled(value)
|
||||
prefs.setManualEnabled(value)
|
||||
}
|
||||
|
||||
fun setManualHost(value: String) {
|
||||
runtime.setManualHost(value)
|
||||
prefs.setManualHost(value)
|
||||
}
|
||||
|
||||
fun setManualPort(value: Int) {
|
||||
runtime.setManualPort(value)
|
||||
prefs.setManualPort(value)
|
||||
}
|
||||
|
||||
fun setManualTls(value: Boolean) {
|
||||
runtime.setManualTls(value)
|
||||
prefs.setManualTls(value)
|
||||
}
|
||||
|
||||
fun setGatewayToken(value: String) {
|
||||
runtime.setGatewayToken(value)
|
||||
prefs.setGatewayToken(value)
|
||||
}
|
||||
|
||||
fun setGatewayBootstrapToken(value: String) {
|
||||
runtime.setGatewayBootstrapToken(value)
|
||||
prefs.setGatewayBootstrapToken(value)
|
||||
}
|
||||
|
||||
fun setGatewayPassword(value: String) {
|
||||
runtime.setGatewayPassword(value)
|
||||
prefs.setGatewayPassword(value)
|
||||
}
|
||||
|
||||
fun setOnboardingCompleted(value: Boolean) {
|
||||
runtime.setOnboardingCompleted(value)
|
||||
if (value) {
|
||||
ensureRuntime()
|
||||
}
|
||||
prefs.setOnboardingCompleted(value)
|
||||
}
|
||||
|
||||
fun setCanvasDebugStatusEnabled(value: Boolean) {
|
||||
runtime.setCanvasDebugStatusEnabled(value)
|
||||
prefs.setCanvasDebugStatusEnabled(value)
|
||||
}
|
||||
|
||||
fun setVoiceScreenActive(active: Boolean) {
|
||||
runtime.setVoiceScreenActive(active)
|
||||
ensureRuntime().setVoiceScreenActive(active)
|
||||
}
|
||||
|
||||
fun setMicEnabled(enabled: Boolean) {
|
||||
runtime.setMicEnabled(enabled)
|
||||
ensureRuntime().setMicEnabled(enabled)
|
||||
}
|
||||
|
||||
fun setSpeakerEnabled(enabled: Boolean) {
|
||||
runtime.setSpeakerEnabled(enabled)
|
||||
ensureRuntime().setSpeakerEnabled(enabled)
|
||||
}
|
||||
|
||||
fun refreshGatewayConnection() {
|
||||
runtime.refreshGatewayConnection()
|
||||
ensureRuntime().refreshGatewayConnection()
|
||||
}
|
||||
|
||||
fun connect(endpoint: GatewayEndpoint) {
|
||||
runtime.connect(endpoint)
|
||||
ensureRuntime().connect(endpoint)
|
||||
}
|
||||
|
||||
fun connectManual() {
|
||||
runtime.connectManual()
|
||||
ensureRuntime().connectManual()
|
||||
}
|
||||
|
||||
fun disconnect() {
|
||||
runtime.disconnect()
|
||||
runtimeRef.value?.disconnect()
|
||||
}
|
||||
|
||||
fun acceptGatewayTrustPrompt() {
|
||||
runtime.acceptGatewayTrustPrompt()
|
||||
runtimeRef.value?.acceptGatewayTrustPrompt()
|
||||
}
|
||||
|
||||
fun declineGatewayTrustPrompt() {
|
||||
runtime.declineGatewayTrustPrompt()
|
||||
runtimeRef.value?.declineGatewayTrustPrompt()
|
||||
}
|
||||
|
||||
fun handleCanvasA2UIActionFromWebView(payloadJson: String) {
|
||||
runtime.handleCanvasA2UIActionFromWebView(payloadJson)
|
||||
ensureRuntime().handleCanvasA2UIActionFromWebView(payloadJson)
|
||||
}
|
||||
|
||||
fun requestCanvasRehydrate(source: String = "screen_tab") {
|
||||
runtime.requestCanvasRehydrate(source = source, force = true)
|
||||
ensureRuntime().requestCanvasRehydrate(source = source, force = true)
|
||||
}
|
||||
|
||||
fun refreshHomeCanvasOverviewIfConnected() {
|
||||
runtime.refreshHomeCanvasOverviewIfConnected()
|
||||
ensureRuntime().refreshHomeCanvasOverviewIfConnected()
|
||||
}
|
||||
|
||||
fun loadChat(sessionKey: String) {
|
||||
runtime.loadChat(sessionKey)
|
||||
ensureRuntime().loadChat(sessionKey)
|
||||
}
|
||||
|
||||
fun refreshChat() {
|
||||
runtime.refreshChat()
|
||||
ensureRuntime().refreshChat()
|
||||
}
|
||||
|
||||
fun refreshChatSessions(limit: Int? = null) {
|
||||
runtime.refreshChatSessions(limit = limit)
|
||||
ensureRuntime().refreshChatSessions(limit = limit)
|
||||
}
|
||||
|
||||
fun setChatThinkingLevel(level: String) {
|
||||
runtime.setChatThinkingLevel(level)
|
||||
ensureRuntime().setChatThinkingLevel(level)
|
||||
}
|
||||
|
||||
fun switchChatSession(sessionKey: String) {
|
||||
runtime.switchChatSession(sessionKey)
|
||||
ensureRuntime().switchChatSession(sessionKey)
|
||||
}
|
||||
|
||||
fun abortChat() {
|
||||
runtime.abortChat()
|
||||
ensureRuntime().abortChat()
|
||||
}
|
||||
|
||||
fun sendChat(message: String, thinking: String, attachments: List<OutgoingAttachment>) {
|
||||
runtime.sendChat(message = message, thinking = thinking, attachments = attachments)
|
||||
ensureRuntime().sendChat(message = message, thinking = thinking, attachments = attachments)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,18 @@ import android.app.Application
|
||||
import android.os.StrictMode
|
||||
|
||||
class NodeApp : Application() {
|
||||
val runtime: NodeRuntime by lazy { NodeRuntime(this) }
|
||||
val prefs: SecurePrefs by lazy { SecurePrefs(this) }
|
||||
|
||||
@Volatile private var runtimeInstance: NodeRuntime? = null
|
||||
|
||||
fun ensureRuntime(): NodeRuntime {
|
||||
runtimeInstance?.let { return it }
|
||||
return synchronized(this) {
|
||||
runtimeInstance ?: NodeRuntime(this, prefs).also { runtimeInstance = it }
|
||||
}
|
||||
}
|
||||
|
||||
fun peekRuntime(): NodeRuntime? = runtimeInstance
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
@@ -28,7 +28,11 @@ class NodeForegroundService : Service() {
|
||||
val initial = buildNotification(title = "OpenClaw Node", text = "Starting…")
|
||||
startForegroundWithTypes(notification = initial)
|
||||
|
||||
val runtime = (application as NodeApp).runtime
|
||||
val runtime = (application as NodeApp).peekRuntime()
|
||||
if (runtime == null) {
|
||||
stopSelf()
|
||||
return
|
||||
}
|
||||
notificationJob =
|
||||
scope.launch {
|
||||
combine(
|
||||
@@ -59,7 +63,7 @@ class NodeForegroundService : Service() {
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
when (intent?.action) {
|
||||
ACTION_STOP -> {
|
||||
(application as NodeApp).runtime.disconnect()
|
||||
(application as NodeApp).peekRuntime()?.disconnect()
|
||||
stopSelf()
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
@@ -43,11 +43,12 @@ import kotlinx.serialization.json.buildJsonObject
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
|
||||
class NodeRuntime(context: Context) {
|
||||
class NodeRuntime(
|
||||
context: Context,
|
||||
val prefs: SecurePrefs = SecurePrefs(context.applicationContext),
|
||||
) {
|
||||
private val appContext = context.applicationContext
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
|
||||
val prefs = SecurePrefs(appContext)
|
||||
private val deviceAuthStore = DeviceAuthStore(prefs)
|
||||
val canvas = CanvasController()
|
||||
val camera = CameraCaptureManager(appContext)
|
||||
|
||||
@@ -265,7 +265,7 @@ class ChatController(
|
||||
}
|
||||
|
||||
val historyJson = session.request("chat.history", """{"sessionKey":"$key"}""")
|
||||
val history = parseHistory(historyJson, sessionKey = key)
|
||||
val history = parseHistory(historyJson, sessionKey = key, previousMessages = _messages.value)
|
||||
_messages.value = history.messages
|
||||
_sessionId.value = history.sessionId
|
||||
history.thinkingLevel?.trim()?.takeIf { it.isNotEmpty() }?.let { _thinkingLevel.value = it }
|
||||
@@ -336,7 +336,7 @@ class ChatController(
|
||||
try {
|
||||
val historyJson =
|
||||
session.request("chat.history", """{"sessionKey":"${_sessionKey.value}"}""")
|
||||
val history = parseHistory(historyJson, sessionKey = _sessionKey.value)
|
||||
val history = parseHistory(historyJson, sessionKey = _sessionKey.value, previousMessages = _messages.value)
|
||||
_messages.value = history.messages
|
||||
_sessionId.value = history.sessionId
|
||||
history.thinkingLevel?.trim()?.takeIf { it.isNotEmpty() }?.let { _thinkingLevel.value = it }
|
||||
@@ -450,7 +450,11 @@ class ChatController(
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseHistory(historyJson: String, sessionKey: String): ChatHistory {
|
||||
private fun parseHistory(
|
||||
historyJson: String,
|
||||
sessionKey: String,
|
||||
previousMessages: List<ChatMessage>,
|
||||
): ChatHistory {
|
||||
val root = json.parseToJsonElement(historyJson).asObjectOrNull() ?: return ChatHistory(sessionKey, null, null, emptyList())
|
||||
val sid = root["sessionId"].asStringOrNull()
|
||||
val thinkingLevel = root["thinkingLevel"].asStringOrNull()
|
||||
@@ -470,7 +474,12 @@ class ChatController(
|
||||
)
|
||||
}
|
||||
|
||||
return ChatHistory(sessionKey = sessionKey, sessionId = sid, thinkingLevel = thinkingLevel, messages = messages)
|
||||
return ChatHistory(
|
||||
sessionKey = sessionKey,
|
||||
sessionId = sid,
|
||||
thinkingLevel = thinkingLevel,
|
||||
messages = reconcileMessageIds(previous = previousMessages, incoming = messages),
|
||||
)
|
||||
}
|
||||
|
||||
private fun parseMessageContent(el: JsonElement): ChatMessageContent? {
|
||||
@@ -519,6 +528,47 @@ class ChatController(
|
||||
}
|
||||
}
|
||||
|
||||
internal fun reconcileMessageIds(previous: List<ChatMessage>, incoming: List<ChatMessage>): List<ChatMessage> {
|
||||
if (previous.isEmpty() || incoming.isEmpty()) return incoming
|
||||
|
||||
val idsByKey = LinkedHashMap<String, ArrayDeque<String>>()
|
||||
for (message in previous) {
|
||||
val key = messageIdentityKey(message) ?: continue
|
||||
idsByKey.getOrPut(key) { ArrayDeque() }.addLast(message.id)
|
||||
}
|
||||
|
||||
return incoming.map { message ->
|
||||
val key = messageIdentityKey(message) ?: return@map message
|
||||
val ids = idsByKey[key] ?: return@map message
|
||||
val reusedId = ids.removeFirstOrNull() ?: return@map message
|
||||
if (ids.isEmpty()) {
|
||||
idsByKey.remove(key)
|
||||
}
|
||||
if (reusedId == message.id) return@map message
|
||||
message.copy(id = reusedId)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun messageIdentityKey(message: ChatMessage): String? {
|
||||
val role = message.role.trim().lowercase()
|
||||
if (role.isEmpty()) return null
|
||||
|
||||
val timestamp = message.timestampMs?.toString().orEmpty()
|
||||
val contentFingerprint =
|
||||
message.content.joinToString(separator = "\u001E") { part ->
|
||||
listOf(
|
||||
part.type.trim().lowercase(),
|
||||
part.text?.trim().orEmpty(),
|
||||
part.mimeType?.trim()?.lowercase().orEmpty(),
|
||||
part.fileName?.trim().orEmpty(),
|
||||
part.base64?.hashCode()?.toString().orEmpty(),
|
||||
).joinToString(separator = "\u001F")
|
||||
}
|
||||
|
||||
if (timestamp.isEmpty() && contentFingerprint.isEmpty()) return null
|
||||
return listOf(role, timestamp, contentFingerprint).joinToString(separator = "|")
|
||||
}
|
||||
|
||||
private fun JsonElement?.asObjectOrNull(): JsonObject? = this as? JsonObject
|
||||
|
||||
private fun JsonElement?.asArrayOrNull(): JsonArray? = this as? JsonArray
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package ai.openclaw.app.ui.chat
|
||||
|
||||
import android.graphics.BitmapFactory
|
||||
import android.util.Base64
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -28,8 +26,7 @@ internal fun rememberBase64ImageState(base64: String): Base64ImageState {
|
||||
image =
|
||||
withContext(Dispatchers.Default) {
|
||||
try {
|
||||
val bytes = Base64.decode(base64, Base64.DEFAULT)
|
||||
val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size) ?: return@withContext null
|
||||
val bitmap = decodeBase64Bitmap(base64) ?: return@withContext null
|
||||
bitmap.asImageBitmap()
|
||||
} catch (_: Throwable) {
|
||||
null
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
package ai.openclaw.app.ui.chat
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.net.Uri
|
||||
import android.util.Base64
|
||||
import android.util.LruCache
|
||||
import androidx.core.graphics.scale
|
||||
import ai.openclaw.app.node.JpegSizeLimiter
|
||||
import java.io.ByteArrayOutputStream
|
||||
import kotlin.math.max
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
private const val CHAT_ATTACHMENT_MAX_WIDTH = 1600
|
||||
private const val CHAT_ATTACHMENT_MAX_BASE64_CHARS = 300 * 1024
|
||||
private const val CHAT_ATTACHMENT_START_QUALITY = 85
|
||||
private const val CHAT_DECODE_MAX_DIMENSION = 1600
|
||||
private const val CHAT_IMAGE_CACHE_BYTES = 16 * 1024 * 1024
|
||||
|
||||
private val decodedBitmapCache =
|
||||
object : LruCache<String, Bitmap>(CHAT_IMAGE_CACHE_BYTES) {
|
||||
override fun sizeOf(key: String, value: Bitmap): Int = value.byteCount.coerceAtLeast(1)
|
||||
}
|
||||
|
||||
internal fun loadSizedImageAttachment(resolver: ContentResolver, uri: Uri): PendingImageAttachment {
|
||||
val fileName = normalizeAttachmentFileName((uri.lastPathSegment ?: "image").substringAfterLast('/'))
|
||||
val bitmap = decodeScaledBitmap(resolver, uri, maxDimension = CHAT_ATTACHMENT_MAX_WIDTH)
|
||||
if (bitmap == null) {
|
||||
throw IllegalStateException("unsupported attachment")
|
||||
}
|
||||
val maxBytes = (CHAT_ATTACHMENT_MAX_BASE64_CHARS / 4) * 3
|
||||
val encoded =
|
||||
JpegSizeLimiter.compressToLimit(
|
||||
initialWidth = bitmap.width,
|
||||
initialHeight = bitmap.height,
|
||||
startQuality = CHAT_ATTACHMENT_START_QUALITY,
|
||||
maxBytes = maxBytes,
|
||||
minSize = 240,
|
||||
encode = { width, height, quality ->
|
||||
val working =
|
||||
if (width == bitmap.width && height == bitmap.height) {
|
||||
bitmap
|
||||
} else {
|
||||
bitmap.scale(width, height, true)
|
||||
}
|
||||
try {
|
||||
val out = ByteArrayOutputStream()
|
||||
if (!working.compress(Bitmap.CompressFormat.JPEG, quality, out)) {
|
||||
throw IllegalStateException("attachment encode failed")
|
||||
}
|
||||
out.toByteArray()
|
||||
} finally {
|
||||
if (working !== bitmap) {
|
||||
working.recycle()
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
val base64 = Base64.encodeToString(encoded.bytes, Base64.NO_WRAP)
|
||||
return PendingImageAttachment(
|
||||
id = uri.toString() + "#" + System.currentTimeMillis().toString(),
|
||||
fileName = fileName,
|
||||
mimeType = "image/jpeg",
|
||||
base64 = base64,
|
||||
)
|
||||
}
|
||||
|
||||
internal fun decodeBase64Bitmap(base64: String, maxDimension: Int = CHAT_DECODE_MAX_DIMENSION): Bitmap? {
|
||||
val cacheKey = "$maxDimension:${base64.length}:${base64.hashCode()}"
|
||||
decodedBitmapCache.get(cacheKey)?.let { return it }
|
||||
|
||||
val bytes = Base64.decode(base64, Base64.DEFAULT)
|
||||
if (bytes.isEmpty()) return null
|
||||
|
||||
val bounds = BitmapFactory.Options().apply { inJustDecodeBounds = true }
|
||||
BitmapFactory.decodeByteArray(bytes, 0, bytes.size, bounds)
|
||||
if (bounds.outWidth <= 0 || bounds.outHeight <= 0) return null
|
||||
|
||||
val bitmap =
|
||||
BitmapFactory.decodeByteArray(
|
||||
bytes,
|
||||
0,
|
||||
bytes.size,
|
||||
BitmapFactory.Options().apply {
|
||||
inSampleSize = computeInSampleSize(bounds.outWidth, bounds.outHeight, maxDimension)
|
||||
inPreferredConfig = Bitmap.Config.RGB_565
|
||||
},
|
||||
) ?: return null
|
||||
|
||||
decodedBitmapCache.put(cacheKey, bitmap)
|
||||
return bitmap
|
||||
}
|
||||
|
||||
internal fun computeInSampleSize(width: Int, height: Int, maxDimension: Int): Int {
|
||||
if (width <= 0 || height <= 0 || maxDimension <= 0) return 1
|
||||
|
||||
var sample = 1
|
||||
var longestEdge = max(width, height)
|
||||
while (longestEdge > maxDimension && sample < 64) {
|
||||
sample *= 2
|
||||
longestEdge = max(width / sample, height / sample)
|
||||
}
|
||||
return sample.coerceAtLeast(1)
|
||||
}
|
||||
|
||||
internal fun normalizeAttachmentFileName(raw: String): String {
|
||||
val trimmed = raw.trim()
|
||||
if (trimmed.isEmpty()) return "image.jpg"
|
||||
val stem = trimmed.substringBeforeLast('.', missingDelimiterValue = trimmed).ifEmpty { "image" }
|
||||
return "$stem.jpg"
|
||||
}
|
||||
|
||||
private fun decodeScaledBitmap(
|
||||
resolver: ContentResolver,
|
||||
uri: Uri,
|
||||
maxDimension: Int,
|
||||
): Bitmap? {
|
||||
val bounds = BitmapFactory.Options().apply { inJustDecodeBounds = true }
|
||||
resolver.openInputStream(uri).use { input ->
|
||||
if (input == null) return null
|
||||
BitmapFactory.decodeStream(input, null, bounds)
|
||||
}
|
||||
if (bounds.outWidth <= 0 || bounds.outHeight <= 0) return null
|
||||
|
||||
val decoded =
|
||||
resolver.openInputStream(uri).use { input ->
|
||||
if (input == null) return null
|
||||
BitmapFactory.decodeStream(
|
||||
input,
|
||||
null,
|
||||
BitmapFactory.Options().apply {
|
||||
inSampleSize = computeInSampleSize(bounds.outWidth, bounds.outHeight, maxDimension)
|
||||
inPreferredConfig = Bitmap.Config.ARGB_8888
|
||||
},
|
||||
)
|
||||
} ?: return null
|
||||
|
||||
val longestEdge = max(decoded.width, decoded.height)
|
||||
if (longestEdge <= maxDimension) return decoded
|
||||
|
||||
val scale = maxDimension.toDouble() / longestEdge.toDouble()
|
||||
val targetWidth = max(1, (decoded.width * scale).roundToInt())
|
||||
val targetHeight = max(1, (decoded.height * scale).roundToInt())
|
||||
val scaled = decoded.scale(targetWidth, targetHeight, true)
|
||||
if (scaled !== decoded) {
|
||||
decoded.recycle()
|
||||
}
|
||||
return scaled
|
||||
}
|
||||
@@ -6,12 +6,14 @@ import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -34,11 +36,19 @@ fun ChatMessageListCard(
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val listState = rememberLazyListState()
|
||||
val displayMessages = remember(messages) { messages.asReversed() }
|
||||
val stream = streamingAssistantText?.trim()
|
||||
|
||||
// With reverseLayout the newest item is at index 0 (bottom of screen).
|
||||
LaunchedEffect(messages.size, pendingRunCount, pendingToolCalls.size, streamingAssistantText) {
|
||||
// New list items/tool rows should animate into view, but token streaming should not restart
|
||||
// that animation on every delta.
|
||||
LaunchedEffect(messages.size, pendingRunCount, pendingToolCalls.size) {
|
||||
listState.animateScrollToItem(index = 0)
|
||||
}
|
||||
LaunchedEffect(stream) {
|
||||
if (!stream.isNullOrEmpty()) {
|
||||
listState.scrollToItem(index = 0)
|
||||
}
|
||||
}
|
||||
|
||||
Box(modifier = modifier.fillMaxWidth()) {
|
||||
LazyColumn(
|
||||
@@ -50,8 +60,6 @@ fun ChatMessageListCard(
|
||||
) {
|
||||
// With reverseLayout = true, index 0 renders at the BOTTOM.
|
||||
// So we emit newest items first: streaming → tools → typing → messages (newest→oldest).
|
||||
|
||||
val stream = streamingAssistantText?.trim()
|
||||
if (!stream.isNullOrEmpty()) {
|
||||
item(key = "stream") {
|
||||
ChatStreamingAssistantBubble(text = stream)
|
||||
@@ -70,8 +78,8 @@ fun ChatMessageListCard(
|
||||
}
|
||||
}
|
||||
|
||||
items(count = messages.size, key = { idx -> messages[messages.size - 1 - idx].id }) { idx ->
|
||||
ChatMessageBubble(message = messages[messages.size - 1 - idx])
|
||||
items(items = displayMessages, key = { it.id }) { message ->
|
||||
ChatMessageBubble(message = message)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
package ai.openclaw.app.ui.chat
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.net.Uri
|
||||
import android.util.Base64
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
@@ -47,7 +44,6 @@ import ai.openclaw.app.ui.mobileDanger
|
||||
import ai.openclaw.app.ui.mobileDangerSoft
|
||||
import ai.openclaw.app.ui.mobileText
|
||||
import ai.openclaw.app.ui.mobileTextSecondary
|
||||
import java.io.ByteArrayOutputStream
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -83,7 +79,7 @@ fun ChatSheetContent(viewModel: MainViewModel) {
|
||||
val next =
|
||||
uris.take(8).mapNotNull { uri ->
|
||||
try {
|
||||
loadImageAttachment(resolver, uri)
|
||||
loadSizedImageAttachment(resolver, uri)
|
||||
} catch (_: Throwable) {
|
||||
null
|
||||
}
|
||||
@@ -160,7 +156,10 @@ private fun ChatThreadSelector(
|
||||
mainSessionKey: String,
|
||||
onSelectSession: (String) -> Unit,
|
||||
) {
|
||||
val sessionOptions = resolveSessionChoices(sessionKey, sessions, mainSessionKey = mainSessionKey)
|
||||
val sessionOptions =
|
||||
remember(sessionKey, sessions, mainSessionKey) {
|
||||
resolveSessionChoices(sessionKey, sessions, mainSessionKey = mainSessionKey)
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().horizontalScroll(rememberScrollState()),
|
||||
@@ -214,24 +213,3 @@ data class PendingImageAttachment(
|
||||
val mimeType: String,
|
||||
val base64: String,
|
||||
)
|
||||
|
||||
private suspend fun loadImageAttachment(resolver: ContentResolver, uri: Uri): PendingImageAttachment {
|
||||
val mimeType = resolver.getType(uri) ?: "image/*"
|
||||
val fileName = (uri.lastPathSegment ?: "image").substringAfterLast('/')
|
||||
val bytes =
|
||||
withContext(Dispatchers.IO) {
|
||||
resolver.openInputStream(uri)?.use { input ->
|
||||
val out = ByteArrayOutputStream()
|
||||
input.copyTo(out)
|
||||
out.toByteArray()
|
||||
} ?: ByteArray(0)
|
||||
}
|
||||
if (bytes.isEmpty()) throw IllegalStateException("empty attachment")
|
||||
val base64 = Base64.encodeToString(bytes, Base64.NO_WRAP)
|
||||
return PendingImageAttachment(
|
||||
id = uri.toString() + "#" + System.currentTimeMillis().toString(),
|
||||
fileName = fileName,
|
||||
mimeType = mimeType,
|
||||
base64 = base64,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
package ai.openclaw.app.chat
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotEquals
|
||||
import org.junit.Test
|
||||
|
||||
class ChatControllerMessageIdentityTest {
|
||||
@Test
|
||||
fun reconcileMessageIdsReusesMatchingIdsAcrossHistoryReload() {
|
||||
val previous =
|
||||
listOf(
|
||||
ChatMessage(
|
||||
id = "msg-1",
|
||||
role = "assistant",
|
||||
content = listOf(ChatMessageContent(type = "text", text = "hello")),
|
||||
timestampMs = 1000L,
|
||||
),
|
||||
ChatMessage(
|
||||
id = "msg-2",
|
||||
role = "user",
|
||||
content = listOf(ChatMessageContent(type = "text", text = "hi")),
|
||||
timestampMs = 2000L,
|
||||
),
|
||||
)
|
||||
|
||||
val incoming =
|
||||
listOf(
|
||||
ChatMessage(
|
||||
id = "new-1",
|
||||
role = "assistant",
|
||||
content = listOf(ChatMessageContent(type = "text", text = "hello")),
|
||||
timestampMs = 1000L,
|
||||
),
|
||||
ChatMessage(
|
||||
id = "new-2",
|
||||
role = "user",
|
||||
content = listOf(ChatMessageContent(type = "text", text = "hi")),
|
||||
timestampMs = 2000L,
|
||||
),
|
||||
)
|
||||
|
||||
val reconciled = reconcileMessageIds(previous = previous, incoming = incoming)
|
||||
|
||||
assertEquals(listOf("msg-1", "msg-2"), reconciled.map { it.id })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun reconcileMessageIdsLeavesNewMessagesUntouched() {
|
||||
val previous =
|
||||
listOf(
|
||||
ChatMessage(
|
||||
id = "msg-1",
|
||||
role = "assistant",
|
||||
content = listOf(ChatMessageContent(type = "text", text = "hello")),
|
||||
timestampMs = 1000L,
|
||||
),
|
||||
)
|
||||
|
||||
val incoming =
|
||||
listOf(
|
||||
ChatMessage(
|
||||
id = "new-1",
|
||||
role = "assistant",
|
||||
content = listOf(ChatMessageContent(type = "text", text = "hello")),
|
||||
timestampMs = 1000L,
|
||||
),
|
||||
ChatMessage(
|
||||
id = "new-2",
|
||||
role = "assistant",
|
||||
content = listOf(ChatMessageContent(type = "text", text = "new reply")),
|
||||
timestampMs = 3000L,
|
||||
),
|
||||
)
|
||||
|
||||
val reconciled = reconcileMessageIds(previous = previous, incoming = incoming)
|
||||
|
||||
assertEquals("msg-1", reconciled[0].id)
|
||||
assertEquals("new-2", reconciled[1].id)
|
||||
assertNotEquals(reconciled[0].id, reconciled[1].id)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package ai.openclaw.app.ui.chat
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
class ChatImageCodecTest {
|
||||
@Test
|
||||
fun computeInSampleSizeCapsLongestEdge() {
|
||||
assertEquals(4, computeInSampleSize(width = 4032, height = 3024, maxDimension = 1600))
|
||||
assertEquals(1, computeInSampleSize(width = 800, height = 600, maxDimension = 1600))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun normalizeAttachmentFileNameForcesJpegExtension() {
|
||||
assertEquals("photo.jpg", normalizeAttachmentFileName("photo.png"))
|
||||
assertEquals("image.jpg", normalizeAttachmentFileName(""))
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,24 @@ final class CanvasA2UIActionMessageHandler: NSObject, WKScriptMessageHandler {
|
||||
static let messageName = "openclawCanvasA2UIAction"
|
||||
static let allMessageNames = [messageName]
|
||||
|
||||
// Compatibility helper for debug/test shims. Runtime dispatch remains
|
||||
// limited to in-app canvas schemes in `didReceive`.
|
||||
static func isLocalNetworkCanvasURL(_ url: URL) -> Bool {
|
||||
guard let scheme = url.scheme?.lowercased(), scheme == "http" || scheme == "https" else {
|
||||
return false
|
||||
}
|
||||
guard let host = url.host?.lowercased(), !host.isEmpty else {
|
||||
return false
|
||||
}
|
||||
if host == "localhost" {
|
||||
return true
|
||||
}
|
||||
guard let ip = Self.parseIPv4(host) else {
|
||||
return false
|
||||
}
|
||||
return Self.isLocalNetworkIPv4(ip)
|
||||
}
|
||||
|
||||
private let sessionKey: String
|
||||
|
||||
init(sessionKey: String) {
|
||||
@@ -104,5 +122,24 @@ final class CanvasA2UIActionMessageHandler: NSObject, WKScriptMessageHandler {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static func parseIPv4(_ host: String) -> (UInt8, UInt8, UInt8, UInt8)? {
|
||||
let parts = host.split(separator: ".", omittingEmptySubsequences: false)
|
||||
guard parts.count == 4 else { return nil }
|
||||
let bytes = parts.compactMap { UInt8($0) }
|
||||
guard bytes.count == 4 else { return nil }
|
||||
return (bytes[0], bytes[1], bytes[2], bytes[3])
|
||||
}
|
||||
|
||||
private static func isLocalNetworkIPv4(_ ip: (UInt8, UInt8, UInt8, UInt8)) -> Bool {
|
||||
let (a, b, _, _) = ip
|
||||
if a == 10 { return true }
|
||||
if a == 172, (16...31).contains(Int(b)) { return true }
|
||||
if a == 192, b == 168 { return true }
|
||||
if a == 127 { return true }
|
||||
if a == 169, b == 254 { return true }
|
||||
if a == 100, (64...127).contains(Int(b)) { return true }
|
||||
return false
|
||||
}
|
||||
// Formatting helpers live in OpenClawKit (`OpenClawCanvasA2UIAction`).
|
||||
}
|
||||
|
||||
@@ -81,22 +81,23 @@ final class CanvasSchemeHandler: NSObject, WKURLSchemeHandler {
|
||||
return self.html("Not Found", title: "Canvas: 404")
|
||||
}
|
||||
|
||||
// Directory traversal guard: served files must live under the session root.
|
||||
let standardizedRoot = sessionRoot.standardizedFileURL
|
||||
let standardizedFile = fileURL.standardizedFileURL
|
||||
guard standardizedFile.path.hasPrefix(standardizedRoot.path) else {
|
||||
// Resolve symlinks before enforcing the session-root boundary so links inside
|
||||
// the canvas tree cannot escape to arbitrary host files.
|
||||
let resolvedRoot = sessionRoot.resolvingSymlinksInPath().standardizedFileURL
|
||||
let resolvedFile = fileURL.resolvingSymlinksInPath().standardizedFileURL
|
||||
guard self.isFileURL(resolvedFile, withinDirectory: resolvedRoot) else {
|
||||
return self.html("Forbidden", title: "Canvas: 403")
|
||||
}
|
||||
|
||||
do {
|
||||
let data = try Data(contentsOf: standardizedFile)
|
||||
let mime = CanvasScheme.mimeType(forExtension: standardizedFile.pathExtension)
|
||||
let servedPath = standardizedFile.path
|
||||
let data = try Data(contentsOf: resolvedFile)
|
||||
let mime = CanvasScheme.mimeType(forExtension: resolvedFile.pathExtension)
|
||||
let servedPath = resolvedFile.path
|
||||
canvasLogger.debug(
|
||||
"served \(session, privacy: .public)/\(path, privacy: .public) -> \(servedPath, privacy: .public)")
|
||||
return CanvasResponse(mime: mime, data: data)
|
||||
} catch {
|
||||
let failedPath = standardizedFile.path
|
||||
let failedPath = resolvedFile.path
|
||||
let errorText = error.localizedDescription
|
||||
canvasLogger
|
||||
.error(
|
||||
@@ -145,6 +146,11 @@ final class CanvasSchemeHandler: NSObject, WKURLSchemeHandler {
|
||||
return nil
|
||||
}
|
||||
|
||||
private func isFileURL(_ fileURL: URL, withinDirectory rootURL: URL) -> Bool {
|
||||
let rootPath = rootURL.path.hasSuffix("/") ? rootURL.path : rootURL.path + "/"
|
||||
return fileURL.path == rootURL.path || fileURL.path.hasPrefix(rootPath)
|
||||
}
|
||||
|
||||
private func html(_ body: String, title: String = "Canvas") -> CanvasResponse {
|
||||
let html = """
|
||||
<!doctype html>
|
||||
|
||||
@@ -254,6 +254,71 @@ struct CronJob: Identifiable, Codable, Equatable {
|
||||
case state
|
||||
}
|
||||
|
||||
init(
|
||||
id: String,
|
||||
agentId: String?,
|
||||
name: String,
|
||||
description: String?,
|
||||
enabled: Bool,
|
||||
deleteAfterRun: Bool?,
|
||||
createdAtMs: Int,
|
||||
updatedAtMs: Int,
|
||||
schedule: CronSchedule,
|
||||
sessionTarget: CronSessionTarget,
|
||||
wakeMode: CronWakeMode,
|
||||
payload: CronPayload,
|
||||
delivery: CronDelivery?,
|
||||
state: CronJobState)
|
||||
{
|
||||
self.init(
|
||||
id: id,
|
||||
agentId: agentId,
|
||||
name: name,
|
||||
description: description,
|
||||
enabled: enabled,
|
||||
deleteAfterRun: deleteAfterRun,
|
||||
createdAtMs: createdAtMs,
|
||||
updatedAtMs: updatedAtMs,
|
||||
schedule: schedule,
|
||||
sessionTarget: .predefined(sessionTarget),
|
||||
wakeMode: wakeMode,
|
||||
payload: payload,
|
||||
delivery: delivery,
|
||||
state: state)
|
||||
}
|
||||
|
||||
init(
|
||||
id: String,
|
||||
agentId: String?,
|
||||
name: String,
|
||||
description: String?,
|
||||
enabled: Bool,
|
||||
deleteAfterRun: Bool?,
|
||||
createdAtMs: Int,
|
||||
updatedAtMs: Int,
|
||||
schedule: CronSchedule,
|
||||
sessionTarget: CronCustomSessionTarget,
|
||||
wakeMode: CronWakeMode,
|
||||
payload: CronPayload,
|
||||
delivery: CronDelivery?,
|
||||
state: CronJobState)
|
||||
{
|
||||
self.id = id
|
||||
self.agentId = agentId
|
||||
self.name = name
|
||||
self.description = description
|
||||
self.enabled = enabled
|
||||
self.deleteAfterRun = deleteAfterRun
|
||||
self.createdAtMs = createdAtMs
|
||||
self.updatedAtMs = updatedAtMs
|
||||
self.schedule = schedule
|
||||
self.sessionTargetRaw = sessionTarget.rawValue
|
||||
self.wakeMode = wakeMode
|
||||
self.payload = payload
|
||||
self.delivery = delivery
|
||||
self.state = state
|
||||
}
|
||||
|
||||
/// Parsed session target (predefined or custom session ID)
|
||||
var parsedSessionTarget: CronCustomSessionTarget {
|
||||
CronCustomSessionTarget.from(self.sessionTargetRaw)
|
||||
|
||||
@@ -89,6 +89,20 @@ private func readLineFromHandle(_ handle: FileHandle, maxBytes: Int) throws -> S
|
||||
return String(data: lineData, encoding: .utf8)
|
||||
}
|
||||
|
||||
func timingSafeHexStringEquals(_ lhs: String, _ rhs: String) -> Bool {
|
||||
let lhsBytes = Array(lhs.utf8)
|
||||
let rhsBytes = Array(rhs.utf8)
|
||||
guard lhsBytes.count == rhsBytes.count else {
|
||||
return false
|
||||
}
|
||||
|
||||
var diff: UInt8 = 0
|
||||
for index in lhsBytes.indices {
|
||||
diff |= lhsBytes[index] ^ rhsBytes[index]
|
||||
}
|
||||
return diff == 0
|
||||
}
|
||||
|
||||
enum ExecApprovalsSocketClient {
|
||||
private struct TimeoutError: LocalizedError {
|
||||
var message: String
|
||||
@@ -854,7 +868,7 @@ private final class ExecApprovalsSocketServer: @unchecked Sendable {
|
||||
error: ExecHostError(code: "INVALID_REQUEST", message: "expired request", reason: "ttl"))
|
||||
}
|
||||
let expected = self.hmacHex(nonce: request.nonce, ts: request.ts, requestJson: request.requestJson)
|
||||
if expected != request.hmac {
|
||||
if !timingSafeHexStringEquals(expected, request.hmac) {
|
||||
return ExecHostResponse(
|
||||
type: "exec-res",
|
||||
id: request.id,
|
||||
|
||||
@@ -23,11 +23,23 @@ enum HostEnvSecurityPolicy {
|
||||
"PS4",
|
||||
"GCONV_PATH",
|
||||
"IFS",
|
||||
"SSLKEYLOGFILE"
|
||||
"SSLKEYLOGFILE",
|
||||
"JAVA_TOOL_OPTIONS",
|
||||
"_JAVA_OPTIONS",
|
||||
"JDK_JAVA_OPTIONS",
|
||||
"PYTHONBREAKPOINT",
|
||||
"DOTNET_STARTUP_HOOKS",
|
||||
"DOTNET_ADDITIONAL_DEPS",
|
||||
"GLIBC_TUNABLES",
|
||||
"MAVEN_OPTS",
|
||||
"SBT_OPTS",
|
||||
"GRADLE_OPTS",
|
||||
"ANT_OPTS"
|
||||
]
|
||||
|
||||
static let blockedOverrideKeys: Set<String> = [
|
||||
"HOME",
|
||||
"GRADLE_USER_HOME",
|
||||
"ZDOTDIR",
|
||||
"GIT_SSH_COMMAND",
|
||||
"GIT_SSH",
|
||||
|
||||
@@ -26,7 +26,12 @@ enum LaunchAgentManager {
|
||||
}
|
||||
|
||||
private static func writePlist(bundlePath: String) {
|
||||
let plist = """
|
||||
let plist = self.plistContents(bundlePath: bundlePath)
|
||||
try? plist.write(to: self.plistURL, atomically: true, encoding: .utf8)
|
||||
}
|
||||
|
||||
static func plistContents(bundlePath: String) -> String {
|
||||
"""
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
@@ -41,8 +46,6 @@ enum LaunchAgentManager {
|
||||
<string>\(FileManager().homeDirectoryForCurrentUser.path)</string>
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
<key>KeepAlive</key>
|
||||
<true/>
|
||||
<key>EnvironmentVariables</key>
|
||||
<dict>
|
||||
<key>PATH</key>
|
||||
@@ -55,7 +58,6 @@ enum LaunchAgentManager {
|
||||
</dict>
|
||||
</plist>
|
||||
"""
|
||||
try? plist.write(to: self.plistURL, atomically: true, encoding: .utf8)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
|
||||
@@ -1099,38 +1099,33 @@ extension MenuSessionsInjector {
|
||||
// MARK: - Width + placement
|
||||
|
||||
private func findInsertIndex(in menu: NSMenu) -> Int? {
|
||||
// Insert right before the separator above "Send Heartbeats".
|
||||
if let idx = menu.items.firstIndex(where: { $0.title == "Send Heartbeats" }) {
|
||||
if let sepIdx = menu.items[..<idx].lastIndex(where: { $0.isSeparatorItem }) {
|
||||
return sepIdx
|
||||
}
|
||||
return idx
|
||||
}
|
||||
|
||||
if let sepIdx = menu.items.firstIndex(where: { $0.isSeparatorItem }) {
|
||||
return sepIdx
|
||||
}
|
||||
|
||||
if menu.items.count >= 1 { return 1 }
|
||||
return menu.items.count
|
||||
self.findDynamicSectionInsertIndex(in: menu)
|
||||
}
|
||||
|
||||
private func findNodesInsertIndex(in menu: NSMenu) -> Int? {
|
||||
if let idx = menu.items.firstIndex(where: { $0.title == "Send Heartbeats" }) {
|
||||
if let sepIdx = menu.items[..<idx].lastIndex(where: { $0.isSeparatorItem }) {
|
||||
return sepIdx
|
||||
}
|
||||
return idx
|
||||
self.findDynamicSectionInsertIndex(in: menu)
|
||||
}
|
||||
|
||||
private func findDynamicSectionInsertIndex(in menu: NSMenu) -> Int? {
|
||||
// Keep controls and action buttons visible by inserting dynamic rows at the
|
||||
// built-in footer boundary, not by matching localized menu item titles.
|
||||
if let footerSeparatorIndex = menu.items.lastIndex(where: { item in
|
||||
item.isSeparatorItem && !self.isInjectedItem(item)
|
||||
}) {
|
||||
return footerSeparatorIndex
|
||||
}
|
||||
|
||||
if let sepIdx = menu.items.firstIndex(where: { $0.isSeparatorItem }) {
|
||||
return sepIdx
|
||||
if let firstBaseItemIndex = menu.items.firstIndex(where: { !self.isInjectedItem($0) }) {
|
||||
return min(firstBaseItemIndex + 1, menu.items.count)
|
||||
}
|
||||
|
||||
if menu.items.count >= 1 { return 1 }
|
||||
return menu.items.count
|
||||
}
|
||||
|
||||
private func isInjectedItem(_ item: NSMenuItem) -> Bool {
|
||||
item.tag == self.tag || item.tag == self.nodesTag
|
||||
}
|
||||
|
||||
private func initialWidth(for menu: NSMenu) -> CGFloat {
|
||||
if let openWidth = self.menuOpenWidth {
|
||||
return max(300, openWidth)
|
||||
@@ -1236,5 +1231,13 @@ extension MenuSessionsInjector {
|
||||
func injectForTesting(into menu: NSMenu) {
|
||||
self.inject(into: menu)
|
||||
}
|
||||
|
||||
func testingFindInsertIndex(in menu: NSMenu) -> Int? {
|
||||
self.findInsertIndex(in: menu)
|
||||
}
|
||||
|
||||
func testingFindNodesInsertIndex(in menu: NSMenu) -> Int? {
|
||||
self.findNodesInsertIndex(in: menu)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -6,7 +6,7 @@ enum NodeServiceManager {
|
||||
|
||||
static func start() async -> String? {
|
||||
let result = await self.runServiceCommandResult(
|
||||
["node", "start"],
|
||||
["start"],
|
||||
timeout: 20,
|
||||
quiet: false)
|
||||
if let error = self.errorMessage(from: result, treatNotLoadedAsError: true) {
|
||||
@@ -18,7 +18,7 @@ enum NodeServiceManager {
|
||||
|
||||
static func stop() async -> String? {
|
||||
let result = await self.runServiceCommandResult(
|
||||
["node", "stop"],
|
||||
["stop"],
|
||||
timeout: 15,
|
||||
quiet: false)
|
||||
if let error = self.errorMessage(from: result, treatNotLoadedAsError: false) {
|
||||
@@ -30,6 +30,14 @@ enum NodeServiceManager {
|
||||
}
|
||||
|
||||
extension NodeServiceManager {
|
||||
private static func serviceCommand(_ args: [String]) -> [String] {
|
||||
CommandResolver.openclawCommand(
|
||||
subcommand: "node",
|
||||
extraArgs: self.withJsonFlag(args),
|
||||
// Service management must always run locally, even if remote mode is configured.
|
||||
configRoot: ["gateway": ["mode": "local"]])
|
||||
}
|
||||
|
||||
private struct CommandResult {
|
||||
let success: Bool
|
||||
let payload: Data?
|
||||
@@ -52,11 +60,7 @@ extension NodeServiceManager {
|
||||
timeout: Double,
|
||||
quiet: Bool) async -> CommandResult
|
||||
{
|
||||
let command = CommandResolver.openclawCommand(
|
||||
subcommand: "service",
|
||||
extraArgs: self.withJsonFlag(args),
|
||||
// Service management must always run locally, even if remote mode is configured.
|
||||
configRoot: ["gateway": ["mode": "local"]])
|
||||
let command = self.serviceCommand(args)
|
||||
var env = ProcessInfo.processInfo.environment
|
||||
env["PATH"] = CommandResolver.preferredPaths().joined(separator: ":")
|
||||
let response = await ShellExecutor.runDetailed(command: command, cwd: nil, env: env, timeout: timeout)
|
||||
@@ -136,3 +140,11 @@ extension NodeServiceManager {
|
||||
TextSummarySupport.summarizeLastLine(text)
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
extension NodeServiceManager {
|
||||
static func _testServiceCommand(_ args: [String]) -> [String] {
|
||||
self.serviceCommand(args)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -515,6 +515,8 @@ public struct PollParams: Codable, Sendable {
|
||||
public struct AgentParams: Codable, Sendable {
|
||||
public let message: String
|
||||
public let agentid: String?
|
||||
public let provider: String?
|
||||
public let model: String?
|
||||
public let to: String?
|
||||
public let replyto: String?
|
||||
public let sessionid: String?
|
||||
@@ -542,6 +544,8 @@ public struct AgentParams: Codable, Sendable {
|
||||
public init(
|
||||
message: String,
|
||||
agentid: String?,
|
||||
provider: String?,
|
||||
model: String?,
|
||||
to: String?,
|
||||
replyto: String?,
|
||||
sessionid: String?,
|
||||
@@ -568,6 +572,8 @@ public struct AgentParams: Codable, Sendable {
|
||||
{
|
||||
self.message = message
|
||||
self.agentid = agentid
|
||||
self.provider = provider
|
||||
self.model = model
|
||||
self.to = to
|
||||
self.replyto = replyto
|
||||
self.sessionid = sessionid
|
||||
@@ -596,6 +602,8 @@ public struct AgentParams: Codable, Sendable {
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case message
|
||||
case agentid = "agentId"
|
||||
case provider
|
||||
case model
|
||||
case to
|
||||
case replyto = "replyTo"
|
||||
case sessionid = "sessionId"
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import Testing
|
||||
@testable import OpenClaw
|
||||
|
||||
struct ExecApprovalsSocketAuthTests {
|
||||
@Test
|
||||
func `timing safe hex compare matches equal strings`() {
|
||||
#expect(timingSafeHexStringEquals(String(repeating: "a", count: 64), String(repeating: "a", count: 64)))
|
||||
}
|
||||
|
||||
@Test
|
||||
func `timing safe hex compare rejects mismatched strings`() {
|
||||
let expected = String(repeating: "a", count: 63) + "b"
|
||||
let provided = String(repeating: "a", count: 63) + "c"
|
||||
#expect(!timingSafeHexStringEquals(expected, provided))
|
||||
}
|
||||
|
||||
@Test
|
||||
func `timing safe hex compare rejects different length strings`() {
|
||||
#expect(!timingSafeHexStringEquals(String(repeating: "a", count: 64), "deadbeef"))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import Foundation
|
||||
import Testing
|
||||
@testable import OpenClaw
|
||||
|
||||
struct LaunchAgentManagerTests {
|
||||
@Test func `launch at login plist does not keep app alive after manual quit`() throws {
|
||||
let plist = LaunchAgentManager.plistContents(bundlePath: "/Applications/OpenClaw.app")
|
||||
let data = try #require(plist.data(using: .utf8))
|
||||
let object = try #require(
|
||||
PropertyListSerialization.propertyList(from: data, format: nil) as? [String: Any]
|
||||
)
|
||||
|
||||
#expect(object["RunAtLoad"] as? Bool == true)
|
||||
#expect(object["KeepAlive"] == nil)
|
||||
|
||||
let args = try #require(object["ProgramArguments"] as? [String])
|
||||
#expect(args == ["/Applications/OpenClaw.app/Contents/MacOS/OpenClaw"])
|
||||
}
|
||||
}
|
||||
@@ -216,6 +216,32 @@ struct LowCoverageHelperTests {
|
||||
#expect(handler._testTextEncodingName(for: "application/octet-stream") == nil)
|
||||
}
|
||||
|
||||
@Test @MainActor func `canvas scheme handler blocks symlink escapes`() throws {
|
||||
let root = FileManager().temporaryDirectory
|
||||
.appendingPathComponent("canvas-\(UUID().uuidString)", isDirectory: true)
|
||||
defer { try? FileManager().removeItem(at: root) }
|
||||
try FileManager().createDirectory(at: root, withIntermediateDirectories: true)
|
||||
|
||||
let session = root.appendingPathComponent("main", isDirectory: true)
|
||||
try FileManager().createDirectory(at: session, withIntermediateDirectories: true)
|
||||
|
||||
let outside = root.deletingLastPathComponent().appendingPathComponent("canvas-secret-\(UUID().uuidString).txt")
|
||||
defer { try? FileManager().removeItem(at: outside) }
|
||||
try "top-secret".write(to: outside, atomically: true, encoding: .utf8)
|
||||
|
||||
let symlink = session.appendingPathComponent("index.html")
|
||||
try FileManager().createSymbolicLink(at: symlink, withDestinationURL: outside)
|
||||
|
||||
let handler = CanvasSchemeHandler(root: root)
|
||||
let url = try #require(CanvasScheme.makeURL(session: "main", path: "index.html"))
|
||||
let response = handler._testResponse(for: url)
|
||||
let body = String(data: response.data, encoding: .utf8) ?? ""
|
||||
|
||||
#expect(response.mime == "text/html")
|
||||
#expect(body.contains("Forbidden"))
|
||||
#expect(!body.contains("top-secret"))
|
||||
}
|
||||
|
||||
@Test @MainActor func `menu context card injector inserts and finds index`() {
|
||||
let injector = MenuContextCardInjector()
|
||||
let menu = NSMenu()
|
||||
|
||||
@@ -5,7 +5,26 @@ import Testing
|
||||
@Suite(.serialized)
|
||||
@MainActor
|
||||
struct MenuSessionsInjectorTests {
|
||||
@Test func `injects disconnected message`() {
|
||||
@Test func anchorsDynamicRowsBelowControlsAndActions() throws {
|
||||
let injector = MenuSessionsInjector()
|
||||
|
||||
let menu = NSMenu()
|
||||
menu.addItem(NSMenuItem(title: "Header", action: nil, keyEquivalent: ""))
|
||||
menu.addItem(.separator())
|
||||
menu.addItem(NSMenuItem(title: "Send Heartbeats", action: nil, keyEquivalent: ""))
|
||||
menu.addItem(NSMenuItem(title: "Browser Control", action: nil, keyEquivalent: ""))
|
||||
menu.addItem(.separator())
|
||||
menu.addItem(NSMenuItem(title: "Open Dashboard", action: nil, keyEquivalent: ""))
|
||||
menu.addItem(NSMenuItem(title: "Open Chat", action: nil, keyEquivalent: ""))
|
||||
menu.addItem(.separator())
|
||||
menu.addItem(NSMenuItem(title: "Settings…", action: nil, keyEquivalent: ""))
|
||||
|
||||
let footerSeparatorIndex = try #require(menu.items.lastIndex(where: { $0.isSeparatorItem }))
|
||||
#expect(injector.testingFindInsertIndex(in: menu) == footerSeparatorIndex)
|
||||
#expect(injector.testingFindNodesInsertIndex(in: menu) == footerSeparatorIndex)
|
||||
}
|
||||
|
||||
@Test func injectsDisconnectedMessage() {
|
||||
let injector = MenuSessionsInjector()
|
||||
injector.setTestingControlChannelConnected(false)
|
||||
injector.setTestingSnapshot(nil, errorText: nil)
|
||||
@@ -19,7 +38,7 @@ struct MenuSessionsInjectorTests {
|
||||
#expect(menu.items.contains { $0.tag == 9_415_557 })
|
||||
}
|
||||
|
||||
@Test func `injects session rows`() {
|
||||
@Test func injectsSessionRows() throws {
|
||||
let injector = MenuSessionsInjector()
|
||||
injector.setTestingControlChannelConnected(true)
|
||||
|
||||
@@ -88,10 +107,22 @@ struct MenuSessionsInjectorTests {
|
||||
menu.addItem(NSMenuItem(title: "Header", action: nil, keyEquivalent: ""))
|
||||
menu.addItem(.separator())
|
||||
menu.addItem(NSMenuItem(title: "Send Heartbeats", action: nil, keyEquivalent: ""))
|
||||
menu.addItem(NSMenuItem(title: "Browser Control", action: nil, keyEquivalent: ""))
|
||||
menu.addItem(.separator())
|
||||
menu.addItem(NSMenuItem(title: "Open Dashboard", action: nil, keyEquivalent: ""))
|
||||
menu.addItem(.separator())
|
||||
menu.addItem(NSMenuItem(title: "Settings…", action: nil, keyEquivalent: ""))
|
||||
|
||||
injector.injectForTesting(into: menu)
|
||||
#expect(menu.items.contains { $0.tag == 9_415_557 })
|
||||
#expect(menu.items.contains { $0.tag == 9_415_557 && $0.isSeparatorItem })
|
||||
let sendHeartbeatsIndex = try #require(menu.items.firstIndex(where: { $0.title == "Send Heartbeats" }))
|
||||
let openDashboardIndex = try #require(menu.items.firstIndex(where: { $0.title == "Open Dashboard" }))
|
||||
let firstInjectedIndex = try #require(menu.items.firstIndex(where: { $0.tag == 9_415_557 }))
|
||||
let settingsIndex = try #require(menu.items.firstIndex(where: { $0.title == "Settings…" }))
|
||||
#expect(sendHeartbeatsIndex < firstInjectedIndex)
|
||||
#expect(openDashboardIndex < firstInjectedIndex)
|
||||
#expect(firstInjectedIndex < settingsIndex)
|
||||
}
|
||||
|
||||
@Test func `cost usage submenu does not use injector delegate`() {
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import Foundation
|
||||
import Testing
|
||||
@testable import OpenClaw
|
||||
|
||||
@Suite(.serialized) struct NodeServiceManagerTests {
|
||||
@Test func `builds node service commands with current CLI shape`() throws {
|
||||
let tmp = try makeTempDirForTests()
|
||||
CommandResolver.setProjectRoot(tmp.path)
|
||||
|
||||
let openclawPath = tmp.appendingPathComponent("node_modules/.bin/openclaw")
|
||||
try makeExecutableForTests(at: openclawPath)
|
||||
|
||||
let start = NodeServiceManager._testServiceCommand(["start"])
|
||||
#expect(start == [openclawPath.path, "node", "start", "--json"])
|
||||
|
||||
let stop = NodeServiceManager._testServiceCommand(["stop"])
|
||||
#expect(stop == [openclawPath.path, "node", "stop", "--json"])
|
||||
}
|
||||
}
|
||||
@@ -515,6 +515,8 @@ public struct PollParams: Codable, Sendable {
|
||||
public struct AgentParams: Codable, Sendable {
|
||||
public let message: String
|
||||
public let agentid: String?
|
||||
public let provider: String?
|
||||
public let model: String?
|
||||
public let to: String?
|
||||
public let replyto: String?
|
||||
public let sessionid: String?
|
||||
@@ -542,6 +544,8 @@ public struct AgentParams: Codable, Sendable {
|
||||
public init(
|
||||
message: String,
|
||||
agentid: String?,
|
||||
provider: String?,
|
||||
model: String?,
|
||||
to: String?,
|
||||
replyto: String?,
|
||||
sessionid: String?,
|
||||
@@ -568,6 +572,8 @@ public struct AgentParams: Codable, Sendable {
|
||||
{
|
||||
self.message = message
|
||||
self.agentid = agentid
|
||||
self.provider = provider
|
||||
self.model = model
|
||||
self.to = to
|
||||
self.replyto = replyto
|
||||
self.sessionid = sessionid
|
||||
@@ -596,6 +602,8 @@ public struct AgentParams: Codable, Sendable {
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case message
|
||||
case agentid = "agentId"
|
||||
case provider
|
||||
case model
|
||||
case to
|
||||
case replyto = "replyTo"
|
||||
case sessionid = "sessionId"
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
# OpenClaw Chrome Extension (Browser Relay)
|
||||
|
||||
Purpose: attach OpenClaw to an existing Chrome tab so the Gateway can automate it (via the local CDP relay server).
|
||||
|
||||
## Dev / load unpacked
|
||||
|
||||
1. Build/run OpenClaw Gateway with browser control enabled.
|
||||
2. Ensure the relay server is reachable at `http://127.0.0.1:18792/` (default).
|
||||
3. Install the extension to a stable path:
|
||||
|
||||
```bash
|
||||
openclaw browser extension install
|
||||
openclaw browser extension path
|
||||
```
|
||||
|
||||
4. Chrome → `chrome://extensions` → enable “Developer mode”.
|
||||
5. “Load unpacked” → select the path printed above.
|
||||
6. Pin the extension. Click the icon on a tab to attach/detach.
|
||||
|
||||
## Options
|
||||
|
||||
- `Relay port`: defaults to `18792`.
|
||||
- `Gateway token`: required. Set this to `gateway.auth.token` (or `OPENCLAW_GATEWAY_TOKEN`).
|
||||
@@ -1,64 +0,0 @@
|
||||
export function reconnectDelayMs(
|
||||
attempt,
|
||||
opts = { baseMs: 1000, maxMs: 30000, jitterMs: 1000, random: Math.random },
|
||||
) {
|
||||
const baseMs = Number.isFinite(opts.baseMs) ? opts.baseMs : 1000;
|
||||
const maxMs = Number.isFinite(opts.maxMs) ? opts.maxMs : 30000;
|
||||
const jitterMs = Number.isFinite(opts.jitterMs) ? opts.jitterMs : 1000;
|
||||
const random = typeof opts.random === "function" ? opts.random : Math.random;
|
||||
const safeAttempt = Math.max(0, Number.isFinite(attempt) ? attempt : 0);
|
||||
const backoff = Math.min(baseMs * 2 ** safeAttempt, maxMs);
|
||||
return backoff + Math.max(0, jitterMs) * random();
|
||||
}
|
||||
|
||||
export async function deriveRelayToken(gatewayToken, port) {
|
||||
const enc = new TextEncoder();
|
||||
const key = await crypto.subtle.importKey(
|
||||
"raw",
|
||||
enc.encode(gatewayToken),
|
||||
{ name: "HMAC", hash: "SHA-256" },
|
||||
false,
|
||||
["sign"],
|
||||
);
|
||||
const sig = await crypto.subtle.sign(
|
||||
"HMAC",
|
||||
key,
|
||||
enc.encode(`openclaw-extension-relay-v1:${port}`),
|
||||
);
|
||||
return [...new Uint8Array(sig)].map((b) => b.toString(16).padStart(2, "0")).join("");
|
||||
}
|
||||
|
||||
export async function buildRelayWsUrl(port, gatewayToken) {
|
||||
const token = String(gatewayToken || "").trim();
|
||||
if (!token) {
|
||||
throw new Error(
|
||||
"Missing gatewayToken in extension settings (chrome.storage.local.gatewayToken)",
|
||||
);
|
||||
}
|
||||
const relayToken = await deriveRelayToken(token, port);
|
||||
return `ws://127.0.0.1:${port}/extension?token=${encodeURIComponent(relayToken)}`;
|
||||
}
|
||||
|
||||
export function isRetryableReconnectError(err) {
|
||||
const message = err instanceof Error ? err.message : String(err || "");
|
||||
if (message.includes("Missing gatewayToken")) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function isMissingTabError(err) {
|
||||
const message = (err instanceof Error ? err.message : String(err || "")).toLowerCase();
|
||||
return (
|
||||
message.includes("no tab with id") ||
|
||||
message.includes("no tab with given id") ||
|
||||
message.includes("tab not found")
|
||||
);
|
||||
}
|
||||
|
||||
export function isLastRemainingTab(allTabs, tabIdToClose) {
|
||||
if (!Array.isArray(allTabs)) {
|
||||
return true;
|
||||
}
|
||||
return allTabs.filter((tab) => tab && tab.id !== tabIdToClose).length === 0;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "OpenClaw Browser Relay",
|
||||
"version": "0.1.0",
|
||||
"description": "Attach OpenClaw to your existing Chrome tab via a local CDP relay server.",
|
||||
"icons": {
|
||||
"16": "icons/icon16.png",
|
||||
"32": "icons/icon32.png",
|
||||
"48": "icons/icon48.png",
|
||||
"128": "icons/icon128.png"
|
||||
},
|
||||
"permissions": ["debugger", "tabs", "activeTab", "storage", "alarms", "webNavigation"],
|
||||
"host_permissions": ["http://127.0.0.1/*", "http://localhost/*"],
|
||||
"background": { "service_worker": "background.js", "type": "module" },
|
||||
"action": {
|
||||
"default_title": "OpenClaw Browser Relay (click to attach/detach)",
|
||||
"default_icon": {
|
||||
"16": "icons/icon16.png",
|
||||
"32": "icons/icon32.png",
|
||||
"48": "icons/icon48.png",
|
||||
"128": "icons/icon128.png"
|
||||
}
|
||||
},
|
||||
"options_ui": { "page": "options.html", "open_in_tab": true }
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
const PORT_GUIDANCE = 'Use gateway port + 3 (for gateway 18789, relay is 18792).'
|
||||
|
||||
function hasCdpVersionShape(data) {
|
||||
return !!data && typeof data === 'object' && 'Browser' in data && 'Protocol-Version' in data
|
||||
}
|
||||
|
||||
export function classifyRelayCheckResponse(res, port) {
|
||||
if (!res) {
|
||||
return { action: 'throw', error: 'No response from service worker' }
|
||||
}
|
||||
|
||||
if (res.status === 401) {
|
||||
return { action: 'status', kind: 'error', message: 'Gateway token rejected. Check token and save again.' }
|
||||
}
|
||||
|
||||
if (res.error) {
|
||||
return { action: 'throw', error: res.error }
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
return { action: 'throw', error: `HTTP ${res.status}` }
|
||||
}
|
||||
|
||||
const contentType = String(res.contentType || '')
|
||||
if (!contentType.includes('application/json')) {
|
||||
return {
|
||||
action: 'status',
|
||||
kind: 'error',
|
||||
message: `Wrong port: this is likely the gateway, not the relay. ${PORT_GUIDANCE}`,
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasCdpVersionShape(res.json)) {
|
||||
return {
|
||||
action: 'status',
|
||||
kind: 'error',
|
||||
message: `Wrong port: expected relay /json/version response. ${PORT_GUIDANCE}`,
|
||||
}
|
||||
}
|
||||
|
||||
return { action: 'status', kind: 'ok', message: `Relay reachable and authenticated at http://127.0.0.1:${port}/` }
|
||||
}
|
||||
|
||||
export function classifyRelayCheckException(err, port) {
|
||||
const message = String(err || '').toLowerCase()
|
||||
if (message.includes('json') || message.includes('syntax')) {
|
||||
return {
|
||||
kind: 'error',
|
||||
message: `Wrong port: this is not a relay endpoint. ${PORT_GUIDANCE}`,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
kind: 'error',
|
||||
message: `Relay not reachable/authenticated at http://127.0.0.1:${port}/. Start OpenClaw browser relay and verify token.`,
|
||||
}
|
||||
}
|
||||
@@ -1,200 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>OpenClaw Browser Relay</title>
|
||||
<style>
|
||||
:root {
|
||||
color-scheme: light dark;
|
||||
--accent: #ff5a36;
|
||||
--panel: color-mix(in oklab, canvas 92%, canvasText 8%);
|
||||
--border: color-mix(in oklab, canvasText 18%, transparent);
|
||||
--muted: color-mix(in oklab, canvasText 70%, transparent);
|
||||
--shadow: 0 10px 30px color-mix(in oklab, canvasText 18%, transparent);
|
||||
font-family: ui-rounded, system-ui, -apple-system, BlinkMacSystemFont, "SF Pro Rounded",
|
||||
"SF Pro Display", "Segoe UI", sans-serif;
|
||||
line-height: 1.4;
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
background:
|
||||
radial-gradient(1000px 500px at 10% 0%, color-mix(in oklab, var(--accent) 30%, transparent), transparent 70%),
|
||||
radial-gradient(900px 450px at 90% 0%, color-mix(in oklab, var(--accent) 18%, transparent), transparent 75%),
|
||||
canvas;
|
||||
color: canvasText;
|
||||
}
|
||||
.wrap {
|
||||
max-width: 820px;
|
||||
margin: 36px auto;
|
||||
padding: 0 24px 48px 24px;
|
||||
}
|
||||
header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
.logo {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 14px;
|
||||
background: color-mix(in oklab, var(--accent) 18%, transparent);
|
||||
border: 1px solid color-mix(in oklab, var(--accent) 35%, transparent);
|
||||
box-shadow: var(--shadow);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
.logo img {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
h1 {
|
||||
font-size: 20px;
|
||||
margin: 0;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
.subtitle {
|
||||
margin: 2px 0 0 0;
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 14px;
|
||||
}
|
||||
.card {
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 16px;
|
||||
padding: 16px;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
.card h2 {
|
||||
margin: 0 0 10px 0;
|
||||
font-size: 14px;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
.card p {
|
||||
margin: 8px 0 0 0;
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
label {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
input {
|
||||
width: 160px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--border);
|
||||
background: color-mix(in oklab, canvas 92%, canvasText 8%);
|
||||
color: canvasText;
|
||||
outline: none;
|
||||
}
|
||||
input:focus {
|
||||
border-color: color-mix(in oklab, var(--accent) 70%, transparent);
|
||||
box-shadow: 0 0 0 4px color-mix(in oklab, var(--accent) 20%, transparent);
|
||||
}
|
||||
button {
|
||||
padding: 10px 14px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid color-mix(in oklab, var(--accent) 55%, transparent);
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
color-mix(in oklab, var(--accent) 80%, white 20%),
|
||||
var(--accent)
|
||||
);
|
||||
color: white;
|
||||
font-weight: 650;
|
||||
letter-spacing: 0.01em;
|
||||
cursor: pointer;
|
||||
}
|
||||
button:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
.hint {
|
||||
margin-top: 10px;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
}
|
||||
code {
|
||||
font-family: ui-monospace, Menlo, Monaco, Consolas, "SF Mono", monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
a {
|
||||
color: color-mix(in oklab, var(--accent) 85%, canvasText 15%);
|
||||
}
|
||||
.status {
|
||||
margin-top: 10px;
|
||||
font-size: 12px;
|
||||
color: color-mix(in oklab, var(--accent) 70%, canvasText 30%);
|
||||
min-height: 16px;
|
||||
}
|
||||
.status[data-kind='ok'] {
|
||||
color: color-mix(in oklab, #16a34a 75%, canvasText 25%);
|
||||
}
|
||||
.status[data-kind='error'] {
|
||||
color: color-mix(in oklab, #ef4444 75%, canvasText 25%);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<header>
|
||||
<div class="logo" aria-hidden="true">
|
||||
<img src="icons/icon128.png" alt="" />
|
||||
</div>
|
||||
<div>
|
||||
<h1>OpenClaw Browser Relay</h1>
|
||||
<p class="subtitle">Click the toolbar button on a tab to attach / detach.</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="grid">
|
||||
<div class="card">
|
||||
<h2>Getting started</h2>
|
||||
<p>
|
||||
If you see a red <code>!</code> badge on the extension icon, the relay server is not reachable.
|
||||
Start OpenClaw’s browser relay on this machine (Gateway or node host), then click the toolbar button again.
|
||||
</p>
|
||||
<p>
|
||||
Full guide (install, remote Gateway, security): <a href="https://docs.openclaw.ai/tools/chrome-extension" target="_blank" rel="noreferrer">docs.openclaw.ai/tools/chrome-extension</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Relay connection</h2>
|
||||
<label for="port">Port</label>
|
||||
<div class="row">
|
||||
<input id="port" inputmode="numeric" pattern="[0-9]*" />
|
||||
</div>
|
||||
<label for="token" style="margin-top: 10px">Gateway token</label>
|
||||
<div class="row">
|
||||
<input id="token" type="password" autocomplete="off" style="width: min(520px, 100%)" />
|
||||
<button id="save" type="button">Save</button>
|
||||
</div>
|
||||
<div class="hint">
|
||||
Default port: <code>18792</code>. Extension connects to: <code id="relay-url">http://127.0.0.1:<port>/</code>.
|
||||
Gateway token must match <code>gateway.auth.token</code> (or <code>OPENCLAW_GATEWAY_TOKEN</code>).
|
||||
</div>
|
||||
<div class="status" id="status"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module" src="options.js"></script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,74 +0,0 @@
|
||||
import { deriveRelayToken } from './background-utils.js'
|
||||
import { classifyRelayCheckException, classifyRelayCheckResponse } from './options-validation.js'
|
||||
|
||||
const DEFAULT_PORT = 18792
|
||||
|
||||
function clampPort(value) {
|
||||
const n = Number.parseInt(String(value || ''), 10)
|
||||
if (!Number.isFinite(n)) return DEFAULT_PORT
|
||||
if (n <= 0 || n > 65535) return DEFAULT_PORT
|
||||
return n
|
||||
}
|
||||
|
||||
function updateRelayUrl(port) {
|
||||
const el = document.getElementById('relay-url')
|
||||
if (!el) return
|
||||
el.textContent = `http://127.0.0.1:${port}/`
|
||||
}
|
||||
|
||||
function setStatus(kind, message) {
|
||||
const status = document.getElementById('status')
|
||||
if (!status) return
|
||||
status.dataset.kind = kind || ''
|
||||
status.textContent = message || ''
|
||||
}
|
||||
|
||||
async function checkRelayReachable(port, token) {
|
||||
const url = `http://127.0.0.1:${port}/json/version`
|
||||
const trimmedToken = String(token || '').trim()
|
||||
if (!trimmedToken) {
|
||||
setStatus('error', 'Gateway token required. Save your gateway token to connect.')
|
||||
return
|
||||
}
|
||||
try {
|
||||
const relayToken = await deriveRelayToken(trimmedToken, port)
|
||||
// Delegate the fetch to the background service worker to bypass
|
||||
// CORS preflight on the custom x-openclaw-relay-token header.
|
||||
const res = await chrome.runtime.sendMessage({
|
||||
type: 'relayCheck',
|
||||
url,
|
||||
token: relayToken,
|
||||
})
|
||||
const result = classifyRelayCheckResponse(res, port)
|
||||
if (result.action === 'throw') throw new Error(result.error)
|
||||
setStatus(result.kind, result.message)
|
||||
} catch (err) {
|
||||
const result = classifyRelayCheckException(err, port)
|
||||
setStatus(result.kind, result.message)
|
||||
}
|
||||
}
|
||||
|
||||
async function load() {
|
||||
const stored = await chrome.storage.local.get(['relayPort', 'gatewayToken'])
|
||||
const port = clampPort(stored.relayPort)
|
||||
const token = String(stored.gatewayToken || '').trim()
|
||||
document.getElementById('port').value = String(port)
|
||||
document.getElementById('token').value = token
|
||||
updateRelayUrl(port)
|
||||
await checkRelayReachable(port, token)
|
||||
}
|
||||
|
||||
async function save() {
|
||||
const portInput = document.getElementById('port')
|
||||
const tokenInput = document.getElementById('token')
|
||||
const port = clampPort(portInput.value)
|
||||
const token = String(tokenInput.value || '').trim()
|
||||
await chrome.storage.local.set({ relayPort: port, gatewayToken: token })
|
||||
portInput.value = String(port)
|
||||
tokenInput.value = token
|
||||
updateRelayUrl(port)
|
||||
await checkRelayReachable(port, token)
|
||||
}
|
||||
|
||||
document.getElementById('save').addEventListener('click', () => void save())
|
||||
void load()
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -47,6 +47,22 @@
|
||||
"source": "Quick Start",
|
||||
"target": "快速开始"
|
||||
},
|
||||
{
|
||||
"source": "Capability Cookbook",
|
||||
"target": "能力扩展手册"
|
||||
},
|
||||
{
|
||||
"source": "Setup Wizard Reference",
|
||||
"target": "设置向导参考"
|
||||
},
|
||||
{
|
||||
"source": "CLI Setup Reference",
|
||||
"target": "CLI 设置参考"
|
||||
},
|
||||
{
|
||||
"source": "Setup Wizard (CLI)",
|
||||
"target": "设置向导(CLI)"
|
||||
},
|
||||
{
|
||||
"source": "Docs directory",
|
||||
"target": "文档目录"
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
---
|
||||
title: "Auth Credential Semantics"
|
||||
summary: "Canonical credential eligibility and resolution semantics for auth profiles"
|
||||
read_when:
|
||||
- Working on auth profile resolution or credential routing
|
||||
- Debugging model auth failures or profile order
|
||||
---
|
||||
|
||||
# Auth Credential Semantics
|
||||
|
||||
This document defines the canonical credential eligibility and resolution semantics used across:
|
||||
|
||||
@@ -700,7 +700,7 @@ openclaw system event --mode now --text "Next heartbeat: check battery."
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### “Nothing runs”
|
||||
### "Nothing runs"
|
||||
|
||||
- Check cron is enabled: `cron.enabled` and `OPENCLAW_SKIP_CRON`.
|
||||
- Check the Gateway is running continuously (cron runs inside the Gateway process).
|
||||
|
||||
@@ -38,6 +38,7 @@ Every request must include the hook token. Prefer headers:
|
||||
- `Authorization: Bearer <token>` (recommended)
|
||||
- `x-openclaw-token: <token>`
|
||||
- Query-string tokens are rejected (`?token=...` returns `400`).
|
||||
- Treat `hooks.token` holders as full-trust callers for the hook ingress surface on that gateway. Hook payload content is still untrusted, but this is not a separate non-owner auth boundary.
|
||||
|
||||
## Endpoints
|
||||
|
||||
@@ -205,6 +206,7 @@ curl -X POST http://127.0.0.1:18789/hooks/gmail \
|
||||
|
||||
- Keep hook endpoints behind loopback, tailnet, or trusted reverse proxy.
|
||||
- Use a dedicated hook token; do not reuse gateway auth tokens.
|
||||
- Prefer a dedicated hook agent with strict `tools.profile` and sandboxing so hook ingress has a narrower blast radius.
|
||||
- Repeated auth failures are rate-limited per client address to slow brute-force attempts.
|
||||
- If you use multi-agent routing, set `hooks.allowedAgentIds` to limit explicit `agentId` selection.
|
||||
- Keep `hooks.allowRequestSessionKey=false` unless you require caller-selected sessions.
|
||||
|
||||
@@ -20,11 +20,21 @@ OpenClaw supports Brave Search API as a `web_search` provider.
|
||||
|
||||
```json5
|
||||
{
|
||||
plugins: {
|
||||
entries: {
|
||||
brave: {
|
||||
config: {
|
||||
webSearch: {
|
||||
apiKey: "BRAVE_API_KEY_HERE",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
tools: {
|
||||
web: {
|
||||
search: {
|
||||
provider: "brave",
|
||||
apiKey: "BRAVE_API_KEY_HERE",
|
||||
maxResults: 5,
|
||||
timeoutSeconds: 30,
|
||||
},
|
||||
@@ -33,6 +43,9 @@ OpenClaw supports Brave Search API as a `web_search` provider.
|
||||
}
|
||||
```
|
||||
|
||||
Provider-specific Brave search settings now live under `plugins.entries.brave.config.webSearch.*`.
|
||||
Legacy `tools.web.search.apiKey` still loads through the compatibility shim, but it is no longer the canonical config path.
|
||||
|
||||
## Tool parameters
|
||||
|
||||
| Parameter | Description |
|
||||
|
||||
@@ -126,7 +126,7 @@ launchctl load ~/Library/LaunchAgents/com.user.poke-messages.plist
|
||||
|
||||
## Onboarding
|
||||
|
||||
BlueBubbles is available in the interactive setup wizard:
|
||||
BlueBubbles is available in interactive onboarding:
|
||||
|
||||
```
|
||||
openclaw onboard
|
||||
|
||||
@@ -96,8 +96,10 @@ You will need to create a new application with a bot, add the bot to your server
|
||||
Your Discord bot token is a secret (like a password). Set it on the machine running OpenClaw before messaging your agent.
|
||||
|
||||
```bash
|
||||
openclaw config set channels.discord.token '"YOUR_BOT_TOKEN"' --json
|
||||
openclaw config set channels.discord.enabled true --json
|
||||
export DISCORD_BOT_TOKEN="YOUR_BOT_TOKEN"
|
||||
openclaw config set channels.discord.token --ref-provider default --ref-source env --ref-id DISCORD_BOT_TOKEN --dry-run
|
||||
openclaw config set channels.discord.token --ref-provider default --ref-source env --ref-id DISCORD_BOT_TOKEN
|
||||
openclaw config set channels.discord.enabled true --strict-json
|
||||
openclaw gateway
|
||||
```
|
||||
|
||||
@@ -121,7 +123,11 @@ openclaw gateway
|
||||
channels: {
|
||||
discord: {
|
||||
enabled: true,
|
||||
token: "YOUR_BOT_TOKEN",
|
||||
token: {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "DISCORD_BOT_TOKEN",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -133,7 +139,7 @@ openclaw gateway
|
||||
DISCORD_BOT_TOKEN=...
|
||||
```
|
||||
|
||||
SecretRef values are also supported for `channels.discord.token` (env/file/exec providers). See [Secrets Management](/gateway/secrets).
|
||||
Plaintext `token` values are supported. SecretRef values are also supported for `channels.discord.token` across env/file/exec providers. See [Secrets Management](/gateway/secrets).
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
@@ -168,7 +174,7 @@ openclaw pairing approve discord <CODE>
|
||||
|
||||
<Note>
|
||||
Token resolution is account-aware. Config token values win over env fallback. `DISCORD_BOT_TOKEN` is only used for the default account.
|
||||
For advanced outbound calls (message tool/channel actions), an explicit per-call `token` is used for that call. Account policy/retry settings still come from the selected account in the active runtime snapshot.
|
||||
For advanced outbound calls (message tool/channel actions), an explicit per-call `token` is used for that call. This applies to send and read/probe-style actions (for example read/search/fetch/thread/pins/permissions). Account policy/retry settings still come from the selected account in the active runtime snapshot.
|
||||
</Note>
|
||||
|
||||
## Recommended: Set up a guild workspace
|
||||
|
||||
@@ -30,9 +30,9 @@ openclaw plugins install @openclaw/feishu
|
||||
|
||||
There are two ways to add the Feishu channel:
|
||||
|
||||
### Method 1: onboarding wizard (recommended)
|
||||
### Method 1: onboarding (recommended)
|
||||
|
||||
If you just installed OpenClaw, run the wizard:
|
||||
If you just installed OpenClaw, run onboarding:
|
||||
|
||||
```bash
|
||||
openclaw onboard
|
||||
@@ -711,7 +711,7 @@ Key options:
|
||||
- ✅ Images
|
||||
- ✅ Files
|
||||
- ✅ Audio
|
||||
- ✅ Video
|
||||
- ✅ Video/media
|
||||
- ✅ Stickers
|
||||
|
||||
### Send
|
||||
@@ -720,4 +720,28 @@ Key options:
|
||||
- ✅ Images
|
||||
- ✅ Files
|
||||
- ✅ Audio
|
||||
- ⚠️ Rich text (partial support)
|
||||
- ✅ Video/media
|
||||
- ✅ Interactive cards
|
||||
- ⚠️ Rich text (post-style formatting and cards, not arbitrary Feishu authoring features)
|
||||
|
||||
### Threads and replies
|
||||
|
||||
- ✅ Inline replies
|
||||
- ✅ Topic-thread replies where Feishu exposes `reply_in_thread`
|
||||
- ✅ Media replies stay thread-aware when replying to a thread/topic message
|
||||
|
||||
## Runtime action surface
|
||||
|
||||
Feishu currently exposes these runtime actions:
|
||||
|
||||
- `send`
|
||||
- `read`
|
||||
- `edit`
|
||||
- `thread-reply`
|
||||
- `pin`
|
||||
- `list-pins`
|
||||
- `unpin`
|
||||
- `member-info`
|
||||
- `channel-info`
|
||||
- `channel-list`
|
||||
- `react` and `reactions` when reactions are enabled in config
|
||||
|
||||
@@ -11,7 +11,7 @@ Goal: let Clawd sit in WhatsApp groups, wake up only when pinged, and keep that
|
||||
|
||||
Note: `agents.list[].groupChat.mentionPatterns` is now used by Telegram/Discord/Slack/iMessage as well; this doc focuses on WhatsApp-specific behavior. For multi-agent setups, set `agents.list[].groupChat.mentionPatterns` per agent (or use `messages.groupChat.mentionPatterns` as a global fallback).
|
||||
|
||||
## What’s implemented (2025-12-03)
|
||||
## Current implementation (2025-12-03)
|
||||
|
||||
- Activation modes: `mention` (default) or `always`. `mention` requires a ping (real WhatsApp @-mentions via `mentionedJids`, safe regex patterns, or the bot’s E.164 anywhere in the text). `always` wakes the agent on every message but it should reply only when it can add meaningful value; otherwise it returns the silent token `NO_REPLY`. Defaults can be set in config (`channels.whatsapp.groups`) and overridden per group via `/activation`. When `channels.whatsapp.groups` is set, it also acts as a group allowlist (include `"*"` to allow all).
|
||||
- Group policy: `channels.whatsapp.groupPolicy` controls whether group messages are accepted (`open|disabled|allowlist`). `allowlist` uses `channels.whatsapp.groupAllowFrom` (fallback: explicit `channels.whatsapp.allowFrom`). Default is `allowlist` (blocked until you add senders).
|
||||
|
||||
@@ -31,7 +31,7 @@ Local checkout (when running from a git repo):
|
||||
openclaw plugins install ./extensions/matrix
|
||||
```
|
||||
|
||||
If you choose Matrix during configure/onboarding and a git checkout is detected,
|
||||
If you choose Matrix during setup and a git checkout is detected,
|
||||
OpenClaw will offer the local install path automatically.
|
||||
|
||||
Details: [Plugins](/tools/plugin)
|
||||
@@ -72,7 +72,7 @@ Details: [Plugins](/tools/plugin)
|
||||
- If both are set, config takes precedence.
|
||||
- With access token: user ID is fetched automatically via `/whoami`.
|
||||
- When set, `channels.matrix.userId` should be the full Matrix ID (example: `@bot:example.org`).
|
||||
5. Restart the gateway (or finish onboarding).
|
||||
5. Restart the gateway (or finish setup).
|
||||
6. Start a DM with the bot or invite it to a room from any Matrix client
|
||||
(Element, Beeper, etc.; see [https://matrix.org/ecosystem/clients/](https://matrix.org/ecosystem/clients/)). Beeper requires E2EE,
|
||||
so set `channels.matrix.encryption: true` and verify the device.
|
||||
|
||||
@@ -28,7 +28,7 @@ Local checkout (when running from a git repo):
|
||||
openclaw plugins install ./extensions/mattermost
|
||||
```
|
||||
|
||||
If you choose Mattermost during configure/onboarding and a git checkout is detected,
|
||||
If you choose Mattermost during setup and a git checkout is detected,
|
||||
OpenClaw will offer the local install path automatically.
|
||||
|
||||
Details: [Plugins](/tools/plugin)
|
||||
@@ -191,6 +191,35 @@ OpenClaw resolves them **user-first**:
|
||||
|
||||
If you need deterministic behavior, always use the explicit prefixes (`user:<id>` / `channel:<id>`).
|
||||
|
||||
## DM channel retry
|
||||
|
||||
When OpenClaw sends to a Mattermost DM target and needs to resolve the direct channel first, it
|
||||
retries transient direct-channel creation failures by default.
|
||||
|
||||
Use `channels.mattermost.dmChannelRetry` to tune that behavior globally for the Mattermost plugin,
|
||||
or `channels.mattermost.accounts.<id>.dmChannelRetry` for one account.
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
mattermost: {
|
||||
dmChannelRetry: {
|
||||
maxRetries: 3,
|
||||
initialDelayMs: 1000,
|
||||
maxDelayMs: 10000,
|
||||
timeoutMs: 30000,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- This applies only to DM channel creation (`/api/v4/channels/direct`), not every Mattermost API call.
|
||||
- Retries apply to transient failures such as rate limits, 5xx responses, and network or timeout errors.
|
||||
- 4xx client errors other than `429` are treated as permanent and are not retried.
|
||||
|
||||
## Reactions (message tool)
|
||||
|
||||
- Use `message action=react` with `channel=mattermost`.
|
||||
|
||||
@@ -33,7 +33,7 @@ Local checkout (when running from a git repo):
|
||||
openclaw plugins install ./extensions/msteams
|
||||
```
|
||||
|
||||
If you choose Teams during configure/onboarding and a git checkout is detected,
|
||||
If you choose Teams during setup and a git checkout is detected,
|
||||
OpenClaw will offer the local install path automatically.
|
||||
|
||||
Details: [Plugins](/tools/plugin)
|
||||
|
||||
@@ -25,7 +25,7 @@ Local checkout (when running from a git repo):
|
||||
openclaw plugins install ./extensions/nextcloud-talk
|
||||
```
|
||||
|
||||
If you choose Nextcloud Talk during configure/onboarding and a git checkout is detected,
|
||||
If you choose Nextcloud Talk during setup and a git checkout is detected,
|
||||
OpenClaw will offer the local install path automatically.
|
||||
|
||||
Details: [Plugins](/tools/plugin)
|
||||
@@ -43,7 +43,7 @@ Details: [Plugins](/tools/plugin)
|
||||
4. Configure OpenClaw:
|
||||
- Config: `channels.nextcloud-talk.baseUrl` + `channels.nextcloud-talk.botSecret`
|
||||
- Or env: `NEXTCLOUD_TALK_BOT_SECRET` (default account only)
|
||||
5. Restart the gateway (or finish onboarding).
|
||||
5. Restart the gateway (or finish setup).
|
||||
|
||||
Minimal config:
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ Nostr is a decentralized protocol for social networking. This channel enables Op
|
||||
|
||||
### Onboarding (recommended)
|
||||
|
||||
- The onboarding wizard (`openclaw onboard`) and `openclaw channels add` list optional channel plugins.
|
||||
- Onboarding (`openclaw onboard`) and `openclaw channels add` list optional channel plugins.
|
||||
- Selecting Nostr prompts you to install the plugin on demand.
|
||||
|
||||
Install defaults:
|
||||
|
||||
@@ -115,7 +115,7 @@ Token resolution order is account-aware. In practice, config values win over env
|
||||
|
||||
`channels.telegram.allowFrom` accepts numeric Telegram user IDs. `telegram:` / `tg:` prefixes are accepted and normalized.
|
||||
`dmPolicy: "allowlist"` with empty `allowFrom` blocks all DMs and is rejected by config validation.
|
||||
The onboarding wizard accepts `@username` input and resolves it to numeric IDs.
|
||||
Onboarding accepts `@username` input and resolves it to numeric IDs.
|
||||
If you upgraded and your config contains `@username` allowlist entries, run `openclaw doctor --fix` to resolve them (best-effort; requires a Telegram bot token).
|
||||
If you previously relied on pairing-store allowlist files, `openclaw doctor --fix` can recover entries into `channels.telegram.allowFrom` in allowlist flows (for example when `dmPolicy: "allowlist"` has no explicit IDs yet).
|
||||
|
||||
|
||||
@@ -255,7 +255,7 @@ openclaw doctor
|
||||
openclaw channels status --probe
|
||||
```
|
||||
|
||||
### Bot doesn't respond to messages
|
||||
### Bot does not respond to messages
|
||||
|
||||
**Check access control:** Ensure your user ID is in `allowFrom`, or temporarily remove
|
||||
`allowFrom` and set `allowedRoles: ["all"]` to test.
|
||||
|
||||
@@ -76,7 +76,7 @@ openclaw pairing approve whatsapp <CODE>
|
||||
</Steps>
|
||||
|
||||
<Note>
|
||||
OpenClaw recommends running WhatsApp on a separate number when possible. (The channel metadata and onboarding flow are optimized for that setup, but personal-number setups are also supported.)
|
||||
OpenClaw recommends running WhatsApp on a separate number when possible. (The channel metadata and setup flow are optimized for that setup, but personal-number setups are also supported.)
|
||||
</Note>
|
||||
|
||||
## Deployment patterns
|
||||
|
||||
@@ -14,7 +14,7 @@ Status: experimental. DMs are supported. The [Capabilities](#capabilities) secti
|
||||
Zalo ships as a plugin and is not bundled with the core install.
|
||||
|
||||
- Install via CLI: `openclaw plugins install @openclaw/zalo`
|
||||
- Or select **Zalo** during onboarding and confirm the install prompt
|
||||
- Or select **Zalo** during setup and confirm the install prompt
|
||||
- Details: [Plugins](/tools/plugin)
|
||||
|
||||
## Quick setup (beginner)
|
||||
@@ -22,11 +22,11 @@ Zalo ships as a plugin and is not bundled with the core install.
|
||||
1. Install the Zalo plugin:
|
||||
- From a source checkout: `openclaw plugins install ./extensions/zalo`
|
||||
- From npm (if published): `openclaw plugins install @openclaw/zalo`
|
||||
- Or pick **Zalo** in onboarding and confirm the install prompt
|
||||
- Or pick **Zalo** in setup and confirm the install prompt
|
||||
2. Set the token:
|
||||
- Env: `ZALO_BOT_TOKEN=...`
|
||||
- Or config: `channels.zalo.accounts.default.botToken: "..."`.
|
||||
3. Restart the gateway (or finish onboarding).
|
||||
3. Restart the gateway (or finish setup).
|
||||
4. DM access is pairing by default; approve the pairing code on first contact.
|
||||
|
||||
Minimal config:
|
||||
|
||||
@@ -41,7 +41,7 @@ No external `zca`/`openzca` CLI binary is required.
|
||||
}
|
||||
```
|
||||
|
||||
4. Restart the Gateway (or finish onboarding).
|
||||
4. Restart the Gateway (or finish setup).
|
||||
5. DM access defaults to pairing; approve the pairing code on first contact.
|
||||
|
||||
## What it is
|
||||
@@ -74,7 +74,7 @@ openclaw directory groups list --channel zalouser --query "work"
|
||||
|
||||
`channels.zalouser.dmPolicy` supports: `pairing | allowlist | open | disabled` (default: `pairing`).
|
||||
|
||||
`channels.zalouser.allowFrom` accepts user IDs or names. During onboarding, names are resolved to IDs using the plugin's in-process contact lookup.
|
||||
`channels.zalouser.allowFrom` accepts user IDs or names. During setup, names are resolved to IDs using the plugin's in-process contact lookup.
|
||||
|
||||
Approve via:
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ title: "acp"
|
||||
|
||||
# acp
|
||||
|
||||
Run the [Agent Client Protocol (ACP)](https://agentclientprotocol.com/) bridge that talks to a OpenClaw Gateway.
|
||||
Run the [Agent Client Protocol (ACP)](https://agentclientprotocol.com/) bridge that talks to an OpenClaw Gateway.
|
||||
|
||||
This command speaks ACP over stdio for IDEs and forwards prompts to the Gateway
|
||||
over WebSocket. It keeps ACP sessions mapped to Gateway session keys.
|
||||
@@ -102,7 +102,7 @@ Permission model (client debug mode):
|
||||
## How to use this
|
||||
|
||||
Use ACP when an IDE (or other client) speaks Agent Client Protocol and you want
|
||||
it to drive a OpenClaw Gateway session.
|
||||
it to drive an OpenClaw Gateway session.
|
||||
|
||||
1. Ensure the Gateway is running (local or remote).
|
||||
2. Configure the Gateway target (config or flags).
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
---
|
||||
summary: "CLI reference for `openclaw browser` (profiles, tabs, actions, extension relay)"
|
||||
summary: "CLI reference for `openclaw browser` (profiles, tabs, actions, Chrome MCP, and CDP)"
|
||||
read_when:
|
||||
- You use `openclaw browser` and want examples for common tasks
|
||||
- You want to control a browser running on another machine via a node host
|
||||
- You want to use the Chrome extension relay (attach/detach via toolbar button)
|
||||
- You want to attach to your local signed-in Chrome via Chrome MCP
|
||||
title: "browser"
|
||||
---
|
||||
|
||||
@@ -14,7 +14,6 @@ Manage OpenClaw’s browser control server and run browser actions (tabs, snapsh
|
||||
Related:
|
||||
|
||||
- Browser tool + API: [Browser tool](/tools/browser)
|
||||
- Chrome extension relay: [Chrome extension](/tools/chrome-extension)
|
||||
|
||||
## Common flags
|
||||
|
||||
@@ -37,13 +36,14 @@ openclaw browser --browser-profile openclaw snapshot
|
||||
|
||||
Profiles are named browser routing configs. In practice:
|
||||
|
||||
- `openclaw`: launches/attaches to a dedicated OpenClaw-managed Chrome instance (isolated user data dir).
|
||||
- `openclaw`: launches or attaches to a dedicated OpenClaw-managed Chrome instance (isolated user data dir).
|
||||
- `user`: controls your existing signed-in Chrome session via Chrome DevTools MCP.
|
||||
- `chrome-relay`: controls your existing Chrome tab(s) via the Chrome extension relay.
|
||||
- custom CDP profiles: point at a local or remote CDP endpoint.
|
||||
|
||||
```bash
|
||||
openclaw browser profiles
|
||||
openclaw browser create-profile --name work --color "#FF5A36"
|
||||
openclaw browser create-profile --name chrome-live --driver existing-session
|
||||
openclaw browser delete-profile --name work
|
||||
```
|
||||
|
||||
@@ -84,20 +84,18 @@ openclaw browser click <ref>
|
||||
openclaw browser type <ref> "hello"
|
||||
```
|
||||
|
||||
## Chrome extension relay (attach via toolbar button)
|
||||
## Existing Chrome via MCP
|
||||
|
||||
This mode lets the agent control an existing Chrome tab that you attach manually (it does not auto-attach).
|
||||
|
||||
Install the unpacked extension to a stable path:
|
||||
Use the built-in `user` profile, or create your own `existing-session` profile:
|
||||
|
||||
```bash
|
||||
openclaw browser extension install
|
||||
openclaw browser extension path
|
||||
openclaw browser --browser-profile user tabs
|
||||
openclaw browser create-profile --name chrome-live --driver existing-session
|
||||
openclaw browser create-profile --name brave-live --driver existing-session --user-data-dir "~/Library/Application Support/BraveSoftware/Brave-Browser"
|
||||
openclaw browser --browser-profile chrome-live tabs
|
||||
```
|
||||
|
||||
Then Chrome → `chrome://extensions` → enable “Developer mode” → “Load unpacked” → select the printed folder.
|
||||
|
||||
Full guide: [Chrome extension](/tools/chrome-extension)
|
||||
This path is host-only. For Docker, headless servers, Browserless, or other remote setups, use a CDP profile instead.
|
||||
|
||||
## Remote browser control (node host proxy)
|
||||
|
||||
|
||||
@@ -7,9 +7,9 @@ title: "config"
|
||||
|
||||
# `openclaw config`
|
||||
|
||||
Config helpers: get/set/unset/validate values by path and print the active
|
||||
config file. Run without a subcommand to open
|
||||
the configure wizard (same as `openclaw configure`).
|
||||
Config helpers for non-interactive edits in `openclaw.json`: get/set/unset/validate
|
||||
values by path and print the active config file. Run without a subcommand to
|
||||
open the configure wizard (same as `openclaw configure`).
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -19,7 +19,10 @@ openclaw config get browser.executablePath
|
||||
openclaw config set browser.executablePath "/usr/bin/google-chrome"
|
||||
openclaw config set agents.defaults.heartbeat.every "2h"
|
||||
openclaw config set agents.list[0].tools.exec.node "node-id-or-name"
|
||||
openclaw config unset tools.web.search.apiKey
|
||||
openclaw config set channels.discord.token --ref-provider default --ref-source env --ref-id DISCORD_BOT_TOKEN
|
||||
openclaw config set secrets.providers.vaultfile --provider-source file --provider-path /etc/openclaw/secrets.json --provider-mode json
|
||||
openclaw config unset plugins.entries.brave.config.webSearch.apiKey
|
||||
openclaw config set channels.discord.token --ref-provider default --ref-source env --ref-id DISCORD_BOT_TOKEN --dry-run
|
||||
openclaw config validate
|
||||
openclaw config validate --json
|
||||
```
|
||||
@@ -51,6 +54,230 @@ openclaw config set gateway.port 19001 --strict-json
|
||||
openclaw config set channels.whatsapp.groups '["*"]' --strict-json
|
||||
```
|
||||
|
||||
## `config set` modes
|
||||
|
||||
`openclaw config set` supports four assignment styles:
|
||||
|
||||
1. Value mode: `openclaw config set <path> <value>`
|
||||
2. SecretRef builder mode:
|
||||
|
||||
```bash
|
||||
openclaw config set channels.discord.token \
|
||||
--ref-provider default \
|
||||
--ref-source env \
|
||||
--ref-id DISCORD_BOT_TOKEN
|
||||
```
|
||||
|
||||
3. Provider builder mode (`secrets.providers.<alias>` path only):
|
||||
|
||||
```bash
|
||||
openclaw config set secrets.providers.vault \
|
||||
--provider-source exec \
|
||||
--provider-command /usr/local/bin/openclaw-vault \
|
||||
--provider-arg read \
|
||||
--provider-arg openai/api-key \
|
||||
--provider-timeout-ms 5000
|
||||
```
|
||||
|
||||
4. Batch mode (`--batch-json` or `--batch-file`):
|
||||
|
||||
```bash
|
||||
openclaw config set --batch-json '[
|
||||
{
|
||||
"path": "secrets.providers.default",
|
||||
"provider": { "source": "env" }
|
||||
},
|
||||
{
|
||||
"path": "channels.discord.token",
|
||||
"ref": { "source": "env", "provider": "default", "id": "DISCORD_BOT_TOKEN" }
|
||||
}
|
||||
]'
|
||||
```
|
||||
|
||||
```bash
|
||||
openclaw config set --batch-file ./config-set.batch.json --dry-run
|
||||
```
|
||||
|
||||
Batch parsing always uses the batch payload (`--batch-json`/`--batch-file`) as the source of truth.
|
||||
`--strict-json` / `--json` do not change batch parsing behavior.
|
||||
|
||||
JSON path/value mode remains supported for both SecretRefs and providers:
|
||||
|
||||
```bash
|
||||
openclaw config set channels.discord.token \
|
||||
'{"source":"env","provider":"default","id":"DISCORD_BOT_TOKEN"}' \
|
||||
--strict-json
|
||||
|
||||
openclaw config set secrets.providers.vaultfile \
|
||||
'{"source":"file","path":"/etc/openclaw/secrets.json","mode":"json"}' \
|
||||
--strict-json
|
||||
```
|
||||
|
||||
## Provider Builder Flags
|
||||
|
||||
Provider builder targets must use `secrets.providers.<alias>` as the path.
|
||||
|
||||
Common flags:
|
||||
|
||||
- `--provider-source <env|file|exec>`
|
||||
- `--provider-timeout-ms <ms>` (`file`, `exec`)
|
||||
|
||||
Env provider (`--provider-source env`):
|
||||
|
||||
- `--provider-allowlist <ENV_VAR>` (repeatable)
|
||||
|
||||
File provider (`--provider-source file`):
|
||||
|
||||
- `--provider-path <path>` (required)
|
||||
- `--provider-mode <singleValue|json>`
|
||||
- `--provider-max-bytes <bytes>`
|
||||
|
||||
Exec provider (`--provider-source exec`):
|
||||
|
||||
- `--provider-command <path>` (required)
|
||||
- `--provider-arg <arg>` (repeatable)
|
||||
- `--provider-no-output-timeout-ms <ms>`
|
||||
- `--provider-max-output-bytes <bytes>`
|
||||
- `--provider-json-only`
|
||||
- `--provider-env <KEY=VALUE>` (repeatable)
|
||||
- `--provider-pass-env <ENV_VAR>` (repeatable)
|
||||
- `--provider-trusted-dir <path>` (repeatable)
|
||||
- `--provider-allow-insecure-path`
|
||||
- `--provider-allow-symlink-command`
|
||||
|
||||
Hardened exec provider example:
|
||||
|
||||
```bash
|
||||
openclaw config set secrets.providers.vault \
|
||||
--provider-source exec \
|
||||
--provider-command /usr/local/bin/openclaw-vault \
|
||||
--provider-arg read \
|
||||
--provider-arg openai/api-key \
|
||||
--provider-json-only \
|
||||
--provider-pass-env VAULT_TOKEN \
|
||||
--provider-trusted-dir /usr/local/bin \
|
||||
--provider-timeout-ms 5000
|
||||
```
|
||||
|
||||
## Dry run
|
||||
|
||||
Use `--dry-run` to validate changes without writing `openclaw.json`.
|
||||
|
||||
```bash
|
||||
openclaw config set channels.discord.token \
|
||||
--ref-provider default \
|
||||
--ref-source env \
|
||||
--ref-id DISCORD_BOT_TOKEN \
|
||||
--dry-run
|
||||
|
||||
openclaw config set channels.discord.token \
|
||||
--ref-provider default \
|
||||
--ref-source env \
|
||||
--ref-id DISCORD_BOT_TOKEN \
|
||||
--dry-run \
|
||||
--json
|
||||
|
||||
openclaw config set channels.discord.token \
|
||||
--ref-provider vault \
|
||||
--ref-source exec \
|
||||
--ref-id discord/token \
|
||||
--dry-run \
|
||||
--allow-exec
|
||||
```
|
||||
|
||||
Dry-run behavior:
|
||||
|
||||
- Builder mode: runs SecretRef resolvability checks for changed refs/providers.
|
||||
- JSON mode (`--strict-json`, `--json`, or batch mode): runs schema validation plus SecretRef resolvability checks.
|
||||
- Exec SecretRef checks are skipped by default during dry-run to avoid command side effects.
|
||||
- Use `--allow-exec` with `--dry-run` to opt in to exec SecretRef checks (this may execute provider commands).
|
||||
- `--allow-exec` is dry-run only and errors if used without `--dry-run`.
|
||||
|
||||
`--dry-run --json` prints a machine-readable report:
|
||||
|
||||
- `ok`: whether dry-run passed
|
||||
- `operations`: number of assignments evaluated
|
||||
- `checks`: whether schema/resolvability checks ran
|
||||
- `checks.resolvabilityComplete`: whether resolvability checks ran to completion (false when exec refs are skipped)
|
||||
- `refsChecked`: number of refs actually resolved during dry-run
|
||||
- `skippedExecRefs`: number of exec refs skipped because `--allow-exec` was not set
|
||||
- `errors`: structured schema/resolvability failures when `ok=false`
|
||||
|
||||
### JSON Output Shape
|
||||
|
||||
```json5
|
||||
{
|
||||
ok: boolean,
|
||||
operations: number,
|
||||
configPath: string,
|
||||
inputModes: ["value" | "json" | "builder", ...],
|
||||
checks: {
|
||||
schema: boolean,
|
||||
resolvability: boolean,
|
||||
resolvabilityComplete: boolean,
|
||||
},
|
||||
refsChecked: number,
|
||||
skippedExecRefs: number,
|
||||
errors?: [
|
||||
{
|
||||
kind: "schema" | "resolvability",
|
||||
message: string,
|
||||
ref?: string, // present for resolvability errors
|
||||
},
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
Success example:
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"operations": 1,
|
||||
"configPath": "~/.openclaw/openclaw.json",
|
||||
"inputModes": ["builder"],
|
||||
"checks": {
|
||||
"schema": false,
|
||||
"resolvability": true,
|
||||
"resolvabilityComplete": true
|
||||
},
|
||||
"refsChecked": 1,
|
||||
"skippedExecRefs": 0
|
||||
}
|
||||
```
|
||||
|
||||
Failure example:
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": false,
|
||||
"operations": 1,
|
||||
"configPath": "~/.openclaw/openclaw.json",
|
||||
"inputModes": ["builder"],
|
||||
"checks": {
|
||||
"schema": false,
|
||||
"resolvability": true,
|
||||
"resolvabilityComplete": true
|
||||
},
|
||||
"refsChecked": 1,
|
||||
"skippedExecRefs": 0,
|
||||
"errors": [
|
||||
{
|
||||
"kind": "resolvability",
|
||||
"message": "Error: Environment variable \"MISSING_TEST_SECRET\" is not set.",
|
||||
"ref": "env:default:MISSING_TEST_SECRET"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
If dry-run fails:
|
||||
|
||||
- `config schema validation failed`: your post-change config shape is invalid; fix path/value or provider/ref object shape.
|
||||
- `SecretRef assignment(s) could not be resolved`: referenced provider/ref currently cannot resolve (missing env var, invalid file pointer, exec provider failure, or provider/source mismatch).
|
||||
- `Dry run note: skipped <n> exec SecretRef resolvability check(s)`: dry-run skipped exec refs; rerun with `--allow-exec` if you need exec resolvability validation.
|
||||
- For batch mode, fix failing entries and rerun `--dry-run` before writing.
|
||||
|
||||
## Subcommands
|
||||
|
||||
- `config file`: Print the active config file path (resolved from `OPENCLAW_CONFIG_PATH` or default location).
|
||||
|
||||
@@ -40,7 +40,7 @@ openclaw message send --channel slack --target user:U012ABCDEF --message "hello"
|
||||
- Zalo (plugin): user id (Bot API)
|
||||
- Zalo Personal / `zalouser` (plugin): thread id (DM/group) from `zca` (`me`, `friend list`, `group list`)
|
||||
|
||||
## Self (“me”)
|
||||
## Self ("me")
|
||||
|
||||
```bash
|
||||
openclaw directory self --channel zalouser
|
||||
|
||||
@@ -10,6 +10,6 @@ title: "docs"
|
||||
Search the live docs index.
|
||||
|
||||
```bash
|
||||
openclaw docs browser extension
|
||||
openclaw docs browser existing-session
|
||||
openclaw docs sandbox allowHostControl
|
||||
```
|
||||
|
||||
@@ -32,6 +32,8 @@ Notes:
|
||||
- Doctor includes a memory-search readiness check and can recommend `openclaw configure --section model` when embedding credentials are missing.
|
||||
- If sandbox mode is enabled but Docker is unavailable, doctor reports a high-signal warning with remediation (`install Docker` or `openclaw config set agents.defaults.sandbox.mode off`).
|
||||
- If `gateway.auth.token`/`gateway.auth.password` are SecretRef-managed and unavailable in the current command path, doctor reports a read-only warning and does not write plaintext fallback credentials.
|
||||
- If channel SecretRef inspection fails in a fix path, doctor continues and reports a warning instead of exiting early.
|
||||
- Telegram `allowFrom` username auto-resolution (`doctor --fix`) requires a resolvable Telegram token in the current command path. If token inspection is unavailable, doctor reports a warning and skips auto-resolution for that pass.
|
||||
|
||||
## macOS: `launchctl` env overrides
|
||||
|
||||
|
||||
@@ -88,7 +88,7 @@ OpenClaw uses a lobster palette for CLI output.
|
||||
- `error` (#E23D2D): errors, failures.
|
||||
- `muted` (#8B7F77): de-emphasis, metadata.
|
||||
|
||||
Palette source of truth: `src/terminal/palette.ts` (aka “lobster seam”).
|
||||
Palette source of truth: `src/terminal/palette.ts` (the “lobster palette”).
|
||||
|
||||
## Command tree
|
||||
|
||||
@@ -101,6 +101,8 @@ openclaw [--dev] [--profile <name>] <command>
|
||||
get
|
||||
set
|
||||
unset
|
||||
file
|
||||
validate
|
||||
completion
|
||||
doctor
|
||||
dashboard
|
||||
@@ -274,17 +276,18 @@ Note: plugins can add additional top-level commands (for example `openclaw voice
|
||||
## Secrets
|
||||
|
||||
- `openclaw secrets reload` — re-resolve refs and atomically swap the runtime snapshot.
|
||||
- `openclaw secrets audit` — scan for plaintext residues, unresolved refs, and precedence drift.
|
||||
- `openclaw secrets configure` — interactive helper for provider setup + SecretRef mapping + preflight/apply.
|
||||
- `openclaw secrets apply --from <plan.json>` — apply a previously generated plan (`--dry-run` supported).
|
||||
- `openclaw secrets audit` — scan for plaintext residues, unresolved refs, and precedence drift (`--allow-exec` to execute exec providers during audit).
|
||||
- `openclaw secrets configure` — interactive helper for provider setup + SecretRef mapping + preflight/apply (`--allow-exec` to execute exec providers during preflight and exec-containing apply flows).
|
||||
- `openclaw secrets apply --from <plan.json>` — apply a previously generated plan (`--dry-run` supported; use `--allow-exec` to permit exec providers in dry-run and exec-containing write plans).
|
||||
|
||||
## Plugins
|
||||
|
||||
Manage extensions and their config:
|
||||
|
||||
- `openclaw plugins list` — discover plugins (use `--json` for machine output).
|
||||
- `openclaw plugins info <id>` — show details for a plugin.
|
||||
- `openclaw plugins install <path|.tgz|npm-spec>` — install a plugin (or add a plugin path to `plugins.load.paths`).
|
||||
- `openclaw plugins inspect <id>` — show details for a plugin (`info` is an alias).
|
||||
- `openclaw plugins install <path|.tgz|npm-spec|plugin@marketplace>` — install a plugin (or add a plugin path to `plugins.load.paths`).
|
||||
- `openclaw plugins marketplace list <marketplace>` — list marketplace entries before install.
|
||||
- `openclaw plugins enable <id>` / `disable <id>` — toggle `plugins.entries.<id>.enabled`.
|
||||
- `openclaw plugins doctor` — report plugin load errors.
|
||||
|
||||
@@ -317,22 +320,22 @@ Initialize config + workspace.
|
||||
Options:
|
||||
|
||||
- `--workspace <dir>`: agent workspace path (default `~/.openclaw/workspace`).
|
||||
- `--wizard`: run the onboarding wizard.
|
||||
- `--non-interactive`: run wizard without prompts.
|
||||
- `--mode <local|remote>`: wizard mode.
|
||||
- `--wizard`: run onboarding.
|
||||
- `--non-interactive`: run onboarding without prompts.
|
||||
- `--mode <local|remote>`: onboard mode.
|
||||
- `--remote-url <url>`: remote Gateway URL.
|
||||
- `--remote-token <token>`: remote Gateway token.
|
||||
|
||||
Wizard auto-runs when any wizard flags are present (`--non-interactive`, `--mode`, `--remote-url`, `--remote-token`).
|
||||
Onboarding auto-runs when any onboarding flags are present (`--non-interactive`, `--mode`, `--remote-url`, `--remote-token`).
|
||||
|
||||
### `onboard`
|
||||
|
||||
Interactive wizard to set up gateway, workspace, and skills.
|
||||
Interactive onboarding for gateway, workspace, and skills.
|
||||
|
||||
Options:
|
||||
|
||||
- `--workspace <dir>`
|
||||
- `--reset` (reset config + credentials + sessions before wizard)
|
||||
- `--reset` (reset config + credentials + sessions before onboarding)
|
||||
- `--reset-scope <config|config+creds+sessions|full>` (default `config+creds+sessions`; use `full` to also remove workspace)
|
||||
- `--non-interactive`
|
||||
- `--mode <local|remote>`
|
||||
@@ -392,7 +395,15 @@ subcommand launches the wizard.
|
||||
Subcommands:
|
||||
|
||||
- `config get <path>`: print a config value (dot/bracket path).
|
||||
- `config set <path> <value>`: set a value (JSON5 or raw string).
|
||||
- `config set`: supports four assignment modes:
|
||||
- value mode: `config set <path> <value>` (JSON5-or-string parsing)
|
||||
- SecretRef builder mode: `config set <path> --ref-provider <provider> --ref-source <source> --ref-id <id>`
|
||||
- provider builder mode: `config set secrets.providers.<alias> --provider-source <env|file|exec> ...`
|
||||
- batch mode: `config set --batch-json '<json>'` or `config set --batch-file <path>`
|
||||
- `config set --dry-run`: validate assignments without writing `openclaw.json` (exec SecretRef checks are skipped by default).
|
||||
- `config set --allow-exec --dry-run`: opt in to exec SecretRef dry-run checks (may execute provider commands).
|
||||
- `config set --dry-run --json`: emit machine-readable dry-run output (checks + completeness signal, operations, refs checked/skipped, errors).
|
||||
- `config set --strict-json`: require JSON5 parsing for path/value input. `--json` remains a legacy alias for strict parsing outside dry-run output mode.
|
||||
- `config unset <path>`: remove a value.
|
||||
- `config file`: print the active config file path.
|
||||
- `config validate`: validate the current config against the schema without starting the gateway.
|
||||
|
||||
@@ -50,6 +50,16 @@ Name lookup:
|
||||
- `--dry-run`
|
||||
- `--verbose`
|
||||
|
||||
## SecretRef behavior
|
||||
|
||||
- `openclaw message` resolves supported channel SecretRefs before running the selected action.
|
||||
- Resolution is scoped to the active action target when possible:
|
||||
- channel-scoped when `--channel` is set (or inferred from prefixed targets like `discord:...`)
|
||||
- account-scoped when `--account` is set (channel globals + selected account surfaces)
|
||||
- when `--account` is omitted, OpenClaw does not force a `default` account SecretRef scope
|
||||
- Unresolved SecretRefs on unrelated channels do not block a targeted message action.
|
||||
- If the selected channel/account SecretRef is unresolved, the command fails closed for that action.
|
||||
|
||||
## Actions
|
||||
|
||||
### Core
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
summary: "CLI reference for `openclaw onboard` (interactive onboarding wizard)"
|
||||
summary: "CLI reference for `openclaw onboard` (interactive onboarding)"
|
||||
read_when:
|
||||
- You want guided setup for gateway, workspace, auth, channels, and skills
|
||||
title: "onboard"
|
||||
@@ -7,13 +7,13 @@ title: "onboard"
|
||||
|
||||
# `openclaw onboard`
|
||||
|
||||
Interactive onboarding wizard (local or remote Gateway setup).
|
||||
Interactive onboarding for local or remote Gateway setup.
|
||||
|
||||
## Related guides
|
||||
|
||||
- CLI onboarding hub: [Onboarding Wizard (CLI)](/start/wizard)
|
||||
- CLI onboarding hub: [Onboarding (CLI)](/start/wizard)
|
||||
- Onboarding overview: [Onboarding Overview](/start/onboarding-overview)
|
||||
- CLI onboarding reference: [CLI Onboarding Reference](/start/wizard-cli-reference)
|
||||
- CLI onboarding reference: [CLI Setup Reference](/start/wizard-cli-reference)
|
||||
- CLI automation: [CLI Automation](/start/wizard-cli-automation)
|
||||
- macOS onboarding: [Onboarding (macOS App)](/start/onboarding)
|
||||
|
||||
@@ -140,7 +140,7 @@ Flow notes:
|
||||
|
||||
- `quickstart`: minimal prompts, auto-generates a gateway token.
|
||||
- `manual`: full prompts for port/bind/auth (alias of `advanced`).
|
||||
- Local onboarding DM scope behavior: [CLI Onboarding Reference](/start/wizard-cli-reference#outputs-and-internals).
|
||||
- Local onboarding DM scope behavior: [CLI Setup Reference](/start/wizard-cli-reference#outputs-and-internals).
|
||||
- Fastest first chat: `openclaw dashboard` (Control UI, no channel setup).
|
||||
- Custom Provider: connect any OpenAI or Anthropic compatible endpoint,
|
||||
including hosted providers not listed. Use Unknown to auto-detect.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
summary: "CLI reference for `openclaw plugins` (list, install, uninstall, enable/disable, doctor)"
|
||||
summary: "CLI reference for `openclaw plugins` (list, install, marketplace, uninstall, enable/disable, doctor)"
|
||||
read_when:
|
||||
- You want to install or manage Gateway plugins or compatible bundles
|
||||
- You want to debug plugin load failures
|
||||
@@ -21,13 +21,15 @@ Related:
|
||||
|
||||
```bash
|
||||
openclaw plugins list
|
||||
openclaw plugins info <id>
|
||||
openclaw plugins install <path-or-spec>
|
||||
openclaw plugins inspect <id>
|
||||
openclaw plugins enable <id>
|
||||
openclaw plugins disable <id>
|
||||
openclaw plugins uninstall <id>
|
||||
openclaw plugins doctor
|
||||
openclaw plugins update <id>
|
||||
openclaw plugins update --all
|
||||
openclaw plugins marketplace list <marketplace>
|
||||
```
|
||||
|
||||
Bundled plugins ship with OpenClaw but start disabled. Use `plugins enable` to
|
||||
@@ -46,6 +48,8 @@ capabilities.
|
||||
```bash
|
||||
openclaw plugins install <path-or-spec>
|
||||
openclaw plugins install <npm-spec> --pin
|
||||
openclaw plugins install <plugin>@<marketplace>
|
||||
openclaw plugins install <plugin> --marketplace <marketplace>
|
||||
```
|
||||
|
||||
Security note: treat plugin installs like running code. Prefer pinned versions.
|
||||
@@ -65,6 +69,31 @@ name, use an explicit scoped spec (for example `@scope/diffs`).
|
||||
|
||||
Supported archives: `.zip`, `.tgz`, `.tar.gz`, `.tar`.
|
||||
|
||||
Claude marketplace installs are also supported.
|
||||
|
||||
Use `plugin@marketplace` shorthand when the marketplace name exists in Claude's
|
||||
local registry cache at `~/.claude/plugins/known_marketplaces.json`:
|
||||
|
||||
```bash
|
||||
openclaw plugins marketplace list <marketplace-name>
|
||||
openclaw plugins install <plugin-name>@<marketplace-name>
|
||||
```
|
||||
|
||||
Use `--marketplace` when you want to pass the marketplace source explicitly:
|
||||
|
||||
```bash
|
||||
openclaw plugins install <plugin-name> --marketplace <marketplace-name>
|
||||
openclaw plugins install <plugin-name> --marketplace <owner/repo>
|
||||
openclaw plugins install <plugin-name> --marketplace ./my-marketplace
|
||||
```
|
||||
|
||||
Marketplace sources can be:
|
||||
|
||||
- a Claude known-marketplace name from `~/.claude/plugins/known_marketplaces.json`
|
||||
- a local marketplace root or `marketplace.json` path
|
||||
- a GitHub repo shorthand such as `owner/repo`
|
||||
- a git URL
|
||||
|
||||
For local paths and archives, OpenClaw auto-detects:
|
||||
|
||||
- native OpenClaw plugins (`openclaw.plugin.json`)
|
||||
@@ -114,8 +143,34 @@ openclaw plugins update --all
|
||||
openclaw plugins update <id> --dry-run
|
||||
```
|
||||
|
||||
Updates only apply to plugins installed from npm (tracked in `plugins.installs`).
|
||||
Updates apply to tracked installs in `plugins.installs`, currently npm and
|
||||
marketplace installs.
|
||||
|
||||
When a stored integrity hash exists and the fetched artifact hash changes,
|
||||
OpenClaw prints a warning and asks for confirmation before proceeding. Use
|
||||
global `--yes` to bypass prompts in CI/non-interactive runs.
|
||||
|
||||
### Inspect
|
||||
|
||||
```bash
|
||||
openclaw plugins inspect <id>
|
||||
openclaw plugins inspect <id> --json
|
||||
```
|
||||
|
||||
Deep introspection for a single plugin. Shows identity, load status, source,
|
||||
registered capabilities, hooks, tools, commands, services, gateway methods,
|
||||
HTTP routes, policy flags, diagnostics, and install metadata.
|
||||
|
||||
Each plugin is classified by what it actually registers at runtime:
|
||||
|
||||
- **plain-capability** — one capability type (e.g. a provider-only plugin)
|
||||
- **hybrid-capability** — multiple capability types (e.g. text + speech + images)
|
||||
- **hook-only** — only hooks, no capabilities or surfaces
|
||||
- **non-capability** — tools/commands/services but no capabilities
|
||||
|
||||
See [Plugins](/tools/plugin#plugin-shapes) for more on the capability model.
|
||||
|
||||
The `--json` flag outputs a machine-readable report suitable for scripting and
|
||||
auditing.
|
||||
|
||||
`info` is an alias for `inspect`.
|
||||
|
||||
@@ -16,8 +16,15 @@ OpenClaw can run agents in isolated sandbox runtimes for security. The `sandbox`
|
||||
Today that usually means:
|
||||
|
||||
- Docker sandbox containers
|
||||
- SSH sandbox runtimes when `agents.defaults.sandbox.backend = "ssh"`
|
||||
- OpenShell sandbox runtimes when `agents.defaults.sandbox.backend = "openshell"`
|
||||
|
||||
For `ssh` and OpenShell `remote`, recreate matters more than with Docker:
|
||||
|
||||
- the remote workspace is canonical after the initial seed
|
||||
- `openclaw sandbox recreate` deletes that canonical remote workspace for the selected scope
|
||||
- next use seeds it again from the current local workspace
|
||||
|
||||
## Commands
|
||||
|
||||
### `openclaw sandbox explain`
|
||||
@@ -97,6 +104,22 @@ openclaw sandbox recreate --all
|
||||
openclaw sandbox recreate --all
|
||||
```
|
||||
|
||||
### After changing SSH target or SSH auth material
|
||||
|
||||
```bash
|
||||
# Edit config:
|
||||
# - agents.defaults.sandbox.backend
|
||||
# - agents.defaults.sandbox.ssh.target
|
||||
# - agents.defaults.sandbox.ssh.workspaceRoot
|
||||
# - agents.defaults.sandbox.ssh.identityFile / certificateFile / knownHostsFile
|
||||
# - agents.defaults.sandbox.ssh.identityData / certificateData / knownHostsData
|
||||
|
||||
openclaw sandbox recreate --all
|
||||
```
|
||||
|
||||
For the core `ssh` backend, recreate deletes the per-scope remote workspace root
|
||||
on the SSH target. The next run seeds it again from the local workspace.
|
||||
|
||||
### After changing OpenShell source, policy, or mode
|
||||
|
||||
```bash
|
||||
@@ -150,7 +173,7 @@ Sandbox settings live in `~/.openclaw/openclaw.json` under `agents.defaults.sand
|
||||
"defaults": {
|
||||
"sandbox": {
|
||||
"mode": "all", // off, non-main, all
|
||||
"backend": "docker", // docker, openshell
|
||||
"backend": "docker", // docker, ssh, openshell
|
||||
"scope": "agent", // session, agent, shared
|
||||
"docker": {
|
||||
"image": "openclaw-sandbox:bookworm-slim",
|
||||
|
||||
@@ -14,9 +14,9 @@ Use `openclaw secrets` to manage SecretRefs and keep the active runtime snapshot
|
||||
Command roles:
|
||||
|
||||
- `reload`: gateway RPC (`secrets.reload`) that re-resolves refs and swaps runtime snapshot only on full success (no config writes).
|
||||
- `audit`: read-only scan of configuration/auth/generated-model stores and legacy residues for plaintext, unresolved refs, and precedence drift.
|
||||
- `audit`: read-only scan of configuration/auth/generated-model stores and legacy residues for plaintext, unresolved refs, and precedence drift (exec refs are skipped unless `--allow-exec` is set).
|
||||
- `configure`: interactive planner for provider setup, target mapping, and preflight (TTY required).
|
||||
- `apply`: execute a saved plan (`--dry-run` for validation only), then scrub targeted plaintext residues.
|
||||
- `apply`: execute a saved plan (`--dry-run` for validation only; dry-run skips exec checks by default, and write mode rejects exec-containing plans unless `--allow-exec` is set), then scrub targeted plaintext residues.
|
||||
|
||||
Recommended operator loop:
|
||||
|
||||
@@ -29,6 +29,8 @@ openclaw secrets audit --check
|
||||
openclaw secrets reload
|
||||
```
|
||||
|
||||
If your plan includes `exec` SecretRefs/providers, pass `--allow-exec` on both dry-run and write apply commands.
|
||||
|
||||
Exit code note for CI/gates:
|
||||
|
||||
- `audit --check` returns `1` on findings.
|
||||
@@ -73,6 +75,7 @@ Header residue note:
|
||||
openclaw secrets audit
|
||||
openclaw secrets audit --check
|
||||
openclaw secrets audit --json
|
||||
openclaw secrets audit --allow-exec
|
||||
```
|
||||
|
||||
Exit behavior:
|
||||
@@ -83,6 +86,7 @@ Exit behavior:
|
||||
Report shape highlights:
|
||||
|
||||
- `status`: `clean | findings | unresolved`
|
||||
- `resolution`: `refsChecked`, `skippedExecRefs`, `resolvabilityComplete`
|
||||
- `summary`: `plaintextCount`, `unresolvedRefCount`, `shadowedRefCount`, `legacyResidueCount`
|
||||
- finding codes:
|
||||
- `PLAINTEXT_FOUND`
|
||||
@@ -115,6 +119,7 @@ Flags:
|
||||
- `--providers-only`: configure `secrets.providers` only, skip credential mapping.
|
||||
- `--skip-provider-setup`: skip provider setup and map credentials to existing providers.
|
||||
- `--agent <id>`: scope `auth-profiles.json` target discovery and writes to one agent store.
|
||||
- `--allow-exec`: allow exec SecretRef checks during preflight/apply (may execute provider commands).
|
||||
|
||||
Notes:
|
||||
|
||||
@@ -124,6 +129,7 @@ Notes:
|
||||
- `configure` supports creating new `auth-profiles.json` mappings directly in the picker flow.
|
||||
- Canonical supported surface: [SecretRef Credential Surface](/reference/secretref-credential-surface).
|
||||
- It performs preflight resolution before apply.
|
||||
- If preflight/apply includes exec refs, keep `--allow-exec` set for both steps.
|
||||
- Generated plans default to scrub options (`scrubEnv`, `scrubAuthProfilesForProviderTargets`, `scrubLegacyAuthJson` all enabled).
|
||||
- Apply path is one-way for scrubbed plaintext values.
|
||||
- Without `--apply`, CLI still prompts `Apply this plan now?` after preflight.
|
||||
@@ -141,10 +147,19 @@ Apply or preflight a plan generated previously:
|
||||
|
||||
```bash
|
||||
openclaw secrets apply --from /tmp/openclaw-secrets-plan.json
|
||||
openclaw secrets apply --from /tmp/openclaw-secrets-plan.json --allow-exec
|
||||
openclaw secrets apply --from /tmp/openclaw-secrets-plan.json --dry-run
|
||||
openclaw secrets apply --from /tmp/openclaw-secrets-plan.json --dry-run --allow-exec
|
||||
openclaw secrets apply --from /tmp/openclaw-secrets-plan.json --json
|
||||
```
|
||||
|
||||
Exec behavior:
|
||||
|
||||
- `--dry-run` validates preflight without writing files.
|
||||
- exec SecretRef checks are skipped by default in dry-run.
|
||||
- write mode rejects plans that contain exec SecretRefs/providers unless `--allow-exec` is set.
|
||||
- Use `--allow-exec` to opt in to exec provider checks/execution in either mode.
|
||||
|
||||
Plan contract details (allowed target paths, validation rules, and failure semantics):
|
||||
|
||||
- [Secrets Apply Plan Contract](/gateway/secrets-plan-contract)
|
||||
|
||||
@@ -30,7 +30,7 @@ This is for cooperative/shared inbox hardening. A single Gateway shared by mutua
|
||||
It also emits `security.trust_model.multi_user_heuristic` when config suggests likely shared-user ingress (for example open DM/group policy, configured group targets, or wildcard sender rules), and reminds you that OpenClaw is a personal-assistant trust model by default.
|
||||
For intentional shared-user setups, the audit guidance is to sandbox all sessions, keep filesystem access workspace-scoped, and keep personal/private identities or credentials off that runtime.
|
||||
It also warns when small models (`<=300B`) are used without sandboxing and with web/browser tools enabled.
|
||||
For webhook ingress, it warns when `hooks.defaultSessionKey` is unset, when request `sessionKey` overrides are enabled, and when overrides are enabled without `hooks.allowedSessionKeyPrefixes`.
|
||||
For webhook ingress, it warns when `hooks.token` reuses the Gateway token, when `hooks.defaultSessionKey` is unset, when `hooks.allowedAgentIds` is unrestricted, when request `sessionKey` overrides are enabled, and when overrides are enabled without `hooks.allowedSessionKeyPrefixes`.
|
||||
It also warns when sandbox Docker settings are configured while sandbox mode is off, when `gateway.nodes.denyCommands` uses ineffective pattern-like/unknown entries (exact node command-name matching only, not shell-text filtering), when `gateway.nodes.allowCommands` explicitly enables dangerous node commands, when global `tools.profile="minimal"` is overridden by agent tool profiles, when open groups expose runtime/filesystem tools without sandbox/workspace guards, and when installed extension plugin tools may be reachable under permissive tool policy.
|
||||
It also flags `gateway.allowRealIpFallback=true` (header-spoofing risk if proxies are misconfigured) and `discovery.mdns.mode="full"` (metadata leakage via mDNS TXT records).
|
||||
It also warns when sandbox browser uses Docker `bridge` network without `sandbox.browser.cdpSourceRange`.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
summary: "CLI reference for `openclaw setup` (initialize config + workspace)"
|
||||
read_when:
|
||||
- You’re doing first-run setup without the full onboarding wizard
|
||||
- You’re doing first-run setup without full CLI onboarding
|
||||
- You want to set the default workspace path
|
||||
title: "setup"
|
||||
---
|
||||
@@ -13,7 +13,7 @@ Initialize `~/.openclaw/openclaw.json` and the agent workspace.
|
||||
Related:
|
||||
|
||||
- Getting started: [Getting started](/start/getting-started)
|
||||
- Wizard: [Onboarding](/start/onboarding)
|
||||
- CLI onboarding: [Onboarding (CLI)](/start/wizard)
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -22,7 +22,7 @@ openclaw setup
|
||||
openclaw setup --workspace ~/.openclaw/workspace
|
||||
```
|
||||
|
||||
To run the wizard via setup:
|
||||
To run onboarding via setup:
|
||||
|
||||
```bash
|
||||
openclaw setup --wizard
|
||||
|
||||
@@ -27,3 +27,4 @@ Notes:
|
||||
- Read-only status surfaces (`status`, `status --json`, `status --all`) resolve supported SecretRefs for their targeted config paths when possible.
|
||||
- If a supported channel SecretRef is configured but unavailable in the current command path, status stays read-only and reports degraded output instead of crashing. Human output shows warnings such as “configured token unavailable in this command path”, and JSON output includes `secretDiagnostics`.
|
||||
- When command-local SecretRef resolution succeeds, status prefers the resolved snapshot and clears transient “secret unavailable” channel markers from the final output.
|
||||
- `status --all` includes a Secrets overview row and a diagnosis section that summarizes secret diagnostics (truncated for readability) without stopping report generation.
|
||||
|
||||
@@ -97,6 +97,25 @@ compaction and can run alongside it.
|
||||
|
||||
See [OpenAI provider](/providers/openai) for model params and overrides.
|
||||
|
||||
## Custom context engines
|
||||
|
||||
Compaction behavior is owned by the active
|
||||
[context engine](/concepts/context-engine). The legacy engine uses the built-in
|
||||
summarization described above. Plugin engines (selected via
|
||||
`plugins.slots.contextEngine`) can implement any compaction strategy — DAG
|
||||
summaries, vector retrieval, incremental condensation, etc.
|
||||
|
||||
When a plugin engine sets `ownsCompaction: true`, OpenClaw delegates all
|
||||
compaction decisions to the engine and does not run built-in auto-compaction.
|
||||
|
||||
When `ownsCompaction` is `false` or unset, OpenClaw may still use Pi's
|
||||
built-in in-attempt auto-compaction, but the active engine's `compact()` method
|
||||
still handles `/compact` and overflow recovery. There is no automatic fallback
|
||||
to the legacy engine's compaction path.
|
||||
|
||||
If you are building a non-owning context engine, implement `compact()` by
|
||||
calling `delegateCompactionToRuntime(...)` from `openclaw/plugin-sdk/core`.
|
||||
|
||||
## Tips
|
||||
|
||||
- Use `/compact` when sessions feel stale or context is bloated.
|
||||
|
||||
268
docs/concepts/context-engine.md
Normal file
268
docs/concepts/context-engine.md
Normal file
@@ -0,0 +1,268 @@
|
||||
---
|
||||
summary: "Context engine: pluggable context assembly, compaction, and subagent lifecycle"
|
||||
read_when:
|
||||
- You want to understand how OpenClaw assembles model context
|
||||
- You are switching between the legacy engine and a plugin engine
|
||||
- You are building a context engine plugin
|
||||
title: "Context Engine"
|
||||
---
|
||||
|
||||
# Context Engine
|
||||
|
||||
A **context engine** controls how OpenClaw builds model context for each run.
|
||||
It decides which messages to include, how to summarize older history, and how
|
||||
to manage context across subagent boundaries.
|
||||
|
||||
OpenClaw ships with a built-in `legacy` engine. Plugins can register
|
||||
alternative engines that replace the active context-engine lifecycle.
|
||||
|
||||
## Quick start
|
||||
|
||||
Check which engine is active:
|
||||
|
||||
```bash
|
||||
openclaw doctor
|
||||
# or inspect config directly:
|
||||
cat ~/.openclaw/openclaw.json | jq '.plugins.slots.contextEngine'
|
||||
```
|
||||
|
||||
### Installing a context engine plugin
|
||||
|
||||
Context engine plugins are installed like any other OpenClaw plugin. Install
|
||||
first, then select the engine in the slot:
|
||||
|
||||
```bash
|
||||
# Install from npm
|
||||
openclaw plugins install @martian-engineering/lossless-claw
|
||||
|
||||
# Or install from a local path (for development)
|
||||
openclaw plugins install -l ./my-context-engine
|
||||
```
|
||||
|
||||
Then enable the plugin and select it as the active engine in your config:
|
||||
|
||||
```json5
|
||||
// openclaw.json
|
||||
{
|
||||
plugins: {
|
||||
slots: {
|
||||
contextEngine: "lossless-claw", // must match the plugin's registered engine id
|
||||
},
|
||||
entries: {
|
||||
"lossless-claw": {
|
||||
enabled: true,
|
||||
// Plugin-specific config goes here (see the plugin's docs)
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Restart the gateway after installing and configuring.
|
||||
|
||||
To switch back to the built-in engine, set `contextEngine` to `"legacy"` (or
|
||||
remove the key entirely — `"legacy"` is the default).
|
||||
|
||||
## How it works
|
||||
|
||||
Every time OpenClaw runs a model prompt, the context engine participates at
|
||||
four lifecycle points:
|
||||
|
||||
1. **Ingest** — called when a new message is added to the session. The engine
|
||||
can store or index the message in its own data store.
|
||||
2. **Assemble** — called before each model run. The engine returns an ordered
|
||||
set of messages (and an optional `systemPromptAddition`) that fit within
|
||||
the token budget.
|
||||
3. **Compact** — called when the context window is full, or when the user runs
|
||||
`/compact`. The engine summarizes older history to free space.
|
||||
4. **After turn** — called after a run completes. The engine can persist state,
|
||||
trigger background compaction, or update indexes.
|
||||
|
||||
### Subagent lifecycle (optional)
|
||||
|
||||
OpenClaw currently calls one subagent lifecycle hook:
|
||||
|
||||
- **onSubagentEnded** — clean up when a subagent session completes or is swept.
|
||||
|
||||
The `prepareSubagentSpawn` hook is part of the interface for future use, but
|
||||
the runtime does not invoke it yet.
|
||||
|
||||
### System prompt addition
|
||||
|
||||
The `assemble` method can return a `systemPromptAddition` string. OpenClaw
|
||||
prepends this to the system prompt for the run. This lets engines inject
|
||||
dynamic recall guidance, retrieval instructions, or context-aware hints
|
||||
without requiring static workspace files.
|
||||
|
||||
## The legacy engine
|
||||
|
||||
The built-in `legacy` engine preserves OpenClaw's original behavior:
|
||||
|
||||
- **Ingest**: no-op (the session manager handles message persistence directly).
|
||||
- **Assemble**: pass-through (the existing sanitize → validate → limit pipeline
|
||||
in the runtime handles context assembly).
|
||||
- **Compact**: delegates to the built-in summarization compaction, which creates
|
||||
a single summary of older messages and keeps recent messages intact.
|
||||
- **After turn**: no-op.
|
||||
|
||||
The legacy engine does not register tools or provide a `systemPromptAddition`.
|
||||
|
||||
When no `plugins.slots.contextEngine` is set (or it's set to `"legacy"`), this
|
||||
engine is used automatically.
|
||||
|
||||
## Plugin engines
|
||||
|
||||
A plugin can register a context engine using the plugin API:
|
||||
|
||||
```ts
|
||||
export default function register(api) {
|
||||
api.registerContextEngine("my-engine", () => ({
|
||||
info: {
|
||||
id: "my-engine",
|
||||
name: "My Context Engine",
|
||||
ownsCompaction: true,
|
||||
},
|
||||
|
||||
async ingest({ sessionId, message, isHeartbeat }) {
|
||||
// Store the message in your data store
|
||||
return { ingested: true };
|
||||
},
|
||||
|
||||
async assemble({ sessionId, messages, tokenBudget }) {
|
||||
// Return messages that fit the budget
|
||||
return {
|
||||
messages: buildContext(messages, tokenBudget),
|
||||
estimatedTokens: countTokens(messages),
|
||||
systemPromptAddition: "Use lcm_grep to search history...",
|
||||
};
|
||||
},
|
||||
|
||||
async compact({ sessionId, force }) {
|
||||
// Summarize older context
|
||||
return { ok: true, compacted: true };
|
||||
},
|
||||
}));
|
||||
}
|
||||
```
|
||||
|
||||
Then enable it in config:
|
||||
|
||||
```json5
|
||||
{
|
||||
plugins: {
|
||||
slots: {
|
||||
contextEngine: "my-engine",
|
||||
},
|
||||
entries: {
|
||||
"my-engine": {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### The ContextEngine interface
|
||||
|
||||
Required members:
|
||||
|
||||
| Member | Kind | Purpose |
|
||||
| ------------------ | -------- | -------------------------------------------------------- |
|
||||
| `info` | Property | Engine id, name, version, and whether it owns compaction |
|
||||
| `ingest(params)` | Method | Store a single message |
|
||||
| `assemble(params)` | Method | Build context for a model run (returns `AssembleResult`) |
|
||||
| `compact(params)` | Method | Summarize/reduce context |
|
||||
|
||||
`assemble` returns an `AssembleResult` with:
|
||||
|
||||
- `messages` — the ordered messages to send to the model.
|
||||
- `estimatedTokens` (required, `number`) — the engine's estimate of total
|
||||
tokens in the assembled context. OpenClaw uses this for compaction threshold
|
||||
decisions and diagnostic reporting.
|
||||
- `systemPromptAddition` (optional, `string`) — prepended to the system prompt.
|
||||
|
||||
Optional members:
|
||||
|
||||
| Member | Kind | Purpose |
|
||||
| ------------------------------ | ------ | --------------------------------------------------------------------------------------------------------------- |
|
||||
| `bootstrap(params)` | Method | Initialize engine state for a session. Called once when the engine first sees a session (e.g., import history). |
|
||||
| `ingestBatch(params)` | Method | Ingest a completed turn as a batch. Called after a run completes, with all messages from that turn at once. |
|
||||
| `afterTurn(params)` | Method | Post-run lifecycle work (persist state, trigger background compaction). |
|
||||
| `prepareSubagentSpawn(params)` | Method | Set up shared state for a child session. |
|
||||
| `onSubagentEnded(params)` | Method | Clean up after a subagent ends. |
|
||||
| `dispose()` | Method | Release resources. Called during gateway shutdown or plugin reload — not per-session. |
|
||||
|
||||
### ownsCompaction
|
||||
|
||||
`ownsCompaction` controls whether Pi's built-in in-attempt auto-compaction stays
|
||||
enabled for the run:
|
||||
|
||||
- `true` — the engine owns compaction behavior. OpenClaw disables Pi's built-in
|
||||
auto-compaction for that run, and the engine's `compact()` implementation is
|
||||
responsible for `/compact`, overflow recovery compaction, and any proactive
|
||||
compaction it wants to do in `afterTurn()`.
|
||||
- `false` or unset — Pi's built-in auto-compaction may still run during prompt
|
||||
execution, but the active engine's `compact()` method is still called for
|
||||
`/compact` and overflow recovery.
|
||||
|
||||
`ownsCompaction: false` does **not** mean OpenClaw automatically falls back to
|
||||
the legacy engine's compaction path.
|
||||
|
||||
That means there are two valid plugin patterns:
|
||||
|
||||
- **Owning mode** — implement your own compaction algorithm and set
|
||||
`ownsCompaction: true`.
|
||||
- **Delegating mode** — set `ownsCompaction: false` and have `compact()` call
|
||||
`delegateCompactionToRuntime(...)` from `openclaw/plugin-sdk/core` to use
|
||||
OpenClaw's built-in compaction behavior.
|
||||
|
||||
A no-op `compact()` is unsafe for an active non-owning engine because it
|
||||
disables the normal `/compact` and overflow-recovery compaction path for that
|
||||
engine slot.
|
||||
|
||||
## Configuration reference
|
||||
|
||||
```json5
|
||||
{
|
||||
plugins: {
|
||||
slots: {
|
||||
// Select the active context engine. Default: "legacy".
|
||||
// Set to a plugin id to use a plugin engine.
|
||||
contextEngine: "legacy",
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
The slot is exclusive at run time — only one registered context engine is
|
||||
resolved for a given run or compaction operation. Other enabled
|
||||
`kind: "context-engine"` plugins can still load and run their registration
|
||||
code; `plugins.slots.contextEngine` only selects which registered engine id
|
||||
OpenClaw resolves when it needs a context engine.
|
||||
|
||||
## Relationship to compaction and memory
|
||||
|
||||
- **Compaction** is one responsibility of the context engine. The legacy engine
|
||||
delegates to OpenClaw's built-in summarization. Plugin engines can implement
|
||||
any compaction strategy (DAG summaries, vector retrieval, etc.).
|
||||
- **Memory plugins** (`plugins.slots.memory`) are separate from context engines.
|
||||
Memory plugins provide search/retrieval; context engines control what the
|
||||
model sees. They can work together — a context engine might use memory
|
||||
plugin data during assembly.
|
||||
- **Session pruning** (trimming old tool results in-memory) still runs
|
||||
regardless of which context engine is active.
|
||||
|
||||
## Tips
|
||||
|
||||
- Use `openclaw doctor` to verify your engine is loading correctly.
|
||||
- If switching engines, existing sessions continue with their current history.
|
||||
The new engine takes over for future runs.
|
||||
- Engine errors are logged and surfaced in diagnostics. If a plugin engine
|
||||
fails to register or the selected engine id cannot be resolved, OpenClaw
|
||||
does not fall back automatically; runs fail until you fix the plugin or
|
||||
switch `plugins.slots.contextEngine` back to `"legacy"`.
|
||||
- For development, use `openclaw plugins install -l ./my-engine` to link a
|
||||
local plugin directory without copying.
|
||||
|
||||
See also: [Compaction](/concepts/compaction), [Context](/concepts/context),
|
||||
[Plugins](/tools/plugin), [Plugin manifest](/plugins/manifest).
|
||||
@@ -116,7 +116,7 @@ Large files are truncated per-file using `agents.defaults.bootstrapMaxChars` (de
|
||||
|
||||
When truncation occurs, the runtime can inject an in-prompt warning block under Project Context. Configure this with `agents.defaults.bootstrapPromptTruncationWarning` (`off`, `once`, `always`; default `once`).
|
||||
|
||||
## Skills: what’s injected vs loaded on-demand
|
||||
## Skills: injected vs loaded on-demand
|
||||
|
||||
The system prompt includes a compact **skills list** (name + description + location). This list has real overhead.
|
||||
|
||||
@@ -131,7 +131,7 @@ Tools affect context in two ways:
|
||||
|
||||
`/context detail` breaks down the biggest tool schemas so you can see what dominates.
|
||||
|
||||
## Commands, directives, and “inline shortcuts”
|
||||
## Commands, directives, and "inline shortcuts"
|
||||
|
||||
Slash commands are handled by the Gateway. There are a few different behaviors:
|
||||
|
||||
@@ -157,7 +157,10 @@ By default, OpenClaw uses the built-in `legacy` context engine for assembly and
|
||||
compaction. If you install a plugin that provides `kind: "context-engine"` and
|
||||
select it with `plugins.slots.contextEngine`, OpenClaw delegates context
|
||||
assembly, `/compact`, and related subagent context lifecycle hooks to that
|
||||
engine instead.
|
||||
engine instead. `ownsCompaction: false` does not auto-fallback to the legacy
|
||||
engine; the active engine must still implement `compact()` correctly. See
|
||||
[Context Engine](/concepts/context-engine) for the full
|
||||
pluggable interface, lifecycle hooks, and configuration.
|
||||
|
||||
## What `/context` actually reports
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ they are tried first, but OpenClaw may rotate to another profile on rate limits/
|
||||
User‑pinned profiles stay locked to that profile; if it fails and model fallbacks
|
||||
are configured, OpenClaw moves to the next model instead of switching profiles.
|
||||
|
||||
### Why OAuth can “look lost”
|
||||
### Why OAuth can "look lost"
|
||||
|
||||
If you have both an OAuth profile and an API key profile for the same provider, round‑robin can switch between them across messages unless pinned. To force a single profile:
|
||||
|
||||
|
||||
@@ -20,13 +20,22 @@ For model selection rules, see [/concepts/models](/concepts/models).
|
||||
OpenClaw merges that output into `models.providers` before writing
|
||||
`models.json`.
|
||||
- Provider manifests can declare `providerAuthEnvVars` so generic env-based
|
||||
auth probes do not need to load plugin runtime.
|
||||
auth probes do not need to load plugin runtime. The remaining core env-var
|
||||
map is now just for non-plugin/core providers and a few generic-precedence
|
||||
cases such as Anthropic API-key-first onboarding.
|
||||
- Provider plugins can also own provider runtime behavior via
|
||||
`resolveDynamicModel`, `prepareDynamicModel`, `normalizeResolvedModel`,
|
||||
`capabilities`, `prepareExtraParams`, `wrapStreamFn`,
|
||||
`capabilities`, `prepareExtraParams`, `wrapStreamFn`, `formatApiKey`,
|
||||
`refreshOAuth`, `buildAuthDoctorHint`,
|
||||
`isCacheTtlEligible`, `buildMissingAuthMessage`,
|
||||
`suppressBuiltInModel`, `augmentModelCatalog`, `prepareRuntimeAuth`,
|
||||
`resolveUsageAuth`, and `fetchUsageSnapshot`.
|
||||
`suppressBuiltInModel`, `augmentModelCatalog`, `isBinaryThinking`,
|
||||
`supportsXHighThinking`, `resolveDefaultThinkingLevel`,
|
||||
`isModernModelRef`, `prepareRuntimeAuth`, `resolveUsageAuth`, and
|
||||
`fetchUsageSnapshot`.
|
||||
- Note: provider runtime `capabilities` is shared runner metadata (provider
|
||||
family, transcript/tooling quirks, transport/cache hints). It is not the
|
||||
same as the [public capability model](/tools/plugin#public-capability-model)
|
||||
which describes what a plugin registers (text inference, speech, etc.).
|
||||
|
||||
## Plugin-owned provider behavior
|
||||
|
||||
@@ -35,6 +44,10 @@ the generic inference loop.
|
||||
|
||||
Typical split:
|
||||
|
||||
- `auth[].run` / `auth[].runNonInteractive`: provider owns onboarding/login
|
||||
flows for `openclaw onboard`, `openclaw models auth`, and headless setup
|
||||
- `wizard.setup` / `wizard.modelPicker`: provider owns auth-choice labels,
|
||||
legacy aliases, onboarding allowlist hints, and setup entries in onboarding/model pickers
|
||||
- `catalog`: provider appears in `models.providers`
|
||||
- `resolveDynamicModel`: provider accepts model ids not present in the local
|
||||
static catalog yet
|
||||
@@ -44,6 +57,12 @@ Typical split:
|
||||
- `capabilities`: provider publishes transcript/tooling/provider-family quirks
|
||||
- `prepareExtraParams`: provider defaults or normalizes per-model request params
|
||||
- `wrapStreamFn`: provider applies request headers/body/model compat wrappers
|
||||
- `formatApiKey`: provider formats stored auth profiles into the runtime
|
||||
`apiKey` string expected by the transport
|
||||
- `refreshOAuth`: provider owns OAuth refresh when the shared `pi-ai`
|
||||
refreshers are not enough
|
||||
- `buildAuthDoctorHint`: provider appends repair guidance when OAuth refresh
|
||||
fails
|
||||
- `isCacheTtlEligible`: provider decides which upstream model ids support prompt-cache TTL
|
||||
- `buildMissingAuthMessage`: provider replaces the generic auth-store error
|
||||
with a provider-specific recovery hint
|
||||
@@ -51,6 +70,11 @@ Typical split:
|
||||
vendor-owned error for direct resolution failures
|
||||
- `augmentModelCatalog`: provider appends synthetic/final catalog rows after
|
||||
discovery and config merging
|
||||
- `isBinaryThinking`: provider owns binary on/off thinking UX
|
||||
- `supportsXHighThinking`: provider opts selected models into `xhigh`
|
||||
- `resolveDefaultThinkingLevel`: provider owns default `/think` policy for a
|
||||
model family
|
||||
- `isModernModelRef`: provider owns live/smoke preferred-model matching
|
||||
- `prepareRuntimeAuth`: provider turns a configured credential into a short
|
||||
lived runtime token
|
||||
- `resolveUsageAuth`: provider resolves usage/quota credentials for `/usage`
|
||||
@@ -60,27 +84,31 @@ Typical split:
|
||||
|
||||
Current bundled examples:
|
||||
|
||||
- `anthropic`: Claude 4.6 forward-compat fallback, usage endpoint fetching,
|
||||
and cache-TTL/provider-family metadata
|
||||
- `anthropic`: Claude 4.6 forward-compat fallback, auth repair hints, usage
|
||||
endpoint fetching, and cache-TTL/provider-family metadata
|
||||
- `openrouter`: pass-through model ids, request wrappers, provider capability
|
||||
hints, and cache-TTL policy
|
||||
- `github-copilot`: forward-compat model fallback, Claude-thinking transcript
|
||||
hints, runtime token exchange, and usage endpoint fetching
|
||||
- `github-copilot`: onboarding/device login, forward-compat model fallback,
|
||||
Claude-thinking transcript hints, runtime token exchange, and usage endpoint
|
||||
fetching
|
||||
- `openai`: GPT-5.4 forward-compat fallback, direct OpenAI transport
|
||||
normalization, Codex-aware missing-auth hints, Spark suppression, synthetic
|
||||
OpenAI/Codex catalog rows, and provider-family metadata
|
||||
- `google-gemini-cli`: Gemini 3.1 forward-compat fallback plus usage-token
|
||||
parsing and quota endpoint fetching for usage surfaces
|
||||
OpenAI/Codex catalog rows, thinking/live-model policy, and
|
||||
provider-family metadata
|
||||
- `google` and `google-gemini-cli`: Gemini 3.1 forward-compat fallback and
|
||||
modern-model matching; Gemini CLI OAuth also owns auth-profile token
|
||||
formatting, usage-token parsing, and quota endpoint fetching for usage
|
||||
surfaces
|
||||
- `moonshot`: shared transport, plugin-owned thinking payload normalization
|
||||
- `kilocode`: shared transport, plugin-owned request headers, reasoning payload
|
||||
normalization, Gemini transcript hints, and cache-TTL policy
|
||||
- `zai`: GLM-5 forward-compat fallback, `tool_stream` defaults, cache-TTL
|
||||
policy, and usage auth + quota fetching
|
||||
policy, binary-thinking/live-model policy, and usage auth + quota fetching
|
||||
- `mistral`, `opencode`, and `opencode-go`: plugin-owned capability metadata
|
||||
- `byteplus`, `cloudflare-ai-gateway`, `huggingface`, `kimi-coding`,
|
||||
`minimax-portal`, `modelstudio`, `nvidia`, `qianfan`, `qwen-portal`,
|
||||
`synthetic`, `together`, `venice`, `vercel-ai-gateway`, and `volcengine`:
|
||||
plugin-owned catalogs only
|
||||
`modelstudio`, `nvidia`, `qianfan`, `synthetic`, `together`, `venice`,
|
||||
`vercel-ai-gateway`, and `volcengine`: plugin-owned catalogs only
|
||||
- `qwen-portal`: plugin-owned catalog, OAuth login, and OAuth refresh
|
||||
- `minimax` and `xiaomi`: plugin-owned catalogs plus usage auth/snapshot logic
|
||||
|
||||
The bundled `openai` plugin now owns both provider ids: `openai` and
|
||||
|
||||
@@ -26,6 +26,7 @@ Related:
|
||||
|
||||
- `agents.defaults.models` is the allowlist/catalog of models OpenClaw can use (plus aliases).
|
||||
- `agents.defaults.imageModel` is used **only when** the primary model can’t accept images.
|
||||
- `agents.defaults.imageGenerationModel` is used by the shared image-generation capability. If omitted, `image_generate` can still infer a provider default from compatible auth-backed image-generation plugins.
|
||||
- Per-agent defaults can override `agents.defaults.model` via `agents.list[].model` plus bindings (see [/concepts/multi-agent](/concepts/multi-agent)).
|
||||
|
||||
## Quick model policy
|
||||
@@ -34,9 +35,9 @@ Related:
|
||||
- Use fallbacks for cost/latency-sensitive tasks and lower-stakes chat.
|
||||
- For tool-enabled agents or untrusted inputs, avoid older/weaker model tiers.
|
||||
|
||||
## Setup wizard (recommended)
|
||||
## Onboarding (recommended)
|
||||
|
||||
If you don’t want to hand-edit config, run the onboarding wizard:
|
||||
If you don’t want to hand-edit config, run onboarding:
|
||||
|
||||
```bash
|
||||
openclaw onboard
|
||||
@@ -49,6 +50,7 @@ subscription** (OAuth) and **Anthropic** (API key or `claude setup-token`).
|
||||
|
||||
- `agents.defaults.model.primary` and `agents.defaults.model.fallbacks`
|
||||
- `agents.defaults.imageModel.primary` and `agents.defaults.imageModel.fallbacks`
|
||||
- `agents.defaults.imageGenerationModel.primary` and `agents.defaults.imageGenerationModel.fallbacks`
|
||||
- `agents.defaults.models` (allowlist + aliases + provider params)
|
||||
- `models.providers` (custom providers written into `models.json`)
|
||||
|
||||
@@ -58,7 +60,7 @@ to `zai/*`.
|
||||
Provider configuration examples (including OpenCode) live in
|
||||
[/gateway/configuration](/gateway/configuration#opencode).
|
||||
|
||||
## “Model is not allowed” (and why replies stop)
|
||||
## "Model is not allowed" (and why replies stop)
|
||||
|
||||
If `agents.defaults.models` is set, it becomes the **allowlist** for `/model` and for
|
||||
session overrides. When a user selects a model that isn’t in that allowlist,
|
||||
|
||||
@@ -9,7 +9,7 @@ status: active
|
||||
|
||||
Goal: multiple _isolated_ agents (separate workspace + `agentDir` + sessions), plus multiple channel accounts (e.g. two WhatsApps) in one running Gateway. Inbound is routed to an agent via bindings.
|
||||
|
||||
## What is “one agent”?
|
||||
## What is "one agent"?
|
||||
|
||||
An **agent** is a fully scoped brain with its own:
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ even before any clients connect.
|
||||
Every WS client begins with a `connect` request. On successful handshake the
|
||||
Gateway upserts a presence entry for that connection.
|
||||
|
||||
#### Why one‑off CLI commands don’t show up
|
||||
#### Why one-off CLI commands do not show up
|
||||
|
||||
The CLI often connects for short, one‑off commands. To avoid spamming the
|
||||
Instances list, `client.mode === "cli"` is **not** turned into a presence entry.
|
||||
|
||||
@@ -90,7 +90,7 @@ more natural.
|
||||
- Modes: `off` (default), `natural` (800–2500ms), `custom` (`minMs`/`maxMs`).
|
||||
- Applies only to **block replies**, not final replies or tool summaries.
|
||||
|
||||
## “Stream chunks or everything”
|
||||
## "Stream chunks or everything"
|
||||
|
||||
This maps to:
|
||||
|
||||
|
||||
@@ -185,7 +185,7 @@ ws.on("message", (data) => {
|
||||
});
|
||||
```
|
||||
|
||||
## Worked example: add a method end‑to‑end
|
||||
## Worked example: add a method end-to-end
|
||||
|
||||
Example: add a new `system.echo` request that returns `{ ok: true, text }`.
|
||||
|
||||
|
||||
@@ -1,534 +0,0 @@
|
||||
# Kilo Gateway Provider Integration Design
|
||||
|
||||
## Overview
|
||||
|
||||
This document outlines the design for integrating "Kilo Gateway" as a first-class provider in OpenClaw, modeled after the existing OpenRouter implementation. Kilo Gateway uses an OpenAI-compatible completions API with a different base URL.
|
||||
|
||||
## Design Decisions
|
||||
|
||||
### 1. Provider Naming
|
||||
|
||||
**Recommendation: `kilocode`**
|
||||
|
||||
Rationale:
|
||||
|
||||
- Matches the user config example provided (`kilocode` provider key)
|
||||
- Consistent with existing provider naming patterns (e.g., `openrouter`, `opencode`, `moonshot`)
|
||||
- Short and memorable
|
||||
- Avoids confusion with generic "kilo" or "gateway" terms
|
||||
|
||||
Alternative considered: `kilo-gateway` - rejected because hyphenated names are less common in the codebase and `kilocode` is more concise.
|
||||
|
||||
### 2. Default Model Reference
|
||||
|
||||
**Recommendation: `kilocode/anthropic/claude-opus-4.6`**
|
||||
|
||||
Rationale:
|
||||
|
||||
- Based on user config example
|
||||
- Claude Opus 4.5 is a capable default model
|
||||
- Explicit model selection avoids reliance on auto-routing
|
||||
|
||||
### 3. Base URL Configuration
|
||||
|
||||
**Recommendation: Hardcoded default with config override**
|
||||
|
||||
- **Default Base URL:** `https://api.kilo.ai/api/gateway/`
|
||||
- **Configurable:** Yes, via `models.providers.kilocode.baseUrl`
|
||||
|
||||
This matches the pattern used by other providers like Moonshot, Venice, and Synthetic.
|
||||
|
||||
### 4. Model Scanning
|
||||
|
||||
**Recommendation: No dedicated model scanning endpoint initially**
|
||||
|
||||
Rationale:
|
||||
|
||||
- Kilo Gateway proxies to OpenRouter, so models are dynamic
|
||||
- Users can manually configure models in their config
|
||||
- If Kilo Gateway exposes a `/models` endpoint in the future, scanning can be added
|
||||
|
||||
### 5. Special Handling
|
||||
|
||||
**Recommendation: Inherit OpenRouter behavior for Anthropic models**
|
||||
|
||||
Since Kilo Gateway proxies to OpenRouter, the same special handling should apply:
|
||||
|
||||
- Cache TTL eligibility for `anthropic/*` models
|
||||
- Extra params (cacheControlTtl) for `anthropic/*` models
|
||||
- Transcript policy follows OpenRouter patterns
|
||||
|
||||
## Files to Modify
|
||||
|
||||
### Core Credential Management
|
||||
|
||||
#### 1. `src/commands/onboard-auth.credentials.ts`
|
||||
|
||||
Add:
|
||||
|
||||
```typescript
|
||||
export const KILOCODE_DEFAULT_MODEL_REF = "kilocode/anthropic/claude-opus-4.6";
|
||||
|
||||
export async function setKilocodeApiKey(key: string, agentDir?: string) {
|
||||
upsertAuthProfile({
|
||||
profileId: "kilocode:default",
|
||||
credential: {
|
||||
type: "api_key",
|
||||
provider: "kilocode",
|
||||
key,
|
||||
},
|
||||
agentDir: resolveAuthAgentDir(agentDir),
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. `src/agents/model-auth.ts`
|
||||
|
||||
Add to `envMap` in `resolveEnvApiKey()`:
|
||||
|
||||
```typescript
|
||||
const envMap: Record<string, string> = {
|
||||
// ... existing entries
|
||||
kilocode: "KILOCODE_API_KEY",
|
||||
};
|
||||
```
|
||||
|
||||
#### 3. `src/config/io.ts`
|
||||
|
||||
Add to `SHELL_ENV_EXPECTED_KEYS`:
|
||||
|
||||
```typescript
|
||||
const SHELL_ENV_EXPECTED_KEYS = [
|
||||
// ... existing entries
|
||||
"KILOCODE_API_KEY",
|
||||
];
|
||||
```
|
||||
|
||||
### Config Application
|
||||
|
||||
#### 4. `src/commands/onboard-auth.config-core.ts`
|
||||
|
||||
Add new functions:
|
||||
|
||||
```typescript
|
||||
export const KILOCODE_BASE_URL = "https://api.kilo.ai/api/gateway/";
|
||||
|
||||
export function applyKilocodeProviderConfig(cfg: OpenClawConfig): OpenClawConfig {
|
||||
const models = { ...cfg.agents?.defaults?.models };
|
||||
models[KILOCODE_DEFAULT_MODEL_REF] = {
|
||||
...models[KILOCODE_DEFAULT_MODEL_REF],
|
||||
alias: models[KILOCODE_DEFAULT_MODEL_REF]?.alias ?? "Kilo Gateway",
|
||||
};
|
||||
|
||||
const providers = { ...cfg.models?.providers };
|
||||
const existingProvider = providers.kilocode;
|
||||
const { apiKey: existingApiKey, ...existingProviderRest } = (existingProvider ?? {}) as Record<
|
||||
string,
|
||||
unknown
|
||||
> as { apiKey?: string };
|
||||
const resolvedApiKey = typeof existingApiKey === "string" ? existingApiKey : undefined;
|
||||
const normalizedApiKey = resolvedApiKey?.trim();
|
||||
|
||||
providers.kilocode = {
|
||||
...existingProviderRest,
|
||||
baseUrl: KILOCODE_BASE_URL,
|
||||
api: "openai-completions",
|
||||
...(normalizedApiKey ? { apiKey: normalizedApiKey } : {}),
|
||||
};
|
||||
|
||||
return {
|
||||
...cfg,
|
||||
agents: {
|
||||
...cfg.agents,
|
||||
defaults: {
|
||||
...cfg.agents?.defaults,
|
||||
models,
|
||||
},
|
||||
},
|
||||
models: {
|
||||
mode: cfg.models?.mode ?? "merge",
|
||||
providers,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function applyKilocodeConfig(cfg: OpenClawConfig): OpenClawConfig {
|
||||
const next = applyKilocodeProviderConfig(cfg);
|
||||
const existingModel = next.agents?.defaults?.model;
|
||||
return {
|
||||
...next,
|
||||
agents: {
|
||||
...next.agents,
|
||||
defaults: {
|
||||
...next.agents?.defaults,
|
||||
model: {
|
||||
...(existingModel && "fallbacks" in (existingModel as Record<string, unknown>)
|
||||
? {
|
||||
fallbacks: (existingModel as { fallbacks?: string[] }).fallbacks,
|
||||
}
|
||||
: undefined),
|
||||
primary: KILOCODE_DEFAULT_MODEL_REF,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Auth Choice System
|
||||
|
||||
#### 5. `src/commands/onboard-types.ts`
|
||||
|
||||
Add to `AuthChoice` type:
|
||||
|
||||
```typescript
|
||||
export type AuthChoice =
|
||||
// ... existing choices
|
||||
"kilocode-api-key";
|
||||
// ...
|
||||
```
|
||||
|
||||
Add to `OnboardOptions`:
|
||||
|
||||
```typescript
|
||||
export type OnboardOptions = {
|
||||
// ... existing options
|
||||
kilocodeApiKey?: string;
|
||||
// ...
|
||||
};
|
||||
```
|
||||
|
||||
#### 6. `src/commands/auth-choice-options.ts`
|
||||
|
||||
Add to `AuthChoiceGroupId`:
|
||||
|
||||
```typescript
|
||||
export type AuthChoiceGroupId =
|
||||
// ... existing groups
|
||||
"kilocode";
|
||||
// ...
|
||||
```
|
||||
|
||||
Add to `AUTH_CHOICE_GROUP_DEFS`:
|
||||
|
||||
```typescript
|
||||
{
|
||||
value: "kilocode",
|
||||
label: "Kilo Gateway",
|
||||
hint: "API key (OpenRouter-compatible)",
|
||||
choices: ["kilocode-api-key"],
|
||||
},
|
||||
```
|
||||
|
||||
Add to `buildAuthChoiceOptions()`:
|
||||
|
||||
```typescript
|
||||
options.push({
|
||||
value: "kilocode-api-key",
|
||||
label: "Kilo Gateway API key",
|
||||
hint: "OpenRouter-compatible gateway",
|
||||
});
|
||||
```
|
||||
|
||||
#### 7. `src/commands/auth-choice.preferred-provider.ts`
|
||||
|
||||
Add mapping:
|
||||
|
||||
```typescript
|
||||
const PREFERRED_PROVIDER_BY_AUTH_CHOICE: Partial<Record<AuthChoice, string>> = {
|
||||
// ... existing mappings
|
||||
"kilocode-api-key": "kilocode",
|
||||
};
|
||||
```
|
||||
|
||||
### Auth Choice Application
|
||||
|
||||
#### 8. `src/commands/auth-choice.apply.api-providers.ts`
|
||||
|
||||
Add import:
|
||||
|
||||
```typescript
|
||||
import {
|
||||
// ... existing imports
|
||||
applyKilocodeConfig,
|
||||
applyKilocodeProviderConfig,
|
||||
KILOCODE_DEFAULT_MODEL_REF,
|
||||
setKilocodeApiKey,
|
||||
} from "./onboard-auth.js";
|
||||
```
|
||||
|
||||
Add handling for `kilocode-api-key`:
|
||||
|
||||
```typescript
|
||||
if (authChoice === "kilocode-api-key") {
|
||||
const store = ensureAuthProfileStore(params.agentDir, {
|
||||
allowKeychainPrompt: false,
|
||||
});
|
||||
const profileOrder = resolveAuthProfileOrder({
|
||||
cfg: nextConfig,
|
||||
store,
|
||||
provider: "kilocode",
|
||||
});
|
||||
const existingProfileId = profileOrder.find((profileId) => Boolean(store.profiles[profileId]));
|
||||
const existingCred = existingProfileId ? store.profiles[existingProfileId] : undefined;
|
||||
let profileId = "kilocode:default";
|
||||
let mode: "api_key" | "oauth" | "token" = "api_key";
|
||||
let hasCredential = false;
|
||||
|
||||
if (existingProfileId && existingCred?.type) {
|
||||
profileId = existingProfileId;
|
||||
mode =
|
||||
existingCred.type === "oauth" ? "oauth" : existingCred.type === "token" ? "token" : "api_key";
|
||||
hasCredential = true;
|
||||
}
|
||||
|
||||
if (!hasCredential && params.opts?.token && params.opts?.tokenProvider === "kilocode") {
|
||||
await setKilocodeApiKey(normalizeApiKeyInput(params.opts.token), params.agentDir);
|
||||
hasCredential = true;
|
||||
}
|
||||
|
||||
if (!hasCredential) {
|
||||
const envKey = resolveEnvApiKey("kilocode");
|
||||
if (envKey) {
|
||||
const useExisting = await params.prompter.confirm({
|
||||
message: `Use existing KILOCODE_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`,
|
||||
initialValue: true,
|
||||
});
|
||||
if (useExisting) {
|
||||
await setKilocodeApiKey(envKey.apiKey, params.agentDir);
|
||||
hasCredential = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasCredential) {
|
||||
const key = await params.prompter.text({
|
||||
message: "Enter Kilo Gateway API key",
|
||||
validate: validateApiKeyInput,
|
||||
});
|
||||
await setKilocodeApiKey(normalizeApiKeyInput(String(key)), params.agentDir);
|
||||
hasCredential = true;
|
||||
}
|
||||
|
||||
if (hasCredential) {
|
||||
nextConfig = applyAuthProfileConfig(nextConfig, {
|
||||
profileId,
|
||||
provider: "kilocode",
|
||||
mode,
|
||||
});
|
||||
}
|
||||
{
|
||||
const applied = await applyDefaultModelChoice({
|
||||
config: nextConfig,
|
||||
setDefaultModel: params.setDefaultModel,
|
||||
defaultModel: KILOCODE_DEFAULT_MODEL_REF,
|
||||
applyDefaultConfig: applyKilocodeConfig,
|
||||
applyProviderConfig: applyKilocodeProviderConfig,
|
||||
noteDefault: KILOCODE_DEFAULT_MODEL_REF,
|
||||
noteAgentModel,
|
||||
prompter: params.prompter,
|
||||
});
|
||||
nextConfig = applied.config;
|
||||
agentModelOverride = applied.agentModelOverride ?? agentModelOverride;
|
||||
}
|
||||
return { config: nextConfig, agentModelOverride };
|
||||
}
|
||||
```
|
||||
|
||||
Also add tokenProvider mapping at the top of the function:
|
||||
|
||||
```typescript
|
||||
if (params.opts.tokenProvider === "kilocode") {
|
||||
authChoice = "kilocode-api-key";
|
||||
}
|
||||
```
|
||||
|
||||
### CLI Registration
|
||||
|
||||
#### 9. `src/cli/program/register.onboard.ts`
|
||||
|
||||
Add CLI option:
|
||||
|
||||
```typescript
|
||||
.option("--kilocode-api-key <key>", "Kilo Gateway API key")
|
||||
```
|
||||
|
||||
Add to action handler:
|
||||
|
||||
```typescript
|
||||
kilocodeApiKey: opts.kilocodeApiKey as string | undefined,
|
||||
```
|
||||
|
||||
Update auth-choice help text:
|
||||
|
||||
```typescript
|
||||
.option(
|
||||
"--auth-choice <choice>",
|
||||
"Auth: setup-token|token|chutes|openai-codex|openai-api-key|openrouter-api-key|kilocode-api-key|ai-gateway-api-key|...",
|
||||
)
|
||||
```
|
||||
|
||||
### Non-Interactive Onboarding
|
||||
|
||||
#### 10. `src/commands/onboard-non-interactive/local/auth-choice.ts`
|
||||
|
||||
Add handling for `kilocode-api-key`:
|
||||
|
||||
```typescript
|
||||
if (authChoice === "kilocode-api-key") {
|
||||
const resolved = await resolveNonInteractiveApiKey({
|
||||
provider: "kilocode",
|
||||
cfg: baseConfig,
|
||||
flagValue: opts.kilocodeApiKey,
|
||||
flagName: "--kilocode-api-key",
|
||||
envVar: "KILOCODE_API_KEY",
|
||||
});
|
||||
await setKilocodeApiKey(resolved.apiKey, agentDir);
|
||||
nextConfig = applyAuthProfileConfig(nextConfig, {
|
||||
profileId: "kilocode:default",
|
||||
provider: "kilocode",
|
||||
mode: "api_key",
|
||||
});
|
||||
// ... apply default model
|
||||
}
|
||||
```
|
||||
|
||||
### Export Updates
|
||||
|
||||
#### 11. `src/commands/onboard-auth.ts`
|
||||
|
||||
Add exports:
|
||||
|
||||
```typescript
|
||||
export {
|
||||
// ... existing exports
|
||||
applyKilocodeConfig,
|
||||
applyKilocodeProviderConfig,
|
||||
KILOCODE_BASE_URL,
|
||||
} from "./onboard-auth.config-core.js";
|
||||
|
||||
export {
|
||||
// ... existing exports
|
||||
KILOCODE_DEFAULT_MODEL_REF,
|
||||
setKilocodeApiKey,
|
||||
} from "./onboard-auth.credentials.js";
|
||||
```
|
||||
|
||||
### Special Handling (Optional)
|
||||
|
||||
#### 12. `src/agents/pi-embedded-runner/cache-ttl.ts`
|
||||
|
||||
Add Kilo Gateway support for Anthropic models:
|
||||
|
||||
```typescript
|
||||
export function isCacheTtlEligibleProvider(provider: string, modelId: string): boolean {
|
||||
const normalizedProvider = provider.toLowerCase();
|
||||
const normalizedModelId = modelId.toLowerCase();
|
||||
if (normalizedProvider === "anthropic") return true;
|
||||
if (normalizedProvider === "openrouter" && normalizedModelId.startsWith("anthropic/"))
|
||||
return true;
|
||||
if (normalizedProvider === "kilocode" && normalizedModelId.startsWith("anthropic/")) return true;
|
||||
return false;
|
||||
}
|
||||
```
|
||||
|
||||
#### 13. `src/agents/transcript-policy.ts`
|
||||
|
||||
Add Kilo Gateway handling (similar to OpenRouter):
|
||||
|
||||
```typescript
|
||||
const isKilocodeGemini = provider === "kilocode" && modelId.toLowerCase().includes("gemini");
|
||||
|
||||
// Include in needsNonImageSanitize check
|
||||
const needsNonImageSanitize =
|
||||
isGoogle || isAnthropic || isMistral || isOpenRouterGemini || isKilocodeGemini;
|
||||
```
|
||||
|
||||
## Configuration Structure
|
||||
|
||||
### User Config Example
|
||||
|
||||
```json
|
||||
{
|
||||
"models": {
|
||||
"mode": "merge",
|
||||
"providers": {
|
||||
"kilocode": {
|
||||
"baseUrl": "https://api.kilo.ai/api/gateway/",
|
||||
"apiKey": "xxxxx",
|
||||
"api": "openai-completions",
|
||||
"models": [
|
||||
{
|
||||
"id": "anthropic/claude-opus-4.6",
|
||||
"name": "Anthropic: Claude Opus 4.6"
|
||||
},
|
||||
{ "id": "minimax/minimax-m2.5:free", "name": "Minimax: Minimax M2.5" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Auth Profile Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"profiles": {
|
||||
"kilocode:default": {
|
||||
"type": "api_key",
|
||||
"provider": "kilocode",
|
||||
"key": "xxxxx"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Considerations
|
||||
|
||||
1. **Unit Tests:**
|
||||
- Test `setKilocodeApiKey()` writes correct profile
|
||||
- Test `applyKilocodeConfig()` sets correct defaults
|
||||
- Test `resolveEnvApiKey("kilocode")` returns correct env var
|
||||
|
||||
2. **Integration Tests:**
|
||||
- Test onboarding flow with `--auth-choice kilocode-api-key`
|
||||
- Test non-interactive onboarding with `--kilocode-api-key`
|
||||
- Test model selection with `kilocode/` prefix
|
||||
|
||||
3. **E2E Tests:**
|
||||
- Test actual API calls through Kilo Gateway (live tests)
|
||||
|
||||
## Migration Notes
|
||||
|
||||
- No migration needed for existing users
|
||||
- New users can immediately use `kilocode-api-key` auth choice
|
||||
- Existing manual config with `kilocode` provider will continue to work
|
||||
|
||||
## Future Considerations
|
||||
|
||||
1. **Model Catalog:** If Kilo Gateway exposes a `/models` endpoint, add scanning support similar to `scanOpenRouterModels()`
|
||||
|
||||
2. **OAuth Support:** If Kilo Gateway adds OAuth, extend the auth system accordingly
|
||||
|
||||
3. **Rate Limiting:** Consider adding rate limit handling specific to Kilo Gateway if needed
|
||||
|
||||
4. **Documentation:** Add docs at `docs/providers/kilocode.md` explaining setup and usage
|
||||
|
||||
## Summary of Changes
|
||||
|
||||
| File | Change Type | Description |
|
||||
| ----------------------------------------------------------- | ----------- | ----------------------------------------------------------------------- |
|
||||
| `src/commands/onboard-auth.credentials.ts` | Add | `KILOCODE_DEFAULT_MODEL_REF`, `setKilocodeApiKey()` |
|
||||
| `src/agents/model-auth.ts` | Modify | Add `kilocode` to `envMap` |
|
||||
| `src/config/io.ts` | Modify | Add `KILOCODE_API_KEY` to shell env keys |
|
||||
| `src/commands/onboard-auth.config-core.ts` | Add | `applyKilocodeProviderConfig()`, `applyKilocodeConfig()` |
|
||||
| `src/commands/onboard-types.ts` | Modify | Add `kilocode-api-key` to `AuthChoice`, add `kilocodeApiKey` to options |
|
||||
| `src/commands/auth-choice-options.ts` | Modify | Add `kilocode` group and option |
|
||||
| `src/commands/auth-choice.preferred-provider.ts` | Modify | Add `kilocode-api-key` mapping |
|
||||
| `src/commands/auth-choice.apply.api-providers.ts` | Modify | Add `kilocode-api-key` handling |
|
||||
| `src/cli/program/register.onboard.ts` | Modify | Add `--kilocode-api-key` option |
|
||||
| `src/commands/onboard-non-interactive/local/auth-choice.ts` | Modify | Add non-interactive handling |
|
||||
| `src/commands/onboard-auth.ts` | Modify | Export new functions |
|
||||
| `src/agents/pi-embedded-runner/cache-ttl.ts` | Modify | Add kilocode support |
|
||||
| `src/agents/transcript-policy.ts` | Modify | Add kilocode Gemini handling |
|
||||
@@ -59,9 +59,13 @@
|
||||
"source": "/compaction",
|
||||
"destination": "/concepts/compaction"
|
||||
},
|
||||
{
|
||||
"source": "/context-engine",
|
||||
"destination": "/concepts/context-engine"
|
||||
},
|
||||
{
|
||||
"source": "/cron",
|
||||
"destination": "/cron-jobs"
|
||||
"destination": "/automation/cron-jobs"
|
||||
},
|
||||
{
|
||||
"source": "/minimax",
|
||||
@@ -509,11 +513,11 @@
|
||||
},
|
||||
{
|
||||
"source": "/model",
|
||||
"destination": "/models"
|
||||
"destination": "/concepts/models"
|
||||
},
|
||||
{
|
||||
"source": "/model/",
|
||||
"destination": "/models"
|
||||
"destination": "/concepts/models"
|
||||
},
|
||||
{
|
||||
"source": "/models",
|
||||
@@ -531,10 +535,6 @@
|
||||
"source": "/onboarding",
|
||||
"destination": "/start/onboarding"
|
||||
},
|
||||
{
|
||||
"source": "/onboarding-config-protocol",
|
||||
"destination": "/experiments/onboarding-config-protocol"
|
||||
},
|
||||
{
|
||||
"source": "/pairing",
|
||||
"destination": "/channels/pairing"
|
||||
@@ -555,10 +555,6 @@
|
||||
"source": "/presence",
|
||||
"destination": "/concepts/presence"
|
||||
},
|
||||
{
|
||||
"source": "/proposals/model-config",
|
||||
"destination": "/experiments/proposals/model-config"
|
||||
},
|
||||
{
|
||||
"source": "/provider-routing",
|
||||
"destination": "/channels/channel-routing"
|
||||
@@ -579,10 +575,6 @@
|
||||
"source": "/remote-gateway-readme",
|
||||
"destination": "/gateway/remote-gateway-readme"
|
||||
},
|
||||
{
|
||||
"source": "/research/memory",
|
||||
"destination": "/experiments/research/memory"
|
||||
},
|
||||
{
|
||||
"source": "/rpc",
|
||||
"destination": "/reference/rpc"
|
||||
@@ -952,6 +944,7 @@
|
||||
"concepts/agent-loop",
|
||||
"concepts/system-prompt",
|
||||
"concepts/context",
|
||||
"concepts/context-engine",
|
||||
"concepts/agent-workspace",
|
||||
"concepts/oauth"
|
||||
]
|
||||
@@ -1018,7 +1011,6 @@
|
||||
"pages": [
|
||||
"tools/browser",
|
||||
"tools/browser-login",
|
||||
"tools/chrome-extension",
|
||||
"tools/browser-linux-troubleshooting"
|
||||
]
|
||||
},
|
||||
@@ -1051,6 +1043,7 @@
|
||||
"plugins/zalouser",
|
||||
"plugins/manifest",
|
||||
"plugins/agent-tools",
|
||||
"tools/capability-cookbook",
|
||||
"prose"
|
||||
]
|
||||
},
|
||||
@@ -1353,21 +1346,6 @@
|
||||
{
|
||||
"group": "Release policy",
|
||||
"pages": ["reference/RELEASING", "reference/test"]
|
||||
},
|
||||
{
|
||||
"group": "Experiments",
|
||||
"pages": [
|
||||
"design/kilo-gateway-integration",
|
||||
"experiments/onboarding-config-protocol",
|
||||
"experiments/plans/acp-thread-bound-agents",
|
||||
"experiments/plans/acp-unified-streaming-refactor",
|
||||
"experiments/plans/browser-evaluate-cdp-refactor",
|
||||
"experiments/plans/openresponses-gateway",
|
||||
"experiments/plans/pty-process-supervision",
|
||||
"experiments/plans/session-binding-channel-agnostic",
|
||||
"experiments/research/memory",
|
||||
"experiments/proposals/model-config"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -1613,7 +1591,6 @@
|
||||
"pages": [
|
||||
"zh-CN/tools/browser",
|
||||
"zh-CN/tools/browser-login",
|
||||
"zh-CN/tools/chrome-extension",
|
||||
"zh-CN/tools/browser-linux-troubleshooting"
|
||||
]
|
||||
},
|
||||
@@ -1934,27 +1911,6 @@
|
||||
{
|
||||
"group": "发布策略",
|
||||
"pages": ["zh-CN/reference/RELEASING", "zh-CN/reference/test"]
|
||||
},
|
||||
{
|
||||
"group": "实验性功能",
|
||||
"pages": [
|
||||
"zh-CN/experiments/onboarding-config-protocol",
|
||||
"zh-CN/experiments/plans/openresponses-gateway",
|
||||
"zh-CN/experiments/plans/cron-add-hardening",
|
||||
"zh-CN/experiments/plans/group-policy-hardening",
|
||||
"zh-CN/experiments/research/memory",
|
||||
"zh-CN/experiments/proposals/model-config"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "重构方案",
|
||||
"pages": [
|
||||
"zh-CN/refactor/clawnet",
|
||||
"zh-CN/refactor/exec-host",
|
||||
"zh-CN/refactor/outbound-session-mirroring",
|
||||
"zh-CN/refactor/plugin-sdk",
|
||||
"zh-CN/refactor/strict-config"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
---
|
||||
summary: "RPC protocol notes for onboarding wizard and config schema"
|
||||
read_when: "Changing onboarding wizard steps or config schema endpoints"
|
||||
title: "Onboarding and Config Protocol"
|
||||
---
|
||||
|
||||
# Onboarding + Config Protocol
|
||||
|
||||
Purpose: shared onboarding + config surfaces across CLI, macOS app, and Web UI.
|
||||
|
||||
## Components
|
||||
|
||||
- Wizard engine (shared session + prompts + onboarding state).
|
||||
- CLI onboarding uses the same wizard flow as the UI clients.
|
||||
- Gateway RPC exposes wizard + config schema endpoints.
|
||||
- macOS onboarding uses the wizard step model.
|
||||
- Web UI renders config forms from JSON Schema + UI hints.
|
||||
|
||||
## Gateway RPC
|
||||
|
||||
- `wizard.start` params: `{ mode?: "local"|"remote", workspace?: string }`
|
||||
- `wizard.next` params: `{ sessionId, answer?: { stepId, value? } }`
|
||||
- `wizard.cancel` params: `{ sessionId }`
|
||||
- `wizard.status` params: `{ sessionId }`
|
||||
- `config.schema` params: `{}`
|
||||
- `config.schema.lookup` params: `{ path }`
|
||||
- `path` accepts standard config segments plus slash-delimited plugin ids, for example `plugins.entries.pack/one.config`.
|
||||
|
||||
Responses (shape)
|
||||
|
||||
- Wizard: `{ sessionId, done, step?, status?, error? }`
|
||||
- Config schema: `{ schema, uiHints, version, generatedAt }`
|
||||
- Config schema lookup: `{ path, schema, hint?, hintPath?, children[] }`
|
||||
|
||||
## UI Hints
|
||||
|
||||
- `uiHints` keyed by path; optional metadata (label/help/group/order/advanced/sensitive/placeholder).
|
||||
- Sensitive fields render as password inputs; no redaction layer.
|
||||
- Unsupported schema nodes fall back to the raw JSON editor.
|
||||
|
||||
## Notes
|
||||
|
||||
- This doc is the single place to track protocol refactors for onboarding/config.
|
||||
@@ -1,375 +0,0 @@
|
||||
# ACP Persistent Bindings for Discord Channels and Telegram Topics
|
||||
|
||||
Status: Draft
|
||||
|
||||
## Summary
|
||||
|
||||
Introduce persistent ACP bindings that map:
|
||||
|
||||
- Discord channels (and existing threads, where needed), and
|
||||
- Telegram forum topics in groups/supergroups (`chatId:topic:topicId`)
|
||||
|
||||
to long-lived ACP sessions, with binding state stored in top-level `bindings[]` entries using explicit binding types.
|
||||
|
||||
This makes ACP usage in high-traffic messaging channels predictable and durable, so users can create dedicated channels/topics such as `codex`, `claude-1`, or `claude-myrepo`.
|
||||
|
||||
## Why
|
||||
|
||||
Current thread-bound ACP behavior is optimized for ephemeral Discord thread workflows. Telegram does not have the same thread model; it has forum topics in groups/supergroups. Users want stable, always-on ACP “workspaces” in chat surfaces, not only temporary thread sessions.
|
||||
|
||||
## Goals
|
||||
|
||||
- Support durable ACP binding for:
|
||||
- Discord channels/threads
|
||||
- Telegram forum topics (groups/supergroups)
|
||||
- Make binding source-of-truth config-driven.
|
||||
- Keep `/acp`, `/new`, `/reset`, `/focus`, and delivery behavior consistent across Discord and Telegram.
|
||||
- Preserve existing temporary binding flows for ad-hoc usage.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- Full redesign of ACP runtime/session internals.
|
||||
- Removing existing ephemeral binding flows.
|
||||
- Expanding to every channel in the first iteration.
|
||||
- Implementing Telegram channel direct-messages topics (`direct_messages_topic_id`) in this phase.
|
||||
- Implementing Telegram private-chat topic variants in this phase.
|
||||
|
||||
## UX Direction
|
||||
|
||||
### 1) Two binding types
|
||||
|
||||
- **Persistent binding**: saved in config, reconciled on startup, intended for “named workspace” channels/topics.
|
||||
- **Temporary binding**: runtime-only, expires by idle/max-age policy.
|
||||
|
||||
### 2) Command behavior
|
||||
|
||||
- `/acp spawn ... --thread here|auto|off` remains available.
|
||||
- Add explicit bind lifecycle controls:
|
||||
- `/acp bind [session|agent] [--persist]`
|
||||
- `/acp unbind [--persist]`
|
||||
- `/acp status` includes whether binding is `persistent` or `temporary`.
|
||||
- In bound conversations, `/new` and `/reset` reset the bound ACP session in place and keep the binding attached.
|
||||
|
||||
### 3) Conversation identity
|
||||
|
||||
- Use canonical conversation IDs:
|
||||
- Discord: channel/thread ID.
|
||||
- Telegram topic: `chatId:topic:topicId`.
|
||||
- Never key Telegram bindings by bare topic ID alone.
|
||||
|
||||
## Config Model (Proposed)
|
||||
|
||||
Unify routing and persistent ACP binding configuration in top-level `bindings[]` with explicit `type` discriminator:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"agents": {
|
||||
"list": [
|
||||
{
|
||||
"id": "main",
|
||||
"default": true,
|
||||
"workspace": "~/.openclaw/workspace-main",
|
||||
"runtime": { "type": "embedded" },
|
||||
},
|
||||
{
|
||||
"id": "codex",
|
||||
"workspace": "~/.openclaw/workspace-codex",
|
||||
"runtime": {
|
||||
"type": "acp",
|
||||
"acp": {
|
||||
"agent": "codex",
|
||||
"backend": "acpx",
|
||||
"mode": "persistent",
|
||||
"cwd": "/workspace/repo-a",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"id": "claude",
|
||||
"workspace": "~/.openclaw/workspace-claude",
|
||||
"runtime": {
|
||||
"type": "acp",
|
||||
"acp": {
|
||||
"agent": "claude",
|
||||
"backend": "acpx",
|
||||
"mode": "persistent",
|
||||
"cwd": "/workspace/repo-b",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
"acp": {
|
||||
"enabled": true,
|
||||
"backend": "acpx",
|
||||
"allowedAgents": ["codex", "claude"],
|
||||
},
|
||||
"bindings": [
|
||||
// Route bindings (existing behavior)
|
||||
{
|
||||
"type": "route",
|
||||
"agentId": "main",
|
||||
"match": { "channel": "discord", "accountId": "default" },
|
||||
},
|
||||
{
|
||||
"type": "route",
|
||||
"agentId": "main",
|
||||
"match": { "channel": "telegram", "accountId": "default" },
|
||||
},
|
||||
// Persistent ACP conversation bindings
|
||||
{
|
||||
"type": "acp",
|
||||
"agentId": "codex",
|
||||
"match": {
|
||||
"channel": "discord",
|
||||
"accountId": "default",
|
||||
"peer": { "kind": "channel", "id": "222222222222222222" },
|
||||
},
|
||||
"acp": {
|
||||
"label": "codex-main",
|
||||
"mode": "persistent",
|
||||
"cwd": "/workspace/repo-a",
|
||||
"backend": "acpx",
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "acp",
|
||||
"agentId": "claude",
|
||||
"match": {
|
||||
"channel": "discord",
|
||||
"accountId": "default",
|
||||
"peer": { "kind": "channel", "id": "333333333333333333" },
|
||||
},
|
||||
"acp": {
|
||||
"label": "claude-repo-b",
|
||||
"mode": "persistent",
|
||||
"cwd": "/workspace/repo-b",
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "acp",
|
||||
"agentId": "codex",
|
||||
"match": {
|
||||
"channel": "telegram",
|
||||
"accountId": "default",
|
||||
"peer": { "kind": "group", "id": "-1001234567890:topic:42" },
|
||||
},
|
||||
"acp": {
|
||||
"label": "tg-codex-42",
|
||||
"mode": "persistent",
|
||||
},
|
||||
},
|
||||
],
|
||||
"channels": {
|
||||
"discord": {
|
||||
"guilds": {
|
||||
"111111111111111111": {
|
||||
"channels": {
|
||||
"222222222222222222": {
|
||||
"enabled": true,
|
||||
"requireMention": false,
|
||||
},
|
||||
"333333333333333333": {
|
||||
"enabled": true,
|
||||
"requireMention": false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"telegram": {
|
||||
"groups": {
|
||||
"-1001234567890": {
|
||||
"topics": {
|
||||
"42": {
|
||||
"requireMention": false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### Minimal Example (No Per-Binding ACP Overrides)
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"agents": {
|
||||
"list": [
|
||||
{ "id": "main", "default": true, "runtime": { "type": "embedded" } },
|
||||
{
|
||||
"id": "codex",
|
||||
"runtime": {
|
||||
"type": "acp",
|
||||
"acp": { "agent": "codex", "backend": "acpx", "mode": "persistent" },
|
||||
},
|
||||
},
|
||||
{
|
||||
"id": "claude",
|
||||
"runtime": {
|
||||
"type": "acp",
|
||||
"acp": { "agent": "claude", "backend": "acpx", "mode": "persistent" },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
"acp": { "enabled": true, "backend": "acpx" },
|
||||
"bindings": [
|
||||
{
|
||||
"type": "route",
|
||||
"agentId": "main",
|
||||
"match": { "channel": "discord", "accountId": "default" },
|
||||
},
|
||||
{
|
||||
"type": "route",
|
||||
"agentId": "main",
|
||||
"match": { "channel": "telegram", "accountId": "default" },
|
||||
},
|
||||
|
||||
{
|
||||
"type": "acp",
|
||||
"agentId": "codex",
|
||||
"match": {
|
||||
"channel": "discord",
|
||||
"accountId": "default",
|
||||
"peer": { "kind": "channel", "id": "222222222222222222" },
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "acp",
|
||||
"agentId": "claude",
|
||||
"match": {
|
||||
"channel": "discord",
|
||||
"accountId": "default",
|
||||
"peer": { "kind": "channel", "id": "333333333333333333" },
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "acp",
|
||||
"agentId": "codex",
|
||||
"match": {
|
||||
"channel": "telegram",
|
||||
"accountId": "default",
|
||||
"peer": { "kind": "group", "id": "-1009876543210:topic:5" },
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- `bindings[].type` is explicit:
|
||||
- `route`: normal agent routing.
|
||||
- `acp`: persistent ACP harness binding for a matched conversation.
|
||||
- For `type: "acp"`, `match.peer.id` is the canonical conversation key:
|
||||
- Discord channel/thread: raw channel/thread ID.
|
||||
- Telegram topic: `chatId:topic:topicId`.
|
||||
- `bindings[].acp.backend` is optional. Backend fallback order:
|
||||
1. `bindings[].acp.backend`
|
||||
2. `agents.list[].runtime.acp.backend`
|
||||
3. global `acp.backend`
|
||||
- `mode`, `cwd`, and `label` follow the same override pattern (`binding override -> agent runtime default -> global/default behavior`).
|
||||
- Keep existing `session.threadBindings.*` and `channels.discord.threadBindings.*` for temporary binding policies.
|
||||
- Persistent entries declare desired state; runtime reconciles to actual ACP sessions/bindings.
|
||||
- One active ACP binding per conversation node is the intended model.
|
||||
- Backward compatibility: missing `type` is interpreted as `route` for legacy entries.
|
||||
|
||||
### Backend Selection
|
||||
|
||||
- ACP session initialization already uses configured backend selection during spawn (`acp.backend` today).
|
||||
- This proposal extends spawn/reconcile logic to prefer typed ACP binding overrides:
|
||||
- `bindings[].acp.backend` for conversation-local override.
|
||||
- `agents.list[].runtime.acp.backend` for per-agent defaults.
|
||||
- If no override exists, keep current behavior (`acp.backend` default).
|
||||
|
||||
## Architecture Fit in Current System
|
||||
|
||||
### Reuse existing components
|
||||
|
||||
- `SessionBindingService` already supports channel-agnostic conversation references.
|
||||
- ACP spawn/bind flows already support binding through service APIs.
|
||||
- Telegram already carries topic/thread context via `MessageThreadId` and `chatId`.
|
||||
|
||||
### New/extended components
|
||||
|
||||
- **Telegram binding adapter** (parallel to Discord adapter):
|
||||
- register adapter per Telegram account,
|
||||
- resolve/list/bind/unbind/touch by canonical conversation ID.
|
||||
- **Typed binding resolver/index**:
|
||||
- split `bindings[]` into `route` and `acp` views,
|
||||
- keep `resolveAgentRoute` on `route` bindings only,
|
||||
- resolve persistent ACP intent from `acp` bindings only.
|
||||
- **Inbound binding resolution for Telegram**:
|
||||
- resolve bound session before route finalization (Discord already does this).
|
||||
- **Persistent binding reconciler**:
|
||||
- on startup: load configured top-level `type: "acp"` bindings, ensure ACP sessions exist, ensure bindings exist.
|
||||
- on config change: apply deltas safely.
|
||||
- **Cutover model**:
|
||||
- no channel-local ACP binding fallback is read,
|
||||
- persistent ACP bindings are sourced only from top-level `bindings[].type="acp"` entries.
|
||||
|
||||
## Phased Delivery
|
||||
|
||||
### Phase 1: Typed binding schema foundation
|
||||
|
||||
- Extend config schema to support `bindings[].type` discriminator:
|
||||
- `route`,
|
||||
- `acp` with optional `acp` override object (`mode`, `backend`, `cwd`, `label`).
|
||||
- Extend agent schema with runtime descriptor to mark ACP-native agents (`agents.list[].runtime.type`).
|
||||
- Add parser/indexer split for route vs ACP bindings.
|
||||
|
||||
### Phase 2: Runtime resolution + Discord/Telegram parity
|
||||
|
||||
- Resolve persistent ACP bindings from top-level `type: "acp"` entries for:
|
||||
- Discord channels/threads,
|
||||
- Telegram forum topics (`chatId:topic:topicId` canonical IDs).
|
||||
- Implement Telegram binding adapter and inbound bound-session override parity with Discord.
|
||||
- Do not include Telegram direct/private topic variants in this phase.
|
||||
|
||||
### Phase 3: Command parity and resets
|
||||
|
||||
- Align `/acp`, `/new`, `/reset`, and `/focus` behavior in bound Telegram/Discord conversations.
|
||||
- Ensure binding survives reset flows as configured.
|
||||
|
||||
### Phase 4: Hardening
|
||||
|
||||
- Better diagnostics (`/acp status`, startup reconciliation logs).
|
||||
- Conflict handling and health checks.
|
||||
|
||||
## Guardrails and Policy
|
||||
|
||||
- Respect ACP enablement and sandbox restrictions exactly as today.
|
||||
- Keep explicit account scoping (`accountId`) to avoid cross-account bleed.
|
||||
- Fail closed on ambiguous routing.
|
||||
- Keep mention/access policy behavior explicit per channel config.
|
||||
|
||||
## Testing Plan
|
||||
|
||||
- Unit:
|
||||
- conversation ID normalization (especially Telegram topic IDs),
|
||||
- reconciler create/update/delete paths,
|
||||
- `/acp bind --persist` and unbind flows.
|
||||
- Integration:
|
||||
- inbound Telegram topic -> bound ACP session resolution,
|
||||
- inbound Discord channel/thread -> persistent binding precedence.
|
||||
- Regression:
|
||||
- temporary bindings continue to work,
|
||||
- unbound channels/topics keep current routing behavior.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- Should `/acp spawn --thread auto` in Telegram topic default to `here`?
|
||||
- Should persistent bindings always bypass mention-gating in bound conversations, or require explicit `requireMention=false`?
|
||||
- Should `/focus` gain `--persist` as an alias for `/acp bind --persist`?
|
||||
|
||||
## Rollout
|
||||
|
||||
- Ship as opt-in per conversation (`bindings[].type="acp"` entry present).
|
||||
- Start with Discord + Telegram only.
|
||||
- Add docs with examples for:
|
||||
- “one channel/topic per agent”
|
||||
- “multiple channels/topics per same agent with different `cwd`”
|
||||
- “team naming patterns (`codex-1`, `claude-repo-x`)".
|
||||
@@ -1,800 +0,0 @@
|
||||
---
|
||||
summary: "Integrate ACP coding agents via a first-class ACP control plane in core and plugin-backed runtimes (acpx first)"
|
||||
owner: "onutc"
|
||||
status: "draft"
|
||||
last_updated: "2026-02-25"
|
||||
title: "ACP Thread Bound Agents"
|
||||
---
|
||||
|
||||
# ACP Thread Bound Agents
|
||||
|
||||
## Overview
|
||||
|
||||
This plan defines how OpenClaw should support ACP coding agents in thread-capable channels (Discord first) with production-level lifecycle and recovery.
|
||||
|
||||
Related document:
|
||||
|
||||
- [Unified Runtime Streaming Refactor Plan](/experiments/plans/acp-unified-streaming-refactor)
|
||||
|
||||
Target user experience:
|
||||
|
||||
- a user spawns or focuses an ACP session into a thread
|
||||
- user messages in that thread route to the bound ACP session
|
||||
- agent output streams back to the same thread persona
|
||||
- session can be persistent or one shot with explicit cleanup controls
|
||||
|
||||
## Decision summary
|
||||
|
||||
Long term recommendation is a hybrid architecture:
|
||||
|
||||
- OpenClaw core owns ACP control plane concerns
|
||||
- session identity and metadata
|
||||
- thread binding and routing decisions
|
||||
- delivery invariants and duplicate suppression
|
||||
- lifecycle cleanup and recovery semantics
|
||||
- ACP runtime backend is pluggable
|
||||
- first backend is an acpx-backed plugin service
|
||||
- runtime does ACP transport, queueing, cancel, reconnect
|
||||
|
||||
OpenClaw should not reimplement ACP transport internals in core.
|
||||
OpenClaw should not rely on a pure plugin-only interception path for routing.
|
||||
|
||||
## North-star architecture (holy grail)
|
||||
|
||||
Treat ACP as a first-class control plane in OpenClaw, with pluggable runtime adapters.
|
||||
|
||||
Non-negotiable invariants:
|
||||
|
||||
- every ACP thread binding references a valid ACP session record
|
||||
- every ACP session has explicit lifecycle state (`creating`, `idle`, `running`, `cancelling`, `closed`, `error`)
|
||||
- every ACP run has explicit run state (`queued`, `running`, `completed`, `failed`, `cancelled`)
|
||||
- spawn, bind, and initial enqueue are atomic
|
||||
- command retries are idempotent (no duplicate runs or duplicate Discord outputs)
|
||||
- bound-thread channel output is a projection of ACP run events, never ad-hoc side effects
|
||||
|
||||
Long-term ownership model:
|
||||
|
||||
- `AcpSessionManager` is the single ACP writer and orchestrator
|
||||
- manager lives in gateway process first; can be moved to a dedicated sidecar later behind the same interface
|
||||
- per ACP session key, manager owns one in-memory actor (serialized command execution)
|
||||
- adapters (`acpx`, future backends) are transport/runtime implementations only
|
||||
|
||||
Long-term persistence model:
|
||||
|
||||
- move ACP control-plane state to a dedicated SQLite store (WAL mode) under OpenClaw state dir
|
||||
- keep `SessionEntry.acp` as compatibility projection during migration, not source-of-truth
|
||||
- store ACP events append-only to support replay, crash recovery, and deterministic delivery
|
||||
|
||||
### Delivery strategy (bridge to holy-grail)
|
||||
|
||||
- short-term bridge
|
||||
- keep current thread binding mechanics and existing ACP config surface
|
||||
- fix metadata-gap bugs and route ACP turns through a single core ACP branch
|
||||
- add idempotency keys and fail-closed routing checks immediately
|
||||
- long-term cutover
|
||||
- move ACP source-of-truth to control-plane DB + actors
|
||||
- make bound-thread delivery purely event-projection based
|
||||
- remove legacy fallback behavior that depends on opportunistic session-entry metadata
|
||||
|
||||
## Why not pure plugin only
|
||||
|
||||
Current plugin hooks are not sufficient for end to end ACP session routing without core changes.
|
||||
|
||||
- inbound routing from thread binding resolves to a session key in core dispatch first
|
||||
- message hooks are fire-and-forget and cannot short-circuit the main reply path
|
||||
- plugin commands are good for control operations but not for replacing core per-turn dispatch flow
|
||||
|
||||
Result:
|
||||
|
||||
- ACP runtime can be pluginized
|
||||
- ACP routing branch must exist in core
|
||||
|
||||
## Existing foundation to reuse
|
||||
|
||||
Already implemented and should remain canonical:
|
||||
|
||||
- thread binding target supports `subagent` and `acp`
|
||||
- inbound thread routing override resolves by binding before normal dispatch
|
||||
- outbound thread identity via webhook in reply delivery
|
||||
- `/focus` and `/unfocus` flow with ACP target compatibility
|
||||
- persistent binding store with restore on startup
|
||||
- unbind lifecycle on archive, delete, unfocus, reset, and delete
|
||||
|
||||
This plan extends that foundation rather than replacing it.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Boundary model
|
||||
|
||||
Core (must be in OpenClaw core):
|
||||
|
||||
- ACP session-mode dispatch branch in the reply pipeline
|
||||
- delivery arbitration to avoid parent plus thread duplication
|
||||
- ACP control-plane persistence (with `SessionEntry.acp` compatibility projection during migration)
|
||||
- lifecycle unbind and runtime detach semantics tied to session reset/delete
|
||||
|
||||
Plugin backend (acpx implementation):
|
||||
|
||||
- ACP runtime worker supervision
|
||||
- acpx process invocation and event parsing
|
||||
- ACP command handlers (`/acp ...`) and operator UX
|
||||
- backend-specific config defaults and diagnostics
|
||||
|
||||
### Runtime ownership model
|
||||
|
||||
- one gateway process owns ACP orchestration state
|
||||
- ACP execution runs in supervised child processes via acpx backend
|
||||
- process strategy is long lived per active ACP session key, not per message
|
||||
|
||||
This avoids startup cost on every prompt and keeps cancel and reconnect semantics reliable.
|
||||
|
||||
### Core runtime contract
|
||||
|
||||
Add a core ACP runtime contract so routing code does not depend on CLI details and can switch backends without changing dispatch logic:
|
||||
|
||||
```ts
|
||||
export type AcpRuntimePromptMode = "prompt" | "steer";
|
||||
|
||||
export type AcpRuntimeHandle = {
|
||||
sessionKey: string;
|
||||
backend: string;
|
||||
runtimeSessionName: string;
|
||||
};
|
||||
|
||||
export type AcpRuntimeEvent =
|
||||
| { type: "text_delta"; stream: "output" | "thought"; text: string }
|
||||
| { type: "tool_call"; name: string; argumentsText: string }
|
||||
| { type: "done"; usage?: Record<string, number> }
|
||||
| { type: "error"; code: string; message: string; retryable?: boolean };
|
||||
|
||||
export interface AcpRuntime {
|
||||
ensureSession(input: {
|
||||
sessionKey: string;
|
||||
agent: string;
|
||||
mode: "persistent" | "oneshot";
|
||||
cwd?: string;
|
||||
env?: Record<string, string>;
|
||||
idempotencyKey: string;
|
||||
}): Promise<AcpRuntimeHandle>;
|
||||
|
||||
submit(input: {
|
||||
handle: AcpRuntimeHandle;
|
||||
text: string;
|
||||
mode: AcpRuntimePromptMode;
|
||||
idempotencyKey: string;
|
||||
}): Promise<{ runtimeRunId: string }>;
|
||||
|
||||
stream(input: {
|
||||
handle: AcpRuntimeHandle;
|
||||
runtimeRunId: string;
|
||||
onEvent: (event: AcpRuntimeEvent) => Promise<void> | void;
|
||||
signal?: AbortSignal;
|
||||
}): Promise<void>;
|
||||
|
||||
cancel(input: {
|
||||
handle: AcpRuntimeHandle;
|
||||
runtimeRunId?: string;
|
||||
reason?: string;
|
||||
idempotencyKey: string;
|
||||
}): Promise<void>;
|
||||
|
||||
close(input: { handle: AcpRuntimeHandle; reason: string; idempotencyKey: string }): Promise<void>;
|
||||
|
||||
health?(): Promise<{ ok: boolean; details?: string }>;
|
||||
}
|
||||
```
|
||||
|
||||
Implementation detail:
|
||||
|
||||
- first backend: `AcpxRuntime` shipped as a plugin service
|
||||
- core resolves runtime via registry and fails with explicit operator error when no ACP runtime backend is available
|
||||
|
||||
### Control-plane data model and persistence
|
||||
|
||||
Long-term source-of-truth is a dedicated ACP SQLite database (WAL mode), for transactional updates and crash-safe recovery:
|
||||
|
||||
- `acp_sessions`
|
||||
- `session_key` (pk), `backend`, `agent`, `mode`, `cwd`, `state`, `created_at`, `updated_at`, `last_error`
|
||||
- `acp_runs`
|
||||
- `run_id` (pk), `session_key` (fk), `state`, `requester_message_id`, `idempotency_key`, `started_at`, `ended_at`, `error_code`, `error_message`
|
||||
- `acp_bindings`
|
||||
- `binding_key` (pk), `thread_id`, `channel_id`, `account_id`, `session_key` (fk), `expires_at`, `bound_at`
|
||||
- `acp_events`
|
||||
- `event_id` (pk), `run_id` (fk), `seq`, `kind`, `payload_json`, `created_at`
|
||||
- `acp_delivery_checkpoint`
|
||||
- `run_id` (pk/fk), `last_event_seq`, `last_discord_message_id`, `updated_at`
|
||||
- `acp_idempotency`
|
||||
- `scope`, `idempotency_key`, `result_json`, `created_at`, unique `(scope, idempotency_key)`
|
||||
|
||||
```ts
|
||||
export type AcpSessionMeta = {
|
||||
backend: string;
|
||||
agent: string;
|
||||
runtimeSessionName: string;
|
||||
mode: "persistent" | "oneshot";
|
||||
cwd?: string;
|
||||
state: "idle" | "running" | "error";
|
||||
lastActivityAt: number;
|
||||
lastError?: string;
|
||||
};
|
||||
```
|
||||
|
||||
Storage rules:
|
||||
|
||||
- keep `SessionEntry.acp` as a compatibility projection during migration
|
||||
- process ids and sockets stay in memory only
|
||||
- durable lifecycle and run status live in ACP DB, not generic session JSON
|
||||
- if runtime owner dies, gateway rehydrates from ACP DB and resumes from checkpoints
|
||||
|
||||
### Routing and delivery
|
||||
|
||||
Inbound:
|
||||
|
||||
- keep current thread binding lookup as first routing step
|
||||
- if bound target is ACP session, route to ACP runtime branch instead of `getReplyFromConfig`
|
||||
- explicit `/acp steer` command uses `mode: "steer"`
|
||||
|
||||
Outbound:
|
||||
|
||||
- ACP event stream is normalized to OpenClaw reply chunks
|
||||
- delivery target is resolved through existing bound destination path
|
||||
- when a bound thread is active for that session turn, parent channel completion is suppressed
|
||||
|
||||
Streaming policy:
|
||||
|
||||
- stream partial output with coalescing window
|
||||
- configurable min interval and max chunk bytes to stay under Discord rate limits
|
||||
- final message always emitted on completion or failure
|
||||
|
||||
### State machines and transaction boundaries
|
||||
|
||||
Session state machine:
|
||||
|
||||
- `creating -> idle -> running -> idle`
|
||||
- `running -> cancelling -> idle | error`
|
||||
- `idle -> closed`
|
||||
- `error -> idle | closed`
|
||||
|
||||
Run state machine:
|
||||
|
||||
- `queued -> running -> completed`
|
||||
- `running -> failed | cancelled`
|
||||
- `queued -> cancelled`
|
||||
|
||||
Required transaction boundaries:
|
||||
|
||||
- spawn transaction
|
||||
- create ACP session row
|
||||
- create/update ACP thread binding row
|
||||
- enqueue initial run row
|
||||
- close transaction
|
||||
- mark session closed
|
||||
- delete/expire binding rows
|
||||
- write final close event
|
||||
- cancel transaction
|
||||
- mark target run cancelling/cancelled with idempotency key
|
||||
|
||||
No partial success is allowed across these boundaries.
|
||||
|
||||
### Per-session actor model
|
||||
|
||||
`AcpSessionManager` runs one actor per ACP session key:
|
||||
|
||||
- actor mailbox serializes `submit`, `cancel`, `close`, and `stream` side effects
|
||||
- actor owns runtime handle hydration and runtime adapter process lifecycle for that session
|
||||
- actor writes run events in-order (`seq`) before any Discord delivery
|
||||
- actor updates delivery checkpoints after successful outbound send
|
||||
|
||||
This removes cross-turn races and prevents duplicate or out-of-order thread output.
|
||||
|
||||
### Idempotency and delivery projection
|
||||
|
||||
All external ACP actions must carry idempotency keys:
|
||||
|
||||
- spawn idempotency key
|
||||
- prompt/steer idempotency key
|
||||
- cancel idempotency key
|
||||
- close idempotency key
|
||||
|
||||
Delivery rules:
|
||||
|
||||
- Discord messages are derived from `acp_events` plus `acp_delivery_checkpoint`
|
||||
- retries resume from checkpoint without re-sending already delivered chunks
|
||||
- final reply emission is exactly-once per run from projection logic
|
||||
|
||||
### Recovery and self-healing
|
||||
|
||||
On gateway start:
|
||||
|
||||
- load non-terminal ACP sessions (`creating`, `idle`, `running`, `cancelling`, `error`)
|
||||
- recreate actors lazily on first inbound event or eagerly under configured cap
|
||||
- reconcile any `running` runs missing heartbeats and mark `failed` or recover via adapter
|
||||
|
||||
On inbound Discord thread message:
|
||||
|
||||
- if binding exists but ACP session is missing, fail closed with explicit stale-binding message
|
||||
- optionally auto-unbind stale binding after operator-safe validation
|
||||
- never silently route stale ACP bindings to normal LLM path
|
||||
|
||||
### Lifecycle and safety
|
||||
|
||||
Supported operations:
|
||||
|
||||
- cancel current run: `/acp cancel`
|
||||
- unbind thread: `/unfocus`
|
||||
- close ACP session: `/acp close`
|
||||
- auto close idle sessions by effective TTL
|
||||
|
||||
TTL policy:
|
||||
|
||||
- effective TTL is minimum of
|
||||
- global/session TTL
|
||||
- Discord thread binding TTL
|
||||
- ACP runtime owner TTL
|
||||
|
||||
Safety controls:
|
||||
|
||||
- allowlist ACP agents by name
|
||||
- restrict workspace roots for ACP sessions
|
||||
- env allowlist passthrough
|
||||
- max concurrent ACP sessions per account and globally
|
||||
- bounded restart backoff for runtime crashes
|
||||
|
||||
## Config surface
|
||||
|
||||
Core keys:
|
||||
|
||||
- `acp.enabled`
|
||||
- `acp.dispatch.enabled` (independent ACP routing kill switch)
|
||||
- `acp.backend` (default `acpx`)
|
||||
- `acp.defaultAgent`
|
||||
- `acp.allowedAgents[]`
|
||||
- `acp.maxConcurrentSessions`
|
||||
- `acp.stream.coalesceIdleMs`
|
||||
- `acp.stream.maxChunkChars`
|
||||
- `acp.runtime.ttlMinutes`
|
||||
- `acp.controlPlane.store` (`sqlite` default)
|
||||
- `acp.controlPlane.storePath`
|
||||
- `acp.controlPlane.recovery.eagerActors`
|
||||
- `acp.controlPlane.recovery.reconcileRunningAfterMs`
|
||||
- `acp.controlPlane.checkpoint.flushEveryEvents`
|
||||
- `acp.controlPlane.checkpoint.flushEveryMs`
|
||||
- `acp.idempotency.ttlHours`
|
||||
- `channels.discord.threadBindings.spawnAcpSessions`
|
||||
|
||||
Plugin/backend keys (acpx plugin section):
|
||||
|
||||
- backend command/path overrides
|
||||
- backend env allowlist
|
||||
- backend per-agent presets
|
||||
- backend startup/stop timeouts
|
||||
- backend max inflight runs per session
|
||||
|
||||
## Implementation specification
|
||||
|
||||
### Control-plane modules (new)
|
||||
|
||||
Add dedicated ACP control-plane modules in core:
|
||||
|
||||
- `src/acp/control-plane/manager.ts`
|
||||
- owns ACP actors, lifecycle transitions, command serialization
|
||||
- `src/acp/control-plane/store.ts`
|
||||
- SQLite schema management, transactions, query helpers
|
||||
- `src/acp/control-plane/events.ts`
|
||||
- typed ACP event definitions and serialization
|
||||
- `src/acp/control-plane/checkpoint.ts`
|
||||
- durable delivery checkpoints and replay cursors
|
||||
- `src/acp/control-plane/idempotency.ts`
|
||||
- idempotency key reservation and response replay
|
||||
- `src/acp/control-plane/recovery.ts`
|
||||
- boot-time reconciliation and actor rehydrate plan
|
||||
|
||||
Compatibility bridge modules:
|
||||
|
||||
- `src/acp/runtime/session-meta.ts`
|
||||
- remains temporarily for projection into `SessionEntry.acp`
|
||||
- must stop being source-of-truth after migration cutover
|
||||
|
||||
### Required invariants (must enforce in code)
|
||||
|
||||
- ACP session creation and thread bind are atomic (single transaction)
|
||||
- there is at most one active run per ACP session actor at a time
|
||||
- event `seq` is strictly increasing per run
|
||||
- delivery checkpoint never advances past last committed event
|
||||
- idempotency replay returns previous success payload for duplicate command keys
|
||||
- stale/missing ACP metadata cannot route into normal non-ACP reply path
|
||||
|
||||
### Core touchpoints
|
||||
|
||||
Core files to change:
|
||||
|
||||
- `src/auto-reply/reply/dispatch-from-config.ts`
|
||||
- ACP branch calls `AcpSessionManager.submit` and event-projection delivery
|
||||
- remove direct ACP fallback that bypasses control-plane invariants
|
||||
- `src/auto-reply/reply/inbound-context.ts` (or nearest normalized context boundary)
|
||||
- expose normalized routing keys and idempotency seeds for ACP control plane
|
||||
- `src/config/sessions/types.ts`
|
||||
- keep `SessionEntry.acp` as projection-only compatibility field
|
||||
- `src/gateway/server-methods/sessions.ts`
|
||||
- reset/delete/archive must call ACP manager close/unbind transaction path
|
||||
- `src/infra/outbound/bound-delivery-router.ts`
|
||||
- enforce fail-closed destination behavior for ACP bound session turns
|
||||
- `src/discord/monitor/thread-bindings.ts`
|
||||
- add ACP stale-binding validation helpers wired to control-plane lookups
|
||||
- `src/auto-reply/reply/commands-acp.ts`
|
||||
- route spawn/cancel/close/steer through ACP manager APIs
|
||||
- `src/agents/acp-spawn.ts`
|
||||
- stop ad-hoc metadata writes; call ACP manager spawn transaction
|
||||
- `src/plugin-sdk/**` and plugin runtime bridge
|
||||
- expose ACP backend registration and health semantics cleanly
|
||||
|
||||
Core files explicitly not replaced:
|
||||
|
||||
- `src/discord/monitor/message-handler.preflight.ts`
|
||||
- keep thread binding override behavior as the canonical session-key resolver
|
||||
|
||||
### ACP runtime registry API
|
||||
|
||||
Add a core registry module:
|
||||
|
||||
- `src/acp/runtime/registry.ts`
|
||||
|
||||
Required API:
|
||||
|
||||
```ts
|
||||
export type AcpRuntimeBackend = {
|
||||
id: string;
|
||||
runtime: AcpRuntime;
|
||||
healthy?: () => boolean;
|
||||
};
|
||||
|
||||
export function registerAcpRuntimeBackend(backend: AcpRuntimeBackend): void;
|
||||
export function unregisterAcpRuntimeBackend(id: string): void;
|
||||
export function getAcpRuntimeBackend(id?: string): AcpRuntimeBackend | null;
|
||||
export function requireAcpRuntimeBackend(id?: string): AcpRuntimeBackend;
|
||||
```
|
||||
|
||||
Behavior:
|
||||
|
||||
- `requireAcpRuntimeBackend` throws a typed ACP backend missing error when unavailable
|
||||
- plugin service registers backend on `start` and unregisters on `stop`
|
||||
- runtime lookups are read-only and process-local
|
||||
|
||||
### acpx runtime plugin contract (implementation detail)
|
||||
|
||||
For the first production backend (`extensions/acpx`), OpenClaw and acpx are
|
||||
connected with a strict command contract:
|
||||
|
||||
- backend id: `acpx`
|
||||
- plugin service id: `acpx-runtime`
|
||||
- runtime handle encoding: `runtimeSessionName = acpx:v1:<base64url(json)>`
|
||||
- encoded payload fields:
|
||||
- `name` (acpx named session; uses OpenClaw `sessionKey`)
|
||||
- `agent` (acpx agent command)
|
||||
- `cwd` (session workspace root)
|
||||
- `mode` (`persistent | oneshot`)
|
||||
|
||||
Command mapping:
|
||||
|
||||
- ensure session:
|
||||
- `acpx --format json --json-strict --cwd <cwd> <agent> sessions ensure --name <name>`
|
||||
- prompt turn:
|
||||
- `acpx --format json --json-strict --cwd <cwd> <agent> prompt --session <name> --file -`
|
||||
- cancel:
|
||||
- `acpx --format json --json-strict --cwd <cwd> <agent> cancel --session <name>`
|
||||
- close:
|
||||
- `acpx --format json --json-strict --cwd <cwd> <agent> sessions close <name>`
|
||||
|
||||
Streaming:
|
||||
|
||||
- OpenClaw consumes ndjson events from `acpx --format json --json-strict`
|
||||
- `text` => `text_delta/output`
|
||||
- `thought` => `text_delta/thought`
|
||||
- `tool_call` => `tool_call`
|
||||
- `done` => `done`
|
||||
- `error` => `error`
|
||||
|
||||
### Session schema patch
|
||||
|
||||
Patch `SessionEntry` in `src/config/sessions/types.ts`:
|
||||
|
||||
```ts
|
||||
type SessionAcpMeta = {
|
||||
backend: string;
|
||||
agent: string;
|
||||
runtimeSessionName: string;
|
||||
mode: "persistent" | "oneshot";
|
||||
cwd?: string;
|
||||
state: "idle" | "running" | "error";
|
||||
lastActivityAt: number;
|
||||
lastError?: string;
|
||||
};
|
||||
```
|
||||
|
||||
Persisted field:
|
||||
|
||||
- `SessionEntry.acp?: SessionAcpMeta`
|
||||
|
||||
Migration rules:
|
||||
|
||||
- phase A: dual-write (`acp` projection + ACP SQLite source-of-truth)
|
||||
- phase B: read-primary from ACP SQLite, fallback-read from legacy `SessionEntry.acp`
|
||||
- phase C: migration command backfills missing ACP rows from valid legacy entries
|
||||
- phase D: remove fallback-read and keep projection optional for UX only
|
||||
- legacy fields (`cliSessionIds`, `claudeCliSessionId`) remain untouched
|
||||
|
||||
### Error contract
|
||||
|
||||
Add stable ACP error codes and user-facing messages:
|
||||
|
||||
- `ACP_BACKEND_MISSING`
|
||||
- message: `ACP runtime backend is not configured. Install and enable the acpx runtime plugin.`
|
||||
- `ACP_BACKEND_UNAVAILABLE`
|
||||
- message: `ACP runtime backend is currently unavailable. Try again in a moment.`
|
||||
- `ACP_SESSION_INIT_FAILED`
|
||||
- message: `Could not initialize ACP session runtime.`
|
||||
- `ACP_TURN_FAILED`
|
||||
- message: `ACP turn failed before completion.`
|
||||
|
||||
Rules:
|
||||
|
||||
- return actionable user-safe message in-thread
|
||||
- log detailed backend/system error only in runtime logs
|
||||
- never silently fall back to normal LLM path when ACP routing was explicitly selected
|
||||
|
||||
### Duplicate delivery arbitration
|
||||
|
||||
Single routing rule for ACP bound turns:
|
||||
|
||||
- if an active thread binding exists for the target ACP session and requester context, deliver only to that bound thread
|
||||
- do not also send to parent channel for the same turn
|
||||
- if bound destination selection is ambiguous, fail closed with explicit error (no implicit parent fallback)
|
||||
- if no active binding exists, use normal session destination behavior
|
||||
|
||||
### Observability and operational readiness
|
||||
|
||||
Required metrics:
|
||||
|
||||
- ACP spawn success/failure count by backend and error code
|
||||
- ACP run latency percentiles (queue wait, runtime turn time, delivery projection time)
|
||||
- ACP actor restart count and restart reason
|
||||
- stale-binding detection count
|
||||
- idempotency replay hit rate
|
||||
- Discord delivery retry and rate-limit counters
|
||||
|
||||
Required logs:
|
||||
|
||||
- structured logs keyed by `sessionKey`, `runId`, `backend`, `threadId`, `idempotencyKey`
|
||||
- explicit state transition logs for session and run state machines
|
||||
- adapter command logs with redaction-safe arguments and exit summary
|
||||
|
||||
Required diagnostics:
|
||||
|
||||
- `/acp sessions` includes state, active run, last error, and binding status
|
||||
- `/acp doctor` (or equivalent) validates backend registration, store health, and stale bindings
|
||||
|
||||
### Config precedence and effective values
|
||||
|
||||
ACP enablement precedence:
|
||||
|
||||
- account override: `channels.discord.accounts.<id>.threadBindings.spawnAcpSessions`
|
||||
- channel override: `channels.discord.threadBindings.spawnAcpSessions`
|
||||
- global ACP gate: `acp.enabled`
|
||||
- dispatch gate: `acp.dispatch.enabled`
|
||||
- backend availability: registered backend for `acp.backend`
|
||||
|
||||
Auto-enable behavior:
|
||||
|
||||
- when ACP is configured (`acp.enabled=true`, `acp.dispatch.enabled=true`, or
|
||||
`acp.backend=acpx`), plugin auto-enable marks `plugins.entries.acpx.enabled=true`
|
||||
unless denylisted or explicitly disabled
|
||||
|
||||
TTL effective value:
|
||||
|
||||
- `min(session ttl, discord thread binding ttl, acp runtime ttl)`
|
||||
|
||||
### Test map
|
||||
|
||||
Unit tests:
|
||||
|
||||
- `src/acp/runtime/registry.test.ts` (new)
|
||||
- `src/auto-reply/reply/dispatch-from-config.acp.test.ts` (new)
|
||||
- `src/infra/outbound/bound-delivery-router.test.ts` (extend ACP fail-closed cases)
|
||||
- `src/config/sessions/types.test.ts` or nearest session-store tests (ACP metadata persistence)
|
||||
|
||||
Integration tests:
|
||||
|
||||
- `src/discord/monitor/reply-delivery.test.ts` (bound ACP delivery target behavior)
|
||||
- `src/discord/monitor/message-handler.preflight*.test.ts` (bound ACP session-key routing continuity)
|
||||
- acpx plugin runtime tests in backend package (service register/start/stop + event normalization)
|
||||
|
||||
Gateway e2e tests:
|
||||
|
||||
- `src/gateway/server.sessions.gateway-server-sessions-a.e2e.test.ts` (extend ACP reset/delete lifecycle coverage)
|
||||
- ACP thread turn roundtrip e2e for spawn, message, stream, cancel, unfocus, restart recovery
|
||||
|
||||
### Rollout guard
|
||||
|
||||
Add independent ACP dispatch kill switch:
|
||||
|
||||
- `acp.dispatch.enabled` default `false` for first release
|
||||
- when disabled:
|
||||
- ACP spawn/focus control commands may still bind sessions
|
||||
- ACP dispatch path does not activate
|
||||
- user receives explicit message that ACP dispatch is disabled by policy
|
||||
- after canary validation, default can be flipped to `true` in a later release
|
||||
|
||||
## Command and UX plan
|
||||
|
||||
### New commands
|
||||
|
||||
- `/acp spawn <agent-id> [--mode persistent|oneshot] [--thread auto|here|off]`
|
||||
- `/acp cancel [session]`
|
||||
- `/acp steer <instruction>`
|
||||
- `/acp close [session]`
|
||||
- `/acp sessions`
|
||||
|
||||
### Existing command compatibility
|
||||
|
||||
- `/focus <sessionKey>` continues to support ACP targets
|
||||
- `/unfocus` keeps current semantics
|
||||
- `/session idle` and `/session max-age` replace the old TTL override
|
||||
|
||||
## Phased rollout
|
||||
|
||||
### Phase 0 ADR and schema freeze
|
||||
|
||||
- ship ADR for ACP control-plane ownership and adapter boundaries
|
||||
- freeze DB schema (`acp_sessions`, `acp_runs`, `acp_bindings`, `acp_events`, `acp_delivery_checkpoint`, `acp_idempotency`)
|
||||
- define stable ACP error codes, event contract, and state-transition guards
|
||||
|
||||
### Phase 1 Control-plane foundation in core
|
||||
|
||||
- implement `AcpSessionManager` and per-session actor runtime
|
||||
- implement ACP SQLite store and transaction helpers
|
||||
- implement idempotency store and replay helpers
|
||||
- implement event append + delivery checkpoint modules
|
||||
- wire spawn/cancel/close APIs to manager with transactional guarantees
|
||||
|
||||
### Phase 2 Core routing and lifecycle integration
|
||||
|
||||
- route thread-bound ACP turns from dispatch pipeline into ACP manager
|
||||
- enforce fail-closed routing when ACP binding/session invariants fail
|
||||
- integrate reset/delete/archive/unfocus lifecycle with ACP close/unbind transactions
|
||||
- add stale-binding detection and optional auto-unbind policy
|
||||
|
||||
### Phase 3 acpx backend adapter/plugin
|
||||
|
||||
- implement `acpx` adapter against runtime contract (`ensureSession`, `submit`, `stream`, `cancel`, `close`)
|
||||
- add backend health checks and startup/teardown registration
|
||||
- normalize acpx ndjson events into ACP runtime events
|
||||
- enforce backend timeouts, process supervision, and restart/backoff policy
|
||||
|
||||
### Phase 4 Delivery projection and channel UX (Discord first)
|
||||
|
||||
- implement event-driven channel projection with checkpoint resume (Discord first)
|
||||
- coalesce streaming chunks with rate-limit aware flush policy
|
||||
- guarantee exactly-once final completion message per run
|
||||
- ship `/acp spawn`, `/acp cancel`, `/acp steer`, `/acp close`, `/acp sessions`
|
||||
|
||||
### Phase 5 Migration and cutover
|
||||
|
||||
- introduce dual-write to `SessionEntry.acp` projection plus ACP SQLite source-of-truth
|
||||
- add migration utility for legacy ACP metadata rows
|
||||
- flip read path to ACP SQLite primary
|
||||
- remove legacy fallback routing that depends on missing `SessionEntry.acp`
|
||||
|
||||
### Phase 6 Hardening, SLOs, and scale limits
|
||||
|
||||
- enforce concurrency limits (global/account/session), queue policies, and timeout budgets
|
||||
- add full telemetry, dashboards, and alert thresholds
|
||||
- chaos-test crash recovery and duplicate-delivery suppression
|
||||
- publish runbook for backend outage, DB corruption, and stale-binding remediation
|
||||
|
||||
### Full implementation checklist
|
||||
|
||||
- core control-plane modules and tests
|
||||
- DB migrations and rollback plan
|
||||
- ACP manager API integration across dispatch and commands
|
||||
- adapter registration interface in plugin runtime bridge
|
||||
- acpx adapter implementation and tests
|
||||
- thread-capable channel delivery projection logic with checkpoint replay (Discord first)
|
||||
- lifecycle hooks for reset/delete/archive/unfocus
|
||||
- stale-binding detector and operator-facing diagnostics
|
||||
- config validation and precedence tests for all new ACP keys
|
||||
- operational docs and troubleshooting runbook
|
||||
|
||||
## Test plan
|
||||
|
||||
Unit tests:
|
||||
|
||||
- ACP DB transaction boundaries (spawn/bind/enqueue atomicity, cancel, close)
|
||||
- ACP state-machine transition guards for sessions and runs
|
||||
- idempotency reservation/replay semantics across all ACP commands
|
||||
- per-session actor serialization and queue ordering
|
||||
- acpx event parser and chunk coalescer
|
||||
- runtime supervisor restart and backoff policy
|
||||
- config precedence and effective TTL calculation
|
||||
- core ACP routing branch selection and fail-closed behavior when backend/session is invalid
|
||||
|
||||
Integration tests:
|
||||
|
||||
- fake ACP adapter process for deterministic streaming and cancel behavior
|
||||
- ACP manager + dispatch integration with transactional persistence
|
||||
- thread-bound inbound routing to ACP session key
|
||||
- thread-bound outbound delivery suppresses parent channel duplication
|
||||
- checkpoint replay recovers after delivery failure and resumes from last event
|
||||
- plugin service registration and teardown of ACP runtime backend
|
||||
|
||||
Gateway e2e tests:
|
||||
|
||||
- spawn ACP with thread, exchange multi-turn prompts, unfocus
|
||||
- gateway restart with persisted ACP DB and bindings, then continue same session
|
||||
- concurrent ACP sessions in multiple threads have no cross-talk
|
||||
- duplicate command retries (same idempotency key) do not create duplicate runs or replies
|
||||
- stale-binding scenario yields explicit error and optional auto-clean behavior
|
||||
|
||||
## Risks and mitigations
|
||||
|
||||
- Duplicate deliveries during transition
|
||||
- Mitigation: single destination resolver and idempotent event checkpoint
|
||||
- Runtime process churn under load
|
||||
- Mitigation: long lived per session owners + concurrency caps + backoff
|
||||
- Plugin absent or misconfigured
|
||||
- Mitigation: explicit operator-facing error and fail-closed ACP routing (no implicit fallback to normal session path)
|
||||
- Config confusion between subagent and ACP gates
|
||||
- Mitigation: explicit ACP keys and command feedback that includes effective policy source
|
||||
- Control-plane store corruption or migration bugs
|
||||
- Mitigation: WAL mode, backup/restore hooks, migration smoke tests, and read-only fallback diagnostics
|
||||
- Actor deadlocks or mailbox starvation
|
||||
- Mitigation: watchdog timers, actor health probes, and bounded mailbox depth with rejection telemetry
|
||||
|
||||
## Acceptance checklist
|
||||
|
||||
- ACP session spawn can create or bind a thread in a supported channel adapter (currently Discord)
|
||||
- all thread messages route to bound ACP session only
|
||||
- ACP outputs appear in the same thread identity with streaming or batches
|
||||
- no duplicate output in parent channel for bound turns
|
||||
- spawn+bind+initial enqueue are atomic in persistent store
|
||||
- ACP command retries are idempotent and do not duplicate runs or outputs
|
||||
- cancel, close, unfocus, archive, reset, and delete perform deterministic cleanup
|
||||
- crash restart preserves mapping and resumes multi turn continuity
|
||||
- concurrent thread bound ACP sessions work independently
|
||||
- ACP backend missing state produces clear actionable error
|
||||
- stale bindings are detected and surfaced explicitly (with optional safe auto-clean)
|
||||
- control-plane metrics and diagnostics are available for operators
|
||||
- new unit, integration, and e2e coverage passes
|
||||
|
||||
## Addendum: targeted refactors for current implementation (status)
|
||||
|
||||
These are non-blocking follow-ups to keep the ACP path maintainable after the current feature set lands.
|
||||
|
||||
### 1) Centralize ACP dispatch policy evaluation (completed)
|
||||
|
||||
- implemented via shared ACP policy helpers in `src/acp/policy.ts`
|
||||
- dispatch, ACP command lifecycle handlers, and ACP spawn path now consume shared policy logic
|
||||
|
||||
### 2) Split ACP command handler by subcommand domain (completed)
|
||||
|
||||
- `src/auto-reply/reply/commands-acp.ts` is now a thin router
|
||||
- subcommand behavior is split into:
|
||||
- `src/auto-reply/reply/commands-acp/lifecycle.ts`
|
||||
- `src/auto-reply/reply/commands-acp/runtime-options.ts`
|
||||
- `src/auto-reply/reply/commands-acp/diagnostics.ts`
|
||||
- shared helpers in `src/auto-reply/reply/commands-acp/shared.ts`
|
||||
|
||||
### 3) Split ACP session manager by responsibility (completed)
|
||||
|
||||
- manager is split into:
|
||||
- `src/acp/control-plane/manager.ts` (public facade + singleton)
|
||||
- `src/acp/control-plane/manager.core.ts` (manager implementation)
|
||||
- `src/acp/control-plane/manager.types.ts` (manager types/deps)
|
||||
- `src/acp/control-plane/manager.utils.ts` (normalization + helper functions)
|
||||
|
||||
### 4) Optional acpx runtime adapter cleanup
|
||||
|
||||
- `extensions/acpx/src/runtime.ts` can be split into:
|
||||
- process execution/supervision
|
||||
- ndjson event parsing/normalization
|
||||
- runtime API surface (`submit`, `cancel`, `close`, etc.)
|
||||
- improves testability and makes backend behavior easier to audit
|
||||
@@ -1,96 +0,0 @@
|
||||
---
|
||||
summary: "Holy grail refactor plan for one unified runtime streaming pipeline across main, subagent, and ACP"
|
||||
owner: "onutc"
|
||||
status: "draft"
|
||||
last_updated: "2026-02-25"
|
||||
title: "Unified Runtime Streaming Refactor Plan"
|
||||
---
|
||||
|
||||
# Unified Runtime Streaming Refactor Plan
|
||||
|
||||
## Objective
|
||||
|
||||
Deliver one shared streaming pipeline for `main`, `subagent`, and `acp` so all runtimes get identical coalescing, chunking, delivery ordering, and crash recovery behavior.
|
||||
|
||||
## Why this exists
|
||||
|
||||
- Current behavior is split across multiple runtime-specific shaping paths.
|
||||
- Formatting/coalescing bugs can be fixed in one path but remain in others.
|
||||
- Delivery consistency, duplicate suppression, and recovery semantics are harder to reason about.
|
||||
|
||||
## Target architecture
|
||||
|
||||
Single pipeline, runtime-specific adapters:
|
||||
|
||||
1. Runtime adapters emit canonical events only.
|
||||
2. Shared stream assembler coalesces and finalizes text/tool/status events.
|
||||
3. Shared channel projector applies channel-specific chunking/formatting once.
|
||||
4. Shared delivery ledger enforces idempotent send/replay semantics.
|
||||
5. Outbound channel adapter executes sends and records delivery checkpoints.
|
||||
|
||||
Canonical event contract:
|
||||
|
||||
- `turn_started`
|
||||
- `text_delta`
|
||||
- `block_final`
|
||||
- `tool_started`
|
||||
- `tool_finished`
|
||||
- `status`
|
||||
- `turn_completed`
|
||||
- `turn_failed`
|
||||
- `turn_cancelled`
|
||||
|
||||
## Workstreams
|
||||
|
||||
### 1) Canonical streaming contract
|
||||
|
||||
- Define strict event schema + validation in core.
|
||||
- Add adapter contract tests to guarantee each runtime emits compatible events.
|
||||
- Reject malformed runtime events early and surface structured diagnostics.
|
||||
|
||||
### 2) Shared stream processor
|
||||
|
||||
- Replace runtime-specific coalescer/projector logic with one processor.
|
||||
- Processor owns text delta buffering, idle flush, max-chunk splitting, and completion flush.
|
||||
- Move ACP/main/subagent config resolution into one helper to prevent drift.
|
||||
|
||||
### 3) Shared channel projection
|
||||
|
||||
- Keep channel adapters dumb: accept finalized blocks and send.
|
||||
- Move Discord-specific chunking quirks to channel projector only.
|
||||
- Keep pipeline channel-agnostic before projection.
|
||||
|
||||
### 4) Delivery ledger + replay
|
||||
|
||||
- Add per-turn/per-chunk delivery IDs.
|
||||
- Record checkpoints before and after physical send.
|
||||
- On restart, replay pending chunks idempotently and avoid duplicates.
|
||||
|
||||
### 5) Migration and cutover
|
||||
|
||||
- Phase 1: shadow mode (new pipeline computes output but old path sends; compare).
|
||||
- Phase 2: runtime-by-runtime cutover (`acp`, then `subagent`, then `main` or reverse by risk).
|
||||
- Phase 3: delete legacy runtime-specific streaming code.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- No changes to ACP policy/permissions model in this refactor.
|
||||
- No channel-specific feature expansion outside projection compatibility fixes.
|
||||
- No transport/backend redesign (acpx plugin contract remains as-is unless needed for event parity).
|
||||
|
||||
## Risks and mitigations
|
||||
|
||||
- Risk: behavioral regressions in existing main/subagent paths.
|
||||
Mitigation: shadow mode diffing + adapter contract tests + channel e2e tests.
|
||||
- Risk: duplicate sends during crash recovery.
|
||||
Mitigation: durable delivery IDs + idempotent replay in delivery adapter.
|
||||
- Risk: runtime adapters diverge again.
|
||||
Mitigation: required shared contract test suite for all adapters.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- All runtimes pass shared streaming contract tests.
|
||||
- Discord ACP/main/subagent produce equivalent spacing/chunking behavior for tiny deltas.
|
||||
- Crash/restart replay sends no duplicate chunk for the same delivery ID.
|
||||
- Legacy ACP projector/coalescer path is removed.
|
||||
- Streaming config resolution is shared and runtime-independent.
|
||||
@@ -1,232 +0,0 @@
|
||||
---
|
||||
summary: "Plan: isolate browser act:evaluate from Playwright queue using CDP, with end-to-end deadlines and safer ref resolution"
|
||||
read_when:
|
||||
- Working on browser `act:evaluate` timeout, abort, or queue blocking issues
|
||||
- Planning CDP based isolation for evaluate execution
|
||||
owner: "openclaw"
|
||||
status: "draft"
|
||||
last_updated: "2026-02-10"
|
||||
title: "Browser Evaluate CDP Refactor"
|
||||
---
|
||||
|
||||
# Browser Evaluate CDP Refactor Plan
|
||||
|
||||
## Context
|
||||
|
||||
`act:evaluate` executes user provided JavaScript in the page. Today it runs via Playwright
|
||||
(`page.evaluate` or `locator.evaluate`). Playwright serializes CDP commands per page, so a
|
||||
stuck or long running evaluate can block the page command queue and make every later action
|
||||
on that tab look "stuck".
|
||||
|
||||
PR #13498 adds a pragmatic safety net (bounded evaluate, abort propagation, and best-effort
|
||||
recovery). This document describes a larger refactor that makes `act:evaluate` inherently
|
||||
isolated from Playwright so a stuck evaluate cannot wedge normal Playwright operations.
|
||||
|
||||
## Goals
|
||||
|
||||
- `act:evaluate` cannot permanently block later browser actions on the same tab.
|
||||
- Timeouts are single source of truth end to end so a caller can rely on a budget.
|
||||
- Abort and timeout are treated the same way across HTTP and in-process dispatch.
|
||||
- Element targeting for evaluate is supported without switching everything off Playwright.
|
||||
- Maintain backward compatibility for existing callers and payloads.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Replace all browser actions (click, type, wait, etc.) with CDP implementations.
|
||||
- Remove the existing safety net introduced in PR #13498 (it remains a useful fallback).
|
||||
- Introduce new unsafe capabilities beyond the existing `browser.evaluateEnabled` gate.
|
||||
- Add process isolation (worker process/thread) for evaluate. If we still see hard to recover
|
||||
stuck states after this refactor, that is a follow-up idea.
|
||||
|
||||
## Current Architecture (Why It Gets Stuck)
|
||||
|
||||
At a high level:
|
||||
|
||||
- Callers send `act:evaluate` to the browser control service.
|
||||
- The route handler calls into Playwright to execute the JavaScript.
|
||||
- Playwright serializes page commands, so an evaluate that never finishes blocks the queue.
|
||||
- A stuck queue means later click/type/wait operations on the tab can appear to hang.
|
||||
|
||||
## Proposed Architecture
|
||||
|
||||
### 1. Deadline Propagation
|
||||
|
||||
Introduce a single budget concept and derive everything from it:
|
||||
|
||||
- Caller sets `timeoutMs` (or a deadline in the future).
|
||||
- The outer request timeout, route handler logic, and the execution budget inside the page
|
||||
all use the same budget, with small headroom where needed for serialization overhead.
|
||||
- Abort is propagated as an `AbortSignal` everywhere so cancellation is consistent.
|
||||
|
||||
Implementation direction:
|
||||
|
||||
- Add a small helper (for example `createBudget({ timeoutMs, signal })`) that returns:
|
||||
- `signal`: the linked AbortSignal
|
||||
- `deadlineAtMs`: absolute deadline
|
||||
- `remainingMs()`: remaining budget for child operations
|
||||
- Use this helper in:
|
||||
- `src/browser/client-fetch.ts` (HTTP and in-process dispatch)
|
||||
- `src/node-host/runner.ts` (proxy path)
|
||||
- browser action implementations (Playwright and CDP)
|
||||
|
||||
### 2. Separate Evaluate Engine (CDP Path)
|
||||
|
||||
Add a CDP based evaluate implementation that does not share Playwright's per page command
|
||||
queue. The key property is that the evaluate transport is a separate WebSocket connection
|
||||
and a separate CDP session attached to the target.
|
||||
|
||||
Implementation direction:
|
||||
|
||||
- New module, for example `src/browser/cdp-evaluate.ts`, that:
|
||||
- Connects to the configured CDP endpoint (browser level socket).
|
||||
- Uses `Target.attachToTarget({ targetId, flatten: true })` to get a `sessionId`.
|
||||
- Runs either:
|
||||
- `Runtime.evaluate` for page level evaluate, or
|
||||
- `DOM.resolveNode` plus `Runtime.callFunctionOn` for element evaluate.
|
||||
- On timeout or abort:
|
||||
- Sends `Runtime.terminateExecution` best-effort for the session.
|
||||
- Closes the WebSocket and returns a clear error.
|
||||
|
||||
Notes:
|
||||
|
||||
- This still executes JavaScript in the page, so termination can have side effects. The win
|
||||
is that it does not wedge the Playwright queue, and it is cancelable at the transport
|
||||
layer by killing the CDP session.
|
||||
|
||||
### 3. Ref Story (Element Targeting Without A Full Rewrite)
|
||||
|
||||
The hard part is element targeting. CDP needs a DOM handle or `backendDOMNodeId`, while
|
||||
today most browser actions use Playwright locators based on refs from snapshots.
|
||||
|
||||
Recommended approach: keep existing refs, but attach an optional CDP resolvable id.
|
||||
|
||||
#### 3.1 Extend Stored Ref Info
|
||||
|
||||
Extend the stored role ref metadata to optionally include a CDP id:
|
||||
|
||||
- Today: `{ role, name, nth }`
|
||||
- Proposed: `{ role, name, nth, backendDOMNodeId?: number }`
|
||||
|
||||
This keeps all existing Playwright based actions working and allows CDP evaluate to accept
|
||||
the same `ref` value when the `backendDOMNodeId` is available.
|
||||
|
||||
#### 3.2 Populate backendDOMNodeId At Snapshot Time
|
||||
|
||||
When producing a role snapshot:
|
||||
|
||||
1. Generate the existing role ref map as today (role, name, nth).
|
||||
2. Fetch the AX tree via CDP (`Accessibility.getFullAXTree`) and compute a parallel map of
|
||||
`(role, name, nth) -> backendDOMNodeId` using the same duplicate handling rules.
|
||||
3. Merge the id back into the stored ref info for the current tab.
|
||||
|
||||
If mapping fails for a ref, leave `backendDOMNodeId` undefined. This makes the feature
|
||||
best-effort and safe to roll out.
|
||||
|
||||
#### 3.3 Evaluate Behavior With Ref
|
||||
|
||||
In `act:evaluate`:
|
||||
|
||||
- If `ref` is present and has `backendDOMNodeId`, run element evaluate via CDP.
|
||||
- If `ref` is present but has no `backendDOMNodeId`, fall back to the Playwright path (with
|
||||
the safety net).
|
||||
|
||||
Optional escape hatch:
|
||||
|
||||
- Extend the request shape to accept `backendDOMNodeId` directly for advanced callers (and
|
||||
for debugging), while keeping `ref` as the primary interface.
|
||||
|
||||
### 4. Keep A Last Resort Recovery Path
|
||||
|
||||
Even with CDP evaluate, there are other ways to wedge a tab or a connection. Keep the
|
||||
existing recovery mechanisms (terminate execution + disconnect Playwright) as a last resort
|
||||
for:
|
||||
|
||||
- legacy callers
|
||||
- environments where CDP attach is blocked
|
||||
- unexpected Playwright edge cases
|
||||
|
||||
## Implementation Plan (Single Iteration)
|
||||
|
||||
### Deliverables
|
||||
|
||||
- A CDP based evaluate engine that runs outside the Playwright per-page command queue.
|
||||
- A single end-to-end timeout/abort budget used consistently by callers and handlers.
|
||||
- Ref metadata that can optionally carry `backendDOMNodeId` for element evaluate.
|
||||
- `act:evaluate` prefers the CDP engine when possible and falls back to Playwright when not.
|
||||
- Tests that prove a stuck evaluate does not wedge later actions.
|
||||
- Logs/metrics that make failures and fallbacks visible.
|
||||
|
||||
### Implementation Checklist
|
||||
|
||||
1. Add a shared "budget" helper to link `timeoutMs` + upstream `AbortSignal` into:
|
||||
- a single `AbortSignal`
|
||||
- an absolute deadline
|
||||
- a `remainingMs()` helper for downstream operations
|
||||
2. Update all caller paths to use that helper so `timeoutMs` means the same thing everywhere:
|
||||
- `src/browser/client-fetch.ts` (HTTP and in-process dispatch)
|
||||
- `src/node-host/runner.ts` (node proxy path)
|
||||
- CLI wrappers that call `/act` (add `--timeout-ms` to `browser evaluate`)
|
||||
3. Implement `src/browser/cdp-evaluate.ts`:
|
||||
- connect to the browser-level CDP socket
|
||||
- `Target.attachToTarget` to get a `sessionId`
|
||||
- run `Runtime.evaluate` for page evaluate
|
||||
- run `DOM.resolveNode` + `Runtime.callFunctionOn` for element evaluate
|
||||
- on timeout/abort: best-effort `Runtime.terminateExecution` then close the socket
|
||||
4. Extend stored role ref metadata to optionally include `backendDOMNodeId`:
|
||||
- keep existing `{ role, name, nth }` behavior for Playwright actions
|
||||
- add `backendDOMNodeId?: number` for CDP element targeting
|
||||
5. Populate `backendDOMNodeId` during snapshot creation (best-effort):
|
||||
- fetch AX tree via CDP (`Accessibility.getFullAXTree`)
|
||||
- compute `(role, name, nth) -> backendDOMNodeId` and merge into the stored ref map
|
||||
- if mapping is ambiguous or missing, leave the id undefined
|
||||
6. Update `act:evaluate` routing:
|
||||
- if no `ref`: always use CDP evaluate
|
||||
- if `ref` resolves to a `backendDOMNodeId`: use CDP element evaluate
|
||||
- otherwise: fall back to Playwright evaluate (still bounded and abortable)
|
||||
7. Keep the existing "last resort" recovery path as a fallback, not the default path.
|
||||
8. Add tests:
|
||||
- stuck evaluate times out within budget and the next click/type succeeds
|
||||
- abort cancels evaluate (client disconnect or timeout) and unblocks subsequent actions
|
||||
- mapping failures cleanly fall back to Playwright
|
||||
9. Add observability:
|
||||
- evaluate duration and timeout counters
|
||||
- terminateExecution usage
|
||||
- fallback rate (CDP -> Playwright) and reasons
|
||||
|
||||
### Acceptance Criteria
|
||||
|
||||
- A deliberately hung `act:evaluate` returns within the caller budget and does not wedge the
|
||||
tab for later actions.
|
||||
- `timeoutMs` behaves consistently across CLI, agent tool, node proxy, and in-process calls.
|
||||
- If `ref` can be mapped to `backendDOMNodeId`, element evaluate uses CDP; otherwise the
|
||||
fallback path is still bounded and recoverable.
|
||||
|
||||
## Testing Plan
|
||||
|
||||
- Unit tests:
|
||||
- `(role, name, nth)` matching logic between role refs and AX tree nodes.
|
||||
- Budget helper behavior (headroom, remaining time math).
|
||||
- Integration tests:
|
||||
- CDP evaluate timeout returns within budget and does not block the next action.
|
||||
- Abort cancels evaluate and triggers termination best-effort.
|
||||
- Contract tests:
|
||||
- Ensure `BrowserActRequest` and `BrowserActResponse` remain compatible.
|
||||
|
||||
## Risks And Mitigations
|
||||
|
||||
- Mapping is imperfect:
|
||||
- Mitigation: best-effort mapping, fallback to Playwright evaluate, and add debug tooling.
|
||||
- `Runtime.terminateExecution` has side effects:
|
||||
- Mitigation: only use on timeout/abort and document the behavior in errors.
|
||||
- Extra overhead:
|
||||
- Mitigation: only fetch AX tree when snapshots are requested, cache per target, and keep
|
||||
CDP session short lived.
|
||||
- Extension relay limitations:
|
||||
- Mitigation: use browser level attach APIs when per page sockets are not available, and
|
||||
keep the current Playwright path as fallback.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- Should the new engine be configurable as `playwright`, `cdp`, or `auto`?
|
||||
- Do we want to expose a new "nodeRef" format for advanced users, or keep `ref` only?
|
||||
- How should frame snapshots and selector scoped snapshots participate in AX mapping?
|
||||
@@ -1,337 +0,0 @@
|
||||
---
|
||||
summary: "Status and next steps for decoupling Discord gateway listeners from long-running agent turns with a Discord-specific inbound worker"
|
||||
owner: "openclaw"
|
||||
status: "in_progress"
|
||||
last_updated: "2026-03-05"
|
||||
title: "Discord Async Inbound Worker Plan"
|
||||
---
|
||||
|
||||
# Discord Async Inbound Worker Plan
|
||||
|
||||
## Objective
|
||||
|
||||
Remove Discord listener timeout as a user-facing failure mode by making inbound Discord turns asynchronous:
|
||||
|
||||
1. Gateway listener accepts and normalizes inbound events quickly.
|
||||
2. A Discord run queue stores serialized jobs keyed by the same ordering boundary we use today.
|
||||
3. A worker executes the actual agent turn outside the Carbon listener lifetime.
|
||||
4. Replies are delivered back to the originating channel or thread after the run completes.
|
||||
|
||||
This is the long-term fix for queued Discord runs timing out at `channels.discord.eventQueue.listenerTimeout` while the agent run itself is still making progress.
|
||||
|
||||
## Current status
|
||||
|
||||
This plan is partially implemented.
|
||||
|
||||
Already done:
|
||||
|
||||
- Discord listener timeout and Discord run timeout are now separate settings.
|
||||
- Accepted inbound Discord turns are enqueued into `src/discord/monitor/inbound-worker.ts`.
|
||||
- The worker now owns the long-running turn instead of the Carbon listener.
|
||||
- Existing per-route ordering is preserved by queue key.
|
||||
- Timeout regression coverage exists for the Discord worker path.
|
||||
|
||||
What this means in plain language:
|
||||
|
||||
- the production timeout bug is fixed
|
||||
- the long-running turn no longer dies just because the Discord listener budget expires
|
||||
- the worker architecture is not finished yet
|
||||
|
||||
What is still missing:
|
||||
|
||||
- `DiscordInboundJob` is still only partially normalized and still carries live runtime references
|
||||
- command semantics (`stop`, `new`, `reset`, future session controls) are not yet fully worker-native
|
||||
- worker observability and operator status are still minimal
|
||||
- there is still no restart durability
|
||||
|
||||
## Why this exists
|
||||
|
||||
Current behavior ties the full agent turn to the listener lifetime:
|
||||
|
||||
- `src/discord/monitor/listeners.ts` applies the timeout and abort boundary.
|
||||
- `src/discord/monitor/message-handler.ts` keeps the queued run inside that boundary.
|
||||
- `src/discord/monitor/message-handler.process.ts` performs media loading, routing, dispatch, typing, draft streaming, and final reply delivery inline.
|
||||
|
||||
That architecture has two bad properties:
|
||||
|
||||
- long but healthy turns can be aborted by the listener watchdog
|
||||
- users can see no reply even when the downstream runtime would have produced one
|
||||
|
||||
Raising the timeout helps but does not change the failure mode.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Do not redesign non-Discord channels in this pass.
|
||||
- Do not broaden this into a generic all-channel worker framework in the first implementation.
|
||||
- Do not extract a shared cross-channel inbound worker abstraction yet; only share low-level primitives when duplication is obvious.
|
||||
- Do not add durable crash recovery in the first pass unless needed to land safely.
|
||||
- Do not change route selection, binding semantics, or ACP policy in this plan.
|
||||
|
||||
## Current constraints
|
||||
|
||||
The current Discord processing path still depends on some live runtime objects that should not stay inside the long-term job payload:
|
||||
|
||||
- Carbon `Client`
|
||||
- raw Discord event shapes
|
||||
- in-memory guild history map
|
||||
- thread binding manager callbacks
|
||||
- live typing and draft stream state
|
||||
|
||||
We already moved execution onto a worker queue, but the normalization boundary is still incomplete. Right now the worker is "run later in the same process with some of the same live objects," not a fully data-only job boundary.
|
||||
|
||||
## Target architecture
|
||||
|
||||
### 1. Listener stage
|
||||
|
||||
`DiscordMessageListener` remains the ingress point, but its job becomes:
|
||||
|
||||
- run preflight and policy checks
|
||||
- normalize accepted input into a serializable `DiscordInboundJob`
|
||||
- enqueue the job into a per-session or per-channel async queue
|
||||
- return immediately to Carbon once the enqueue succeeds
|
||||
|
||||
The listener should no longer own the end-to-end LLM turn lifetime.
|
||||
|
||||
### 2. Normalized job payload
|
||||
|
||||
Introduce a serializable job descriptor that contains only the data needed to run the turn later.
|
||||
|
||||
Minimum shape:
|
||||
|
||||
- route identity
|
||||
- `agentId`
|
||||
- `sessionKey`
|
||||
- `accountId`
|
||||
- `channel`
|
||||
- delivery identity
|
||||
- destination channel id
|
||||
- reply target message id
|
||||
- thread id if present
|
||||
- sender identity
|
||||
- sender id, label, username, tag
|
||||
- channel context
|
||||
- guild id
|
||||
- channel name or slug
|
||||
- thread metadata
|
||||
- resolved system prompt override
|
||||
- normalized message body
|
||||
- base text
|
||||
- effective message text
|
||||
- attachment descriptors or resolved media references
|
||||
- gating decisions
|
||||
- mention requirement outcome
|
||||
- command authorization outcome
|
||||
- bound session or agent metadata if applicable
|
||||
|
||||
The job payload must not contain live Carbon objects or mutable closures.
|
||||
|
||||
Current implementation status:
|
||||
|
||||
- partially done
|
||||
- `src/discord/monitor/inbound-job.ts` exists and defines the worker handoff
|
||||
- the payload still contains live Discord runtime context and should be reduced further
|
||||
|
||||
### 3. Worker stage
|
||||
|
||||
Add a Discord-specific worker runner responsible for:
|
||||
|
||||
- reconstructing the turn context from `DiscordInboundJob`
|
||||
- loading media and any additional channel metadata needed for the run
|
||||
- dispatching the agent turn
|
||||
- delivering final reply payloads
|
||||
- updating status and diagnostics
|
||||
|
||||
Recommended location:
|
||||
|
||||
- `src/discord/monitor/inbound-worker.ts`
|
||||
- `src/discord/monitor/inbound-job.ts`
|
||||
|
||||
### 4. Ordering model
|
||||
|
||||
Ordering must remain equivalent to today for a given route boundary.
|
||||
|
||||
Recommended key:
|
||||
|
||||
- use the same queue key logic as `resolveDiscordRunQueueKey(...)`
|
||||
|
||||
This preserves existing behavior:
|
||||
|
||||
- one bound agent conversation does not interleave with itself
|
||||
- different Discord channels can still progress independently
|
||||
|
||||
### 5. Timeout model
|
||||
|
||||
After cutover, there are two separate timeout classes:
|
||||
|
||||
- listener timeout
|
||||
- only covers normalization and enqueue
|
||||
- should be short
|
||||
- run timeout
|
||||
- optional, worker-owned, explicit, and user-visible
|
||||
- should not be inherited accidentally from Carbon listener settings
|
||||
|
||||
This removes the current accidental coupling between "Discord gateway listener stayed alive" and "agent run is healthy."
|
||||
|
||||
## Recommended implementation phases
|
||||
|
||||
### Phase 1: normalization boundary
|
||||
|
||||
- Status: partially implemented
|
||||
- Done:
|
||||
- extracted `buildDiscordInboundJob(...)`
|
||||
- added worker handoff tests
|
||||
- Remaining:
|
||||
- make `DiscordInboundJob` plain data only
|
||||
- move live runtime dependencies to worker-owned services instead of per-job payload
|
||||
- stop rebuilding process context by stitching live listener refs back into the job
|
||||
|
||||
### Phase 2: in-memory worker queue
|
||||
|
||||
- Status: implemented
|
||||
- Done:
|
||||
- added `DiscordInboundWorkerQueue` keyed by resolved run queue key
|
||||
- listener enqueues jobs instead of directly awaiting `processDiscordMessage(...)`
|
||||
- worker executes jobs in-process, in memory only
|
||||
|
||||
This is the first functional cutover.
|
||||
|
||||
### Phase 3: process split
|
||||
|
||||
- Status: not started
|
||||
- Move delivery, typing, and draft streaming ownership behind worker-facing adapters.
|
||||
- Replace direct use of live preflight context with worker context reconstruction.
|
||||
- Keep `processDiscordMessage(...)` temporarily as a facade if needed, then split it.
|
||||
|
||||
### Phase 4: command semantics
|
||||
|
||||
- Status: not started
|
||||
Make sure native Discord commands still behave correctly when work is queued:
|
||||
|
||||
- `stop`
|
||||
- `new`
|
||||
- `reset`
|
||||
- any future session-control commands
|
||||
|
||||
The worker queue must expose enough run state for commands to target the active or queued turn.
|
||||
|
||||
### Phase 5: observability and operator UX
|
||||
|
||||
- Status: not started
|
||||
- emit queue depth and active worker counts into monitor status
|
||||
- record enqueue time, start time, finish time, and timeout or cancellation reason
|
||||
- surface worker-owned timeout or delivery failures clearly in logs
|
||||
|
||||
### Phase 6: optional durability follow-up
|
||||
|
||||
- Status: not started
|
||||
Only after the in-memory version is stable:
|
||||
|
||||
- decide whether queued Discord jobs should survive gateway restart
|
||||
- if yes, persist job descriptors and delivery checkpoints
|
||||
- if no, document the explicit in-memory boundary
|
||||
|
||||
This should be a separate follow-up unless restart recovery is required to land.
|
||||
|
||||
## File impact
|
||||
|
||||
Current primary files:
|
||||
|
||||
- `src/discord/monitor/listeners.ts`
|
||||
- `src/discord/monitor/message-handler.ts`
|
||||
- `src/discord/monitor/message-handler.preflight.ts`
|
||||
- `src/discord/monitor/message-handler.process.ts`
|
||||
- `src/discord/monitor/status.ts`
|
||||
|
||||
Current worker files:
|
||||
|
||||
- `src/discord/monitor/inbound-job.ts`
|
||||
- `src/discord/monitor/inbound-worker.ts`
|
||||
- `src/discord/monitor/inbound-job.test.ts`
|
||||
- `src/discord/monitor/message-handler.queue.test.ts`
|
||||
|
||||
Likely next touch points:
|
||||
|
||||
- `src/auto-reply/dispatch.ts`
|
||||
- `src/discord/monitor/reply-delivery.ts`
|
||||
- `src/discord/monitor/thread-bindings.ts`
|
||||
- `src/discord/monitor/native-command.ts`
|
||||
|
||||
## Next step now
|
||||
|
||||
The next step is to make the worker boundary real instead of partial.
|
||||
|
||||
Do this next:
|
||||
|
||||
1. Move live runtime dependencies out of `DiscordInboundJob`
|
||||
2. Keep those dependencies on the Discord worker instance instead
|
||||
3. Reduce queued jobs to plain Discord-specific data:
|
||||
- route identity
|
||||
- delivery target
|
||||
- sender info
|
||||
- normalized message snapshot
|
||||
- gating and binding decisions
|
||||
4. Reconstruct worker execution context from that plain data inside the worker
|
||||
|
||||
In practice, that means:
|
||||
|
||||
- `client`
|
||||
- `threadBindings`
|
||||
- `guildHistories`
|
||||
- `discordRestFetch`
|
||||
- other mutable runtime-only handles
|
||||
|
||||
should stop living on each queued job and instead live on the worker itself or behind worker-owned adapters.
|
||||
|
||||
After that lands, the next follow-up should be command-state cleanup for `stop`, `new`, and `reset`.
|
||||
|
||||
## Testing plan
|
||||
|
||||
Keep the existing timeout repro coverage in:
|
||||
|
||||
- `src/discord/monitor/message-handler.queue.test.ts`
|
||||
|
||||
Add new tests for:
|
||||
|
||||
1. listener returns after enqueue without awaiting full turn
|
||||
2. per-route ordering is preserved
|
||||
3. different channels still run concurrently
|
||||
4. replies are delivered to the original message destination
|
||||
5. `stop` cancels the active worker-owned run
|
||||
6. worker failure produces visible diagnostics without blocking later jobs
|
||||
7. ACP-bound Discord channels still route correctly under worker execution
|
||||
|
||||
## Risks and mitigations
|
||||
|
||||
- Risk: command semantics drift from current synchronous behavior
|
||||
Mitigation: land command-state plumbing in the same cutover, not later
|
||||
|
||||
- Risk: reply delivery loses thread or reply-to context
|
||||
Mitigation: make delivery identity first-class in `DiscordInboundJob`
|
||||
|
||||
- Risk: duplicate sends during retries or queue restarts
|
||||
Mitigation: keep first pass in-memory only, or add explicit delivery idempotency before persistence
|
||||
|
||||
- Risk: `message-handler.process.ts` becomes harder to reason about during migration
|
||||
Mitigation: split into normalization, execution, and delivery helpers before or during worker cutover
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
The plan is complete when:
|
||||
|
||||
1. Discord listener timeout no longer aborts healthy long-running turns.
|
||||
2. Listener lifetime and agent-turn lifetime are separate concepts in code.
|
||||
3. Existing per-session ordering is preserved.
|
||||
4. ACP-bound Discord channels work through the same worker path.
|
||||
5. `stop` targets the worker-owned run instead of the old listener-owned call stack.
|
||||
6. Timeout and delivery failures become explicit worker outcomes, not silent listener drops.
|
||||
|
||||
## Remaining landing strategy
|
||||
|
||||
Finish this in follow-up PRs:
|
||||
|
||||
1. make `DiscordInboundJob` plain-data only and move live runtime refs onto the worker
|
||||
2. clean up command-state ownership for `stop`, `new`, and `reset`
|
||||
3. add worker observability and operator status
|
||||
4. decide whether durability is needed or explicitly document the in-memory boundary
|
||||
|
||||
This is still a bounded follow-up if kept Discord-only and if we continue to avoid a premature cross-channel worker abstraction.
|
||||
@@ -1,126 +0,0 @@
|
||||
---
|
||||
summary: "Plan: Add OpenResponses /v1/responses endpoint and deprecate chat completions cleanly"
|
||||
read_when:
|
||||
- Designing or implementing `/v1/responses` gateway support
|
||||
- Planning migration from Chat Completions compatibility
|
||||
owner: "openclaw"
|
||||
status: "draft"
|
||||
last_updated: "2026-01-19"
|
||||
title: "OpenResponses Gateway Plan"
|
||||
---
|
||||
|
||||
# OpenResponses Gateway Integration Plan
|
||||
|
||||
## Context
|
||||
|
||||
OpenClaw Gateway currently exposes a minimal OpenAI-compatible Chat Completions endpoint at
|
||||
`/v1/chat/completions` (see [OpenAI Chat Completions](/gateway/openai-http-api)).
|
||||
|
||||
Open Responses is an open inference standard based on the OpenAI Responses API. It is designed
|
||||
for agentic workflows and uses item-based inputs plus semantic streaming events. The OpenResponses
|
||||
spec defines `/v1/responses`, not `/v1/chat/completions`.
|
||||
|
||||
## Goals
|
||||
|
||||
- Add a `/v1/responses` endpoint that adheres to OpenResponses semantics.
|
||||
- Keep Chat Completions as a compatibility layer that is easy to disable and eventually remove.
|
||||
- Standardize validation and parsing with isolated, reusable schemas.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Full OpenResponses feature parity in the first pass (images, files, hosted tools).
|
||||
- Replacing internal agent execution logic or tool orchestration.
|
||||
- Changing the existing `/v1/chat/completions` behavior during the first phase.
|
||||
|
||||
## Research Summary
|
||||
|
||||
Sources: OpenResponses OpenAPI, OpenResponses specification site, and the Hugging Face blog post.
|
||||
|
||||
Key points extracted:
|
||||
|
||||
- `POST /v1/responses` accepts `CreateResponseBody` fields like `model`, `input` (string or
|
||||
`ItemParam[]`), `instructions`, `tools`, `tool_choice`, `stream`, `max_output_tokens`, and
|
||||
`max_tool_calls`.
|
||||
- `ItemParam` is a discriminated union of:
|
||||
- `message` items with roles `system`, `developer`, `user`, `assistant`
|
||||
- `function_call` and `function_call_output`
|
||||
- `reasoning`
|
||||
- `item_reference`
|
||||
- Successful responses return a `ResponseResource` with `object: "response"`, `status`, and
|
||||
`output` items.
|
||||
- Streaming uses semantic events such as:
|
||||
- `response.created`, `response.in_progress`, `response.completed`, `response.failed`
|
||||
- `response.output_item.added`, `response.output_item.done`
|
||||
- `response.content_part.added`, `response.content_part.done`
|
||||
- `response.output_text.delta`, `response.output_text.done`
|
||||
- The spec requires:
|
||||
- `Content-Type: text/event-stream`
|
||||
- `event:` must match the JSON `type` field
|
||||
- terminal event must be literal `[DONE]`
|
||||
- Reasoning items may expose `content`, `encrypted_content`, and `summary`.
|
||||
- HF examples include `OpenResponses-Version: latest` in requests (optional header).
|
||||
|
||||
## Proposed Architecture
|
||||
|
||||
- Add `src/gateway/open-responses.schema.ts` containing Zod schemas only (no gateway imports).
|
||||
- Add `src/gateway/openresponses-http.ts` (or `open-responses-http.ts`) for `/v1/responses`.
|
||||
- Keep `src/gateway/openai-http.ts` intact as a legacy compatibility adapter.
|
||||
- Add config `gateway.http.endpoints.responses.enabled` (default `false`).
|
||||
- Keep `gateway.http.endpoints.chatCompletions.enabled` independent; allow both endpoints to be
|
||||
toggled separately.
|
||||
- Emit a startup warning when Chat Completions is enabled to signal legacy status.
|
||||
|
||||
## Deprecation Path for Chat Completions
|
||||
|
||||
- Maintain strict module boundaries: no shared schema types between responses and chat completions.
|
||||
- Make Chat Completions opt-in by config so it can be disabled without code changes.
|
||||
- Update docs to label Chat Completions as legacy once `/v1/responses` is stable.
|
||||
- Optional future step: map Chat Completions requests to the Responses handler for a simpler
|
||||
removal path.
|
||||
|
||||
## Phase 1 Support Subset
|
||||
|
||||
- Accept `input` as string or `ItemParam[]` with message roles and `function_call_output`.
|
||||
- Extract system and developer messages into `extraSystemPrompt`.
|
||||
- Use the most recent `user` or `function_call_output` as the current message for agent runs.
|
||||
- Reject unsupported content parts (image/file) with `invalid_request_error`.
|
||||
- Return a single assistant message with `output_text` content.
|
||||
- Return `usage` with zeroed values until token accounting is wired.
|
||||
|
||||
## Validation Strategy (No SDK)
|
||||
|
||||
- Implement Zod schemas for the supported subset of:
|
||||
- `CreateResponseBody`
|
||||
- `ItemParam` + message content part unions
|
||||
- `ResponseResource`
|
||||
- Streaming event shapes used by the gateway
|
||||
- Keep schemas in a single, isolated module to avoid drift and allow future codegen.
|
||||
|
||||
## Streaming Implementation (Phase 1)
|
||||
|
||||
- SSE lines with both `event:` and `data:`.
|
||||
- Required sequence (minimum viable):
|
||||
- `response.created`
|
||||
- `response.output_item.added`
|
||||
- `response.content_part.added`
|
||||
- `response.output_text.delta` (repeat as needed)
|
||||
- `response.output_text.done`
|
||||
- `response.content_part.done`
|
||||
- `response.completed`
|
||||
- `[DONE]`
|
||||
|
||||
## Tests and Verification Plan
|
||||
|
||||
- Add e2e coverage for `/v1/responses`:
|
||||
- Auth required
|
||||
- Non-stream response shape
|
||||
- Stream event ordering and `[DONE]`
|
||||
- Session routing with headers and `user`
|
||||
- Keep `src/gateway/openai-http.test.ts` unchanged.
|
||||
- Manual: curl to `/v1/responses` with `stream: true` and verify event ordering and terminal
|
||||
`[DONE]`.
|
||||
|
||||
## Doc Updates (Follow-up)
|
||||
|
||||
- Add a new docs page for `/v1/responses` usage and examples.
|
||||
- Update `/gateway/openai-http-api` with a legacy note and pointer to `/v1/responses`.
|
||||
@@ -1,195 +0,0 @@
|
||||
---
|
||||
summary: "Production plan for reliable interactive process supervision (PTY + non-PTY) with explicit ownership, unified lifecycle, and deterministic cleanup"
|
||||
read_when:
|
||||
- Working on exec/process lifecycle ownership and cleanup
|
||||
- Debugging PTY and non-PTY supervision behavior
|
||||
owner: "openclaw"
|
||||
status: "in-progress"
|
||||
last_updated: "2026-02-15"
|
||||
title: "PTY and Process Supervision Plan"
|
||||
---
|
||||
|
||||
# PTY and Process Supervision Plan
|
||||
|
||||
## 1. Problem and goal
|
||||
|
||||
We need one reliable lifecycle for long-running command execution across:
|
||||
|
||||
- `exec` foreground runs
|
||||
- `exec` background runs
|
||||
- `process` follow up actions (`poll`, `log`, `send-keys`, `paste`, `submit`, `kill`, `remove`)
|
||||
- CLI agent runner subprocesses
|
||||
|
||||
The goal is not just to support PTY. The goal is predictable ownership, cancellation, timeout, and cleanup with no unsafe process matching heuristics.
|
||||
|
||||
## 2. Scope and boundaries
|
||||
|
||||
- Keep implementation internal in `src/process/supervisor`.
|
||||
- Do not create a new package for this.
|
||||
- Keep current behavior compatibility where practical.
|
||||
- Do not broaden scope to terminal replay or tmux style session persistence.
|
||||
|
||||
## 3. Implemented in this branch
|
||||
|
||||
### Supervisor baseline already present
|
||||
|
||||
- Supervisor module is in place under `src/process/supervisor/*`.
|
||||
- Exec runtime and CLI runner are already routed through supervisor spawn and wait.
|
||||
- Registry finalization is idempotent.
|
||||
|
||||
### This pass completed
|
||||
|
||||
1. Explicit PTY command contract
|
||||
|
||||
- `SpawnInput` is now a discriminated union in `src/process/supervisor/types.ts`.
|
||||
- PTY runs require `ptyCommand` instead of reusing generic `argv`.
|
||||
- Supervisor no longer rebuilds PTY command strings from argv joins in `src/process/supervisor/supervisor.ts`.
|
||||
- Exec runtime now passes `ptyCommand` directly in `src/agents/bash-tools.exec-runtime.ts`.
|
||||
|
||||
2. Process layer type decoupling
|
||||
|
||||
- Supervisor types no longer import `SessionStdin` from agents.
|
||||
- Process local stdin contract lives in `src/process/supervisor/types.ts` (`ManagedRunStdin`).
|
||||
- Adapters now depend only on process level types:
|
||||
- `src/process/supervisor/adapters/child.ts`
|
||||
- `src/process/supervisor/adapters/pty.ts`
|
||||
|
||||
3. Process tool lifecycle ownership improvement
|
||||
|
||||
- `src/agents/bash-tools.process.ts` now requests cancellation through supervisor first.
|
||||
- `process kill/remove` now use process-tree fallback termination when supervisor lookup misses.
|
||||
- `remove` keeps deterministic remove behavior by dropping running session entries immediately after termination is requested.
|
||||
|
||||
4. Single source watchdog defaults
|
||||
|
||||
- Added shared defaults in `src/agents/cli-watchdog-defaults.ts`.
|
||||
- `src/agents/cli-backends.ts` consumes the shared defaults.
|
||||
- `src/agents/cli-runner/reliability.ts` consumes the same shared defaults.
|
||||
|
||||
5. Dead helper cleanup
|
||||
|
||||
- Removed unused `killSession` helper path from `src/agents/bash-tools.shared.ts`.
|
||||
|
||||
6. Direct supervisor path tests added
|
||||
|
||||
- Added `src/agents/bash-tools.process.supervisor.test.ts` to cover kill and remove routing through supervisor cancellation.
|
||||
|
||||
7. Reliability gap fixes completed
|
||||
|
||||
- `src/agents/bash-tools.process.ts` now falls back to real OS-level process termination when supervisor lookup misses.
|
||||
- `src/process/supervisor/adapters/child.ts` now uses process-tree termination semantics for default cancel/timeout kill paths.
|
||||
- Added shared process-tree utility in `src/process/kill-tree.ts`.
|
||||
|
||||
8. PTY contract edge-case coverage added
|
||||
|
||||
- Added `src/process/supervisor/supervisor.pty-command.test.ts` for verbatim PTY command forwarding and empty-command rejection.
|
||||
- Added `src/process/supervisor/adapters/child.test.ts` for process-tree kill behavior in child adapter cancellation.
|
||||
|
||||
## 4. Remaining gaps and decisions
|
||||
|
||||
### Reliability status
|
||||
|
||||
The two required reliability gaps for this pass are now closed:
|
||||
|
||||
- `process kill/remove` now has a real OS termination fallback when supervisor lookup misses.
|
||||
- child cancel/timeout now uses process-tree kill semantics for default kill path.
|
||||
- Regression tests were added for both behaviors.
|
||||
|
||||
### Durability and startup reconciliation
|
||||
|
||||
Restart behavior is now explicitly defined as in-memory lifecycle only.
|
||||
|
||||
- `reconcileOrphans()` remains a no-op in `src/process/supervisor/supervisor.ts` by design.
|
||||
- Active runs are not recovered after process restart.
|
||||
- This boundary is intentional for this implementation pass to avoid partial persistence risks.
|
||||
|
||||
### Maintainability follow-ups
|
||||
|
||||
1. `runExecProcess` in `src/agents/bash-tools.exec-runtime.ts` still handles multiple responsibilities and can be split into focused helpers in a follow-up.
|
||||
|
||||
## 5. Implementation plan
|
||||
|
||||
The implementation pass for required reliability and contract items is complete.
|
||||
|
||||
Completed:
|
||||
|
||||
- `process kill/remove` fallback real termination
|
||||
- process-tree cancellation for child adapter default kill path
|
||||
- regression tests for fallback kill and child adapter kill path
|
||||
- PTY command edge-case tests under explicit `ptyCommand`
|
||||
- explicit in-memory restart boundary with `reconcileOrphans()` no-op by design
|
||||
|
||||
Optional follow-up:
|
||||
|
||||
- split `runExecProcess` into focused helpers with no behavior drift
|
||||
|
||||
## 6. File map
|
||||
|
||||
### Process supervisor
|
||||
|
||||
- `src/process/supervisor/types.ts` updated with discriminated spawn input and process local stdin contract.
|
||||
- `src/process/supervisor/supervisor.ts` updated to use explicit `ptyCommand`.
|
||||
- `src/process/supervisor/adapters/child.ts` and `src/process/supervisor/adapters/pty.ts` decoupled from agent types.
|
||||
- `src/process/supervisor/registry.ts` idempotent finalize unchanged and retained.
|
||||
|
||||
### Exec and process integration
|
||||
|
||||
- `src/agents/bash-tools.exec-runtime.ts` updated to pass PTY command explicitly and keep fallback path.
|
||||
- `src/agents/bash-tools.process.ts` updated to cancel via supervisor with real process-tree fallback termination.
|
||||
- `src/agents/bash-tools.shared.ts` removed direct kill helper path.
|
||||
|
||||
### CLI reliability
|
||||
|
||||
- `src/agents/cli-watchdog-defaults.ts` added as shared baseline.
|
||||
- `src/agents/cli-backends.ts` and `src/agents/cli-runner/reliability.ts` now consume same defaults.
|
||||
|
||||
## 7. Validation run in this pass
|
||||
|
||||
Unit tests:
|
||||
|
||||
- `pnpm vitest src/process/supervisor/registry.test.ts`
|
||||
- `pnpm vitest src/process/supervisor/supervisor.test.ts`
|
||||
- `pnpm vitest src/process/supervisor/supervisor.pty-command.test.ts`
|
||||
- `pnpm vitest src/process/supervisor/adapters/child.test.ts`
|
||||
- `pnpm vitest src/agents/cli-backends.test.ts`
|
||||
- `pnpm vitest src/agents/bash-tools.exec.pty-cleanup.test.ts`
|
||||
- `pnpm vitest src/agents/bash-tools.process.poll-timeout.test.ts`
|
||||
- `pnpm vitest src/agents/bash-tools.process.supervisor.test.ts`
|
||||
- `pnpm vitest src/process/exec.test.ts`
|
||||
|
||||
E2E targets:
|
||||
|
||||
- `pnpm vitest src/agents/cli-runner.test.ts`
|
||||
- `pnpm vitest run src/agents/bash-tools.exec.pty-fallback.test.ts src/agents/bash-tools.exec.background-abort.test.ts src/agents/bash-tools.process.send-keys.test.ts`
|
||||
|
||||
Typecheck note:
|
||||
|
||||
- Use `pnpm build` (and `pnpm check` for full lint/docs gate) in this repo. Older notes that mention `pnpm tsgo` are obsolete.
|
||||
|
||||
## 8. Operational guarantees preserved
|
||||
|
||||
- Exec env hardening behavior is unchanged.
|
||||
- Approval and allowlist flow is unchanged.
|
||||
- Output sanitization and output caps are unchanged.
|
||||
- PTY adapter still guarantees wait settlement on forced kill and listener disposal.
|
||||
|
||||
## 9. Definition of done
|
||||
|
||||
1. Supervisor is lifecycle owner for managed runs.
|
||||
2. PTY spawn uses explicit command contract with no argv reconstruction.
|
||||
3. Process layer has no type dependency on agent layer for supervisor stdin contracts.
|
||||
4. Watchdog defaults are single source.
|
||||
5. Targeted unit and e2e tests remain green.
|
||||
6. Restart durability boundary is explicitly documented or fully implemented.
|
||||
|
||||
## 10. Summary
|
||||
|
||||
The branch now has a coherent and safer supervision shape:
|
||||
|
||||
- explicit PTY contract
|
||||
- cleaner process layering
|
||||
- supervisor driven cancellation path for process operations
|
||||
- real fallback termination when supervisor lookup misses
|
||||
- process-tree cancellation for child-run default kill paths
|
||||
- unified watchdog defaults
|
||||
- explicit in-memory restart boundary (no orphan reconciliation across restart in this pass)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user