Compare commits
511 Commits
v2026.6.1-
...
codex/cron
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
feebf7a1e6 | ||
|
|
732ceadfb7 | ||
|
|
41339c6370 | ||
|
|
25f3c2a22b | ||
|
|
6d5061c234 | ||
|
|
286e5ffe07 | ||
|
|
158c4d7540 | ||
|
|
344e04b5d5 | ||
|
|
ec47d1cdd5 | ||
|
|
8c89d35a8a | ||
|
|
d358294f89 | ||
|
|
3480832614 | ||
|
|
e0ab71d3dc | ||
|
|
21b262f507 | ||
|
|
3a64302585 | ||
|
|
38f1db6d67 | ||
|
|
8f6f2617ec | ||
|
|
f4868b79e3 | ||
|
|
d3ab7e92ef | ||
|
|
acacd32415 | ||
|
|
0b26a1bca7 | ||
|
|
0bcdb9c0d1 | ||
|
|
946eed685d | ||
|
|
c219c62598 | ||
|
|
5483ff705f | ||
|
|
70a989a97a | ||
|
|
b7450f83a1 | ||
|
|
ff5667a582 | ||
|
|
d6bea4c5ac | ||
|
|
79896a24d9 | ||
|
|
a7d5ae1872 | ||
|
|
446a2b24c3 | ||
|
|
e4993ec00f | ||
|
|
90493ee8e2 | ||
|
|
60dcaa3cf5 | ||
|
|
b3b203bf67 | ||
|
|
0a4927d0b8 | ||
|
|
a61c94b1f1 | ||
|
|
a9f099d279 | ||
|
|
2fa60af960 | ||
|
|
07006943de | ||
|
|
9dc1694eb7 | ||
|
|
98ff56d70e | ||
|
|
03ccdb9fbc | ||
|
|
6d7b80fa1c | ||
|
|
409d1a7135 | ||
|
|
d31f4e2d62 | ||
|
|
e5e6cf04a2 | ||
|
|
4f8740029a | ||
|
|
9159b3bf8e | ||
|
|
eddf1c776d | ||
|
|
6ec579a0c2 | ||
|
|
87eaac4010 | ||
|
|
529282dcff | ||
|
|
b1fccd0605 | ||
|
|
287dee4593 | ||
|
|
b96c0d932f | ||
|
|
a46181f168 | ||
|
|
1b5cb4a0d3 | ||
|
|
9947a26768 | ||
|
|
2accf3875b | ||
|
|
76c8b36031 | ||
|
|
44fea3c94a | ||
|
|
c68938c19e | ||
|
|
a7c8b2a46a | ||
|
|
5a0d9d6326 | ||
|
|
7cee0bca0b | ||
|
|
7074cf8e23 | ||
|
|
26301f318f | ||
|
|
f49f5973b0 | ||
|
|
1e4ff80604 | ||
|
|
84dca54ef2 | ||
|
|
4a67e4b976 | ||
|
|
41ee6b1dd6 | ||
|
|
04f93c2fb4 | ||
|
|
3cdb87be86 | ||
|
|
17a285f298 | ||
|
|
c2d7b4a486 | ||
|
|
0b98aea71a | ||
|
|
114864185b | ||
|
|
1bd1483b62 | ||
|
|
a5ef086e3c | ||
|
|
a10faca06f | ||
|
|
380a8f140e | ||
|
|
34c3827290 | ||
|
|
54fe0e7f71 | ||
|
|
932d6ea8e5 | ||
|
|
d004b80c91 | ||
|
|
5820378b90 | ||
|
|
d5df1a1cd6 | ||
|
|
175cfe4846 | ||
|
|
85e5d486df | ||
|
|
b6cee3fc35 | ||
|
|
d48b9274d8 | ||
|
|
6d788a237c | ||
|
|
7ccbffcb1b | ||
|
|
2c92973398 | ||
|
|
ed4c4afc0f | ||
|
|
a462601f05 | ||
|
|
f472778717 | ||
|
|
7c1a83ff2e | ||
|
|
f8fcb35064 | ||
|
|
c0b05a2100 | ||
|
|
2a512025ad | ||
|
|
7f79bd8683 | ||
|
|
a4b09d72b9 | ||
|
|
58160094e8 | ||
|
|
c0c4156b6d | ||
|
|
3f66797578 | ||
|
|
f02c1209aa | ||
|
|
5056dd47ca | ||
|
|
97dde19577 | ||
|
|
7cbdebc4ed | ||
|
|
17795c6c4c | ||
|
|
6b25b78800 | ||
|
|
78b3f60dbd | ||
|
|
8f1ae5967e | ||
|
|
d82bfcecb1 | ||
|
|
5629c44547 | ||
|
|
a8bf14da84 | ||
|
|
a9f014e9df | ||
|
|
d76f2c0c3b | ||
|
|
f2a46b0661 | ||
|
|
0fa384c6f6 | ||
|
|
6d643ccd11 | ||
|
|
8b546facaf | ||
|
|
1f35ad12b3 | ||
|
|
3d4d30fd5a | ||
|
|
dd46fd36a3 | ||
|
|
85633eb615 | ||
|
|
2a3421a0da | ||
|
|
e38b8f6a20 | ||
|
|
646974b7d8 | ||
|
|
a86a1de849 | ||
|
|
be336cc1e4 | ||
|
|
8cecf2c7ea | ||
|
|
6af047c7f6 | ||
|
|
ac8338bb02 | ||
|
|
0188c541de | ||
|
|
97509ed1d7 | ||
|
|
432a5978b9 | ||
|
|
5f6a8083bf | ||
|
|
36d7ac31c2 | ||
|
|
aed3743630 | ||
|
|
28b1ea7c0d | ||
|
|
661c763b28 | ||
|
|
36a596aa9f | ||
|
|
c208a10619 | ||
|
|
e59e65be67 | ||
|
|
054e734e53 | ||
|
|
d007b9aba3 | ||
|
|
5d4868c036 | ||
|
|
8bf6206a3e | ||
|
|
1d3cfc4b01 | ||
|
|
1ff2ffa160 | ||
|
|
d07ba5f265 | ||
|
|
f789081bae | ||
|
|
388dc56ba5 | ||
|
|
6c7644268f | ||
|
|
c8d21fe7f0 | ||
|
|
00d846daf7 | ||
|
|
1b9860aa56 | ||
|
|
97d4d5effb | ||
|
|
12c6ef6d57 | ||
|
|
96277245dc | ||
|
|
eef24d452f | ||
|
|
c3baec7136 | ||
|
|
4bb86877e2 | ||
|
|
3509f7613e | ||
|
|
36c1a3e006 | ||
|
|
212eaead01 | ||
|
|
984c3ded9a | ||
|
|
0b7c94a5e1 | ||
|
|
0b61add479 | ||
|
|
2d11402208 | ||
|
|
f6e8a1b2a8 | ||
|
|
5a4f868de0 | ||
|
|
4115f0c82f | ||
|
|
cd0af35e5c | ||
|
|
1824aa07a0 | ||
|
|
5259fa4495 | ||
|
|
2ffeca1d78 | ||
|
|
895dccd058 | ||
|
|
06434d85a0 | ||
|
|
a326faa10c | ||
|
|
6467ddd7ed | ||
|
|
95880ae21c | ||
|
|
d830e4affc | ||
|
|
10d10faa25 | ||
|
|
e992af4b6e | ||
|
|
b1bdc29d33 | ||
|
|
e7aac172d5 | ||
|
|
4b7f39e406 | ||
|
|
335c3a8d31 | ||
|
|
fd6b3255f8 | ||
|
|
355cbc5071 | ||
|
|
b4dfa950b5 | ||
|
|
2d61521bd3 | ||
|
|
30b9e123b8 | ||
|
|
a14be505ff | ||
|
|
2c48dd2277 | ||
|
|
4a285d529a | ||
|
|
07821e4bb8 | ||
|
|
4bae78858f | ||
|
|
1db2c2a3e0 | ||
|
|
eb417bc672 | ||
|
|
5d6216a7f1 | ||
|
|
bce3d5bf92 | ||
|
|
ad9f7f9a59 | ||
|
|
a25338f2b7 | ||
|
|
6997453098 | ||
|
|
c35fda3cfa | ||
|
|
8ea6b5d5b2 | ||
|
|
a02a7aaddb | ||
|
|
19fb9f1299 | ||
|
|
2664f59519 | ||
|
|
1cca70940c | ||
|
|
43d0aaec3d | ||
|
|
5487855815 | ||
|
|
45f7aec156 | ||
|
|
286c8e3632 | ||
|
|
e24582d53c | ||
|
|
3e9b197bd0 | ||
|
|
601ab84f35 | ||
|
|
abc3fa0396 | ||
|
|
db576c4a2d | ||
|
|
5e52a9b513 | ||
|
|
3d7523b618 | ||
|
|
afbf895af0 | ||
|
|
af9bad9fe7 | ||
|
|
3995d57797 | ||
|
|
dcf21ac3ad | ||
|
|
e128efa13a | ||
|
|
7f1c991e44 | ||
|
|
a682e64813 | ||
|
|
e31f351923 | ||
|
|
5f505236a6 | ||
|
|
3d1ec37129 | ||
|
|
6c8e065e3b | ||
|
|
cd3887c28a | ||
|
|
92d363773e | ||
|
|
4d3411349b | ||
|
|
5912b9e738 | ||
|
|
64d01ff8a8 | ||
|
|
06f973dd4f | ||
|
|
0552ec899f | ||
|
|
f37ce4ed9b | ||
|
|
20e0d068a7 | ||
|
|
c0400397df | ||
|
|
732d6972d7 | ||
|
|
438eb26d39 | ||
|
|
fd1e314e59 | ||
|
|
a4b4fed412 | ||
|
|
5be282e459 | ||
|
|
4df832412e | ||
|
|
3901f48b0e | ||
|
|
85d2dd8ed2 | ||
|
|
46bd5ebd11 | ||
|
|
5c93de3e7f | ||
|
|
b579c0a65b | ||
|
|
94adfc8d10 | ||
|
|
6883351085 | ||
|
|
93fd17447a | ||
|
|
ebf20241bd | ||
|
|
16808524cb | ||
|
|
58de2b689f | ||
|
|
55467f0b94 | ||
|
|
6ba25c10dc | ||
|
|
3419cf5a0d | ||
|
|
265926aa47 | ||
|
|
63ed9adfe9 | ||
|
|
e6b5083660 | ||
|
|
6349af6502 | ||
|
|
ffbd02fe8e | ||
|
|
75bc80bb42 | ||
|
|
1e7a0d8987 | ||
|
|
39f319c7a4 | ||
|
|
7c4fb1bd2c | ||
|
|
7d5d62511f | ||
|
|
cc6a6f5682 | ||
|
|
7a8d307bdc | ||
|
|
b7d363cadf | ||
|
|
68b4dd1816 | ||
|
|
0e16e72091 | ||
|
|
9ead0ae921 | ||
|
|
3128ec9858 | ||
|
|
1ec291c682 | ||
|
|
9d9a6140a3 | ||
|
|
674bd6fc93 | ||
|
|
b2a55a282a | ||
|
|
3cf4c1ad69 | ||
|
|
fa9ce6ea0e | ||
|
|
0f1f1a1fd7 | ||
|
|
d944aaa9ec | ||
|
|
baade28397 | ||
|
|
883c0f1254 | ||
|
|
793ab78ebb | ||
|
|
57ea5aff81 | ||
|
|
f1d65b3cd6 | ||
|
|
e6b951a6a6 | ||
|
|
55e9194a4c | ||
|
|
8929838159 | ||
|
|
a355c8897d | ||
|
|
b06dc17537 | ||
|
|
7967a3582c | ||
|
|
2e6016fdec | ||
|
|
8a1a8ea8a3 | ||
|
|
4608f7dcf9 | ||
|
|
49ac93bda6 | ||
|
|
f6653b9b35 | ||
|
|
2f92fddef0 | ||
|
|
489efc8f5e | ||
|
|
459abfc26b | ||
|
|
340cc2c1e4 | ||
|
|
be8cb5d4ea | ||
|
|
222ade9fa6 | ||
|
|
6667b9734a | ||
|
|
ebbb2e8f01 | ||
|
|
dea3e835c5 | ||
|
|
722af385d2 | ||
|
|
dacd18a8aa | ||
|
|
8a9acd2940 | ||
|
|
bd8353dbaa | ||
|
|
3baf78dd0a | ||
|
|
1ed7692d2f | ||
|
|
12798eb789 | ||
|
|
02192bd27f | ||
|
|
086274fd7e | ||
|
|
ed07a7a2de | ||
|
|
829fb5dcb3 | ||
|
|
4c6285e8ff | ||
|
|
7c52969d49 | ||
|
|
42d3acfc99 | ||
|
|
32f98d7fe8 | ||
|
|
4bd7421182 | ||
|
|
d91d8ff060 | ||
|
|
af44fb9b6c | ||
|
|
45e0545e82 | ||
|
|
2d17cb295d | ||
|
|
e8120a72e1 | ||
|
|
0904f3e553 | ||
|
|
2770aa5f4c | ||
|
|
285401ced8 | ||
|
|
64697fbe24 | ||
|
|
e9aae26b22 | ||
|
|
cb12a9af94 | ||
|
|
65d7fa2420 | ||
|
|
bd4a7f4119 | ||
|
|
14f61d0637 | ||
|
|
0f3a63b12e | ||
|
|
a14eacf372 | ||
|
|
646df2da83 | ||
|
|
211321ce5c | ||
|
|
a34e822cd4 | ||
|
|
8c180c9153 | ||
|
|
990f0baff9 | ||
|
|
bd8baeb323 | ||
|
|
0771bbbd20 | ||
|
|
74cf5c7e7d | ||
|
|
0cfd6b0504 | ||
|
|
4e45010203 | ||
|
|
afdf9aaea0 | ||
|
|
72ed2121f8 | ||
|
|
2405bbcbaf | ||
|
|
403190572b | ||
|
|
67983a00c8 | ||
|
|
61aa499b53 | ||
|
|
420450b5cb | ||
|
|
f8491b0fcf | ||
|
|
98e943ebdd | ||
|
|
f8d5f162a1 | ||
|
|
a2fdd5bc70 | ||
|
|
2af2111ae0 | ||
|
|
c9d35c7172 | ||
|
|
50b69e16dc | ||
|
|
fe97c6000c | ||
|
|
a99cbf29bd | ||
|
|
05ea36a81f | ||
|
|
eb58c88598 | ||
|
|
5a67c5c556 | ||
|
|
5a55135146 | ||
|
|
193988bc5b | ||
|
|
a20f57bf2e | ||
|
|
66f797b22c | ||
|
|
65a805ac28 | ||
|
|
b18bab0bcc | ||
|
|
9ac30b587e | ||
|
|
82de264710 | ||
|
|
7f7f0775ed | ||
|
|
30819ed3da | ||
|
|
1c3095e029 | ||
|
|
62cfc613f1 | ||
|
|
64a946ac21 | ||
|
|
96187089d4 | ||
|
|
965e680603 | ||
|
|
1cf39a2d6f | ||
|
|
92b3d52e8a | ||
|
|
8ba6dfeaf6 | ||
|
|
bddcf4448c | ||
|
|
c8a67768e3 | ||
|
|
26e61b2087 | ||
|
|
ee48028028 | ||
|
|
3c324590ae | ||
|
|
ba88b7a178 | ||
|
|
d767e296e2 | ||
|
|
83cd3cbe2a | ||
|
|
16807824cc | ||
|
|
e3d24faecd | ||
|
|
469bec97ef | ||
|
|
101db565ca | ||
|
|
ef26e8dfce | ||
|
|
25c19e013a | ||
|
|
f2eea90dac | ||
|
|
3113fe95ea | ||
|
|
4e1f8b8ac7 | ||
|
|
0b8f6b81e6 | ||
|
|
ab1042d115 | ||
|
|
9153aab037 | ||
|
|
285a792aa8 | ||
|
|
a8bc1716dd | ||
|
|
373ef81e83 | ||
|
|
c053b90290 | ||
|
|
fbdf593778 | ||
|
|
488b65ab87 | ||
|
|
6668eb8225 | ||
|
|
72436217ff | ||
|
|
460cf7ed75 | ||
|
|
461999c060 | ||
|
|
9cb347e4c3 | ||
|
|
1d7e5f48ed | ||
|
|
1fd2259e28 | ||
|
|
3f54d150b3 | ||
|
|
a9866a405c | ||
|
|
0b9187c780 | ||
|
|
b1ec23e05f | ||
|
|
050f0c0af6 | ||
|
|
dfeb5b81ca | ||
|
|
d9f6e03e32 | ||
|
|
fed7d1f385 | ||
|
|
0a9e594420 | ||
|
|
c1ce51546e | ||
|
|
1b928592ef | ||
|
|
12087ac9d4 | ||
|
|
00caead80a | ||
|
|
4b54a423f0 | ||
|
|
bdd6cf3d5e | ||
|
|
cb7a4239ef | ||
|
|
b226a752a1 | ||
|
|
110f7d55e3 | ||
|
|
645c7dc40b | ||
|
|
a4847297b8 | ||
|
|
4253517070 | ||
|
|
e8c126eaf2 | ||
|
|
2075d19923 | ||
|
|
9e58ef1c82 | ||
|
|
eaeccf5fdf | ||
|
|
2c0e835b48 | ||
|
|
b942a958b3 | ||
|
|
42bcf9cd0b | ||
|
|
a0fbb6cfe2 | ||
|
|
408fa6e951 | ||
|
|
671909d6d3 | ||
|
|
409f78a1ea | ||
|
|
3e592a8bd7 | ||
|
|
e895479a21 | ||
|
|
930bc9691b | ||
|
|
b9f181635f | ||
|
|
c2aaf8afec | ||
|
|
cbc5f277bb | ||
|
|
44b388f863 | ||
|
|
c0e49a2c52 | ||
|
|
c1e132195d | ||
|
|
5bd8dbd0b8 | ||
|
|
421ea1f458 | ||
|
|
1f91e97353 | ||
|
|
d4f6e0a1f2 | ||
|
|
ec2455a842 | ||
|
|
1742f3f77c | ||
|
|
5117f457bb | ||
|
|
8fe5e83462 | ||
|
|
27097bed65 | ||
|
|
1849a86dd2 | ||
|
|
5280d1d95d | ||
|
|
bcdc93d651 | ||
|
|
0751b6f2c9 | ||
|
|
7d9fae5b3a | ||
|
|
a595aba60e | ||
|
|
75645aec08 | ||
|
|
d10d71cdb6 | ||
|
|
c69a8d633d | ||
|
|
d8ebbedf45 | ||
|
|
9ed1766696 | ||
|
|
bed0fb7bad | ||
|
|
db6fc20559 | ||
|
|
1364acbe4c | ||
|
|
d2988e0248 | ||
|
|
8c8c8c8e32 | ||
|
|
8bee3be90a | ||
|
|
87d890003d | ||
|
|
aed7de306e | ||
|
|
859cb52b44 | ||
|
|
4685a84e9b | ||
|
|
f30235bed2 | ||
|
|
4f8f6c7693 | ||
|
|
055063f06b | ||
|
|
dac33c8ecb | ||
|
|
75ebf1c870 | ||
|
|
e4a32b9e8e | ||
|
|
22e3b2e94e | ||
|
|
729420c34a | ||
|
|
0b5be66ef7 |
@@ -22,6 +22,8 @@ Use when:
|
||||
- Read dependency docs/source/types when the finding depends on external behavior.
|
||||
- Reject unrealistic edge cases, speculative risks, broad rewrites, and fixes that over-complicate the codebase.
|
||||
- Prefer small fixes at the right ownership boundary; no refactor unless it clearly improves the bug class.
|
||||
- When an accepted finding shows a bug class or repeated pattern, inspect the current PR scope for sibling instances before fixing.
|
||||
- Fix the scoped bug class at once when practical; stop at touched surfaces, owner boundaries, and clear follow-up territory.
|
||||
- Keep going until structured review returns no accepted/actionable findings.
|
||||
- If a review-triggered fix changes code, rerun focused tests and rerun the structured review helper.
|
||||
- For security-audit suppression changes, verify accepted findings remain auditable: suppressed findings stay in structured output, active output keeps an unsuppressible suppression notice, and aggregate findings cannot hide unrelated active risk.
|
||||
|
||||
@@ -16,6 +16,10 @@ Use this with `$release-openclaw-maintainer` and `$openclaw-testing` when a rele
|
||||
- Watch one parent run plus compact child summaries. Avoid broad `gh run view` polling loops; REST quota is easy to burn.
|
||||
- Fetch logs only for failed or currently-blocking jobs. If quota is low, stop polling and wait for reset.
|
||||
- Treat live-provider flakes separately from code failures: prove key validity, provider HTTP status, retry evidence, and exact failing lane before editing code.
|
||||
- Full Release Validation parent monitors fail fast: once a required child job
|
||||
fails, the parent cancels the remaining child matrix and prints the failed
|
||||
job summary. Inspect that first red job instead of waiting for unrelated
|
||||
matrix tails.
|
||||
|
||||
## Preflight
|
||||
|
||||
@@ -73,6 +77,9 @@ gh workflow run full-release-validation.yml \
|
||||
```
|
||||
|
||||
Use `release_profile=stable` unless the operator explicitly asks for the broad advisory provider/media matrix. Use narrow `rerun_group` after focused fixes.
|
||||
Publish with `openclaw-release-publish.yml` using `release_profile=from-validation`
|
||||
unless a maintainer intentionally wants to cross-check a specific profile; the
|
||||
publish workflow reads the effective profile from the full-validation manifest.
|
||||
|
||||
## Watch
|
||||
|
||||
|
||||
@@ -49,17 +49,21 @@ Use this skill for release and publish-time workflow. Load `$release-private` if
|
||||
the next beta number until the matching npm package has actually published.
|
||||
If a published beta needs a fix, commit the fix on the release branch and
|
||||
increment to the next `-beta.N`.
|
||||
- For a beta release train, run the fast local preflight first, publish the
|
||||
beta to npm `beta`, then run the expensive published-package roster focused
|
||||
on install/update/Docker/Parallels/NPM Telegram. If anything fails, fix it on
|
||||
the release branch, commit/push/pull, increment beta number, and repeat. Run
|
||||
the full expensive roster at least once before stable/latest promotion; for
|
||||
later beta attempts, rerun only lanes whose evidence changed unless the fix
|
||||
touches broad release, install/update, plugin, Docker, Parallels, or live QA
|
||||
behavior. After each beta is published, scan current `main` once for critical
|
||||
fixes that landed after the release branch cut and backport only important
|
||||
low-risk fixes. Operators may authorize up to 4 autonomous beta attempts;
|
||||
after 4 failed beta attempts, stop and report.
|
||||
- For a beta release train, keep Full Release Validation as a pre-publish gate
|
||||
unless the operator explicitly waives it. Run the fast local preflight, npm
|
||||
preflight, full release validation, and performance in parallel where safe.
|
||||
If anything fails before npm publish, fix it on the release branch,
|
||||
forward-port the fix to `main`, move the unpublished beta tag/prerelease to
|
||||
the fixed commit, and rerun the affected pre-publish gates. If anything fails
|
||||
after npm publish, fix it, forward-port to `main`, increment beta number, and
|
||||
repeat. After each beta publish, run the published-package roster focused on
|
||||
install/update/Docker/Parallels/NPM Telegram. For later beta attempts, rerun
|
||||
only lanes whose evidence changed unless the fix touches broad release,
|
||||
install/update, plugin, Docker, Parallels, or live QA behavior. After each
|
||||
beta is live, scan current `main` once for critical fixes that landed after
|
||||
the release branch cut and backport only important low-risk fixes. Operators
|
||||
may authorize up to 4 autonomous beta attempts; after 4 failed beta attempts,
|
||||
stop and report.
|
||||
- As soon as the release candidate SHA exists, dispatch `OpenClaw Performance`
|
||||
with `target_ref=<release-sha>` in parallel with the other release work. Do
|
||||
not wait for full release validation to start the performance signal.
|
||||
@@ -468,8 +472,10 @@ node --import tsx scripts/openclaw-npm-postpublish-verify.ts <published-version>
|
||||
- The npm workflow and the private mac publish workflow accept
|
||||
`preflight_only=true` to run validation/build/package steps without uploading
|
||||
public release assets.
|
||||
- Real npm publish requires a prior successful npm preflight run id so the
|
||||
publish job promotes the prepared tarball instead of rebuilding it.
|
||||
- Real npm publish requires a prior successful npm preflight run id and the
|
||||
successful Full Release Validation run id for the same tag/SHA so the publish
|
||||
job promotes the prepared tarball instead of rebuilding it and attaches the
|
||||
correct release evidence.
|
||||
- Real private mac publish requires a prior successful private mac preflight
|
||||
run id so the publish job promotes the prepared artifacts instead of
|
||||
rebuilding or renotarizing them again.
|
||||
@@ -499,11 +505,12 @@ node --import tsx scripts/openclaw-npm-postpublish-verify.ts <published-version>
|
||||
instead of uploading public GitHub release assets.
|
||||
- Private smoke-test runs upload ad-hoc, non-notarized build artifacts as
|
||||
workflow artifacts and intentionally skip stable `appcast.xml` generation.
|
||||
- For stable releases, npm preflight, public mac validation, private mac
|
||||
validation, and private mac preflight must all pass before any real publish
|
||||
run starts. For beta releases, npm preflight plus the selected Docker,
|
||||
install/update, Parallels, and release-check lanes are sufficient unless mac
|
||||
beta validation was explicitly requested.
|
||||
- For stable releases, npm preflight, Full Release Validation, public mac
|
||||
validation, private mac validation, and private mac preflight must all pass
|
||||
before any real publish run starts. For beta releases, npm preflight and Full
|
||||
Release Validation must pass before npm publish unless the operator explicitly
|
||||
waives the full gate; mac beta validation is still only required when
|
||||
requested.
|
||||
- Real publish runs may be dispatched from `main` or from a
|
||||
`release/YYYY.M.D` branch. For release-branch runs, the tag must be contained
|
||||
in that release branch, and the real publish must reuse a successful preflight
|
||||
|
||||
156
.github/workflows/ci-check-arm-testbox.yml
vendored
Normal file
@@ -0,0 +1,156 @@
|
||||
name: Blacksmith ARM Testbox
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
testbox_id:
|
||||
type: string
|
||||
description: "Testbox session ID"
|
||||
required: true
|
||||
pull_request:
|
||||
paths:
|
||||
- ".github/workflows/**"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||
PNPM_CONFIG_STORE_DIR: "/tmp/openclaw-pnpm-store"
|
||||
PNPM_CONFIG_VERIFY_DEPS_BEFORE_RUN: "false"
|
||||
|
||||
jobs:
|
||||
check-arm:
|
||||
if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }}
|
||||
permissions:
|
||||
contents: read
|
||||
name: "check-arm"
|
||||
runs-on: blacksmith-16vcpu-ubuntu-2404-arm
|
||||
timeout-minutes: 120
|
||||
steps:
|
||||
- name: Begin Testbox
|
||||
uses: useblacksmith/begin-testbox@d0e04585c26905fdd92c94a09c159544c7ee1b67
|
||||
with:
|
||||
testbox_id: ${{ inputs.testbox_id }}
|
||||
- name: Verify ARM runner
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
runner_arch="$(uname -m)"
|
||||
echo "check-arm runner architecture: ${runner_arch}"
|
||||
case "$runner_arch" in
|
||||
aarch64 | arm64)
|
||||
;;
|
||||
*)
|
||||
echo "check-arm requires an ARM64 runner; got ${runner_arch}" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
- name: Checkout
|
||||
shell: bash
|
||||
env:
|
||||
CHECKOUT_REPO: ${{ github.repository }}
|
||||
CHECKOUT_SHA: ${{ github.sha }}
|
||||
CHECKOUT_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
workdir="$GITHUB_WORKSPACE"
|
||||
if [[ -z "$CHECKOUT_TOKEN" ]]; then
|
||||
echo "checkout token is missing" >&2
|
||||
exit 1
|
||||
fi
|
||||
auth_header="$(printf 'x-access-token:%s' "$CHECKOUT_TOKEN" | base64 | tr -d '\n')"
|
||||
|
||||
reset_checkout_dir() {
|
||||
mkdir -p "$workdir"
|
||||
find "$workdir" -mindepth 1 -maxdepth 1 -exec rm -rf {} +
|
||||
}
|
||||
|
||||
checkout_attempt() {
|
||||
local attempt="$1"
|
||||
|
||||
reset_checkout_dir
|
||||
git init "$workdir" >/dev/null
|
||||
git config --global --add safe.directory "$workdir"
|
||||
git -C "$workdir" remote add origin "https://github.com/${CHECKOUT_REPO}"
|
||||
git -C "$workdir" config gc.auto 0
|
||||
|
||||
timeout --signal=TERM --kill-after=10s 30s git -C "$workdir" \
|
||||
-c protocol.version=2 \
|
||||
-c "http.extraheader=AUTHORIZATION: basic ${auth_header}" \
|
||||
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
|
||||
"+${CHECKOUT_SHA}:refs/remotes/origin/ci-target" || return 1
|
||||
|
||||
git -C "$workdir" checkout --force --detach "$CHECKOUT_SHA" || return 1
|
||||
test -f "$workdir/.github/actions/setup-node-env/action.yml" || return 1
|
||||
echo "checkout attempt ${attempt}/5 succeeded"
|
||||
}
|
||||
|
||||
for attempt in 1 2 3 4 5; do
|
||||
if checkout_attempt "$attempt"; then
|
||||
exit 0
|
||||
fi
|
||||
echo "checkout attempt ${attempt}/5 failed"
|
||||
sleep $((attempt * 5))
|
||||
done
|
||||
|
||||
echo "checkout failed after 5 attempts" >&2
|
||||
exit 1
|
||||
- name: Setup Node environment
|
||||
uses: ./.github/actions/setup-node-env
|
||||
with:
|
||||
install-bun: "false"
|
||||
- name: Prepare Testbox shell
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
timeout --signal=TERM --kill-after=10s 30s git \
|
||||
-c protocol.version=2 \
|
||||
fetch --no-tags --prune --no-recurse-submodules --depth=50 origin \
|
||||
"+refs/heads/main:refs/remotes/origin/main"
|
||||
|
||||
node_bin="$(dirname "$(node -p 'process.execPath')")"
|
||||
sudo ln -sf "$node_bin/node" /usr/local/bin/node
|
||||
sudo ln -sf "$node_bin/npm" /usr/local/bin/npm
|
||||
sudo ln -sf "$node_bin/npx" /usr/local/bin/npx
|
||||
sudo ln -sf "$node_bin/corepack" /usr/local/bin/corepack
|
||||
sudo tee /usr/local/bin/pnpm >/dev/null <<'PNPM'
|
||||
#!/usr/bin/env bash
|
||||
exec /usr/local/bin/corepack pnpm "$@"
|
||||
PNPM
|
||||
sudo chmod 0755 /usr/local/bin/pnpm
|
||||
|
||||
- name: Hydrate Testbox provider env helper
|
||||
shell: bash
|
||||
env:
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
ANTHROPIC_API_KEY_OLD: ${{ secrets.ANTHROPIC_API_KEY_OLD }}
|
||||
ANTHROPIC_API_TOKEN: ${{ secrets.ANTHROPIC_API_TOKEN }}
|
||||
CEREBRAS_API_KEY: ${{ secrets.CEREBRAS_API_KEY }}
|
||||
DEEPINFRA_API_KEY: ${{ secrets.DEEPINFRA_API_KEY }}
|
||||
FACTORY_API_KEY: ${{ secrets.FACTORY_API_KEY }}
|
||||
FIREWORKS_API_KEY: ${{ secrets.FIREWORKS_API_KEY }}
|
||||
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
|
||||
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
|
||||
GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
|
||||
KIMI_API_KEY: ${{ secrets.KIMI_API_KEY }}
|
||||
MINIMAX_API_KEY: ${{ secrets.MINIMAX_API_KEY }}
|
||||
MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }}
|
||||
MOONSHOT_API_KEY: ${{ secrets.MOONSHOT_API_KEY }}
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
|
||||
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
|
||||
QWEN_API_KEY: ${{ secrets.QWEN_API_KEY }}
|
||||
TOGETHER_API_KEY: ${{ secrets.TOGETHER_API_KEY }}
|
||||
XAI_API_KEY: ${{ secrets.XAI_API_KEY }}
|
||||
ZAI_API_KEY: ${{ secrets.ZAI_API_KEY }}
|
||||
Z_AI_API_KEY: ${{ secrets.Z_AI_API_KEY }}
|
||||
run: bash scripts/ci-hydrate-testbox-env.sh
|
||||
|
||||
- name: Run Testbox
|
||||
uses: useblacksmith/run-testbox@5ca05834db1d3813554d1dd109e5f2087a8d7cbc
|
||||
if: success()
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||
136
.github/workflows/ci-check-testbox.yml
vendored
@@ -139,139 +139,3 @@ jobs:
|
||||
if: success()
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||
|
||||
check-arm:
|
||||
if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }}
|
||||
permissions:
|
||||
contents: read
|
||||
name: "check-arm"
|
||||
runs-on: blacksmith-16vcpu-ubuntu-2404-arm
|
||||
timeout-minutes: 120
|
||||
steps:
|
||||
- name: Begin Testbox
|
||||
uses: useblacksmith/begin-testbox@d0e04585c26905fdd92c94a09c159544c7ee1b67
|
||||
with:
|
||||
testbox_id: ${{ inputs.testbox_id }}
|
||||
- name: Verify ARM runner
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
runner_arch="$(uname -m)"
|
||||
echo "check-arm runner architecture: ${runner_arch}"
|
||||
case "$runner_arch" in
|
||||
aarch64 | arm64)
|
||||
;;
|
||||
*)
|
||||
echo "check-arm requires an ARM64 runner; got ${runner_arch}" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
- name: Checkout
|
||||
shell: bash
|
||||
env:
|
||||
CHECKOUT_REPO: ${{ github.repository }}
|
||||
CHECKOUT_SHA: ${{ github.sha }}
|
||||
CHECKOUT_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
workdir="$GITHUB_WORKSPACE"
|
||||
if [[ -z "$CHECKOUT_TOKEN" ]]; then
|
||||
echo "checkout token is missing" >&2
|
||||
exit 1
|
||||
fi
|
||||
auth_header="$(printf 'x-access-token:%s' "$CHECKOUT_TOKEN" | base64 | tr -d '\n')"
|
||||
|
||||
reset_checkout_dir() {
|
||||
mkdir -p "$workdir"
|
||||
find "$workdir" -mindepth 1 -maxdepth 1 -exec rm -rf {} +
|
||||
}
|
||||
|
||||
checkout_attempt() {
|
||||
local attempt="$1"
|
||||
|
||||
reset_checkout_dir
|
||||
git init "$workdir" >/dev/null
|
||||
git config --global --add safe.directory "$workdir"
|
||||
git -C "$workdir" remote add origin "https://github.com/${CHECKOUT_REPO}"
|
||||
git -C "$workdir" config gc.auto 0
|
||||
|
||||
timeout --signal=TERM --kill-after=10s 30s git -C "$workdir" \
|
||||
-c protocol.version=2 \
|
||||
-c "http.extraheader=AUTHORIZATION: basic ${auth_header}" \
|
||||
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
|
||||
"+${CHECKOUT_SHA}:refs/remotes/origin/ci-target" || return 1
|
||||
|
||||
git -C "$workdir" checkout --force --detach "$CHECKOUT_SHA" || return 1
|
||||
test -f "$workdir/.github/actions/setup-node-env/action.yml" || return 1
|
||||
echo "checkout attempt ${attempt}/5 succeeded"
|
||||
}
|
||||
|
||||
for attempt in 1 2 3 4 5; do
|
||||
if checkout_attempt "$attempt"; then
|
||||
exit 0
|
||||
fi
|
||||
echo "checkout attempt ${attempt}/5 failed"
|
||||
sleep $((attempt * 5))
|
||||
done
|
||||
|
||||
echo "checkout failed after 5 attempts" >&2
|
||||
exit 1
|
||||
- name: Setup Node environment
|
||||
uses: ./.github/actions/setup-node-env
|
||||
with:
|
||||
install-bun: "false"
|
||||
- name: Prepare Testbox shell
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
timeout --signal=TERM --kill-after=10s 30s git \
|
||||
-c protocol.version=2 \
|
||||
fetch --no-tags --prune --no-recurse-submodules --depth=50 origin \
|
||||
"+refs/heads/main:refs/remotes/origin/main"
|
||||
|
||||
node_bin="$(dirname "$(node -p 'process.execPath')")"
|
||||
sudo ln -sf "$node_bin/node" /usr/local/bin/node
|
||||
sudo ln -sf "$node_bin/npm" /usr/local/bin/npm
|
||||
sudo ln -sf "$node_bin/npx" /usr/local/bin/npx
|
||||
sudo ln -sf "$node_bin/corepack" /usr/local/bin/corepack
|
||||
sudo tee /usr/local/bin/pnpm >/dev/null <<'PNPM'
|
||||
#!/usr/bin/env bash
|
||||
exec /usr/local/bin/corepack pnpm "$@"
|
||||
PNPM
|
||||
sudo chmod 0755 /usr/local/bin/pnpm
|
||||
|
||||
- name: Hydrate Testbox provider env helper
|
||||
shell: bash
|
||||
env:
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
ANTHROPIC_API_KEY_OLD: ${{ secrets.ANTHROPIC_API_KEY_OLD }}
|
||||
ANTHROPIC_API_TOKEN: ${{ secrets.ANTHROPIC_API_TOKEN }}
|
||||
CEREBRAS_API_KEY: ${{ secrets.CEREBRAS_API_KEY }}
|
||||
DEEPINFRA_API_KEY: ${{ secrets.DEEPINFRA_API_KEY }}
|
||||
FACTORY_API_KEY: ${{ secrets.FACTORY_API_KEY }}
|
||||
FIREWORKS_API_KEY: ${{ secrets.FIREWORKS_API_KEY }}
|
||||
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
|
||||
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
|
||||
GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
|
||||
KIMI_API_KEY: ${{ secrets.KIMI_API_KEY }}
|
||||
MINIMAX_API_KEY: ${{ secrets.MINIMAX_API_KEY }}
|
||||
MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }}
|
||||
MOONSHOT_API_KEY: ${{ secrets.MOONSHOT_API_KEY }}
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
|
||||
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
|
||||
QWEN_API_KEY: ${{ secrets.QWEN_API_KEY }}
|
||||
TOGETHER_API_KEY: ${{ secrets.TOGETHER_API_KEY }}
|
||||
XAI_API_KEY: ${{ secrets.XAI_API_KEY }}
|
||||
ZAI_API_KEY: ${{ secrets.ZAI_API_KEY }}
|
||||
Z_AI_API_KEY: ${{ secrets.Z_AI_API_KEY }}
|
||||
run: bash scripts/ci-hydrate-testbox-env.sh
|
||||
|
||||
- name: Run Testbox
|
||||
uses: useblacksmith/run-testbox@5ca05834db1d3813554d1dd109e5f2087a8d7cbc
|
||||
if: success()
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||
|
||||
35
.github/workflows/ci.yml
vendored
@@ -605,7 +605,19 @@ jobs:
|
||||
restore-keys: |
|
||||
${{ runner.os }}-build-all-v3-
|
||||
|
||||
- name: Restore dist build cache
|
||||
id: dist_build_cache
|
||||
uses: actions/cache/restore@v5
|
||||
with:
|
||||
path: |
|
||||
dist/
|
||||
dist-runtime/
|
||||
extensions/*/src/host/**/.bundle.hash
|
||||
extensions/*/src/host/**/*.bundle.js
|
||||
key: ${{ runner.os }}-dist-build-${{ needs.preflight.outputs.checkout_revision }}
|
||||
|
||||
- name: Build dist
|
||||
if: steps.dist_build_cache.outputs.cache-hit != 'true'
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=8192
|
||||
run: pnpm build:ci-artifacts
|
||||
@@ -614,14 +626,6 @@ jobs:
|
||||
if: needs.preflight.outputs.run_control_ui_i18n == 'true'
|
||||
run: pnpm ui:i18n:check
|
||||
|
||||
- name: Cache dist build
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: |
|
||||
dist/
|
||||
dist-runtime/
|
||||
key: ${{ runner.os }}-dist-build-${{ needs.preflight.outputs.checkout_revision }}
|
||||
|
||||
- name: Pack built runtime artifacts
|
||||
run: tar --posix -cf dist-runtime-build.tar.zst --use-compress-program zstdmt dist dist-runtime
|
||||
|
||||
@@ -751,6 +755,18 @@ jobs:
|
||||
done
|
||||
exit "$failures"
|
||||
|
||||
- name: Save dist build cache
|
||||
if: steps.dist_build_cache.outputs.cache-hit != 'true'
|
||||
uses: actions/cache/save@v5
|
||||
continue-on-error: true
|
||||
with:
|
||||
path: |
|
||||
dist/
|
||||
dist-runtime/
|
||||
extensions/*/src/host/**/.bundle.hash
|
||||
extensions/*/src/host/**/*.bundle.js
|
||||
key: ${{ steps.dist_build_cache.outputs.cache-primary-key }}
|
||||
|
||||
- name: Upload gateway watch regression artifacts
|
||||
if: always() && needs.preflight.outputs.run_check_additional == 'true'
|
||||
uses: actions/upload-artifact@v7
|
||||
@@ -1151,7 +1167,8 @@ jobs:
|
||||
OPENCLAW_NODE_TEST_CONFIGS_JSON: ${{ toJson(matrix.configs) }}
|
||||
OPENCLAW_NODE_TEST_INCLUDE_PATTERNS_JSON: ${{ toJson(matrix.includePatterns) }}
|
||||
OPENCLAW_VITEST_SHARD_NAME: ${{ matrix.shard_name }}
|
||||
OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS: "900000"
|
||||
OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS: "300000"
|
||||
OPENCLAW_VITEST_NO_OUTPUT_RETRY: "1"
|
||||
OPENCLAW_TEST_PROJECTS_PARALLEL: "2"
|
||||
shell: bash
|
||||
run: |
|
||||
|
||||
21
.github/workflows/crabbox-hydrate.yml
vendored
@@ -32,11 +32,11 @@ permissions:
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||
PNPM_CONFIG_CHILD_CONCURRENCY: "1"
|
||||
PNPM_CONFIG_MODULES_DIR: "/tmp/openclaw-pnpm-node-modules"
|
||||
PNPM_CONFIG_MODULES_DIR: "/var/tmp/openclaw-pnpm-node-modules"
|
||||
PNPM_CONFIG_NETWORK_CONCURRENCY: "1"
|
||||
PNPM_CONFIG_STORE_DIR: "/tmp/openclaw-pnpm-store"
|
||||
PNPM_CONFIG_STORE_DIR: "/var/tmp/openclaw-pnpm-store"
|
||||
PNPM_CONFIG_VERIFY_DEPS_BEFORE_RUN: "false"
|
||||
PNPM_CONFIG_VIRTUAL_STORE_DIR: "/tmp/openclaw-pnpm-virtual-store"
|
||||
PNPM_CONFIG_VIRTUAL_STORE_DIR: "/var/tmp/openclaw-pnpm-virtual-store"
|
||||
|
||||
jobs:
|
||||
hydrate:
|
||||
@@ -120,6 +120,21 @@ jobs:
|
||||
append_pnpm_option_arg PNPM_CONFIG_MODULES_DIR modules-dir
|
||||
append_pnpm_option_arg PNPM_CONFIG_NETWORK_CONCURRENCY network-concurrency
|
||||
append_pnpm_option_arg PNPM_CONFIG_VIRTUAL_STORE_DIR virtual-store-dir
|
||||
reset_crabbox_pnpm_path() {
|
||||
local path="$1"
|
||||
if [ -z "$path" ]; then
|
||||
return
|
||||
fi
|
||||
case "$path" in
|
||||
/var/tmp/openclaw-pnpm-*) rm -rf "$path" ;;
|
||||
esac
|
||||
}
|
||||
reset_crabbox_pnpm_path "${PNPM_CONFIG_MODULES_DIR:-}"
|
||||
reset_crabbox_pnpm_path "${PNPM_CONFIG_STORE_DIR:-}"
|
||||
reset_crabbox_pnpm_path "${PNPM_CONFIG_VIRTUAL_STORE_DIR:-}"
|
||||
if [ -L node_modules ] && [ "$(readlink node_modules)" = "${PNPM_CONFIG_MODULES_DIR:-}" ]; then
|
||||
rm -f node_modules
|
||||
fi
|
||||
if [ -n "${PNPM_CONFIG_MODULES_DIR:-}" ]; then
|
||||
mkdir -p "$PNPM_CONFIG_MODULES_DIR"
|
||||
ln -sfn . "$PNPM_CONFIG_MODULES_DIR/node_modules"
|
||||
|
||||
7
.github/workflows/docker-release.yml
vendored
@@ -4,6 +4,7 @@ on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
- "!v*-alpha.*"
|
||||
paths-ignore:
|
||||
- "docs/**"
|
||||
- "**/*.md"
|
||||
@@ -38,7 +39,11 @@ jobs:
|
||||
RELEASE_TAG: ${{ inputs.tag }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [[ ! "${RELEASE_TAG}" =~ ^v[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*(-(alpha|beta)\.[1-9][0-9]*)?$ ]]; then
|
||||
if [[ "${RELEASE_TAG}" == *"-alpha."* ]]; then
|
||||
echo "Docker alpha image publishing is disabled."
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! "${RELEASE_TAG}" =~ ^v[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*(-beta\.[1-9][0-9]*)?$ ]]; then
|
||||
echo "Invalid release tag: ${RELEASE_TAG}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
122
.github/workflows/full-release-validation.yml
vendored
@@ -229,7 +229,7 @@ jobs:
|
||||
needs: [resolve_target]
|
||||
if: inputs.rerun_group == 'all'
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 45
|
||||
timeout-minutes: 20
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
@@ -245,54 +245,11 @@ jobs:
|
||||
DOCKER_BUILDKIT: "1"
|
||||
run: |
|
||||
set -euo pipefail
|
||||
timeout --kill-after=30s 35m docker build \
|
||||
timeout --kill-after=30s 15m docker build \
|
||||
--target runtime-assets \
|
||||
--build-arg OPENCLAW_EXTENSIONS="diagnostics-otel,codex" \
|
||||
.
|
||||
|
||||
- name: Build and smoke test final Docker runtime image
|
||||
env:
|
||||
DOCKER_BUILDKIT: "1"
|
||||
TARGET_SHA: ${{ needs.resolve_target.outputs.sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
image_ref="openclaw-release-runtime-smoke:${TARGET_SHA}"
|
||||
timeout --kill-after=30s 35m docker build \
|
||||
--build-arg OPENCLAW_EXTENSIONS="diagnostics-otel,codex" \
|
||||
-t "${image_ref}" \
|
||||
.
|
||||
docker run --rm --entrypoint /bin/sh "${image_ref}" -lc '
|
||||
set -eu
|
||||
test -f /app/src/agents/templates/HEARTBEAT.md
|
||||
temp_root="$(mktemp -d)"
|
||||
trap "rm -rf \"${temp_root}\"" EXIT
|
||||
mkdir -p "${temp_root}/home" "${temp_root}/cwd"
|
||||
cd "${temp_root}/cwd"
|
||||
set +e
|
||||
HOME="${temp_root}/home" \
|
||||
USERPROFILE="${temp_root}/home" \
|
||||
OPENCLAW_HOME="${temp_root}/home" \
|
||||
OPENCLAW_NO_ONBOARD=1 \
|
||||
OPENCLAW_SUPPRESS_NOTES=1 \
|
||||
OPENCLAW_DISABLE_BUNDLED_PLUGINS=1 \
|
||||
OPENCLAW_DISABLE_BUNDLED_ENTRY_SOURCE_FALLBACK=1 \
|
||||
AWS_EC2_METADATA_DISABLED=true \
|
||||
AWS_SHARED_CREDENTIALS_FILE="${temp_root}/home/.aws/credentials" \
|
||||
AWS_CONFIG_FILE="${temp_root}/home/.aws/config" \
|
||||
node /app/openclaw.mjs agent --message "workspace bootstrap smoke" --session-id "workspace-bootstrap-smoke" --local --timeout 1 --json \
|
||||
>"${temp_root}/out.log" 2>&1
|
||||
status="$?"
|
||||
set -e
|
||||
if grep -F "Missing workspace template:" "${temp_root}/out.log"; then
|
||||
cat "${temp_root}/out.log"
|
||||
exit 1
|
||||
fi
|
||||
test -f "${temp_root}/home/.openclaw/workspace/HEARTBEAT.md"
|
||||
if [ "${status}" -ne 0 ]; then
|
||||
cat "${temp_root}/out.log"
|
||||
fi
|
||||
'
|
||||
|
||||
normal_ci:
|
||||
name: Run normal full CI
|
||||
needs: [resolve_target, docker_runtime_assets_preflight]
|
||||
@@ -380,6 +337,21 @@ jobs:
|
||||
gh_with_retry api --paginate "repos/${GITHUB_REPOSITORY}/actions/runs/${run_id}/jobs?per_page=100" --jq '.jobs[]'
|
||||
}
|
||||
|
||||
fail_fast_failed_jobs() {
|
||||
local failed_jobs_json
|
||||
failed_jobs_json="$(
|
||||
fetch_child_jobs |
|
||||
jq -s '[.[] | select(.status == "completed" and .conclusion != "success" and .conclusion != "skipped")]'
|
||||
)"
|
||||
if jq -e 'length > 0' <<< "$failed_jobs_json" >/dev/null; then
|
||||
echo "::error::${workflow} has failed child jobs before the workflow completed; cancelling the remaining matrix."
|
||||
jq '.[] | {name, conclusion, url: .html_url}' <<< "$failed_jobs_json"
|
||||
cancel_child
|
||||
trap - EXIT INT TERM
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
cancel_child() {
|
||||
if [[ -n "${run_id:-}" ]]; then
|
||||
echo "Cancelling child workflow ${workflow}: ${run_id}" >&2
|
||||
@@ -395,6 +367,9 @@ jobs:
|
||||
break
|
||||
fi
|
||||
poll_count=$((poll_count + 1))
|
||||
if (( poll_count % 2 == 0 )); then
|
||||
fail_fast_failed_jobs
|
||||
fi
|
||||
if (( poll_count % 10 == 0 )); then
|
||||
echo "Still waiting on ${workflow}: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${run_id}"
|
||||
fetch_child_jobs | jq 'select(.status != "completed") | {name, status, url: .html_url}' || true
|
||||
@@ -510,6 +485,21 @@ jobs:
|
||||
gh_with_retry api --paginate "repos/${GITHUB_REPOSITORY}/actions/runs/${run_id}/jobs?per_page=100" --jq '.jobs[]'
|
||||
}
|
||||
|
||||
fail_fast_failed_jobs() {
|
||||
local failed_jobs_json
|
||||
failed_jobs_json="$(
|
||||
fetch_child_jobs |
|
||||
jq -s '[.[] | select(.status == "completed" and .conclusion != "success" and .conclusion != "skipped")]'
|
||||
)"
|
||||
if jq -e 'length > 0' <<< "$failed_jobs_json" >/dev/null; then
|
||||
echo "::error::${workflow} has failed child jobs before the workflow completed; cancelling the remaining matrix."
|
||||
jq '.[] | {name, conclusion, url: .html_url}' <<< "$failed_jobs_json"
|
||||
cancel_child
|
||||
trap - EXIT INT TERM
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
cancel_child() {
|
||||
if [[ -n "${run_id:-}" ]]; then
|
||||
echo "Cancelling child workflow ${workflow}: ${run_id}" >&2
|
||||
@@ -525,6 +515,9 @@ jobs:
|
||||
break
|
||||
fi
|
||||
poll_count=$((poll_count + 1))
|
||||
if (( poll_count % 2 == 0 )); then
|
||||
fail_fast_failed_jobs
|
||||
fi
|
||||
if (( poll_count % 10 == 0 )); then
|
||||
echo "Still waiting on ${workflow}: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${run_id}"
|
||||
fetch_child_jobs | jq 'select(.status != "completed") | {name, status, url: .html_url}' || true
|
||||
@@ -690,6 +683,24 @@ jobs:
|
||||
[[ "$saw_advisory" == "1" && "$failed" == "0" ]]
|
||||
}
|
||||
|
||||
fail_fast_failed_jobs() {
|
||||
local failed_jobs_json
|
||||
if [[ "$workflow" == "openclaw-release-checks.yml" && "$CHILD_WORKFLOW_REF" =~ ^tideclaw/alpha/[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{4}Z$ ]]; then
|
||||
return 0
|
||||
fi
|
||||
failed_jobs_json="$(
|
||||
fetch_child_jobs |
|
||||
jq -s '[.[] | select(.status == "completed" and .conclusion != "success" and .conclusion != "skipped")]'
|
||||
)"
|
||||
if jq -e 'length > 0' <<< "$failed_jobs_json" >/dev/null; then
|
||||
echo "::error::${workflow} has failed child jobs before the workflow completed; cancelling the remaining matrix."
|
||||
jq '.[] | {name, conclusion, url: .html_url}' <<< "$failed_jobs_json"
|
||||
cancel_child
|
||||
trap - EXIT INT TERM
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
cancel_child() {
|
||||
if [[ -n "${run_id:-}" ]]; then
|
||||
echo "Cancelling child workflow ${workflow}: ${run_id}" >&2
|
||||
@@ -705,6 +716,9 @@ jobs:
|
||||
break
|
||||
fi
|
||||
poll_count=$((poll_count + 1))
|
||||
if (( poll_count % 2 == 0 )); then
|
||||
fail_fast_failed_jobs
|
||||
fi
|
||||
if (( poll_count % 10 == 0 )); then
|
||||
echo "Still waiting on ${workflow}: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${run_id}"
|
||||
fetch_child_jobs | jq 'select(.status != "completed") | {name, status, url: .html_url}' || true
|
||||
@@ -962,6 +976,21 @@ jobs:
|
||||
}
|
||||
trap cancel_child EXIT INT TERM
|
||||
|
||||
fail_fast_failed_jobs() {
|
||||
local failed_jobs_json
|
||||
failed_jobs_json="$(
|
||||
gh_with_retry run view "$run_id" --json jobs \
|
||||
--jq '[.jobs[] | select(.status == "completed" and .conclusion != "success" and .conclusion != "skipped")]'
|
||||
)"
|
||||
if jq -e 'length > 0' <<< "$failed_jobs_json" >/dev/null; then
|
||||
echo "::error::npm-telegram-beta-e2e.yml has failed child jobs before the workflow completed; cancelling the remaining run."
|
||||
jq '.[] | {name, conclusion, url}' <<< "$failed_jobs_json"
|
||||
cancel_child
|
||||
trap - EXIT INT TERM
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
poll_count=0
|
||||
while true; do
|
||||
status="$(gh_with_retry run view "$run_id" --json status --jq '.status')"
|
||||
@@ -969,6 +998,9 @@ jobs:
|
||||
break
|
||||
fi
|
||||
poll_count=$((poll_count + 1))
|
||||
if (( poll_count % 2 == 0 )); then
|
||||
fail_fast_failed_jobs
|
||||
fi
|
||||
if (( poll_count % 10 == 0 )); then
|
||||
echo "Still waiting on npm-telegram-beta-e2e.yml: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${run_id}"
|
||||
gh_with_retry run view "$run_id" --json jobs --jq '.jobs[] | select(.status != "completed") | {name, status, url}' || true
|
||||
|
||||
16
.github/workflows/openclaw-release-checks.yml
vendored
@@ -798,7 +798,7 @@ jobs:
|
||||
- name: Build private QA runtime
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=8192
|
||||
run: pnpm build
|
||||
run: node scripts/build-all.mjs qaRuntime
|
||||
|
||||
- name: Run parity lane
|
||||
env:
|
||||
@@ -876,7 +876,7 @@ jobs:
|
||||
- name: Build private QA runtime
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=8192
|
||||
run: pnpm build
|
||||
run: node scripts/build-all.mjs qaRuntime
|
||||
|
||||
- name: Generate parity report
|
||||
run: |
|
||||
@@ -934,7 +934,7 @@ jobs:
|
||||
- name: Build private QA runtime
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=8192
|
||||
run: pnpm build
|
||||
run: node scripts/build-all.mjs qaRuntime
|
||||
|
||||
- name: Run runtime parity lane
|
||||
id: runtime_parity_lane
|
||||
@@ -1101,7 +1101,7 @@ jobs:
|
||||
- name: Build private QA runtime
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=8192
|
||||
run: pnpm build
|
||||
run: node scripts/build-all.mjs qaRuntime
|
||||
|
||||
- name: Run Matrix live lane
|
||||
id: run_lane
|
||||
@@ -1199,7 +1199,7 @@ jobs:
|
||||
- name: Build private QA runtime
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=8192
|
||||
run: pnpm build
|
||||
run: node scripts/build-all.mjs qaRuntime
|
||||
|
||||
- name: Run Telegram live lane
|
||||
id: run_lane
|
||||
@@ -1295,7 +1295,7 @@ jobs:
|
||||
- name: Build private QA runtime
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=8192
|
||||
run: pnpm build
|
||||
run: node scripts/build-all.mjs qaRuntime
|
||||
|
||||
- name: Run Discord live lane
|
||||
id: run_lane
|
||||
@@ -1393,7 +1393,7 @@ jobs:
|
||||
- name: Build private QA runtime
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=8192
|
||||
run: pnpm build
|
||||
run: node scripts/build-all.mjs qaRuntime
|
||||
|
||||
- name: Run WhatsApp live lane
|
||||
id: run_lane
|
||||
@@ -1488,7 +1488,7 @@ jobs:
|
||||
- name: Build private QA runtime
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=8192
|
||||
run: pnpm build
|
||||
run: node scripts/build-all.mjs qaRuntime
|
||||
|
||||
- name: Run Slack live lane
|
||||
id: run_lane
|
||||
|
||||
25
.github/workflows/openclaw-release-publish.yml
vendored
@@ -46,11 +46,12 @@ on:
|
||||
default: true
|
||||
type: boolean
|
||||
release_profile:
|
||||
description: Release coverage profile used for release evidence summaries
|
||||
description: Release coverage profile used for release evidence summaries; default reads it from the validation manifest
|
||||
required: false
|
||||
default: beta
|
||||
default: from-validation
|
||||
type: choice
|
||||
options:
|
||||
- from-validation
|
||||
- beta
|
||||
- stable
|
||||
- full
|
||||
@@ -135,9 +136,9 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
case "$RELEASE_PROFILE" in
|
||||
beta|stable|full) ;;
|
||||
from-validation|beta|stable|full) ;;
|
||||
*)
|
||||
echo "release_profile must be one of: beta, stable, full" >&2
|
||||
echo "release_profile must be one of: from-validation, beta, stable, full" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
@@ -259,6 +260,7 @@ jobs:
|
||||
echo "sha=$release_sha" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Validate full release validation manifest
|
||||
id: full_manifest
|
||||
if: ${{ inputs.publish_openclaw_npm }}
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
@@ -289,7 +291,7 @@ jobs:
|
||||
echo "Full release validation target SHA mismatch: expected $EXPECTED_SHA, got $target_sha" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ "$release_profile" != "$EXPECTED_RELEASE_PROFILE" ]]; then
|
||||
if [[ "$EXPECTED_RELEASE_PROFILE" != "from-validation" && "$release_profile" != "$EXPECTED_RELEASE_PROFILE" ]]; then
|
||||
echo "Full release validation profile mismatch: expected $EXPECTED_RELEASE_PROFILE, got $release_profile" >&2
|
||||
exit 1
|
||||
fi
|
||||
@@ -297,6 +299,7 @@ jobs:
|
||||
echo "Full release validation must run rerun_group=all before npm publish; got $rerun_group" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "release_profile=$release_profile" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Validate release tag is reachable from a trusted release branch
|
||||
env:
|
||||
@@ -332,7 +335,7 @@ jobs:
|
||||
env:
|
||||
RELEASE_TAG: ${{ inputs.tag }}
|
||||
TARGET_SHA: ${{ steps.manifest.outputs.sha || steps.ref.outputs.sha }}
|
||||
RELEASE_PROFILE: ${{ inputs.release_profile }}
|
||||
RELEASE_PROFILE: ${{ steps.full_manifest.outputs.release_profile || inputs.release_profile }}
|
||||
FULL_RELEASE_VALIDATION_RUN_ID: ${{ inputs.full_release_validation_run_id }}
|
||||
run: |
|
||||
{
|
||||
@@ -501,7 +504,7 @@ jobs:
|
||||
wait_for_run() {
|
||||
local workflow="$1"
|
||||
local run_id="$2"
|
||||
local status conclusion url updated_at created_at duration_seconds duration_label last_state
|
||||
local status conclusion url updated_at created_at duration_seconds duration_label last_state failed_json
|
||||
|
||||
last_state=""
|
||||
while true; do
|
||||
@@ -510,6 +513,14 @@ jobs:
|
||||
if [[ "$status" == "completed" ]]; then
|
||||
break
|
||||
fi
|
||||
failed_json="$(gh run view --repo "$GITHUB_REPOSITORY" "$run_id" --json jobs \
|
||||
--jq '[.jobs[] | select(.status == "completed" and .conclusion != "success" and .conclusion != "skipped")]' || true)"
|
||||
if [[ -n "${failed_json}" ]] && jq -e 'length > 0' <<< "$failed_json" >/dev/null; then
|
||||
echo "${workflow} has failed jobs before the workflow completed: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${run_id}" >&2
|
||||
jq '.[] | {name, conclusion, url}' <<< "$failed_json" >&2 || true
|
||||
print_failed_run_summary "${run_id}"
|
||||
return 1
|
||||
fi
|
||||
url="$(printf '%s' "$run_json" | jq -r '.url')"
|
||||
updated_at="$(printf '%s' "$run_json" | jq -r '.updatedAt')"
|
||||
state="${status}:${updated_at}"
|
||||
|
||||
@@ -818,6 +818,7 @@ jobs:
|
||||
OPENCLAW_QA_CONVEX_SECRET_CI: ${{ secrets.OPENCLAW_QA_CONVEX_SECRET_CI }}
|
||||
OPENCLAW_QA_REDACT_PUBLIC_METADATA: "1"
|
||||
OPENCLAW_QA_SLACK_CAPTURE_CONTENT: "1"
|
||||
OPENCLAW_QA_TRANSPORT_READY_TIMEOUT_MS: "180000"
|
||||
INPUT_SCENARIO: ${{ github.event_name == 'workflow_dispatch' && inputs.slack_scenario || '' }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
61
CHANGELOG.md
@@ -2,7 +2,7 @@
|
||||
|
||||
Docs: https://docs.openclaw.ai
|
||||
|
||||
## 2026.6.1
|
||||
## 2026.6.2
|
||||
|
||||
### Highlights
|
||||
|
||||
@@ -45,6 +45,61 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- Discord: match the shipped `libopus-wasm` error shape so corrupt voice packets are treated as decode noise instead of crashing receive recovery.
|
||||
- Canvas: restore A2UI Google, X, and legacy Granola compatibility image assets in the bundled host payload.
|
||||
- Agents/providers: avoid loading owner plugin runtimes for explicitly configured custom provider models during OpenAI-compatible transport setup.
|
||||
- Tooling: fail Codex app-server protocol generation before invoking Cargo when local disk headroom is too low.
|
||||
- Release/CI/E2E: fail early when Crabbox sparse-sync full checkouts do not have enough local disk, with guidance for moving the sync root.
|
||||
- Release/CI/E2E: reset shared Crabbox pnpm hydrate state before installs so stale `/var/tmp` stores cannot leave `pnpm install` spinning after completion.
|
||||
- Release/CI/E2E: print heartbeat progress during centralized Docker builds while keeping successful build logs quiet.
|
||||
- Release/CI/E2E: avoid heartbeat-tail delays in Docker E2E log wrappers while reporting captured log bytes during long runs.
|
||||
- Release/CI/E2E: keep release user-journey logs and temporary plugin fixtures under per-run scratch roots so parallel runs cannot collide or leak artifacts.
|
||||
- Release/CI/E2E: bound release candidate GitHub API calls so stalled network requests cannot wedge workflow and artifact polling.
|
||||
- Release/CI/E2E: bound Discord smoke API calls in cross-OS release checks so host-side round trips cannot hang on stalled fetches.
|
||||
- Release/CI/E2E: bound RPC RTT gateway readiness probes so a half-open local HTTP response cannot stall cleanup past the readiness deadline.
|
||||
- Release/CI/E2E: stop RPC RTT gateway process groups so pnpm wrapper children cannot survive measurement cleanup.
|
||||
- Release/CI/E2E: fail the kitchen-sink RPC walk when command RSS sampling captures no process samples.
|
||||
- Release/CI/E2E: fail kitchen-sink RPC commands that exit cleanly only after their timeout expires.
|
||||
- Release/CI/E2E: force-stop memory/fd repro gateway children that survive listener cleanup.
|
||||
- Release/CI/E2E: remove fallback ClawHub skill-install home directories when proof runs fail.
|
||||
- Release/CI/E2E: let plugin lifecycle measurement wrappers exit promptly after external shutdown while preserving descendant cleanup.
|
||||
- Gateway: cancel client stop fallback termination when the socket closes normally during shutdown.
|
||||
- Installers: fail the PowerShell installer when interactive onboarding exits non-zero.
|
||||
- Scripts/UI: stop descendant processes from wrapped non-interactive commands when `run-with-env` receives shutdown signals.
|
||||
- Release/CI/E2E: write multi-node update Docker artifacts to unique per-run directories by default so parallel runs cannot overwrite evidence.
|
||||
- Release/CI/E2E: write package Telegram Docker artifacts to unique per-run directories by default so parallel live/RTT runs cannot overwrite evidence.
|
||||
- Release/CI/E2E: keep plugin lifecycle matrix resource artifacts under a unique per-run scratch root so parallel runs cannot overwrite tarballs or inspect output.
|
||||
- Release/CI/E2E: bound mock OpenAI readiness probes in web-search and Telegram RTT Docker smokes so stalled HTTP accepts cannot hang cleanup or fall through.
|
||||
- Tooling: cancel oversized pnpm audit advisory responses before failing so registry error paths do not leave response bodies open.
|
||||
- Release/CI/E2E: stop tracked gateway and mock service process groups so descendant helpers do not survive E2E cleanup.
|
||||
- Release/CI/E2E: exit Telegram credential proof wrappers promptly after forwarded shutdown signals while keeping the descendant force-kill guard armed.
|
||||
- Release/CI/E2E: reject oversized ClickClack fixture request bodies before release journey smokes can accumulate unbounded payloads.
|
||||
- Release/CI/E2E: reject oversized OpenAI image-auth mock request bodies before Docker proof runs can accumulate unbounded payloads.
|
||||
- Release/CI/E2E: require the Kitchen Sink RPC walk to prove every expected plugin tool is cataloged and effective before invoking tool fixtures.
|
||||
- Release/CI/E2E: stop tracked Docker build commands when centralized build wrappers receive shutdown signals.
|
||||
- Release/CI/E2E: cover MCP channel pairing reconnects by asserting the same temporary client state is reused across reconnects.
|
||||
- Release/CI/E2E: require QA channel baseline and reconnect scenarios to assert their scenario markers instead of accepting any outbound reply.
|
||||
- Release/CI/E2E: fail secret-provider proof runs when temporary state cleanup still fails after retries instead of hiding the cleanup error.
|
||||
- Release/CI/E2E: fail package-candidate ref proofs when temporary source worktree cleanup fails instead of leaving stale worktrees behind.
|
||||
- Release/CI/E2E: remove package tarball extract directories when tar extraction fails before validation can continue.
|
||||
- Release/CI/E2E: retry generated temp-state cleanup after removal failures and route plugin lifecycle measurement edits to their owner tests.
|
||||
- Release/CI/E2E: close parent gateway log handles after spawning RPC RTT probes so repeated measurements do not leak file descriptors.
|
||||
- Release/CI/E2E: fail RPC RTT probes when temporary state cleanup fails instead of hiding leftover scratch directories.
|
||||
- Release/CI/E2E: fail Kitchen Sink RPC walks when temporary state cleanup still fails after retries instead of silently preserving scratch roots.
|
||||
- Control UI: lazy-load the usage view so the initial app bundle stays below the chunk warning threshold.
|
||||
- Build: keep Baileys optional image backends external so source builds do not warn about missing `jimp` or `sharp`.
|
||||
- Build: render independent CLI startup metadata help snapshots concurrently to cut cold build-all metadata time.
|
||||
- Plugins: stop timed-out package-boundary prep steps by process group so descendant TypeScript/helper processes do not survive local check cleanup.
|
||||
- Control UI: serve static assets asynchronously after safe-open checks so large UI files do not block Gateway request handling.
|
||||
- Scripts/UI: forward direct wrapper SIGHUP shutdown to child processes so terminal hangups do not leave wrapped dev commands running.
|
||||
- Gateway: return the post-expiration pending-work revision from node drains so reconnecting nodes do not observe stale queue revisions after expired items are pruned.
|
||||
- Release/CI/E2E: keep temporary full-sync checkouts alive while slow Crabbox leases boot, so sparse worktree runs do not lose their sync source before file-list generation.
|
||||
- Release/CI/E2E: normalize inherited Linux `C.UTF-8` locale settings before raw AWS macOS Crabbox bootstrap commands, avoiding macOS locale warnings during package-manager hydration.
|
||||
- Release/CI/E2E: keep gateway watch regression checks from copying large static plugin assets inside the measured idle window.
|
||||
- Update: keep core updates nonblocking when a missing external plugin repair download stalls, while still blocking installed active plugin payload smoke failures.
|
||||
- Agents/providers: keep streaming tool-call argument parsing record-shaped when providers emit valid non-object JSON such as `null` or arrays.
|
||||
- Release/CI/E2E: reset incremental log readers when watched log files rotate without shrinking, so same-size replacements do not hide new readiness or RPC lines.
|
||||
- Talk: preserve explicit `null` payloads on controller-created turn and output-audio lifecycle events.
|
||||
- Agents/TUI: keep local custom provider runs from loading plugin runtime and auth alias metadata when plugins are disabled.
|
||||
- Agents/TUI: restore in-flight TUI run switch-back behavior, keep no-policy native hook fallback available, guard vanished workspaces, and keep lightweight isolated subagents lightweight.
|
||||
- Agents/media: keep async image, music, and video generation starts from ending the Codex turn, so mixed requests can continue with summaries or other work while media renders in the background.
|
||||
@@ -54,6 +109,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Agents/Codex: surface Skill Workshop guidance in Codex app-server prompts when `skill_workshop` is available. Thanks @shakkernerd.
|
||||
- Agents/auth: write auth profiles atomically, add force re-login recovery, preserve workspaces during state-only uninstall, and compact before oversized turns so recovery paths avoid partial state.
|
||||
- Skills: skip disabled skill env overrides from stale persisted snapshots so disabled skill `apiKey` SecretRefs cannot abort embedded or channel turns. (#79072, #79173) Thanks @zeus1959.
|
||||
- Skill Workshop: render the Control UI tab from filtered navigation state and keep filtered fallback routing stable.
|
||||
- CLI: avoid live catalog validation during `openclaw agents add`, so adding a secondary agent no longer depends on provider catalog availability. (#76284, #88314) Thanks @zhangguiping-xydt.
|
||||
- CLI: keep `plugins list --json` on the snapshot-only path so plugin sweeps avoid loading the full runtime status graph.
|
||||
- CLI/desktop: bridge WSL clipboard operations through the shell and recognize manual-update launchd jobs. (#88764)
|
||||
@@ -63,6 +119,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Cron: keep SQLite cron migrations compatible with legacy run-log tables, archived job stores, diagnostic cron names, and legacy one-shot delete-after-run behavior. (#88285)
|
||||
- Cron: keep update delivery validation scoped, harden restart state, and retire MCP runtimes on isolated cron cleanup.
|
||||
- Memory: serialize QMD update/embed writes per store, preserve phase signals on read errors, harden envelope metadata sanitization, and rewrite generated transcript paths on rollover so memory/search state survives concurrent gateway and CLI activity. (#66339, #85931) Thanks @openperf and @amittell.
|
||||
- Memory: keep vector-disabled FTS indexes from resolving embedding providers during sync and search.
|
||||
- Providers: bound generated media downloads from OpenAI, Runway, xAI, MiniMax, BytePlus, DashScope-compatible, FAL, OpenRouter, Google, Vydra, and Comfy providers.
|
||||
- Providers: resolve Google defaults to `google-generative-ai`, register Vertex static catalog rows, align Foundry reasoning metadata, skip DeepSeek V4 thinking params on Foundry fallback, use MiniMax account OAuth endpoints, preserve Copilot Claude 1M capabilities, suppress disabled Ollama reasoning output, keep OpenAI stop-finished tool calls, and avoid replay ids when the Responses store is disabled. (#88480, #88512)
|
||||
- Providers: cap GitHub Copilot OAuth request timeouts before creating abort signals.
|
||||
@@ -83,6 +140,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Channels: preserve long Feishu streaming replies, send visible fallbacks when accepted Feishu turns produce no final reply, tolerate iMessage self-chat timestamp skew, preserve colon-prefixed slash commands in mention parsing, decode Nostr `npub` allowlists correctly, and suppress raw provider errors during channel delivery. (#87896)
|
||||
- Config/status/doctor: skip unresolved shell references in state-dir dotenv files, resolve gateway auth secrets during deep status audits, respect explicit PI runtime policy, report runtime tool-schema errors, and keep post-upgrade JSON stable. (#88288)
|
||||
- Gateway/session state: list commands from the Gateway plugin registry, harden MCP loopback tool schemas, hide phantom agent-store rows from `sessions.list`, make task persistence failures explicit, and carry session UUIDs on interactive dispatch events.
|
||||
- Gateway/plugins: narrow plugin lookup memoization to the stable plugin/runtime inputs, avoiding repeated lookup work without mixing disabled or filtered plugin state.
|
||||
- OpenAI/TTS: handle speed directives for OpenAI TTS voices. (#74089)
|
||||
- CI/Crabbox: keep default runner capacity on the Azure credit-backed on-demand D4 lane with the Azure SSH port and a Git-independent full check job, so broad validation avoids low-priority spot quota stalls, hydrate port mismatches, non-Git hydrated workspaces, and stale AWS region hints.
|
||||
- CI/Crabbox: route Crabbox wrapper and Testbox workflow edits to their regression tests so changed-test gates do not silently run zero specs.
|
||||
@@ -657,6 +715,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Gateway/sessions: allow shared-secret bearer callers to read and stream session history without an explicit scope header. (#81815) Thanks @medns.
|
||||
- Agents/embedded runner: classify HTML auth provider responses as `auth_html` and return a re-authentication hint instead of the CDN-blocked copy that `upstream_html` returns. Cloudflare Access login pages, nginx basic-auth challenges, and gateway login walls all produce HTML auth bodies that were previously misdiagnosed as transient CDN blocks. (#79900) Thanks @martingarramon.
|
||||
- TUI/streaming watchdog: dismiss the `This response is taking longer than expected` notice as soon as a chat event for the same run arrives, so the message no longer sits next to the recovered response when the run was only briefly silent. Refs #67052, #69081 (closed), prior attempt #69026. Thanks @jpruit20 and @romneyda.
|
||||
- Agents/auth profiles: replace the bare `No available auth profile for <provider> (all in cooldown or unavailable)` TUI error with plain-language copy that explains what happened in user terms (sign-in expired, provider asking us to slow down, billing issue on the account, etc.) and suggests the matching `openclaw models auth login --provider <provider>` recovery command for sign-in and billing causes, while falling back to the underlying provider error for cases without a clear recovery path. Thanks @romneyda.
|
||||
- Agents/Pi: tolerate OpenClaw-owned transcript writes while embedded prompts are released for model I/O, keeping long-running Feishu, Slack, Telegram, and cron turns from failing with false session-takeover errors. Fixes #84059. (#84250) Thanks @tianxiaochannel-oss88.
|
||||
|
||||
## 2026.5.20
|
||||
|
||||
@@ -218,6 +218,7 @@ Current OpenClaw Android implication:
|
||||
- Google Play build excludes SMS send/search, Call Log search, and recent-photo access unless the product is intentionally positioned and approved under the relevant policy exception.
|
||||
- The repo now ships this split as Android product flavors:
|
||||
- `play`: removes `READ_SMS`, `SEND_SMS`, `READ_CALL_LOG`, `READ_MEDIA_IMAGES`, `READ_MEDIA_VISUAL_USER_SELECTED`, and `READ_EXTERNAL_STORAGE`; hides SMS, Call Log, and Photos surfaces in onboarding, settings, and advertised node capabilities.
|
||||
- Installed-app listing is user controlled. `device.apps` is advertised only after the user enables **Settings > Phone Capabilities > Installed Apps**. The command defaults to launcher-visible apps and does not require `QUERY_ALL_PACKAGES`.
|
||||
- `thirdParty`: keeps the full permission set and the existing SMS / Call Log / Photos functionality.
|
||||
|
||||
Policy links:
|
||||
|
||||
@@ -65,8 +65,8 @@ android {
|
||||
applicationId = "ai.openclaw.app"
|
||||
minSdk = 31
|
||||
targetSdk = 36
|
||||
versionCode = 2026053101
|
||||
versionName = "2026.6.1"
|
||||
versionCode = 2026060201
|
||||
versionName = "2026.6.2"
|
||||
ndk {
|
||||
// Support all major ABIs — native libs are tiny (~47 KB per ABI)
|
||||
abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
|
||||
|
||||
@@ -148,6 +148,7 @@ class MainViewModel(
|
||||
val gatewayBootstrapToken: StateFlow<String> = prefs.gatewayBootstrapToken
|
||||
val onboardingCompleted: StateFlow<Boolean> = prefs.onboardingCompleted
|
||||
val canvasDebugStatusEnabled: StateFlow<Boolean> = prefs.canvasDebugStatusEnabled
|
||||
val installedAppsSharingEnabled: StateFlow<Boolean> = prefs.installedAppsSharingEnabled
|
||||
val speakerEnabled: StateFlow<Boolean> = prefs.speakerEnabled
|
||||
val voiceCaptureMode: StateFlow<VoiceCaptureMode> = runtimeState(initial = VoiceCaptureMode.Off) { it.voiceCaptureMode }
|
||||
val micEnabled: StateFlow<Boolean> = runtimeState(initial = false) { it.micEnabled }
|
||||
@@ -299,6 +300,10 @@ class MainViewModel(
|
||||
prefs.setCanvasDebugStatusEnabled(value)
|
||||
}
|
||||
|
||||
fun setInstalledAppsSharingEnabled(value: Boolean) {
|
||||
ensureRuntime().setInstalledAppsSharingEnabled(value)
|
||||
}
|
||||
|
||||
fun setNotificationForwardingEnabled(value: Boolean) {
|
||||
ensureRuntime().setNotificationForwardingEnabled(value)
|
||||
}
|
||||
|
||||
@@ -207,6 +207,7 @@ class NodeRuntime(
|
||||
callLogAvailable = { SensitiveFeatureConfig.callLogEnabled },
|
||||
photosAvailable = { SensitiveFeatureConfig.photosEnabled },
|
||||
hasRecordAudioPermission = { hasRecordAudioPermission() },
|
||||
installedAppsSharingEnabled = { installedAppsSharingEnabled.value },
|
||||
manualTls = { manualTls.value },
|
||||
)
|
||||
|
||||
@@ -245,6 +246,7 @@ class NodeRuntime(
|
||||
smsTelephonyAvailable = { sms.hasTelephonyFeature() },
|
||||
callLogAvailable = { SensitiveFeatureConfig.callLogEnabled },
|
||||
photosAvailable = { SensitiveFeatureConfig.photosEnabled },
|
||||
installedAppsSharingEnabled = { installedAppsSharingEnabled.value },
|
||||
debugBuild = { BuildConfig.DEBUG },
|
||||
onCanvasA2uiPush = {
|
||||
_canvasA2uiHydrated.value = true
|
||||
@@ -866,6 +868,7 @@ class NodeRuntime(
|
||||
|
||||
val lastDiscoveredStableId: StateFlow<String> = prefs.lastDiscoveredStableId
|
||||
val canvasDebugStatusEnabled: StateFlow<Boolean> = prefs.canvasDebugStatusEnabled
|
||||
val installedAppsSharingEnabled: StateFlow<Boolean> = prefs.installedAppsSharingEnabled
|
||||
val notificationForwardingEnabled: StateFlow<Boolean> = prefs.notificationForwardingEnabled
|
||||
val notificationForwardingMode: StateFlow<NotificationPackageFilterMode> =
|
||||
prefs.notificationForwardingMode
|
||||
@@ -1077,6 +1080,12 @@ class NodeRuntime(
|
||||
prefs.setCanvasDebugStatusEnabled(value)
|
||||
}
|
||||
|
||||
fun setInstalledAppsSharingEnabled(value: Boolean) {
|
||||
if (prefs.installedAppsSharingEnabled.value == value) return
|
||||
prefs.setInstalledAppsSharingEnabled(value)
|
||||
refreshNodeSurfaceAfterSharingChange()
|
||||
}
|
||||
|
||||
fun setNotificationForwardingEnabled(value: Boolean) {
|
||||
prefs.setNotificationForwardingEnabled(value)
|
||||
}
|
||||
@@ -1414,6 +1423,11 @@ class NodeRuntime(
|
||||
connectWithAuth(endpoint = endpoint, auth = resolveGatewayConnectAuth(), reconnect = true)
|
||||
}
|
||||
|
||||
private fun refreshNodeSurfaceAfterSharingChange() {
|
||||
val endpoint = connectedEndpoint ?: return
|
||||
connectWithAuth(endpoint = endpoint, auth = resolveGatewayConnectAuth(), reconnect = true)
|
||||
}
|
||||
|
||||
private fun connectWithAuth(
|
||||
endpoint: GatewayEndpoint,
|
||||
auth: GatewayConnectAuth,
|
||||
|
||||
@@ -40,11 +40,13 @@ class SecurePrefs(
|
||||
private const val notificationsForwardingMaxEventsPerMinuteKey =
|
||||
"notifications.forwarding.maxEventsPerMinute"
|
||||
private const val notificationsForwardingSessionKeyKey = "notifications.forwarding.sessionKey"
|
||||
private const val installedAppsSharingEnabledKey = "device.apps.sharing.enabled"
|
||||
private const val voiceMicEnabledKey = "voice.micEnabled"
|
||||
}
|
||||
|
||||
private val appContext = context.applicationContext
|
||||
private val json = Json { ignoreUnknownKeys = true }
|
||||
|
||||
// Non-secret UI/runtime preferences stay readable for migration and backup behavior.
|
||||
private val plainPrefs: SharedPreferences =
|
||||
appContext.getSharedPreferences(plainPrefsName, Context.MODE_PRIVATE)
|
||||
@@ -114,6 +116,10 @@ class SecurePrefs(
|
||||
MutableStateFlow(plainPrefs.getBoolean("canvas.debugStatusEnabled", false))
|
||||
val canvasDebugStatusEnabled: StateFlow<Boolean> = _canvasDebugStatusEnabled
|
||||
|
||||
private val _installedAppsSharingEnabled =
|
||||
MutableStateFlow(plainPrefs.getBoolean(installedAppsSharingEnabledKey, false))
|
||||
val installedAppsSharingEnabled: StateFlow<Boolean> = _installedAppsSharingEnabled
|
||||
|
||||
private val _notificationForwardingEnabled =
|
||||
MutableStateFlow(plainPrefs.getBoolean(notificationsForwardingEnabledKey, defaultNotificationForwardingEnabled))
|
||||
val notificationForwardingEnabled: StateFlow<Boolean> = _notificationForwardingEnabled
|
||||
@@ -252,6 +258,11 @@ class SecurePrefs(
|
||||
_canvasDebugStatusEnabled.value = value
|
||||
}
|
||||
|
||||
fun setInstalledAppsSharingEnabled(value: Boolean) {
|
||||
plainPrefs.edit { putBoolean(installedAppsSharingEnabledKey, value) }
|
||||
_installedAppsSharingEnabled.value = value
|
||||
}
|
||||
|
||||
internal fun getNotificationForwardingPolicy(appPackageName: String): NotificationForwardingPolicy {
|
||||
val modeRaw = plainPrefs.getString(notificationsForwardingModeKey, null)
|
||||
val mode = NotificationPackageFilterMode.fromRawValue(modeRaw)
|
||||
|
||||
@@ -28,6 +28,7 @@ class ConnectionManager(
|
||||
private val callLogAvailable: () -> Boolean,
|
||||
private val photosAvailable: () -> Boolean,
|
||||
private val hasRecordAudioPermission: () -> Boolean,
|
||||
private val installedAppsSharingEnabled: () -> Boolean,
|
||||
private val manualTls: () -> Boolean,
|
||||
) {
|
||||
companion object {
|
||||
@@ -115,6 +116,7 @@ class ConnectionManager(
|
||||
voiceWakeEnabled = voiceWakeMode() != VoiceWakeMode.Off && hasRecordAudioPermission(),
|
||||
motionActivityAvailable = motionActivityAvailable(),
|
||||
motionPedometerAvailable = motionPedometerAvailable(),
|
||||
installedAppsSharingEnabled = installedAppsSharingEnabled(),
|
||||
debugBuild = BuildConfig.DEBUG,
|
||||
)
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import android.app.ActivityManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.NetworkCapabilities
|
||||
@@ -24,16 +25,121 @@ import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.put
|
||||
import java.util.Locale
|
||||
|
||||
private const val DEFAULT_DEVICE_APPS_LIMIT = 100
|
||||
private const val MAX_DEVICE_APPS_LIMIT = 200
|
||||
private const val DEVICE_APPS_SYSTEM_FLAGS =
|
||||
ApplicationInfo.FLAG_SYSTEM or ApplicationInfo.FLAG_UPDATED_SYSTEM_APP
|
||||
|
||||
internal fun isSystemDeviceApp(appInfo: ApplicationInfo): Boolean =
|
||||
(appInfo.flags and DEVICE_APPS_SYSTEM_FLAGS) != 0
|
||||
|
||||
internal data class DeviceAppEntry(
|
||||
val label: String,
|
||||
val packageName: String,
|
||||
val system: Boolean,
|
||||
val enabled: Boolean,
|
||||
val launchable: Boolean,
|
||||
)
|
||||
|
||||
internal interface DeviceAppSource {
|
||||
fun listApps(includeNonLaunchable: Boolean): List<DeviceAppEntry>
|
||||
}
|
||||
|
||||
private class AndroidDeviceAppSource(
|
||||
private val appContext: Context,
|
||||
) : DeviceAppSource {
|
||||
override fun listApps(includeNonLaunchable: Boolean): List<DeviceAppEntry> {
|
||||
val packageManager = appContext.packageManager
|
||||
val launcherIntent = Intent(Intent.ACTION_MAIN).apply { addCategory(Intent.CATEGORY_LAUNCHER) }
|
||||
val launchablePackages =
|
||||
packageManager
|
||||
.queryIntentActivities(launcherIntent, PackageManager.MATCH_ALL)
|
||||
.asSequence()
|
||||
.mapNotNull {
|
||||
it.activityInfo
|
||||
?.packageName
|
||||
?.trim()
|
||||
?.takeIf(String::isNotEmpty)
|
||||
}.toSet()
|
||||
|
||||
val appInfos =
|
||||
if (includeNonLaunchable) {
|
||||
packageManager.getInstalledApplications(PackageManager.MATCH_ALL)
|
||||
} else {
|
||||
launchablePackages.mapNotNull { packageName ->
|
||||
runCatching { packageManager.getApplicationInfo(packageName, 0) }.getOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
return appInfos
|
||||
.asSequence()
|
||||
.mapNotNull { appInfo ->
|
||||
appInfo.packageName
|
||||
?.trim()
|
||||
?.takeIf(String::isNotEmpty)
|
||||
?.let { packageName ->
|
||||
val label = packageManager.getApplicationLabel(appInfo).toString().trim()
|
||||
DeviceAppEntry(
|
||||
label = label.ifEmpty { packageName },
|
||||
packageName = packageName,
|
||||
system = isSystemDeviceApp(appInfo),
|
||||
enabled = appInfo.enabled,
|
||||
launchable = packageName in launchablePackages,
|
||||
)
|
||||
}
|
||||
}.distinctBy { it.packageName }
|
||||
.sortedWith(compareBy<DeviceAppEntry> { it.label.lowercase() }.thenBy { it.packageName })
|
||||
.toList()
|
||||
}
|
||||
}
|
||||
|
||||
private data class DeviceAppsRequest(
|
||||
val includeSystem: Boolean,
|
||||
val includeDisabled: Boolean,
|
||||
val includeNonLaunchable: Boolean,
|
||||
val query: String?,
|
||||
val limit: Int,
|
||||
)
|
||||
|
||||
/**
|
||||
* Gateway device command adapter for Android status, info, permission, and health snapshots.
|
||||
*/
|
||||
class DeviceHandler(
|
||||
class DeviceHandler private constructor(
|
||||
private val appContext: Context,
|
||||
private val smsEnabled: Boolean = SensitiveFeatureConfig.smsEnabled,
|
||||
private val callLogEnabled: Boolean = SensitiveFeatureConfig.callLogEnabled,
|
||||
private val photosEnabled: Boolean = SensitiveFeatureConfig.photosEnabled,
|
||||
private val appSource: DeviceAppSource = AndroidDeviceAppSource(appContext),
|
||||
) {
|
||||
constructor(
|
||||
appContext: Context,
|
||||
smsEnabled: Boolean = SensitiveFeatureConfig.smsEnabled,
|
||||
callLogEnabled: Boolean = SensitiveFeatureConfig.callLogEnabled,
|
||||
photosEnabled: Boolean = SensitiveFeatureConfig.photosEnabled,
|
||||
) : this(
|
||||
appContext = appContext,
|
||||
smsEnabled = smsEnabled,
|
||||
callLogEnabled = callLogEnabled,
|
||||
photosEnabled = photosEnabled,
|
||||
appSource = AndroidDeviceAppSource(appContext),
|
||||
)
|
||||
|
||||
companion object {
|
||||
internal fun forTesting(
|
||||
appContext: Context,
|
||||
appSource: DeviceAppSource,
|
||||
smsEnabled: Boolean = SensitiveFeatureConfig.smsEnabled,
|
||||
callLogEnabled: Boolean = SensitiveFeatureConfig.callLogEnabled,
|
||||
photosEnabled: Boolean = SensitiveFeatureConfig.photosEnabled,
|
||||
): DeviceHandler =
|
||||
DeviceHandler(
|
||||
appContext = appContext,
|
||||
smsEnabled = smsEnabled,
|
||||
callLogEnabled = callLogEnabled,
|
||||
photosEnabled = photosEnabled,
|
||||
appSource = appSource,
|
||||
)
|
||||
|
||||
/**
|
||||
* SMS is available only when the feature flag, telephony hardware, and at least one SMS permission align.
|
||||
*/
|
||||
@@ -74,6 +180,48 @@ class DeviceHandler(
|
||||
/** Returns coarse device health for memory, power, thermal, battery, and security patch state. */
|
||||
fun handleDeviceHealth(_paramsJson: String?): GatewaySession.InvokeResult = GatewaySession.InvokeResult.ok(healthPayloadJson())
|
||||
|
||||
fun handleDeviceApps(paramsJson: String?): GatewaySession.InvokeResult {
|
||||
val request = parseDeviceAppsRequest(paramsJson)
|
||||
val matchingApps =
|
||||
appSource
|
||||
.listApps(includeNonLaunchable = request.includeNonLaunchable)
|
||||
.asSequence()
|
||||
.filter { request.includeSystem || !it.system }
|
||||
.filter { request.includeDisabled || it.enabled }
|
||||
.filter { app ->
|
||||
val query = request.query ?: return@filter true
|
||||
app.label.contains(query, ignoreCase = true) || app.packageName.contains(query, ignoreCase = true)
|
||||
}.toList()
|
||||
val limitedApps = matchingApps.take(request.limit)
|
||||
|
||||
return GatewaySession.InvokeResult.ok(
|
||||
buildJsonObject {
|
||||
put("count", JsonPrimitive(limitedApps.size))
|
||||
put("totalMatched", JsonPrimitive(matchingApps.size))
|
||||
put("truncated", JsonPrimitive(matchingApps.size > limitedApps.size))
|
||||
put("visibility", JsonPrimitive(if (request.includeNonLaunchable) "android-visible" else "launcher"))
|
||||
put("includeSystem", JsonPrimitive(request.includeSystem))
|
||||
put("includeDisabled", JsonPrimitive(request.includeDisabled))
|
||||
put(
|
||||
"apps",
|
||||
buildJsonArray {
|
||||
for (app in limitedApps) {
|
||||
add(
|
||||
buildJsonObject {
|
||||
put("label", JsonPrimitive(app.label))
|
||||
put("packageName", JsonPrimitive(app.packageName))
|
||||
put("system", JsonPrimitive(app.system))
|
||||
put("enabled", JsonPrimitive(app.enabled))
|
||||
put("launchable", JsonPrimitive(app.launchable))
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}.toString(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun statusPayloadJson(): String {
|
||||
val battery = readBatterySnapshot()
|
||||
val powerManager = appContext.getSystemService(PowerManager::class.java)
|
||||
@@ -365,6 +513,24 @@ class DeviceHandler(
|
||||
}.toString()
|
||||
}
|
||||
|
||||
private fun parseDeviceAppsRequest(paramsJson: String?): DeviceAppsRequest {
|
||||
val params = parseJsonParamsObject(paramsJson)
|
||||
val includeSystem = parseJsonBooleanFlag(params, "includeSystem") ?: false
|
||||
val includeDisabled = parseJsonBooleanFlag(params, "includeDisabled") ?: false
|
||||
val includeNonLaunchable = parseJsonBooleanFlag(params, "includeNonLaunchable") ?: false
|
||||
val query = parseJsonString(params, "query")?.trim()?.takeIf { it.isNotEmpty() }
|
||||
val limit =
|
||||
(parseJsonInt(params, "limit") ?: DEFAULT_DEVICE_APPS_LIMIT)
|
||||
.coerceIn(1, MAX_DEVICE_APPS_LIMIT)
|
||||
return DeviceAppsRequest(
|
||||
includeSystem = includeSystem,
|
||||
includeDisabled = includeDisabled,
|
||||
includeNonLaunchable = includeNonLaunchable,
|
||||
query = query,
|
||||
limit = limit,
|
||||
)
|
||||
}
|
||||
|
||||
private fun readBatterySnapshot(): BatterySnapshot {
|
||||
// ACTION_BATTERY_CHANGED is sticky; registerReceiver(null, ...) reads the last system snapshot.
|
||||
val intent = appContext.registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
|
||||
|
||||
@@ -28,6 +28,7 @@ data class NodeRuntimeFlags(
|
||||
val voiceWakeEnabled: Boolean,
|
||||
val motionActivityAvailable: Boolean,
|
||||
val motionPedometerAvailable: Boolean,
|
||||
val installedAppsSharingEnabled: Boolean,
|
||||
val debugBuild: Boolean,
|
||||
)
|
||||
|
||||
@@ -43,6 +44,7 @@ enum class InvokeCommandAvailability {
|
||||
PhotosAvailable,
|
||||
MotionActivityAvailable,
|
||||
MotionPedometerAvailable,
|
||||
InstalledAppsSharingEnabled,
|
||||
DebugBuild,
|
||||
}
|
||||
|
||||
@@ -193,6 +195,10 @@ object InvokeCommandRegistry {
|
||||
InvokeCommandSpec(
|
||||
name = OpenClawDeviceCommand.Health.rawValue,
|
||||
),
|
||||
InvokeCommandSpec(
|
||||
name = OpenClawDeviceCommand.Apps.rawValue,
|
||||
availability = InvokeCommandAvailability.InstalledAppsSharingEnabled,
|
||||
),
|
||||
InvokeCommandSpec(
|
||||
name = OpenClawNotificationsCommand.List.rawValue,
|
||||
),
|
||||
@@ -281,6 +287,7 @@ object InvokeCommandRegistry {
|
||||
InvokeCommandAvailability.PhotosAvailable -> flags.photosAvailable
|
||||
InvokeCommandAvailability.MotionActivityAvailable -> flags.motionActivityAvailable
|
||||
InvokeCommandAvailability.MotionPedometerAvailable -> flags.motionPedometerAvailable
|
||||
InvokeCommandAvailability.InstalledAppsSharingEnabled -> flags.installedAppsSharingEnabled
|
||||
InvokeCommandAvailability.DebugBuild -> flags.debugBuild
|
||||
}
|
||||
}.map { it.name }
|
||||
|
||||
@@ -85,6 +85,7 @@ class InvokeDispatcher(
|
||||
private val smsTelephonyAvailable: () -> Boolean,
|
||||
private val callLogAvailable: () -> Boolean,
|
||||
private val photosAvailable: () -> Boolean,
|
||||
private val installedAppsSharingEnabled: () -> Boolean,
|
||||
private val debugBuild: () -> Boolean,
|
||||
private val onCanvasA2uiPush: () -> Unit,
|
||||
private val onCanvasA2uiReset: () -> Unit,
|
||||
@@ -193,6 +194,7 @@ class InvokeDispatcher(
|
||||
OpenClawDeviceCommand.Info.rawValue -> deviceHandler.handleDeviceInfo(paramsJson)
|
||||
OpenClawDeviceCommand.Permissions.rawValue -> deviceHandler.handleDevicePermissions(paramsJson)
|
||||
OpenClawDeviceCommand.Health.rawValue -> deviceHandler.handleDeviceHealth(paramsJson)
|
||||
OpenClawDeviceCommand.Apps.rawValue -> deviceHandler.handleDeviceApps(paramsJson)
|
||||
|
||||
// Notifications command
|
||||
OpenClawNotificationsCommand.List.rawValue -> notificationsHandler.handleNotificationsList(paramsJson)
|
||||
@@ -348,6 +350,15 @@ class InvokeDispatcher(
|
||||
message = "PHOTOS_UNAVAILABLE: photos not available on this build",
|
||||
)
|
||||
}
|
||||
InvokeCommandAvailability.InstalledAppsSharingEnabled ->
|
||||
if (installedAppsSharingEnabled()) {
|
||||
null
|
||||
} else {
|
||||
GatewaySession.InvokeResult.error(
|
||||
code = "INSTALLED_APPS_SHARING_DISABLED",
|
||||
message = "INSTALLED_APPS_SHARING_DISABLED: enable Installed Apps in Settings",
|
||||
)
|
||||
}
|
||||
InvokeCommandAvailability.DebugBuild ->
|
||||
if (debugBuild()) {
|
||||
null
|
||||
|
||||
@@ -112,6 +112,7 @@ enum class OpenClawDeviceCommand(
|
||||
Info("device.info"),
|
||||
Permissions("device.permissions"),
|
||||
Health("device.health"),
|
||||
Apps("device.apps"),
|
||||
;
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -6,6 +6,7 @@ import ai.openclaw.app.SensitiveFeatureConfig
|
||||
import ai.openclaw.app.gateway.GatewayEndpoint
|
||||
import ai.openclaw.app.node.DeviceNotificationListenerService
|
||||
import ai.openclaw.app.ui.design.ClawDesignTheme
|
||||
import ai.openclaw.app.ui.design.ClawErrorState
|
||||
import ai.openclaw.app.ui.design.ClawListItem
|
||||
import ai.openclaw.app.ui.design.ClawPanel
|
||||
import ai.openclaw.app.ui.design.ClawPrimaryButton
|
||||
@@ -473,6 +474,14 @@ private fun GatewaySetupScreen(
|
||||
onClick = { advancedOpen = true },
|
||||
)
|
||||
}
|
||||
error?.let { message ->
|
||||
item {
|
||||
ClawErrorState(
|
||||
title = "Setup code issue",
|
||||
body = message,
|
||||
)
|
||||
}
|
||||
}
|
||||
item {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Surface(
|
||||
@@ -505,9 +514,6 @@ private fun GatewaySetupScreen(
|
||||
}
|
||||
ClawTextField(value = token, onValueChange = onTokenChange, placeholder = "Token optional")
|
||||
ClawTextField(value = password, onValueChange = onPasswordChange, placeholder = "Password optional")
|
||||
error?.let {
|
||||
Text(text = it, style = ClawTheme.type.caption, color = ClawTheme.colors.warning)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,11 +18,15 @@ import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.only
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.safeDrawing
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
@@ -78,9 +82,16 @@ internal fun ProvidersModelsScreen(
|
||||
}
|
||||
}
|
||||
|
||||
ClawScaffold(contentPadding = PaddingValues(start = 20.dp, top = 13.dp, end = 20.dp, bottom = 13.dp)) {
|
||||
ClawScaffold(
|
||||
contentPadding = PaddingValues(start = 20.dp, top = 13.dp, end = 20.dp, bottom = 6.dp),
|
||||
contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
|
||||
) {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
LazyColumn(verticalArrangement = Arrangement.spacedBy(7.dp), contentPadding = PaddingValues(bottom = 112.dp)) {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.spacedBy(7.dp),
|
||||
contentPadding = PaddingValues(bottom = 4.dp),
|
||||
) {
|
||||
item {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Row(
|
||||
|
||||
@@ -13,11 +13,14 @@ import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.only
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.safeDrawing
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
@@ -88,8 +91,15 @@ internal fun SessionsScreen(
|
||||
}
|
||||
}
|
||||
|
||||
ClawScaffold(contentPadding = PaddingValues(start = 20.dp, top = 14.dp, end = 20.dp, bottom = 20.dp)) {
|
||||
LazyColumn(verticalArrangement = Arrangement.spacedBy(7.dp)) {
|
||||
ClawScaffold(
|
||||
contentPadding = PaddingValues(start = 20.dp, top = 14.dp, end = 20.dp, bottom = 6.dp),
|
||||
contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.spacedBy(7.dp),
|
||||
contentPadding = PaddingValues(bottom = 4.dp),
|
||||
) {
|
||||
item {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
@@ -133,11 +143,16 @@ internal fun SessionsScreen(
|
||||
|
||||
if (visibleSessions.isEmpty()) {
|
||||
item {
|
||||
ClawEmptyState(
|
||||
title = emptySessionTitle(filter),
|
||||
body = emptySessionBody(filter),
|
||||
action = { ClawPrimaryButton(text = "Start Chat", onClick = onOpenChat) },
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier.fillParentMaxHeight(0.56f).fillMaxWidth(),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
ClawEmptyState(
|
||||
title = emptySessionTitle(filter),
|
||||
body = emptySessionBody(filter),
|
||||
action = { ClawPrimaryButton(text = "Start Chat", onClick = onOpenChat) },
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
items(visibleSessions, key = { it.key }) { session ->
|
||||
@@ -155,10 +170,6 @@ internal fun SessionsScreen(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,11 +44,15 @@ import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.only
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.safeDrawing
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
@@ -714,6 +718,7 @@ private fun PhoneCapabilitiesScreen(
|
||||
val locationPreciseEnabled by viewModel.locationPreciseEnabled.collectAsState()
|
||||
val preventSleep by viewModel.preventSleep.collectAsState()
|
||||
val canvasDebugStatusEnabled by viewModel.canvasDebugStatusEnabled.collectAsState()
|
||||
val installedAppsSharingEnabled by viewModel.installedAppsSharingEnabled.collectAsState()
|
||||
val cameraPermissionLauncher =
|
||||
rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
|
||||
viewModel.setCameraEnabled(granted)
|
||||
@@ -768,6 +773,13 @@ private fun PhoneCapabilitiesScreen(
|
||||
listOf(
|
||||
SettingsToggleRow("Camera", "Allow camera tools when requested.", Icons.Default.CameraAlt, cameraEnabled, ::setCameraAccess),
|
||||
SettingsToggleRow("Precise Location", "Share precise location while location is enabled.", Icons.Default.LocationOn, locationPreciseEnabled, ::setPreciseLocation),
|
||||
SettingsToggleRow(
|
||||
"Installed Apps",
|
||||
if (installedAppsSharingEnabled) "OpenClaw can list launcher-visible apps." else "App list stays on this phone.",
|
||||
Icons.Default.Storage,
|
||||
installedAppsSharingEnabled,
|
||||
viewModel::setInstalledAppsSharingEnabled,
|
||||
),
|
||||
SettingsToggleRow("Keep Awake", "Keep the node available during active work.", Icons.Default.Bolt, preventSleep, viewModel::setPreventSleep),
|
||||
SettingsToggleRow("Canvas Status", "Show screen-sharing debug state.", Icons.AutoMirrored.Filled.ScreenShare, canvasDebugStatusEnabled, viewModel::setCanvasDebugStatusEnabled),
|
||||
),
|
||||
@@ -1020,8 +1032,11 @@ internal fun SettingsDetailFrame(
|
||||
onBack: () -> Unit,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
ClawScaffold(contentPadding = PaddingValues(start = ClawTheme.spacing.lg, top = 14.dp, end = ClawTheme.spacing.lg, bottom = 20.dp)) {
|
||||
LazyColumn(verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
ClawScaffold(
|
||||
contentPadding = PaddingValues(start = ClawTheme.spacing.lg, top = 14.dp, end = ClawTheme.spacing.lg, bottom = 6.dp),
|
||||
contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
|
||||
) {
|
||||
LazyColumn(modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.spacedBy(10.dp), contentPadding = PaddingValues(bottom = 4.dp)) {
|
||||
item {
|
||||
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(9.dp)) {
|
||||
SettingsBackButton(onClick = onBack)
|
||||
@@ -1037,9 +1052,6 @@ internal fun SettingsDetailFrame(
|
||||
content()
|
||||
}
|
||||
}
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1245,6 +1257,7 @@ private fun cronJobStatus(job: GatewayCronJobSummary): ClawStatus {
|
||||
}
|
||||
}
|
||||
|
||||
/** Applies query/system visibility rules while always preserving selected packages. */
|
||||
internal fun filterNotificationAppsForPicker(
|
||||
apps: List<InstalledApp>,
|
||||
selectedPackages: Set<String>,
|
||||
@@ -1263,6 +1276,7 @@ internal fun filterNotificationAppsForPicker(
|
||||
}
|
||||
}
|
||||
|
||||
/** Summarizes allowlist/blocklist mode with an empty-state warning when needed. */
|
||||
private fun notificationPackageSelectionSummary(
|
||||
mode: NotificationPackageFilterMode,
|
||||
selectedCount: Int,
|
||||
@@ -1282,6 +1296,7 @@ private fun notificationPackageSelectionSummary(
|
||||
}
|
||||
}
|
||||
|
||||
/** Builds compact two-letter app badges from package-picker labels. */
|
||||
private fun notificationAppBadge(label: String): String {
|
||||
val initials =
|
||||
label
|
||||
|
||||
@@ -9,11 +9,14 @@ import ai.openclaw.app.HomeDestination
|
||||
import ai.openclaw.app.MainViewModel
|
||||
import ai.openclaw.app.NodeRuntime
|
||||
import ai.openclaw.app.ui.chat.ChatScreen
|
||||
import ai.openclaw.app.ui.design.ClawBottomNav
|
||||
import ai.openclaw.app.ui.design.ClawDesignTheme
|
||||
import ai.openclaw.app.ui.design.ClawEmptyState
|
||||
import ai.openclaw.app.ui.design.ClawNavItem
|
||||
import ai.openclaw.app.ui.design.ClawPanel
|
||||
import ai.openclaw.app.ui.design.ClawPrimaryButton
|
||||
import ai.openclaw.app.ui.design.ClawScaffold
|
||||
import ai.openclaw.app.ui.design.ClawSecondaryButton
|
||||
import ai.openclaw.app.ui.design.ClawTheme
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
@@ -24,20 +27,26 @@ import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.ime
|
||||
import androidx.compose.foundation.layout.only
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.safeDrawing
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.automirrored.filled.ExitToApp
|
||||
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
|
||||
import androidx.compose.material.icons.automirrored.filled.ScreenShare
|
||||
import androidx.compose.material.icons.filled.Cloud
|
||||
import androidx.compose.material.icons.filled.Home
|
||||
import androidx.compose.material.icons.filled.Lock
|
||||
import androidx.compose.material.icons.filled.Mic
|
||||
import androidx.compose.material.icons.filled.Notifications
|
||||
@@ -54,6 +63,7 @@ import androidx.compose.material.icons.outlined.Settings
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
@@ -69,23 +79,32 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
private enum class Tab(
|
||||
internal enum class Tab(
|
||||
val key: String,
|
||||
val label: String,
|
||||
val icon: ImageVector,
|
||||
) {
|
||||
Overview(key = "overview", label = "Home"),
|
||||
Chat(key = "chat", label = "Chat"),
|
||||
Voice(key = "voice", label = "Voice"),
|
||||
Sessions(key = "sessions", label = "Sessions"),
|
||||
Settings(key = "settings", label = "Settings"),
|
||||
ProvidersModels(key = "providers-models", label = "Providers"),
|
||||
Overview(key = "overview", label = "Home", icon = Icons.Default.Home),
|
||||
Chat(key = "chat", label = "Chat", icon = Icons.Outlined.ChatBubbleOutline),
|
||||
Voice(key = "voice", label = "Voice", icon = Icons.Outlined.MicNone),
|
||||
Sessions(key = "sessions", label = "Sessions", icon = Icons.Outlined.AccessTime),
|
||||
Settings(key = "settings", label = "Settings", icon = Icons.Outlined.Settings),
|
||||
ProvidersModels(key = "providers-models", label = "Providers", icon = Icons.Outlined.Inventory2),
|
||||
}
|
||||
|
||||
private val shellNavTabs = listOf(Tab.Overview, Tab.Chat, Tab.Voice, Tab.Settings)
|
||||
|
||||
private val shellContentInsets: WindowInsets
|
||||
@Composable get() = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal)
|
||||
|
||||
internal fun shellBottomNavVisible(keyboardVisible: Boolean, commandOpen: Boolean): Boolean = !keyboardVisible && !commandOpen
|
||||
|
||||
/** Main post-onboarding shell that owns top-level Android navigation state. */
|
||||
@Composable
|
||||
fun ShellScreen(
|
||||
@@ -131,117 +150,144 @@ fun ShellScreen(
|
||||
commandOpen = false
|
||||
}
|
||||
|
||||
Box(modifier = modifier.fillMaxSize()) {
|
||||
when (activeTab) {
|
||||
Tab.Overview ->
|
||||
OverviewScreen(
|
||||
viewModel = viewModel,
|
||||
onSelectTab = { activeTab = it },
|
||||
onOpenSettingsRoute = {
|
||||
settingsRoute = it
|
||||
returnToOverviewFromSettings = true
|
||||
activeTab = Tab.Settings
|
||||
},
|
||||
onOpenCommand = { commandOpen = true },
|
||||
)
|
||||
Tab.Chat ->
|
||||
ChatShellScreen(
|
||||
viewModel = viewModel,
|
||||
onBack = { activeTab = Tab.Overview },
|
||||
onVoice = { activeTab = Tab.Voice },
|
||||
)
|
||||
Tab.Voice ->
|
||||
VoiceShellScreen(
|
||||
viewModel = viewModel,
|
||||
onOpenCommand = { commandOpen = true },
|
||||
onOpenGatewaySettings = {
|
||||
settingsRoute = SettingsRoute.Gateway
|
||||
returnToOverviewFromSettings = false
|
||||
activeTab = Tab.Settings
|
||||
},
|
||||
onOpenVoiceSettings = {
|
||||
settingsRoute = SettingsRoute.Voice
|
||||
returnToOverviewFromSettings = false
|
||||
activeTab = Tab.Settings
|
||||
},
|
||||
)
|
||||
Tab.ProvidersModels ->
|
||||
ProvidersModelsScreen(
|
||||
viewModel = viewModel,
|
||||
onBack = { activeTab = Tab.Overview },
|
||||
onAddProvider = {
|
||||
settingsRoute = SettingsRoute.Gateway
|
||||
returnToOverviewFromSettings = false
|
||||
activeTab = Tab.Settings
|
||||
},
|
||||
)
|
||||
Tab.Sessions ->
|
||||
SessionsScreen(
|
||||
viewModel = viewModel,
|
||||
onOpenCommand = { commandOpen = true },
|
||||
onOpenChat = { activeTab = Tab.Chat },
|
||||
)
|
||||
Tab.Settings ->
|
||||
SettingsShellScreen(
|
||||
viewModel = viewModel,
|
||||
route = settingsRoute,
|
||||
onRouteChange = {
|
||||
settingsRoute = it
|
||||
returnToOverviewFromSettings = false
|
||||
},
|
||||
onRouteBack = {
|
||||
settingsRoute = SettingsRoute.Home
|
||||
if (returnToOverviewFromSettings) {
|
||||
val density = LocalDensity.current
|
||||
val keyboardVisible = WindowInsets.ime.getBottom(density) > 0
|
||||
val showBottomNav = shellBottomNavVisible(keyboardVisible = keyboardVisible, commandOpen = commandOpen)
|
||||
|
||||
Scaffold(
|
||||
modifier = modifier.fillMaxSize(),
|
||||
containerColor = ClawTheme.colors.canvas,
|
||||
contentWindowInsets = WindowInsets(0, 0, 0, 0),
|
||||
bottomBar = {
|
||||
if (showBottomNav) {
|
||||
ClawBottomNav(
|
||||
items = shellNavTabs.map { ClawNavItem(key = it.key, label = it.label, icon = it.icon) },
|
||||
selectedKey = if (activeTab in shellNavTabs) activeTab.key else Tab.Overview.key,
|
||||
onSelect = { key ->
|
||||
val next = shellNavTabs.firstOrNull { it.key == key } ?: Tab.Overview
|
||||
if (next == Tab.Settings) {
|
||||
settingsRoute = SettingsRoute.Home
|
||||
returnToOverviewFromSettings = false
|
||||
activeTab = Tab.Overview
|
||||
}
|
||||
activeTab = next
|
||||
},
|
||||
onOpenCommand = { commandOpen = true },
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
) { shellPadding ->
|
||||
Box(modifier = Modifier.fillMaxSize().padding(shellPadding)) {
|
||||
when (activeTab) {
|
||||
Tab.Overview ->
|
||||
OverviewScreen(
|
||||
viewModel = viewModel,
|
||||
onSelectTab = { activeTab = it },
|
||||
onOpenSettingsRoute = {
|
||||
settingsRoute = it
|
||||
returnToOverviewFromSettings = true
|
||||
activeTab = Tab.Settings
|
||||
},
|
||||
onOpenCommand = { commandOpen = true },
|
||||
)
|
||||
Tab.Chat ->
|
||||
ChatShellScreen(
|
||||
viewModel = viewModel,
|
||||
onVoice = { activeTab = Tab.Voice },
|
||||
onOpenSessions = { activeTab = Tab.Sessions },
|
||||
)
|
||||
Tab.Voice ->
|
||||
VoiceShellScreen(
|
||||
viewModel = viewModel,
|
||||
onOpenCommand = { commandOpen = true },
|
||||
onOpenGatewaySettings = {
|
||||
settingsRoute = SettingsRoute.Gateway
|
||||
returnToOverviewFromSettings = false
|
||||
activeTab = Tab.Settings
|
||||
},
|
||||
onOpenVoiceSettings = {
|
||||
settingsRoute = SettingsRoute.Voice
|
||||
returnToOverviewFromSettings = false
|
||||
activeTab = Tab.Settings
|
||||
},
|
||||
)
|
||||
Tab.ProvidersModels ->
|
||||
ProvidersModelsScreen(
|
||||
viewModel = viewModel,
|
||||
onBack = { activeTab = Tab.Overview },
|
||||
onAddProvider = {
|
||||
settingsRoute = SettingsRoute.Gateway
|
||||
returnToOverviewFromSettings = false
|
||||
activeTab = Tab.Settings
|
||||
},
|
||||
)
|
||||
Tab.Sessions ->
|
||||
SessionsScreen(
|
||||
viewModel = viewModel,
|
||||
onOpenCommand = { commandOpen = true },
|
||||
onOpenChat = { activeTab = Tab.Chat },
|
||||
)
|
||||
Tab.Settings ->
|
||||
SettingsShellScreen(
|
||||
viewModel = viewModel,
|
||||
route = settingsRoute,
|
||||
onRouteChange = {
|
||||
settingsRoute = it
|
||||
returnToOverviewFromSettings = false
|
||||
},
|
||||
onRouteBack = {
|
||||
settingsRoute = SettingsRoute.Home
|
||||
if (returnToOverviewFromSettings) {
|
||||
returnToOverviewFromSettings = false
|
||||
activeTab = Tab.Overview
|
||||
}
|
||||
},
|
||||
onBackHome = { activeTab = Tab.Overview },
|
||||
onOpenCommand = { commandOpen = true },
|
||||
)
|
||||
}
|
||||
|
||||
if (commandOpen) {
|
||||
CommandPalette(
|
||||
viewModel = viewModel,
|
||||
onDismiss = { commandOpen = false },
|
||||
onOpenChat = {
|
||||
activeTab = Tab.Chat
|
||||
commandOpen = false
|
||||
},
|
||||
onOpenVoice = {
|
||||
activeTab = Tab.Voice
|
||||
commandOpen = false
|
||||
},
|
||||
onOpenSessions = {
|
||||
activeTab = Tab.Sessions
|
||||
commandOpen = false
|
||||
},
|
||||
onOpenProviders = {
|
||||
activeTab = Tab.ProvidersModels
|
||||
commandOpen = false
|
||||
},
|
||||
onOpenSettings = {
|
||||
settingsRoute = SettingsRoute.Home
|
||||
returnToOverviewFromSettings = false
|
||||
activeTab = Tab.Settings
|
||||
commandOpen = false
|
||||
},
|
||||
onOpenSession = { sessionKey ->
|
||||
viewModel.switchChatSession(sessionKey)
|
||||
activeTab = Tab.Chat
|
||||
commandOpen = false
|
||||
},
|
||||
)
|
||||
}
|
||||
if (commandOpen) {
|
||||
CommandPalette(
|
||||
viewModel = viewModel,
|
||||
onDismiss = { commandOpen = false },
|
||||
onOpenChat = {
|
||||
activeTab = Tab.Chat
|
||||
commandOpen = false
|
||||
},
|
||||
onOpenVoice = {
|
||||
activeTab = Tab.Voice
|
||||
commandOpen = false
|
||||
},
|
||||
onOpenSessions = {
|
||||
activeTab = Tab.Sessions
|
||||
commandOpen = false
|
||||
},
|
||||
onOpenProviders = {
|
||||
activeTab = Tab.ProvidersModels
|
||||
commandOpen = false
|
||||
},
|
||||
onOpenSettings = {
|
||||
settingsRoute = SettingsRoute.Home
|
||||
returnToOverviewFromSettings = false
|
||||
activeTab = Tab.Settings
|
||||
commandOpen = false
|
||||
},
|
||||
onOpenSession = { sessionKey ->
|
||||
viewModel.switchChatSession(sessionKey)
|
||||
activeTab = Tab.Chat
|
||||
commandOpen = false
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pendingTrust?.let { prompt ->
|
||||
// Gateway certificate trust is modal across the shell so navigation
|
||||
// cannot hide a changed TLS identity prompt.
|
||||
GatewayTrustDialog(
|
||||
prompt = prompt,
|
||||
onAccept = viewModel::acceptGatewayTrustPrompt,
|
||||
onDecline = viewModel::declineGatewayTrustPrompt,
|
||||
)
|
||||
pendingTrust?.let { prompt ->
|
||||
// Gateway certificate trust is modal across the shell so navigation
|
||||
// cannot hide a changed TLS identity prompt.
|
||||
GatewayTrustDialog(
|
||||
prompt = prompt,
|
||||
onAccept = viewModel::acceptGatewayTrustPrompt,
|
||||
onDecline = viewModel::declineGatewayTrustPrompt,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -289,33 +335,39 @@ private fun OverviewScreen(
|
||||
val isConnected by viewModel.isConnected.collectAsState()
|
||||
val sessions by viewModel.chatSessions.collectAsState()
|
||||
val pendingRunCount by viewModel.pendingRunCount.collectAsState()
|
||||
val statusText by viewModel.statusText.collectAsState()
|
||||
val models by viewModel.modelCatalog.collectAsState()
|
||||
val providers by viewModel.modelAuthProviders.collectAsState()
|
||||
val agents by viewModel.gatewayAgents.collectAsState()
|
||||
val pendingToolCalls by viewModel.chatPendingToolCalls.collectAsState()
|
||||
val cronStatus by viewModel.cronStatus.collectAsState()
|
||||
val usageSummary by viewModel.usageSummary.collectAsState()
|
||||
val skillsSummary by viewModel.skillsSummary.collectAsState()
|
||||
val nodesDevicesSummary by viewModel.nodesDevicesSummary.collectAsState()
|
||||
val channelsSummary by viewModel.channelsSummary.collectAsState()
|
||||
val readyProviderCount = providers.count { modelProviderReady(it.status) }
|
||||
val attentionRows =
|
||||
homeAttentionRows(
|
||||
isConnected = isConnected,
|
||||
pendingApprovals = pendingToolCalls.size,
|
||||
channelsSummary = channelsSummary,
|
||||
nodesDevicesSummary = nodesDevicesSummary,
|
||||
readyProviderCount = readyProviderCount,
|
||||
)
|
||||
|
||||
LaunchedEffect(isConnected) {
|
||||
if (isConnected) {
|
||||
viewModel.refreshChatSessions(limit = 20)
|
||||
viewModel.refreshModelCatalog()
|
||||
viewModel.refreshAgents()
|
||||
viewModel.refreshCronJobs()
|
||||
viewModel.refreshUsage()
|
||||
viewModel.refreshSkills()
|
||||
viewModel.refreshNodesDevices()
|
||||
viewModel.refreshChannels()
|
||||
}
|
||||
}
|
||||
|
||||
ClawScaffold(contentPadding = PaddingValues(start = 20.dp, top = 14.dp, end = 20.dp, bottom = 20.dp)) {
|
||||
ClawScaffold(
|
||||
contentPadding = PaddingValues(start = 20.dp, top = 14.dp, end = 20.dp, bottom = 6.dp),
|
||||
contentWindowInsets = shellContentInsets,
|
||||
) {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
LazyColumn(verticalArrangement = Arrangement.spacedBy(10.dp), contentPadding = PaddingValues(bottom = 104.dp)) {
|
||||
LazyColumn(modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.spacedBy(12.dp), contentPadding = PaddingValues(bottom = 4.dp)) {
|
||||
item {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
@@ -334,41 +386,20 @@ private fun OverviewScreen(
|
||||
}
|
||||
|
||||
item {
|
||||
SectionLabel(title = "MODULES")
|
||||
CompanionHeroPanel(
|
||||
statusText = gatewaySummary(statusText, isConnected),
|
||||
isConnected = isConnected,
|
||||
pendingRunCount = pendingRunCount,
|
||||
onOpenChat = { onSelectTab(Tab.Chat) },
|
||||
onOpenVoice = { onSelectTab(Tab.Voice) },
|
||||
onOpenGateway = { onOpenSettingsRoute(SettingsRoute.Gateway) },
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
ModuleList(
|
||||
rows =
|
||||
listOf(
|
||||
ModuleRow("Chat", null, null, Icons.Outlined.ChatBubbleOutline, Tab.Chat),
|
||||
ModuleRow("Sessions", null, if (sessions.isEmpty()) "Empty" else "${sessions.size} recent", Icons.Outlined.AccessTime, Tab.Sessions),
|
||||
ModuleRow("Voice", null, if (isConnected) "Ready" else "Offline", Icons.Outlined.MicNone, Tab.Voice),
|
||||
ModuleRow(
|
||||
title = "Providers & Models",
|
||||
subtitle = null,
|
||||
metadata =
|
||||
when {
|
||||
!isConnected -> "Offline"
|
||||
readyProviderCount > 0 -> "$readyProviderCount ready"
|
||||
models.isNotEmpty() -> "${models.size} models"
|
||||
else -> "Setup"
|
||||
},
|
||||
icon = Icons.Outlined.Inventory2,
|
||||
tab = Tab.ProvidersModels,
|
||||
),
|
||||
ModuleRow("Channels", null, channelsSummaryText(channelsSummary), Icons.Default.Notifications, Tab.Settings, SettingsRoute.Channels),
|
||||
ModuleRow("Agents", null, if (agents.isEmpty()) "Load" else "${agents.size} ready", Icons.Default.Person, Tab.Settings, SettingsRoute.Agents),
|
||||
ModuleRow("Approvals", null, approvalsSummary(pendingToolCalls.size), Icons.Default.Lock, Tab.Settings, SettingsRoute.Approvals),
|
||||
ModuleRow("Cron Jobs", null, cronJobsSummary(cronStatus.jobs), Icons.Outlined.AccessTime, Tab.Settings, SettingsRoute.CronJobs),
|
||||
ModuleRow("Skills", null, skillsSummaryText(skillsSummary.skills), Icons.Default.Settings, Tab.Settings, SettingsRoute.Skills),
|
||||
ModuleRow("Nodes & Devices", null, nodesDevicesSummaryText(nodesDevicesSummary), Icons.Default.Cloud, Tab.Settings, SettingsRoute.NodesDevices),
|
||||
ModuleRow("Usage", null, usageSummaryText(usageSummary.providers.size), Icons.Default.Storage, Tab.Settings, SettingsRoute.Usage),
|
||||
ModuleRow("Settings", null, null, Icons.Outlined.Settings, Tab.Settings, SettingsRoute.Home),
|
||||
),
|
||||
onSelectTab = onSelectTab,
|
||||
onOpenSettingsRoute = onOpenSettingsRoute,
|
||||
)
|
||||
if (attentionRows.isNotEmpty()) {
|
||||
item {
|
||||
HomeAttentionPanel(rows = attentionRows, onSelectTab = onSelectTab, onOpenSettingsRoute = onOpenSettingsRoute)
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
@@ -397,7 +428,7 @@ private fun OverviewScreen(
|
||||
item {
|
||||
RecentSessionList(
|
||||
rows =
|
||||
sessions.take(7).map { session ->
|
||||
sessions.take(5).map { session ->
|
||||
RecentSessionListItem(
|
||||
key = session.key,
|
||||
title = displaySessionTitle(session.displayName),
|
||||
@@ -412,8 +443,39 @@ private fun OverviewScreen(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
SectionLabel(title = "Control center")
|
||||
}
|
||||
|
||||
item {
|
||||
ModuleList(
|
||||
rows =
|
||||
listOf(
|
||||
ModuleRow("Sessions", "Conversation history", if (sessions.isEmpty()) "Empty" else "${sessions.size} recent", Icons.Outlined.AccessTime, Tab.Sessions),
|
||||
ModuleRow(
|
||||
title = "Providers & Models",
|
||||
subtitle = "Model setup",
|
||||
metadata =
|
||||
when {
|
||||
!isConnected -> "Offline"
|
||||
readyProviderCount > 0 -> "$readyProviderCount ready"
|
||||
models.isNotEmpty() -> "${models.size} models"
|
||||
else -> "Setup"
|
||||
},
|
||||
icon = Icons.Outlined.Inventory2,
|
||||
tab = Tab.ProvidersModels,
|
||||
),
|
||||
ModuleRow("Channels", "Connected messengers", channelsSummaryText(channelsSummary), Icons.Default.Notifications, Tab.Settings, SettingsRoute.Channels),
|
||||
ModuleRow("Nodes & Devices", "Phone and node health", nodesDevicesSummaryText(nodesDevicesSummary), Icons.Default.Cloud, Tab.Settings, SettingsRoute.NodesDevices),
|
||||
ModuleRow("Approvals", "Tool decisions", approvalsSummary(pendingToolCalls.size), Icons.Default.Lock, Tab.Settings, SettingsRoute.Approvals),
|
||||
ModuleRow("Settings", "More runtime controls", null, Icons.Outlined.Settings, Tab.Settings, SettingsRoute.Home),
|
||||
),
|
||||
onSelectTab = onSelectTab,
|
||||
onOpenSettingsRoute = onOpenSettingsRoute,
|
||||
)
|
||||
}
|
||||
}
|
||||
OverviewChatButton(onClick = { onSelectTab(Tab.Chat) }, modifier = Modifier.align(Alignment.BottomEnd).padding(bottom = 20.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -427,26 +489,109 @@ private data class ModuleRow(
|
||||
val settingsRoute: SettingsRoute? = null,
|
||||
)
|
||||
|
||||
/** Floating overview shortcut that keeps chat one tap away from module lists. */
|
||||
@Composable
|
||||
private fun OverviewChatButton(
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
private fun CompanionHeroPanel(
|
||||
statusText: String,
|
||||
isConnected: Boolean,
|
||||
pendingRunCount: Int,
|
||||
onOpenChat: () -> Unit,
|
||||
onOpenVoice: () -> Unit,
|
||||
onOpenGateway: () -> Unit,
|
||||
) {
|
||||
Surface(
|
||||
onClick = onClick,
|
||||
modifier = modifier.height(ClawTheme.spacing.touchTarget),
|
||||
shape = RoundedCornerShape(ClawTheme.radii.button),
|
||||
color = ClawTheme.colors.primary,
|
||||
contentColor = ClawTheme.colors.primaryText,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Icon(imageVector = Icons.Outlined.ChatBubbleOutline, contentDescription = null, modifier = Modifier.size(18.dp))
|
||||
Text(text = "Chat", style = ClawTheme.type.label.copy(fontSize = 16.sp, lineHeight = 20.sp))
|
||||
ClawPanel(contentPadding = PaddingValues(16.dp)) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(14.dp)) {
|
||||
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
Surface(
|
||||
modifier = Modifier.size(38.dp),
|
||||
shape = CircleShape,
|
||||
color = if (isConnected) ClawTheme.colors.successSoft else ClawTheme.colors.surfacePressed,
|
||||
border = BorderStroke(1.dp, if (isConnected) ClawTheme.colors.success else ClawTheme.colors.border),
|
||||
) {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
Icon(imageVector = Icons.Outlined.ChatBubbleOutline, contentDescription = null, modifier = Modifier.size(19.dp), tint = if (isConnected) ClawTheme.colors.success else ClawTheme.colors.text)
|
||||
}
|
||||
}
|
||||
Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(3.dp)) {
|
||||
Text(text = if (pendingRunCount > 0) "OpenClaw is working" else "Ready when you are", style = ClawTheme.type.title.copy(fontSize = 20.sp, lineHeight = 24.sp), color = ClawTheme.colors.text)
|
||||
Text(text = statusText, style = ClawTheme.type.body, color = ClawTheme.colors.textMuted, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
}
|
||||
}
|
||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(9.dp)) {
|
||||
ClawPrimaryButton(text = "Start chat", icon = Icons.Outlined.ChatBubbleOutline, onClick = onOpenChat, modifier = Modifier.weight(1f))
|
||||
ClawSecondaryButton(text = "Voice", icon = Icons.Outlined.MicNone, onClick = onOpenVoice, modifier = Modifier.weight(1f))
|
||||
}
|
||||
if (!isConnected) {
|
||||
ClawSecondaryButton(text = "Reconnect gateway", icon = Icons.Default.Cloud, onClick = onOpenGateway, modifier = Modifier.fillMaxWidth())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal data class HomeAttentionRow(
|
||||
val title: String,
|
||||
val subtitle: String,
|
||||
val icon: ImageVector,
|
||||
val tab: Tab,
|
||||
val settingsRoute: SettingsRoute? = null,
|
||||
)
|
||||
|
||||
internal fun homeAttentionRows(
|
||||
isConnected: Boolean,
|
||||
pendingApprovals: Int,
|
||||
channelsSummary: GatewayChannelsSummary,
|
||||
nodesDevicesSummary: GatewayNodesDevicesSummary,
|
||||
readyProviderCount: Int,
|
||||
): List<HomeAttentionRow> =
|
||||
listOfNotNull(
|
||||
if (!isConnected) {
|
||||
HomeAttentionRow("Gateway", "Connect before chat, voice, and live status.", Icons.Default.Cloud, Tab.Settings, SettingsRoute.Gateway)
|
||||
} else {
|
||||
null
|
||||
},
|
||||
if (pendingApprovals > 0) {
|
||||
HomeAttentionRow("Approvals", approvalsSummary(pendingApprovals), Icons.Default.Lock, Tab.Settings, SettingsRoute.Approvals)
|
||||
} else {
|
||||
null
|
||||
},
|
||||
if (channelsSummary.channels.any { it.error != null }) {
|
||||
HomeAttentionRow("Channels", channelsSummaryText(channelsSummary), Icons.Default.Notifications, Tab.Settings, SettingsRoute.Channels)
|
||||
} else {
|
||||
null
|
||||
},
|
||||
if (nodesDevicesSummary.pendingDevices.isNotEmpty()) {
|
||||
HomeAttentionRow("Nodes & Devices", nodesDevicesSummaryText(nodesDevicesSummary), Icons.Default.Cloud, Tab.Settings, SettingsRoute.NodesDevices)
|
||||
} else {
|
||||
null
|
||||
},
|
||||
if (isConnected && readyProviderCount == 0) {
|
||||
HomeAttentionRow("Providers", "No ready providers", Icons.Outlined.Inventory2, Tab.ProvidersModels)
|
||||
} else {
|
||||
null
|
||||
},
|
||||
)
|
||||
|
||||
@Composable
|
||||
private fun HomeAttentionPanel(
|
||||
rows: List<HomeAttentionRow>,
|
||||
onSelectTab: (Tab) -> Unit,
|
||||
onOpenSettingsRoute: (SettingsRoute) -> Unit,
|
||||
) {
|
||||
ClawPanel(contentPadding = PaddingValues(horizontal = 14.dp, vertical = 8.dp)) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
Text(text = "Needs attention", style = ClawTheme.type.caption.copy(fontSize = 12.5.sp, lineHeight = 16.sp), color = ClawTheme.colors.warning)
|
||||
rows.forEach { row ->
|
||||
ModuleListRow(
|
||||
row = ModuleRow(row.title, row.subtitle, null, row.icon, row.tab, row.settingsRoute),
|
||||
onClick = {
|
||||
val route = row.settingsRoute
|
||||
if (route == null) {
|
||||
onSelectTab(row.tab)
|
||||
} else {
|
||||
onOpenSettingsRoute(route)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -527,14 +672,18 @@ private fun ModuleListRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(9.dp),
|
||||
) {
|
||||
Icon(imageVector = row.icon, contentDescription = null, modifier = Modifier.size(20.dp), tint = ClawTheme.colors.text)
|
||||
Text(
|
||||
text = row.title,
|
||||
style = ClawTheme.type.body,
|
||||
color = ClawTheme.colors.text,
|
||||
modifier = Modifier.weight(1f),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(1.dp)) {
|
||||
Text(
|
||||
text = row.title,
|
||||
style = ClawTheme.type.body,
|
||||
color = ClawTheme.colors.text,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
row.subtitle?.let {
|
||||
Text(text = it, style = ClawTheme.type.caption.copy(fontSize = 12.5.sp, lineHeight = 16.sp), color = ClawTheme.colors.textSubtle, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
}
|
||||
}
|
||||
row.metadata?.let {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
Box(modifier = Modifier.size(4.5.dp).clip(CircleShape).background(statusDotColor(it)))
|
||||
@@ -638,11 +787,18 @@ private fun RecentSessionRowContent(
|
||||
@Composable
|
||||
private fun ChatShellScreen(
|
||||
viewModel: MainViewModel,
|
||||
onBack: () -> Unit,
|
||||
onVoice: () -> Unit,
|
||||
onOpenSessions: () -> Unit,
|
||||
) {
|
||||
ClawScaffold(contentPadding = PaddingValues(start = 0.dp, top = 8.dp, end = 0.dp, bottom = 8.dp)) {
|
||||
ChatScreen(viewModel = viewModel, onBack = onBack, onVoice = onVoice)
|
||||
ClawScaffold(
|
||||
contentPadding = PaddingValues(start = 0.dp, top = 8.dp, end = 0.dp, bottom = 0.dp),
|
||||
contentWindowInsets = shellContentInsets,
|
||||
) {
|
||||
ChatScreen(
|
||||
viewModel = viewModel,
|
||||
onVoice = onVoice,
|
||||
onOpenSessions = onOpenSessions,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -653,7 +809,10 @@ private fun VoiceShellScreen(
|
||||
onOpenGatewaySettings: () -> Unit,
|
||||
onOpenVoiceSettings: () -> Unit,
|
||||
) {
|
||||
ClawScaffold(contentPadding = PaddingValues(start = 0.dp, top = 8.dp, end = 0.dp, bottom = 8.dp)) {
|
||||
ClawScaffold(
|
||||
contentPadding = PaddingValues(start = 0.dp, top = 8.dp, end = 0.dp, bottom = 0.dp),
|
||||
contentWindowInsets = shellContentInsets,
|
||||
) {
|
||||
VoiceScreen(
|
||||
viewModel = viewModel,
|
||||
onOpenCommand = onOpenCommand,
|
||||
@@ -669,6 +828,7 @@ private fun SettingsShellScreen(
|
||||
route: SettingsRoute,
|
||||
onRouteChange: (SettingsRoute) -> Unit,
|
||||
onRouteBack: () -> Unit,
|
||||
onBackHome: () -> Unit,
|
||||
onOpenCommand: () -> Unit,
|
||||
) {
|
||||
val displayName by viewModel.displayName.collectAsState()
|
||||
@@ -707,14 +867,18 @@ private fun SettingsShellScreen(
|
||||
return
|
||||
}
|
||||
|
||||
ClawScaffold(contentPadding = PaddingValues(start = 20.dp, top = 14.dp, end = 20.dp, bottom = 20.dp)) {
|
||||
LazyColumn(verticalArrangement = Arrangement.spacedBy(13.dp)) {
|
||||
ClawScaffold(
|
||||
contentPadding = PaddingValues(start = 20.dp, top = 14.dp, end = 20.dp, bottom = 6.dp),
|
||||
contentWindowInsets = shellContentInsets,
|
||||
) {
|
||||
LazyColumn(modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.spacedBy(13.dp), contentPadding = PaddingValues(bottom = 4.dp)) {
|
||||
item {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(9.dp),
|
||||
) {
|
||||
PlainIconButton(icon = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back to home", onClick = onBackHome)
|
||||
Text(text = "Settings", style = ClawTheme.type.title.copy(fontSize = 16.sp, lineHeight = 20.sp), color = ClawTheme.colors.text, modifier = Modifier.weight(1f))
|
||||
SettingsSearchButton(onClick = onOpenCommand)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import ai.openclaw.app.MainViewModel
|
||||
import ai.openclaw.app.chat.ChatMessage
|
||||
import ai.openclaw.app.chat.ChatMessageContent
|
||||
import ai.openclaw.app.chat.ChatPendingToolCall
|
||||
import ai.openclaw.app.chat.ChatSessionEntry
|
||||
import ai.openclaw.app.chat.OutgoingAttachment
|
||||
import ai.openclaw.app.ui.design.ClawListItem
|
||||
import ai.openclaw.app.ui.design.ClawLoadingState
|
||||
@@ -37,11 +38,11 @@ import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.automirrored.filled.Send
|
||||
import androidx.compose.material.icons.filled.AttachFile
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.Mic
|
||||
import androidx.compose.material.icons.filled.MoreHoriz
|
||||
import androidx.compose.material.icons.filled.Refresh
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
@@ -78,8 +79,8 @@ import java.util.Locale
|
||||
@Composable
|
||||
fun ChatScreen(
|
||||
viewModel: MainViewModel,
|
||||
onBack: () -> Unit,
|
||||
onVoice: () -> Unit,
|
||||
onOpenSessions: () -> Unit,
|
||||
) {
|
||||
val messages by viewModel.chatMessages.collectAsState()
|
||||
val historyLoading by viewModel.chatHistoryLoading.collectAsState()
|
||||
@@ -158,13 +159,23 @@ fun ChatScreen(
|
||||
thinkingLevel = thinkingLevel,
|
||||
healthOk = healthOk,
|
||||
pendingRunCount = pendingRunCount,
|
||||
onBack = onBack,
|
||||
onMore = {
|
||||
viewModel.refreshChat()
|
||||
viewModel.refreshChatSessions(limit = 100)
|
||||
},
|
||||
)
|
||||
|
||||
ChatSessionSwitcher(
|
||||
sessionKey = sessionKey,
|
||||
sessions = sessions,
|
||||
mainSessionKey = mainSessionKey,
|
||||
onSelectSession = { key ->
|
||||
viewModel.switchChatSession(key)
|
||||
viewModel.refreshChatSessions(limit = 100)
|
||||
},
|
||||
onOpenSessions = onOpenSessions,
|
||||
)
|
||||
|
||||
errorText?.takeIf { it.isNotBlank() }?.let { error ->
|
||||
ChatNotice(title = "Chat needs attention", body = userFacingChatError(error))
|
||||
}
|
||||
@@ -214,13 +225,88 @@ fun ChatScreen(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChatSessionSwitcher(
|
||||
sessionKey: String,
|
||||
sessions: List<ChatSessionEntry>,
|
||||
mainSessionKey: String,
|
||||
onSelectSession: (String) -> Unit,
|
||||
onOpenSessions: () -> Unit,
|
||||
) {
|
||||
val choices =
|
||||
remember(sessionKey, sessions, mainSessionKey) {
|
||||
resolveCompactSessionChoices(
|
||||
currentSessionKey = sessionKey,
|
||||
sessions = sessions,
|
||||
mainSessionKey = mainSessionKey,
|
||||
)
|
||||
}
|
||||
if (choices.size <= 1 && sessions.size <= 1) return
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().horizontalScroll(rememberScrollState()),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||
) {
|
||||
choices.forEach { entry ->
|
||||
ChatSessionChip(
|
||||
text = chatSessionChipText(entry = entry, mainSessionKey = mainSessionKey),
|
||||
active = isActiveSessionChoice(entry.key, sessionKey, mainSessionKey),
|
||||
onClick = { onSelectSession(entry.key) },
|
||||
)
|
||||
}
|
||||
if (sessions.size > choices.size) {
|
||||
Surface(
|
||||
onClick = onOpenSessions,
|
||||
modifier = Modifier.heightIn(min = 36.dp),
|
||||
shape = RoundedCornerShape(ClawTheme.radii.pill),
|
||||
color = ClawTheme.colors.canvas,
|
||||
contentColor = ClawTheme.colors.textMuted,
|
||||
border = BorderStroke(1.dp, ClawTheme.colors.border),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 10.dp, vertical = 7.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(5.dp),
|
||||
) {
|
||||
Icon(imageVector = Icons.Default.MoreHoriz, contentDescription = null, modifier = Modifier.size(16.dp))
|
||||
Text(text = "All", style = ClawTheme.type.caption, maxLines = 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChatSessionChip(
|
||||
text: String,
|
||||
active: Boolean,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
Surface(
|
||||
onClick = onClick,
|
||||
modifier = Modifier.heightIn(min = 36.dp),
|
||||
shape = RoundedCornerShape(ClawTheme.radii.pill),
|
||||
color = if (active) ClawTheme.colors.primary else ClawTheme.colors.surfaceRaised,
|
||||
contentColor = if (active) ClawTheme.colors.primaryText else ClawTheme.colors.text,
|
||||
border = BorderStroke(1.dp, if (active) ClawTheme.colors.primary else ClawTheme.colors.border),
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
modifier = Modifier.padding(horizontal = 11.dp, vertical = 7.dp),
|
||||
style = ClawTheme.type.caption,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChatHeader(
|
||||
sessionTitle: String,
|
||||
thinkingLevel: String,
|
||||
healthOk: Boolean,
|
||||
pendingRunCount: Int,
|
||||
onBack: () -> Unit,
|
||||
onMore: () -> Unit,
|
||||
) {
|
||||
Row(
|
||||
@@ -228,7 +314,7 @@ private fun ChatHeader(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||
) {
|
||||
HeaderIcon(icon = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back", onClick = onBack)
|
||||
Box(modifier = Modifier.size(ClawTheme.spacing.touchTarget))
|
||||
|
||||
Column(
|
||||
modifier = Modifier.weight(1f),
|
||||
@@ -786,13 +872,33 @@ private fun AttachmentChip(
|
||||
|
||||
private fun currentSessionTitle(
|
||||
sessionKey: String,
|
||||
sessions: List<ai.openclaw.app.chat.ChatSessionEntry>,
|
||||
sessions: List<ChatSessionEntry>,
|
||||
): String {
|
||||
val entry = sessions.firstOrNull { it.key == sessionKey }
|
||||
val name = entry?.displayName?.takeIf { it.isNotBlank() } ?: return "New chat"
|
||||
return friendlySessionName(name)
|
||||
}
|
||||
|
||||
private fun chatSessionChipText(
|
||||
entry: ChatSessionEntry,
|
||||
mainSessionKey: String,
|
||||
): String {
|
||||
val mainKey = mainSessionKey.trim().ifEmpty { "main" }
|
||||
if (entry.key == mainKey || (entry.key == "main" && mainKey == "main")) return "Main"
|
||||
val name = entry.displayName?.takeIf { it.isNotBlank() } ?: entry.key.takeIf { entry.updatedAtMs != null } ?: "Current"
|
||||
return friendlySessionName(name)
|
||||
}
|
||||
|
||||
private fun isActiveSessionChoice(
|
||||
choiceKey: String,
|
||||
sessionKey: String,
|
||||
mainSessionKey: String,
|
||||
): Boolean {
|
||||
val mainKey = mainSessionKey.trim().ifEmpty { "main" }
|
||||
val current = sessionKey.trim().let { if (it == "main" && mainKey != "main") mainKey else it }
|
||||
return choiceKey == current
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SendButton(
|
||||
enabled: Boolean,
|
||||
|
||||
@@ -4,22 +4,9 @@ import ai.openclaw.app.chat.ChatSessionEntry
|
||||
|
||||
private const val RECENT_WINDOW_MS = 24 * 60 * 60 * 1000L
|
||||
|
||||
/**
|
||||
* Derive a human-friendly label from a raw session key.
|
||||
* Examples:
|
||||
* "telegram:g-agent-main-main" -> "Main"
|
||||
* "agent:main:main" -> "Main"
|
||||
* "discord:g-server-channel" -> "Server Channel"
|
||||
* "my-custom-session" -> "My Custom Session"
|
||||
*/
|
||||
fun friendlySessionName(key: String): String {
|
||||
// Strip common prefixes like "telegram:", "agent:", "discord:" etc.
|
||||
val stripped = key.substringAfterLast(":")
|
||||
|
||||
// Remove leading "g-" prefix (gateway artifact)
|
||||
val cleaned = if (stripped.startsWith("g-")) stripped.removePrefix("g-") else stripped
|
||||
|
||||
// Split on hyphens/underscores, title-case each word, collapse "main main" -> "Main"
|
||||
val words =
|
||||
cleaned
|
||||
.split('-', '_')
|
||||
@@ -78,3 +65,29 @@ fun resolveSessionChoices(
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
fun resolveCompactSessionChoices(
|
||||
currentSessionKey: String,
|
||||
sessions: List<ChatSessionEntry>,
|
||||
mainSessionKey: String,
|
||||
nowMs: Long = System.currentTimeMillis(),
|
||||
maxOptions: Int = 5,
|
||||
): List<ChatSessionEntry> {
|
||||
val allChoices =
|
||||
resolveSessionChoices(
|
||||
currentSessionKey = currentSessionKey,
|
||||
sessions = sessions,
|
||||
mainSessionKey = mainSessionKey,
|
||||
nowMs = nowMs,
|
||||
)
|
||||
val mainKey = mainSessionKey.trim().ifEmpty { "main" }
|
||||
val current = currentSessionKey.trim().let { if (it == "main" && mainKey != "main") mainKey else it }
|
||||
val pinnedRank = listOf(mainKey, current).filter { it.isNotBlank() }.distinct().withIndex().associate { it.value to it.index }
|
||||
val unpinnedRank = pinnedRank.size
|
||||
|
||||
return allChoices
|
||||
.withIndex()
|
||||
.sortedWith(compareBy({ pinnedRank[it.value.key] ?: unpinnedRank }, { it.index }))
|
||||
.take(maxOptions)
|
||||
.map { it.value }
|
||||
}
|
||||
|
||||
@@ -61,6 +61,7 @@ internal enum class ClawStatus {
|
||||
internal fun ClawScaffold(
|
||||
modifier: Modifier = Modifier,
|
||||
contentPadding: PaddingValues = PaddingValues(horizontal = ClawTheme.spacing.lg, vertical = ClawTheme.spacing.lg),
|
||||
contentWindowInsets: WindowInsets = WindowInsets.safeDrawing,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
Box(
|
||||
@@ -68,7 +69,7 @@ internal fun ClawScaffold(
|
||||
modifier
|
||||
.fillMaxSize()
|
||||
.background(ClawTheme.colors.canvas)
|
||||
.windowInsetsPadding(WindowInsets.safeDrawing)
|
||||
.windowInsetsPadding(contentWindowInsets)
|
||||
.padding(contentPadding),
|
||||
) {
|
||||
content()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package ai.openclaw.app.ui.design
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -91,27 +92,29 @@ internal fun ClawBottomNav(
|
||||
) {
|
||||
val safeInsets = WindowInsets.navigationBars.only(androidx.compose.foundation.layout.WindowInsetsSides.Bottom)
|
||||
|
||||
Surface(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
color = ClawTheme.colors.surface.copy(alpha = 0.96f),
|
||||
border = BorderStroke(1.dp, ClawTheme.colors.border),
|
||||
shape = RoundedCornerShape(topStart = ClawTheme.radii.sheet, topEnd = ClawTheme.radii.sheet),
|
||||
) {
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
.windowInsetsPadding(safeInsets)
|
||||
.padding(horizontal = 8.dp, vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
Box(modifier = modifier.fillMaxWidth().background(ClawTheme.colors.canvas)) {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
color = ClawTheme.colors.surface.copy(alpha = 0.96f),
|
||||
border = BorderStroke(1.dp, ClawTheme.colors.border),
|
||||
shape = RoundedCornerShape(topStart = ClawTheme.radii.sheet, topEnd = ClawTheme.radii.sheet),
|
||||
) {
|
||||
items.forEach { item ->
|
||||
ClawBottomNavItem(
|
||||
item = item,
|
||||
selected = item.key == selectedKey,
|
||||
onClick = { onSelect(item.key) },
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
.windowInsetsPadding(safeInsets)
|
||||
.padding(horizontal = 8.dp, vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
items.forEach { item ->
|
||||
ClawBottomNavItem(
|
||||
item = item,
|
||||
selected = item.key == selectedKey,
|
||||
onClick = { onSelect(item.key) },
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -129,7 +132,7 @@ private fun ClawBottomNavItem(
|
||||
modifier = modifier.heightIn(min = 48.dp),
|
||||
shape = RoundedCornerShape(ClawTheme.radii.control),
|
||||
color = if (selected) ClawTheme.colors.primary else Color.Transparent,
|
||||
contentColor = if (selected) ClawTheme.colors.primaryText else ClawTheme.colors.textSubtle,
|
||||
contentColor = if (selected) ClawTheme.colors.primaryText else ClawTheme.colors.textMuted,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(horizontal = 5.dp, vertical = 6.dp),
|
||||
|
||||
@@ -62,6 +62,21 @@ class SecurePrefsTest {
|
||||
assertFalse(plainPrefs.getBoolean("talk.enabled", false))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun installedAppsSharing_defaultsOffAndPersistsOptIn() {
|
||||
val context = RuntimeEnvironment.getApplication()
|
||||
val plainPrefs = context.getSharedPreferences("openclaw.node", Context.MODE_PRIVATE)
|
||||
plainPrefs.edit().clear().commit()
|
||||
val prefs = SecurePrefs(context)
|
||||
|
||||
assertFalse(prefs.installedAppsSharingEnabled.value)
|
||||
|
||||
prefs.setInstalledAppsSharingEnabled(true)
|
||||
|
||||
assertTrue(prefs.installedAppsSharingEnabled.value)
|
||||
assertTrue(plainPrefs.getBoolean("device.apps.sharing.enabled", false))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun saveGatewayBootstrapToken_persistsSeparatelyFromSharedToken() {
|
||||
val context = RuntimeEnvironment.getApplication()
|
||||
|
||||
@@ -9,6 +9,7 @@ import ai.openclaw.app.gateway.isLoopbackGatewayHost
|
||||
import ai.openclaw.app.protocol.OpenClawCallLogCommand
|
||||
import ai.openclaw.app.protocol.OpenClawCameraCommand
|
||||
import ai.openclaw.app.protocol.OpenClawCapability
|
||||
import ai.openclaw.app.protocol.OpenClawDeviceCommand
|
||||
import ai.openclaw.app.protocol.OpenClawLocationCommand
|
||||
import ai.openclaw.app.protocol.OpenClawMotionCommand
|
||||
import ai.openclaw.app.protocol.OpenClawPhotosCommand
|
||||
@@ -475,6 +476,15 @@ class ConnectionManagerTest {
|
||||
assertTrue(options.caps.contains(OpenClawCapability.VoiceWake.rawValue))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun buildNodeConnectOptions_advertisesDeviceAppsOnlyWhenUserOptedIn() {
|
||||
val disabled = newManager(installedAppsSharingEnabled = false).buildNodeConnectOptions()
|
||||
val enabled = newManager(installedAppsSharingEnabled = true).buildNodeConnectOptions()
|
||||
|
||||
assertFalse(disabled.commands.contains(OpenClawDeviceCommand.Apps.rawValue))
|
||||
assertTrue(enabled.commands.contains(OpenClawDeviceCommand.Apps.rawValue))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun buildNodeConnectOptions_omitsVoiceWakeWithoutMicrophonePermission() {
|
||||
val options =
|
||||
@@ -546,6 +556,7 @@ class ConnectionManagerTest {
|
||||
callLogAvailable: Boolean = false,
|
||||
photosAvailable: Boolean = false,
|
||||
hasRecordAudioPermission: Boolean = false,
|
||||
installedAppsSharingEnabled: Boolean = false,
|
||||
): ConnectionManager {
|
||||
val context = RuntimeEnvironment.getApplication()
|
||||
val prefs =
|
||||
@@ -567,6 +578,7 @@ class ConnectionManagerTest {
|
||||
callLogAvailable = { callLogAvailable },
|
||||
photosAvailable = { photosAvailable },
|
||||
hasRecordAudioPermission = { hasRecordAudioPermission },
|
||||
installedAppsSharingEnabled = { installedAppsSharingEnabled },
|
||||
manualTls = { false },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package ai.openclaw.app.node
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.ApplicationInfo
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.boolean
|
||||
@@ -320,6 +321,108 @@ class DeviceHandlerTest {
|
||||
system["securityPatchLevel"]?.jsonPrimitive?.content
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleDeviceApps_filtersAndLimitsVisibleApps() {
|
||||
val handler =
|
||||
DeviceHandler.forTesting(
|
||||
appContext = appContext(),
|
||||
appSource =
|
||||
FakeDeviceAppSource(
|
||||
listOf(
|
||||
DeviceAppEntry(
|
||||
label = "Calendar",
|
||||
packageName = "com.google.android.calendar",
|
||||
system = false,
|
||||
enabled = true,
|
||||
launchable = true,
|
||||
),
|
||||
DeviceAppEntry(
|
||||
label = "Android System",
|
||||
packageName = "android",
|
||||
system = true,
|
||||
enabled = true,
|
||||
launchable = false,
|
||||
),
|
||||
DeviceAppEntry(
|
||||
label = "Disabled App",
|
||||
packageName = "com.example.disabled",
|
||||
system = false,
|
||||
enabled = false,
|
||||
launchable = true,
|
||||
),
|
||||
DeviceAppEntry(
|
||||
label = "Gmail",
|
||||
packageName = "com.google.android.gm",
|
||||
system = false,
|
||||
enabled = true,
|
||||
launchable = true,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
val result = handler.handleDeviceApps("""{"query":"google","limit":1}""")
|
||||
|
||||
assertTrue(result.ok)
|
||||
val payload = parsePayload(result.payloadJson)
|
||||
assertEquals("1", payload.getValue("count").jsonPrimitive.content)
|
||||
assertEquals("2", payload.getValue("totalMatched").jsonPrimitive.content)
|
||||
assertTrue(payload.getValue("truncated").jsonPrimitive.boolean)
|
||||
assertEquals("launcher", payload.getValue("visibility").jsonPrimitive.content)
|
||||
val apps = payload.getValue("apps").jsonArray
|
||||
assertEquals(1, apps.size)
|
||||
val app = apps.first().jsonObject
|
||||
assertEquals("Calendar", app.getValue("label").jsonPrimitive.content)
|
||||
assertEquals("com.google.android.calendar", app.getValue("packageName").jsonPrimitive.content)
|
||||
assertTrue(!app.getValue("system").jsonPrimitive.boolean)
|
||||
assertTrue(app.getValue("enabled").jsonPrimitive.boolean)
|
||||
assertTrue(app.getValue("launchable").jsonPrimitive.boolean)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleDeviceApps_canIncludeSystemAndNonLaunchableApps() {
|
||||
val source =
|
||||
FakeDeviceAppSource(
|
||||
listOf(
|
||||
DeviceAppEntry(
|
||||
label = "Android System",
|
||||
packageName = "android",
|
||||
system = true,
|
||||
enabled = true,
|
||||
launchable = false,
|
||||
),
|
||||
),
|
||||
)
|
||||
val handler = DeviceHandler.forTesting(appContext = appContext(), appSource = source)
|
||||
|
||||
val result = handler.handleDeviceApps("""{"includeSystem":true,"includeNonLaunchable":true}""")
|
||||
|
||||
assertTrue(result.ok)
|
||||
val payload = parsePayload(result.payloadJson)
|
||||
assertEquals("android-visible", payload.getValue("visibility").jsonPrimitive.content)
|
||||
assertTrue(payload.getValue("includeSystem").jsonPrimitive.boolean)
|
||||
val app =
|
||||
payload
|
||||
.getValue("apps")
|
||||
.jsonArray
|
||||
.first()
|
||||
.jsonObject
|
||||
assertEquals("android", app.getValue("packageName").jsonPrimitive.content)
|
||||
assertTrue(app.getValue("system").jsonPrimitive.boolean)
|
||||
assertTrue(!app.getValue("launchable").jsonPrimitive.boolean)
|
||||
assertTrue(source.includeNonLaunchableRequests.single())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun isSystemDeviceApp_treatsUpdatedBuiltInsAsSystemApps() {
|
||||
val appInfo =
|
||||
ApplicationInfo().apply {
|
||||
flags = ApplicationInfo.FLAG_UPDATED_SYSTEM_APP
|
||||
}
|
||||
|
||||
assertTrue(isSystemDeviceApp(appInfo))
|
||||
}
|
||||
|
||||
private fun appContext(): Context = RuntimeEnvironment.getApplication()
|
||||
|
||||
private fun parsePayload(payloadJson: String?): JsonObject {
|
||||
@@ -327,3 +430,14 @@ class DeviceHandlerTest {
|
||||
return Json.parseToJsonElement(jsonString).jsonObject
|
||||
}
|
||||
}
|
||||
|
||||
private class FakeDeviceAppSource(
|
||||
private val apps: List<DeviceAppEntry>,
|
||||
) : DeviceAppSource {
|
||||
val includeNonLaunchableRequests = mutableListOf<Boolean>()
|
||||
|
||||
override fun listApps(includeNonLaunchable: Boolean): List<DeviceAppEntry> {
|
||||
includeNonLaunchableRequests += includeNonLaunchable
|
||||
return apps
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,6 +115,15 @@ class InvokeCommandRegistryTest {
|
||||
assertMissingAll(commands, optionalCommands + debugCommands)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun advertisedCommands_includesDeviceAppsOnlyWhenUserOptedIn() {
|
||||
val disabled = InvokeCommandRegistry.advertisedCommands(defaultFlags(installedAppsSharingEnabled = false))
|
||||
val enabled = InvokeCommandRegistry.advertisedCommands(defaultFlags(installedAppsSharingEnabled = true))
|
||||
|
||||
assertFalse(disabled.contains(OpenClawDeviceCommand.Apps.rawValue))
|
||||
assertTrue(enabled.contains(OpenClawDeviceCommand.Apps.rawValue))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun advertisedCommands_includesFeatureCommandsWhenEnabled() {
|
||||
val commands =
|
||||
@@ -151,6 +160,7 @@ class InvokeCommandRegistryTest {
|
||||
voiceWakeEnabled = false,
|
||||
motionActivityAvailable = true,
|
||||
motionPedometerAvailable = false,
|
||||
installedAppsSharingEnabled = false,
|
||||
debugBuild = false,
|
||||
),
|
||||
)
|
||||
@@ -262,6 +272,7 @@ class InvokeCommandRegistryTest {
|
||||
voiceWakeEnabled: Boolean = false,
|
||||
motionActivityAvailable: Boolean = false,
|
||||
motionPedometerAvailable: Boolean = false,
|
||||
installedAppsSharingEnabled: Boolean = false,
|
||||
debugBuild: Boolean = false,
|
||||
): NodeRuntimeFlags =
|
||||
NodeRuntimeFlags(
|
||||
@@ -275,6 +286,7 @@ class InvokeCommandRegistryTest {
|
||||
voiceWakeEnabled = voiceWakeEnabled,
|
||||
motionActivityAvailable = motionActivityAvailable,
|
||||
motionPedometerAvailable = motionPedometerAvailable,
|
||||
installedAppsSharingEnabled = installedAppsSharingEnabled,
|
||||
debugBuild = debugBuild,
|
||||
)
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import ai.openclaw.app.gateway.DeviceIdentityStore
|
||||
import ai.openclaw.app.gateway.GatewaySession
|
||||
import ai.openclaw.app.protocol.OpenClawCallLogCommand
|
||||
import ai.openclaw.app.protocol.OpenClawCameraCommand
|
||||
import ai.openclaw.app.protocol.OpenClawDeviceCommand
|
||||
import ai.openclaw.app.protocol.OpenClawLocationCommand
|
||||
import ai.openclaw.app.protocol.OpenClawMotionCommand
|
||||
import ai.openclaw.app.protocol.OpenClawPhotosCommand
|
||||
@@ -170,6 +171,20 @@ class InvokeDispatcherTest {
|
||||
assertEquals("LOCATION_DISABLED: enable Location in Settings", result.error?.message)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleInvoke_blocksDeviceAppsWhenSharingDisabled() =
|
||||
runTest {
|
||||
val result =
|
||||
newDispatcher(installedAppsSharingEnabled = false)
|
||||
.handleInvoke(OpenClawDeviceCommand.Apps.rawValue, """{"limit":1}""")
|
||||
|
||||
assertEquals("INSTALLED_APPS_SHARING_DISABLED", result.error?.code)
|
||||
assertEquals(
|
||||
"INSTALLED_APPS_SHARING_DISABLED: enable Installed Apps in Settings",
|
||||
result.error?.message,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleInvoke_blocksMotionActivityWhenUnavailable() =
|
||||
runTest {
|
||||
@@ -250,6 +265,7 @@ class InvokeDispatcherTest {
|
||||
smsTelephonyAvailable: Boolean = true,
|
||||
callLogAvailable: Boolean = false,
|
||||
photosAvailable: Boolean = true,
|
||||
installedAppsSharingEnabled: Boolean = true,
|
||||
debugBuild: Boolean = false,
|
||||
motionActivityAvailable: Boolean = false,
|
||||
motionPedometerAvailable: Boolean = false,
|
||||
@@ -297,6 +313,7 @@ class InvokeDispatcherTest {
|
||||
smsTelephonyAvailable = { smsTelephonyAvailable },
|
||||
callLogAvailable = { callLogAvailable },
|
||||
photosAvailable = { photosAvailable },
|
||||
installedAppsSharingEnabled = { installedAppsSharingEnabled },
|
||||
debugBuild = { debugBuild },
|
||||
onCanvasA2uiPush = {},
|
||||
onCanvasA2uiReset = {},
|
||||
|
||||
@@ -57,6 +57,7 @@ class OpenClawProtocolConstantsTest {
|
||||
assertEquals("device.info", OpenClawDeviceCommand.Info.rawValue)
|
||||
assertEquals("device.permissions", OpenClawDeviceCommand.Permissions.rawValue)
|
||||
assertEquals("device.health", OpenClawDeviceCommand.Health.rawValue)
|
||||
assertEquals("device.apps", OpenClawDeviceCommand.Apps.rawValue)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
package ai.openclaw.app.ui
|
||||
|
||||
import ai.openclaw.app.GatewayChannelSummary
|
||||
import ai.openclaw.app.GatewayChannelsSummary
|
||||
import ai.openclaw.app.GatewayNodesDevicesSummary
|
||||
import ai.openclaw.app.GatewayPendingDeviceSummary
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class ShellScreenLogicTest {
|
||||
@Test
|
||||
fun bottomNavHidesForKeyboardAndCommandPalette() {
|
||||
assertTrue(shellBottomNavVisible(keyboardVisible = false, commandOpen = false))
|
||||
assertFalse(shellBottomNavVisible(keyboardVisible = true, commandOpen = false))
|
||||
assertFalse(shellBottomNavVisible(keyboardVisible = false, commandOpen = true))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun homeAttentionRowsSurfaceGatewayWhenDisconnected() {
|
||||
val rows =
|
||||
homeAttentionRows(
|
||||
isConnected = false,
|
||||
pendingApprovals = 0,
|
||||
channelsSummary = emptyChannels(),
|
||||
nodesDevicesSummary = emptyNodesDevices(),
|
||||
readyProviderCount = 0,
|
||||
)
|
||||
|
||||
assertEquals(listOf("Gateway"), rows.map { it.title })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun homeAttentionRowsSurfaceOnlyActionableConnectedIssues() {
|
||||
val rows =
|
||||
homeAttentionRows(
|
||||
isConnected = true,
|
||||
pendingApprovals = 2,
|
||||
channelsSummary =
|
||||
GatewayChannelsSummary(
|
||||
channels =
|
||||
listOf(
|
||||
GatewayChannelSummary(
|
||||
id = "telegram",
|
||||
label = "Telegram",
|
||||
accountCount = 1,
|
||||
enabled = true,
|
||||
configured = true,
|
||||
linked = true,
|
||||
running = false,
|
||||
connected = false,
|
||||
error = "offline",
|
||||
),
|
||||
),
|
||||
),
|
||||
nodesDevicesSummary =
|
||||
GatewayNodesDevicesSummary(
|
||||
nodes = emptyList(),
|
||||
pendingDevices =
|
||||
listOf(
|
||||
GatewayPendingDeviceSummary(
|
||||
requestId = "request-1",
|
||||
deviceId = "device-1",
|
||||
displayName = "Phone",
|
||||
remoteIp = null,
|
||||
roles = emptyList(),
|
||||
scopes = emptyList(),
|
||||
requestedAtMs = null,
|
||||
repair = false,
|
||||
),
|
||||
),
|
||||
pairedDevices = emptyList(),
|
||||
),
|
||||
readyProviderCount = 0,
|
||||
)
|
||||
|
||||
assertEquals(listOf("Approvals", "Channels", "Nodes & Devices", "Providers"), rows.map { it.title })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun homeAttentionRowsStayQuietWhenConnectedAndHealthy() {
|
||||
val rows =
|
||||
homeAttentionRows(
|
||||
isConnected = true,
|
||||
pendingApprovals = 0,
|
||||
channelsSummary = emptyChannels(),
|
||||
nodesDevicesSummary = emptyNodesDevices(),
|
||||
readyProviderCount = 1,
|
||||
)
|
||||
|
||||
assertEquals(emptyList<String>(), rows.map { it.title })
|
||||
}
|
||||
|
||||
private fun emptyChannels(): GatewayChannelsSummary = GatewayChannelsSummary(channels = emptyList())
|
||||
|
||||
private fun emptyNodesDevices(): GatewayNodesDevicesSummary = GatewayNodesDevicesSummary(nodes = emptyList(), pendingDevices = emptyList(), pairedDevices = emptyList())
|
||||
}
|
||||
@@ -32,4 +32,29 @@ class SessionFiltersTest {
|
||||
val result = resolveSessionChoices("custom", sessions, mainSessionKey = "main", nowMs = now).map { it.key }
|
||||
assertEquals(listOf("main", "custom"), result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun compactChoicesKeepMainAndCurrentWhileCappingRecentSessions() {
|
||||
val now = 1_700_000_000_000L
|
||||
val sessions =
|
||||
listOf(
|
||||
ChatSessionEntry(key = "recent-1", updatedAtMs = now - 1),
|
||||
ChatSessionEntry(key = "recent-2", updatedAtMs = now - 2),
|
||||
ChatSessionEntry(key = "recent-3", updatedAtMs = now - 3),
|
||||
ChatSessionEntry(key = "recent-4", updatedAtMs = now - 4),
|
||||
ChatSessionEntry(key = "main", updatedAtMs = now - 5),
|
||||
ChatSessionEntry(key = "active-old", updatedAtMs = now - 30 * 60 * 60 * 1000L),
|
||||
)
|
||||
|
||||
val result =
|
||||
resolveCompactSessionChoices(
|
||||
currentSessionKey = "active-old",
|
||||
sessions = sessions,
|
||||
mainSessionKey = "main",
|
||||
nowMs = now,
|
||||
maxOptions = 4,
|
||||
).map { it.key }
|
||||
|
||||
assertEquals(listOf("main", "active-old", "recent-1", "recent-2"), result)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
# OpenClaw iOS Changelog
|
||||
|
||||
## 2026.6.2 - 2026-06-02
|
||||
|
||||
Maintenance update for the current OpenClaw release.
|
||||
|
||||
## 2026.6.1 - 2026-06-01
|
||||
|
||||
Maintenance update for the current OpenClaw release.
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
// Source of truth: apps/ios/version.json
|
||||
// Generated by scripts/ios-sync-versioning.ts.
|
||||
|
||||
OPENCLAW_IOS_VERSION = 2026.6.1
|
||||
OPENCLAW_MARKETING_VERSION = 2026.6.1
|
||||
OPENCLAW_IOS_VERSION = 2026.6.2
|
||||
OPENCLAW_MARKETING_VERSION = 2026.6.2
|
||||
OPENCLAW_BUILD_VERSION = 1
|
||||
|
||||
#include? "../build/Version.xcconfig"
|
||||
|
||||
@@ -1,5 +1 @@
|
||||
Maintenance update for the current OpenClaw release.
|
||||
|
||||
- Added hosted push relay defaults, realtime Talk playback, and safer WebSocket ping handling for mobile sessions.
|
||||
- Updated App Store screenshots to cover Gateway pairing, Command, Chat, Talk, Agent, and Settings flows.
|
||||
- Highlighted realtime Talk relay, Gateway connection status, node capabilities, push wake, and privacy controls.
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"version": "2026.6.1"
|
||||
"version": "2026.6.2"
|
||||
}
|
||||
|
||||
@@ -514,12 +514,16 @@ extension GatewayConnection {
|
||||
var params: [String: AnyCodable] = [
|
||||
"message": AnyCodable(trimmed),
|
||||
"sessionKey": AnyCodable(sessionKey),
|
||||
"thinking": AnyCodable(invocation.thinking ?? "default"),
|
||||
"deliver": AnyCodable(invocation.deliver),
|
||||
"to": AnyCodable(invocation.to ?? ""),
|
||||
"channel": AnyCodable(invocation.channel.rawValue),
|
||||
"idempotencyKey": AnyCodable(invocation.idempotencyKey),
|
||||
]
|
||||
if let thinking = invocation.thinking?.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||
!thinking.isEmpty
|
||||
{
|
||||
params["thinking"] = AnyCodable(thinking)
|
||||
}
|
||||
if let timeout = invocation.timeoutSeconds {
|
||||
params["timeout"] = AnyCodable(timeout)
|
||||
}
|
||||
@@ -664,7 +668,7 @@ extension GatewayConnection {
|
||||
func chatSend(
|
||||
sessionKey: String,
|
||||
message: String,
|
||||
thinking: String,
|
||||
thinking: String?,
|
||||
idempotencyKey: String,
|
||||
attachments: [OpenClawChatAttachmentPayload],
|
||||
timeoutMs: Int = 30000) async throws -> OpenClawChatSendResponse
|
||||
@@ -673,10 +677,14 @@ extension GatewayConnection {
|
||||
var params: [String: AnyCodable] = [
|
||||
"sessionKey": AnyCodable(resolvedKey),
|
||||
"message": AnyCodable(message),
|
||||
"thinking": AnyCodable(thinking),
|
||||
"idempotencyKey": AnyCodable(idempotencyKey),
|
||||
"timeoutMs": AnyCodable(timeoutMs),
|
||||
]
|
||||
if let thinking = thinking?.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||
!thinking.isEmpty
|
||||
{
|
||||
params["thinking"] = AnyCodable(thinking)
|
||||
}
|
||||
|
||||
if !attachments.isEmpty {
|
||||
let encoded = attachments.map { att in
|
||||
|
||||
@@ -15,9 +15,9 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>2026.6.1</string>
|
||||
<string>2026.6.2</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>2026053100</string>
|
||||
<string>2026060200</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>OpenClaw</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
|
||||
@@ -387,7 +387,7 @@ actor TalkModeRuntime {
|
||||
let response = try await GatewayConnection.shared.chatSend(
|
||||
sessionKey: sessionKey,
|
||||
message: prompt,
|
||||
thinking: "low",
|
||||
thinking: nil,
|
||||
idempotencyKey: runId,
|
||||
attachments: [])
|
||||
guard self.isCurrent(gen) else { return }
|
||||
|
||||
@@ -34,7 +34,7 @@ enum VoiceWakeForwarder {
|
||||
|
||||
struct ForwardOptions {
|
||||
var sessionKey: String = "main"
|
||||
var thinking: String = "low"
|
||||
var thinking: String?
|
||||
var deliver: Bool = true
|
||||
var to: String?
|
||||
var channel: GatewayAgentChannel = .webchat
|
||||
@@ -97,7 +97,6 @@ enum VoiceWakeForwarder {
|
||||
|
||||
return ForwardOptions(
|
||||
sessionKey: sessionKey,
|
||||
thinking: "low",
|
||||
deliver: true,
|
||||
to: to,
|
||||
channel: channel,
|
||||
|
||||
@@ -173,9 +173,57 @@ private func makeTestGatewayConnection() -> (GatewayConnection, FakeWebSocketSes
|
||||
|
||||
let json = try JSONSerialization.jsonObject(with: payloadData) as? [String: Any]
|
||||
let params = json?["params"] as? [String: Any]
|
||||
#expect(params?["thinking"] == nil)
|
||||
#expect(params?["voiceWakeTrigger"] as? String == "")
|
||||
}
|
||||
|
||||
@Test func `chat send omits thinking when inheriting session default`() async throws {
|
||||
let recorder = WebSocketMessageRecorder()
|
||||
let session = GatewayTestWebSocketSession(taskFactory: {
|
||||
GatewayTestWebSocketTask(sendHook: { task, message, sendIndex in
|
||||
recorder.append(message)
|
||||
guard sendIndex > 0,
|
||||
let data = Self.messageData(message),
|
||||
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
||||
let id = json["id"] as? String
|
||||
else { return }
|
||||
task.emitReceiveSuccess(.data(Self.chatSendOkResponseData(id: id)))
|
||||
})
|
||||
})
|
||||
let connection = GatewayConnection(
|
||||
configProvider: {
|
||||
(url: URL(string: "ws://127.0.0.1:1")!, token: nil, password: nil)
|
||||
},
|
||||
sessionBox: WebSocketSessionBox(session: session))
|
||||
|
||||
_ = try await connection.chatSend(
|
||||
sessionKey: "main",
|
||||
message: "hello",
|
||||
thinking: nil,
|
||||
idempotencyKey: "chat-1",
|
||||
attachments: [])
|
||||
await connection.shutdown()
|
||||
|
||||
guard let chatMessage = recorder.snapshot().reversed().first(where: { message in
|
||||
guard let data = Self.messageData(message),
|
||||
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
|
||||
else { return false }
|
||||
return json["method"] as? String == "chat.send"
|
||||
}) else {
|
||||
Issue.record("expected chat.send websocket payload")
|
||||
return
|
||||
}
|
||||
|
||||
guard let payloadData = Self.messageData(chatMessage) else {
|
||||
Issue.record("unexpected chat.send websocket message type")
|
||||
return
|
||||
}
|
||||
|
||||
let json = try JSONSerialization.jsonObject(with: payloadData) as? [String: Any]
|
||||
let params = json?["params"] as? [String: Any]
|
||||
#expect(params?["thinking"] == nil)
|
||||
}
|
||||
|
||||
private static func messageData(_ message: URLSessionWebSocketTask.Message) -> Data? {
|
||||
switch message {
|
||||
case let .string(text):
|
||||
@@ -186,4 +234,15 @@ private func makeTestGatewayConnection() -> (GatewayConnection, FakeWebSocketSes
|
||||
nil
|
||||
}
|
||||
}
|
||||
|
||||
private static func chatSendOkResponseData(id: String) -> Data {
|
||||
Data("""
|
||||
{
|
||||
"type": "res",
|
||||
"id": "\(id)",
|
||||
"ok": true,
|
||||
"payload": { "runId": "chat-1", "status": "ok" }
|
||||
}
|
||||
""".utf8)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import Testing
|
||||
@Test func `forward options defaults`() {
|
||||
let opts = VoiceWakeForwarder.ForwardOptions()
|
||||
#expect(opts.sessionKey == "main")
|
||||
#expect(opts.thinking == "low")
|
||||
#expect(opts.thinking == nil)
|
||||
#expect(opts.deliver == true)
|
||||
#expect(opts.to == nil)
|
||||
#expect(opts.channel == .webchat)
|
||||
@@ -38,6 +38,7 @@ import Testing
|
||||
#expect(opts.channel == .telegram)
|
||||
#expect(opts.to == "telegram:6812765697")
|
||||
#expect(opts.voiceWakeTrigger == "open claw")
|
||||
#expect(opts.thinking == nil)
|
||||
#expect(opts.channel.shouldDeliver(opts.deliver) == true)
|
||||
}
|
||||
|
||||
|
||||
@@ -6896,6 +6896,20 @@ public struct ChatHistoryParams: Codable, Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
public struct ChatMetadataParams: Codable, Sendable {
|
||||
public let agentid: String?
|
||||
|
||||
public init(
|
||||
agentid: String? = nil)
|
||||
{
|
||||
self.agentid = agentid
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case agentid = "agentId"
|
||||
}
|
||||
}
|
||||
|
||||
public struct ChatMessageGetParams: Codable, Sendable {
|
||||
public let sessionkey: String
|
||||
public let agentid: String?
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
63d49032a9b4dc4874a0ca17be73ecc97a2df5d1f47b4e72db34868423370558 plugin-sdk-api-baseline.json
|
||||
af79f7d711afa0a8563782b8f5cdd7e46b9aea245f5e7ebc464327a8969ed65e plugin-sdk-api-baseline.jsonl
|
||||
a9501e226bb26befb02072cf5e60c3dc124cbd5dc0b16eb281789d0843f72f71 plugin-sdk-api-baseline.json
|
||||
b106090dc12bf7e46beac4ed160f0cff0ef8039291f24172b693e8d8b752d571 plugin-sdk-api-baseline.jsonl
|
||||
|
||||
|
Before Width: | Height: | Size: 146 KiB After Width: | Height: | Size: 251 KiB |
BIN
docs/assets/showcase/caldav-calendar.png
Normal file
|
After Width: | Height: | Size: 250 KiB |
|
Before Width: | Height: | Size: 135 KiB After Width: | Height: | Size: 251 KiB |
BIN
docs/assets/showcase/homeassistant.png
Normal file
|
After Width: | Height: | Size: 219 KiB |
BIN
docs/assets/showcase/openrouter-transcribe.png
Normal file
|
After Width: | Height: | Size: 257 KiB |
BIN
docs/assets/showcase/r2-upload.png
Normal file
|
After Width: | Height: | Size: 262 KiB |
|
Before Width: | Height: | Size: 134 KiB After Width: | Height: | Size: 244 KiB |
@@ -122,6 +122,33 @@ This fires ~5–6 times per month instead of 0–1 times per month. OpenClaw use
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
### Command payloads
|
||||
|
||||
Use command payloads for deterministic scripts that should run inside the Gateway scheduler without starting a model-backed isolated agent turn. Command jobs execute on the Gateway host, capture stdout/stderr, record the run in cron history, and reuse the same `announce`, `webhook`, and `none` delivery modes as isolated jobs.
|
||||
|
||||
<Note>
|
||||
Command cron is an operator-admin Gateway automation surface, not an agent
|
||||
`tools.exec` call. Creating, updating, removing, or manually running cron jobs
|
||||
requires `operator.admin`; scheduled command runs later execute inside the
|
||||
Gateway process as that admin-authored automation. Agent exec policy such as
|
||||
`tools.exec.mode`, approval prompts, and per-agent tool allowlists governs
|
||||
model-visible exec tools, not command cron payloads.
|
||||
</Note>
|
||||
|
||||
```bash
|
||||
openclaw cron create "*/15 * * * *" \
|
||||
--name "Queue depth probe" \
|
||||
--command "scripts/check-queue.sh" \
|
||||
--command-cwd "/srv/app" \
|
||||
--announce \
|
||||
--channel telegram \
|
||||
--to "-1001234567890"
|
||||
```
|
||||
|
||||
`--command <shell>` stores `argv: ["sh", "-lc", <shell>]`. Use `--command-argv '["node","scripts/report.mjs"]'` when you want exact argv execution without shell parsing. Optional `--command-env KEY=VALUE`, `--command-input`, `--timeout-seconds`, `--no-output-timeout-seconds`, and `--output-max-bytes` fields control the process environment, stdin, and output bounds.
|
||||
|
||||
If stdout is non-empty, that text is the delivered result. If stdout is empty and stderr is non-empty, stderr is delivered. If both streams are present, cron delivers a small `stdout:` / `stderr:` block. A zero exit code records the run as `ok`; non-zero exit, signal, timeout, or no-output timeout records `error` and can trigger failure alerts. A command that prints only `NO_REPLY` uses the normal cron silent-token suppression and posts nothing back to chat.
|
||||
|
||||
### Payload options for isolated jobs
|
||||
|
||||
<ParamField path="--message" type="string" required>
|
||||
@@ -246,6 +273,17 @@ Failure notifications follow a separate destination path:
|
||||
--webhook "https://example.invalid/openclaw/cron"
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="Command output">
|
||||
```bash
|
||||
openclaw cron create "*/15 * * * *" \
|
||||
--name "Queue depth probe" \
|
||||
--command "scripts/check-queue.sh" \
|
||||
--command-cwd "/srv/app" \
|
||||
--announce \
|
||||
--channel telegram \
|
||||
--to "-1001234567890"
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## Webhooks
|
||||
|
||||
@@ -319,6 +319,7 @@ curl "https://api.telegram.org/bot<bot_token>/getUpdates"
|
||||
- `progress` keeps one editable status draft for tool progress, clears it at completion, and sends the final answer as a normal message
|
||||
- `streaming.preview.toolProgress` controls whether tool/progress updates reuse the same edited preview message (default: `true` when preview streaming is active)
|
||||
- `streaming.preview.commandText` controls command/exec detail inside those tool-progress lines: `raw` (default, preserves released behavior) or `status` (tool label only)
|
||||
- `streaming.progress.commentary` (default: `false`) opts into assistant commentary/preamble text in the temporary progress draft
|
||||
- legacy `channels.telegram.streamMode` and boolean `streaming` values are detected; run `openclaw doctor --fix` to migrate them to `channels.telegram.streaming.mode`
|
||||
|
||||
Tool-progress preview updates are the short status lines shown while tools run, for example command execution, file reads, planning updates, patch summaries, or Codex preamble/commentary text in Codex app-server mode. Telegram keeps these enabled by default to match released OpenClaw behavior from `v2026.4.22` and later.
|
||||
|
||||
82
docs/clawhub/cli.md
Normal file
@@ -0,0 +1,82 @@
|
||||
---
|
||||
summary: "ClawHub CLI entry points for discovering, installing, publishing, and verifying OpenClaw skills and plugins."
|
||||
read_when:
|
||||
- You want to use ClawHub from the command line
|
||||
- You want to install ClawHub skills or plugins through OpenClaw
|
||||
- You want to publish ClawHub packages
|
||||
title: "ClawHub CLI"
|
||||
---
|
||||
|
||||
# ClawHub CLI
|
||||
|
||||
OpenClaw has two command-line entry points for ClawHub:
|
||||
|
||||
- `openclaw skills` and `openclaw plugins` install and manage ClawHub packages
|
||||
inside OpenClaw.
|
||||
- The standalone `clawhub` CLI handles publisher workflows such as login,
|
||||
publish, transfer, and sync.
|
||||
|
||||
## Discover and install
|
||||
|
||||
Use OpenClaw commands when you want to install or update packages for a local
|
||||
OpenClaw agent or Gateway.
|
||||
|
||||
```bash
|
||||
openclaw skills search "calendar"
|
||||
openclaw skills install <slug>
|
||||
openclaw skills update <slug>
|
||||
openclaw skills verify <slug>
|
||||
|
||||
openclaw plugins search "calendar"
|
||||
openclaw plugins install clawhub:<package>
|
||||
openclaw plugins update <id-or-npm-spec>
|
||||
```
|
||||
|
||||
Skill installs target the active workspace `skills/` directory by default. Add
|
||||
`--global` to install into the shared managed skills directory.
|
||||
|
||||
Plugin installs use the `clawhub:` prefix when you want ClawHub resolution
|
||||
instead of npm or another install source.
|
||||
|
||||
## Publish and maintain
|
||||
|
||||
Install the standalone ClawHub CLI for publisher workflows:
|
||||
|
||||
```bash
|
||||
npm i -g clawhub
|
||||
clawhub login
|
||||
```
|
||||
|
||||
Publish plugin packages with `clawhub package publish`:
|
||||
|
||||
```bash
|
||||
clawhub package publish your-org/your-plugin --dry-run
|
||||
clawhub package publish your-org/your-plugin
|
||||
clawhub package publish your-org/your-plugin@v1.0.0
|
||||
```
|
||||
|
||||
Publish skill folders with `clawhub skill publish`:
|
||||
|
||||
```bash
|
||||
clawhub skill publish ./skills/review-helper
|
||||
clawhub skill publish ./skills/review-helper --version 1.0.0
|
||||
```
|
||||
|
||||
When local skill scan state or package ownership needs maintenance, use the
|
||||
relevant standalone command:
|
||||
|
||||
```bash
|
||||
clawhub sync --all
|
||||
clawhub package transfer @old-owner/package --to new-owner
|
||||
```
|
||||
|
||||
## Related
|
||||
|
||||
- [`openclaw skills`](/cli/skills) - local skill search, install, update, and
|
||||
verification
|
||||
- [`openclaw plugins`](/cli/plugins) - plugin search, install, update, and
|
||||
inspection
|
||||
- [ClawHub publishing](/clawhub/publishing) - owner scope, release validation,
|
||||
and review flow
|
||||
- [Creating skills](/tools/creating-skills) - skill authoring and publish flow
|
||||
- [Building plugins](/plugins/building-plugins) - plugin package authoring
|
||||
@@ -34,6 +34,27 @@ openclaw cron create "0 18 * * 1-5" \
|
||||
--webhook "https://example.invalid/openclaw/cron"
|
||||
```
|
||||
|
||||
Use `--command` for deterministic shell-style jobs that should run inside OpenClaw cron without starting an isolated agent/model run:
|
||||
|
||||
<Note>
|
||||
Command cron jobs are admin-authored Gateway automation. Creating, editing,
|
||||
removing, or manually running them requires `operator.admin`; the scheduled run
|
||||
later executes in the Gateway process, not as an agent `tools.exec` tool call.
|
||||
`tools.exec.*` and exec approvals still govern model-visible exec tools.
|
||||
</Note>
|
||||
|
||||
```bash
|
||||
openclaw cron create "*/15 * * * *" \
|
||||
--name "Queue depth probe" \
|
||||
--command "scripts/check-queue.sh" \
|
||||
--command-cwd "/srv/app" \
|
||||
--announce \
|
||||
--channel telegram \
|
||||
--to "-1001234567890"
|
||||
```
|
||||
|
||||
`--command <shell>` stores `argv: ["sh", "-lc", <shell>]`. Use `--command-argv '["node","scripts/report.mjs"]'` for exact argv execution. Command jobs capture stdout/stderr, record normal cron history, and route output through the same `announce`, `webhook`, or `none` delivery modes as isolated jobs. A command that prints only `NO_REPLY` is suppressed.
|
||||
|
||||
## Sessions
|
||||
|
||||
`--session` accepts `main`, `isolated`, `current`, or `session:<id>`.
|
||||
@@ -92,6 +113,10 @@ Note: isolated cron runs treat run-level agent failures as job errors even when
|
||||
no reply payload is produced, so model/provider failures still increment error
|
||||
counters and trigger failure notifications.
|
||||
|
||||
Command cron jobs do not start an isolated agent turn. A zero exit code records
|
||||
`ok`; non-zero exit, signal, timeout, or no-output timeout records `error` and
|
||||
can trigger the same failure notification path.
|
||||
|
||||
If an isolated run times out before the first model request, `openclaw cron show`
|
||||
and `openclaw cron runs` include a phase-specific error such as
|
||||
`setup timed out before runner start` or
|
||||
@@ -252,6 +277,21 @@ openclaw cron create "0 7 * * *" \
|
||||
|
||||
`--light-context` applies to isolated agent-turn jobs only. For cron runs, lightweight mode keeps bootstrap context empty instead of injecting the full workspace bootstrap set.
|
||||
|
||||
Create a command job with exact argv, cwd, env, stdin, and output limits:
|
||||
|
||||
```bash
|
||||
openclaw cron create "*/30 * * * *" \
|
||||
--name "Position export" \
|
||||
--command-argv '["node","scripts/export-position.mjs"]' \
|
||||
--command-cwd "/srv/app" \
|
||||
--command-env "NODE_ENV=production" \
|
||||
--command-input '{"mode":"summary"}' \
|
||||
--timeout-seconds 120 \
|
||||
--no-output-timeout-seconds 30 \
|
||||
--output-max-bytes 65536 \
|
||||
--webhook "https://example.invalid/openclaw/cron"
|
||||
```
|
||||
|
||||
## Common admin commands
|
||||
|
||||
Manual run and inspection:
|
||||
|
||||
@@ -93,6 +93,7 @@ openclaw onboard --non-interactive \
|
||||
|
||||
`--custom-api-key` is optional in non-interactive mode. If omitted, onboarding checks `CUSTOM_API_KEY`.
|
||||
OpenClaw marks common vision model IDs as image-capable automatically. Pass `--custom-image-input` for unknown custom vision IDs, or `--custom-text-input` to force text-only metadata.
|
||||
Use `--custom-compatibility openai-responses` for OpenAI-compatible endpoints that support `/v1/responses` but not `/v1/chat/completions`.
|
||||
|
||||
LM Studio also supports a provider-specific key flag in non-interactive mode:
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ instead of creating a separate health gate.
|
||||
|
||||
Policy currently manages configured channels, MCP servers, model providers,
|
||||
network SSRF posture, ingress/channel access posture, Gateway exposure posture, agent workspace posture,
|
||||
OpenClaw config secret provider/auth profile posture, and governed tool
|
||||
data-handling posture, OpenClaw config secret provider/auth profile posture, and governed tool
|
||||
declarations. For example, IT or a workspace operator can record that Telegram
|
||||
is not an approved channel provider, restrict MCP servers and model refs to
|
||||
approved entries, require private-network fetch/browser access to remain
|
||||
@@ -28,7 +28,9 @@ to stay within reviewed bounds, require Gateway bind/auth/HTTP exposure to stay
|
||||
bounds, require agent workspace access and tool denies to stay in a reviewed
|
||||
posture, require OpenClaw config SecretRefs to use managed providers, require
|
||||
config auth profiles to carry provider/mode metadata, require governed tools to
|
||||
carry risk and sensitivity metadata, then use `doctor --lint` as the shared
|
||||
carry risk and sensitivity metadata, require sensitive logging redaction, deny
|
||||
telemetry content capture, require session retention maintenance, deny session
|
||||
transcript memory indexing, then use `doctor --lint` as the shared
|
||||
conformance gate.
|
||||
|
||||
Use policy when a workspace needs a durable statement such as "these channels
|
||||
@@ -52,7 +54,7 @@ doctor can report the missing artifact.
|
||||
Policy is authored, not generated from the user's current settings. A minimal
|
||||
policy for channels, MCP servers, model providers, network posture, ingress/channel access, Gateway
|
||||
exposure, agent workspace posture, configured sandbox runtime posture, OpenClaw
|
||||
config secret provider/auth profile posture, and tool metadata looks like this:
|
||||
data-handling posture, config secret provider/auth profile posture, and tool metadata looks like this:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
@@ -118,6 +120,20 @@ config secret provider/auth profile posture, and tool metadata looks like this:
|
||||
"denyTools": ["exec", "process", "write", "edit", "apply_patch"],
|
||||
},
|
||||
},
|
||||
"dataHandling": {
|
||||
"sensitiveLogging": {
|
||||
"requireRedaction": true,
|
||||
},
|
||||
"telemetry": {
|
||||
"denyContentCapture": true,
|
||||
},
|
||||
"retention": {
|
||||
"requireSessionMaintenance": true,
|
||||
},
|
||||
"memory": {
|
||||
"denySessionTranscriptIndexing": true,
|
||||
},
|
||||
},
|
||||
"secrets": {
|
||||
"requireManagedProviders": true,
|
||||
"denySources": ["exec"],
|
||||
@@ -155,7 +171,8 @@ when a concrete rule is present. OpenClaw reads current `channels.*` settings
|
||||
`mcp.servers.*`, `models.providers.*`, selected agent model refs, network SSRF
|
||||
settings, direct-message session scope, channel DM policy, channel group policy,
|
||||
channel/group mention gates, Gateway bind/auth/Control UI/Tailscale/remote/HTTP
|
||||
posture, OpenClaw config agent sandbox workspace access and tool deny posture, config secret
|
||||
posture, OpenClaw config agent sandbox workspace access and tool deny posture,
|
||||
data-handling config posture, config secret
|
||||
provider and SecretRef provenance, config auth profile metadata, configured
|
||||
global/per-agent tool posture, and `TOOLS.md` declarations as evidence, then
|
||||
reports observed state that does not conform. If a policy denies non-loopback
|
||||
@@ -176,6 +193,11 @@ runtime. Secret evidence records
|
||||
provider/source posture and SecretRef metadata, never raw secret values. Policy
|
||||
does not read or attest per-agent credential stores such as `auth-profiles.json`;
|
||||
those stores remain owned by the existing auth and credential flows.
|
||||
Data-handling evidence is config-level posture only: it checks configured
|
||||
redaction mode, telemetry content-capture toggles, session maintenance mode, and
|
||||
session-transcript memory indexing settings. It does not inspect raw logs,
|
||||
telemetry exports, transcript contents, memory files, or prove that no personal
|
||||
data or secrets exist.
|
||||
|
||||
### Policy rule reference
|
||||
|
||||
@@ -183,6 +205,8 @@ Each policy field below is optional. A check runs only when the matching rule is
|
||||
present in `policy.jsonc`. The observed state is existing OpenClaw config or
|
||||
workspace metadata; policy reports drift but does not rewrite runtime behavior
|
||||
unless a repair path is explicitly available and enabled.
|
||||
Policy files are strict: unsupported sections or rule keys are reported as
|
||||
`policy/policy-jsonc-invalid` instead of being ignored.
|
||||
|
||||
Policy overlays keep broad top-level rules global, then let named scope blocks
|
||||
add stricter normal policy sections for explicit selectors. A scope name is a
|
||||
@@ -194,7 +218,8 @@ its own finding against the same observed config.
|
||||
|
||||
Use `scopes.<scopeName>` when one set of agents or channels needs stricter
|
||||
policy than the top-level baseline. Agent-scoped sections use `agentIds`, which
|
||||
supports `tools.*`, `agents.workspace.*`, and `sandbox.*`. Channel-scoped
|
||||
supports `tools.*`, `agents.workspace.*`, `sandbox.*`, and
|
||||
`dataHandling.memory.*`. Channel-scoped
|
||||
ingress uses `channelIds`, which supports `ingress.channels.*`. Unsupported
|
||||
sections are rejected instead of being ignored. If an `agentIds` entry is not
|
||||
present in `agents.list[]`, OpenClaw evaluates the scoped rule against inherited
|
||||
@@ -233,6 +258,11 @@ global/default posture for that runtime agent id.
|
||||
"requireMode": ["all"],
|
||||
"allowBackends": ["docker"],
|
||||
},
|
||||
"dataHandling": {
|
||||
"memory": {
|
||||
"denySessionTranscriptIndexing": true,
|
||||
},
|
||||
},
|
||||
},
|
||||
"shell-sandbox": {
|
||||
"agentIds": ["shell-agent"],
|
||||
@@ -274,10 +304,10 @@ groups where those fields cannot be observed.
|
||||
Top-level `ingress.session.requireDmScope` remains global because
|
||||
`session.dmScope` is not channel-attributable evidence.
|
||||
|
||||
| Selector | Supported sections | Use when |
|
||||
| ------------ | ------------------------------------------ | ------------------------------------------------- |
|
||||
| `agentIds` | `tools`, `agents.workspace`, and `sandbox` | One or more runtime agents need stricter rules. |
|
||||
| `channelIds` | `ingress.channels` | One or more channels need stricter ingress rules. |
|
||||
| Selector | Supported sections | Use when |
|
||||
| ------------ | ----------------------------------------------------------------- | ------------------------------------------------- |
|
||||
| `agentIds` | `tools`, `agents.workspace`, `sandbox`, and `dataHandling.memory` | One or more runtime agents need stricter rules. |
|
||||
| `channelIds` | `ingress.channels` | One or more channels need stricter ingress rules. |
|
||||
|
||||
Every scope present in `policy.jsonc` must be valid and enforceable.
|
||||
|
||||
@@ -354,6 +384,15 @@ Policy treats missing `sandbox.mode` as the implicit default `off`, so
|
||||
`sandbox.requireMode` reports a fresh or unconfigured sandbox as outside an
|
||||
allowlist such as `["all"]`.
|
||||
|
||||
#### Data Handling
|
||||
|
||||
| Policy field | Observed state | Use when |
|
||||
| --------------------------------------------------- | ------------------------------------------------------------------------------------ | ---------------------------------------------------------------------- |
|
||||
| `dataHandling.sensitiveLogging.requireRedaction` | `logging.redactSensitive` | Set to `true` to reject `logging.redactSensitive: "off"`. |
|
||||
| `dataHandling.telemetry.denyContentCapture` | `diagnostics.otel.captureContent` | Set to `true` to reject telemetry content capture. |
|
||||
| `dataHandling.retention.requireSessionMaintenance` | `session.maintenance.mode` | Set to `true` to require effective session maintenance mode `enforce`. |
|
||||
| `dataHandling.memory.denySessionTranscriptIndexing` | `memory.qmd.sessions.enabled` and `agents.*.memorySearch.experimental.sessionMemory` | Set to `true` to reject session transcript indexing into memory. |
|
||||
|
||||
#### Secrets
|
||||
|
||||
| Policy field | Observed state | Use when |
|
||||
@@ -674,63 +713,67 @@ choose a different interval.
|
||||
|
||||
Policy currently verifies:
|
||||
|
||||
| Check id | Finding |
|
||||
| ------------------------------------------------- | --------------------------------------------------------------------------------- |
|
||||
| `policy/policy-jsonc-missing` | Policy is enabled but `policy.jsonc` is missing. |
|
||||
| `policy/policy-jsonc-invalid` | Policy cannot be parsed or contains malformed rule entries. |
|
||||
| `policy/policy-hash-mismatch` | Policy does not match configured `expectedHash`. |
|
||||
| `policy/attestation-hash-mismatch` | Current policy evidence no longer matches the accepted attestation. |
|
||||
| `policy/policy-conformance-invalid` | A baseline or checked policy file has invalid comparison syntax. |
|
||||
| `policy/policy-conformance-missing` | A checked policy file is missing a rule required by the baseline policy file. |
|
||||
| `policy/policy-conformance-weaker` | A checked policy file has a weaker value than the baseline policy file. |
|
||||
| `policy/channels-denied-provider` | An enabled channel matches a channel deny rule. |
|
||||
| `policy/mcp-denied-server` | A configured MCP server is denied by policy. |
|
||||
| `policy/mcp-unapproved-server` | A configured MCP server is outside the allowlist. |
|
||||
| `policy/models-denied-provider` | A configured model provider or model ref uses a denied provider. |
|
||||
| `policy/models-unapproved-provider` | A configured model provider or model ref is outside the allowlist. |
|
||||
| `policy/network-private-access-enabled` | A private-network SSRF escape hatch is enabled when policy denies it. |
|
||||
| `policy/ingress-dm-policy-unapproved` | A channel DM policy is outside the policy allowlist. |
|
||||
| `policy/ingress-dm-scope-unapproved` | `session.dmScope` does not match the policy-required DM isolation scope. |
|
||||
| `policy/ingress-open-groups-denied` | A channel group policy is `open` while policy denies open group ingress. |
|
||||
| `policy/ingress-group-mention-required` | A channel or group entry disables mention gates while policy requires them. |
|
||||
| `policy/gateway-non-loopback-bind` | Gateway bind posture permits non-loopback exposure when policy denies it. |
|
||||
| `policy/gateway-auth-disabled` | Gateway authentication is disabled when policy requires auth. |
|
||||
| `policy/gateway-rate-limit-missing` | Gateway auth rate-limit posture is not explicit when policy requires it. |
|
||||
| `policy/gateway-control-ui-insecure` | Gateway Control UI insecure exposure toggles are enabled. |
|
||||
| `policy/gateway-tailscale-funnel` | Gateway Tailscale Funnel exposure is enabled when policy denies it. |
|
||||
| `policy/gateway-remote-enabled` | Gateway remote mode is active when policy denies it. |
|
||||
| `policy/gateway-http-endpoint-enabled` | A Gateway HTTP API endpoint is enabled while denied by policy. |
|
||||
| `policy/gateway-http-url-fetch-unrestricted` | Gateway HTTP URL-fetch input lacks a required URL allowlist. |
|
||||
| `policy/agents-workspace-access-denied` | Agent sandbox mode or workspace access is outside the policy allowlist. |
|
||||
| `policy/agents-tool-not-denied` | An agent or default config does not deny a tool required by policy. |
|
||||
| `policy/tools-profile-unapproved` | A configured global or per-agent tool profile is outside the allowlist. |
|
||||
| `policy/tools-fs-workspace-only-required` | Filesystem tools are not configured with workspace-only path posture. |
|
||||
| `policy/tools-exec-security-unapproved` | Exec security mode is outside the policy allowlist. |
|
||||
| `policy/tools-exec-ask-unapproved` | Exec ask mode is outside the policy allowlist. |
|
||||
| `policy/tools-exec-host-unapproved` | Exec host routing is outside the policy allowlist. |
|
||||
| `policy/tools-elevated-enabled` | Elevated tool mode is enabled when policy denies it. |
|
||||
| `policy/tools-also-allow-missing` | A configured `alsoAllow` list is missing an entry required by policy. |
|
||||
| `policy/tools-also-allow-unexpected` | A configured `alsoAllow` list includes an entry not expected by policy. |
|
||||
| `policy/tools-required-deny-missing` | A global or per-agent tool deny list does not include a required denied tool. |
|
||||
| `policy/sandbox-mode-unapproved` | Sandbox mode is outside the policy allowlist. |
|
||||
| `policy/sandbox-backend-unapproved` | Sandbox backend is outside the policy allowlist. |
|
||||
| `policy/sandbox-container-posture-unobservable` | A container posture rule is enabled for a backend that cannot observe it. |
|
||||
| `policy/sandbox-container-host-network-denied` | A container-backed sandbox or browser uses host network mode. |
|
||||
| `policy/sandbox-container-namespace-join-denied` | A container-backed sandbox or browser joins another container namespace. |
|
||||
| `policy/sandbox-container-mount-mode-required` | A container-backed sandbox or browser mount is not read-only. |
|
||||
| `policy/sandbox-container-runtime-socket-mount` | A container-backed sandbox or browser mount exposes the container runtime socket. |
|
||||
| `policy/sandbox-container-unconfined-profile` | Container sandbox profile is unconfined when policy denies it. |
|
||||
| `policy/sandbox-browser-cdp-source-range-missing` | Sandbox browser CDP source range is missing when policy requires one. |
|
||||
| `policy/secrets-unmanaged-provider` | A config SecretRef references a provider not declared under `secrets.providers`. |
|
||||
| `policy/secrets-denied-provider-source` | A config secret provider or SecretRef uses a source denied by policy. |
|
||||
| `policy/secrets-insecure-provider` | A secret provider opts into insecure posture when policy denies it. |
|
||||
| `policy/auth-profile-invalid-metadata` | A config auth profile is missing valid provider or mode metadata. |
|
||||
| `policy/auth-profile-unapproved-mode` | A config auth profile mode is outside the policy allowlist. |
|
||||
| `policy/tools-missing-risk-level` | A governed tool declaration is missing risk metadata. |
|
||||
| `policy/tools-unknown-risk-level` | A governed tool declaration uses an unknown risk value. |
|
||||
| `policy/tools-missing-sensitivity-token` | A governed tool declaration is missing sensitivity metadata. |
|
||||
| `policy/tools-missing-owner` | A governed tool declaration is missing owner metadata. |
|
||||
| `policy/tools-unknown-sensitivity-token` | A governed tool declaration uses an unknown sensitivity value. |
|
||||
| Check id | Finding |
|
||||
| -------------------------------------------------------- | --------------------------------------------------------------------------------- |
|
||||
| `policy/policy-jsonc-missing` | Policy is enabled but `policy.jsonc` is missing. |
|
||||
| `policy/policy-jsonc-invalid` | Policy cannot be parsed or contains malformed rule entries. |
|
||||
| `policy/policy-hash-mismatch` | Policy does not match configured `expectedHash`. |
|
||||
| `policy/attestation-hash-mismatch` | Current policy evidence no longer matches the accepted attestation. |
|
||||
| `policy/policy-conformance-invalid` | A baseline or checked policy file has invalid comparison syntax. |
|
||||
| `policy/policy-conformance-missing` | A checked policy file is missing a rule required by the baseline policy file. |
|
||||
| `policy/policy-conformance-weaker` | A checked policy file has a weaker value than the baseline policy file. |
|
||||
| `policy/channels-denied-provider` | An enabled channel matches a channel deny rule. |
|
||||
| `policy/mcp-denied-server` | A configured MCP server is denied by policy. |
|
||||
| `policy/mcp-unapproved-server` | A configured MCP server is outside the allowlist. |
|
||||
| `policy/models-denied-provider` | A configured model provider or model ref uses a denied provider. |
|
||||
| `policy/models-unapproved-provider` | A configured model provider or model ref is outside the allowlist. |
|
||||
| `policy/network-private-access-enabled` | A private-network SSRF escape hatch is enabled when policy denies it. |
|
||||
| `policy/ingress-dm-policy-unapproved` | A channel DM policy is outside the policy allowlist. |
|
||||
| `policy/ingress-dm-scope-unapproved` | `session.dmScope` does not match the policy-required DM isolation scope. |
|
||||
| `policy/ingress-open-groups-denied` | A channel group policy is `open` while policy denies open group ingress. |
|
||||
| `policy/ingress-group-mention-required` | A channel or group entry disables mention gates while policy requires them. |
|
||||
| `policy/gateway-non-loopback-bind` | Gateway bind posture permits non-loopback exposure when policy denies it. |
|
||||
| `policy/gateway-auth-disabled` | Gateway authentication is disabled when policy requires auth. |
|
||||
| `policy/gateway-rate-limit-missing` | Gateway auth rate-limit posture is not explicit when policy requires it. |
|
||||
| `policy/gateway-control-ui-insecure` | Gateway Control UI insecure exposure toggles are enabled. |
|
||||
| `policy/gateway-tailscale-funnel` | Gateway Tailscale Funnel exposure is enabled when policy denies it. |
|
||||
| `policy/gateway-remote-enabled` | Gateway remote mode is active when policy denies it. |
|
||||
| `policy/gateway-http-endpoint-enabled` | A Gateway HTTP API endpoint is enabled while denied by policy. |
|
||||
| `policy/gateway-http-url-fetch-unrestricted` | Gateway HTTP URL-fetch input lacks a required URL allowlist. |
|
||||
| `policy/agents-workspace-access-denied` | Agent sandbox mode or workspace access is outside the policy allowlist. |
|
||||
| `policy/agents-tool-not-denied` | An agent or default config does not deny a tool required by policy. |
|
||||
| `policy/tools-profile-unapproved` | A configured global or per-agent tool profile is outside the allowlist. |
|
||||
| `policy/tools-fs-workspace-only-required` | Filesystem tools are not configured with workspace-only path posture. |
|
||||
| `policy/tools-exec-security-unapproved` | Exec security mode is outside the policy allowlist. |
|
||||
| `policy/tools-exec-ask-unapproved` | Exec ask mode is outside the policy allowlist. |
|
||||
| `policy/tools-exec-host-unapproved` | Exec host routing is outside the policy allowlist. |
|
||||
| `policy/tools-elevated-enabled` | Elevated tool mode is enabled when policy denies it. |
|
||||
| `policy/tools-also-allow-missing` | A configured `alsoAllow` list is missing an entry required by policy. |
|
||||
| `policy/tools-also-allow-unexpected` | A configured `alsoAllow` list includes an entry not expected by policy. |
|
||||
| `policy/tools-required-deny-missing` | A global or per-agent tool deny list does not include a required denied tool. |
|
||||
| `policy/sandbox-mode-unapproved` | Sandbox mode is outside the policy allowlist. |
|
||||
| `policy/sandbox-backend-unapproved` | Sandbox backend is outside the policy allowlist. |
|
||||
| `policy/sandbox-container-posture-unobservable` | A container posture rule is enabled for a backend that cannot observe it. |
|
||||
| `policy/sandbox-container-host-network-denied` | A container-backed sandbox or browser uses host network mode. |
|
||||
| `policy/sandbox-container-namespace-join-denied` | A container-backed sandbox or browser joins another container namespace. |
|
||||
| `policy/sandbox-container-mount-mode-required` | A container-backed sandbox or browser mount is not read-only. |
|
||||
| `policy/sandbox-container-runtime-socket-mount` | A container-backed sandbox or browser mount exposes the container runtime socket. |
|
||||
| `policy/sandbox-container-unconfined-profile` | Container sandbox profile is unconfined when policy denies it. |
|
||||
| `policy/sandbox-browser-cdp-source-range-missing` | Sandbox browser CDP source range is missing when policy requires one. |
|
||||
| `policy/data-handling-redaction-disabled` | Sensitive logging redaction is disabled when policy requires it. |
|
||||
| `policy/data-handling-telemetry-content-capture` | Telemetry content capture is enabled when policy denies it. |
|
||||
| `policy/data-handling-session-retention-not-enforced` | Session retention maintenance is not enforced when policy requires it. |
|
||||
| `policy/data-handling-session-transcript-memory-enabled` | Session transcript memory indexing is enabled when policy denies it. |
|
||||
| `policy/secrets-unmanaged-provider` | A config SecretRef references a provider not declared under `secrets.providers`. |
|
||||
| `policy/secrets-denied-provider-source` | A config secret provider or SecretRef uses a source denied by policy. |
|
||||
| `policy/secrets-insecure-provider` | A secret provider opts into insecure posture when policy denies it. |
|
||||
| `policy/auth-profile-invalid-metadata` | A config auth profile is missing valid provider or mode metadata. |
|
||||
| `policy/auth-profile-unapproved-mode` | A config auth profile mode is outside the policy allowlist. |
|
||||
| `policy/tools-missing-risk-level` | A governed tool declaration is missing risk metadata. |
|
||||
| `policy/tools-unknown-risk-level` | A governed tool declaration uses an unknown risk value. |
|
||||
| `policy/tools-missing-sensitivity-token` | A governed tool declaration is missing sensitivity metadata. |
|
||||
| `policy/tools-missing-owner` | A governed tool declaration is missing owner metadata. |
|
||||
| `policy/tools-unknown-sensitivity-token` | A governed tool declaration uses an unknown sensitivity value. |
|
||||
|
||||
Policy findings can include both `target` and `requirement`. `target` is the
|
||||
observed workspace thing that does not conform. `requirement` is the authored
|
||||
|
||||
@@ -32,9 +32,8 @@ 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:
|
||||
For webhook ingress, startup logs a non-fatal security warning and audit flags `hooks.token` reuse of active Gateway shared-secret auth values, including `gateway.auth.token` / `OPENCLAW_GATEWAY_TOKEN` and `gateway.auth.password` / `OPENCLAW_GATEWAY_PASSWORD`. It also warns when:
|
||||
|
||||
- `hooks.token` reuses an active Gateway shared-secret auth value (`gateway.auth.token` / `OPENCLAW_GATEWAY_TOKEN` or `gateway.auth.password` / `OPENCLAW_GATEWAY_PASSWORD`)
|
||||
- `hooks.token` is short
|
||||
- `hooks.path="/"`
|
||||
- `hooks.defaultSessionKey` is unset
|
||||
@@ -43,7 +42,7 @@ For webhook ingress, it warns when:
|
||||
- overrides are enabled without `hooks.allowedSessionKeyPrefixes`
|
||||
|
||||
If Gateway password auth is supplied only at startup, pass the same value to `openclaw security audit --auth password --password <password>` so the audit can check it against `hooks.token`.
|
||||
Password-mode reuse is an audit finding for compatibility; rotate one of the secrets instead of expecting Gateway startup to reject that configuration.
|
||||
Run `openclaw doctor --fix` to rotate a persisted reused `hooks.token`, then update external hook senders to use the new hook token.
|
||||
|
||||
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 write/edit tools are disabled but `exec` is still available without a constraining sandbox filesystem boundary, when open groups expose runtime/filesystem tools without sandbox/workspace guards, and when installed 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).
|
||||
|
||||
@@ -194,10 +194,12 @@ OpenClaw resolves that behavior by conversation type:
|
||||
`message(action=send)`.
|
||||
- Internal orchestration allows silence by default.
|
||||
|
||||
OpenClaw also uses silent replies for internal runner failures that happen
|
||||
before any assistant reply in non-direct chats, so groups/channels do not see
|
||||
gateway error boilerplate. Direct chats show compact failure copy by default;
|
||||
raw runner details are shown only when `/verbose full` is enabled.
|
||||
OpenClaw also uses silent replies for generic internal runner failures in
|
||||
non-direct chats, so groups/channels do not see gateway error boilerplate.
|
||||
Classified failures with user-facing recovery copy, such as missing auth,
|
||||
rate-limit, or overload notices, can still be delivered. Direct chats show
|
||||
compact failure copy by default; raw runner details are shown only when
|
||||
`/verbose full` is enabled.
|
||||
|
||||
Defaults live under `agents.defaults.silentReply`; `surfaces.<id>.silentReply`
|
||||
can override group/internal policy per surface.
|
||||
|
||||
@@ -110,8 +110,8 @@ writes.
|
||||
## Session maintenance
|
||||
|
||||
OpenClaw automatically bounds session storage over time. By default, it runs
|
||||
in `warn` mode (reports what would be cleaned). Set `session.maintenance.mode`
|
||||
to `"enforce"` for automatic cleanup:
|
||||
in `enforce` mode and applies cleanup during maintenance. Set
|
||||
`session.maintenance.mode` to `"warn"` to report what would be cleaned without mutating the store/files:
|
||||
|
||||
```json5
|
||||
{
|
||||
|
||||
@@ -372,6 +372,30 @@ its own control markers and channel delivery.
|
||||
For CLIs that emit Claude Code stream-json compatible JSONL, set
|
||||
`jsonlDialect: "claude-stream-json"` on that backend's config.
|
||||
|
||||
## Native compaction ownership
|
||||
|
||||
Some CLI backends run an agent that compacts its **own** transcript, so OpenClaw must
|
||||
not run its safeguard summarizer against them - doing so fights the backend's own
|
||||
compaction and can hard-fail the turn.
|
||||
|
||||
`claude-cli` has no harness endpoint - Claude Code compacts internally - so it declares
|
||||
`ownsNativeCompaction: true`, and OpenClaw returns a no-op from the compaction path.
|
||||
Native-harness sessions such as Codex keep routing to their harness compaction endpoint
|
||||
instead.
|
||||
|
||||
Because the backend owns compaction, the old stopgap of setting
|
||||
`contextTokens: 1_000_000` purely to keep OpenClaw's safeguard from firing on a
|
||||
claude-cli session is **no longer needed** - the opt-out replaces it.
|
||||
|
||||
```typescript
|
||||
api.registerCliBackend({ id: "my-cli", ownsNativeCompaction: true /* ... */ });
|
||||
```
|
||||
|
||||
Only declare `ownsNativeCompaction` for a backend that genuinely owns its compaction: it
|
||||
must reliably bound its own transcript as it nears its context window and persist a
|
||||
resumable session (e.g. `--resume` / `--session-id`); otherwise a deferred session can
|
||||
stay over budget. Matching `agentHarnessId` sessions still route to the harness endpoint.
|
||||
|
||||
## Bundle MCP overlays
|
||||
|
||||
CLI backends do **not** receive OpenClaw tool calls directly, but a backend can
|
||||
|
||||
@@ -1272,7 +1272,7 @@ See [Multi-Agent Sandbox & Tools](/tools/multi-agent-sandbox-tools) for preceden
|
||||
resetTriggers: ["/new", "/reset"],
|
||||
store: "~/.openclaw/agents/{agentId}/sessions/sessions.json",
|
||||
maintenance: {
|
||||
mode: "warn", // warn | enforce
|
||||
mode: "enforce", // enforce (default) | warn
|
||||
pruneAfter: "30d",
|
||||
maxEntries: 500,
|
||||
resetArchiveRetention: "30d", // duration or false
|
||||
@@ -1311,7 +1311,7 @@ See [Multi-Agent Sandbox & Tools](/tools/multi-agent-sandbox-tools) for preceden
|
||||
- **`agentToAgent.maxPingPongTurns`**: maximum reply-back turns between agents during agent-to-agent exchanges (integer, range: `0`-`20`, default: `5`). `0` disables ping-pong chaining.
|
||||
- **`sendPolicy`**: match by `channel`, `chatType` (`direct|group|channel`, with legacy `dm` alias), `keyPrefix`, or `rawKeyPrefix`. First deny wins.
|
||||
- **`maintenance`**: session-store cleanup + retention controls.
|
||||
- `mode`: `warn` emits warnings only; `enforce` applies cleanup.
|
||||
- `mode`: `enforce` applies cleanup and is the default; `warn` emits warnings only.
|
||||
- `pruneAfter`: age cutoff for stale entries (default `30d`).
|
||||
- `maxEntries`: maximum number of entries in `sessions.json` (default `500`). Runtime writes batch cleanup with a small high-water buffer for production-sized caps; `openclaw sessions cleanup --enforce` applies the cap immediately.
|
||||
- `rotateBytes`: deprecated and ignored; `openclaw doctor --fix` removes it from older configs.
|
||||
|
||||
@@ -469,7 +469,7 @@ Experimental built-in tool flags. Default off unless a strict-agentic GPT-5 auto
|
||||
|
||||
- `model`: default model for spawned sub-agents. If omitted, sub-agents inherit the caller's model.
|
||||
- `allowAgents`: default allowlist of configured target agent ids for `sessions_spawn` when the requester agent does not set its own `subagents.allowAgents` (`["*"]` = any configured target; default: same agent only). Stale entries whose agent config was deleted are rejected by `sessions_spawn` and omitted from `agents_list`; run `openclaw doctor --fix` to clean them up.
|
||||
- `runTimeoutSeconds`: default timeout (seconds) for `sessions_spawn` when the tool call omits `runTimeoutSeconds`. `0` means no timeout.
|
||||
- `runTimeoutSeconds`: default timeout (seconds) for `sessions_spawn`. `0` means no timeout.
|
||||
- `announceTimeoutMs`: per-call timeout (milliseconds) for gateway `agent` announce delivery attempts. Default: `120000`. Transient retries can make the total announce wait longer than one configured timeout.
|
||||
- Per-subagent tool policy: `tools.subagents.tools.allow` / `tools.subagents.tools.deny`.
|
||||
|
||||
|
||||
@@ -730,8 +730,8 @@ Query-string hook tokens are rejected.
|
||||
Validation and safety notes:
|
||||
|
||||
- `hooks.enabled=true` requires a non-empty `hooks.token`.
|
||||
- `hooks.token` must be distinct from `gateway.auth.token` / `OPENCLAW_GATEWAY_TOKEN`; reusing the Gateway token fails startup validation.
|
||||
- `openclaw security audit` also flags `hooks.token` reuse of active Gateway password auth (`gateway.auth.password` / `OPENCLAW_GATEWAY_PASSWORD`, or `--auth password --password <password>`) as a critical finding; password-mode reuse stays startup-compatible and should be repaired by rotating one of the secrets.
|
||||
- `hooks.token` should be distinct from active Gateway shared-secret auth (`gateway.auth.token` / `OPENCLAW_GATEWAY_TOKEN` or `gateway.auth.password` / `OPENCLAW_GATEWAY_PASSWORD`); startup logs a non-fatal security warning when it detects reuse.
|
||||
- `openclaw security audit` flags hook/Gateway auth reuse as a critical finding, including Gateway password auth supplied only at audit time (`--auth password --password <password>`). Run `openclaw doctor --fix` to rotate a persisted reused `hooks.token`, then update external hook senders to use the new hook token.
|
||||
- `hooks.path` cannot be `/`; use a dedicated subpath such as `/hooks`.
|
||||
- If `hooks.allowRequestSessionKey=true`, constrain `hooks.allowedSessionKeyPrefixes` (for example `["hook:"]`).
|
||||
- If a mapping or preset uses a templated `sessionKey`, set `hooks.allowedSessionKeyPrefixes` and `hooks.allowRequestSessionKey=true`. Static mapping keys do not require that opt-in.
|
||||
|
||||
@@ -329,6 +329,7 @@ Android nodes can advertise additional command families when the corresponding c
|
||||
Available families:
|
||||
|
||||
- `device.status`, `device.info`, `device.permissions`, `device.health`
|
||||
- `device.apps` when Installed Apps sharing is enabled in Android Settings
|
||||
- `notifications.list`, `notifications.actions`
|
||||
- `photos.latest`
|
||||
- `contacts.search`, `contacts.add`
|
||||
@@ -341,12 +342,14 @@ Example invokes:
|
||||
|
||||
```bash
|
||||
openclaw nodes invoke --node <idOrNameOrIp> --command device.status --params '{}'
|
||||
openclaw nodes invoke --node <idOrNameOrIp> --command device.apps --params '{"limit":10}'
|
||||
openclaw nodes invoke --node <idOrNameOrIp> --command notifications.list --params '{}'
|
||||
openclaw nodes invoke --node <idOrNameOrIp> --command photos.latest --params '{"limit":1}'
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- `device.apps` is opt-in and returns launcher-visible apps by default.
|
||||
- Motion commands are capability-gated by available sensors.
|
||||
|
||||
## System commands (node host / mac node)
|
||||
|
||||
@@ -219,8 +219,9 @@ See [Camera node](/nodes/camera) for parameters and CLI helpers.
|
||||
- By default, Android Talk uses native speech recognition, Gateway chat, and `talk.speak` through the configured gateway Talk provider. Local system TTS is used only when `talk.speak` is unavailable.
|
||||
- Android Talk uses realtime Gateway relay only when `talk.realtime.mode` is `realtime` and `talk.realtime.transport` is `gateway-relay`.
|
||||
- Voice wake remains disabled in the Android UX/runtime.
|
||||
- Additional Android command families (availability depends on device + permissions):
|
||||
- Additional Android command families (availability depends on device, permissions, and user settings):
|
||||
- `device.status`, `device.info`, `device.permissions`, `device.health`
|
||||
- `device.apps` only when **Settings > Phone Capabilities > Installed Apps** is enabled; it lists launcher-visible apps by default.
|
||||
- `notifications.list`, `notifications.actions` (see [Notification forwarding](#notification-forwarding) below)
|
||||
- `photos.latest`
|
||||
- `contacts.search`, `contacts.add`
|
||||
|
||||
@@ -208,10 +208,28 @@ only for behavior that really belongs to the backend.
|
||||
| `authEpochMode` | Decide how auth changes invalidate stored CLI sessions |
|
||||
| `nativeToolMode` | Declare whether the CLI has always-on native tools |
|
||||
| `bundleMcp` / `bundleMcpMode` | Opt into OpenClaw's loopback MCP tool bridge |
|
||||
| `ownsNativeCompaction` | Backend owns its own compaction - OpenClaw defers |
|
||||
|
||||
Keep these hooks provider-owned. Do not add CLI-specific branches to core when a
|
||||
backend hook can express the behavior.
|
||||
|
||||
### `ownsNativeCompaction`: opting out of OpenClaw compaction
|
||||
|
||||
If your backend runs an agent that compacts its **own** transcript, set
|
||||
`ownsNativeCompaction: true` so OpenClaw's safeguard summarizer never runs against its
|
||||
sessions - the CLI compaction lifecycle returns a no-op and the turn proceeds. `claude-cli`
|
||||
declares it because Claude Code compacts internally with no harness endpoint. Native-harness
|
||||
sessions such as Codex keep routing to their harness compaction endpoint instead.
|
||||
|
||||
**Only declare it when all of the following hold**, or a deferred over-budget session can
|
||||
stay over budget / go stale (OpenClaw no longer rescues it):
|
||||
|
||||
- the backend reliably compacts or bounds its own transcript as it nears its window;
|
||||
- it persists a resumable session so the compacted state survives turns
|
||||
(e.g. `--resume` / `--session-id`);
|
||||
- it is not a native-harness compaction session - matching `agentHarnessId` sessions
|
||||
route to the harness endpoint instead.
|
||||
|
||||
## MCP tool bridge
|
||||
|
||||
CLI backends do not receive OpenClaw tools by default. If the CLI can consume an
|
||||
|
||||
@@ -120,6 +120,7 @@ observation-only.
|
||||
|
||||
- **`before_tool_call`** - rewrite tool params, block execution, or require approval
|
||||
- `after_tool_call` - observe tool results, errors, and duration
|
||||
- `resolve_exec_env` - contribute plugin-owned environment variables to `exec`
|
||||
- **`tool_result_persist`** - rewrite the assistant message produced from a tool result
|
||||
- **`before_message_write`** - inspect or block an in-progress message write (rare)
|
||||
|
||||
@@ -233,6 +234,28 @@ for host-trusted gates such as workspace policy, budget enforcement, or
|
||||
reserved workflow safety. External plugins should use normal `before_tool_call`
|
||||
hooks.
|
||||
|
||||
### Exec environment hook
|
||||
|
||||
`resolve_exec_env` lets plugins contribute environment variables to `exec`
|
||||
tool invocations after the base exec environment is built and before the
|
||||
command runs. It receives:
|
||||
|
||||
- `event.sessionKey`
|
||||
- `event.toolName`, currently always `"exec"`
|
||||
- `event.host`, one of `"gateway"`, `"sandbox"`, or `"node"`
|
||||
- context fields such as `ctx.agentId`, `ctx.sessionKey`,
|
||||
`ctx.messageProvider`, and `ctx.channelId`
|
||||
|
||||
Return a `Record<string, string>` to merge into the exec environment. Handlers
|
||||
run in priority order, and later hook results override earlier hook results for
|
||||
the same key.
|
||||
|
||||
Hook output is filtered through the host exec environment key policy before it
|
||||
is merged. Invalid keys, `PATH`, and dangerous host override keys such as
|
||||
`LD_*`, `DYLD_*`, `NODE_OPTIONS`, proxy variables, and TLS override variables
|
||||
are dropped. The filtered plugin env is included in gateway approval/audit
|
||||
metadata and forwarded to node-host execution requests.
|
||||
|
||||
### Tool result persistence
|
||||
|
||||
Tool results can include structured `details` for UI rendering, diagnostics,
|
||||
|
||||
@@ -27,7 +27,7 @@ settings and governed workspace declarations. Policy currently covers channel
|
||||
conformance, governed tool metadata, MCP server posture, model-provider posture,
|
||||
private-network access posture, Gateway exposure posture, agent workspace/tool
|
||||
posture, configured global/per-agent tool posture, configured sandbox runtime
|
||||
posture, ingress/channel access posture, and OpenClaw config secret
|
||||
posture, ingress/channel access posture, data-handling posture, and OpenClaw config secret
|
||||
provider/auth profile posture.
|
||||
|
||||
Policy stores authored requirements in `policy.jsonc`, observes existing
|
||||
@@ -55,9 +55,16 @@ and require sandbox browser CDP source ranges.
|
||||
These checks observe config conformance only; they do not read runtime approval
|
||||
state, inspect live containers, or add runtime enforcement.
|
||||
|
||||
Data-handling rules can require sensitive logging redaction, deny telemetry
|
||||
content capture, require session retention maintenance, and deny session
|
||||
transcript memory indexing. These checks observe config conformance only; they
|
||||
do not inspect raw logs, telemetry exports, transcripts, memory files, secrets,
|
||||
or personal data.
|
||||
|
||||
Named policy scopes under `scopes.<scopeName>` can add stricter normal policy
|
||||
sections for the selector they list. `agentIds` supports `tools`,
|
||||
`agents.workspace`, and `sandbox`; `channelIds` supports `ingress.channels`.
|
||||
`agents.workspace`, `sandbox`, and `dataHandling.memory`; `channelIds` supports
|
||||
`ingress.channels`.
|
||||
Runtime agent ids that are not explicitly listed in `agents.list[]` are checked
|
||||
against inherited global/default posture rather than silently passing with no
|
||||
evidence. Every scope present in `policy.jsonc` must be valid and enforceable
|
||||
|
||||
@@ -353,7 +353,7 @@ API key auth, and dynamic model resolution.
|
||||
| --- | --- | --- |
|
||||
| `openai-compatible` | Shared OpenAI-style replay policy for OpenAI-compatible transports, including tool-call-id sanitation, assistant-first ordering fixes, and generic Gemini-turn validation where the transport needs it | `moonshot`, `ollama`, `xai`, `zai` |
|
||||
| `anthropic-by-model` | Claude-aware replay policy chosen by `modelId`, so Anthropic-message transports only get Claude-specific thinking-block cleanup when the resolved model is actually a Claude id | `amazon-bedrock`, `anthropic-vertex` |
|
||||
| `google-gemini` | Native Gemini replay policy plus bootstrap replay sanitation and tagged reasoning-output mode | `google`, `google-gemini-cli` |
|
||||
| `google-gemini` | Native Gemini replay policy plus bootstrap replay sanitation. The shared family keeps the text-output Gemini CLI on tagged reasoning; the direct `google` provider overrides `resolveReasoningOutputMode` to `native` because Gemini API thinking arrives as native thought parts. | `google`, `google-gemini-cli` |
|
||||
| `passthrough-gemini` | Gemini thought-signature sanitation for Gemini models running through OpenAI-compatible proxy transports; does not enable native Gemini replay validation or bootstrap rewrites | `openrouter`, `kilocode`, `opencode`, `opencode-go` |
|
||||
| `hybrid-anthropic-openai` | Hybrid policy for providers that mix Anthropic-message and OpenAI-compatible model surfaces in one plugin; optional Claude-only thinking-block dropping stays scoped to the Anthropic side | `minimax` |
|
||||
|
||||
@@ -376,6 +376,13 @@ API key auth, and dynamic model resolution.
|
||||
- `openclaw/plugin-sdk/provider-stream` - `ProviderStreamFamily`, `buildProviderStreamFamilyHooks(...)`, `composeProviderStreamWrappers(...)`, plus the shared OpenAI/Codex wrappers (`createOpenAIAttributionHeadersWrapper`, `createOpenAIFastModeWrapper`, `createOpenAIServiceTierWrapper`, `createOpenAIResponsesContextManagementWrapper`, `createCodexNativeWebSearchWrapper`), DeepSeek V4 OpenAI-compatible wrapper (`createDeepSeekV4OpenAICompatibleThinkingWrapper`), Anthropic Messages thinking prefill cleanup (`createAnthropicThinkingPrefillPayloadWrapper`), plain-text tool-call compat (`createPlainTextToolCallCompatWrapper`), and shared proxy/provider wrappers (`createOpenRouterWrapper`, `createToolStreamWrapper`, `createMinimaxFastModeWrapper`).
|
||||
- `openclaw/plugin-sdk/provider-tools` - `ProviderToolCompatFamily`, `buildProviderToolCompatFamilyHooks("deepseek" | "gemini" | "openai")`, and underlying provider schema helpers.
|
||||
|
||||
For Gemini-family providers, keep the reasoning-output mode aligned with
|
||||
the transport. Direct Google Gemini API providers should use `native`
|
||||
reasoning output so OpenClaw consumes native thought parts without adding
|
||||
`<think>` / `<final>` prompt directives. Text-only Gemini CLI-style
|
||||
backends that parse a final JSON/text response can keep the shared
|
||||
`google-gemini` tagged contract.
|
||||
|
||||
Some stream helpers stay provider-local on purpose. `@openclaw/anthropic-provider` keeps `wrapAnthropicProviderStream`, `resolveAnthropicBetas`, `resolveAnthropicFastMode`, `resolveAnthropicServiceTier`, and the lower-level Anthropic wrapper builders in its own public `api.ts` / `contract-api.ts` seam because they encode Claude OAuth beta handling and `context1m` gating. The xAI plugin similarly keeps native xAI Responses shaping in its own `wrapStreamFn` (`/fast` aliases, default `tool_stream`, unsupported strict-tool cleanup, xAI-specific reasoning-payload removal).
|
||||
|
||||
The same package-root pattern also backs `@openclaw/openai-provider` (provider builders, default-model helpers, realtime provider builders) and `@openclaw/openrouter-provider` (provider builder plus onboarding/config helpers).
|
||||
|
||||
@@ -292,7 +292,8 @@ Workboard stops auto-moving that card until you move it back to `todo` or
|
||||
2. Create a card with a title, notes, priority, labels, optional agent, and
|
||||
optional linked session.
|
||||
3. Or open Sessions and choose Add to Workboard for an existing session.
|
||||
4. Drag the card between columns or use the column controls.
|
||||
4. Drag the card between columns or focus the compact status control on the card
|
||||
and use its menu or ArrowLeft/ArrowRight.
|
||||
5. Start work from the card to create or reuse a dashboard session.
|
||||
6. Open the linked session from the card while the agent works.
|
||||
7. Let lifecycle sync move running work into review or blocked, then manually
|
||||
|
||||
@@ -58,6 +58,15 @@ explicitly to use Gemini, Voyage, Mistral, DeepInfra, Bedrock, GitHub Copilot,
|
||||
Ollama, a local GGUF model, or an OpenAI-compatible `/v1/embeddings` endpoint.
|
||||
Legacy configs that still say `provider: "auto"` resolve to `openai`.
|
||||
|
||||
<Warning>
|
||||
Changing the embedding provider, model, provider settings, sources, scope,
|
||||
chunking, or tokenizer can make the existing SQLite vector index incompatible.
|
||||
OpenClaw pauses vector search and reports an index identity warning instead of
|
||||
automatically re-embedding everything. Rebuild when you are ready with
|
||||
`openclaw memory status --index --agent <id>` or
|
||||
`openclaw memory index --force --agent <id>`.
|
||||
</Warning>
|
||||
|
||||
If OpenAI embeddings are unreachable from your network, memory recall fails open
|
||||
instead of blocking the turn. Set the existing `memorySearch.provider` field to a
|
||||
reachable local, Ollama, regional, or OpenAI-compatible provider to restore
|
||||
@@ -155,7 +164,8 @@ Use `provider: "openai-compatible"` for a generic OpenAI-compatible
|
||||
| `outputDimensionality` | `number` | `3072` | For Embedding 2: 768, 1536, or 3072 |
|
||||
|
||||
<Warning>
|
||||
Changing model or `outputDimensionality` triggers an automatic full reindex.
|
||||
Changing model or `outputDimensionality` changes the index identity. OpenClaw
|
||||
pauses vector search until you explicitly rebuild the memory index.
|
||||
</Warning>
|
||||
|
||||
</Accordion>
|
||||
|
||||
@@ -105,7 +105,7 @@ Per-agent heartbeat is supported at `agents.list[].heartbeat`.
|
||||
|
||||
- Prompt caching is automatic on supported recent models. OpenClaw does not need to inject block-level cache markers.
|
||||
- OpenClaw uses `prompt_cache_key` to keep cache routing stable across turns. Direct OpenAI hosts use `prompt_cache_retention: "24h"` when `cacheRetention: "long"` is selected.
|
||||
- OpenAI-compatible Completions providers receive `prompt_cache_key` only when their model config explicitly sets `compat.supportsPromptCacheKey: true`; with that same opt-in, explicit `cacheRetention: "long"` also forwards `prompt_cache_retention: "24h"`, and `cacheRetention: "none"` suppresses both fields.
|
||||
- OpenAI-compatible Completions providers receive `prompt_cache_key` only when their model config explicitly sets `compat.supportsPromptCacheKey: true`. Long-retention forwarding is a separate capability: explicit `cacheRetention: "long"` sends `prompt_cache_retention: "24h"` only when that compat entry also supports long cache retention. Providers such as Mistral can opt into cache keys while setting `compat.supportsLongCacheRetention: false` to suppress the long-retention field. `cacheRetention: "none"` suppresses both fields.
|
||||
- OpenAI responses expose cached prompt tokens via `usage.prompt_tokens_details.cached_tokens` (or `input_tokens_details.cached_tokens` on Responses API events). OpenClaw maps that to `cacheRead`.
|
||||
- OpenAI does not expose a separate cache-write token counter, so `cacheWrite` stays `0` on OpenAI paths even when the provider is warming a cache.
|
||||
- OpenAI returns useful tracing and rate-limit headers such as `x-request-id`, `openai-processing-ms`, and `x-ratelimit-*`, but cache-hit accounting should come from the usage payload, not from headers.
|
||||
|
||||
@@ -78,7 +78,7 @@ OpenClaw resolves these via `src/config/sessions.ts`.
|
||||
|
||||
Session persistence has automatic maintenance controls (`session.maintenance`) for `sessions.json`, transcript artifacts, and trajectory sidecars:
|
||||
|
||||
- `mode`: `warn` (default) or `enforce`
|
||||
- `mode`: `enforce` (default) or `warn`
|
||||
- `pruneAfter`: stale-entry age cutoff (default `30d`)
|
||||
- `maxEntries`: cap entries in `sessions.json` (default `500`)
|
||||
- `resetArchiveRetention`: retention for `*.reset.<timestamp>` transcript archives (default: same as `pruneAfter`; `false` disables cleanup)
|
||||
|
||||
@@ -101,6 +101,8 @@ Automated UK school meal booking via ParentPay. Uses mouse coordinates for relia
|
||||
**@julianengel** • `files` `r2` `presigned-urls`
|
||||
|
||||
Upload to Cloudflare R2/S3 and generate secure presigned download links. Useful for remote OpenClaw instances.
|
||||
|
||||
<img src="/assets/showcase/r2-upload.png" alt="R2 upload skill on ClawHub" />
|
||||
</Card>
|
||||
|
||||
<Card title="iOS app via Telegram" icon="mobile">
|
||||
@@ -269,6 +271,8 @@ Vapi voice assistant to OpenClaw HTTP bridge. Near real-time phone calls with yo
|
||||
**@obviyus** • `transcription` `multilingual` `skill`
|
||||
|
||||
Multi-lingual audio transcription via OpenRouter (Gemini, and more). Available on ClawHub.
|
||||
|
||||
<img src="/assets/showcase/openrouter-transcribe.png" alt="OpenRouter transcription skill on ClawHub" />
|
||||
</Card>
|
||||
|
||||
</CardGroup>
|
||||
@@ -289,6 +293,8 @@ OpenClaw gateway running on Home Assistant OS with SSH tunnel support and persis
|
||||
**ClawHub** • `homeassistant` `skill` `automation`
|
||||
|
||||
Control and automate Home Assistant devices via natural language.
|
||||
|
||||
<img src="/assets/showcase/homeassistant.png" alt="Home Assistant skill on ClawHub" />
|
||||
</Card>
|
||||
|
||||
<Card title="Nix packaging" icon="snowflake" href="https://github.com/openclaw/nix-openclaw">
|
||||
@@ -301,6 +307,8 @@ Batteries-included nixified OpenClaw configuration for reproducible deployments.
|
||||
**ClawHub** • `calendar` `caldav` `skill`
|
||||
|
||||
Calendar skill using khal and vdirsyncer. Self-hosted calendar integration.
|
||||
|
||||
<img src="/assets/showcase/caldav-calendar.png" alt="CalDAV calendar skill on ClawHub" />
|
||||
</Card>
|
||||
|
||||
</CardGroup>
|
||||
|
||||
@@ -219,7 +219,7 @@ What you set:
|
||||
- `--custom-model-id`
|
||||
- `--custom-api-key` (optional; falls back to `CUSTOM_API_KEY`)
|
||||
- `--custom-provider-id` (optional)
|
||||
- `--custom-compatibility <openai|anthropic>` (optional; default `openai`)
|
||||
- `--custom-compatibility <openai|openai-responses|anthropic>` (optional; default `openai`)
|
||||
- `--custom-image-input` / `--custom-text-input` (optional; override inferred model input capability)
|
||||
|
||||
</Accordion>
|
||||
|
||||
@@ -286,8 +286,9 @@ different operation limit:
|
||||
openclaw config set plugins.entries.acpx.config.timeoutSeconds 180
|
||||
```
|
||||
|
||||
Runtime turns use OpenClaw agent/run timeouts, including `/acp timeout` and
|
||||
`sessions_spawn.timeoutSeconds`. Restart the gateway after changing this value.
|
||||
Runtime turns use OpenClaw agent/run timeouts, including `/acp timeout`.
|
||||
`sessions_spawn` does not accept per-call timeout overrides. Restart the
|
||||
gateway after changing this value.
|
||||
|
||||
### Health probe agent configuration
|
||||
|
||||
|
||||
@@ -549,12 +549,11 @@ Two ways to start an ACP session:
|
||||
`streamLogPath` pointing to a session-scoped JSONL log
|
||||
(`<sessionId>.acp-stream.jsonl`) you can tail for full relay history.
|
||||
</ParamField>
|
||||
<ParamField path="runTimeoutSeconds" type="number">
|
||||
Aborts the ACP child turn after N seconds. `0` keeps the turn on the
|
||||
gateway's no-timeout path. The same value is applied to the Gateway
|
||||
run and ACP runtime so stalled/quota-exhausted harnesses do not
|
||||
occupy the parent agent lane indefinitely.
|
||||
</ParamField>
|
||||
|
||||
ACP `sessions_spawn` runs use `agents.defaults.subagents.runTimeoutSeconds` for
|
||||
their default child turn limit. The tool does not accept per-call timeout
|
||||
overrides.
|
||||
|
||||
<ParamField path="model" type="string">
|
||||
Explicit model override for the ACP child session. Codex ACP spawns
|
||||
normalize OpenAI refs such as `openai/gpt-5.4` to Codex ACP startup
|
||||
|
||||
@@ -144,9 +144,15 @@ when set at the narrower session or agent scope.
|
||||
### `exec.ask`
|
||||
|
||||
<ParamField path="ask" type='"off" | "on-miss" | "always"'>
|
||||
- `off` - never prompt.
|
||||
- `on-miss` - prompt only when the allowlist does not match.
|
||||
- `always` - prompt on every command. `allow-always` durable trust does **not** suppress prompts when effective ask mode is `always`.
|
||||
Configured ask policy for host exec. Controls the baseline approval
|
||||
prompt behavior from `tools.exec.ask` and host approvals defaults. The
|
||||
per-call `ask` tool parameter (see [Exec tool](/tools/exec#parameters))
|
||||
can only harden that baseline, and channel-origin model calls ignore it
|
||||
when the effective host ask is `off`.
|
||||
|
||||
- `off` - never prompt.
|
||||
- `on-miss` - prompt only when the allowlist does not match.
|
||||
- `always` - prompt on every command. `allow-always` durable trust does **not** suppress prompts when effective ask mode is `always`.
|
||||
|
||||
</ParamField>
|
||||
|
||||
|
||||
@@ -52,7 +52,11 @@ force `security=full` only when the operator explicitly grants elevated access.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="ask" type="'off' | 'on-miss' | 'always'">
|
||||
Approval prompt behavior for `gateway` / `node` execution.
|
||||
The baseline ask mode comes from `tools.exec.ask` and host approvals.
|
||||
For channel-origin model calls, per-call `ask` is ignored when the
|
||||
effective host ask is `off`; otherwise it can only harden to a stricter
|
||||
mode. Trusted internal/API callers that construct exec tools with an
|
||||
explicit `ask` value are unchanged.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="node" type="string">
|
||||
|
||||
@@ -141,7 +141,7 @@ session to confirm the effective tool list.
|
||||
|
||||
- **Model:** native sub-agents inherit the caller unless you set `agents.defaults.subagents.model` (or per-agent `agents.list[].subagents.model`). ACP runtime spawns use the same configured subagent model when present; otherwise the ACP harness keeps its own default. An explicit `sessions_spawn.model` still wins.
|
||||
- **Thinking:** native sub-agents inherit the caller unless you set `agents.defaults.subagents.thinking` (or per-agent `agents.list[].subagents.thinking`). ACP runtime spawns also apply `agents.defaults.models["provider/model"].params.thinking` for the selected model. An explicit `sessions_spawn.thinking` still wins.
|
||||
- **Run timeout:** if `sessions_spawn.runTimeoutSeconds` is omitted, OpenClaw uses `agents.defaults.subagents.runTimeoutSeconds` when set; otherwise it falls back to `0` (no timeout).
|
||||
- **Run timeout:** OpenClaw uses `agents.defaults.subagents.runTimeoutSeconds` when set; otherwise it falls back to `0` (no timeout). `sessions_spawn` does not accept per-call timeout overrides.
|
||||
- **Task delivery:** native sub-agents receive the delegated task in their first visible `[Subagent Task]` message. The sub-agent system prompt carries runtime rules and routing context, not a hidden duplicate of the task.
|
||||
|
||||
Accepted native sub-agent spawns include the resolved child model metadata in
|
||||
@@ -208,9 +208,6 @@ Per-agent overrides use `agents.list[].subagents.delegationMode`.
|
||||
<ParamField path="thinking" type="string">
|
||||
Override thinking level for the sub-agent run.
|
||||
</ParamField>
|
||||
<ParamField path="runTimeoutSeconds" type="number">
|
||||
Defaults to `agents.defaults.subagents.runTimeoutSeconds` when set, otherwise `0`. When set, the sub-agent run is aborted after N seconds.
|
||||
</ParamField>
|
||||
<ParamField path="thread" type="boolean" default="false">
|
||||
When `true`, requests channel thread binding for this sub-agent session.
|
||||
</ParamField>
|
||||
@@ -375,7 +372,7 @@ remain spawnable while inheriting defaults.
|
||||
- Archive uses `sessions.delete` and renames the transcript to `*.deleted.<timestamp>` (same folder).
|
||||
- `cleanup: "delete"` archives immediately after announce (still keeps the transcript via rename).
|
||||
- Auto-archive is best-effort; pending timers are lost if the gateway restarts.
|
||||
- `runTimeoutSeconds` does **not** auto-archive; it only stops the run. The session remains until auto-archive.
|
||||
- Configured run timeouts do **not** auto-archive; they only stop the run. The session remains until auto-archive.
|
||||
- Auto-archive applies equally to depth-1 and depth-2 sessions.
|
||||
- Browser cleanup is separate from archive cleanup: tracked browser tabs/processes are best-effort closed when the run finishes, even if the transcript/session record is kept.
|
||||
|
||||
@@ -394,7 +391,7 @@ worker sub-sub-agents.
|
||||
maxSpawnDepth: 2, // allow sub-agents to spawn children (default: 1)
|
||||
maxChildrenPerAgent: 5, // max active children per agent session (default: 5)
|
||||
maxConcurrent: 8, // global concurrency lane cap (default: 8)
|
||||
runTimeoutSeconds: 900, // default timeout for sessions_spawn when omitted (0 = no timeout)
|
||||
runTimeoutSeconds: 900, // default timeout for sessions_spawn (0 = no timeout)
|
||||
announceTimeoutMs: 120000, // per-call gateway announce timeout
|
||||
},
|
||||
},
|
||||
|
||||
@@ -180,7 +180,7 @@ Activity entries keep only sanitized summaries and redacted, truncated output pr
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Send and history semantics">
|
||||
- `chat.send` is **non-blocking**: it acks immediately with `{ runId, status: "started" }` and the response streams via `chat` events.
|
||||
- `chat.send` is **non-blocking**: it acks immediately with `{ runId, status: "started" }` and the response streams via `chat` events. Trusted Control UI clients may also receive optional ACK timing metadata for local diagnostics.
|
||||
- Chat uploads accept images plus non-video files. Images keep the native image path; other files are stored as managed media and shown in history as attachment links.
|
||||
- Re-sending with the same `idempotencyKey` returns `{ status: "in_flight" }` while running, and `{ status: "ok" }` after completion.
|
||||
- `chat.history` responses are size-bounded for UI safety. When transcript entries are too large, Gateway may truncate long text fields, omit heavy metadata blocks, and replace oversized messages with a placeholder (`[chat.history omitted: message too large]`).
|
||||
|
||||
4
extensions/acpx/npm-shrinkwrap.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@openclaw/acpx",
|
||||
"version": "2026.6.1",
|
||||
"version": "2026.6.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@openclaw/acpx",
|
||||
"version": "2026.6.1",
|
||||
"version": "2026.6.2",
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/claude-agent-acp": "0.39.0",
|
||||
"@zed-industries/codex-acp": "0.15.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/acpx",
|
||||
"version": "2026.6.1",
|
||||
"version": "2026.6.2",
|
||||
"description": "OpenClaw ACP runtime backend with plugin-owned session and transport management.",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -26,10 +26,10 @@
|
||||
"minHostVersion": ">=2026.4.25"
|
||||
},
|
||||
"compat": {
|
||||
"pluginApi": ">=2026.6.1"
|
||||
"pluginApi": ">=2026.6.2"
|
||||
},
|
||||
"build": {
|
||||
"openclawVersion": "2026.6.1",
|
||||
"openclawVersion": "2026.6.2",
|
||||
"staticAssets": [
|
||||
{
|
||||
"source": "./src/runtime-internals/mcp-proxy.mjs",
|
||||
|
||||
@@ -215,6 +215,7 @@ describe("AcpxRuntime fresh reset wrapper", () => {
|
||||
agent: "codex",
|
||||
mode: "persistent",
|
||||
model: "gpt-5.4",
|
||||
sessionOptions: { model: "gpt-5.4" },
|
||||
});
|
||||
});
|
||||
|
||||
@@ -619,7 +620,7 @@ describe("AcpxRuntime fresh reset wrapper", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("does not normalize model startup for non-Codex ACP agents", async () => {
|
||||
it("passes model startup through sessionOptions for non-Codex ACP agents", async () => {
|
||||
const baseStore: TestSessionStore = {
|
||||
load: vi.fn(async () => undefined),
|
||||
save: vi.fn(async () => {}),
|
||||
@@ -648,6 +649,7 @@ describe("AcpxRuntime fresh reset wrapper", () => {
|
||||
agent: "main",
|
||||
mode: "persistent",
|
||||
model: "openai/gpt-5.5",
|
||||
sessionOptions: { model: "openai/gpt-5.5" },
|
||||
});
|
||||
});
|
||||
|
||||
@@ -694,6 +696,7 @@ describe("AcpxRuntime fresh reset wrapper", () => {
|
||||
agent: "codex",
|
||||
mode: "persistent",
|
||||
model: "gpt-5.5",
|
||||
sessionOptions: { model: "gpt-5.5" },
|
||||
});
|
||||
});
|
||||
|
||||
@@ -728,6 +731,7 @@ describe("AcpxRuntime fresh reset wrapper", () => {
|
||||
mode: "persistent",
|
||||
model: "gpt-5.4/xhigh",
|
||||
thinking: "x-high",
|
||||
sessionOptions: { model: "gpt-5.4/xhigh" },
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
type AcpRuntimeStatus,
|
||||
type AcpRuntimeTurn,
|
||||
type AcpRuntimeTurnResult,
|
||||
type SessionAgentOptions,
|
||||
} from "acpx/runtime";
|
||||
import { parseStrictPositiveInteger } from "openclaw/plugin-sdk/number-runtime";
|
||||
import { redactSensitiveText } from "openclaw/plugin-sdk/security-runtime";
|
||||
@@ -49,6 +50,8 @@ type AcpxRuntimeTestOptions = Record<string, unknown> & {
|
||||
openclawProcessCleanup?: AcpxProcessCleanupDeps;
|
||||
};
|
||||
type OpenClawRuntimeTurnInput = Parameters<NonNullable<AcpRuntime["startTurn"]>>[0];
|
||||
type OpenClawRuntimeEnsureInput = Parameters<AcpRuntime["ensureSession"]>[0];
|
||||
type AcpxDelegateEnsureInput = Parameters<BaseAcpxRuntime["ensureSession"]>[0];
|
||||
|
||||
type ResetAwareSessionStore = AcpSessionStore & {
|
||||
markFresh: (sessionKey: string) => void;
|
||||
@@ -547,6 +550,16 @@ function codexAcpSessionModelId(override: CodexAcpModelOverride): string {
|
||||
: override.model;
|
||||
}
|
||||
|
||||
function withAcpxSessionOptions(input: OpenClawRuntimeEnsureInput): AcpxDelegateEnsureInput {
|
||||
const existingOptions = (input as { sessionOptions?: SessionAgentOptions }).sessionOptions;
|
||||
const model = input.model?.trim() || existingOptions?.model;
|
||||
const sessionOptions = model ? { ...existingOptions, model } : existingOptions;
|
||||
return {
|
||||
...input,
|
||||
...(sessionOptions ? { sessionOptions } : {}),
|
||||
} as AcpxDelegateEnsureInput;
|
||||
}
|
||||
|
||||
function quoteShellArg(value: string): string {
|
||||
if (/^[A-Za-z0-9_./:=@+-]+$/.test(value)) {
|
||||
return value;
|
||||
@@ -942,7 +955,7 @@ export class AcpxRuntime implements AcpRuntime {
|
||||
this.withCodexWrapperDiagnostics({
|
||||
command: stableLaunchCommand,
|
||||
fallbackCode: "ACP_SESSION_INIT_FAILED",
|
||||
run: () => delegate.ensureSession(input),
|
||||
run: () => delegate.ensureSession(withAcpxSessionOptions(input)),
|
||||
}),
|
||||
});
|
||||
}
|
||||
@@ -962,7 +975,7 @@ export class AcpxRuntime implements AcpRuntime {
|
||||
this.withCodexWrapperDiagnostics({
|
||||
command: stableLaunchCommand,
|
||||
fallbackCode: "ACP_SESSION_INIT_FAILED",
|
||||
run: () => delegate.ensureSession(normalizedInput),
|
||||
run: () => delegate.ensureSession(withAcpxSessionOptions(normalizedInput)),
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
@@ -3218,7 +3218,7 @@ describe("active-memory plugin", () => {
|
||||
testing.setSetupGraceTimeoutMsForTests(0);
|
||||
api.pluginConfig = {
|
||||
agents: ["main"],
|
||||
timeoutMs: 100,
|
||||
timeoutMs: 1_000,
|
||||
};
|
||||
plugin.register(api as unknown as OpenClawPluginApi);
|
||||
hoisted.sessionStore["agent:main:memory-get-miss"] = {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/admin-http-rpc",
|
||||
"version": "2026.6.1",
|
||||
"version": "2026.6.2",
|
||||
"private": true,
|
||||
"description": "OpenClaw admin HTTP RPC endpoint",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/alibaba-provider",
|
||||
"version": "2026.6.1",
|
||||
"version": "2026.6.2",
|
||||
"private": true,
|
||||
"description": "OpenClaw Alibaba Model Studio video provider plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@openclaw/amazon-bedrock-mantle-provider",
|
||||
"version": "2026.6.1",
|
||||
"version": "2026.6.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@openclaw/amazon-bedrock-mantle-provider",
|
||||
"version": "2026.6.1",
|
||||
"version": "2026.6.2",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "0.100.1",
|
||||
"@aws/bedrock-token-generator": "1.1.0"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/amazon-bedrock-mantle-provider",
|
||||
"version": "2026.6.1",
|
||||
"version": "2026.6.2",
|
||||
"description": "OpenClaw Amazon Bedrock Mantle provider plugin for OpenAI-compatible model routing.",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -24,10 +24,10 @@
|
||||
"minHostVersion": ">=2026.5.12-beta.1"
|
||||
},
|
||||
"compat": {
|
||||
"pluginApi": ">=2026.6.1"
|
||||
"pluginApi": ">=2026.6.2"
|
||||
},
|
||||
"build": {
|
||||
"openclawVersion": "2026.6.1",
|
||||
"openclawVersion": "2026.6.2",
|
||||
"bundledDist": false
|
||||
},
|
||||
"release": {
|
||||
|
||||
4
extensions/amazon-bedrock/npm-shrinkwrap.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@openclaw/amazon-bedrock-provider",
|
||||
"version": "2026.6.1",
|
||||
"version": "2026.6.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@openclaw/amazon-bedrock-provider",
|
||||
"version": "2026.6.1",
|
||||
"version": "2026.6.2",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-bedrock": "3.1056.0",
|
||||
"@aws-sdk/client-bedrock-runtime": "3.1056.0",
|
||||
|
||||