mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-16 11:08:43 +08:00
Compare commits
344 Commits
release/20
...
codex/refa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
97059b9697 | ||
|
|
f395fca214 | ||
|
|
68a5d4b5f5 | ||
|
|
2abcddaa2f | ||
|
|
faffa4b8f7 | ||
|
|
45ccb20d98 | ||
|
|
dbd74318f7 | ||
|
|
4f21111df9 | ||
|
|
d9efe22cd3 | ||
|
|
107462abae | ||
|
|
04c30720a0 | ||
|
|
58601a7f0e | ||
|
|
9c3d186d7c | ||
|
|
990edcfbf5 | ||
|
|
1a4e815e37 | ||
|
|
aefd49909d | ||
|
|
c67dc59b02 | ||
|
|
e1744184b8 | ||
|
|
2e240e772b | ||
|
|
5836982506 | ||
|
|
40093f5a93 | ||
|
|
d0851435e8 | ||
|
|
3826cda4d8 | ||
|
|
771881d189 | ||
|
|
6db496b04b | ||
|
|
4892bbc10f | ||
|
|
7f6df80537 | ||
|
|
1a2e418500 | ||
|
|
a823cc3b1c | ||
|
|
fd855c831f | ||
|
|
ccf5976d06 | ||
|
|
b5999bc6a0 | ||
|
|
fc6d448138 | ||
|
|
2e745ba225 | ||
|
|
ef47dd610c | ||
|
|
fbc3fa3876 | ||
|
|
d28691da97 | ||
|
|
65e6d9c98c | ||
|
|
d498b1cce4 | ||
|
|
210877a73e | ||
|
|
d5b1d4529f | ||
|
|
36f6008842 | ||
|
|
cf88b4c024 | ||
|
|
a15427d605 | ||
|
|
23d74dad12 | ||
|
|
ae8da992ce | ||
|
|
4644e0c102 | ||
|
|
fff193402e | ||
|
|
c78f9376d9 | ||
|
|
d4a74b4993 | ||
|
|
aa04aef629 | ||
|
|
0716ebc1e5 | ||
|
|
cda040b4e5 | ||
|
|
00479b12d1 | ||
|
|
2a6eeceb40 | ||
|
|
30f5e6f639 | ||
|
|
dae9345407 | ||
|
|
a12942518e | ||
|
|
b2c5e790b4 | ||
|
|
37e3e895b0 | ||
|
|
2f34d06b42 | ||
|
|
db5e415888 | ||
|
|
b9e8e6d66e | ||
|
|
a57b4c513f | ||
|
|
333a93ce44 | ||
|
|
46a5a5ee5f | ||
|
|
af091174db | ||
|
|
1affe4fcdf | ||
|
|
439a9e97fd | ||
|
|
1c86769cb8 | ||
|
|
b1caba5906 | ||
|
|
d2c0d3ac9b | ||
|
|
e99ab385cf | ||
|
|
19130e0dc2 | ||
|
|
8d08b90489 | ||
|
|
a3e9dfee0e | ||
|
|
cf4000b47b | ||
|
|
85ebbec117 | ||
|
|
f85ef0d114 | ||
|
|
d1299658ac | ||
|
|
a02813164d | ||
|
|
462c076a24 | ||
|
|
d68de3f77d | ||
|
|
2e20dd8dbf | ||
|
|
66880a5d73 | ||
|
|
231b5a14d5 | ||
|
|
c5db07eddc | ||
|
|
808f677ab4 | ||
|
|
7259cb5c77 | ||
|
|
889bc52ba5 | ||
|
|
c8cf57a1a5 | ||
|
|
9a0bae06b7 | ||
|
|
7f49f875de | ||
|
|
bd10e1998b | ||
|
|
650c5cac33 | ||
|
|
965fa05df3 | ||
|
|
9f32bea397 | ||
|
|
7fb0d45b48 | ||
|
|
8ae1adfdcc | ||
|
|
4001be54e4 | ||
|
|
e8b35a8280 | ||
|
|
077d3f1366 | ||
|
|
8afbc98018 | ||
|
|
f58f8c86b9 | ||
|
|
24e729fc4e | ||
|
|
aef670cf0c | ||
|
|
399f5bc993 | ||
|
|
34678d8dfa | ||
|
|
c60b424124 | ||
|
|
340c2456bb | ||
|
|
971542b7f6 | ||
|
|
b3dc274034 | ||
|
|
1acca038b1 | ||
|
|
fd4f5b3f59 | ||
|
|
ac3a98e55d | ||
|
|
57e8c50d19 | ||
|
|
274b7b1d9f | ||
|
|
efca4b7e64 | ||
|
|
65b460f234 | ||
|
|
4c3c0ff5f9 | ||
|
|
9a27af9507 | ||
|
|
1d9b9ef48f | ||
|
|
8f62ec6177 | ||
|
|
b72634f56d | ||
|
|
99e7dad0e4 | ||
|
|
d626e99c31 | ||
|
|
c2754150c9 | ||
|
|
5b21384ab6 | ||
|
|
edd76238fe | ||
|
|
d6b3950734 | ||
|
|
61145dc252 | ||
|
|
382db15e33 | ||
|
|
1a8747620e | ||
|
|
e55cebf4c2 | ||
|
|
2d4a9eb405 | ||
|
|
47759c3506 | ||
|
|
3429e33feb | ||
|
|
894f521aa5 | ||
|
|
97c5e6c235 | ||
|
|
9974641d1e | ||
|
|
924f4c1964 | ||
|
|
2f57352eaa | ||
|
|
c11fcbcb6a | ||
|
|
5b6810211c | ||
|
|
e4313bac97 | ||
|
|
6ebb303ef0 | ||
|
|
ae68006a8f | ||
|
|
735f59af73 | ||
|
|
47112fc423 | ||
|
|
8549a203d4 | ||
|
|
d912909230 | ||
|
|
e6ffcf7362 | ||
|
|
8047350445 | ||
|
|
15e4fbf593 | ||
|
|
4e4ea1c16b | ||
|
|
b2da129e51 | ||
|
|
5b21a0337b | ||
|
|
dbf24fe35a | ||
|
|
d03932af18 | ||
|
|
13a079b3f8 | ||
|
|
e58310b000 | ||
|
|
a1814586c6 | ||
|
|
ca2410ab07 | ||
|
|
d20fdf3b38 | ||
|
|
689ebc815b | ||
|
|
22069bcc56 | ||
|
|
b01a54de6f | ||
|
|
45e36a241a | ||
|
|
5cb6f8aa9f | ||
|
|
b9ad8649d0 | ||
|
|
4e8a527542 | ||
|
|
0eb92fa79c | ||
|
|
f1e303404c | ||
|
|
80d2b40fac | ||
|
|
a3bc0097c8 | ||
|
|
93318050e1 | ||
|
|
18fbcef496 | ||
|
|
e8b142feb1 | ||
|
|
547cc0f109 | ||
|
|
bb71f46251 | ||
|
|
a6aa84f2d0 | ||
|
|
3b94949437 | ||
|
|
45056a463a | ||
|
|
c773d8cd8e | ||
|
|
eb1b640854 | ||
|
|
ddacb7ba39 | ||
|
|
762d8d8e64 | ||
|
|
205ab8d4bd | ||
|
|
7994880864 | ||
|
|
afe75b3387 | ||
|
|
84cbaf1832 | ||
|
|
5892dc8522 | ||
|
|
a55accb4b6 | ||
|
|
cdd71103c9 | ||
|
|
7328caba82 | ||
|
|
3ec16bbad3 | ||
|
|
cc831f8684 | ||
|
|
89cc175b2e | ||
|
|
3c02c239b4 | ||
|
|
7359206b76 | ||
|
|
37d6fd2e81 | ||
|
|
8ecf55b36a | ||
|
|
2e8a2d617d | ||
|
|
27e24ca683 | ||
|
|
68e234f9e2 | ||
|
|
5854e0c8f6 | ||
|
|
eaeedbf1f9 | ||
|
|
dc493bc9a2 | ||
|
|
78c66742ab | ||
|
|
4c23d1d597 | ||
|
|
8eb1fa09c6 | ||
|
|
2d2c1e63f0 | ||
|
|
6cdbccaa9e | ||
|
|
9f522ee7df | ||
|
|
7404b2b5b4 | ||
|
|
73aabcceda | ||
|
|
b1fc8673df | ||
|
|
4cf4e54179 | ||
|
|
84519f7e3c | ||
|
|
6314c377bb | ||
|
|
d3e7e03669 | ||
|
|
64f9f3c278 | ||
|
|
cd3eb438f0 | ||
|
|
26281a8a11 | ||
|
|
4208c89ec4 | ||
|
|
c9c19a1106 | ||
|
|
f78d7b52d8 | ||
|
|
ff6940036b | ||
|
|
b477bfe84b | ||
|
|
d4237cb14d | ||
|
|
20bc546d94 | ||
|
|
069cb8d636 | ||
|
|
8cc5d2d85c | ||
|
|
690f27749c | ||
|
|
7af8153388 | ||
|
|
a66a065ffb | ||
|
|
64d0fc8336 | ||
|
|
f3df863aff | ||
|
|
26b9736922 | ||
|
|
44f45d8729 | ||
|
|
1c655008cd | ||
|
|
618d78144e | ||
|
|
56f2102c28 | ||
|
|
99db98a7ce | ||
|
|
0dbfa1f6be | ||
|
|
f06f2f17c2 | ||
|
|
7bd533a80e | ||
|
|
561b293c7a | ||
|
|
21aa8faf8a | ||
|
|
b0bd9c8ed8 | ||
|
|
c5d599c8c4 | ||
|
|
3a1a5c0dac | ||
|
|
0849cac106 | ||
|
|
32ce06daf8 | ||
|
|
751d3db1cc | ||
|
|
4640baa299 | ||
|
|
c9f0bfd476 | ||
|
|
ab559a7257 | ||
|
|
7c08804541 | ||
|
|
991471b8ec | ||
|
|
7190fc4de8 | ||
|
|
9d9389bc6b | ||
|
|
6c88811b4b | ||
|
|
4a6666796f | ||
|
|
68222ba5e3 | ||
|
|
1bd783045b | ||
|
|
6cf06e8e7e | ||
|
|
ded3a93058 | ||
|
|
0063f3076c | ||
|
|
8c7e5c6918 | ||
|
|
e338037034 | ||
|
|
05796759ad | ||
|
|
d8b3e523ff | ||
|
|
4809ac70fa | ||
|
|
777edadb36 | ||
|
|
8d9ce35b92 | ||
|
|
69bf333dde | ||
|
|
e3a6da0f51 | ||
|
|
8ec1c0676b | ||
|
|
e4b6b9ea66 | ||
|
|
aba3751ad7 | ||
|
|
9921825e17 | ||
|
|
652e616a29 | ||
|
|
f385491c23 | ||
|
|
7387083a95 | ||
|
|
462092936a | ||
|
|
da4671ebcc | ||
|
|
9386d6214f | ||
|
|
8673c65c6b | ||
|
|
f3eb8e9714 | ||
|
|
f80f472190 | ||
|
|
3643de4ba7 | ||
|
|
41a9277844 | ||
|
|
79901fb4ba | ||
|
|
e728957989 | ||
|
|
d9124c9700 | ||
|
|
81c553e2fb | ||
|
|
0fc5a57a34 | ||
|
|
1bd04ac983 | ||
|
|
294779e5d6 | ||
|
|
888835cfe6 | ||
|
|
a716950a3c | ||
|
|
667bc2c4ca | ||
|
|
01b004c594 | ||
|
|
9cf1ef1d90 | ||
|
|
1c5099803f | ||
|
|
0d4968d466 | ||
|
|
0efe5857bc | ||
|
|
fed2c36611 | ||
|
|
bcc1105b30 | ||
|
|
b750d314b7 | ||
|
|
d4819948f3 | ||
|
|
4a3d06ee37 | ||
|
|
ff04e24ead | ||
|
|
a956ab8481 | ||
|
|
3b78d41a9e | ||
|
|
3c9c4aa428 | ||
|
|
6223a538bc | ||
|
|
8be3beec74 | ||
|
|
6a2ec62865 | ||
|
|
301213a05f | ||
|
|
9827490f5f | ||
|
|
575cae59d4 | ||
|
|
4e4dc10db0 | ||
|
|
34a1102506 | ||
|
|
1156ab637c | ||
|
|
0fc2faa0f4 | ||
|
|
fdf3667e09 | ||
|
|
de9260f813 | ||
|
|
4d45884419 | ||
|
|
1bea7d8ef3 | ||
|
|
008d785a80 | ||
|
|
eebcb100b8 | ||
|
|
b5295a6a34 | ||
|
|
3d6252a517 | ||
|
|
c4d3f0545c | ||
|
|
6b0525f237 | ||
|
|
287b10a895 | ||
|
|
ea813a2476 | ||
|
|
9be1699074 | ||
|
|
777f7409d8 | ||
|
|
2bec2caf0c | ||
|
|
047785eb30 | ||
|
|
f7ee25291a |
@@ -54,6 +54,13 @@ pnpm crabbox:run -- --help | sed -n '1,120p'
|
||||
- For broad OpenClaw maintainer `pnpm` gates, prefer the repo wrapper with
|
||||
`--provider blacksmith-testbox` or the repo Testbox helpers when the standing
|
||||
Testbox policy applies.
|
||||
- Cold Testbox acquisition and hydration often take tens of seconds. When broad
|
||||
remote proof is likely, immediately start
|
||||
`node scripts/crabbox-wrapper.mjs warmup --provider blacksmith-testbox --keep --timing-json`
|
||||
in a background command session while inspecting, editing, and running
|
||||
focused local tests. Poll later, reuse the returned `tbx_...` with
|
||||
`--provider blacksmith-testbox --id <tbx_id>`, and stop it before handoff.
|
||||
Do not warm speculatively when remote proof is unlikely.
|
||||
- Always report the actual provider and id. `cbx_...` means AWS Crabbox;
|
||||
`tbx_...` means Blacksmith Testbox through Crabbox. If the output only says
|
||||
`blacksmith testbox list`, use `blacksmith testbox list --all` before
|
||||
|
||||
@@ -150,9 +150,21 @@ Use this skill for release and publish-time workflow. Load `$release-private` if
|
||||
- Stable Windows Hub release closeout requires the signed
|
||||
`OpenClawCompanion-Setup-x64.exe`, `OpenClawCompanion-Setup-arm64.exe`, and
|
||||
`OpenClawCompanion-SHA256SUMS.txt` assets on the canonical
|
||||
`openclaw/openclaw` GitHub Release. Use the public `Windows Node Release`
|
||||
workflow after the matching `openclaw/openclaw-windows-node` release exists;
|
||||
it verifies Authenticode signatures on Windows before uploading assets.
|
||||
`openclaw/openclaw` GitHub Release. Pass the exact signed
|
||||
`openclaw/openclaw-windows-node` release tag as `windows_node_tag` to
|
||||
`OpenClaw Release Publish`, together with the candidate-approved
|
||||
`windows_node_installer_digests` map; it prevalidates the published source
|
||||
release and required installers against that map before any publish child,
|
||||
dispatches the public `Windows Node Release` workflow while the OpenClaw
|
||||
release is still a draft, carries those pinned source asset digests
|
||||
unchanged, verifies the expected OpenClaw Foundation Authenticode signer on
|
||||
Windows, re-downloads and checksum-verifies the promoted asset contract, and
|
||||
blocks publication until the canonical asset contract is present. Use direct
|
||||
`Windows Node Release` dispatch only for recovery, always with an exact tag,
|
||||
never `latest`, and the explicit `expected_installer_digests` JSON map from
|
||||
the approved source release. Recovery rejects unexpected
|
||||
`OpenClawCompanion-*` target asset names, then replaces the expected contract
|
||||
assets with the pinned source bytes.
|
||||
- Website Windows Hub download links should target exact canonical
|
||||
`openclaw/openclaw/releases/download/vYYYY.M.PATCH/...` assets for the current
|
||||
stable release, or `releases/latest/download/...` only after verifying the
|
||||
@@ -675,19 +687,23 @@ node --import tsx scripts/openclaw-npm-postpublish-verify.ts <published-version>
|
||||
where npm did not publish the beta version, delete/recreate the same beta
|
||||
tag and any accidental draft/incomplete prerelease at the fixed commit
|
||||
instead of skipping a prerelease number.
|
||||
22. Start `.github/workflows/openclaw-npm-release.yml` from the same branch with
|
||||
22. Start `.github/workflows/openclaw-release-publish.yml` from the same branch with
|
||||
the same tag for the real publish, choose `npm_dist_tag` (`beta` default,
|
||||
`latest` only when you intentionally want direct stable publish), keep it
|
||||
the same as the preflight run, and pass the successful npm
|
||||
`preflight_run_id`.
|
||||
`preflight_run_id` plus the successful `full_release_validation_run_id`.
|
||||
For stable publish, also pass the exact non-prerelease
|
||||
`openclaw/openclaw-windows-node` tag as `windows_node_tag` and its
|
||||
candidate-approved installer digest map as `windows_node_installer_digests`.
|
||||
23. Wait for `npm-release` approval from `@openclaw/openclaw-release-managers`.
|
||||
24. Wait for the real publish workflow to run postpublish verification,
|
||||
create or update the GitHub release as a draft, upload dependency evidence,
|
||||
promote and verify the required Windows Hub assets for stable releases,
|
||||
append release verification proof, and only then undraft/publish it. If a
|
||||
waited plugin publish fails after OpenClaw npm succeeds, the workflow keeps
|
||||
the release draft with OpenClaw npm evidence and exits red; do not undraft
|
||||
until the plugin publish gap is repaired. The standalone verifier command
|
||||
remains the recovery probe:
|
||||
waited plugin publish or Windows Hub promotion fails after OpenClaw npm
|
||||
succeeds, the workflow keeps the release draft with OpenClaw npm evidence
|
||||
and exits red; do not undraft until the gap is repaired. The standalone
|
||||
verifier command remains the recovery probe:
|
||||
`node --import tsx scripts/openclaw-npm-postpublish-verify.ts <published-version>`.
|
||||
25. Run the post-published beta verification roster. First scan current `main`
|
||||
for critical fixes that landed after the release branch cut; backport only
|
||||
|
||||
11
.github/workflows/ci.yml
vendored
11
.github/workflows/ci.yml
vendored
@@ -1358,6 +1358,8 @@ jobs:
|
||||
- check_name: check-additional-boundaries-bcd
|
||||
group: boundaries
|
||||
boundary_shard: 2/4,3/4,4/4
|
||||
- check_name: check-session-accessor-boundary
|
||||
group: session-accessor-boundary
|
||||
- check_name: check-additional-extension-channels
|
||||
group: extension-channels
|
||||
- check_name: check-additional-extension-bundled
|
||||
@@ -1504,6 +1506,15 @@ jobs:
|
||||
boundaries)
|
||||
node scripts/run-additional-boundary-checks.mjs
|
||||
;;
|
||||
session-accessor-boundary)
|
||||
if [ ! -f scripts/check-session-accessor-boundary.mjs ]; then
|
||||
echo "[skip] session accessor boundary check is not present in this checkout"
|
||||
elif ! node -e 'const pkg = require("./package.json"); process.exit(pkg.scripts?.["lint:tmp:session-accessor-boundary"] ? 0 : 1);'; then
|
||||
echo "[skip] session accessor boundary script is not present in package.json"
|
||||
else
|
||||
run_check "lint:tmp:session-accessor-boundary" pnpm run lint:tmp:session-accessor-boundary
|
||||
fi
|
||||
;;
|
||||
extension-channels)
|
||||
run_check "lint:extensions:channels" pnpm run lint:extensions:channels
|
||||
;;
|
||||
|
||||
@@ -783,7 +783,7 @@ jobs:
|
||||
fi
|
||||
|
||||
args=(
|
||||
-f ref="$TARGET_SHA"
|
||||
-f ref="$TARGET_REF"
|
||||
-f expected_sha="$TARGET_SHA"
|
||||
-f provider="$PROVIDER"
|
||||
-f mode="$MODE"
|
||||
|
||||
@@ -437,8 +437,17 @@ jobs:
|
||||
echo "::warning::Could not generate motion-trimmed desktop previews; continuing with screenshots and full MP4 links."
|
||||
fi
|
||||
|
||||
baseline_status="$(jq -r '.scenarios[0].status' "$root/baseline/discord-qa-summary.json")"
|
||||
candidate_status="$(jq -r '.scenarios[0].status' "$root/candidate/discord-qa-summary.json")"
|
||||
read_discord_status_reaction_status() {
|
||||
local lane="$1"
|
||||
if [[ -f "$root/$lane/qa-evidence.json" ]]; then
|
||||
jq -r '.entries[0].result.status' "$root/$lane/qa-evidence.json"
|
||||
return
|
||||
fi
|
||||
jq -r '.scenarios[0].status' "$root/$lane/discord-qa-summary.json"
|
||||
}
|
||||
|
||||
baseline_status="$(read_discord_status_reaction_status baseline)"
|
||||
candidate_status="$(read_discord_status_reaction_status candidate)"
|
||||
|
||||
jq -n \
|
||||
--arg baseline_status "$baseline_status" \
|
||||
|
||||
@@ -451,8 +451,17 @@ jobs:
|
||||
|
||||
capture_candidate_discord_web
|
||||
|
||||
baseline_status="$(jq -r '.scenarios[] | select(.id == "discord-thread-reply-filepath-attachment") | .status' "$root/baseline/discord-qa-summary.json")"
|
||||
candidate_status="$(jq -r '.scenarios[] | select(.id == "discord-thread-reply-filepath-attachment") | .status' "$root/candidate/discord-qa-summary.json")"
|
||||
read_discord_thread_attachment_status() {
|
||||
local lane="$1"
|
||||
if [[ -f "$root/$lane/qa-evidence.json" ]]; then
|
||||
jq -r '.entries[] | select(.test.id == "discord-thread-reply-filepath-attachment") | .result.status' "$root/$lane/qa-evidence.json"
|
||||
return
|
||||
fi
|
||||
jq -r '.scenarios[] | select(.id == "discord-thread-reply-filepath-attachment") | .status' "$root/$lane/discord-qa-summary.json"
|
||||
}
|
||||
|
||||
baseline_status="$(read_discord_thread_attachment_status baseline)"
|
||||
candidate_status="$(read_discord_thread_attachment_status candidate)"
|
||||
comparison_status="fail"
|
||||
if [[ "$baseline_status" == "fail" && "$candidate_status" == "pass" ]]; then
|
||||
comparison_status="pass"
|
||||
|
||||
5
.github/workflows/mantis-telegram-live.yml
vendored
5
.github/workflows/mantis-telegram-live.yml
vendored
@@ -379,7 +379,6 @@ jobs:
|
||||
OPENCLAW_QA_CONVEX_SECRET_CI: ${{ secrets.OPENCLAW_QA_CONVEX_SECRET_CI }}
|
||||
OPENCLAW_QA_CREDENTIAL_ACQUIRE_TIMEOUT_MS: "1800000"
|
||||
OPENCLAW_QA_REDACT_PUBLIC_METADATA: "1"
|
||||
OPENCLAW_QA_TELEGRAM_CAPTURE_CONTENT: "1"
|
||||
CRABBOX_COORDINATOR: ${{ secrets.CRABBOX_COORDINATOR }}
|
||||
CRABBOX_COORDINATOR_TOKEN: ${{ secrets.CRABBOX_COORDINATOR_TOKEN }}
|
||||
OPENCLAW_QA_MANTIS_CRABBOX_COORDINATOR: ${{ secrets.OPENCLAW_QA_MANTIS_CRABBOX_COORDINATOR }}
|
||||
@@ -445,8 +444,8 @@ jobs:
|
||||
telegram_exit=$?
|
||||
set -e
|
||||
|
||||
if [[ ! -f "$root/telegram-qa-summary.json" ]]; then
|
||||
echo "Telegram live QA did not produce a summary." >&2
|
||||
if [[ ! -f "$root/qa-evidence.json" && ! -f "$root/telegram-qa-summary.json" ]]; then
|
||||
echo "Telegram live QA did not produce an evidence summary." >&2
|
||||
exit "$telegram_exit"
|
||||
fi
|
||||
echo "telegram_exit=${telegram_exit}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
1
.github/workflows/npm-telegram-beta-e2e.yml
vendored
1
.github/workflows/npm-telegram-beta-e2e.yml
vendored
@@ -220,7 +220,6 @@ jobs:
|
||||
OPENCLAW_QA_CONVEX_SECRET_CI: ${{ secrets.OPENCLAW_QA_CONVEX_SECRET_CI }}
|
||||
OPENCLAW_QA_CREDENTIAL_ACQUIRE_TIMEOUT_MS: "1800000"
|
||||
OPENCLAW_QA_REDACT_PUBLIC_METADATA: "1"
|
||||
OPENCLAW_QA_TELEGRAM_CAPTURE_CONTENT: "1"
|
||||
INPUT_SCENARIO: ${{ inputs.scenario }}
|
||||
PACKAGE_ARTIFACT_NAME: ${{ inputs.package_artifact_name || '' }}
|
||||
run: |
|
||||
|
||||
@@ -420,6 +420,7 @@ jobs:
|
||||
add_suite live-cache
|
||||
|
||||
add_profile_suite native-live-src-agents "stable full"
|
||||
add_profile_suite native-live-src-agents-zai-coding "stable full"
|
||||
add_profile_suite native-live-src-gateway-core "beta minimum stable full"
|
||||
add_profile_suite native-live-src-gateway-profiles-anthropic "stable full"
|
||||
add_profile_suite native-live-src-gateway-profiles-anthropic-smoke "stable"
|
||||
@@ -1748,6 +1749,7 @@ jobs:
|
||||
anthropic) require_any Anthropic ANTHROPIC_API_KEY ANTHROPIC_API_KEY_OLD ANTHROPIC_API_TOKEN ;;
|
||||
google) require_any Google GEMINI_API_KEY GOOGLE_API_KEY ;;
|
||||
minimax) require_any MiniMax MINIMAX_API_KEY ;;
|
||||
moonshot) require_any Moonshot MOONSHOT_API_KEY KIMI_API_KEY ;;
|
||||
openai) require_any OpenAI OPENAI_API_KEY ;;
|
||||
opencode-go) require_any OpenCode OPENCODE_API_KEY OPENCODE_ZEN_API_KEY ;;
|
||||
openrouter) require_any OpenRouter OPENROUTER_API_KEY ;;
|
||||
@@ -1836,7 +1838,7 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
all_providers=(anthropic google minimax openai opencode-go openrouter xai zai fireworks)
|
||||
all_providers=(anthropic google minimax moonshot openai opencode-go openrouter xai zai fireworks)
|
||||
|
||||
normalize_provider() {
|
||||
local value="${1,,}"
|
||||
@@ -1922,6 +1924,7 @@ jobs:
|
||||
anthropic) require_any Anthropic ANTHROPIC_API_KEY ANTHROPIC_API_KEY_OLD ANTHROPIC_API_TOKEN ;;
|
||||
google) require_any Google GEMINI_API_KEY GOOGLE_API_KEY ;;
|
||||
minimax) require_any MiniMax MINIMAX_API_KEY ;;
|
||||
moonshot) require_any Moonshot MOONSHOT_API_KEY KIMI_API_KEY ;;
|
||||
openai) require_any OpenAI OPENAI_API_KEY ;;
|
||||
opencode-go) require_any OpenCode OPENCODE_API_KEY OPENCODE_ZEN_API_KEY ;;
|
||||
openrouter) require_any OpenRouter OPENROUTER_API_KEY ;;
|
||||
@@ -1954,6 +1957,12 @@ jobs:
|
||||
timeout_minutes: 60
|
||||
profile_env_only: false
|
||||
profiles: stable full
|
||||
- suite_id: native-live-src-agents-zai-coding
|
||||
label: Native live Z.AI Coding Plan
|
||||
command: ZAI_CODING_LIVE_TEST=1 node .release-harness/scripts/test-live-shard.mjs native-live-src-agents-zai-coding
|
||||
timeout_minutes: 15
|
||||
profile_env_only: false
|
||||
profiles: stable full
|
||||
- suite_id: native-live-src-gateway-core
|
||||
label: Native live gateway core
|
||||
command: OPENCLAW_LIVE_CODEX_HARNESS=1 OPENCLAW_LIVE_CODEX_HARNESS_AUTH=api-key node .release-harness/scripts/test-live-shard.mjs native-live-src-gateway-core
|
||||
|
||||
9
.github/workflows/openclaw-performance.yml
vendored
9
.github/workflows/openclaw-performance.yml
vendored
@@ -527,6 +527,13 @@ jobs:
|
||||
cleanup_gateway
|
||||
trap - EXIT
|
||||
|
||||
if node -e "const fs=require('node:fs'); const scripts=require('./package.json').scripts||{}; process.exit(scripts['test:sqlite:perf:smoke'] && fs.existsSync('scripts/bench-sqlite-state.ts') ? 0 : 1)"; then
|
||||
pnpm test:sqlite:perf:smoke
|
||||
cp .artifacts/sqlite-perf/smoke.json "$SOURCE_PERF_DIR/sqlite-perf-smoke.json"
|
||||
else
|
||||
echo "SQLite state smoke probe is not available in ${TESTED_REF}; continuing with the remaining source probes." >> "$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
|
||||
summary_args=(node "$PERFORMANCE_HELPER_DIR/scripts/openclaw-performance-source-summary.mjs" \
|
||||
--source-dir "$SOURCE_PERF_DIR" \
|
||||
--output "$SOURCE_PERF_DIR/index.md")
|
||||
@@ -604,7 +611,7 @@ jobs:
|
||||
|
||||
## Source probes
|
||||
|
||||
Additional gateway boot, memory, plugin pressure, mock hello-loop, and CLI startup numbers are in [source/index.md](source/index.md).
|
||||
Additional gateway boot, memory, plugin pressure, mock hello-loop, CLI startup, and SQLite state smoke numbers are in [source/index.md](source/index.md).
|
||||
EOF
|
||||
fi
|
||||
fi
|
||||
|
||||
25
.github/workflows/openclaw-release-checks.yml
vendored
25
.github/workflows/openclaw-release-checks.yml
vendored
@@ -1181,7 +1181,7 @@ jobs:
|
||||
runtime_tool_coverage_release_checks:
|
||||
name: Enforce QA Lab runtime tool coverage
|
||||
needs: [resolve_target, qa_lab_runtime_parity_release_checks]
|
||||
if: always() && contains(fromJSON('["all","qa","qa-parity"]'), needs.resolve_target.outputs.rerun_group)
|
||||
if: contains(fromJSON('["all","qa","qa-parity"]'), needs.resolve_target.outputs.rerun_group)
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 15
|
||||
permissions:
|
||||
@@ -1204,13 +1204,35 @@ jobs:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
install-bun: "true"
|
||||
|
||||
- name: Download runtime parity status
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: release-check-status-qa-runtime-parity-${{ needs.resolve_target.outputs.revision }}
|
||||
path: .artifacts/release-check-status/
|
||||
|
||||
- name: Verify runtime parity producer status
|
||||
id: verify_runtime_parity_status
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
status_path=".artifacts/release-check-status/qa_lab_runtime_parity_release_checks.env"
|
||||
status="$(sed -n 's/^status=//p' "$status_path" | tail -n 1)"
|
||||
if [[ "$status" != "success" ]]; then
|
||||
echo "Runtime parity producer status is ${status:-missing}; skipping coverage artifact consumer."
|
||||
echo "ready=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
echo "ready=true" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Download runtime parity artifacts
|
||||
if: steps.verify_runtime_parity_status.outputs.ready == 'true'
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: release-qa-runtime-parity-${{ needs.resolve_target.outputs.revision }}
|
||||
path: .artifacts/qa-e2e/
|
||||
|
||||
- name: Enforce standard runtime tool coverage
|
||||
if: steps.verify_runtime_parity_status.outputs.ready == 'true'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
pnpm openclaw qa coverage \
|
||||
@@ -1412,7 +1434,6 @@ jobs:
|
||||
OPENCLAW_QA_CONVEX_SECRET_CI: ${{ secrets.OPENCLAW_QA_CONVEX_SECRET_CI }}
|
||||
OPENCLAW_QA_CREDENTIAL_ACQUIRE_TIMEOUT_MS: "1800000"
|
||||
OPENCLAW_QA_REDACT_PUBLIC_METADATA: "1"
|
||||
OPENCLAW_QA_TELEGRAM_CAPTURE_CONTENT: "1"
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
|
||||
416
.github/workflows/openclaw-release-publish.yml
vendored
416
.github/workflows/openclaw-release-publish.yml
vendored
@@ -15,6 +15,14 @@ on:
|
||||
description: Successful Full Release Validation run id for this tag/SHA, required when publish_openclaw_npm=true
|
||||
required: false
|
||||
type: string
|
||||
windows_node_tag:
|
||||
description: Exact openclaw-windows-node release tag, required for stable OpenClaw publish
|
||||
required: false
|
||||
type: string
|
||||
windows_node_installer_digests:
|
||||
description: Candidate-approved compact JSON map of Windows installer names to pinned sha256 digests
|
||||
required: false
|
||||
type: string
|
||||
npm_telegram_run_id:
|
||||
description: Optional successful NPM Telegram Beta E2E run id to include in final release evidence
|
||||
required: false
|
||||
@@ -81,12 +89,15 @@ jobs:
|
||||
outputs:
|
||||
sha: ${{ steps.manifest.outputs.sha || steps.ref.outputs.sha }}
|
||||
preflight_artifact_name: ${{ steps.preflight_artifact.outputs.name }}
|
||||
windows_node_installer_digests: ${{ steps.windows_source.outputs.installer_digests }}
|
||||
steps:
|
||||
- name: Validate inputs
|
||||
env:
|
||||
RELEASE_TAG: ${{ inputs.tag }}
|
||||
PREFLIGHT_RUN_ID: ${{ inputs.preflight_run_id }}
|
||||
FULL_RELEASE_VALIDATION_RUN_ID: ${{ inputs.full_release_validation_run_id }}
|
||||
WINDOWS_NODE_TAG: ${{ inputs.windows_node_tag }}
|
||||
WINDOWS_NODE_INSTALLER_DIGESTS: ${{ inputs.windows_node_installer_digests }}
|
||||
PUBLISH_OPENCLAW_NPM: ${{ inputs.publish_openclaw_npm && 'true' || 'false' }}
|
||||
PLUGIN_PUBLISH_SCOPE: ${{ inputs.plugin_publish_scope }}
|
||||
PLUGINS: ${{ inputs.plugins }}
|
||||
@@ -115,6 +126,22 @@ jobs:
|
||||
echo "publish_openclaw_npm=true requires full_release_validation_run_id." >&2
|
||||
exit 1
|
||||
fi
|
||||
stable_release=true
|
||||
if [[ "${RELEASE_TAG}" == *"-alpha."* || "${RELEASE_TAG}" == *"-beta."* ]]; then
|
||||
stable_release=false
|
||||
fi
|
||||
if [[ -n "${WINDOWS_NODE_TAG}" && ! "${WINDOWS_NODE_TAG}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+([-.][0-9A-Za-z]+([.-][0-9A-Za-z]+)*)?$ ]]; then
|
||||
echo "windows_node_tag must be an explicit openclaw-windows-node release tag, not latest: ${WINDOWS_NODE_TAG}" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ "${PUBLISH_OPENCLAW_NPM}" == "true" && "${stable_release}" == "true" && -z "${WINDOWS_NODE_TAG}" ]]; then
|
||||
echo "Stable OpenClaw publish requires an explicit windows_node_tag." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ "${PUBLISH_OPENCLAW_NPM}" == "true" && "${stable_release}" == "true" && -z "${WINDOWS_NODE_INSTALLER_DIGESTS}" ]]; then
|
||||
echo "Stable OpenClaw publish requires candidate-approved windows_node_installer_digests." >&2
|
||||
exit 1
|
||||
fi
|
||||
tideclaw_alpha_publish=false
|
||||
if [[ "${RELEASE_TAG}" == *"-alpha."* && "${RELEASE_NPM_DIST_TAG}" == "alpha" && "${WORKFLOW_REF}" =~ ^refs/heads/tideclaw/alpha/[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{4}Z$ ]]; then
|
||||
tideclaw_alpha_publish=true
|
||||
@@ -143,6 +170,73 @@ jobs:
|
||||
;;
|
||||
esac
|
||||
|
||||
- name: Validate stable Windows source release
|
||||
id: windows_source
|
||||
if: ${{ inputs.publish_openclaw_npm }}
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
RELEASE_TAG: ${{ inputs.tag }}
|
||||
WINDOWS_NODE_TAG: ${{ inputs.windows_node_tag }}
|
||||
APPROVED_INSTALLER_DIGESTS: ${{ inputs.windows_node_installer_digests }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [[ "${RELEASE_TAG}" == *"-alpha."* || "${RELEASE_TAG}" == *"-beta."* ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
source_json="$(gh release view "${WINDOWS_NODE_TAG}" \
|
||||
--repo openclaw/openclaw-windows-node \
|
||||
--json tagName,isDraft,isPrerelease,assets,url)"
|
||||
if [[ "$(printf '%s' "${source_json}" | jq -r '.tagName')" != "${WINDOWS_NODE_TAG}" ]]; then
|
||||
echo "Windows source release tag does not match ${WINDOWS_NODE_TAG}." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ "$(printf '%s' "${source_json}" | jq -r '.isDraft')" == "true" ]]; then
|
||||
echo "Stable OpenClaw publish requires a published Windows source release." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ "$(printf '%s' "${source_json}" | jq -r '.isPrerelease')" == "true" ]]; then
|
||||
echo "Stable OpenClaw publish requires a non-prerelease Windows source release." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
required_assets=(
|
||||
"OpenClawCompanion-Setup-x64.exe"
|
||||
"OpenClawCompanion-Setup-arm64.exe"
|
||||
)
|
||||
required_assets_json="$(printf '%s\n' "${required_assets[@]}" | jq -R . | jq -sc .)"
|
||||
if ! approved_installer_digests="$(printf '%s' "${APPROVED_INSTALLER_DIGESTS}" | jq -ce --argjson names "${required_assets_json}" '
|
||||
if type == "object" and
|
||||
(keys | sort) == ($names | sort) and
|
||||
all(.[]; type == "string" and test("^sha256:[a-f0-9]{64}$"))
|
||||
then .
|
||||
else error("invalid candidate-approved Windows installer digest map")
|
||||
end
|
||||
')"; then
|
||||
echo "windows_node_installer_digests must contain exactly the candidate-approved current installer asset contract." >&2
|
||||
exit 1
|
||||
fi
|
||||
for asset_name in "${required_assets[@]}"; do
|
||||
asset_matches="$(printf '%s' "${source_json}" | jq -c --arg name "${asset_name}" '[.assets[]? | select(.name == $name)]')"
|
||||
asset_match_count="$(printf '%s' "${asset_matches}" | jq 'length')"
|
||||
if [[ "${asset_match_count}" != "1" ]]; then
|
||||
echo "Windows source release ${WINDOWS_NODE_TAG} must contain exactly one required asset ${asset_name}; found ${asset_match_count}." >&2
|
||||
exit 1
|
||||
fi
|
||||
asset_digest="$(printf '%s' "${asset_matches}" | jq -r '.[0].digest // empty')"
|
||||
if [[ ! "${asset_digest}" =~ ^sha256:[a-f0-9]{64}$ ]]; then
|
||||
echo "Windows source release ${WINDOWS_NODE_TAG} asset ${asset_name} is missing its immutable SHA-256 digest." >&2
|
||||
exit 1
|
||||
fi
|
||||
approved_digest="$(printf '%s' "${approved_installer_digests}" | jq -r --arg name "${asset_name}" '.[$name]')"
|
||||
if [[ "${asset_digest}" != "${approved_digest}" ]]; then
|
||||
echo "Windows source release ${WINDOWS_NODE_TAG} asset ${asset_name} no longer matches its candidate-approved digest." >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
echo "installer_digests=${approved_installer_digests}" >> "$GITHUB_OUTPUT"
|
||||
echo "- Windows Node source release: prevalidated \`${WINDOWS_NODE_TAG}\`" >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Download OpenClaw npm preflight manifest
|
||||
id: preflight_artifact
|
||||
if: ${{ inputs.publish_openclaw_npm }}
|
||||
@@ -337,6 +431,7 @@ jobs:
|
||||
TARGET_SHA: ${{ steps.manifest.outputs.sha || steps.ref.outputs.sha }}
|
||||
RELEASE_PROFILE: ${{ steps.full_manifest.outputs.release_profile || inputs.release_profile }}
|
||||
FULL_RELEASE_VALIDATION_RUN_ID: ${{ inputs.full_release_validation_run_id }}
|
||||
WINDOWS_NODE_TAG: ${{ inputs.windows_node_tag }}
|
||||
run: |
|
||||
{
|
||||
echo "### Release target"
|
||||
@@ -347,13 +442,16 @@ jobs:
|
||||
if [[ -n "${FULL_RELEASE_VALIDATION_RUN_ID// }" ]]; then
|
||||
echo "- Full release validation: \`${FULL_RELEASE_VALIDATION_RUN_ID}\`"
|
||||
fi
|
||||
if [[ -n "${WINDOWS_NODE_TAG// }" ]]; then
|
||||
echo "- Windows Node source release: \`${WINDOWS_NODE_TAG}\`"
|
||||
fi
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
publish:
|
||||
name: Publish plugins, then OpenClaw
|
||||
needs: [resolve_release_target]
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
timeout-minutes: 120
|
||||
environment: npm-release
|
||||
steps:
|
||||
- name: Checkout release SHA
|
||||
@@ -383,11 +481,19 @@ jobs:
|
||||
WAIT_FOR_CLAWHUB: ${{ inputs.wait_for_clawhub && 'true' || 'false' }}
|
||||
PREFLIGHT_ARTIFACT_NAME: ${{ needs.resolve_release_target.outputs.preflight_artifact_name }}
|
||||
NPM_TELEGRAM_RUN_ID: ${{ inputs.npm_telegram_run_id }}
|
||||
WINDOWS_NODE_TAG: ${{ inputs.windows_node_tag }}
|
||||
WINDOWS_NODE_INSTALLER_DIGESTS: ${{ needs.resolve_release_target.outputs.windows_node_installer_digests }}
|
||||
POSTPUBLISH_EVIDENCE_DIR: ${{ runner.temp }}/openclaw-release-postpublish-evidence
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
dispatch_workflow() {
|
||||
is_stable_release() {
|
||||
[[ "${RELEASE_TAG}" != *"-alpha."* && "${RELEASE_TAG}" != *"-beta."* ]]
|
||||
}
|
||||
|
||||
dispatch_workflow_at_ref() {
|
||||
local workflow_ref="$1"
|
||||
shift
|
||||
local workflow="$1"
|
||||
shift
|
||||
|
||||
@@ -397,7 +503,7 @@ jobs:
|
||||
-F per_page=100 \
|
||||
--jq '[.workflow_runs[].id]')"
|
||||
|
||||
dispatch_output="$(gh workflow run --repo "$GITHUB_REPOSITORY" "$workflow" --ref "$CHILD_WORKFLOW_REF" "$@" 2>&1)"
|
||||
dispatch_output="$(gh workflow run --repo "$GITHUB_REPOSITORY" "$workflow" --ref "$workflow_ref" "$@" 2>&1)"
|
||||
printf '%s\n' "$dispatch_output" >&2
|
||||
run_id="$(
|
||||
printf '%s\n' "$dispatch_output" |
|
||||
@@ -432,6 +538,10 @@ jobs:
|
||||
printf '%s\n' "${run_id}"
|
||||
}
|
||||
|
||||
dispatch_workflow() {
|
||||
dispatch_workflow_at_ref "$CHILD_WORKFLOW_REF" "$@"
|
||||
}
|
||||
|
||||
print_pending_deployments() {
|
||||
local workflow="$1"
|
||||
local run_id="$2"
|
||||
@@ -710,6 +820,71 @@ jobs:
|
||||
exit 1
|
||||
}
|
||||
|
||||
resolve_clawhub_release_plan() {
|
||||
local -a plan_args
|
||||
|
||||
clawhub_plan_path="${RUNNER_TEMP}/openclaw-release-clawhub-plan.json"
|
||||
plan_args=(
|
||||
--release-tag "${RELEASE_TAG}"
|
||||
--release-publish-branch "${CHILD_WORKFLOW_REF}"
|
||||
--release-publish-run-id "${GITHUB_RUN_ID}"
|
||||
--plugin-publish-scope "${PLUGIN_PUBLISH_SCOPE}"
|
||||
)
|
||||
if [[ -n "${PLUGINS// }" ]]; then
|
||||
plan_args+=(--plugins "${PLUGINS}")
|
||||
fi
|
||||
|
||||
CLAWHUB_REGISTRY="${CLAWHUB_REGISTRY:-https://clawhub.ai}" \
|
||||
node --import tsx scripts/openclaw-release-clawhub-plan.ts "${plan_args[@]}" > "${clawhub_plan_path}"
|
||||
|
||||
echo "Resolved OpenClaw release ClawHub dispatch plan:"
|
||||
cat "${clawhub_plan_path}"
|
||||
|
||||
clawhub_workflow_ref="$(jq -r '.clawHubWorkflowRef' "${clawhub_plan_path}")"
|
||||
normal_plugins="$(jq -r '.summary.normalPlugins' "${clawhub_plan_path}")"
|
||||
bootstrap_plugins="$(jq -r '.summary.bootstrapPlugins' "${clawhub_plan_path}")"
|
||||
missing_trusted_plugins="$(jq -r '.summary.missingTrustedPlugins' "${clawhub_plan_path}")"
|
||||
normal_plugin_count="$(jq -r '.summary.normalCount' "${clawhub_plan_path}")"
|
||||
bootstrap_plugin_count="$(jq -r '.summary.bootstrapCount' "${clawhub_plan_path}")"
|
||||
missing_trusted_plugin_count="$(jq -r '.summary.missingTrustedPublisherCount' "${clawhub_plan_path}")"
|
||||
|
||||
{
|
||||
echo "### ClawHub release plan"
|
||||
echo
|
||||
echo "- Normal OIDC candidates: \`${normal_plugin_count}\`"
|
||||
echo "- Bootstrap/repair candidates: \`${bootstrap_plugin_count}\`"
|
||||
echo "- Existing-package trusted-publisher repairs: \`${missing_trusted_plugin_count}\`"
|
||||
if [[ -n "${normal_plugins}" ]]; then
|
||||
echo "- Normal plugins: \`${normal_plugins}\`"
|
||||
fi
|
||||
if [[ -n "${bootstrap_plugins}" ]]; then
|
||||
echo "- Bootstrap/repair plugins: \`${bootstrap_plugins}\`"
|
||||
fi
|
||||
if [[ -n "${missing_trusted_plugins}" ]]; then
|
||||
echo "- Trusted-publisher repair plugins: \`${missing_trusted_plugins}\`"
|
||||
fi
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
}
|
||||
|
||||
append_clawhub_dispatch_args() {
|
||||
local target="$1"
|
||||
while IFS=$'\t' read -r key value; do
|
||||
clawhub_dispatch_args+=(-f "${key}=${value}")
|
||||
done < <(jq -r --arg target "${target}" '.[$target].inputs | to_entries[] | [.key, .value] | @tsv' "${clawhub_plan_path}")
|
||||
}
|
||||
|
||||
write_clawhub_runtime_state() {
|
||||
local force_skip_clawhub="$1"
|
||||
local output_path="$2"
|
||||
node --import tsx scripts/openclaw-release-clawhub-runtime-state.ts \
|
||||
--repository "${GITHUB_REPOSITORY}" \
|
||||
--wait-for-clawhub "${WAIT_FOR_CLAWHUB}" \
|
||||
--force-skip-clawhub "${force_skip_clawhub}" \
|
||||
--normal-run-id "${plugin_clawhub_run_id:-}" \
|
||||
--bootstrap-run-id "${plugin_clawhub_bootstrap_run_id:-}" \
|
||||
--bootstrap-completed "${plugin_clawhub_bootstrap_completed:-false}" > "${output_path}"
|
||||
}
|
||||
|
||||
create_or_update_github_release() {
|
||||
local release_version notes_version title notes_file changelog_file latest_arg prerelease_args
|
||||
release_version="${RELEASE_TAG#v}"
|
||||
@@ -765,10 +940,105 @@ jobs:
|
||||
}
|
||||
|
||||
publish_github_release() {
|
||||
if is_stable_release; then
|
||||
verify_windows_release_asset_contract
|
||||
fi
|
||||
gh release edit "${RELEASE_TAG}" --repo "$GITHUB_REPOSITORY" --draft=false
|
||||
echo "- GitHub release: https://github.com/${GITHUB_REPOSITORY}/releases/tag/${RELEASE_TAG}" >> "$GITHUB_STEP_SUMMARY"
|
||||
}
|
||||
|
||||
verify_windows_release_asset_contract() {
|
||||
local actual_companion_assets actual_digest asset_name expected_companion_assets expected_digest expected_hash expected_installer_names manifest_dir manifest_json manifest_path release_json
|
||||
# Add future promoted installer names, such as MSIX x64/ARM64, here.
|
||||
local -a installer_assets=(
|
||||
"OpenClawCompanion-Setup-x64.exe"
|
||||
"OpenClawCompanion-Setup-arm64.exe"
|
||||
)
|
||||
local -a required_assets=(
|
||||
"${installer_assets[@]}"
|
||||
"OpenClawCompanion-SHA256SUMS.txt"
|
||||
)
|
||||
|
||||
release_json="$(gh release view "${RELEASE_TAG}" --repo "$GITHUB_REPOSITORY" --json assets,url)"
|
||||
expected_companion_assets="$(printf '%s\n' "${required_assets[@]}" | jq -R . | jq -sc 'sort')"
|
||||
actual_companion_assets="$(printf '%s' "${release_json}" | jq -c '
|
||||
[.assets[]? | select(.name | startswith("OpenClawCompanion-")) | .name] | sort
|
||||
')"
|
||||
if [[ "${actual_companion_assets}" != "${expected_companion_assets}" ]]; then
|
||||
echo "Stable release OpenClawCompanion asset names do not exactly match the current contract." >&2
|
||||
return 1
|
||||
fi
|
||||
for asset_name in "${required_assets[@]}"; do
|
||||
if ! printf '%s' "${release_json}" | jq -e --arg name "${asset_name}" 'any(.assets[]?; .name == $name)' >/dev/null; then
|
||||
echo "Stable release is missing required Windows asset ${asset_name}." >&2
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
|
||||
manifest_dir="${RUNNER_TEMP}/openclaw-windows-release-contract"
|
||||
manifest_path="${manifest_dir}/OpenClawCompanion-SHA256SUMS.txt"
|
||||
rm -rf "${manifest_dir}"
|
||||
mkdir -p "${manifest_dir}"
|
||||
gh release download "${RELEASE_TAG}" \
|
||||
--repo "$GITHUB_REPOSITORY" \
|
||||
--pattern "OpenClawCompanion-SHA256SUMS.txt" \
|
||||
--dir "${manifest_dir}"
|
||||
if ! manifest_json="$(jq -Rsc '
|
||||
split("\n") as $lines |
|
||||
(if $lines[-1] == "" then $lines[0:-1] else $lines end) |
|
||||
map(sub("\r$"; "")) |
|
||||
if all(.[]; test("^(?<hash>[a-f0-9]{64}) (?<name>[^/\\\\]+)$"))
|
||||
then map(capture("^(?<hash>[a-f0-9]{64}) (?<name>[^/\\\\]+)$"))
|
||||
else error("malformed Windows checksum manifest entry")
|
||||
end
|
||||
' "${manifest_path}")"; then
|
||||
echo "Stable release Windows checksum manifest contains malformed entries." >&2
|
||||
return 1
|
||||
fi
|
||||
expected_installer_names="$(printf '%s\n' "${installer_assets[@]}" | jq -R . | jq -sc 'sort')"
|
||||
if ! printf '%s' "${manifest_json}" | jq -e --argjson expected "${expected_installer_names}" '
|
||||
length == ($expected | length) and
|
||||
([.[].name] | sort) == $expected and
|
||||
([.[].name] | unique | length) == length
|
||||
' >/dev/null; then
|
||||
echo "Stable release Windows checksum manifest does not exactly match the installer asset contract." >&2
|
||||
return 1
|
||||
fi
|
||||
for asset_name in "${installer_assets[@]}"; do
|
||||
expected_digest="$(printf '%s' "${WINDOWS_NODE_INSTALLER_DIGESTS}" | jq -r --arg name "${asset_name}" '.[$name] // empty')"
|
||||
actual_digest="$(printf '%s' "${release_json}" | jq -r --arg name "${asset_name}" '.assets[]? | select(.name == $name) | .digest // empty')"
|
||||
if [[ -z "${expected_digest}" || "${actual_digest}" != "${expected_digest}" ]]; then
|
||||
echo "Stable release Windows asset ${asset_name} does not match its pinned digest." >&2
|
||||
return 1
|
||||
fi
|
||||
expected_hash="${expected_digest#sha256:}"
|
||||
if ! printf '%s' "${manifest_json}" | jq -e --arg name "${asset_name}" --arg hash "${expected_hash}" '
|
||||
any(.[]; .name == $name and .hash == $hash)
|
||||
' >/dev/null; then
|
||||
echo "Stable release Windows checksum manifest does not match pinned digest for ${asset_name}." >&2
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
echo "- Windows Hub asset contract: verified" >> "$GITHUB_STEP_SUMMARY"
|
||||
}
|
||||
|
||||
promote_windows_release_assets() {
|
||||
if ! is_stable_release; then
|
||||
return 0
|
||||
fi
|
||||
if [[ -z "${WINDOWS_NODE_INSTALLER_DIGESTS// }" ]]; then
|
||||
echo "Stable release is missing prevalidated Windows installer digests." >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
windows_node_run_id="$(dispatch_workflow windows-node-release.yml \
|
||||
-f tag="${RELEASE_TAG}" \
|
||||
-f windows_node_tag="${WINDOWS_NODE_TAG}" \
|
||||
-f expected_installer_digests="${WINDOWS_NODE_INSTALLER_DIGESTS}")"
|
||||
echo "- Windows Node release run ID: \`${windows_node_run_id}\`" >> "$GITHUB_STEP_SUMMARY"
|
||||
wait_for_run windows-node-release.yml "${windows_node_run_id}"
|
||||
}
|
||||
|
||||
upload_dependency_evidence_release_asset() {
|
||||
local release_version download_dir asset_path asset_name artifact_name
|
||||
release_version="${RELEASE_TAG#v}"
|
||||
@@ -798,7 +1068,7 @@ jobs:
|
||||
}
|
||||
|
||||
verify_published_release() {
|
||||
local release_version evidence_path skip_clawhub
|
||||
local release_version evidence_path skip_clawhub clawhub_runtime_state_path
|
||||
local -a verify_args
|
||||
|
||||
skip_clawhub="${1:-false}"
|
||||
@@ -815,17 +1085,18 @@ jobs:
|
||||
--dist-tag "${RELEASE_NPM_DIST_TAG}"
|
||||
--repo "${GITHUB_REPOSITORY}"
|
||||
--workflow-ref "${CHILD_WORKFLOW_REF}"
|
||||
--clawhub-workflow-ref "${clawhub_workflow_ref}"
|
||||
--full-release-validation-run "${FULL_RELEASE_VALIDATION_RUN_ID}"
|
||||
--plugin-npm-run "${plugin_npm_run_id}"
|
||||
--openclaw-npm-run "${openclaw_npm_run_id}"
|
||||
--evidence-out "${evidence_path}"
|
||||
--skip-github-release
|
||||
)
|
||||
if [[ "${skip_clawhub}" == "true" || "${WAIT_FOR_CLAWHUB}" != "true" ]]; then
|
||||
verify_args+=(--skip-clawhub)
|
||||
else
|
||||
verify_args+=(--plugin-clawhub-run "${plugin_clawhub_run_id}")
|
||||
fi
|
||||
clawhub_runtime_state_path="${RUNNER_TEMP}/openclaw-release-clawhub-runtime-state-verify.json"
|
||||
write_clawhub_runtime_state "${skip_clawhub}" "${clawhub_runtime_state_path}"
|
||||
while IFS= read -r arg; do
|
||||
verify_args+=("${arg}")
|
||||
done < <(jq -r '.verifierArgs[]' "${clawhub_runtime_state_path}")
|
||||
if [[ -n "${PLUGINS// }" ]]; then
|
||||
verify_args+=(--plugins "${PLUGINS}")
|
||||
fi
|
||||
@@ -841,7 +1112,7 @@ jobs:
|
||||
}
|
||||
|
||||
append_release_proof_to_github_release() {
|
||||
local release_version body_file notes_file tarball integrity telegram_line clawhub_line
|
||||
local release_version body_file notes_file tarball integrity telegram_line clawhub_line clawhub_bootstrap_line clawhub_runtime_state_path windows_line
|
||||
|
||||
release_version="${RELEASE_TAG#v}"
|
||||
body_file="${RUNNER_TEMP}/release-body.md"
|
||||
@@ -855,10 +1126,13 @@ jobs:
|
||||
else
|
||||
telegram_line="- npm Telegram beta E2E: not supplied"
|
||||
fi
|
||||
if [[ "${WAIT_FOR_CLAWHUB}" == "true" ]]; then
|
||||
clawhub_line="- plugin ClawHub publish: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${plugin_clawhub_run_id}"
|
||||
else
|
||||
clawhub_line="- plugin ClawHub publish: dispatched separately, not awaited by this proof: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${plugin_clawhub_run_id}"
|
||||
clawhub_runtime_state_path="${RUNNER_TEMP}/openclaw-release-clawhub-runtime-state-proof.json"
|
||||
write_clawhub_runtime_state false "${clawhub_runtime_state_path}"
|
||||
clawhub_line="$(jq -r '.proofLines.normal' "${clawhub_runtime_state_path}")"
|
||||
clawhub_bootstrap_line="$(jq -r '.proofLines.bootstrap' "${clawhub_runtime_state_path}")"
|
||||
windows_line=""
|
||||
if [[ -n "${windows_node_run_id// }" ]]; then
|
||||
windows_line="- Windows Hub promotion: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${windows_node_run_id} from openclaw/openclaw-windows-node@${WINDOWS_NODE_TAG}"
|
||||
fi
|
||||
|
||||
RELEASE_BODY_FILE="${body_file}" \
|
||||
@@ -875,7 +1149,9 @@ jobs:
|
||||
PLUGIN_NPM_RUN_ID="${plugin_npm_run_id}" \
|
||||
OPENCLAW_NPM_RUN_ID="${openclaw_npm_run_id}" \
|
||||
CLAWHUB_LINE="${clawhub_line}" \
|
||||
CLAWHUB_BOOTSTRAP_LINE="${clawhub_bootstrap_line}" \
|
||||
TELEGRAM_LINE="${telegram_line}" \
|
||||
WINDOWS_LINE="${windows_line}" \
|
||||
node --input-type=module <<'NODE'
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
|
||||
@@ -899,8 +1175,10 @@ jobs:
|
||||
`- full release validation: https://github.com/${process.env.RELEASE_REPO}/actions/runs/${process.env.FULL_RELEASE_VALIDATION_RUN_ID}`,
|
||||
`- plugin npm publish: https://github.com/${process.env.RELEASE_REPO}/actions/runs/${process.env.PLUGIN_NPM_RUN_ID}`,
|
||||
process.env.CLAWHUB_LINE,
|
||||
process.env.CLAWHUB_BOOTSTRAP_LINE,
|
||||
`- OpenClaw npm publish: https://github.com/${process.env.RELEASE_REPO}/actions/runs/${process.env.OPENCLAW_NPM_RUN_ID}`,
|
||||
process.env.TELEGRAM_LINE,
|
||||
...(process.env.WINDOWS_LINE ? [process.env.WINDOWS_LINE] : []),
|
||||
].join("\n");
|
||||
|
||||
const withoutOldProof = body.replace(/\n?### Release verification\n[\s\S]*?(?=\n### |\n## |$)/, "");
|
||||
@@ -915,6 +1193,7 @@ jobs:
|
||||
echo "### Publish sequence"
|
||||
echo
|
||||
echo "- Workflow ref: \`${CHILD_WORKFLOW_REF}\`"
|
||||
echo "- ClawHub workflow ref: release tag \`${RELEASE_TAG}\`"
|
||||
echo "- Release tag: \`${RELEASE_TAG}\`"
|
||||
echo "- Release SHA: \`${TARGET_SHA}\`"
|
||||
echo "- Release approval: this workflow job"
|
||||
@@ -924,6 +1203,9 @@ jobs:
|
||||
else
|
||||
echo "- OpenClaw npm publish: skipped by input"
|
||||
fi
|
||||
if is_stable_release && [[ "${PUBLISH_OPENCLAW_NPM}" == "true" ]]; then
|
||||
echo "- Windows Hub promotion: required before the GitHub release can be published"
|
||||
fi
|
||||
if [[ "${WAIT_FOR_CLAWHUB}" == "true" ]]; then
|
||||
echo "- Workflow completion waits for ClawHub"
|
||||
else
|
||||
@@ -933,27 +1215,66 @@ jobs:
|
||||
|
||||
guard_existing_public_release
|
||||
guard_openclaw_npm_not_already_published
|
||||
resolve_clawhub_release_plan
|
||||
|
||||
npm_args=(-f publish_scope="${PLUGIN_PUBLISH_SCOPE}" -f ref="${TARGET_SHA}" -f release_publish_run_id="${GITHUB_RUN_ID}")
|
||||
clawhub_args=(-f publish_scope="${PLUGIN_PUBLISH_SCOPE}" -f ref="${TARGET_SHA}" -f release_publish_run_id="${GITHUB_RUN_ID}")
|
||||
if [[ -n "${PLUGINS}" ]]; then
|
||||
npm_args+=(-f plugins="${PLUGINS}")
|
||||
clawhub_args+=(-f plugins="${PLUGINS}")
|
||||
fi
|
||||
|
||||
plugin_npm_run_id="$(dispatch_workflow plugin-npm-release.yml "${npm_args[@]}")"
|
||||
plugin_clawhub_run_id="$(dispatch_workflow plugin-clawhub-release.yml "${clawhub_args[@]}")"
|
||||
plugin_clawhub_run_id=""
|
||||
if [[ "$(jq -r '.normal.shouldDispatch' "${clawhub_plan_path}")" == "true" ]]; then
|
||||
clawhub_dispatch_args=()
|
||||
append_clawhub_dispatch_args normal
|
||||
plugin_clawhub_run_id="$(dispatch_workflow_at_ref \
|
||||
"$(jq -r '.normal.ref' "${clawhub_plan_path}")" \
|
||||
"$(jq -r '.normal.workflow' "${clawhub_plan_path}")" \
|
||||
"${clawhub_dispatch_args[@]}")"
|
||||
else
|
||||
echo "- plugin-clawhub-release.yml: no normal OIDC candidates" >> "$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
plugin_clawhub_bootstrap_run_id=""
|
||||
plugin_clawhub_bootstrap_completed="false"
|
||||
if [[ "$(jq -r '.bootstrap.shouldDispatch' "${clawhub_plan_path}")" == "true" ]]; then
|
||||
clawhub_dispatch_args=()
|
||||
append_clawhub_dispatch_args bootstrap
|
||||
plugin_clawhub_bootstrap_run_id="$(dispatch_workflow_at_ref \
|
||||
"$(jq -r '.bootstrap.ref' "${clawhub_plan_path}")" \
|
||||
"$(jq -r '.bootstrap.workflow' "${clawhub_plan_path}")" \
|
||||
"${clawhub_dispatch_args[@]}")"
|
||||
else
|
||||
echo "- plugin-clawhub-new.yml: no bootstrap candidates" >> "$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
{
|
||||
echo "- Plugin npm run ID: \`${plugin_npm_run_id}\`"
|
||||
echo "- Plugin ClawHub run ID: \`${plugin_clawhub_run_id}\`"
|
||||
echo "- Plugin ClawHub run ID: \`${plugin_clawhub_run_id:-none}\`"
|
||||
echo "- Plugin ClawHub bootstrap run ID: \`${plugin_clawhub_bootstrap_run_id:-none}\`"
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
if ! wait_for_run plugin-npm-release.yml "${plugin_npm_run_id}"; then
|
||||
echo "Plugin npm publish failed; cancelling ClawHub publish child ${plugin_clawhub_run_id}." >&2
|
||||
gh run cancel --repo "$GITHUB_REPOSITORY" "${plugin_clawhub_run_id}" >/dev/null 2>&1 || true
|
||||
echo "Plugin npm publish failed; cancelling dispatched ClawHub child workflows." >&2
|
||||
if [[ -n "${plugin_clawhub_run_id}" ]]; then
|
||||
gh run cancel --repo "$GITHUB_REPOSITORY" "${plugin_clawhub_run_id}" >/dev/null 2>&1 || true
|
||||
fi
|
||||
if [[ -n "${plugin_clawhub_bootstrap_run_id}" ]]; then
|
||||
gh run cancel --repo "$GITHUB_REPOSITORY" "${plugin_clawhub_bootstrap_run_id}" >/dev/null 2>&1 || true
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -n "${plugin_clawhub_bootstrap_run_id}" && "${WAIT_FOR_CLAWHUB}" == "true" ]]; then
|
||||
echo "Waiting for plugin-clawhub-new.yml bootstrap to finish before continuing release publish."
|
||||
if wait_for_run plugin-clawhub-new.yml "${plugin_clawhub_bootstrap_run_id}"; then
|
||||
plugin_clawhub_bootstrap_completed="true"
|
||||
else
|
||||
if [[ -n "${plugin_clawhub_run_id}" ]]; then
|
||||
gh run cancel --repo "$GITHUB_REPOSITORY" "${plugin_clawhub_run_id}" >/dev/null 2>&1 || true
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
openclaw_npm_run_id=""
|
||||
if [[ "${PUBLISH_OPENCLAW_NPM}" == "true" ]]; then
|
||||
openclaw_npm_run_id="$(dispatch_workflow openclaw-npm-release.yml \
|
||||
@@ -970,23 +1291,52 @@ jobs:
|
||||
|
||||
clawhub_result=""
|
||||
clawhub_pid=""
|
||||
clawhub_bootstrap_result=""
|
||||
clawhub_bootstrap_pid=""
|
||||
if [[ "${WAIT_FOR_CLAWHUB}" == "true" ]]; then
|
||||
clawhub_result="$RUNNER_TEMP/clawhub-result.txt"
|
||||
wait_run_pid=""
|
||||
wait_for_run_background plugin-clawhub-release.yml "${plugin_clawhub_run_id}" "${clawhub_result}"
|
||||
clawhub_pid="${wait_run_pid}"
|
||||
if [[ -n "${plugin_clawhub_run_id}" ]]; then
|
||||
clawhub_result="$RUNNER_TEMP/clawhub-result.txt"
|
||||
wait_run_pid=""
|
||||
wait_for_run_background plugin-clawhub-release.yml "${plugin_clawhub_run_id}" "${clawhub_result}"
|
||||
clawhub_pid="${wait_run_pid}"
|
||||
fi
|
||||
if [[ -n "${plugin_clawhub_bootstrap_run_id}" ]]; then
|
||||
if [[ "${plugin_clawhub_bootstrap_completed}" == "true" ]]; then
|
||||
echo "- plugin-clawhub-new.yml: bootstrap already completed before continuing" >> "$GITHUB_STEP_SUMMARY"
|
||||
else
|
||||
clawhub_bootstrap_result="$RUNNER_TEMP/clawhub-bootstrap-result.txt"
|
||||
wait_run_pid=""
|
||||
wait_for_run_background plugin-clawhub-new.yml "${plugin_clawhub_bootstrap_run_id}" "${clawhub_bootstrap_result}"
|
||||
clawhub_bootstrap_pid="${wait_run_pid}"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
if wait_for_job_success plugin-clawhub-release.yml "${plugin_clawhub_run_id}" "Validate release publish approval"; then
|
||||
if [[ -n "${plugin_clawhub_run_id}" ]]; then
|
||||
wait_for_job_success plugin-clawhub-release.yml "${plugin_clawhub_run_id}" "Validate release publish approval"
|
||||
if approve_child_publish_environment plugin-clawhub-release.yml "${plugin_clawhub_run_id}"; then
|
||||
:
|
||||
else
|
||||
echo "- plugin-clawhub-release.yml: child environment gate not ready; publish was left dispatched (${plugin_clawhub_run_id})" >> "$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
echo "- plugin-clawhub-release.yml: publish not awaited (${plugin_clawhub_run_id})" >> "$GITHUB_STEP_SUMMARY"
|
||||
else
|
||||
echo "::warning::plugin-clawhub-release.yml did not reach publish approval; continuing because wait_for_clawhub=false."
|
||||
echo "- plugin-clawhub-release.yml: non-blocking before publish approval (${plugin_clawhub_run_id})" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "- plugin-clawhub-release.yml: no normal OIDC publish to await" >> "$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
if [[ -n "${plugin_clawhub_bootstrap_run_id}" ]]; then
|
||||
if [[ "${plugin_clawhub_bootstrap_completed}" == "true" ]]; then
|
||||
echo "- plugin-clawhub-new.yml: bootstrap already completed before continuing" >> "$GITHUB_STEP_SUMMARY"
|
||||
else
|
||||
wait_for_job_success plugin-clawhub-new.yml "${plugin_clawhub_bootstrap_run_id}" "Validate release publish approval"
|
||||
if approve_child_publish_environment plugin-clawhub-new.yml "${plugin_clawhub_bootstrap_run_id}"; then
|
||||
:
|
||||
else
|
||||
echo "- plugin-clawhub-new.yml: child environment gate not ready; bootstrap was left dispatched (${plugin_clawhub_bootstrap_run_id})" >> "$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
echo "- plugin-clawhub-new.yml: bootstrap not awaited (${plugin_clawhub_bootstrap_run_id})" >> "$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
else
|
||||
echo "- plugin-clawhub-new.yml: no bootstrap publish to await" >> "$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
echo "- plugin-clawhub-release.yml: publish not awaited (${plugin_clawhub_run_id})" >> "$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
|
||||
openclaw_result=""
|
||||
@@ -1000,6 +1350,7 @@ jobs:
|
||||
|
||||
failed=0
|
||||
openclaw_failed=0
|
||||
windows_node_run_id=""
|
||||
if [[ -n "${openclaw_pid}" ]] && ! wait "${openclaw_pid}"; then
|
||||
failed=1
|
||||
openclaw_failed=1
|
||||
@@ -1015,6 +1366,12 @@ jobs:
|
||||
if [[ -f "${clawhub_result}" && "$(cat "${clawhub_result}")" != "success" ]]; then
|
||||
failed=1
|
||||
fi
|
||||
if [[ -n "${clawhub_bootstrap_pid}" ]] && ! wait "${clawhub_bootstrap_pid}"; then
|
||||
failed=1
|
||||
fi
|
||||
if [[ -f "${clawhub_bootstrap_result}" && "$(cat "${clawhub_bootstrap_result}")" != "success" ]]; then
|
||||
failed=1
|
||||
fi
|
||||
|
||||
if [[ -n "${openclaw_npm_run_id}" && "${openclaw_failed}" == "0" ]]; then
|
||||
if [[ "${failed}" == "0" ]]; then
|
||||
@@ -1024,6 +1381,9 @@ jobs:
|
||||
fi
|
||||
create_or_update_github_release
|
||||
upload_dependency_evidence_release_asset
|
||||
if ! promote_windows_release_assets; then
|
||||
failed=1
|
||||
fi
|
||||
append_release_proof_to_github_release
|
||||
if [[ "${failed}" == "0" ]]; then
|
||||
publish_github_release
|
||||
|
||||
504
.github/workflows/plugin-clawhub-new.yml
vendored
Normal file
504
.github/workflows/plugin-clawhub-new.yml
vendored
Normal file
@@ -0,0 +1,504 @@
|
||||
name: Plugin ClawHub New
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
plugins:
|
||||
description: Comma-separated plugin package names to bootstrap on ClawHub
|
||||
required: true
|
||||
type: string
|
||||
ref:
|
||||
description: Commit SHA on main, a release branch, or the matching Tideclaw alpha branch to publish from; defaults to the workflow ref
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
release_publish_run_id:
|
||||
description: Approved OpenClaw Release Publish workflow run id
|
||||
required: false
|
||||
type: string
|
||||
release_publish_branch:
|
||||
description: Branch name of the approving OpenClaw Release Publish workflow run
|
||||
required: false
|
||||
type: string
|
||||
dry_run:
|
||||
description: Validate the token-gated ClawHub bootstrap handoff without publishing.
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
|
||||
concurrency:
|
||||
group: plugin-clawhub-new-${{ github.event_name == 'workflow_dispatch' && inputs.ref || github.sha }}
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||
NODE_VERSION: "24.15.0"
|
||||
CLAWHUB_REGISTRY: "https://clawhub.ai"
|
||||
CLAWHUB_CLI_PACKAGE: "clawhub@0.21.0"
|
||||
|
||||
jobs:
|
||||
resolve_bootstrap_plan:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
outputs:
|
||||
ref_revision: ${{ steps.ref.outputs.sha }}
|
||||
has_bootstrap_candidates: ${{ steps.plan.outputs.has_bootstrap_candidates }}
|
||||
bootstrap_candidate_count: ${{ steps.plan.outputs.bootstrap_candidate_count }}
|
||||
matrix: ${{ steps.plan.outputs.matrix }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
ref: ${{ github.ref }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Resolve checked-out ref
|
||||
id: ref
|
||||
env:
|
||||
TARGET_REF: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || '' }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git fetch --no-tags origin \
|
||||
+refs/heads/main:refs/remotes/origin/main \
|
||||
'+refs/heads/release/*:refs/remotes/origin/release/*'
|
||||
if [[ -n "${TARGET_REF}" ]]; then
|
||||
if git rev-parse --verify --quiet "${TARGET_REF}^{commit}" >/dev/null; then
|
||||
target_sha="$(git rev-parse "${TARGET_REF}^{commit}")"
|
||||
elif git rev-parse --verify --quiet "origin/${TARGET_REF}^{commit}" >/dev/null; then
|
||||
target_sha="$(git rev-parse "origin/${TARGET_REF}^{commit}")"
|
||||
else
|
||||
echo "Unable to resolve requested publish ref: ${TARGET_REF}" >&2
|
||||
exit 1
|
||||
fi
|
||||
git checkout --detach "${target_sha}"
|
||||
fi
|
||||
echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Validate ref is on a trusted publish branch
|
||||
env:
|
||||
TRUSTED_PUBLISH_BRANCH: ${{ inputs.release_publish_branch || github.ref_name }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if git merge-base --is-ancestor HEAD origin/main; then
|
||||
exit 0
|
||||
fi
|
||||
while IFS= read -r release_ref; do
|
||||
if git merge-base --is-ancestor HEAD "${release_ref}"; then
|
||||
exit 0
|
||||
fi
|
||||
done < <(git for-each-ref --format='%(refname)' refs/remotes/origin/release)
|
||||
if [[ "${TRUSTED_PUBLISH_BRANCH}" =~ ^tideclaw/alpha/[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{4}Z$ ]]; then
|
||||
alpha_branch="${TRUSTED_PUBLISH_BRANCH}"
|
||||
git fetch --no-tags origin "+refs/heads/${alpha_branch}:refs/remotes/origin/${alpha_branch}"
|
||||
if git merge-base --is-ancestor HEAD "refs/remotes/origin/${alpha_branch}"; then
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
echo "Plugin ClawHub bootstraps must target a commit reachable from main, release/*, or the matching Tideclaw alpha branch." >&2
|
||||
exit 1
|
||||
|
||||
- name: Setup Node environment
|
||||
uses: ./.github/actions/setup-node-env
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
install-bun: "false"
|
||||
|
||||
- name: Validate publishable plugin metadata
|
||||
env:
|
||||
RELEASE_PLUGINS: ${{ inputs.plugins }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [[ -z "${RELEASE_PLUGINS// }" ]]; then
|
||||
echo "Plugin ClawHub bootstrap requires at least one package name in plugins." >&2
|
||||
exit 1
|
||||
fi
|
||||
pnpm release:plugins:clawhub:check -- --selection-mode selected --plugins "${RELEASE_PLUGINS}"
|
||||
|
||||
- name: Resolve plugin bootstrap plan
|
||||
id: plan
|
||||
env:
|
||||
RELEASE_PLUGINS: ${{ inputs.plugins }}
|
||||
CLAWHUB_REGISTRY: ${{ env.CLAWHUB_REGISTRY }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p .local
|
||||
node --import tsx scripts/plugin-clawhub-release-plan.ts \
|
||||
--selection-mode selected \
|
||||
--plugins "${RELEASE_PLUGINS}" > .local/plugin-clawhub-release-plan.json
|
||||
|
||||
cat .local/plugin-clawhub-release-plan.json
|
||||
|
||||
bootstrap_candidate_count="$(jq -r '(.bootstrapCandidates | length) + (.missingTrustedPublisher | length)' .local/plugin-clawhub-release-plan.json)"
|
||||
selected_count="$(jq -r '.all | length' .local/plugin-clawhub-release-plan.json)"
|
||||
matrix_json="$(
|
||||
jq -c '
|
||||
[
|
||||
.bootstrapCandidates[]? + {
|
||||
bootstrapMode: "publish",
|
||||
requiresManualOverride: false
|
||||
},
|
||||
.missingTrustedPublisher[]? + {
|
||||
bootstrapMode: (if .alreadyPublished then "configure-only" else "publish" end),
|
||||
requiresManualOverride: true
|
||||
}
|
||||
]
|
||||
' .local/plugin-clawhub-release-plan.json
|
||||
)"
|
||||
has_bootstrap_candidates="false"
|
||||
if [[ "${bootstrap_candidate_count}" != "0" ]]; then
|
||||
has_bootstrap_candidates="true"
|
||||
fi
|
||||
|
||||
invalid_scope="$(
|
||||
jq -r '
|
||||
(.bootstrapCandidates[]?, .missingTrustedPublisher[]?)
|
||||
| select(.packageName | startswith("@openclaw/") | not)
|
||||
| "- \(.packageName)@\(.version)"
|
||||
' .local/plugin-clawhub-release-plan.json
|
||||
)"
|
||||
if [[ -n "${invalid_scope}" ]]; then
|
||||
echo "Plugin ClawHub bootstrap only supports @openclaw/* packages." >&2
|
||||
printf '%s\n' "${invalid_scope}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
not_bootstrap="$(
|
||||
jq -r '
|
||||
(.bootstrapCandidates | map(.packageName)) as $bootstrapNames
|
||||
| (.missingTrustedPublisher | map(.packageName)) as $repairNames
|
||||
| .all[]?
|
||||
| select(.packageName as $name | ($bootstrapNames + $repairNames | index($name) | not))
|
||||
| "- \(.packageName)@\(.version)"
|
||||
' .local/plugin-clawhub-release-plan.json
|
||||
)"
|
||||
if [[ -n "${not_bootstrap}" ]]; then
|
||||
echo "Selected packages must all be first-publish bootstrap candidates or trusted-publisher repair candidates." >&2
|
||||
printf '%s\n' "${not_bootstrap}" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ "${selected_count}" == "0" || "${bootstrap_candidate_count}" == "0" ]]; then
|
||||
echo "No selected packages require ClawHub bootstrap." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
{
|
||||
echo "bootstrap_candidate_count=${bootstrap_candidate_count}"
|
||||
echo "has_bootstrap_candidates=${has_bootstrap_candidates}"
|
||||
echo "matrix=${matrix_json}"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
echo "ClawHub bootstrap candidates:"
|
||||
jq -r '
|
||||
.bootstrapCandidates[]? | "- \(.packageName)@\(.version) [\(.publishTag)] from \(.packageDir)"
|
||||
' .local/plugin-clawhub-release-plan.json
|
||||
echo "ClawHub trusted-publisher repair candidates:"
|
||||
jq -r '
|
||||
.missingTrustedPublisher[]? | "- \(.packageName)@\(.version) [\(.publishTag)] from \(.packageDir), alreadyPublished=\(.alreadyPublished)"
|
||||
' .local/plugin-clawhub-release-plan.json
|
||||
|
||||
- name: Validate Tideclaw alpha plugin channels
|
||||
env:
|
||||
TRUSTED_PUBLISH_BRANCH: ${{ inputs.release_publish_branch || github.ref_name }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [[ ! "${TRUSTED_PUBLISH_BRANCH}" =~ ^tideclaw/alpha/[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{4}Z$ ]]; then
|
||||
exit 0
|
||||
fi
|
||||
invalid="$(
|
||||
jq -r '
|
||||
(.bootstrapCandidates[]?, .missingTrustedPublisher[]?)
|
||||
| select(.publishTag != "alpha" or .channel != "alpha")
|
||||
| "- \(.packageName)@\(.version) [\(.publishTag)]"
|
||||
' .local/plugin-clawhub-release-plan.json
|
||||
)"
|
||||
if [[ -n "${invalid}" ]]; then
|
||||
echo "Tideclaw alpha ClawHub bootstraps may only publish alpha plugin versions." >&2
|
||||
printf '%s\n' "${invalid}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
validate_release_publish_approval:
|
||||
name: Validate release publish approval
|
||||
needs: resolve_bootstrap_plan
|
||||
if: github.event_name == 'workflow_dispatch' && needs.resolve_bootstrap_plan.outputs.has_bootstrap_candidates == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Validate release publish approval run
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
RELEASE_PUBLISH_RUN_ID: ${{ inputs.release_publish_run_id }}
|
||||
EXPECTED_WORKFLOW_BRANCH: ${{ inputs.release_publish_branch || github.ref_name }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [[ -z "${RELEASE_PUBLISH_RUN_ID// }" ]]; then
|
||||
if [[ "${GITHUB_ACTOR}" == "github-actions[bot]" ]]; then
|
||||
echo "Plugin ClawHub bootstrap dispatched by another workflow must include release_publish_run_id." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "Direct Plugin ClawHub New dispatch; relying on this workflow's clawhub-plugin-bootstrap environment approval."
|
||||
exit 0
|
||||
fi
|
||||
direct_recovery=false
|
||||
if [[ "${GITHUB_ACTOR}" != "github-actions[bot]" ]]; then
|
||||
direct_recovery=true
|
||||
echo "Direct Plugin ClawHub New recovery with release_publish_run_id; relying on this workflow's clawhub-plugin-bootstrap environment approval."
|
||||
fi
|
||||
RUN_JSON="$(gh run view "$RELEASE_PUBLISH_RUN_ID" --repo "$GITHUB_REPOSITORY" --json workflowName,headBranch,event,status,conclusion,url)"
|
||||
printf '%s' "$RUN_JSON" | DIRECT_RELEASE_RECOVERY="${direct_recovery}" node scripts/validate-release-publish-approval.mjs
|
||||
|
||||
validate_bootstrap_trusted_publisher_cli:
|
||||
needs: [resolve_bootstrap_plan, validate_release_publish_approval]
|
||||
if: always() && github.event_name == 'workflow_dispatch' && inputs.dry_run != true && needs.resolve_bootstrap_plan.outputs.has_bootstrap_candidates == 'true' && needs.validate_release_publish_approval.result == 'success'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Validate pinned ClawHub trusted publisher CLI support
|
||||
env:
|
||||
CLAWHUB_CLI_PACKAGE: ${{ env.CLAWHUB_CLI_PACKAGE }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
help_output="$(
|
||||
npm exec --yes --package "${CLAWHUB_CLI_PACKAGE}" -- \
|
||||
clawhub package trusted-publisher set --help 2>&1 || true
|
||||
)"
|
||||
printf '%s\n' "${help_output}"
|
||||
if ! grep -Fq "Usage: clawhub package trusted-publisher set" <<<"${help_output}"; then
|
||||
echo "::error::CLAW-277 03 - Split OpenClaw plugin ClawHub publishing into OIDC release and token bootstrap workflows requires ${CLAWHUB_CLI_PACKAGE} to expose 'package trusted-publisher set' before token bootstrap publish can run. The pinned CLI returned parent help or no set command, so this workflow is stopping before creating a ClawHub package row."
|
||||
exit 1
|
||||
fi
|
||||
for required_flag in --repository --workflow-filename; do
|
||||
if ! grep -Fq -- "${required_flag}" <<<"${help_output}"; then
|
||||
echo "::error::CLAW-277 03 - Split OpenClaw plugin ClawHub publishing into OIDC release and token bootstrap workflows requires ${CLAWHUB_CLI_PACKAGE} trusted-publisher set help to include ${required_flag}."
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
publish_bootstrap_plugins:
|
||||
needs:
|
||||
[
|
||||
resolve_bootstrap_plan,
|
||||
validate_release_publish_approval,
|
||||
validate_bootstrap_trusted_publisher_cli,
|
||||
]
|
||||
if: always() && github.event_name == 'workflow_dispatch' && needs.resolve_bootstrap_plan.outputs.has_bootstrap_candidates == 'true' && needs.validate_release_publish_approval.result == 'success' && (inputs.dry_run == true || needs.validate_bootstrap_trusted_publisher_cli.result == 'success')
|
||||
runs-on: ubuntu-latest
|
||||
environment: clawhub-plugin-bootstrap
|
||||
permissions:
|
||||
contents: read
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: 8
|
||||
matrix:
|
||||
plugin: ${{ fromJson(needs.resolve_bootstrap_plan.outputs.matrix) }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
ref: ${{ github.ref }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Checkout target revision
|
||||
env:
|
||||
TARGET_SHA: ${{ needs.resolve_bootstrap_plan.outputs.ref_revision }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git fetch --no-tags origin \
|
||||
+refs/heads/main:refs/remotes/origin/main \
|
||||
'+refs/heads/release/*:refs/remotes/origin/release/*'
|
||||
git checkout --detach "${TARGET_SHA}"
|
||||
|
||||
- name: Setup Node environment
|
||||
uses: ./.github/actions/setup-node-env
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
install-bun: "true"
|
||||
install-deps: "true"
|
||||
|
||||
- name: Verify package-local runtime build
|
||||
run: node scripts/check-plugin-npm-runtime-builds.mjs --package "${{ matrix.plugin.packageDir }}"
|
||||
|
||||
- name: Install pinned ClawHub CLI wrapper
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cat > "${RUNNER_TEMP}/clawhub" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
exec npm exec --yes --package "${CLAWHUB_CLI_PACKAGE}" -- clawhub "$@"
|
||||
EOF
|
||||
chmod +x "${RUNNER_TEMP}/clawhub"
|
||||
echo "${RUNNER_TEMP}" >> "${GITHUB_PATH}"
|
||||
|
||||
- name: Write ClawHub token config
|
||||
if: inputs.dry_run != true
|
||||
env:
|
||||
CLAWHUB_REGISTRY: ${{ env.CLAWHUB_REGISTRY }}
|
||||
CLAWHUB_TOKEN: ${{ secrets.CLAWHUB_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
config_path="${RUNNER_TEMP}/clawhub-config.json"
|
||||
CONFIG_PATH="${config_path}" node --input-type=module <<'NODE'
|
||||
import { writeFileSync } from "node:fs";
|
||||
|
||||
const registry = process.env.CLAWHUB_REGISTRY?.trim();
|
||||
const token = process.env.CLAWHUB_TOKEN?.trim();
|
||||
const configPath = process.env.CONFIG_PATH;
|
||||
if (!registry) {
|
||||
throw new Error("CLAWHUB_REGISTRY is required for token-gated ClawHub bootstrap.");
|
||||
}
|
||||
if (!token) {
|
||||
throw new Error("CLAWHUB_TOKEN is required for token-gated ClawHub bootstrap.");
|
||||
}
|
||||
if (!configPath) {
|
||||
throw new Error("CONFIG_PATH is required.");
|
||||
}
|
||||
|
||||
writeFileSync(configPath, `${JSON.stringify({ registry, token }, null, 2)}\n`, {
|
||||
encoding: "utf8",
|
||||
mode: 0o600,
|
||||
});
|
||||
NODE
|
||||
echo "CLAWHUB_CONFIG_PATH=${config_path}" >> "${GITHUB_ENV}"
|
||||
|
||||
- name: Publish ClawHub bootstrap package
|
||||
env:
|
||||
CLAWHUB_REGISTRY: ${{ env.CLAWHUB_REGISTRY }}
|
||||
SOURCE_REPO: ${{ github.repository }}
|
||||
SOURCE_COMMIT: ${{ needs.resolve_bootstrap_plan.outputs.ref_revision }}
|
||||
SOURCE_REF: ${{ github.ref }}
|
||||
PACKAGE_TAG: ${{ matrix.plugin.publishTag }}
|
||||
PACKAGE_DIR: ${{ matrix.plugin.packageDir }}
|
||||
BOOTSTRAP_MODE: ${{ matrix.plugin.bootstrapMode }}
|
||||
REQUIRES_MANUAL_OVERRIDE: ${{ matrix.plugin.requiresManualOverride && 'true' || 'false' }}
|
||||
DRY_RUN: ${{ inputs.dry_run && 'true' || 'false' }}
|
||||
OPENCLAW_PLUGIN_NPM_RUNTIME_BUILD: "0"
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [[ "${BOOTSTRAP_MODE}" == "configure-only" ]]; then
|
||||
echo "Skipping bootstrap publish because ${PACKAGE_DIR} version is already present on ClawHub; configuring trusted publisher only."
|
||||
elif [[ "${DRY_RUN}" == "true" ]]; then
|
||||
bash scripts/plugin-clawhub-publish.sh --dry-run "${PACKAGE_DIR}"
|
||||
else
|
||||
if [[ "${REQUIRES_MANUAL_OVERRIDE}" == "true" ]]; then
|
||||
export OPENCLAW_CLAWHUB_MANUAL_OVERRIDE_REASON="GitHub Actions trusted publisher repair before OIDC migration"
|
||||
fi
|
||||
bash scripts/plugin-clawhub-publish.sh --publish "${PACKAGE_DIR}"
|
||||
fi
|
||||
|
||||
- name: Configure trusted publisher for normal OIDC releases
|
||||
if: inputs.dry_run != true
|
||||
env:
|
||||
CLAWHUB_CLI_PACKAGE: ${{ env.CLAWHUB_CLI_PACKAGE }}
|
||||
PACKAGE_NAME: ${{ matrix.plugin.packageName }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
npm exec --yes --package "${CLAWHUB_CLI_PACKAGE}" -- \
|
||||
clawhub package trusted-publisher set "${PACKAGE_NAME}" \
|
||||
--repository openclaw/openclaw \
|
||||
--workflow-filename plugin-clawhub-release.yml
|
||||
|
||||
verify_bootstrap_clawhub_package:
|
||||
needs: [resolve_bootstrap_plan, publish_bootstrap_plugins]
|
||||
if: github.event_name == 'workflow_dispatch' && inputs.dry_run != true && needs.resolve_bootstrap_plan.outputs.has_bootstrap_candidates == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: 8
|
||||
matrix:
|
||||
plugin: ${{ fromJson(needs.resolve_bootstrap_plan.outputs.matrix) }}
|
||||
steps:
|
||||
- name: Verify bootstrap ClawHub package and trusted publisher
|
||||
env:
|
||||
CLAWHUB_REGISTRY: ${{ env.CLAWHUB_REGISTRY }}
|
||||
PACKAGE_NAME: ${{ matrix.plugin.packageName }}
|
||||
PACKAGE_VERSION: ${{ matrix.plugin.version }}
|
||||
PACKAGE_TAG: ${{ matrix.plugin.publishTag }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
node --input-type=module <<'EOF'
|
||||
const registry = (process.env.CLAWHUB_REGISTRY ?? "https://clawhub.ai").replace(/\/+$/, "");
|
||||
const packageName = process.env.PACKAGE_NAME;
|
||||
const packageVersion = process.env.PACKAGE_VERSION;
|
||||
const packageTag = process.env.PACKAGE_TAG;
|
||||
if (!packageName || !packageVersion || !packageTag) {
|
||||
throw new Error("Missing ClawHub bootstrap verification env.");
|
||||
}
|
||||
const encodedName = encodeURIComponent(packageName);
|
||||
const encodedVersion = encodeURIComponent(packageVersion);
|
||||
const detailUrl = `${registry}/api/v1/packages/${encodedName}`;
|
||||
const trustedPublisherUrl = `${detailUrl}/trusted-publisher`;
|
||||
const versionUrl = `${detailUrl}/versions/${encodedVersion}`;
|
||||
const artifactUrl = `${versionUrl}/artifact/download`;
|
||||
|
||||
async function fetchWithRetry(url, options = {}) {
|
||||
let lastStatus = "unknown";
|
||||
for (let attempt = 1; attempt <= 12; attempt += 1) {
|
||||
try {
|
||||
const response = await fetch(url, { redirect: "manual", ...options });
|
||||
lastStatus = response.status;
|
||||
if (response.status !== 429 && response.status < 500) {
|
||||
return response;
|
||||
}
|
||||
} catch (error) {
|
||||
lastStatus = error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, attempt * 5000));
|
||||
}
|
||||
throw new Error(`${url} did not stabilize; last status ${lastStatus}.`);
|
||||
}
|
||||
|
||||
const detailResponse = await fetchWithRetry(detailUrl, {
|
||||
headers: { accept: "application/json" },
|
||||
});
|
||||
if (!detailResponse.ok) {
|
||||
throw new Error(`${detailUrl} returned HTTP ${detailResponse.status}.`);
|
||||
}
|
||||
const detail = await detailResponse.json();
|
||||
const tags = detail?.package?.tags ?? {};
|
||||
if (tags[packageTag] !== packageVersion) {
|
||||
throw new Error(
|
||||
`${packageName}: ClawHub tag ${packageTag} points to ${tags[packageTag] ?? "<missing>"}, expected ${packageVersion}.`,
|
||||
);
|
||||
}
|
||||
|
||||
const trustedPublisherResponse = await fetchWithRetry(trustedPublisherUrl, {
|
||||
headers: { accept: "application/json" },
|
||||
});
|
||||
if (!trustedPublisherResponse.ok) {
|
||||
throw new Error(`${trustedPublisherUrl} returned HTTP ${trustedPublisherResponse.status}.`);
|
||||
}
|
||||
const trustedPublisherDetail = await trustedPublisherResponse.json();
|
||||
const trustedPublisher = trustedPublisherDetail?.trustedPublisher;
|
||||
if (
|
||||
trustedPublisher?.repository !== "openclaw/openclaw" ||
|
||||
trustedPublisher?.workflowFilename !== "plugin-clawhub-release.yml" ||
|
||||
trustedPublisher?.environment != null
|
||||
) {
|
||||
throw new Error(
|
||||
`${packageName}: trusted publisher config did not match openclaw/openclaw plugin-clawhub-release.yml without an environment pin.`,
|
||||
);
|
||||
}
|
||||
|
||||
const versionResponse = await fetchWithRetry(versionUrl);
|
||||
if (!versionResponse.ok) {
|
||||
throw new Error(`${versionUrl} returned HTTP ${versionResponse.status}.`);
|
||||
}
|
||||
const artifactResponse = await fetchWithRetry(artifactUrl, { method: "HEAD" });
|
||||
if (artifactResponse.status < 200 || artifactResponse.status >= 400) {
|
||||
throw new Error(`${artifactUrl} returned HTTP ${artifactResponse.status}.`);
|
||||
}
|
||||
console.log(`${packageName}@${packageVersion} bootstrap verified on ClawHub.`);
|
||||
EOF
|
||||
258
.github/workflows/plugin-clawhub-release.yml
vendored
258
.github/workflows/plugin-clawhub-release.yml
vendored
@@ -16,7 +16,7 @@ on:
|
||||
required: false
|
||||
type: string
|
||||
ref:
|
||||
description: Commit SHA on main, a release branch, or the matching Tideclaw alpha branch to publish from; defaults to the workflow ref
|
||||
description: Dry-run target ref to validate; real OIDC publishes must dispatch the workflow with --ref set to the target release tag/ref
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
@@ -24,6 +24,10 @@ on:
|
||||
description: Approved OpenClaw Release Publish workflow run id
|
||||
required: false
|
||||
type: string
|
||||
release_publish_branch:
|
||||
description: Branch name of the approving OpenClaw Release Publish workflow run
|
||||
required: false
|
||||
type: string
|
||||
dry_run:
|
||||
description: Validate the full ClawHub artifact handoff without publishing.
|
||||
required: false
|
||||
@@ -38,9 +42,7 @@ env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||
NODE_VERSION: "24.15.0"
|
||||
CLAWHUB_REGISTRY: "https://clawhub.ai"
|
||||
CLAWHUB_REPOSITORY: "openclaw/clawhub"
|
||||
# Pinned to a reviewed ClawHub commit so release behavior stays reproducible.
|
||||
CLAWHUB_REF: "c9bb13023598dcc547fdf4a93b9d42512b8c8854"
|
||||
CLAWHUB_CLI_PACKAGE: "clawhub@0.21.0"
|
||||
|
||||
jobs:
|
||||
preview_plugins_clawhub:
|
||||
@@ -50,9 +52,15 @@ jobs:
|
||||
outputs:
|
||||
ref_revision: ${{ steps.ref.outputs.sha }}
|
||||
has_candidates: ${{ steps.plan.outputs.has_candidates }}
|
||||
has_bootstrap_candidates: ${{ steps.plan.outputs.has_bootstrap_candidates }}
|
||||
has_missing_trusted_publisher: ${{ steps.plan.outputs.has_missing_trusted_publisher }}
|
||||
candidate_count: ${{ steps.plan.outputs.candidate_count }}
|
||||
bootstrap_candidate_count: ${{ steps.plan.outputs.bootstrap_candidate_count }}
|
||||
missing_trusted_publisher_count: ${{ steps.plan.outputs.missing_trusted_publisher_count }}
|
||||
skipped_published_count: ${{ steps.plan.outputs.skipped_published_count }}
|
||||
matrix: ${{ steps.plan.outputs.matrix }}
|
||||
bootstrap_matrix: ${{ steps.plan.outputs.bootstrap_matrix }}
|
||||
missing_trusted_publisher_matrix: ${{ steps.plan.outputs.missing_trusted_publisher_matrix }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
@@ -83,9 +91,27 @@ jobs:
|
||||
fi
|
||||
echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Validate OIDC source matches workflow ref
|
||||
env:
|
||||
TARGET_SHA: ${{ steps.ref.outputs.sha }}
|
||||
WORKFLOW_SHA: ${{ github.sha }}
|
||||
DRY_RUN: ${{ inputs.dry_run && 'true' || 'false' }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [[ "${TARGET_SHA}" != "${WORKFLOW_SHA}" ]]; then
|
||||
if [[ "${DRY_RUN}" == "true" ]]; then
|
||||
echo "Dry-run publish target differs from workflow ref; allowing validation-only dispatch."
|
||||
exit 0
|
||||
fi
|
||||
echo "Plugin ClawHub OIDC publishes must run from the same ref that is being published." >&2
|
||||
echo "The ref input is only supported for dry_run=true." >&2
|
||||
echo "For real publishes, dispatch this workflow with --ref pointing at the target release tag/ref and omit the ref input." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Validate ref is on a trusted publish branch
|
||||
env:
|
||||
WORKFLOW_REF: ${{ github.ref }}
|
||||
TRUSTED_PUBLISH_BRANCH: ${{ inputs.release_publish_branch || github.ref_name }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if git merge-base --is-ancestor HEAD origin/main; then
|
||||
@@ -96,8 +122,8 @@ jobs:
|
||||
exit 0
|
||||
fi
|
||||
done < <(git for-each-ref --format='%(refname)' refs/remotes/origin/release)
|
||||
if [[ "${WORKFLOW_REF}" =~ ^refs/heads/tideclaw/alpha/[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{4}Z$ ]]; then
|
||||
alpha_branch="${WORKFLOW_REF#refs/heads/}"
|
||||
if [[ "${TRUSTED_PUBLISH_BRANCH}" =~ ^tideclaw/alpha/[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{4}Z$ ]]; then
|
||||
alpha_branch="${TRUSTED_PUBLISH_BRANCH}"
|
||||
git fetch --no-tags origin "+refs/heads/${alpha_branch}:refs/remotes/origin/${alpha_branch}"
|
||||
if git merge-base --is-ancestor HEAD "refs/remotes/origin/${alpha_branch}"; then
|
||||
exit 0
|
||||
@@ -158,36 +184,78 @@ jobs:
|
||||
cat .local/plugin-clawhub-release-plan.json
|
||||
|
||||
candidate_count="$(jq -r '.candidates | length' .local/plugin-clawhub-release-plan.json)"
|
||||
bootstrap_candidate_count="$(jq -r '.bootstrapCandidates | length' .local/plugin-clawhub-release-plan.json)"
|
||||
missing_trusted_publisher_count="$(jq -r '.missingTrustedPublisher | length' .local/plugin-clawhub-release-plan.json)"
|
||||
skipped_published_count="$(jq -r '.skippedPublished | length' .local/plugin-clawhub-release-plan.json)"
|
||||
has_candidates="false"
|
||||
if [[ "${candidate_count}" != "0" ]]; then
|
||||
has_candidates="true"
|
||||
fi
|
||||
has_bootstrap_candidates="false"
|
||||
if [[ "${bootstrap_candidate_count}" != "0" ]]; then
|
||||
has_bootstrap_candidates="true"
|
||||
fi
|
||||
has_missing_trusted_publisher="false"
|
||||
if [[ "${missing_trusted_publisher_count}" != "0" ]]; then
|
||||
has_missing_trusted_publisher="true"
|
||||
fi
|
||||
matrix_json="$(jq -c '.candidates' .local/plugin-clawhub-release-plan.json)"
|
||||
bootstrap_matrix_json="$(jq -c '.bootstrapCandidates' .local/plugin-clawhub-release-plan.json)"
|
||||
missing_trusted_publisher_matrix_json="$(jq -c '.missingTrustedPublisher' .local/plugin-clawhub-release-plan.json)"
|
||||
|
||||
{
|
||||
echo "candidate_count=${candidate_count}"
|
||||
echo "bootstrap_candidate_count=${bootstrap_candidate_count}"
|
||||
echo "missing_trusted_publisher_count=${missing_trusted_publisher_count}"
|
||||
echo "skipped_published_count=${skipped_published_count}"
|
||||
echo "has_candidates=${has_candidates}"
|
||||
echo "has_bootstrap_candidates=${has_bootstrap_candidates}"
|
||||
echo "has_missing_trusted_publisher=${has_missing_trusted_publisher}"
|
||||
echo "matrix=${matrix_json}"
|
||||
echo "bootstrap_matrix=${bootstrap_matrix_json}"
|
||||
echo "missing_trusted_publisher_matrix=${missing_trusted_publisher_matrix_json}"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
echo "Plugin release candidates:"
|
||||
jq -r '.candidates[]? | "- \(.packageName)@\(.version) [\(.publishTag)] from \(.packageDir)"' .local/plugin-clawhub-release-plan.json
|
||||
|
||||
echo "Bootstrap candidates requiring token bootstrap:"
|
||||
jq -r '.bootstrapCandidates[]? | "- \(.packageName)@\(.version) [\(.publishTag)] from \(.packageDir)"' .local/plugin-clawhub-release-plan.json
|
||||
|
||||
echo "Missing trusted publisher candidates:"
|
||||
jq -r '.missingTrustedPublisher[]? | "- \(.packageName)@\(.version) [\(.publishTag)] from \(.packageDir)"' .local/plugin-clawhub-release-plan.json
|
||||
|
||||
echo "Already published / skipped:"
|
||||
jq -r '.skippedPublished[]? | "- \(.packageName)@\(.version)"' .local/plugin-clawhub-release-plan.json
|
||||
|
||||
- name: Fail when trusted publisher is missing
|
||||
if: steps.plan.outputs.missing_trusted_publisher_count != '0'
|
||||
run: |
|
||||
echo "::error::One or more ClawHub packages exist but do not have trusted publishing configured. Configure trusted publishing before running the normal OIDC publish workflow."
|
||||
jq -r '.missingTrustedPublisher[]? | "::error::Missing trusted publisher: \(.packageName)@\(.version). Configure trusted publishing for openclaw/openclaw, workflow plugin-clawhub-release.yml."' .local/plugin-clawhub-release-plan.json
|
||||
exit 1
|
||||
|
||||
- name: Fail normal publish when bootstrap is required
|
||||
if: steps.plan.outputs.bootstrap_candidate_count != '0'
|
||||
run: |
|
||||
echo "::error::One or more ClawHub packages do not exist yet and require the token-gated Plugin ClawHub New bootstrap workflow before normal OIDC publish can run."
|
||||
jq -r '.bootstrapCandidates[]? | "::error::Bootstrap required: \(.packageName)@\(.version). Dispatch plugin-clawhub-new.yml for this package, then rerun the normal release."' .local/plugin-clawhub-release-plan.json
|
||||
exit 1
|
||||
|
||||
- name: Fail manual publish when target versions already exist
|
||||
if: github.event_name == 'workflow_dispatch' && inputs.publish_scope == 'selected' && steps.plan.outputs.skipped_published_count != '0'
|
||||
if: github.event_name == 'workflow_dispatch' && inputs.dry_run != true && inputs.publish_scope == 'selected' && steps.plan.outputs.skipped_published_count != '0'
|
||||
run: |
|
||||
echo "::error::One or more selected plugin versions already exist on ClawHub. Bump the version before running a real publish."
|
||||
exit 1
|
||||
|
||||
- name: Validate Tideclaw alpha plugin channels
|
||||
if: startsWith(github.ref, 'refs/heads/tideclaw/alpha/')
|
||||
env:
|
||||
TRUSTED_PUBLISH_BRANCH: ${{ inputs.release_publish_branch || github.ref_name }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [[ ! "${TRUSTED_PUBLISH_BRANCH}" =~ ^tideclaw/alpha/[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{4}Z$ ]]; then
|
||||
exit 0
|
||||
fi
|
||||
invalid="$(
|
||||
jq -r '.candidates[]? | select(.publishTag != "alpha" or .channel != "alpha") | "- \(.packageName)@\(.version) [\(.publishTag)]"' .local/plugin-clawhub-release-plan.json
|
||||
)"
|
||||
@@ -197,12 +265,6 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Verify OpenClaw ClawHub package ownership
|
||||
if: steps.plan.outputs.has_candidates == 'true'
|
||||
env:
|
||||
CLAWHUB_REGISTRY: ${{ env.CLAWHUB_REGISTRY }}
|
||||
run: node --import tsx scripts/plugin-clawhub-owner-preflight.ts .local/plugin-clawhub-release-plan.json
|
||||
|
||||
validate_release_publish_approval:
|
||||
name: Validate release publish approval
|
||||
needs: preview_plugins_clawhub
|
||||
@@ -221,7 +283,7 @@ jobs:
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
RELEASE_PUBLISH_RUN_ID: ${{ inputs.release_publish_run_id }}
|
||||
EXPECTED_WORKFLOW_BRANCH: ${{ github.ref_name }}
|
||||
EXPECTED_WORKFLOW_BRANCH: ${{ inputs.release_publish_branch || github.ref_name }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [[ -z "${RELEASE_PUBLISH_RUN_ID// }" ]]; then
|
||||
@@ -240,99 +302,8 @@ jobs:
|
||||
RUN_JSON="$(gh run view "$RELEASE_PUBLISH_RUN_ID" --repo "$GITHUB_REPOSITORY" --json workflowName,headBranch,event,status,conclusion,url)"
|
||||
printf '%s' "$RUN_JSON" | DIRECT_RELEASE_RECOVERY="${direct_recovery}" node scripts/validate-release-publish-approval.mjs
|
||||
|
||||
preview_plugin_pack:
|
||||
needs: preview_plugins_clawhub
|
||||
if: needs.preview_plugins_clawhub.outputs.has_candidates == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: 12
|
||||
matrix:
|
||||
plugin: ${{ fromJson(needs.preview_plugins_clawhub.outputs.matrix) }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
ref: ${{ github.ref }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Checkout target revision
|
||||
env:
|
||||
TARGET_SHA: ${{ needs.preview_plugins_clawhub.outputs.ref_revision }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git fetch --no-tags origin \
|
||||
+refs/heads/main:refs/remotes/origin/main \
|
||||
'+refs/heads/release/*:refs/remotes/origin/release/*'
|
||||
git checkout --detach "${TARGET_SHA}"
|
||||
|
||||
- name: Setup Node environment
|
||||
uses: ./.github/actions/setup-node-env
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
install-bun: "true"
|
||||
install-deps: "true"
|
||||
|
||||
- name: Checkout ClawHub CLI source
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
repository: ${{ env.CLAWHUB_REPOSITORY }}
|
||||
ref: main
|
||||
path: clawhub-source
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Checkout pinned ClawHub CLI revision
|
||||
working-directory: clawhub-source
|
||||
env:
|
||||
CLAWHUB_REF: ${{ env.CLAWHUB_REF }}
|
||||
run: git checkout --detach "${CLAWHUB_REF}"
|
||||
|
||||
- name: Install ClawHub CLI dependencies
|
||||
working-directory: clawhub-source
|
||||
run: |
|
||||
set -euo pipefail
|
||||
for attempt in 1 2 3; do
|
||||
if bun install --frozen-lockfile; then
|
||||
exit 0
|
||||
fi
|
||||
status="$?"
|
||||
if [[ "${attempt}" == "3" ]]; then
|
||||
exit "${status}"
|
||||
fi
|
||||
echo "bun install failed while preparing ClawHub CLI; retrying (${attempt}/3)."
|
||||
rm -rf node_modules "${RUNNER_TEMP}/bun-install-cache" || true
|
||||
sleep $((attempt * 15))
|
||||
done
|
||||
|
||||
- name: Bootstrap ClawHub CLI
|
||||
run: |
|
||||
cat > "$RUNNER_TEMP/clawhub" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
exec bun "$GITHUB_WORKSPACE/clawhub-source/packages/clawhub/src/cli.ts" "$@"
|
||||
EOF
|
||||
chmod +x "$RUNNER_TEMP/clawhub"
|
||||
echo "$RUNNER_TEMP" >> "$GITHUB_PATH"
|
||||
|
||||
- name: Verify package-local runtime build
|
||||
run: node scripts/check-plugin-npm-runtime-builds.mjs --package "${{ matrix.plugin.packageDir }}"
|
||||
|
||||
- name: Preview publish command
|
||||
env:
|
||||
CLAWHUB_REGISTRY: ${{ env.CLAWHUB_REGISTRY }}
|
||||
SOURCE_REPO: ${{ github.repository }}
|
||||
SOURCE_COMMIT: ${{ needs.preview_plugins_clawhub.outputs.ref_revision }}
|
||||
SOURCE_REF: ${{ github.ref }}
|
||||
PACKAGE_TAG: ${{ matrix.plugin.publishTag }}
|
||||
PACKAGE_DIR: ${{ matrix.plugin.packageDir }}
|
||||
run: bash scripts/plugin-clawhub-publish.sh --dry-run "${PACKAGE_DIR}"
|
||||
|
||||
pack_plugins_clawhub_artifacts:
|
||||
needs: [preview_plugins_clawhub, preview_plugin_pack, validate_release_publish_approval]
|
||||
needs: [preview_plugins_clawhub, validate_release_publish_approval]
|
||||
if: github.event_name == 'workflow_dispatch' && needs.preview_plugins_clawhub.outputs.has_candidates == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
@@ -367,47 +338,19 @@ jobs:
|
||||
install-bun: "true"
|
||||
install-deps: "true"
|
||||
|
||||
- name: Checkout ClawHub CLI source
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
repository: ${{ env.CLAWHUB_REPOSITORY }}
|
||||
ref: main
|
||||
path: clawhub-source
|
||||
fetch-depth: 0
|
||||
- name: Verify package-local runtime build
|
||||
run: node scripts/check-plugin-npm-runtime-builds.mjs --package "${{ matrix.plugin.packageDir }}"
|
||||
|
||||
- name: Checkout pinned ClawHub CLI revision
|
||||
working-directory: clawhub-source
|
||||
env:
|
||||
CLAWHUB_REF: ${{ env.CLAWHUB_REF }}
|
||||
run: git checkout --detach "${CLAWHUB_REF}"
|
||||
|
||||
- name: Install ClawHub CLI dependencies
|
||||
working-directory: clawhub-source
|
||||
- name: Install pinned ClawHub CLI wrapper
|
||||
run: |
|
||||
set -euo pipefail
|
||||
for attempt in 1 2 3; do
|
||||
if bun install --frozen-lockfile; then
|
||||
exit 0
|
||||
fi
|
||||
status="$?"
|
||||
if [[ "${attempt}" == "3" ]]; then
|
||||
exit "${status}"
|
||||
fi
|
||||
echo "bun install failed while preparing ClawHub CLI; retrying (${attempt}/3)."
|
||||
rm -rf node_modules "${RUNNER_TEMP}/bun-install-cache" || true
|
||||
sleep $((attempt * 15))
|
||||
done
|
||||
|
||||
- name: Bootstrap ClawHub CLI
|
||||
run: |
|
||||
cat > "$RUNNER_TEMP/clawhub" <<'EOF'
|
||||
cat > "${RUNNER_TEMP}/clawhub" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
exec bun "$GITHUB_WORKSPACE/clawhub-source/packages/clawhub/src/cli.ts" "$@"
|
||||
exec npm exec --yes --package "${CLAWHUB_CLI_PACKAGE}" -- clawhub "$@"
|
||||
EOF
|
||||
chmod +x "$RUNNER_TEMP/clawhub"
|
||||
echo "$RUNNER_TEMP" >> "$GITHUB_PATH"
|
||||
chmod +x "${RUNNER_TEMP}/clawhub"
|
||||
echo "${RUNNER_TEMP}" >> "${GITHUB_PATH}"
|
||||
|
||||
- name: Pack ClawHub package artifact
|
||||
env:
|
||||
@@ -428,19 +371,23 @@ jobs:
|
||||
if-no-files-found: error
|
||||
retention-days: 7
|
||||
|
||||
approve_plugin_clawhub_release:
|
||||
approve_plugins_clawhub_release:
|
||||
needs: [preview_plugins_clawhub, pack_plugins_clawhub_artifacts]
|
||||
if: github.event_name == 'workflow_dispatch' && inputs.dry_run != true && needs.preview_plugins_clawhub.outputs.has_candidates == 'true'
|
||||
if: always() && github.event_name == 'workflow_dispatch' && inputs.dry_run != true && needs.preview_plugins_clawhub.outputs.has_candidates == 'true' && needs.pack_plugins_clawhub_artifacts.result == 'success'
|
||||
runs-on: ubuntu-latest
|
||||
environment: clawhub-plugin-release
|
||||
permissions: {}
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Approve ClawHub package publish
|
||||
run: echo "ClawHub package publish approved."
|
||||
- name: Approve Plugin ClawHub release publish
|
||||
run: |
|
||||
echo "Approved CLAW-277 03 - Split OpenClaw plugin ClawHub publishing into OIDC release and token bootstrap workflows release publish gate."
|
||||
|
||||
publish_plugins_clawhub:
|
||||
needs: [preview_plugins_clawhub, pack_plugins_clawhub_artifacts, approve_plugin_clawhub_release]
|
||||
if: always() && github.event_name == 'workflow_dispatch' && needs.preview_plugins_clawhub.outputs.has_candidates == 'true' && needs.pack_plugins_clawhub_artifacts.result == 'success' && (inputs.dry_run == true || needs.approve_plugin_clawhub_release.result == 'success')
|
||||
needs:
|
||||
[preview_plugins_clawhub, pack_plugins_clawhub_artifacts, approve_plugins_clawhub_release]
|
||||
if: always() && github.event_name == 'workflow_dispatch' && needs.preview_plugins_clawhub.outputs.has_candidates == 'true' && needs.pack_plugins_clawhub_artifacts.result == 'success' && (inputs.dry_run == true || needs.approve_plugins_clawhub_release.result == 'success')
|
||||
uses: openclaw/clawhub/.github/workflows/package-publish.yml@9d49df109d4ad3dc8a6ecf05d26b39f46d294721
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
@@ -450,19 +397,18 @@ jobs:
|
||||
max-parallel: 32
|
||||
matrix:
|
||||
plugin: ${{ fromJson(needs.preview_plugins_clawhub.outputs.matrix) }}
|
||||
uses: openclaw/clawhub/.github/workflows/package-publish.yml@c9bb13023598dcc547fdf4a93b9d42512b8c8854
|
||||
with:
|
||||
dry_run: ${{ inputs.dry_run }}
|
||||
json: true
|
||||
package_artifact_name: ${{ matrix.plugin.artifactName }}
|
||||
dry_run: ${{ inputs.dry_run }}
|
||||
registry: https://clawhub.ai
|
||||
site: https://clawhub.ai
|
||||
tags: ${{ matrix.plugin.publishTag }}
|
||||
source_repo: ${{ github.repository }}
|
||||
source_commit: ${{ needs.preview_plugins_clawhub.outputs.ref_revision }}
|
||||
source_ref: ${{ github.ref }}
|
||||
tags: ${{ matrix.plugin.publishTag }}
|
||||
secrets:
|
||||
clawhub_token: ${{ secrets.CLAWHUB_TOKEN }}
|
||||
source_path: ${{ matrix.plugin.packageDir }}
|
||||
inspector_artifact_name: ${{ matrix.plugin.artifactName }}-inspector
|
||||
publish_json_artifact_name: ${{ matrix.plugin.artifactName }}-publish-json
|
||||
|
||||
verify_published_clawhub_package:
|
||||
needs: [preview_plugins_clawhub, publish_plugins_clawhub]
|
||||
|
||||
@@ -532,7 +532,6 @@ jobs:
|
||||
OPENCLAW_QA_CONVEX_SECRET_CI: ${{ secrets.OPENCLAW_QA_CONVEX_SECRET_CI }}
|
||||
OPENCLAW_QA_CREDENTIAL_ACQUIRE_TIMEOUT_MS: "1800000"
|
||||
OPENCLAW_QA_REDACT_PUBLIC_METADATA: "1"
|
||||
OPENCLAW_QA_TELEGRAM_CAPTURE_CONTENT: "1"
|
||||
INPUT_SCENARIO: ${{ github.event_name == 'workflow_dispatch' && inputs.scenario || '' }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
11
.github/workflows/stale.yml
vendored
11
.github/workflows/stale.yml
vendored
@@ -68,7 +68,7 @@ jobs:
|
||||
days-before-pr-close: 7
|
||||
stale-issue-label: stale
|
||||
stale-pr-label: stale
|
||||
exempt-issue-labels: enhancement,maintainer,pinned,security,no-stale,bad-barnacle
|
||||
exempt-issue-labels: enhancement,maintainer,pinned,security,no-stale,bad-barnacle,clawsweeper:queueable-fix,clawsweeper:source-repro,clawsweeper:fix-shape-clear
|
||||
exempt-pr-labels: maintainer,no-stale,bad-barnacle
|
||||
operations-per-run: 2000
|
||||
ascending: true
|
||||
@@ -100,7 +100,7 @@ jobs:
|
||||
days-before-pr-stale: -1
|
||||
days-before-pr-close: -1
|
||||
stale-issue-label: stale
|
||||
exempt-issue-labels: enhancement,maintainer,pinned,security,no-stale,bad-barnacle
|
||||
exempt-issue-labels: enhancement,maintainer,pinned,security,no-stale,bad-barnacle,clawsweeper:queueable-fix,clawsweeper:source-repro,clawsweeper:fix-shape-clear
|
||||
operations-per-run: 2000
|
||||
ascending: true
|
||||
include-only-assigned: true
|
||||
@@ -172,7 +172,7 @@ jobs:
|
||||
days-before-pr-close: 7
|
||||
stale-issue-label: stale
|
||||
stale-pr-label: stale
|
||||
exempt-issue-labels: enhancement,maintainer,pinned,security,no-stale,bad-barnacle
|
||||
exempt-issue-labels: enhancement,maintainer,pinned,security,no-stale,bad-barnacle,clawsweeper:queueable-fix,clawsweeper:source-repro,clawsweeper:fix-shape-clear
|
||||
exempt-pr-labels: maintainer,no-stale,bad-barnacle
|
||||
operations-per-run: 2000
|
||||
ascending: true
|
||||
@@ -203,7 +203,7 @@ jobs:
|
||||
days-before-pr-stale: -1
|
||||
days-before-pr-close: -1
|
||||
stale-issue-label: stale
|
||||
exempt-issue-labels: enhancement,maintainer,pinned,security,no-stale,bad-barnacle
|
||||
exempt-issue-labels: enhancement,maintainer,pinned,security,no-stale,bad-barnacle,clawsweeper:queueable-fix,clawsweeper:source-repro,clawsweeper:fix-shape-clear
|
||||
operations-per-run: 2000
|
||||
ascending: true
|
||||
include-only-assigned: true
|
||||
@@ -277,6 +277,9 @@ jobs:
|
||||
"security",
|
||||
"no-stale",
|
||||
"bad-barnacle",
|
||||
"clawsweeper:queueable-fix",
|
||||
"clawsweeper:source-repro",
|
||||
"clawsweeper:fix-shape-clear",
|
||||
]);
|
||||
const prExemptLabels = new Set(["maintainer", "no-stale", "bad-barnacle"]);
|
||||
const maintainerAssociations = new Set(["OWNER", "MEMBER", "COLLABORATOR"]);
|
||||
|
||||
223
.github/workflows/windows-node-release.yml
vendored
223
.github/workflows/windows-node-release.yml
vendored
@@ -8,9 +8,12 @@ on:
|
||||
required: true
|
||||
type: string
|
||||
windows_node_tag:
|
||||
description: openclaw-windows-node release tag to promote, or latest
|
||||
description: Exact openclaw-windows-node release tag to promote, for example v0.6.3
|
||||
required: true
|
||||
type: string
|
||||
expected_installer_digests:
|
||||
description: Compact JSON map of installer asset names to pinned source sha256 digests
|
||||
required: true
|
||||
default: latest
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
@@ -31,46 +34,129 @@ jobs:
|
||||
env:
|
||||
RELEASE_TAG: ${{ inputs.tag }}
|
||||
WINDOWS_NODE_TAG: ${{ inputs.windows_node_tag }}
|
||||
EXPECTED_INSTALLER_DIGESTS: ${{ inputs.expected_installer_digests }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
if ($env:RELEASE_TAG -notmatch '^v[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*((-(alpha|beta)\.[1-9][0-9]*)|(-[1-9][0-9]*))?$') {
|
||||
throw "Invalid OpenClaw release tag: $env:RELEASE_TAG"
|
||||
}
|
||||
if ($env:WINDOWS_NODE_TAG -ne "latest" -and $env:WINDOWS_NODE_TAG -notmatch '^v[0-9]+\.[0-9]+\.[0-9]+([-.][0-9A-Za-z.-]+)?$') {
|
||||
throw "Invalid openclaw-windows-node release tag: $env:WINDOWS_NODE_TAG"
|
||||
$stableRelease = -not (
|
||||
$env:RELEASE_TAG.Contains("-alpha.") -or
|
||||
$env:RELEASE_TAG.Contains("-beta.")
|
||||
)
|
||||
if ($env:WINDOWS_NODE_TAG -notmatch '^v[0-9]+\.[0-9]+\.[0-9]+([-.][0-9A-Za-z]+([.-][0-9A-Za-z]+)*)?$') {
|
||||
throw "windows_node_tag must be an explicit openclaw-windows-node release tag, not latest: $env:WINDOWS_NODE_TAG"
|
||||
}
|
||||
|
||||
try {
|
||||
$expectedDigests = $env:EXPECTED_INSTALLER_DIGESTS | ConvertFrom-Json -AsHashtable
|
||||
} catch {
|
||||
throw "expected_installer_digests must be a JSON object: $_"
|
||||
}
|
||||
# Add future signed installer names, such as MSIX x64/ARM64, here.
|
||||
$requiredInstallerNames = @(
|
||||
"OpenClawCompanion-Setup-x64.exe",
|
||||
"OpenClawCompanion-Setup-arm64.exe"
|
||||
)
|
||||
$allowedTargetCompanionAssetNames = @(
|
||||
$requiredInstallerNames
|
||||
"OpenClawCompanion-SHA256SUMS.txt"
|
||||
)
|
||||
if ($expectedDigests.Count -ne $requiredInstallerNames.Count) {
|
||||
throw "expected_installer_digests must contain exactly the current installer asset contract."
|
||||
}
|
||||
foreach ($name in $requiredInstallerNames) {
|
||||
$digest = [string]$expectedDigests[$name]
|
||||
if ($digest -notmatch '^sha256:[A-Fa-f0-9]{64}$') {
|
||||
throw "expected_installer_digests is missing a valid pinned digest for $name."
|
||||
}
|
||||
}
|
||||
|
||||
$targetRelease = gh release view $env:RELEASE_TAG --repo $env:GITHUB_REPOSITORY --json tagName,isDraft,isPrerelease,assets,url | ConvertFrom-Json
|
||||
if ($targetRelease.tagName -ne $env:RELEASE_TAG) {
|
||||
throw "OpenClaw release tag mismatch: expected $env:RELEASE_TAG, got $($targetRelease.tagName)"
|
||||
}
|
||||
$unexpectedTargetCompanionAssets = @(
|
||||
$targetRelease.assets |
|
||||
Where-Object {
|
||||
$_.name.StartsWith("OpenClawCompanion-") -and
|
||||
$_.name -notin $allowedTargetCompanionAssetNames
|
||||
} |
|
||||
ForEach-Object name |
|
||||
Sort-Object
|
||||
)
|
||||
if ($unexpectedTargetCompanionAssets.Count -ne 0) {
|
||||
throw "Target OpenClaw release contains unexpected OpenClawCompanion assets before upload: $($unexpectedTargetCompanionAssets -join ', ')"
|
||||
}
|
||||
|
||||
$sourceRelease = gh release view $env:WINDOWS_NODE_TAG --repo openclaw/openclaw-windows-node --json tagName,isDraft,isPrerelease,assets,url | ConvertFrom-Json
|
||||
if ($sourceRelease.tagName -ne $env:WINDOWS_NODE_TAG) {
|
||||
throw "Windows source release tag mismatch: expected $env:WINDOWS_NODE_TAG, got $($sourceRelease.tagName)"
|
||||
}
|
||||
if ($sourceRelease.isDraft) {
|
||||
throw "Windows source release must be published: $($sourceRelease.url)"
|
||||
}
|
||||
if ($stableRelease -and $sourceRelease.isPrerelease) {
|
||||
throw "Stable OpenClaw releases require a non-prerelease Windows source release: $($sourceRelease.url)"
|
||||
}
|
||||
foreach ($name in $requiredInstallerNames) {
|
||||
$sourceAssets = @($sourceRelease.assets | Where-Object name -eq $name)
|
||||
if ($sourceAssets.Count -ne 1) {
|
||||
throw "Windows source release must contain exactly one required asset $name; found $($sourceAssets.Count)."
|
||||
}
|
||||
if ([string]$sourceAssets[0].digest -ne [string]$expectedDigests[$name]) {
|
||||
throw "Windows source release asset digest does not match the pinned digest: $name"
|
||||
}
|
||||
}
|
||||
gh release view $env:RELEASE_TAG --repo $env:GITHUB_REPOSITORY | Out-Null
|
||||
|
||||
- name: Download Windows Hub release installers
|
||||
shell: pwsh
|
||||
env:
|
||||
WINDOWS_NODE_TAG: ${{ inputs.windows_node_tag }}
|
||||
EXPECTED_INSTALLER_DIGESTS: ${{ inputs.expected_installer_digests }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
New-Item -ItemType Directory -Force -Path dist | Out-Null
|
||||
$tagArgs = @()
|
||||
if ($env:WINDOWS_NODE_TAG -ne "latest") {
|
||||
$tagArgs += $env:WINDOWS_NODE_TAG
|
||||
}
|
||||
gh release download @tagArgs `
|
||||
--repo openclaw/openclaw-windows-node `
|
||||
--pattern "OpenClawCompanion-Setup-*.exe" `
|
||||
--dir dist
|
||||
|
||||
$expected = @(
|
||||
"dist/OpenClawCompanion-Setup-x64.exe",
|
||||
"dist/OpenClawCompanion-Setup-arm64.exe"
|
||||
# Add future signed installer patterns, such as MSIX x64/ARM64, here.
|
||||
# Every matched installer is signature-checked, checksummed, and promoted.
|
||||
$installerPatterns = @(
|
||||
"OpenClawCompanion-Setup-x64.exe",
|
||||
"OpenClawCompanion-Setup-arm64.exe"
|
||||
)
|
||||
foreach ($file in $expected) {
|
||||
if (-not (Test-Path -LiteralPath $file)) {
|
||||
throw "Missing expected Windows installer: $file"
|
||||
$downloadArgs = @(
|
||||
$env:WINDOWS_NODE_TAG,
|
||||
"--repo", "openclaw/openclaw-windows-node",
|
||||
"--dir", "dist"
|
||||
)
|
||||
foreach ($pattern in $installerPatterns) {
|
||||
$downloadArgs += @("--pattern", $pattern)
|
||||
}
|
||||
gh release download @downloadArgs
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Failed to download Windows release assets from $env:WINDOWS_NODE_TAG."
|
||||
}
|
||||
|
||||
foreach ($pattern in $installerPatterns) {
|
||||
$patternMatches = @(Get-ChildItem -LiteralPath dist -File | Where-Object Name -Like $pattern)
|
||||
if ($patternMatches.Count -ne 1) {
|
||||
throw "Expected exactly one Windows installer matching '$pattern', found $($patternMatches.Count)."
|
||||
}
|
||||
}
|
||||
|
||||
$expectedDigests = $env:EXPECTED_INSTALLER_DIGESTS | ConvertFrom-Json -AsHashtable
|
||||
foreach ($file in Get-ChildItem -LiteralPath dist -File) {
|
||||
$expectedHash = ([string]$expectedDigests[$file.Name]) -replace '^sha256:', ''
|
||||
$actualHash = (Get-FileHash -Algorithm SHA256 -LiteralPath $file.FullName).Hash
|
||||
if ($actualHash -ne $expectedHash) {
|
||||
throw "Downloaded Windows source asset does not match pinned digest: $($file.Name)"
|
||||
}
|
||||
}
|
||||
|
||||
- name: Verify Authenticode signatures
|
||||
shell: pwsh
|
||||
run: |
|
||||
Get-ChildItem -LiteralPath dist -Filter "OpenClawCompanion-Setup-*.exe" | ForEach-Object {
|
||||
$expectedSignerSubject = "CN=OpenClaw Foundation, O=OpenClaw Foundation, L=Mill Valley, S=California, C=US"
|
||||
Get-ChildItem -LiteralPath dist -File | ForEach-Object {
|
||||
$signature = Get-AuthenticodeSignature -LiteralPath $_.FullName
|
||||
if ($signature.Status -ne "Valid") {
|
||||
throw "$($_.Name) Authenticode signature was $($signature.Status)."
|
||||
@@ -78,6 +164,9 @@ jobs:
|
||||
if (-not $signature.SignerCertificate) {
|
||||
throw "$($_.Name) has no signer certificate."
|
||||
}
|
||||
if ($signature.SignerCertificate.Subject -ne $expectedSignerSubject) {
|
||||
throw "$($_.Name) has unexpected signer subject $($signature.SignerCertificate.Subject)."
|
||||
}
|
||||
[pscustomobject]@{
|
||||
File = $_.Name
|
||||
Signer = $signature.SignerCertificate.Subject
|
||||
@@ -88,7 +177,7 @@ jobs:
|
||||
- name: Write SHA-256 manifest
|
||||
shell: pwsh
|
||||
run: |
|
||||
Get-ChildItem -LiteralPath dist -Filter "OpenClawCompanion-Setup-*.exe" |
|
||||
Get-ChildItem -LiteralPath dist -File |
|
||||
Sort-Object Name |
|
||||
ForEach-Object {
|
||||
$hash = Get-FileHash -Algorithm SHA256 -LiteralPath $_.FullName
|
||||
@@ -101,12 +190,81 @@ jobs:
|
||||
RELEASE_TAG: ${{ inputs.tag }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
gh release upload $env:RELEASE_TAG `
|
||||
dist/OpenClawCompanion-Setup-x64.exe `
|
||||
dist/OpenClawCompanion-Setup-arm64.exe `
|
||||
dist/OpenClawCompanion-SHA256SUMS.txt `
|
||||
--repo $env:GITHUB_REPOSITORY `
|
||||
--clobber
|
||||
$releaseAssets = @(Get-ChildItem -LiteralPath dist -File | Sort-Object Name | ForEach-Object FullName)
|
||||
gh release upload $env:RELEASE_TAG @releaseAssets --repo $env:GITHUB_REPOSITORY --clobber
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Failed to upload Windows release assets to $env:RELEASE_TAG."
|
||||
}
|
||||
|
||||
- name: Verify promoted release asset contract
|
||||
shell: pwsh
|
||||
env:
|
||||
RELEASE_TAG: ${{ inputs.tag }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
New-Item -ItemType Directory -Force -Path verified | Out-Null
|
||||
$expectedAssets = @(Get-ChildItem -LiteralPath dist -File | Sort-Object Name)
|
||||
$expectedCompanionAssetNames = @($expectedAssets | ForEach-Object Name | Sort-Object)
|
||||
$targetRelease = gh release view $env:RELEASE_TAG --repo $env:GITHUB_REPOSITORY --json assets | ConvertFrom-Json
|
||||
$actualCompanionAssetNames = @(
|
||||
$targetRelease.assets |
|
||||
Where-Object { $_.name.StartsWith("OpenClawCompanion-") } |
|
||||
ForEach-Object name |
|
||||
Sort-Object
|
||||
)
|
||||
$assetContractDiff = @(
|
||||
Compare-Object `
|
||||
-ReferenceObject $expectedCompanionAssetNames `
|
||||
-DifferenceObject $actualCompanionAssetNames
|
||||
)
|
||||
if (
|
||||
$actualCompanionAssetNames.Count -ne $expectedCompanionAssetNames.Count -or
|
||||
$assetContractDiff.Count -ne 0
|
||||
) {
|
||||
throw "Promoted OpenClawCompanion asset names do not exactly match the current contract."
|
||||
}
|
||||
|
||||
foreach ($asset in $expectedAssets) {
|
||||
gh release download $env:RELEASE_TAG `
|
||||
--repo $env:GITHUB_REPOSITORY `
|
||||
--pattern $asset.Name `
|
||||
--dir verified
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Failed to download promoted Windows release asset $($asset.Name)."
|
||||
}
|
||||
}
|
||||
|
||||
$manifestPath = "verified/OpenClawCompanion-SHA256SUMS.txt"
|
||||
$manifestEntries = @(Get-Content -LiteralPath $manifestPath | ForEach-Object {
|
||||
if ($_ -notmatch '^([A-Fa-f0-9]{64}) ([^\\/]+)$') {
|
||||
throw "Invalid Windows SHA-256 manifest entry: $_"
|
||||
}
|
||||
[PSCustomObject]@{
|
||||
Hash = $Matches[1]
|
||||
Name = $Matches[2]
|
||||
}
|
||||
})
|
||||
$expectedInstallerNames = @(
|
||||
$expectedAssets |
|
||||
Where-Object Name -ne "OpenClawCompanion-SHA256SUMS.txt" |
|
||||
ForEach-Object Name
|
||||
)
|
||||
$manifestInstallerNames = @($manifestEntries | ForEach-Object Name | Sort-Object)
|
||||
$contractDiff = @(
|
||||
Compare-Object `
|
||||
-ReferenceObject $expectedInstallerNames `
|
||||
-DifferenceObject $manifestInstallerNames
|
||||
)
|
||||
if ($contractDiff.Count -ne 0) {
|
||||
throw "Promoted Windows SHA-256 manifest does not match the installer asset contract."
|
||||
}
|
||||
|
||||
foreach ($entry in $manifestEntries) {
|
||||
$hash = (Get-FileHash -Algorithm SHA256 -LiteralPath "verified/$($entry.Name)").Hash
|
||||
if ($hash -ne $entry.Hash) {
|
||||
throw "Promoted Windows release asset checksum mismatch: $($entry.Name)"
|
||||
}
|
||||
}
|
||||
|
||||
- name: Summary
|
||||
shell: pwsh
|
||||
@@ -119,8 +277,9 @@ jobs:
|
||||
|
||||
OpenClaw release: $env:RELEASE_TAG
|
||||
Source release: openclaw/openclaw-windows-node@$env:WINDOWS_NODE_TAG
|
||||
|
||||
- https://github.com/openclaw/openclaw/releases/download/$env:RELEASE_TAG/OpenClawCompanion-Setup-x64.exe
|
||||
- https://github.com/openclaw/openclaw/releases/download/$env:RELEASE_TAG/OpenClawCompanion-Setup-arm64.exe
|
||||
- https://github.com/openclaw/openclaw/releases/download/$env:RELEASE_TAG/OpenClawCompanion-SHA256SUMS.txt
|
||||
"@ >> $env:GITHUB_STEP_SUMMARY
|
||||
Get-ChildItem -LiteralPath dist -File |
|
||||
Sort-Object Name |
|
||||
ForEach-Object {
|
||||
"- https://github.com/openclaw/openclaw/releases/download/$env:RELEASE_TAG/$($_.Name)"
|
||||
} >> $env:GITHUB_STEP_SUMMARY
|
||||
|
||||
41
CHANGELOG.md
41
CHANGELOG.md
@@ -2,6 +2,35 @@
|
||||
|
||||
Docs: https://docs.openclaw.ai
|
||||
|
||||
## 2026.6.8
|
||||
|
||||
### Highlights
|
||||
|
||||
- Telegram and WhatsApp channel delivery are richer and less brittle: Telegram can send structured rich text with tables, lists, expandable blockquotes, prompt-preserving CLI backend delivery, retired native draft migration, and safer rich-media boundaries, while WhatsApp now honors configured ACP bindings. (#92679, #84082, #89421, #92513) Thanks @obviyus, @jzakirov, @spacegeologist, and @TurboTheTurtle.
|
||||
- Agent and Gateway recovery is sharper across account-scoped DM sends, generated media completions, restart shutdown aborts, yielded subagent pauses, yielded cron media, heartbeat dedupe, session identity prompts, and unknown OpenAI agent selector rejection. (#92788, #91246, #91357, #92631, #92146, #91287, #92468, #92510) Thanks @yetval, @TurboTheTurtle, @ooiuuii, @openperf, @IWhatsskill, @ZengWen-DT, and @zhangguiping-xydt.
|
||||
- Provider/model handling expands and tightens with GLM-5.2, Claude Haiku 4.5 catalog rows, OpenRouter and Google Vertex provider-prefix normalization, managed SecretRef auth, bounded model browse discovery, storeless OpenAI Responses replay gating, and Claude 4.5 Copilot tool-streaming safety. (#92796, #90116, #92627, #91218, #90686, #92247, #90706, #75393) Thanks @arkyu2077, @liuhao1024, @bymle, @rohitjavvadi, @samson910022, @snowzlm, and @Kailigithub.
|
||||
- `/usage` and reply payload hooks now have a native full footer renderer, default template, fixed-decimal formatting, credential-aware limits, better partial-count handling, and warnings for broken templates instead of silent bad output. (#92657, #89835, #89629) Thanks @Marvinthebored.
|
||||
- UI and mobile flows are steadier: workspace files can collapse and start collapsed, WebChat backscroll survives streaming, the sidebar session picker remains interactive above the desktop workbench, reset soft args survive UI dispatch, stale dashboard session parent lineage is preserved, and iOS reconnects stale foreground gateways. (#92779, #92622, #92705, #91353, #90658, #92552) Thanks @shakkernerd, @TurboTheTurtle, @NianJiuZst, @zhouhe-xydt, @luoyanglang, and @Solvely-Colin.
|
||||
- Memory, state, and diagnostics recover cleaner: oversized OpenAI embedding batches split before 431s, QMD memory search stays available in transient mode, SQLite avoids WAL on NFS state volumes, stuck-session recovery scheduling no longer resets warning backoff, and Infinity chunk limits stay genuinely unbounded. (#92650, #92618, #92639, #91247, #92752, #92735) Thanks @mushuiyu886, @TurboTheTurtle, @849261680, @gnanam1990, and @yhterrance.
|
||||
|
||||
### Changes
|
||||
|
||||
- Providers/models: add GLM-5.2 support and Claude Haiku 4.5 catalog entries while keeping provider-qualified model IDs normalized across OpenRouter and Google Vertex paths. (#92796, #90116, #92627, #91218) Thanks @arkyu2077, @liuhao1024, and @bymle.
|
||||
- Channel plugins: ship Telegram rich-message delivery and WhatsApp ACP binding support, including rich prompt handoff to CLI backends and transport fixtures for richer drafts. (#92679, #92513) Thanks @obviyus and @TurboTheTurtle.
|
||||
- Agent commands: support `/btw` in CLI-backed sessions and keep CLI usage-error exits classified as usage failures instead of successful runs. (#92669, #92162) Thanks @joshavant and @Pandah97.
|
||||
- Usage hooks: add built-in full footer rendering, default footer templates, per-turn usage state, credential-aware limits, and fixed-decimal formatting for usage-bar templates. (#92657, #89835, #89629) Thanks @Marvinthebored.
|
||||
- Docs and operator guidance: document node config examples, clarify before-install hook scope, correct agent default concurrency comments, refresh ZAI provider docs, and update channel/group docs for current Telegram and WhatsApp behavior. (#92677, #92766, #92695) Thanks @liuhao1024, @sallyom, and @ArielSmoliar.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Channels and delivery: preserve account-scoped DM channel send policy, rich Telegram final replies, rich Telegram tables and lists, Telegram thread-create CLI remapping, Slack outbound `message_sent` hooks, contributed message-tool schema optionality, same-channel generated media completions, and channel chunking around surrogate pairs and Infinity limits. (#92788, #92679, #89421, #89943, #91137, #91246, #92735) Thanks @yetval, @obviyus, @spacegeologist, @rishitamrakar, @lundog, @TurboTheTurtle, and @yhterrance.
|
||||
- Discord: give generated auto-thread titles a 60-second timeout and 4,096-token reasoning-model output budget, clamped to the selected model output cap. (#64734) Thanks @hanamizuki.
|
||||
- Agent, cron, and Gateway runtime: mark active main sessions before restart shutdown aborts, pause yielded subagent runs whose terminal also signals abort, preserve yielded media completions, de-duplicate main-session heartbeat events, expose session identity in runtime prompts, reject unknown OpenAI agent selectors, keep generated media completions and slash-command block replies in WebChat, and require admin privileges for HTTP session/model override surfaces. (#91357, #92631, #92146, #91287, #92468, #92510, #91246, #92651, #92646) Thanks @ooiuuii, @openperf, @IWhatsskill, @ZengWen-DT, @zhangguiping-xydt, and @TurboTheTurtle.
|
||||
- Providers and model replay: preserve storeless OpenAI Responses replay compatibility, avoid eager tool streaming for Claude 4.5 in Copilot, honor profile auth for SecretRef model entries, bound model browsing, strip provider prefixes where runtimes need bare IDs, and surface nested embedding fetch failures. (#90706, #75393, #90686, #92247, #92627, #91218, #92628) Thanks @snowzlm, @Kailigithub, @rohitjavvadi, @samson910022, @liuhao1024, @bymle, and @mushuiyu886.
|
||||
- Memory, state, diagnostics, and config: split header-too-large embedding batches, keep QMD memory search enabled in transient mode, avoid SQLite WAL on NFS volumes, preserve recovery scheduling outside stuck-session warning backoff, and keep shell environment fallbacks contained in config write tests. (#92650, #92618, #92639, #91247, #92752) Thanks @mushuiyu886, @TurboTheTurtle, @849261680, and @gnanam1990.
|
||||
- UI/mobile/TUI: preserve dashboard session parent lineage, WebChat backscroll, reset soft command args, sidebar session picker interactivity, collapsed workspace files, resolved `/model` confirmation refs, and stale foreground iOS Gateway reconnects. (#90658, #92622, #91353, #92705, #92779, #92773, #92552) Thanks @luoyanglang, @TurboTheTurtle, @zhouhe-xydt, @NianJiuZst, @shakkernerd, @NarahariRaghava, and @Solvely-Colin.
|
||||
- Release and test reliability: extend slow Gateway/full-suite watchdogs, split local full-suite shards when throttled, stabilize plugin auth marker fixtures, avoid brittle provider-ref error text, and keep QA Lab bootstrap selection assertions aligned with flow-only scenarios. (#92652)
|
||||
|
||||
## 2026.6.6
|
||||
|
||||
### Highlights
|
||||
@@ -20,17 +49,17 @@ Docs: https://docs.openclaw.ai
|
||||
- Plugins/ClawHub: dogfood reusable package publishing, let dry runs skip publish approval, allow declared installed trusted hooks, report managed plugin version drift, and warn instead of failing on retired Skill Workshop configuration. (#91574, #91591, #90004, #90927, #90838) Thanks @Patrick-Erichsen, @brokemac79, and @lonexreb.
|
||||
- Memory/providers: move the local llama.cpp runtime into its provider plugin, batch embeddings across files, persist the agent model catalog cache, and keep QMD JSON search one-shot while filtering stale REM recall previews. (#91324, #89138, #90457, #91837, #91851) Thanks @osolmaz, @mushuiyu886, @ai-hpc, and @TurboTheTurtle.
|
||||
- Channels/mobile: add the QQBot group mention toggle, improve iPad and iPhone control surfaces, and expose the active connection host in the TUI footer. (#91423, #91557, #89909) Thanks @cxyhhhhh, @Solvely-Colin, and @baskduf.
|
||||
- Performance: prewarm TUI runtime plugins, deduplicate plugin auto-enable fanout, stop `/models` derived-registry rescan storms, trim dense text-delta snapshots, and reuse prepared startup model metadata. (#90782, #89978, #92127, #91580, #91531) Thanks @RomneyDa, @obuchowski, and @ai-hpc.
|
||||
- Performance: prewarm TUI runtime plugins, deduplicate plugin auto-enable fanout, trim dense text-delta snapshots, and reuse prepared startup model metadata. (#90782, #89978, #91580, #91531) Thanks @RomneyDa and @ai-hpc.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Agent/session recovery: drop stale approval follow-ups after session rebind, remove drained reply-queue items by identity, recover stale main and visible replies, preserve Codex context-engine compaction ownership, project thinking catalog compatibility through SDK sessions, retry same-model assistant calls across short rate-limit windows, lower the default compaction timeout to 180 seconds while respecting explicit configuration, and keep provider-failure terminal lifecycle state correct. (#85679, #91450, #91566, #91840, #91590, #91911, #91361, #91895) Thanks @openperf, @yetval, @joshavant, @lanzhi-lee, @wangmiao0668000666, and @TurboTheTurtle.
|
||||
- Agent/session recovery: drop stale approval follow-ups after session rebind, remove drained reply-queue items by identity, recover stale main and visible replies, preserve Codex context-engine compaction ownership, lower the default compaction timeout to 180 seconds while respecting explicit configuration, and keep provider-failure terminal lifecycle state correct. (#85679, #91450, #91566, #91840, #91590, #91361, #91895) Thanks @openperf, @yetval, @joshavant, @wangmiao0668000666, and @TurboTheTurtle.
|
||||
- User-visible content boundaries: suppress Codex/Harmony protocol artifacts, neutralize browser and LanceDB memory media directives, redact transcript images, and preserve native `/compact` replies through source suppression. (#89151, #91422, #91425, #91529, #90212) Thanks @joelnishanth, @pgondhi987, @joshavant, and @snowzlm.
|
||||
- Channel delivery: keep WhatsApp captured replies attached to the successor controller after restart, retry Feishu rate limits, preserve Mattermost thread replies, canonicalize LINE webhook paths, restore Discord reply hydration and runtime timeout exports, and show OpenAI Realtime WebRTC assistant transcripts. (#85823, #89659, #91684, #91649, #90263, #91686, #90426) Thanks @itsuzef, @ladygege, @jacobtomlinson, @fuller-stack-dev, and @shushushv.
|
||||
- Cron: cancel active task runs cleanly, preserve terminal timeout/cancel state, and recover no-deliver tool warnings instead of silently losing the outcome. (#90666, #90678) Thanks @ai-hpc.
|
||||
- Gateway/config/auth: share the approval runtime socket token, replace arrays explicitly in `config.patch`, keep indexed `replacePaths` consent from widening to whole arrays, reject malformed Gateway RPC timeout inputs, skip the deleted-agent guard only for valid ACP harness sessions, surface headless LaunchAgent state, verify SQLite auth migration before cleanup, and arm QMD startup maintenance. (#87105, #91551, #91966, #54646, #40953, #91219, #91614, #91740, #91978) Thanks @fuller-stack-dev, @yetval, @ruanrrn, @comeran, and @scotthuang.
|
||||
- Providers/Codex: clarify quota errors, restore the Codex synthetic usage line, canonicalize Codex protocol assets, require API-key auth for realtime voice, normalize ACP model refs, preserve Gemma 4 `reasoning_content`, honor Ollama's provider-declared thinking default in SDK sessions, and avoid guardian review for local models. (#91390, #91709, #91507, #91567, #88630, #91657, #91696) Thanks @hxy91819, @brokemac79, @RomneyDa, @joshavant, @openperf, and @Coder-Wangyankun.
|
||||
- Updates/builds: recover package Gateway restarts after refresh failure, expose plugin convergence repair, fall back to Corepack in PATH-less pnpm environments, seed the correct Docker store packages, keep ClawHub dry-run and publish paths reusable, and keep beta GitHub release pages draft until OpenClaw npm, dependency evidence, postpublish verification, and required plugin publishes pass. (#91581, #91599, #91547, #91591) Thanks @fuller-stack-dev, @sallyom, and @Patrick-Erichsen.
|
||||
- Gateway/config/auth: share the approval runtime socket token, replace arrays explicitly in `config.patch`, skip the deleted-agent guard only for valid ACP harness sessions, surface headless LaunchAgent state, verify SQLite auth migration before cleanup, and arm QMD startup maintenance. (#87105, #91551, #91219, #91614, #91740, #91978) Thanks @fuller-stack-dev and @scotthuang.
|
||||
- Providers/Codex: clarify quota errors, restore the Codex synthetic usage line, canonicalize Codex protocol assets, require API-key auth for realtime voice, normalize ACP model refs, preserve Gemma 4 `reasoning_content`, and avoid guardian review for local models. (#91390, #91709, #91507, #91567, #88630, #91696) Thanks @hxy91819, @brokemac79, @RomneyDa, @joshavant, and @Coder-Wangyankun.
|
||||
- Updates/builds: recover package Gateway restarts after refresh failure, expose plugin convergence repair, fall back to Corepack in PATH-less pnpm environments, seed the correct Docker store packages, and keep ClawHub dry-run and publish paths reusable. (#91581, #91599, #91547, #91591) Thanks @fuller-stack-dev, @sallyom, and @Patrick-Erichsen.
|
||||
- UI: require explicit user intent before opening chat sessions and drain restored chat queues after session switches. (#91480) Thanks @TurboTheTurtle.
|
||||
- Android: avoid the `dataSync` foreground-service type for persistent nodes. (#80082) Thanks @davelutztx.
|
||||
- Native hooks: bound relay lifetimes so abandoned native hook connections cannot linger indefinitely. (#91550) Thanks @joshavant.
|
||||
@@ -79,7 +108,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Release/CI/E2E: Docker E2E and live Docker harness runs now apply default memory, CPU, and process ceilings while preserving explicit per-lane overrides.
|
||||
- Release/CI/E2E: plugin lifecycle matrix resource sampling now fails phases that exceed RSS, wall-clock, or CPU ceilings instead of only logging the measurements.
|
||||
- Release/CI/E2E: Codex npm plugin live assertions now cap transcript discovery and diagnostic log reads so failure proof stays bounded.
|
||||
- Memory: keep doctor REM harness previews aligned with live REM by dropping short-term recall snippets whose source files disappeared before rendering preview output, retry dreaming narrative message reads after delayed assistant-text persistence, and abort orphaned `memory_search` embedding work after tool timeout. (#89091, #91742) Thanks @samzong, @frankekn, @bennewell35, and @dreamhunter2333.
|
||||
- Memory: keep doctor REM harness previews aligned with live REM by dropping short-term recall snippets whose source files disappeared before rendering preview output. Thanks @samzong and @frankekn.
|
||||
- Tests/state isolation: QA Lab valid-tool-call metrics now require runtime tool-call evidence when runtime parity data is available instead of counting tool-backed scenario pass status alone.
|
||||
- Tests/state isolation: QA Lab runtime parity now fails planned-only tool-call rows without matching tool results instead of treating matching mock plans as real tool evidence.
|
||||
- Tests/state isolation: provider, media, auth, cron, task, session, sandbox, Gateway, and Codex timeout fixtures now scope more home/state/env data per test, reducing cross-test leakage and making release validation failures less noisy. (#90027, #89974)
|
||||
|
||||
16
Dockerfile
16
Dockerfile
@@ -116,11 +116,19 @@ RUN pnpm_config_verify_deps_before_run=false pnpm canvas:a2ui:bundle || \
|
||||
echo "/* A2UI bundle unavailable in this build */" > extensions/canvas/src/host/a2ui/a2ui.bundle.js && \
|
||||
echo "stub" > extensions/canvas/src/host/a2ui/.bundle.hash && \
|
||||
rm -rf vendor/a2ui apps/shared/OpenClawKit/Tools/CanvasA2UI)
|
||||
RUN NODE_OPTIONS=--max-old-space-size=8192 pnpm_config_verify_deps_before_run=false pnpm build:docker
|
||||
RUN if printf '%s\n' "$OPENCLAW_EXTENSIONS" | tr ',' ' ' | tr ' ' '\n' | grep -qx 'qa-lab'; then \
|
||||
export OPENCLAW_BUILD_PRIVATE_QA=1 OPENCLAW_ENABLE_PRIVATE_QA_CLI=1; \
|
||||
fi && \
|
||||
NODE_OPTIONS=--max-old-space-size=8192 pnpm_config_verify_deps_before_run=false pnpm build:docker
|
||||
# Force pnpm for UI build (Bun may fail on ARM/Synology architectures)
|
||||
ENV OPENCLAW_PREFER_PNPM=1
|
||||
RUN pnpm_config_verify_deps_before_run=false pnpm ui:build
|
||||
RUN pnpm_config_verify_deps_before_run=false pnpm qa:lab:build
|
||||
RUN if printf '%s\n' "$OPENCLAW_EXTENSIONS" | tr ',' ' ' | tr ' ' '\n' | grep -qx 'qa-lab'; then \
|
||||
pnpm_config_verify_deps_before_run=false pnpm qa:lab:build && \
|
||||
mkdir -p dist/extensions/qa-lab/web && \
|
||||
rm -rf dist/extensions/qa-lab/web/dist && \
|
||||
cp -R extensions/qa-lab/web/dist dist/extensions/qa-lab/web/dist; \
|
||||
fi
|
||||
|
||||
# Prune dev dependencies, omitted plugin runtime packages, and build-only
|
||||
# metadata before copying runtime assets into the final image.
|
||||
@@ -139,6 +147,10 @@ RUN --mount=type=cache,id=openclaw-pnpm-store,target=/root/.local/share/pnpm/sto
|
||||
OPENCLAW_EXTENSIONS="$OPENCLAW_EXTENSIONS" OPENCLAW_BUNDLED_PLUGIN_DIR="$OPENCLAW_BUNDLED_PLUGIN_DIR" node scripts/prune-docker-plugin-dist.mjs && \
|
||||
node scripts/postinstall-bundled-plugins.mjs && \
|
||||
find dist -type f \( -name '*.d.ts' -o -name '*.d.mts' -o -name '*.d.cts' -o -name '*.map' \) -delete && \
|
||||
rm -rf \
|
||||
/app/node_modules/openclaw \
|
||||
/app/node_modules/.bin/openclaw \
|
||||
/app/node_modules/.pnpm/openclaw@*/node_modules/openclaw && \
|
||||
node scripts/check-package-dist-imports.mjs /app
|
||||
|
||||
# ── Runtime base image ──────────────────────────────────────────
|
||||
|
||||
@@ -65,8 +65,8 @@ android {
|
||||
applicationId = "ai.openclaw.app"
|
||||
minSdk = 31
|
||||
targetSdk = 36
|
||||
versionCode = 2026060601
|
||||
versionName = "2026.6.6"
|
||||
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")
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
// Source of truth: apps/ios/version.json
|
||||
// Generated by scripts/ios-sync-versioning.ts.
|
||||
|
||||
OPENCLAW_IOS_VERSION = 2026.6.6
|
||||
OPENCLAW_MARKETING_VERSION = 2026.6.6
|
||||
OPENCLAW_IOS_VERSION = 2026.6.2
|
||||
OPENCLAW_MARKETING_VERSION = 2026.6.2
|
||||
OPENCLAW_BUILD_VERSION = 1
|
||||
|
||||
#include? "../build/Version.xcconfig"
|
||||
|
||||
@@ -188,6 +188,7 @@ final class NodeAppModel {
|
||||
@ObservationIgnored private var backgroundGraceTaskTimer: Task<Void, Never>?
|
||||
private var backgroundReconnectSuppressed = false
|
||||
private var backgroundReconnectLeaseUntil: Date?
|
||||
@ObservationIgnored private var foregroundGatewayResumeCheckInFlight = false
|
||||
private var lastSignificantLocationWakeAt: Date?
|
||||
@ObservationIgnored private let watchReplyCoordinator = WatchReplyCoordinator()
|
||||
private var watchExecApprovalPromptsByID: [String: ExecApprovalPrompt] = [:]
|
||||
@@ -214,6 +215,7 @@ final class NodeAppModel {
|
||||
private static let watchExecApprovalBridgeStateKey = "watch.execApproval.bridge.state.v1"
|
||||
private static let backgroundAliveLastSuccessAtMsKey = "gateway.backgroundAlive.lastSuccessAtMs"
|
||||
private static let backgroundAliveLastTriggerKey = "gateway.backgroundAlive.lastTrigger"
|
||||
private static let foregroundResumeHealthTimeoutSeconds = 1
|
||||
|
||||
var cameraHUDText: String?
|
||||
var cameraHUDKind: CameraHUDKind?
|
||||
@@ -417,9 +419,7 @@ final class NodeAppModel {
|
||||
self.isBackgrounded = false
|
||||
self.endBackgroundConnectionGracePeriod(reason: "scene_foreground")
|
||||
self.clearBackgroundReconnectSuppression(reason: "scene_foreground")
|
||||
if self.operatorConnected {
|
||||
self.startGatewayHealthMonitor()
|
||||
}
|
||||
var shouldStartGatewayHealthMonitor = self.operatorConnected
|
||||
if phase == .active {
|
||||
self.voiceWake.resumeAfterExternalAudioCapture(wasSuspended: self.backgroundVoiceWakeSuspended)
|
||||
self.backgroundVoiceWakeSuspended = false
|
||||
@@ -444,6 +444,8 @@ final class NodeAppModel {
|
||||
// iOS may suspend network sockets in background without a clean close.
|
||||
// On foreground, force a fresh handshake to avoid "connected but dead" states.
|
||||
if backgroundedFor >= 3.0 {
|
||||
shouldStartGatewayHealthMonitor = false
|
||||
self.foregroundGatewayResumeCheckInFlight = true
|
||||
Task { [weak self] in
|
||||
guard let self else { return }
|
||||
let operatorWasConnected = await MainActor.run { self.operatorConnected }
|
||||
@@ -452,31 +454,26 @@ final class NodeAppModel {
|
||||
let healthy = await (try? self.operatorGateway.request(
|
||||
method: "health",
|
||||
paramsJSON: nil,
|
||||
timeoutSeconds: 2)) != nil
|
||||
timeoutSeconds: Self.foregroundResumeHealthTimeoutSeconds)) != nil
|
||||
if healthy {
|
||||
await MainActor.run { self.startGatewayHealthMonitor() }
|
||||
await MainActor.run {
|
||||
self.foregroundGatewayResumeCheckInFlight = false
|
||||
self.startGatewayHealthMonitor()
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
await self.operatorGateway.disconnect()
|
||||
await self.nodeGateway.disconnect()
|
||||
await MainActor.run {
|
||||
guard !self.isAppleReviewDemoModeEnabled else { return }
|
||||
self.setOperatorConnected(false)
|
||||
self.gatewayConnected = false
|
||||
// Foreground recovery must actively restart the saved gateway config.
|
||||
// Disconnecting stale sockets alone can leave us idle if the old
|
||||
// reconnect tasks were suppressed or otherwise got stuck in background.
|
||||
self.gatewayStatusText = "Reconnecting…"
|
||||
self.talkMode.updateGatewayConnected(false)
|
||||
if let cfg = self.activeGatewayConnectConfig {
|
||||
self.applyGatewayConnectConfig(cfg)
|
||||
}
|
||||
self.foregroundGatewayResumeCheckInFlight = false
|
||||
}
|
||||
await self.restartGatewaySessionsAfterForegroundStaleConnection()
|
||||
}
|
||||
}
|
||||
}
|
||||
if shouldStartGatewayHealthMonitor {
|
||||
self.startGatewayHealthMonitor()
|
||||
}
|
||||
@unknown default:
|
||||
self.isBackgrounded = false
|
||||
self.endBackgroundConnectionGracePeriod(reason: "scene_unknown")
|
||||
@@ -786,6 +783,12 @@ final class NodeAppModel {
|
||||
|
||||
func refreshGatewayOverviewIfConnected() async {
|
||||
guard await self.isOperatorConnected() else { return }
|
||||
if self.foregroundGatewayResumeCheckInFlight {
|
||||
GatewayDiagnostics.log("gateway overview refresh deferred reason=foreground_resume_check")
|
||||
try? await Task.sleep(
|
||||
nanoseconds: UInt64(Self.foregroundResumeHealthTimeoutSeconds) * 1_000_000_000)
|
||||
guard await self.isOperatorConnected(), !self.foregroundGatewayResumeCheckInFlight else { return }
|
||||
}
|
||||
await self.refreshBrandingFromGateway()
|
||||
await self.refreshAgentsFromGateway()
|
||||
}
|
||||
@@ -1986,12 +1989,33 @@ extension NodeAppModel {
|
||||
}
|
||||
|
||||
func resetGatewaySessionsForForcedReconnect() async {
|
||||
self.nodeGatewayTask?.cancel()
|
||||
let nodeGatewayTask = self.nodeGatewayTask
|
||||
let operatorGatewayTask = self.operatorGatewayTask
|
||||
nodeGatewayTask?.cancel()
|
||||
self.nodeGatewayTask = nil
|
||||
self.operatorGatewayTask?.cancel()
|
||||
operatorGatewayTask?.cancel()
|
||||
self.operatorGatewayTask = nil
|
||||
await self.operatorGateway.disconnect()
|
||||
await self.nodeGateway.disconnect()
|
||||
// Foreground recovery reuses the same config immediately after reset.
|
||||
// Wait for canceled loops so their shutdown cleanup cannot clobber the new reconnect state.
|
||||
if let operatorGatewayTask {
|
||||
await operatorGatewayTask.value
|
||||
}
|
||||
if let nodeGatewayTask {
|
||||
await nodeGatewayTask.value
|
||||
}
|
||||
}
|
||||
|
||||
private func restartGatewaySessionsAfterForegroundStaleConnection() async {
|
||||
await self.resetGatewaySessionsForForcedReconnect()
|
||||
guard !self.isAppleReviewDemoModeEnabled else { return }
|
||||
self.setOperatorConnected(false)
|
||||
self.gatewayConnected = false
|
||||
self.gatewayStatusText = "Reconnecting…"
|
||||
self.talkMode.updateGatewayConnected(false)
|
||||
guard let cfg = self.activeGatewayConnectConfig else { return }
|
||||
self.applyGatewayConnectConfig(cfg, forceReconnect: true)
|
||||
}
|
||||
|
||||
func disconnectGateway() {
|
||||
@@ -4826,6 +4850,10 @@ extension NodeAppModel {
|
||||
(self.nodeGatewayTask != nil, self.operatorGatewayTask != nil)
|
||||
}
|
||||
|
||||
func _test_restartGatewaySessionsAfterForegroundStaleConnection() async {
|
||||
await self.restartGatewaySessionsAfterForegroundStaleConnection()
|
||||
}
|
||||
|
||||
func _test_handleSuccessfulBootstrapGatewayOnboarding() async {
|
||||
await self.handleSuccessfulBootstrapGatewayOnboarding(
|
||||
url: URL(string: "wss://gateway.example")!,
|
||||
|
||||
@@ -356,6 +356,20 @@ import UIKit
|
||||
#expect(!appModel._test_hasGatewayLoopTasks().operator)
|
||||
}
|
||||
|
||||
@Test @MainActor func foregroundStaleConnectionRestartReappliesActiveGatewayConfig() async {
|
||||
let appModel = NodeAppModel()
|
||||
defer { appModel.disconnectGateway() }
|
||||
|
||||
let config = Self.makeGatewayConnectConfig()
|
||||
appModel.applyGatewayConnectConfig(config)
|
||||
await appModel._test_restartGatewaySessionsAfterForegroundStaleConnection()
|
||||
|
||||
#expect(appModel.gatewayStatusText == "Reconnecting…")
|
||||
#expect(appModel.activeGatewayConnectConfig?.hasSameConnectionInputs(as: config) == true)
|
||||
#expect(appModel._test_hasGatewayLoopTasks().node)
|
||||
#expect(appModel._test_hasGatewayLoopTasks().operator)
|
||||
}
|
||||
|
||||
@Test @MainActor func loadLastConnectionReadsSavedValues() {
|
||||
let prior = KeychainStore.loadString(service: "ai.openclaw.gateway", account: "lastConnection")
|
||||
defer {
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"version": "2026.6.6"
|
||||
"version": "2026.6.2"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"originHash" : "035a4fe955164c62c1628de75f6437a14443a947eea2a1b0176ba484d6fde6f8",
|
||||
"originHash" : "ae9f37f50cff0d32d189e60948f61e2fa1704e997a6ef4ad5e37f6a11c165ea4",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "axorcist",
|
||||
@@ -42,8 +42,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/steipete/Peekaboo.git",
|
||||
"state" : {
|
||||
"revision" : "3a56ed2aa769bfefb5a78722dfce3c34088cfba1",
|
||||
"version" : "3.4.0"
|
||||
"revision" : "ee0e3185431788dad533ffca77cd75315aa3d26f",
|
||||
"version" : "3.4.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -51,8 +51,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/sparkle-project/Sparkle",
|
||||
"state" : {
|
||||
"revision" : "6276ba2b404829d139c45ff98427cf90e2efc59b",
|
||||
"version" : "2.9.2"
|
||||
"revision" : "d46d456107feacc80711b21847b82b07bd9fb46e",
|
||||
"version" : "2.9.3"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -78,8 +78,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/apple/swift-log.git",
|
||||
"state" : {
|
||||
"revision" : "2aed77ae5ec9a86d8fe42c12275e4c2653a286ee",
|
||||
"version" : "1.13.1"
|
||||
"revision" : "92448c359f00ebe36ae97d3bd9086f13c7692b5a",
|
||||
"version" : "1.13.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -19,7 +19,7 @@ let package = Package(
|
||||
.package(url: "https://github.com/swiftlang/swift-subprocess.git", from: "0.4.0"),
|
||||
.package(url: "https://github.com/apple/swift-log.git", from: "1.10.1"),
|
||||
.package(url: "https://github.com/sparkle-project/Sparkle", from: "2.9.0"),
|
||||
.package(url: "https://github.com/steipete/Peekaboo.git", exact: "3.4.0"),
|
||||
.package(url: "https://github.com/steipete/Peekaboo.git", exact: "3.4.1"),
|
||||
.package(path: "../shared/OpenClawKit"),
|
||||
.package(path: "../swabble"),
|
||||
],
|
||||
|
||||
@@ -72,7 +72,7 @@ final class CronJobsStore {
|
||||
do {
|
||||
if let status = try? await GatewayConnection.shared.cronStatus() {
|
||||
self.schedulerEnabled = status.enabled
|
||||
self.schedulerStorePath = status.storePath
|
||||
self.schedulerStorePath = status.sqlitePath ?? status.storePath
|
||||
self.schedulerNextWakeAtMs = status.nextWakeAtMs
|
||||
}
|
||||
self.jobs = try await GatewayConnection.shared.cronList(includeDisabled: true)
|
||||
|
||||
@@ -364,12 +364,17 @@ enum ExecApprovalsStore {
|
||||
}
|
||||
close(fd)
|
||||
closed = true
|
||||
let linked = link(tempURL.path, targetURL.path)
|
||||
if linked == -1 {
|
||||
let copied = copyfile(
|
||||
tempURL.path,
|
||||
targetURL.path,
|
||||
nil,
|
||||
copyfile_flags_t(COPYFILE_EXCL))
|
||||
if copied == -1 {
|
||||
if errno == EEXIST {
|
||||
try? FileManager().removeItem(at: tempURL)
|
||||
return false
|
||||
}
|
||||
try? FileManager().removeItem(at: targetURL)
|
||||
throw POSIXError(POSIXErrorCode(rawValue: errno) ?? .EIO)
|
||||
}
|
||||
try? FileManager().removeItem(at: tempURL)
|
||||
|
||||
@@ -775,6 +775,7 @@ extension GatewayConnection {
|
||||
struct CronSchedulerStatus: Decodable {
|
||||
let enabled: Bool
|
||||
let storePath: String
|
||||
let sqlitePath: String?
|
||||
let jobs: Int
|
||||
let nextWakeAtMs: Int?
|
||||
}
|
||||
|
||||
@@ -15,9 +15,9 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>2026.6.6</string>
|
||||
<string>2026.6.2</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>2026060600</string>
|
||||
<string>2026060200</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>OpenClaw</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
|
||||
@@ -92,7 +92,13 @@ extension VoiceWakeOverlayController {
|
||||
|
||||
let contentHeight = ceil(used.height + (textInset.height * 2))
|
||||
let total = contentHeight + self.verticalPadding * 2
|
||||
self.model.isOverflowing = total > self.maxHeight
|
||||
// Defer the overflow state mutation to break the SwiftUI onChange → measuredHeight →
|
||||
// isOverflowing → re-render → onChange synchronous render loop (fixes #43480).
|
||||
let overflowing = total > self.maxHeight
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self, self.model.isOverflowing != overflowing else { return }
|
||||
self.model.isOverflowing = overflowing
|
||||
}
|
||||
return max(self.minHeight, min(total, self.maxHeight))
|
||||
}
|
||||
|
||||
|
||||
203
apps/macos/Tests/OpenClawIPCTests/CronModelsTests.swift
Normal file
203
apps/macos/Tests/OpenClawIPCTests/CronModelsTests.swift
Normal file
@@ -0,0 +1,203 @@
|
||||
import Foundation
|
||||
import Testing
|
||||
@testable import OpenClaw
|
||||
|
||||
struct CronModelsTests {
|
||||
private func makeCronJob(
|
||||
name: String,
|
||||
payloadText: String,
|
||||
state: CronJobState = CronJobState()) -> CronJob
|
||||
{
|
||||
CronJob(
|
||||
id: "x",
|
||||
agentId: nil,
|
||||
name: name,
|
||||
description: nil,
|
||||
enabled: true,
|
||||
deleteAfterRun: nil,
|
||||
createdAtMs: 0,
|
||||
updatedAtMs: 0,
|
||||
schedule: .at(at: "2026-02-03T18:00:00Z"),
|
||||
sessionTarget: .main,
|
||||
wakeMode: .now,
|
||||
payload: .systemEvent(text: payloadText),
|
||||
delivery: nil,
|
||||
state: state)
|
||||
}
|
||||
|
||||
@Test func `schedule at encodes and decodes`() throws {
|
||||
let schedule = CronSchedule.at(at: "2026-02-03T18:00:00Z")
|
||||
let data = try JSONEncoder().encode(schedule)
|
||||
let decoded = try JSONDecoder().decode(CronSchedule.self, from: data)
|
||||
#expect(decoded == schedule)
|
||||
}
|
||||
|
||||
@Test func `schedule at decodes legacy at ms`() throws {
|
||||
let json = """
|
||||
{"kind":"at","atMs":1700000000000}
|
||||
"""
|
||||
let decoded = try JSONDecoder().decode(CronSchedule.self, from: Data(json.utf8))
|
||||
if case let .at(at) = decoded {
|
||||
#expect(at.hasPrefix("2023-"))
|
||||
} else {
|
||||
#expect(Bool(false))
|
||||
}
|
||||
}
|
||||
|
||||
@Test func `schedule every encodes and decodes with anchor`() throws {
|
||||
let schedule = CronSchedule.every(everyMs: 5000, anchorMs: 10000)
|
||||
let data = try JSONEncoder().encode(schedule)
|
||||
let decoded = try JSONDecoder().decode(CronSchedule.self, from: data)
|
||||
#expect(decoded == schedule)
|
||||
}
|
||||
|
||||
@Test func `schedule cron encodes and decodes with timezone`() throws {
|
||||
let schedule = CronSchedule.cron(expr: "*/5 * * * *", tz: "Europe/Vienna")
|
||||
let data = try JSONEncoder().encode(schedule)
|
||||
let decoded = try JSONDecoder().decode(CronSchedule.self, from: data)
|
||||
#expect(decoded == schedule)
|
||||
}
|
||||
|
||||
@Test func `payload agent turn encodes and decodes`() throws {
|
||||
let payload = CronPayload.agentTurn(
|
||||
message: "hello",
|
||||
thinking: "low",
|
||||
timeoutSeconds: 15,
|
||||
deliver: true,
|
||||
channel: "whatsapp",
|
||||
to: "+15551234567",
|
||||
bestEffortDeliver: false)
|
||||
let data = try JSONEncoder().encode(payload)
|
||||
let decoded = try JSONDecoder().decode(CronPayload.self, from: data)
|
||||
#expect(decoded == payload)
|
||||
}
|
||||
|
||||
@Test func `job encodes and decodes delete after run`() throws {
|
||||
let job = CronJob(
|
||||
id: "job-1",
|
||||
agentId: nil,
|
||||
name: "One-shot",
|
||||
description: nil,
|
||||
enabled: true,
|
||||
deleteAfterRun: true,
|
||||
createdAtMs: 0,
|
||||
updatedAtMs: 0,
|
||||
schedule: .at(at: "2026-02-03T18:00:00Z"),
|
||||
sessionTarget: .main,
|
||||
wakeMode: .now,
|
||||
payload: .systemEvent(text: "ping"),
|
||||
delivery: nil,
|
||||
state: CronJobState())
|
||||
let data = try JSONEncoder().encode(job)
|
||||
let decoded = try JSONDecoder().decode(CronJob.self, from: data)
|
||||
#expect(decoded.deleteAfterRun == true)
|
||||
}
|
||||
|
||||
@Test func `schedule decode rejects unknown kind`() {
|
||||
let json = """
|
||||
{"kind":"wat","at":"2026-02-03T18:00:00Z"}
|
||||
"""
|
||||
#expect(throws: DecodingError.self) {
|
||||
_ = try JSONDecoder().decode(CronSchedule.self, from: Data(json.utf8))
|
||||
}
|
||||
}
|
||||
|
||||
@Test func `payload decode rejects unknown kind`() {
|
||||
let json = """
|
||||
{"kind":"wat","text":"hello"}
|
||||
"""
|
||||
#expect(throws: DecodingError.self) {
|
||||
_ = try JSONDecoder().decode(CronPayload.self, from: Data(json.utf8))
|
||||
}
|
||||
}
|
||||
|
||||
@Test func `display name trims whitespace and falls back`() {
|
||||
let base = self.makeCronJob(name: " hello ", payloadText: "hi")
|
||||
#expect(base.displayName == "hello")
|
||||
|
||||
var unnamed = base
|
||||
unnamed.name = " "
|
||||
#expect(unnamed.displayName == "Untitled job")
|
||||
}
|
||||
|
||||
@Test func `next run date and last run date derive from state`() {
|
||||
let job = self.makeCronJob(
|
||||
name: "t",
|
||||
payloadText: "hi",
|
||||
state: CronJobState(
|
||||
nextRunAtMs: 1_700_000_000_000,
|
||||
runningAtMs: nil,
|
||||
lastRunAtMs: 1_700_000_050_000,
|
||||
lastStatus: nil,
|
||||
lastError: nil,
|
||||
lastDurationMs: nil))
|
||||
#expect(job.nextRunDate == Date(timeIntervalSince1970: 1_700_000_000))
|
||||
#expect(job.lastRunDate == Date(timeIntervalSince1970: 1_700_000_050))
|
||||
}
|
||||
|
||||
@Test func `decode cron list response skips malformed jobs`() throws {
|
||||
let json = """
|
||||
{
|
||||
"jobs": [
|
||||
{
|
||||
"id": "good",
|
||||
"name": "Healthy job",
|
||||
"enabled": true,
|
||||
"createdAtMs": 1,
|
||||
"updatedAtMs": 2,
|
||||
"schedule": { "kind": "at", "at": "2026-03-01T10:00:00Z" },
|
||||
"sessionTarget": "main",
|
||||
"wakeMode": "now",
|
||||
"payload": { "kind": "systemEvent", "text": "hello" },
|
||||
"state": {}
|
||||
},
|
||||
{
|
||||
"id": "bad",
|
||||
"name": "Broken job",
|
||||
"enabled": true,
|
||||
"createdAtMs": 1,
|
||||
"updatedAtMs": 2,
|
||||
"schedule": { "kind": "at", "at": "2026-03-01T10:00:00Z" },
|
||||
"payload": { "kind": "systemEvent", "text": "hello" },
|
||||
"state": {}
|
||||
}
|
||||
],
|
||||
"total": 2,
|
||||
"offset": 0,
|
||||
"limit": 50,
|
||||
"hasMore": false,
|
||||
"nextOffset": null
|
||||
}
|
||||
"""
|
||||
|
||||
let jobs = try GatewayConnection.decodeCronListResponse(Data(json.utf8))
|
||||
|
||||
#expect(jobs.count == 1)
|
||||
#expect(jobs.first?.id == "good")
|
||||
}
|
||||
|
||||
@Test func `decode cron runs response skips malformed entries`() throws {
|
||||
let json = """
|
||||
{
|
||||
"entries": [
|
||||
{
|
||||
"ts": 1,
|
||||
"jobId": "good",
|
||||
"action": "finished",
|
||||
"status": "ok"
|
||||
},
|
||||
{
|
||||
"jobId": "bad",
|
||||
"action": "finished",
|
||||
"status": "ok"
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
|
||||
let entries = try GatewayConnection.decodeCronRunsResponse(Data(json.utf8))
|
||||
|
||||
#expect(entries.count == 1)
|
||||
#expect(entries.first?.jobId == "good")
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,20 @@ import Testing
|
||||
@testable import OpenClaw
|
||||
|
||||
struct DeepLinkAgentPolicyTests {
|
||||
@Test func `validate message for handle rejects too long when unkeyed`() {
|
||||
let msg = String(repeating: "a", count: DeepLinkAgentPolicy.maxUnkeyedConfirmChars + 1)
|
||||
let res = DeepLinkAgentPolicy.validateMessageForHandle(message: msg, allowUnattended: false)
|
||||
switch res {
|
||||
case let .failure(error):
|
||||
#expect(
|
||||
error == .messageTooLongForConfirmation(
|
||||
max: DeepLinkAgentPolicy.maxUnkeyedConfirmChars,
|
||||
actual: DeepLinkAgentPolicy.maxUnkeyedConfirmChars + 1))
|
||||
case .success:
|
||||
Issue.record("expected failure, got success")
|
||||
}
|
||||
}
|
||||
|
||||
@Test func `validate message for handle allows too long when keyed`() {
|
||||
let msg = String(repeating: "a", count: DeepLinkAgentPolicy.maxUnkeyedConfirmChars + 1)
|
||||
let res = DeepLinkAgentPolicy.validateMessageForHandle(message: msg, allowUnattended: true)
|
||||
|
||||
@@ -56,6 +56,7 @@ struct ExecApprovalsStoreRefactorTests {
|
||||
let home = root.appendingPathComponent("home", isDirectory: true)
|
||||
let stateDir = root.appendingPathComponent("state", isDirectory: true)
|
||||
defer { try? FileManager().removeItem(at: root) }
|
||||
try Self.seedCurrentApprovalsFile(in: stateDir)
|
||||
|
||||
try await self.withLockedEnv([
|
||||
"OPENCLAW_HOME": home.path,
|
||||
@@ -196,4 +197,19 @@ struct ExecApprovalsStoreRefactorTests {
|
||||
}
|
||||
return identifier
|
||||
}
|
||||
|
||||
private static func seedCurrentApprovalsFile(in stateDir: URL) throws {
|
||||
try FileManager().createDirectory(at: stateDir, withIntermediateDirectories: true)
|
||||
let file = ExecApprovalsFile(
|
||||
version: 1,
|
||||
socket: ExecApprovalsSocketConfig(
|
||||
path: stateDir.appendingPathComponent("exec-approvals.sock").path,
|
||||
token: "test-token"),
|
||||
defaults: nil,
|
||||
agents: [:])
|
||||
let encoder = JSONEncoder()
|
||||
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
|
||||
try encoder.encode(file)
|
||||
.write(to: stateDir.appendingPathComponent("exec-approvals.json"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,22 +17,6 @@ struct GatewayConnectionTests {
|
||||
return (conn, cfg)
|
||||
}
|
||||
|
||||
private func withConnection<T>(
|
||||
session: GatewayTestWebSocketSession,
|
||||
token: String? = nil,
|
||||
_ body: (GatewayConnection, ConfigSource) async throws -> T) async throws -> T
|
||||
{
|
||||
let (conn, cfg) = try self.makeConnection(session: session, token: token)
|
||||
do {
|
||||
let result = try await body(conn, cfg)
|
||||
await conn.shutdown()
|
||||
return result
|
||||
} catch {
|
||||
await conn.shutdown()
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
private func makeSession(helloDelayMs: Int = 0) -> GatewayTestWebSocketSession {
|
||||
GatewayTestWebSocketSession(
|
||||
taskFactory: {
|
||||
@@ -74,104 +58,99 @@ struct GatewayConnectionTests {
|
||||
|
||||
@Test func `request reuses single web socket for same config`() async throws {
|
||||
let session = self.makeSession()
|
||||
let (conn, _) = try self.makeConnection(session: session)
|
||||
|
||||
try await self.withConnection(session: session) { conn, _ in
|
||||
_ = try await conn.request(method: "status", params: nil)
|
||||
#expect(session.snapshotMakeCount() == 1)
|
||||
_ = try await conn.request(method: "status", params: nil)
|
||||
#expect(session.snapshotMakeCount() == 1)
|
||||
|
||||
_ = try await conn.request(method: "status", params: nil)
|
||||
#expect(session.snapshotMakeCount() == 1)
|
||||
#expect(session.snapshotCancelCount() == 0)
|
||||
}
|
||||
_ = try await conn.request(method: "status", params: nil)
|
||||
#expect(session.snapshotMakeCount() == 1)
|
||||
#expect(session.snapshotCancelCount() == 0)
|
||||
}
|
||||
|
||||
@Test func `request reconfigures and cancels on token change`() async throws {
|
||||
let session = self.makeSession()
|
||||
let (conn, cfg) = try self.makeConnection(session: session, token: "a")
|
||||
|
||||
try await self.withConnection(session: session, token: "a") { conn, cfg in
|
||||
_ = try await conn.request(method: "status", params: nil)
|
||||
#expect(session.snapshotMakeCount() == 1)
|
||||
_ = try await conn.request(method: "status", params: nil)
|
||||
#expect(session.snapshotMakeCount() == 1)
|
||||
|
||||
cfg.setToken("b")
|
||||
_ = try await conn.request(method: "status", params: nil)
|
||||
#expect(session.snapshotMakeCount() == 2)
|
||||
#expect(session.snapshotCancelCount() == 1)
|
||||
}
|
||||
cfg.setToken("b")
|
||||
_ = try await conn.request(method: "status", params: nil)
|
||||
#expect(session.snapshotMakeCount() == 2)
|
||||
#expect(session.snapshotCancelCount() == 1)
|
||||
}
|
||||
|
||||
@Test func `concurrent requests still use single web socket`() async throws {
|
||||
let session = self.makeSession(helloDelayMs: 150)
|
||||
let (conn, _) = try self.makeConnection(session: session)
|
||||
|
||||
try await self.withConnection(session: session) { conn, _ in
|
||||
async let r1: Data = conn.request(method: "status", params: nil)
|
||||
async let r2: Data = conn.request(method: "status", params: nil)
|
||||
_ = try await (r1, r2)
|
||||
async let r1: Data = conn.request(method: "status", params: nil)
|
||||
async let r2: Data = conn.request(method: "status", params: nil)
|
||||
_ = try await (r1, r2)
|
||||
|
||||
#expect(session.snapshotMakeCount() == 1)
|
||||
}
|
||||
#expect(session.snapshotMakeCount() == 1)
|
||||
}
|
||||
|
||||
@Test func `subscribe replays latest snapshot`() async throws {
|
||||
let session = self.makeSession()
|
||||
let (conn, _) = try self.makeConnection(session: session)
|
||||
|
||||
try await self.withConnection(session: session) { conn, _ in
|
||||
_ = try await conn.request(method: "status", params: nil)
|
||||
_ = try await conn.request(method: "status", params: nil)
|
||||
|
||||
let stream = await conn.subscribe(bufferingNewest: 10)
|
||||
var iterator = stream.makeAsyncIterator()
|
||||
let first = await iterator.next()
|
||||
let stream = await conn.subscribe(bufferingNewest: 10)
|
||||
var iterator = stream.makeAsyncIterator()
|
||||
let first = await iterator.next()
|
||||
|
||||
guard case let .snapshot(snap) = first else {
|
||||
Issue.record("expected snapshot, got \(String(describing: first))")
|
||||
return
|
||||
}
|
||||
#expect(snap.type == "hello-ok")
|
||||
guard case let .snapshot(snap) = first else {
|
||||
Issue.record("expected snapshot, got \(String(describing: first))")
|
||||
return
|
||||
}
|
||||
#expect(snap.type == "hello-ok")
|
||||
}
|
||||
|
||||
@Test func `subscribe emits seq gap before event`() async throws {
|
||||
let session = self.makeSession()
|
||||
let (conn, _) = try self.makeConnection(session: session)
|
||||
|
||||
try await self.withConnection(session: session) { conn, _ in
|
||||
let stream = await conn.subscribe(bufferingNewest: 10)
|
||||
var iterator = stream.makeAsyncIterator()
|
||||
let stream = await conn.subscribe(bufferingNewest: 10)
|
||||
var iterator = stream.makeAsyncIterator()
|
||||
|
||||
_ = try await conn.request(method: "status", params: nil)
|
||||
_ = await iterator.next() // snapshot
|
||||
_ = try await conn.request(method: "status", params: nil)
|
||||
_ = await iterator.next() // snapshot
|
||||
|
||||
let evt1 = Data(
|
||||
"""
|
||||
{"type":"event","event":"presence","payload":{"presence":[]},"seq":1}
|
||||
""".utf8)
|
||||
session.latestTask()?.emitReceiveSuccess(.data(evt1))
|
||||
let evt1 = Data(
|
||||
"""
|
||||
{"type":"event","event":"presence","payload":{"presence":[]},"seq":1}
|
||||
""".utf8)
|
||||
session.latestTask()?.emitReceiveSuccess(.data(evt1))
|
||||
|
||||
let firstEvent = await iterator.next()
|
||||
guard case let .event(firstFrame) = firstEvent else {
|
||||
Issue.record("expected event, got \(String(describing: firstEvent))")
|
||||
return
|
||||
}
|
||||
#expect(firstFrame.seq == 1)
|
||||
|
||||
let evt3 = Data(
|
||||
"""
|
||||
{"type":"event","event":"presence","payload":{"presence":[]},"seq":3}
|
||||
""".utf8)
|
||||
session.latestTask()?.emitReceiveSuccess(.data(evt3))
|
||||
|
||||
let gap = await iterator.next()
|
||||
guard case let .seqGap(expected, received) = gap else {
|
||||
Issue.record("expected seqGap, got \(String(describing: gap))")
|
||||
return
|
||||
}
|
||||
#expect(expected == 2)
|
||||
#expect(received == 3)
|
||||
|
||||
let secondEvent = await iterator.next()
|
||||
guard case let .event(secondFrame) = secondEvent else {
|
||||
Issue.record("expected event, got \(String(describing: secondEvent))")
|
||||
return
|
||||
}
|
||||
#expect(secondFrame.seq == 3)
|
||||
let firstEvent = await iterator.next()
|
||||
guard case let .event(firstFrame) = firstEvent else {
|
||||
Issue.record("expected event, got \(String(describing: firstEvent))")
|
||||
return
|
||||
}
|
||||
#expect(firstFrame.seq == 1)
|
||||
|
||||
let evt3 = Data(
|
||||
"""
|
||||
{"type":"event","event":"presence","payload":{"presence":[]},"seq":3}
|
||||
""".utf8)
|
||||
session.latestTask()?.emitReceiveSuccess(.data(evt3))
|
||||
|
||||
let gap = await iterator.next()
|
||||
guard case let .seqGap(expected, received) = gap else {
|
||||
Issue.record("expected seqGap, got \(String(describing: gap))")
|
||||
return
|
||||
}
|
||||
#expect(expected == 2)
|
||||
#expect(received == 3)
|
||||
|
||||
let secondEvent = await iterator.next()
|
||||
guard case let .event(secondFrame) = secondEvent else {
|
||||
Issue.record("expected event, got \(String(describing: secondEvent))")
|
||||
return
|
||||
}
|
||||
#expect(secondFrame.seq == 3)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,75 +110,21 @@ struct GatewayChannelConnectTests {
|
||||
})
|
||||
}
|
||||
|
||||
private static func connectWithTestTimeout(_ channel: GatewayChannelActor) async throws {
|
||||
try await AsyncTimeout.withTimeout(
|
||||
seconds: 5,
|
||||
onTimeout: {
|
||||
NSError(
|
||||
domain: "GatewayChannelConnectTests",
|
||||
code: 1,
|
||||
userInfo: [NSLocalizedDescriptionKey: "test gateway connect timed out"])
|
||||
},
|
||||
operation: { try await channel.connect() })
|
||||
}
|
||||
|
||||
private static func shutdownWithTestTimeout(_ channel: GatewayChannelActor) async throws {
|
||||
try await AsyncTimeout.withTimeout(
|
||||
seconds: 5,
|
||||
onTimeout: {
|
||||
NSError(
|
||||
domain: "GatewayChannelConnectTests",
|
||||
code: 2,
|
||||
userInfo: [NSLocalizedDescriptionKey: "test gateway shutdown timed out"])
|
||||
},
|
||||
operation: { await channel.shutdown() })
|
||||
}
|
||||
|
||||
private func withTemporaryStateDir(_ operation: () async throws -> Void) async throws {
|
||||
private func withTemporaryStateDir<T>(_ operation: () async throws -> T) async throws -> T {
|
||||
let tempDir = FileManager.default.temporaryDirectory
|
||||
.appendingPathComponent(UUID().uuidString, isDirectory: true)
|
||||
try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true)
|
||||
try Self.seedDeviceIdentity(in: tempDir)
|
||||
defer { try? FileManager.default.removeItem(at: tempDir) }
|
||||
|
||||
func restoreStateDir(_ previousStateDir: String?) {
|
||||
let previousStateDir = ProcessInfo.processInfo.environment["OPENCLAW_STATE_DIR"]
|
||||
setenv("OPENCLAW_STATE_DIR", tempDir.path, 1)
|
||||
defer {
|
||||
if let previousStateDir {
|
||||
setenv("OPENCLAW_STATE_DIR", previousStateDir, 1)
|
||||
} else {
|
||||
unsetenv("OPENCLAW_STATE_DIR")
|
||||
}
|
||||
try? FileManager.default.removeItem(at: tempDir)
|
||||
}
|
||||
|
||||
await TestIsolationLock.shared.acquire()
|
||||
let previousStateDir = getenv("OPENCLAW_STATE_DIR").map { String(cString: $0) }
|
||||
setenv("OPENCLAW_STATE_DIR", tempDir.path, 1)
|
||||
|
||||
do {
|
||||
try await operation()
|
||||
restoreStateDir(previousStateDir)
|
||||
await TestIsolationLock.shared.release()
|
||||
} catch {
|
||||
restoreStateDir(previousStateDir)
|
||||
await TestIsolationLock.shared.release()
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
private static func seedDeviceIdentity(in stateDir: URL) throws {
|
||||
let identityDir = stateDir.appendingPathComponent("identity", isDirectory: true)
|
||||
try FileManager.default.createDirectory(at: identityDir, withIntermediateDirectories: true)
|
||||
let identity = """
|
||||
{
|
||||
"deviceId": "56475aa75463474c0285df5dbf2bcab73da651358839e9b77481b2eab107708c",
|
||||
"publicKey": "A6EHv/POEL4dcN0Y50vAmWfk1jCbpQ1fHdyGZBJVMbg=",
|
||||
"privateKey": "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=",
|
||||
"createdAtMs": 1700000000000
|
||||
}
|
||||
"""
|
||||
try identity.write(
|
||||
to: identityDir.appendingPathComponent("device.json", isDirectory: false),
|
||||
atomically: true,
|
||||
encoding: .utf8)
|
||||
return try await operation()
|
||||
}
|
||||
|
||||
@Test func `concurrent connect is single flight on success`() async throws {
|
||||
@@ -195,7 +141,6 @@ struct GatewayChannelConnectTests {
|
||||
_ = try await t2.value
|
||||
|
||||
#expect(session.snapshotMakeCount() == 1)
|
||||
try await Self.shutdownWithTestTimeout(channel)
|
||||
}
|
||||
|
||||
@Test func `connect advertises compatible protocol range`() async throws {
|
||||
@@ -211,29 +156,13 @@ struct GatewayChannelConnectTests {
|
||||
let channel = try GatewayChannelActor(
|
||||
url: #require(URL(string: "ws://example.invalid")),
|
||||
token: nil,
|
||||
session: WebSocketSessionBox(session: session),
|
||||
connectOptions: GatewayConnectOptions(
|
||||
role: "operator",
|
||||
scopes: [],
|
||||
caps: [],
|
||||
commands: [],
|
||||
permissions: [:],
|
||||
clientId: "openclaw-macos-test",
|
||||
clientMode: "test",
|
||||
clientDisplayName: "OpenClaw macOS Test",
|
||||
includeDeviceIdentity: false))
|
||||
session: WebSocketSessionBox(session: session))
|
||||
|
||||
do {
|
||||
try await Self.connectWithTestTimeout(channel)
|
||||
try await channel.connect()
|
||||
|
||||
let params = try #require(recorder.snapshot())
|
||||
#expect(params["minProtocol"] as? Int == GATEWAY_MIN_PROTOCOL_VERSION)
|
||||
#expect(params["maxProtocol"] as? Int == GATEWAY_PROTOCOL_VERSION)
|
||||
} catch {
|
||||
try? await Self.shutdownWithTestTimeout(channel)
|
||||
throw error
|
||||
}
|
||||
try await Self.shutdownWithTestTimeout(channel)
|
||||
let params = try #require(recorder.snapshot())
|
||||
#expect(params["minProtocol"] as? Int == GATEWAY_MIN_PROTOCOL_VERSION)
|
||||
#expect(params["maxProtocol"] as? Int == GATEWAY_PROTOCOL_VERSION)
|
||||
}
|
||||
|
||||
@Test func `concurrent connect shares failure`() async throws {
|
||||
@@ -256,7 +185,6 @@ struct GatewayChannelConnectTests {
|
||||
if case .failure = r2 { true } else { false }
|
||||
}())
|
||||
#expect(session.snapshotMakeCount() == 1)
|
||||
try await Self.shutdownWithTestTimeout(channel)
|
||||
}
|
||||
|
||||
@Test func `default operator connect scopes preserve pairing and admin`() async throws {
|
||||
@@ -275,7 +203,7 @@ struct GatewayChannelConnectTests {
|
||||
token: nil,
|
||||
session: WebSocketSessionBox(session: session))
|
||||
|
||||
try await Self.connectWithTestTimeout(channel)
|
||||
try await channel.connect()
|
||||
|
||||
#expect(capture.snapshot() == [
|
||||
"operator.admin",
|
||||
@@ -284,7 +212,6 @@ struct GatewayChannelConnectTests {
|
||||
"operator.approvals",
|
||||
"operator.pairing",
|
||||
])
|
||||
try await Self.shutdownWithTestTimeout(channel)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -304,14 +231,13 @@ struct GatewayChannelConnectTests {
|
||||
bootstrapToken: "setup-bootstrap-token",
|
||||
session: WebSocketSessionBox(session: session))
|
||||
|
||||
try await Self.connectWithTestTimeout(channel)
|
||||
try await channel.connect()
|
||||
|
||||
#expect(capture.snapshot() == [
|
||||
"operator.approvals",
|
||||
"operator.read",
|
||||
"operator.write",
|
||||
])
|
||||
try await Self.shutdownWithTestTimeout(channel)
|
||||
}
|
||||
|
||||
@Test func `stored device token connect scopes reuse cached scopes`() async throws {
|
||||
@@ -336,10 +262,9 @@ struct GatewayChannelConnectTests {
|
||||
token: nil,
|
||||
session: WebSocketSessionBox(session: session))
|
||||
|
||||
try await Self.connectWithTestTimeout(channel)
|
||||
try await channel.connect()
|
||||
|
||||
#expect(capture.snapshot() == storedEntry.scopes)
|
||||
try await Self.shutdownWithTestTimeout(channel)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -376,10 +301,9 @@ struct GatewayChannelConnectTests {
|
||||
clientMode: "ui",
|
||||
clientDisplayName: "OpenClaw macOS Debug CLI"))
|
||||
|
||||
try await Self.connectWithTestTimeout(channel)
|
||||
try await channel.connect()
|
||||
|
||||
#expect(capture.snapshot() == requestedScopes)
|
||||
try await Self.shutdownWithTestTimeout(channel)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -395,7 +319,7 @@ struct GatewayChannelConnectTests {
|
||||
session: WebSocketSessionBox(session: session))
|
||||
|
||||
do {
|
||||
try await Self.connectWithTestTimeout(channel)
|
||||
try await channel.connect()
|
||||
Issue.record("expected GatewayConnectAuthError")
|
||||
} catch let error as GatewayConnectAuthError {
|
||||
#expect(error.detail == .authTokenMissing)
|
||||
@@ -404,11 +328,8 @@ struct GatewayChannelConnectTests {
|
||||
#expect(error.recommendedNextStep == .updateAuthConfiguration)
|
||||
#expect(error.recommendedNextStepCode == GatewayConnectRecoveryNextStep.updateAuthConfiguration.rawValue)
|
||||
} catch {
|
||||
try? await Self.shutdownWithTestTimeout(channel)
|
||||
Issue.record("unexpected error: \(error)")
|
||||
return
|
||||
}
|
||||
try await Self.shutdownWithTestTimeout(channel)
|
||||
}
|
||||
|
||||
@Test func `connect maps user cancelled authentication with cached TLS failure`() async throws {
|
||||
@@ -426,15 +347,12 @@ struct GatewayChannelConnectTests {
|
||||
session: WebSocketSessionBox(session: session))
|
||||
|
||||
do {
|
||||
try await Self.connectWithTestTimeout(channel)
|
||||
try await channel.connect()
|
||||
Issue.record("expected GatewayTLSValidationError")
|
||||
} catch let error as GatewayTLSValidationError {
|
||||
#expect(error.failure == failure)
|
||||
} catch {
|
||||
try? await Self.shutdownWithTestTimeout(channel)
|
||||
Issue.record("unexpected error: \(error)")
|
||||
return
|
||||
}
|
||||
try await Self.shutdownWithTestTimeout(channel)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,93 +96,64 @@ private final class TrustedDeviceRetryGatewaySession: WebSocketSessioning, Gatew
|
||||
|
||||
@Suite(.serialized)
|
||||
struct GatewayChannelDeviceTokenRetryTests {
|
||||
@Test(.disabled(
|
||||
"Synthetic retry fixture hangs Swift Testing on release runners; device auth details stay covered elsewhere."))
|
||||
func `remote pinned TLS retries stale shared token with stored device token`() async throws {
|
||||
@Test func `remote pinned TLS retries stale shared token with stored device token`() async throws {
|
||||
let tempDir = FileManager.default.temporaryDirectory
|
||||
.appendingPathComponent(UUID().uuidString, isDirectory: true)
|
||||
try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true)
|
||||
try Self.seedDeviceIdentity(in: tempDir)
|
||||
defer { try? FileManager.default.removeItem(at: tempDir) }
|
||||
|
||||
try await TestIsolation.withEnvValues(["OPENCLAW_STATE_DIR": tempDir.path]) {
|
||||
let identity = DeviceIdentityStore.loadOrCreate()
|
||||
_ = DeviceAuthStore.storeToken(
|
||||
deviceId: identity.deviceId,
|
||||
role: "operator",
|
||||
token: "stored-device-token")
|
||||
|
||||
let recorder = ConnectAuthRecorder()
|
||||
let session = TrustedDeviceRetryGatewaySession(
|
||||
recorder: recorder,
|
||||
allowsDeviceTokenRetryAuth: true)
|
||||
let options = GatewayConnectOptions(
|
||||
role: "operator",
|
||||
scopes: ["operator.read"],
|
||||
caps: [],
|
||||
commands: [],
|
||||
permissions: [:],
|
||||
clientId: "openclaw-ios-test",
|
||||
clientMode: "ui",
|
||||
clientDisplayName: "iOS Test",
|
||||
includeDeviceIdentity: true)
|
||||
let channel = try GatewayChannelActor(
|
||||
url: #require(URL(string: "wss://gateway.example.com")),
|
||||
token: "stale-shared-token",
|
||||
session: WebSocketSessionBox(session: session),
|
||||
connectOptions: options)
|
||||
|
||||
do {
|
||||
do {
|
||||
try await Self.connectWithTestTimeout(channel)
|
||||
Issue.record("expected stale shared-token connect to fail before device-token retry")
|
||||
} catch let error as GatewayConnectAuthError {
|
||||
#expect(error.detail == .authTokenMismatch)
|
||||
}
|
||||
|
||||
try await Self.connectWithTestTimeout(channel)
|
||||
|
||||
let firstAuth = try #require(recorder.auth(at: 0))
|
||||
#expect(firstAuth["token"] as? String == "stale-shared-token")
|
||||
#expect(firstAuth["deviceToken"] == nil)
|
||||
|
||||
let retryAuth = try #require(recorder.auth(at: 1))
|
||||
#expect(retryAuth["token"] as? String == "stale-shared-token")
|
||||
#expect(retryAuth["deviceToken"] as? String == "stored-device-token")
|
||||
} catch {
|
||||
await channel.shutdown()
|
||||
throw error
|
||||
let previousStateDir = ProcessInfo.processInfo.environment["OPENCLAW_STATE_DIR"]
|
||||
setenv("OPENCLAW_STATE_DIR", tempDir.path, 1)
|
||||
defer {
|
||||
if let previousStateDir {
|
||||
setenv("OPENCLAW_STATE_DIR", previousStateDir, 1)
|
||||
} else {
|
||||
unsetenv("OPENCLAW_STATE_DIR")
|
||||
}
|
||||
await channel.shutdown()
|
||||
try? FileManager.default.removeItem(at: tempDir)
|
||||
}
|
||||
}
|
||||
|
||||
private static func connectWithTestTimeout(_ channel: GatewayChannelActor) async throws {
|
||||
try await AsyncTimeout.withTimeout(
|
||||
seconds: 5,
|
||||
onTimeout: {
|
||||
NSError(
|
||||
domain: "GatewayChannelDeviceTokenRetryTests",
|
||||
code: 1,
|
||||
userInfo: [NSLocalizedDescriptionKey: "test gateway connect timed out"])
|
||||
},
|
||||
operation: { try await channel.connect() })
|
||||
}
|
||||
let identity = DeviceIdentityStore.loadOrCreate()
|
||||
_ = DeviceAuthStore.storeToken(
|
||||
deviceId: identity.deviceId,
|
||||
role: "operator",
|
||||
token: "stored-device-token")
|
||||
|
||||
private static func seedDeviceIdentity(in stateDir: URL) throws {
|
||||
let identityDir = stateDir.appendingPathComponent("identity", isDirectory: true)
|
||||
try FileManager.default.createDirectory(at: identityDir, withIntermediateDirectories: true)
|
||||
let identity = """
|
||||
{
|
||||
"deviceId": "56475aa75463474c0285df5dbf2bcab73da651358839e9b77481b2eab107708c",
|
||||
"publicKey": "A6EHv/POEL4dcN0Y50vAmWfk1jCbpQ1fHdyGZBJVMbg=",
|
||||
"privateKey": "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=",
|
||||
"createdAtMs": 1700000000000
|
||||
}
|
||||
"""
|
||||
try identity.write(
|
||||
to: identityDir.appendingPathComponent("device.json", isDirectory: false),
|
||||
atomically: true,
|
||||
encoding: .utf8)
|
||||
let recorder = ConnectAuthRecorder()
|
||||
let session = TrustedDeviceRetryGatewaySession(
|
||||
recorder: recorder,
|
||||
allowsDeviceTokenRetryAuth: true)
|
||||
let options = GatewayConnectOptions(
|
||||
role: "operator",
|
||||
scopes: ["operator.read"],
|
||||
caps: [],
|
||||
commands: [],
|
||||
permissions: [:],
|
||||
clientId: "openclaw-ios-test",
|
||||
clientMode: "ui",
|
||||
clientDisplayName: "iOS Test",
|
||||
includeDeviceIdentity: true)
|
||||
let channel = try GatewayChannelActor(
|
||||
url: #require(URL(string: "wss://gateway.example.com")),
|
||||
token: "stale-shared-token",
|
||||
session: WebSocketSessionBox(session: session),
|
||||
connectOptions: options)
|
||||
|
||||
do {
|
||||
try await channel.connect()
|
||||
Issue.record("expected stale shared-token connect to fail before device-token retry")
|
||||
} catch let error as GatewayConnectAuthError {
|
||||
#expect(error.detail == .authTokenMismatch)
|
||||
}
|
||||
|
||||
try await channel.connect()
|
||||
|
||||
let firstAuth = try #require(recorder.auth(at: 0))
|
||||
#expect(firstAuth["token"] as? String == "stale-shared-token")
|
||||
#expect(firstAuth["deviceToken"] == nil)
|
||||
|
||||
let retryAuth = try #require(recorder.auth(at: 1))
|
||||
#expect(retryAuth["token"] as? String == "stale-shared-token")
|
||||
#expect(retryAuth["deviceToken"] as? String == "stored-device-token")
|
||||
|
||||
await channel.shutdown()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,5 @@ struct GatewayChannelRequestTests {
|
||||
|
||||
// Give the delayed send failure task time to run; this used to crash due to a double-resume.
|
||||
try? await Task.sleep(nanoseconds: 250 * 1_000_000)
|
||||
await channel.shutdown()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2074,6 +2074,204 @@ public struct SessionsCompactionRestoreResult: Codable, Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
public struct SessionFileBrowserEntry: Codable, Sendable {
|
||||
public let path: String
|
||||
public let name: String
|
||||
public let kind: AnyCodable
|
||||
public let sessionkind: SessionFileRelevance?
|
||||
public let size: Int?
|
||||
public let updatedatms: Int?
|
||||
|
||||
public init(
|
||||
path: String,
|
||||
name: String,
|
||||
kind: AnyCodable,
|
||||
sessionkind: SessionFileRelevance?,
|
||||
size: Int?,
|
||||
updatedatms: Int?)
|
||||
{
|
||||
self.path = path
|
||||
self.name = name
|
||||
self.kind = kind
|
||||
self.sessionkind = sessionkind
|
||||
self.size = size
|
||||
self.updatedatms = updatedatms
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case path
|
||||
case name
|
||||
case kind
|
||||
case sessionkind = "sessionKind"
|
||||
case size
|
||||
case updatedatms = "updatedAtMs"
|
||||
}
|
||||
}
|
||||
|
||||
public struct SessionFileBrowserResult: Codable, Sendable {
|
||||
public let path: String
|
||||
public let parentpath: String?
|
||||
public let search: String?
|
||||
public let entries: [SessionFileBrowserEntry]
|
||||
public let truncated: Bool?
|
||||
|
||||
public init(
|
||||
path: String,
|
||||
parentpath: String?,
|
||||
search: String?,
|
||||
entries: [SessionFileBrowserEntry],
|
||||
truncated: Bool?)
|
||||
{
|
||||
self.path = path
|
||||
self.parentpath = parentpath
|
||||
self.search = search
|
||||
self.entries = entries
|
||||
self.truncated = truncated
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case path
|
||||
case parentpath = "parentPath"
|
||||
case search
|
||||
case entries
|
||||
case truncated
|
||||
}
|
||||
}
|
||||
|
||||
public struct SessionFileEntry: Codable, Sendable {
|
||||
public let path: String
|
||||
public let name: String
|
||||
public let kind: SessionFileKind
|
||||
public let missing: Bool
|
||||
public let size: Int?
|
||||
public let updatedatms: Int?
|
||||
public let content: String?
|
||||
|
||||
public init(
|
||||
path: String,
|
||||
name: String,
|
||||
kind: SessionFileKind,
|
||||
missing: Bool,
|
||||
size: Int?,
|
||||
updatedatms: Int?,
|
||||
content: String?)
|
||||
{
|
||||
self.path = path
|
||||
self.name = name
|
||||
self.kind = kind
|
||||
self.missing = missing
|
||||
self.size = size
|
||||
self.updatedatms = updatedatms
|
||||
self.content = content
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case path
|
||||
case name
|
||||
case kind
|
||||
case missing
|
||||
case size
|
||||
case updatedatms = "updatedAtMs"
|
||||
case content
|
||||
}
|
||||
}
|
||||
|
||||
public struct SessionsFilesListParams: Codable, Sendable {
|
||||
public let sessionkey: String
|
||||
public let agentid: String?
|
||||
public let path: String?
|
||||
public let search: String?
|
||||
|
||||
public init(
|
||||
sessionkey: String,
|
||||
agentid: String? = nil,
|
||||
path: String?,
|
||||
search: String?)
|
||||
{
|
||||
self.sessionkey = sessionkey
|
||||
self.agentid = agentid
|
||||
self.path = path
|
||||
self.search = search
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case sessionkey = "sessionKey"
|
||||
case agentid = "agentId"
|
||||
case path
|
||||
case search
|
||||
}
|
||||
}
|
||||
|
||||
public struct SessionsFilesListResult: Codable, Sendable {
|
||||
public let sessionkey: String
|
||||
public let root: String?
|
||||
public let files: [SessionFileEntry]
|
||||
public let browser: SessionFileBrowserResult?
|
||||
|
||||
public init(
|
||||
sessionkey: String,
|
||||
root: String?,
|
||||
files: [SessionFileEntry],
|
||||
browser: SessionFileBrowserResult?)
|
||||
{
|
||||
self.sessionkey = sessionkey
|
||||
self.root = root
|
||||
self.files = files
|
||||
self.browser = browser
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case sessionkey = "sessionKey"
|
||||
case root
|
||||
case files
|
||||
case browser
|
||||
}
|
||||
}
|
||||
|
||||
public struct SessionsFilesGetParams: Codable, Sendable {
|
||||
public let sessionkey: String
|
||||
public let path: String
|
||||
public let agentid: String?
|
||||
|
||||
public init(
|
||||
sessionkey: String,
|
||||
path: String,
|
||||
agentid: String? = nil)
|
||||
{
|
||||
self.sessionkey = sessionkey
|
||||
self.path = path
|
||||
self.agentid = agentid
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case sessionkey = "sessionKey"
|
||||
case path
|
||||
case agentid = "agentId"
|
||||
}
|
||||
}
|
||||
|
||||
public struct SessionsFilesGetResult: Codable, Sendable {
|
||||
public let sessionkey: String
|
||||
public let root: String?
|
||||
public let file: SessionFileEntry
|
||||
|
||||
public init(
|
||||
sessionkey: String,
|
||||
root: String?,
|
||||
file: SessionFileEntry)
|
||||
{
|
||||
self.sessionkey = sessionkey
|
||||
self.root = root
|
||||
self.file = file
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case sessionkey = "sessionKey"
|
||||
case root
|
||||
case file
|
||||
}
|
||||
}
|
||||
|
||||
public struct SessionsCreateParams: Codable, Sendable {
|
||||
public let key: String?
|
||||
public let agentid: String?
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
761191a4d9d369cf2b81a9895c4a3a008e5b84b4009842667daa9fdc1c36371c config-baseline.json
|
||||
cf4bb4479d9004385c016fc6dba1db9509852762979635de4c7e1b33e89fe87c config-baseline.core.json
|
||||
7641a0edd1f7ddb904d394381017cb645d2c2c1a86d875df30e9bf11f8dd012e config-baseline.channel.json
|
||||
b0dec5acfe60557e728e5ad03cc36d19d2432d51f755656c97846afa7fbe374a config-baseline.plugin.json
|
||||
0485ba902d2afd89d2c41cde7180d0cec2900b2db6804b9f97d42b7d85cd3af5 config-baseline.json
|
||||
72bb80be618406f3337eaa2560d2559a35e49bd29576de8dd4a3aec1a6a94d92 config-baseline.core.json
|
||||
1218f5555541b61bd5ddcac6441f15061b44789e2471d4ffecbe3059777c55c1 config-baseline.channel.json
|
||||
a14ac4261e98403d1a7e047070e6f151938444e27382b860315bd0c74fda4861 config-baseline.plugin.json
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
fbb605dd3077cb40826330ee3120e1955824b9f4b10acb908e654e333ed1a1ff plugin-sdk-api-baseline.json
|
||||
407f0fd91ea56310b75b332f533ac3e76e769b0ea02d8e1977fa550c348ac26c plugin-sdk-api-baseline.jsonl
|
||||
b121079a0912b3051a9fc319a675ef920da9db23364ca0c0ccd3c9f0a05a3a49 plugin-sdk-api-baseline.json
|
||||
61a0108da670e0f44ba4b861c002eb6eaa5cf63e392d4e7e7de42044cbe7d115 plugin-sdk-api-baseline.jsonl
|
||||
|
||||
@@ -311,7 +311,9 @@ $OPENCLAW_STATE_DIR/tasks/runs.sqlite
|
||||
|
||||
The registry loads into memory at gateway start and syncs writes to SQLite for durability across restarts.
|
||||
The Gateway keeps the SQLite write-ahead log bounded by using SQLite's default
|
||||
autocheckpoint threshold plus periodic and shutdown `TRUNCATE` checkpoints.
|
||||
autocheckpoint threshold plus periodic `PASSIVE` checkpoints. Shutdown and
|
||||
explicit maintenance checkpoints still use `TRUNCATE` so normal closes can
|
||||
reclaim WAL space without making the background sweeper wait on active readers.
|
||||
|
||||
### Automatic maintenance
|
||||
|
||||
|
||||
@@ -161,17 +161,20 @@ Control how agents process messages:
|
||||
<Step title="Incoming message arrives">
|
||||
A WhatsApp group or DM message arrives.
|
||||
</Step>
|
||||
<Step title="Broadcast check">
|
||||
System checks if peer ID is in `broadcast`.
|
||||
<Step title="Route and admission">
|
||||
OpenClaw applies channel allowlists, group activation rules, and configured ACP binding ownership.
|
||||
</Step>
|
||||
<Step title="If in broadcast list">
|
||||
<Step title="Broadcast check">
|
||||
If no configured ACP binding owns the route, OpenClaw checks whether the peer ID is in `broadcast`.
|
||||
</Step>
|
||||
<Step title="If broadcast applies">
|
||||
- All listed agents process the message.
|
||||
- Each agent has its own session key and isolated context.
|
||||
- Agents process in parallel (default) or sequentially.
|
||||
|
||||
</Step>
|
||||
<Step title="If not in broadcast list">
|
||||
Normal routing applies (first matching binding).
|
||||
<Step title="If broadcast does not apply">
|
||||
OpenClaw dispatches the ordinary route or the configured ACP session route selected during routing.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
@@ -322,7 +325,7 @@ Broadcast groups work alongside existing routing:
|
||||
- `GROUP_B`: agent1 AND agent2 respond (broadcast).
|
||||
|
||||
<Note>
|
||||
**Precedence:** `broadcast` takes priority over `bindings`.
|
||||
**Precedence:** `broadcast` takes priority over ordinary route bindings. Configured ACP bindings (`bindings[].type="acp"`) are exclusive: when one matches, OpenClaw dispatches to the configured ACP session instead of fan-out broadcast.
|
||||
</Note>
|
||||
|
||||
## Troubleshooting
|
||||
@@ -343,9 +346,9 @@ Broadcast groups work alongside existing routing:
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="Only one agent responding">
|
||||
**Cause:** Peer ID might be in `bindings` but not `broadcast`.
|
||||
**Cause:** Peer ID might be in ordinary route bindings but not `broadcast`, or it might match an exclusive configured ACP binding.
|
||||
|
||||
**Fix:** Add to broadcast config or remove from bindings.
|
||||
**Fix:** Add ordinary route-bound peers to broadcast config, or remove/change the configured ACP binding if fan-out broadcast is desired.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="Performance issues">
|
||||
|
||||
@@ -416,7 +416,9 @@ Enable `dynamicAgentCreation` to automatically create **isolated agent instances
|
||||
This is essential for public bots where you want each user to have their own private AI assistant experience.
|
||||
|
||||
<Note>
|
||||
**Account limitation**: `dynamicAgentCreation` currently works with the **default Feishu account only**. Named/multi-account setups are not yet fully supported — dynamic bindings are created without `accountId`, so messages to named accounts may still route to `agent:main`. Track progress in [Issue #42837](https://github.com/openclaw/openclaw/issues/42837).
|
||||
Dynamic bindings include the normalized Feishu `accountId`, so default and named accounts route each sender to the correct dynamic agent.
|
||||
|
||||
If a named account created an unscoped dynamic agent on an older release, that legacy agent still counts toward `maxAgents`. Confirm that it is not used by the default account before removing it, or temporarily increase `maxAgents`; OpenClaw cannot safely infer which account owns ambiguous legacy state.
|
||||
</Note>
|
||||
|
||||
### Quick setup
|
||||
@@ -447,7 +449,7 @@ This is essential for public bots where you want each user to have their own pri
|
||||
|
||||
When a new user sends their first DM:
|
||||
|
||||
1. The channel generates a unique `agentId` = `feishu-{user_open_id}`
|
||||
1. The channel generates a unique `agentId`: `feishu-{user_open_id}` for the default account, or a bounded account-prefixed identity digest for a named account
|
||||
2. Creates a new workspace at `workspaceTemplate` path
|
||||
3. Registers the agent and creates a binding for this user
|
||||
4. The workspace helper ensures bootstrap files (`AGENTS.md`, `SOUL.md`, `USER.md`, etc.) on first access
|
||||
@@ -464,22 +466,23 @@ When a new user sends their first DM:
|
||||
|
||||
Template variables:
|
||||
|
||||
- `{agentId}` - the generated agent ID (e.g., `feishu-ou_xxxxxx`)
|
||||
- `{agentId}` - the generated agent ID (e.g., `feishu-ou_xxxxxx` or `feishu-support-<identity_digest>`)
|
||||
- `{userId}` - the sender's Feishu open_id (e.g., `ou_xxxxxx`)
|
||||
|
||||
### Session scope
|
||||
|
||||
`session.dmScope` controls how direct messages are mapped to agent sessions. This is a **global setting** that affects all channels.
|
||||
|
||||
| Value | Behavior | Best for |
|
||||
| -------------------- | --------------------------------------------------------- | ------------------------------------------------------------------ |
|
||||
| `"main"` | Each user's DM maps to their agent's main session | Single-user bots where you want `USER.md` / `SOUL.md` to auto-load |
|
||||
| `"per-channel-peer"` | Each (channel + user) combination gets a separate session | Public multi-user bots needing stronger isolation |
|
||||
| Value | Behavior | Best for |
|
||||
| ---------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------ |
|
||||
| `"main"` | Each user's DM maps to their agent's main session | Single-user bots where you want `USER.md` / `SOUL.md` to auto-load |
|
||||
| `"per-channel-peer"` | Each (channel + user) combination gets a separate session | Public multi-user bots needing stronger isolation |
|
||||
| `"per-account-channel-peer"` | Each (account + channel + user) combination gets a separate session | Multi-account bots needing account-level session isolation |
|
||||
|
||||
**Tradeoff**: Using `"main"` enables automatic bootstrap file loading (`USER.md`, `SOUL.md`, `MEMORY.md`), but means all DMs across all channels share the same session key pattern. For public multi-user bots where isolation matters more than bootstrap auto-loading, consider `"per-channel-peer"` and manage bootstrap files manually.
|
||||
|
||||
<Note>
|
||||
`"per-account-channel-peer"` is not recommended with `dynamicAgentCreation` because dynamic bindings are created without `accountId`. Use it only with manual bindings.
|
||||
Use `"per-account-channel-peer"` when named Feishu accounts should keep separate sessions for the same sender. Dynamic bindings preserve the account scope.
|
||||
</Note>
|
||||
|
||||
```json5
|
||||
|
||||
@@ -586,7 +586,7 @@ Group inbound payloads set:
|
||||
- `WasMentioned` (mention gating result)
|
||||
- Telegram forum topics also include `MessageThreadId` and `IsForum`.
|
||||
|
||||
The agent system prompt includes a group intro on the first turn of a new group session. It reminds the model to respond like a human, avoid Markdown tables, minimize empty lines and follow normal chat spacing, and avoid typing literal `\n` sequences. Channel-sourced group names and participant labels are rendered as fenced untrusted metadata, not inline system instructions.
|
||||
The agent system prompt includes a group intro on the first turn of a new group session. It reminds the model to respond like a human, minimize empty lines and follow normal chat spacing, and avoid typing literal `\n` sequences. Non-Telegram groups also discourage Markdown tables; Telegram rich-text guidance comes from the Telegram channel prompt. Channel-sourced group names and participant labels are rendered as fenced untrusted metadata, not inline system instructions.
|
||||
|
||||
## iMessage specifics
|
||||
|
||||
|
||||
@@ -311,7 +311,6 @@ curl "https://api.telegram.org/bot<bot_token>/getUpdates"
|
||||
|
||||
- direct chats: preview message + `editMessageText`
|
||||
- groups/topics: preview message + `editMessageText`
|
||||
- direct-chat tool progress: optional native `sendMessageDraft` status preview when enabled and supported
|
||||
|
||||
Requirement:
|
||||
|
||||
@@ -320,29 +319,10 @@ curl "https://api.telegram.org/bot<bot_token>/getUpdates"
|
||||
- `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`
|
||||
- legacy `channels.telegram.streamMode`, boolean `streaming` values, and retired native draft preview keys are detected; run `openclaw doctor --fix` to migrate them to current streaming config
|
||||
|
||||
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.
|
||||
|
||||
Direct chats can use native Telegram drafts for these tool-progress lines without persisting tool chatter into chat history. Native drafts stop before answer text starts; final answers stay on the normal persistent delivery path. This lane is off by default and should be gated to trusted DM IDs first:
|
||||
|
||||
```json
|
||||
{
|
||||
"channels": {
|
||||
"telegram": {
|
||||
"streaming": {
|
||||
"mode": "partial",
|
||||
"preview": {
|
||||
"toolProgress": true,
|
||||
"nativeToolProgress": true,
|
||||
"nativeToolProgressAllowFrom": ["123456789"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
To keep the edited preview for answer text but hide tool-progress lines, set:
|
||||
|
||||
```json
|
||||
@@ -420,14 +400,16 @@ curl "https://api.telegram.org/bot<bot_token>/getUpdates"
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Formatting and HTML fallback">
|
||||
Outbound text uses Telegram `parse_mode: "HTML"`.
|
||||
<Accordion title="Rich message formatting">
|
||||
Outbound text uses Telegram rich messages.
|
||||
|
||||
- Markdown-ish text is rendered to Telegram-safe HTML.
|
||||
- Supported Telegram HTML tags are preserved; unsupported HTML is escaped.
|
||||
- If Telegram rejects parsed HTML, OpenClaw retries as plain text.
|
||||
- Markdown text is sent as rich Markdown without converting it to HTML.
|
||||
- Explicit HTML payloads are sent as rich HTML.
|
||||
- Media captions still use Telegram HTML captions because rich messages do not replace captions.
|
||||
|
||||
Link previews are enabled by default and can be disabled with `channels.telegram.linkPreview: false`.
|
||||
Long rich text is split automatically across Telegram's rich text and rich block limits. Tables over Telegram's column limit are sent as code blocks.
|
||||
|
||||
Link previews are enabled by default. `channels.telegram.linkPreview: false` skips automatic entity detection for rich text.
|
||||
|
||||
</Accordion>
|
||||
|
||||
|
||||
@@ -319,6 +319,40 @@ content and identifiers.
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## Configured ACP bindings
|
||||
|
||||
WhatsApp supports persistent ACP bindings with top-level `bindings[]` entries:
|
||||
|
||||
```json5
|
||||
{
|
||||
bindings: [
|
||||
{
|
||||
type: "acp",
|
||||
agentId: "codex",
|
||||
match: {
|
||||
channel: "whatsapp",
|
||||
accountId: "work",
|
||||
peer: { kind: "direct", id: "+15555550123" },
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "acp",
|
||||
agentId: "codex",
|
||||
match: {
|
||||
channel: "whatsapp",
|
||||
accountId: "work",
|
||||
peer: { kind: "group", id: "120363424282127706@g.us" },
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
- Direct chats match E.164 numbers such as `+15555550123`.
|
||||
- Groups match WhatsApp group JIDs such as `120363424282127706@g.us`.
|
||||
- Group allowlists, sender policy, and mention or activation gating run before OpenClaw ensures the configured ACP session exists.
|
||||
- A matched configured ACP binding owns the route. WhatsApp broadcast groups do not fan out that turn to ordinary WhatsApp sessions.
|
||||
|
||||
## Personal-number and self-chat behavior
|
||||
|
||||
When the linked self number is also present in `allowFrom`, WhatsApp self-chat safeguards activate:
|
||||
|
||||
10
docs/ci.md
10
docs/ci.md
@@ -183,7 +183,7 @@ The workflow installs OCM from a pinned release and Kova from `openclaw/Kova` at
|
||||
- `mock-deep-profile`: CPU/heap/trace profiling for startup, gateway, and agent-turn hotspots.
|
||||
- `live-openai-candidate`: a real OpenAI `openai/gpt-5.5` agent turn, skipped when `OPENAI_API_KEY` is unavailable.
|
||||
|
||||
The mock-provider lane also runs OpenClaw-native source probes after the Kova pass: gateway boot timing and memory across default, hook, and 50-plugin startup cases; bundled plugin import RSS, repeated mock-OpenAI `channel-chat-baseline` hello loops, and CLI startup commands against the booted gateway. When the previous published mock-provider source report is available for the tested ref, the source summary compares current RSS and heap values against that baseline and marks large RSS increases as `watch`. The source probe Markdown summary lives at `source/index.md` in the report bundle, with raw JSON beside it.
|
||||
The mock-provider lane also runs OpenClaw-native source probes after the Kova pass: gateway boot timing and memory across default, hook, and 50-plugin startup cases; bundled plugin import RSS, repeated mock-OpenAI `channel-chat-baseline` hello loops, CLI startup commands against the booted gateway, and the SQLite state smoke performance probe. When the previous published mock-provider source report is available for the tested ref, the source summary compares current RSS and heap values against that baseline and marks large RSS increases as `watch`. The source probe Markdown summary lives at `source/index.md` in the report bundle, with raw JSON beside it.
|
||||
|
||||
Every lane uploads GitHub artifacts. When `CLAWGRIT_REPORTS_TOKEN` is configured, the workflow also commits `report.json`, `report.md`, bundles, `index.md`, and source-probe artifacts into `openclaw/clawgrit-reports` under `openclaw-performance/<tested-ref>/<run-id>-<attempt>/<lane>/`. The current tested-ref pointer is written as `openclaw-performance/<tested-ref>/latest-<lane>.json`.
|
||||
|
||||
@@ -200,13 +200,19 @@ from `release/YYYY.M.PATCH` or `main` after the release tag exists and after the
|
||||
OpenClaw npm preflight has succeeded. It verifies `pnpm plugins:sync:check`,
|
||||
dispatches `Plugin NPM Release` for all publishable plugin packages, dispatches
|
||||
`Plugin ClawHub Release` for the same release SHA, and only then dispatches
|
||||
`OpenClaw NPM Release` with the saved `preflight_run_id`.
|
||||
`OpenClaw NPM Release` with the saved `preflight_run_id`. Stable publish also
|
||||
requires an exact `windows_node_tag`; the workflow verifies the Windows source
|
||||
release and compares its x64/ARM64 installers with the candidate-approved
|
||||
`windows_node_installer_digests` input before any publish child, then promotes
|
||||
and verifies those same pinned installer digests plus the exact companion asset
|
||||
and checksum contract before publishing the GitHub release draft.
|
||||
|
||||
```bash
|
||||
gh workflow run openclaw-release-publish.yml \
|
||||
--ref release/YYYY.M.PATCH \
|
||||
-f tag=vYYYY.M.PATCH-beta.N \
|
||||
-f preflight_run_id=<successful-openclaw-npm-preflight-run-id> \
|
||||
-f full_release_validation_run_id=<successful-full-release-validation-run-id> \
|
||||
-f npm_dist_tag=beta
|
||||
```
|
||||
|
||||
|
||||
@@ -174,7 +174,22 @@ Notes:
|
||||
or `--element`.
|
||||
- `existing-session` / `user` profiles support page screenshots and `--ref`
|
||||
screenshots from snapshot output, but not CSS `--element` screenshots.
|
||||
- `--labels` overlays current snapshot refs on the screenshot.
|
||||
- `--labels` overlays current snapshot refs on the screenshot. On
|
||||
Playwright-backed profiles, it works with `--full-page` (full-page label
|
||||
overlay), `--ref` (element-clip label overlay by ARIA ref), and `--element`
|
||||
(element-clip label overlay by CSS selector); in element-clip modes, labels
|
||||
are projected relative to the element. The response also includes an
|
||||
`annotations` array with each ref's bounding box. Each item has `ref`,
|
||||
`number`, `role`, optional `name`, and `box: {x, y, width, height}`;
|
||||
coordinates are in the captured image's space (viewport / fullpage /
|
||||
element-relative). The field is omitted when empty.
|
||||
`existing-session` profiles render a chrome-mcp overlay on page screenshots
|
||||
but do not use the Playwright projection helper and do not include
|
||||
`annotations`; CSS `--element` screenshots are unsupported there. Without
|
||||
Playwright or chrome-mcp, labeled screenshots are not available. Prior
|
||||
releases ignored `--full-page`, `--ref`, and `--element` on labeled
|
||||
Playwright screenshots and always returned a viewport capture; labeled
|
||||
screenshots now honor those scopes.
|
||||
- `snapshot --urls` appends discovered link destinations to AI snapshots so
|
||||
agents can choose direct navigation targets instead of guessing from link
|
||||
text alone.
|
||||
|
||||
@@ -182,7 +182,10 @@ Interactive onboarding behavior with reference mode:
|
||||
### Non-interactive Z.AI endpoint choices
|
||||
|
||||
<Note>
|
||||
`--auth-choice zai-api-key` auto-detects the best Z.AI endpoint for your key (prefers the general API with `zai/glm-5.1`). If you specifically want the GLM Coding Plan endpoints, pick `zai-coding-global` or `zai-coding-cn`.
|
||||
`--auth-choice zai-api-key` auto-detects the best Z.AI endpoint and model for
|
||||
your key. Coding Plan endpoints prefer `zai/glm-5.2`; general API endpoints use
|
||||
`zai/glm-5.1`. To force a Coding Plan endpoint, pick `zai-coding-global` or
|
||||
`zai-coding-cn`.
|
||||
</Note>
|
||||
|
||||
```bash
|
||||
|
||||
@@ -159,7 +159,7 @@ is available, then fall back to `latest`.
|
||||
<Accordion title="--dangerously-force-unsafe-install">
|
||||
`--dangerously-force-unsafe-install` is deprecated and is now a no-op. OpenClaw no longer runs built-in install-time dangerous-code blocking for plugin installs.
|
||||
|
||||
Use the shared operator-owned `security.installPolicy` surface when host-specific install policy is required. Plugin `before_install` hooks and `security.installPolicy` can still block installs.
|
||||
Use the shared operator-owned `security.installPolicy` surface when host-specific install policy is required. Plugin `before_install` hooks are plugin-runtime lifecycle hooks and are not the primary policy boundary for CLI installs.
|
||||
|
||||
If a plugin you published on ClawHub is hidden or blocked by a registry scan, use the publisher steps in [ClawHub publishing](/clawhub/publishing). `--dangerously-force-unsafe-install` does not ask ClawHub to rescan the plugin or make a blocked release public.
|
||||
|
||||
@@ -405,7 +405,7 @@ Updates apply to tracked plugin installs in the managed plugin index and tracked
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="--dangerously-force-unsafe-install on update">
|
||||
`--dangerously-force-unsafe-install` is also accepted on `plugins update` for compatibility, but it is deprecated and no longer changes plugin update behavior. Operator `security.installPolicy` and plugin `before_install` hooks can still block updates.
|
||||
`--dangerously-force-unsafe-install` is also accepted on `plugins update` for compatibility, but it is deprecated and no longer changes plugin update behavior. Operator `security.installPolicy` can still block updates; plugin `before_install` hooks only apply in processes where plugin hooks are loaded.
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ openclaw wiki status
|
||||
openclaw wiki doctor
|
||||
openclaw wiki init
|
||||
openclaw wiki ingest ./notes/alpha.md
|
||||
openclaw wiki okf import ./knowledge-catalog/okf/bundles/ga4
|
||||
openclaw wiki compile
|
||||
openclaw wiki lint
|
||||
openclaw wiki search "alpha"
|
||||
@@ -104,6 +105,31 @@ Notes:
|
||||
- imported source pages keep provenance in frontmatter
|
||||
- auto-compile can run after ingest when enabled
|
||||
|
||||
### `wiki okf import <path>`
|
||||
|
||||
Import an unpacked Open Knowledge Format bundle into wiki concept pages.
|
||||
|
||||
The importer reads every non-reserved `.md` concept document in the OKF
|
||||
directory tree, requires a non-empty `type` field, and treats unknown OKF
|
||||
`type` values as generic concepts. Reserved OKF `index.md` and `log.md` files
|
||||
are not imported as concepts.
|
||||
|
||||
Imported pages are flattened under `concepts/` so existing wiki compile,
|
||||
search, get, digest, and dashboard flows see them immediately. The original OKF
|
||||
concept ID, `type`, `resource`, `tags`, timestamp, source path, and full
|
||||
frontmatter are preserved in the page frontmatter. Internal OKF markdown links
|
||||
are rewritten to the generated wiki pages; broken or external links are left
|
||||
unchanged.
|
||||
|
||||
Examples:
|
||||
|
||||
```bash
|
||||
openclaw wiki okf import ./bundles/ga4
|
||||
openclaw wiki okf import ./bundles/ga4 --json
|
||||
openclaw wiki search "BigQuery Table" --mode source-evidence --json
|
||||
openclaw wiki get <path-from-json-result>
|
||||
```
|
||||
|
||||
### `wiki compile`
|
||||
|
||||
Rebuild indexes, related blocks, dashboards, and compiled digests.
|
||||
@@ -233,6 +259,8 @@ These require the official `obsidian` CLI on `PATH` when
|
||||
- Use `wiki lint` before trusting contradictory or low-confidence content.
|
||||
- Use `wiki compile` after bulk imports or source changes when you want fresh
|
||||
dashboards and compiled digests immediately.
|
||||
- Use `wiki okf import` when a data catalog, documentation export, or agent
|
||||
enrichment pipeline already emits OKF markdown bundles.
|
||||
- Use `wiki bridge import` when bridge mode depends on newly exported memory
|
||||
artifacts.
|
||||
|
||||
|
||||
@@ -479,6 +479,9 @@ names that plugin registers. Active Memory lists those tools in the recall
|
||||
prompt and passes the same list to the embedded sub-agent. If none of the
|
||||
configured tools are available, or the memory sub-agent fails, Active Memory
|
||||
skips recall for that turn and the main reply continues without memory context.
|
||||
For custom recall tools, non-empty model-visible tool output counts as recall
|
||||
evidence unless structured result fields explicitly report an empty result or
|
||||
failure.
|
||||
`toolsAllow` only accepts concrete memory tool names. Wildcards, `group:*`
|
||||
entries, and core agent tools such as `read`, `exec`, `message`, and
|
||||
`web_search` are ignored before the hidden memory sub-agent starts.
|
||||
@@ -743,7 +746,11 @@ Before v2026.5.2 the plugin silently extended your configured `timeoutMs` by an
|
||||
extra 30000 ms during cold-start so model warm-up, embedding-index load, and
|
||||
the first recall could share one larger budget. v2026.5.2 moved that grace
|
||||
behind an explicit `setupGraceTimeoutMs` config — your configured `timeoutMs`
|
||||
is now the budget by default, unless you opt in.
|
||||
is now the recall-work budget by default, unless you opt in. The blocking hook
|
||||
uses two bounded phases around that budget: up to 1500 ms for session/config
|
||||
preflight before recall starts, then a separate fixed 1500 ms for abort
|
||||
settlement and transcript recovery after recall work stops. Neither allowance
|
||||
extends model or tool execution.
|
||||
|
||||
If you upgraded from v2026.4.x and you set `timeoutMs` to a value tuned for the
|
||||
old implicit-grace world (the recommended starter `timeoutMs: 15000` is one
|
||||
@@ -765,14 +772,16 @@ outer watchdog budgets back to the pre-v5.2 effective values:
|
||||
}
|
||||
```
|
||||
|
||||
Per the v2026.5.2 changelog: _"use the configured recall timeout as the
|
||||
blocking prompt-build hook budget by default and move cold-start setup grace
|
||||
behind explicit `setupGraceTimeoutMs` config, so the plugin no longer silently
|
||||
extends 15000 ms configs to 45000 ms on the main lane."_
|
||||
The v2026.5.2 change removed the old implicit 30000 ms cold-start extension.
|
||||
Beyond the configured recall-work budget, the hook can use up to 1500 ms for
|
||||
preflight and another 1500 ms for post-recall completion. Its worst-case
|
||||
blocking time is therefore `timeoutMs + setupGraceTimeoutMs + 3000` ms.
|
||||
|
||||
The embedded recall runner uses the same effective timeout budget, so
|
||||
`setupGraceTimeoutMs` covers both the outer prompt-build watchdog and the inner
|
||||
blocking recall run.
|
||||
blocking recall run. The preflight cap covers session/config checks before that
|
||||
budget begins. The post-recall allowance lets the outer hook settle abort
|
||||
cleanup and read any final transcript state.
|
||||
|
||||
For resource-tight gateways where cold-start latency is a known trade-off,
|
||||
lower values (5000–15000 ms) work too — the trade-off is a higher chance of
|
||||
|
||||
@@ -97,7 +97,7 @@ These run inside the agent loop or gateway pipeline:
|
||||
- **`agent_end`**: inspect the final message list and run metadata after completion.
|
||||
- **`before_compaction` / `after_compaction`**: observe or annotate compaction cycles.
|
||||
- **`before_tool_call` / `after_tool_call`**: intercept tool params/results.
|
||||
- **`before_install`**: inspect install context and optionally block skill or plugin installs after operator install policy runs.
|
||||
- **`before_install`**: inspect staged skill or plugin install material after operator install policy runs, when plugin hooks are loaded in the current OpenClaw process.
|
||||
- **`tool_result_persist`**: synchronously transform tool results before they are written to an OpenClaw-owned session transcript.
|
||||
- **`message_received` / `message_sending` / `message_sent`**: inbound + outbound message hooks.
|
||||
- **`session_start` / `session_end`**: session lifecycle boundaries.
|
||||
@@ -109,6 +109,7 @@ Hook decision rules for outbound/tool guards:
|
||||
- `before_tool_call`: `{ block: false }` is a no-op and does not clear a prior block.
|
||||
- `before_install`: `{ block: true }` is terminal and stops lower-priority handlers.
|
||||
- `before_install`: `{ block: false }` is a no-op and does not clear a prior block.
|
||||
- Use `security.installPolicy`, not `before_install`, for operator-owned install allow/block decisions that must cover CLI install and update paths.
|
||||
- `message_sending`: `{ cancel: true }` is terminal and stops lower-priority handlers.
|
||||
- `message_sending`: `{ cancel: false }` is a no-op and does not clear a prior cancel.
|
||||
|
||||
|
||||
@@ -247,12 +247,13 @@ of only a bot-to-bot Slack transcript.
|
||||
evidence pipeline. It checks out the trusted candidate ref in a separate
|
||||
worktree, runs `pnpm openclaw qa telegram --credential-source convex
|
||||
--credential-role ci`, writes a `mantis-evidence.json` manifest from the
|
||||
Telegram QA summary and observed-message artifact, renders the redacted
|
||||
transcript HTML through a Crabbox desktop browser, generates a motion-trimmed GIF
|
||||
with `crabbox media preview`, and posts the inline PR evidence comment when a PR
|
||||
number is available. This lane is transcript-visual rather than logged-in
|
||||
Telegram Web proof: the Telegram Bot API gives stable live message evidence, but
|
||||
Telegram Web login state is not required for normal Mantis automation.
|
||||
Telegram QA summary, `qa-evidence.json`, and report artifacts, renders the
|
||||
redacted evidence HTML through a Crabbox desktop browser, generates a
|
||||
motion-trimmed GIF with `crabbox media preview`, and posts the inline PR
|
||||
evidence comment when a PR number is available. This lane is QA-evidence visual
|
||||
rather than logged-in Telegram Web proof: the Telegram Bot API gives stable live
|
||||
message evidence, but Telegram Web login state is not required for normal Mantis
|
||||
automation.
|
||||
|
||||
`Mantis Telegram Desktop Proof` is the agentic native Telegram Desktop
|
||||
before/after wrapper. A maintainer can trigger it from a PR comment with
|
||||
@@ -494,8 +495,8 @@ zero:
|
||||
|
||||
- `pnpm openclaw qa discord` already runs a live Discord lane with driver and
|
||||
SUT bots.
|
||||
- The live transport runner already writes reports and observed-message
|
||||
artifacts under `.artifacts/qa-e2e/`.
|
||||
- The live transport runner already writes reports, QA evidence, and
|
||||
transport-specific artifacts under `.artifacts/qa-e2e/`.
|
||||
- Convex credential leases already provide exclusive access to shared live
|
||||
transport credentials.
|
||||
- The browser control service already supports screenshots, snapshots,
|
||||
|
||||
@@ -264,7 +264,7 @@ Gemini CLI JSON replies are parsed from `response`; usage falls back to `stats`,
|
||||
|
||||
- Provider: `zai`
|
||||
- Auth: `ZAI_API_KEY`
|
||||
- Example model: `zai/glm-5.1`
|
||||
- Example model: `zai/glm-5.2`
|
||||
- CLI: `openclaw onboard --auth-choice zai-api-key`
|
||||
- Model refs use the canonical `zai/*` provider ID.
|
||||
- `zai-api-key` auto-detects the matching Z.AI endpoint; `zai-coding-global`, `zai-coding-cn`, `zai-global`, and `zai-cn` force a specific surface
|
||||
@@ -368,6 +368,7 @@ Kimi K2 model IDs:
|
||||
[//]: # "moonshot-kimi-k2-model-refs:start"
|
||||
|
||||
- `moonshot/kimi-k2.6`
|
||||
- `moonshot/kimi-k2.7-code`
|
||||
- `moonshot/kimi-k2.5`
|
||||
- `moonshot/kimi-k2-thinking`
|
||||
- `moonshot/kimi-k2-thinking-turbo`
|
||||
|
||||
@@ -318,17 +318,17 @@ Matrix has a [dedicated page](/concepts/qa-matrix) because of its scenario count
|
||||
|
||||
These lanes register through `extensions/qa-lab/src/live-transports/shared/live-transport-cli.ts` and accept the same flags:
|
||||
|
||||
| Flag | Default | Description |
|
||||
| ------------------------------------- | -------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
|
||||
| `--scenario <id>` | - | Run only this scenario. Repeatable. |
|
||||
| `--output-dir <path>` | `<repo>/.artifacts/qa-e2e/<transport>-<timestamp>` | Where reports/summary/observed messages and the output log are written. Relative paths resolve against `--repo-root`. |
|
||||
| `--repo-root <path>` | `process.cwd()` | Repository root when invoking from a neutral cwd. |
|
||||
| `--sut-account <id>` | `sut` | Temporary account id inside the QA gateway config. |
|
||||
| `--provider-mode <mode>` | `live-frontier` | `mock-openai` or `live-frontier` (legacy `live-openai` still works). |
|
||||
| `--model <ref>` / `--alt-model <ref>` | provider default | Primary/alternate model refs. |
|
||||
| `--fast` | off | Provider fast mode where supported. |
|
||||
| `--credential-source <env\|convex>` | `env` | See [Convex credential pool](#convex-credential-pool). |
|
||||
| `--credential-role <maintainer\|ci>` | `ci` in CI, `maintainer` otherwise | Role used when `--credential-source convex`. |
|
||||
| Flag | Default | Description |
|
||||
| ------------------------------------- | -------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `--scenario <id>` | - | Run only this scenario. Repeatable. |
|
||||
| `--output-dir <path>` | `<repo>/.artifacts/qa-e2e/<transport>-<timestamp>` | Where reports, summaries, evidence, transport-specific artifacts, and the output log are written. Relative paths resolve against `--repo-root`. |
|
||||
| `--repo-root <path>` | `process.cwd()` | Repository root when invoking from a neutral cwd. |
|
||||
| `--sut-account <id>` | `sut` | Temporary account id inside the QA gateway config. |
|
||||
| `--provider-mode <mode>` | `live-frontier` | `mock-openai` or `live-frontier` (legacy `live-openai` still works). |
|
||||
| `--model <ref>` / `--alt-model <ref>` | provider default | Primary/alternate model refs. |
|
||||
| `--fast` | off | Provider fast mode where supported. |
|
||||
| `--credential-source <env\|convex>` | `env` | See [Convex credential pool](#convex-credential-pool). |
|
||||
| `--credential-role <maintainer\|ci>` | `ci` in CI, `maintainer` otherwise | Role used when `--credential-source convex`. |
|
||||
|
||||
Each lane exits non-zero on any failed scenario. `--allow-failures` writes artifacts without setting a failing exit code.
|
||||
|
||||
@@ -346,10 +346,6 @@ Required env when `--credential-source env`:
|
||||
- `OPENCLAW_QA_TELEGRAM_DRIVER_BOT_TOKEN`
|
||||
- `OPENCLAW_QA_TELEGRAM_SUT_BOT_TOKEN`
|
||||
|
||||
Optional:
|
||||
|
||||
- `OPENCLAW_QA_TELEGRAM_CAPTURE_CONTENT=1` keeps message bodies in observed-message artifacts (default redacts).
|
||||
|
||||
Scenarios (`extensions/qa-lab/src/live-transports/telegram/telegram-live.runtime.ts`):
|
||||
|
||||
- `telegram-canary`
|
||||
@@ -374,27 +370,27 @@ The implicit default set always covers canary, mention gating, native command re
|
||||
Output artifacts:
|
||||
|
||||
- `telegram-qa-report.md`
|
||||
- `telegram-qa-summary.json` - includes per-reply RTT (driver send → observed SUT reply) starting with the canary.
|
||||
- `telegram-qa-observed-messages.json` - bodies redacted unless `OPENCLAW_QA_TELEGRAM_CAPTURE_CONTENT=1`.
|
||||
- `qa-evidence.json` - evidence entries for the live transport checks, including profile, coverage, provider, channel, artifacts, result, and RTT fields.
|
||||
|
||||
Package RTT comparison uses the same Telegram credential contract while keeping
|
||||
its RTT sample controls on the RTT harness path:
|
||||
Package Telegram runs use the same Telegram credential contract. Repeated RTT
|
||||
measurement is part of the normal package Telegram live lane; the RTT
|
||||
distribution is folded into `qa-evidence.json` under `result.timing` for the
|
||||
selected RTT check.
|
||||
|
||||
```bash
|
||||
pnpm rtt openclaw@beta \
|
||||
--credential-source convex \
|
||||
--credential-role maintainer \
|
||||
--samples 20 \
|
||||
--sample-timeout-ms 30000
|
||||
OPENCLAW_QA_CREDENTIAL_SOURCE=convex \
|
||||
pnpm test:docker:npm-telegram-live
|
||||
```
|
||||
|
||||
When `--credential-source convex` is set, the RTT Docker wrapper leases a
|
||||
`kind: "telegram"` credential, exports the leased group/driver/SUT bot env into
|
||||
the installed-package run, heartbeats the lease, and releases it on shutdown.
|
||||
`--samples` and `--sample-timeout-ms` still feed
|
||||
`OPENCLAW_NPM_TELEGRAM_WARM_SAMPLES` and
|
||||
`OPENCLAW_NPM_TELEGRAM_SAMPLE_TIMEOUT_MS`, so `result.json` remains comparable
|
||||
across env-backed and Convex-backed RTT runs.
|
||||
When `OPENCLAW_QA_CREDENTIAL_SOURCE=convex` is set, the package live wrapper
|
||||
leases a `kind: "telegram"` credential, exports the leased group/driver/SUT bot
|
||||
env into the installed-package run, heartbeats the lease, and releases it on
|
||||
shutdown. The package wrapper defaults to 20 RTT checks of
|
||||
`telegram-mentioned-message-reply`, a 30s RTT timeout, and Convex role
|
||||
`maintainer` outside CI when Convex is selected. Override
|
||||
`OPENCLAW_NPM_TELEGRAM_RTT_SAMPLES`, `OPENCLAW_NPM_TELEGRAM_RTT_TIMEOUT_MS`,
|
||||
or `OPENCLAW_NPM_TELEGRAM_RTT_MAX_FAILURES` to tune RTT measurement without
|
||||
creating a separate RTT command or Telegram-specific summary format.
|
||||
|
||||
### Discord QA
|
||||
|
||||
@@ -447,7 +443,7 @@ pnpm openclaw qa discord \
|
||||
Output artifacts:
|
||||
|
||||
- `discord-qa-report.md`
|
||||
- `discord-qa-summary.json`
|
||||
- `qa-evidence.json` - evidence entries for the live transport checks.
|
||||
- `discord-qa-observed-messages.json` - bodies redacted unless `OPENCLAW_QA_DISCORD_CAPTURE_CONTENT=1`.
|
||||
- `discord-qa-reaction-timelines.json` and `discord-status-reactions-tool-only-timeline.png` when the status-reaction scenario runs.
|
||||
|
||||
@@ -495,7 +491,7 @@ Scenarios (`extensions/qa-lab/src/live-transports/slack/slack-live.runtime.ts`):
|
||||
Output artifacts:
|
||||
|
||||
- `slack-qa-report.md`
|
||||
- `slack-qa-summary.json`
|
||||
- `qa-evidence.json` - evidence entries for the live transport checks.
|
||||
- `slack-qa-observed-messages.json` - bodies redacted unless `OPENCLAW_QA_SLACK_CAPTURE_CONTENT=1`.
|
||||
- `approval-checkpoints/` - only when Mantis sets
|
||||
`OPENCLAW_QA_SLACK_APPROVAL_CHECKPOINT_DIR`; contains checkpoint JSON,
|
||||
@@ -740,7 +736,7 @@ poll and upload-file coverage run through deterministic gateway `poll` and
|
||||
Output artifacts:
|
||||
|
||||
- `whatsapp-qa-report.md`
|
||||
- `whatsapp-qa-summary.json`
|
||||
- `qa-evidence.json` - evidence entries for the live transport checks.
|
||||
- `whatsapp-qa-observed-messages.json` - bodies redacted unless `OPENCLAW_QA_WHATSAPP_CAPTURE_CONTENT=1`.
|
||||
|
||||
### Convex credential pool
|
||||
@@ -787,9 +783,10 @@ the source of truth for one test run and should define:
|
||||
- docs and code refs
|
||||
- optional plugin requirements
|
||||
- optional gateway config patch
|
||||
- the executable `qa-flow`
|
||||
- an executable `qa-flow` block for flow scenarios, or `execution.kind`/`execution.path`
|
||||
for Vitest and Playwright scenarios
|
||||
|
||||
The reusable runtime surface that backs `qa-flow` is allowed to stay generic
|
||||
The reusable runtime surface that backs `qa-flow` blocks is allowed to stay generic
|
||||
and cross-cutting. For example, markdown scenarios can combine transport-side
|
||||
helpers with browser-side helpers that drive the embedded Control UI through the
|
||||
Gateway `browser.request` seam without adding a special-case runner.
|
||||
@@ -915,6 +912,7 @@ The report should answer:
|
||||
For the inventory of available scenarios - useful when sizing follow-up work or wiring a new transport - run `pnpm openclaw qa coverage` (add `--json` for machine-readable output).
|
||||
When choosing focused proof for a touched behavior or file path, run `pnpm openclaw qa coverage --match <query>`.
|
||||
The match report searches scenario metadata, docs refs, code refs, coverage IDs, plugins, and provider requirements, then prints matching `qa suite --scenario ...` targets.
|
||||
Every `qa suite` scenario execution writes a `qa-evidence.json` artifact. Flow scenarios also write `qa-suite-summary.json` for existing suite/report tooling; scenarios that declare `execution.kind: vitest` or `execution.kind: playwright` run the matching test path and write `qa-vitest-report.md` or `qa-playwright-report.md` plus per-scenario logs.
|
||||
Treat it as a discovery aid, not a gate replacement; the selected scenario still needs the right provider mode, live transport, Multipass, Testbox, or release lane for the behavior under test.
|
||||
|
||||
For character and style checks, run the same scenario across multiple live model
|
||||
|
||||
@@ -30,6 +30,201 @@ title: "Usage tracking"
|
||||
- CLI: `openclaw channels list` prints the same usage snapshot alongside provider config (use `--no-usage` to skip).
|
||||
- macOS menu bar: "Usage" section under Context (only if available).
|
||||
|
||||
## Custom `/usage full` footer
|
||||
|
||||
`/usage full` shows a built-in compact footer with model, reasoning, fast/slow,
|
||||
context window, turn tokens, cache, and cost when those fields are available. No
|
||||
template file is required.
|
||||
|
||||
`messages.usageTemplate` is only for advanced custom layouts. The value is a
|
||||
JSON file path (supports `~`) or an inline object, and it replaces the built-in
|
||||
footer when valid:
|
||||
|
||||
```json
|
||||
{
|
||||
"messages": {
|
||||
"usageTemplate": "~/.openclaw/usage-footer.json"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Missing or empty templates fall back to the built-in footer quietly. Unreadable
|
||||
or invalid configured templates also fall back to the built-in footer and emit an
|
||||
operator warning.
|
||||
|
||||
Start custom templates from the built-in shape, then edit the parts you want to
|
||||
change:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"schema": "openclaw.usageBar.v1",
|
||||
"scales": {
|
||||
"braille": "⠐⡀⡄⡆⡇⣇⣧⣷⣿",
|
||||
"block": "░▏▎▍▌▋▊▉█",
|
||||
"shade": "░▒▓█",
|
||||
"moon": "🌑🌘🌗🌖🌕",
|
||||
"level": "▁▂▃▄▅▆▇█",
|
||||
"weather": ["🥶", "☁️", "🌥", "⛅️", "🌤", "☀️"],
|
||||
"plants": ["", "🍂", "🌱", "☘️", "🍀", "🌿"],
|
||||
"moons6": ["🌑", "🌚", "🌘", "🌗", "🌖", "🌝"],
|
||||
},
|
||||
"aliases": {
|
||||
"models": {
|
||||
"claude-opus-4-6": "opus46",
|
||||
"claude-opus-4-8": "opus48",
|
||||
"claude-sonnet-4-6": "sonnet46",
|
||||
"claude-haiku-4-5": "haiku45",
|
||||
"gpt-5.5": "gpt5.5",
|
||||
},
|
||||
"reasoning": {
|
||||
"off": "🌑",
|
||||
"minimal": "🌚",
|
||||
"low": "🌘",
|
||||
"medium": "🌗",
|
||||
"high": "🌕",
|
||||
"xhigh": "🌝",
|
||||
},
|
||||
},
|
||||
"output": {
|
||||
"sep": "",
|
||||
"default": [
|
||||
{ "text": "{model.provider}{identity.emoji|🤖} {model.display_name|alias:models}" },
|
||||
{ "map": "model.is_fallback", "cases": { "true": " 🔄" } },
|
||||
{ "map": "model.is_override", "cases": { "true": " 📌" } },
|
||||
{ "when": "model.reasoning", "text": " {model.reasoning|alias:reasoning}" },
|
||||
{ "map": "state.fast_mode", "cases": { "true": " ⚡", "false": " 🐌" } },
|
||||
{
|
||||
"when": "context.max_tokens",
|
||||
"text": " | 📚 [{context.pct_used|meter:5:braille}]{context.max_tokens|num}",
|
||||
},
|
||||
{
|
||||
"when": "usage.has_split_tokens",
|
||||
"text": " ↕️ {usage.input_tokens|num|?}/{usage.output_tokens|num|?}",
|
||||
},
|
||||
{ "when": "usage.has_total_only_tokens", "text": " ↕️ {usage.total_tokens|num}" },
|
||||
{ "when": "usage.cache_hit_pct", "text": " 🗄 {usage.cache_hit_pct|pct}" },
|
||||
{ "when": "cost.turn_usd", "text": " 💰{cost.turn_usd|fixed:4}" },
|
||||
],
|
||||
"surfaces": {
|
||||
"discord": [
|
||||
{ "text": "-# -\n" },
|
||||
{ "text": "-# {model.provider}{identity.emoji|🤖} {model.display_name|alias:models}" },
|
||||
{ "map": "model.is_fallback", "cases": { "true": "🔄" } },
|
||||
{ "map": "model.is_override", "cases": { "true": "📌" } },
|
||||
{ "when": "model.reasoning", "text": " {model.reasoning|alias:reasoning}" },
|
||||
{ "map": "state.fast_mode", "cases": { "true": " ⚡️", "false": " 🐌" } },
|
||||
{
|
||||
"when": "context.max_tokens",
|
||||
"text": " | 📚 [{context.pct_used|meter:5:braille}]{context.max_tokens|num}",
|
||||
},
|
||||
{
|
||||
"when": "usage.has_split_tokens",
|
||||
"text": " ↕️ {usage.input_tokens|num|?}/{usage.output_tokens|num|?}",
|
||||
},
|
||||
{ "when": "usage.has_total_only_tokens", "text": " ↕️ {usage.total_tokens|num}" },
|
||||
{ "when": "usage.cache_hit_pct", "text": " 🗄 {usage.cache_hit_pct|pct}" },
|
||||
{ "when": "cost.turn_usd", "text": " 💰{cost.turn_usd|fixed:4}" },
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### Shape
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"schema": "openclaw.usageBar.v1",
|
||||
"scales": { "<name>": "low-to-high glyphs" }, // string (1 glyph/char) or array
|
||||
"aliases": { "<table>": { "<value>": "<label>" } },
|
||||
"output": {
|
||||
"sep": "", // joins surviving pieces
|
||||
"default": [
|
||||
/* pieces */
|
||||
], // fallback for any surface
|
||||
"surfaces": {
|
||||
"discord": [
|
||||
/* pieces */
|
||||
],
|
||||
"telegram": [
|
||||
/* pieces */
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Each surface is an ordered list of **pieces**; the engine renders each, drops
|
||||
empties, and joins survivors with `sep`. A surface with no entry uses
|
||||
`output.default`.
|
||||
|
||||
### Contract Paths
|
||||
|
||||
A piece reads values from the per-turn contract by dot-path. Absent values are
|
||||
empty (so a `when` guard or a `|fallback` keeps the piece clean).
|
||||
|
||||
| Path | Meaning |
|
||||
| ----------------------------------------------------------------------------------- | -------------------------------------- |
|
||||
| `surface` | channel id (`discord`/`telegram`/etc.) |
|
||||
| `model.provider` / `model.display_name` | provider id / model id |
|
||||
| `model.reasoning` | effort (`off` through `xhigh`) |
|
||||
| `model.is_fallback` / `model.is_override` | bool: fallback used / model pinned |
|
||||
| `state.fast_mode` | bool: fast vs slow |
|
||||
| `context.max_tokens` / `context.pct_used` | window budget / 0-100 used |
|
||||
| `usage.input_tokens` / `usage.output_tokens` / `usage.total_tokens` | turn aggregate |
|
||||
| `usage.has_split_tokens` / `usage.has_total_only_tokens` / `usage.cache_hit_pct` | token display guards and cache percent |
|
||||
| `usage.last.input_tokens` / `usage.last.output_tokens` / `usage.last.cache_hit_pct` | final model call only |
|
||||
| `cost.turn_usd` | estimated turn cost |
|
||||
| `identity.name` / `identity.emoji` | agent name / chosen emoji |
|
||||
|
||||
(Provider rate-limit windows are **not** in this contract.)
|
||||
|
||||
### Verbs
|
||||
|
||||
Pipe a value through verbs left to right; a non-verb segment is the fallback.
|
||||
|
||||
| Verb | Effect | Example |
|
||||
| --------------- | ------------------------------------- | --------------------------------- |
|
||||
| `num` | compact count | `272000 -> 272k` |
|
||||
| `fixed:N` | N decimals (default 2) | `0.0377` |
|
||||
| `dur` | seconds to duration | `14820 -> 4h07m` |
|
||||
| `pct` | append `%` | `96 -> 96%` |
|
||||
| `inv` | `100 - x` | for used to remaining |
|
||||
| `alias:TABLE` | lookup in `aliases`, echo if unlisted | `medium -> 🌗` |
|
||||
| `meter:W:SCALE` | W-cell glyph bar over a 0-100 value | `[⣿⣿⠐⠐⠐]` (`meter:1` = one glyph) |
|
||||
|
||||
### Piece forms
|
||||
|
||||
- `{ "text": "📚 {context.max_tokens|num}" }`: literal + interpolation.
|
||||
- `{ "when": "<path>", "text": "..." }`: render only if the path is truthy.
|
||||
- `{ "map": "<path>", "cases": { "true": "⚡", "false": "🐌" } }`: value to glyph.
|
||||
- `{ "each": "limits.windows", "item": "{label}" }`: iterate an array.
|
||||
|
||||
### Example
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"schema": "openclaw.usageBar.v1",
|
||||
"scales": { "braille": "⠐⡀⡄⡆⡇⣇⣧⣷⣿" },
|
||||
"aliases": { "reasoning": { "medium": "🌗", "high": "🌕" } },
|
||||
"output": {
|
||||
"surfaces": {
|
||||
"discord": [
|
||||
{ "text": "{model.display_name}" },
|
||||
{ "when": "model.reasoning", "text": " {model.reasoning|alias:reasoning}" },
|
||||
{ "map": "state.fast_mode", "cases": { "true": " ⚡", "false": " 🐌" } },
|
||||
{
|
||||
"when": "context.max_tokens",
|
||||
"text": " | 📚 [{context.pct_used|meter:5:braille}]{context.max_tokens|num}",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
renders e.g. `claude-sonnet-4-6 🌗 🐌 | 📚 [⣿⣿⣿⣿⣧]272k`.
|
||||
|
||||
## Providers + credentials
|
||||
|
||||
- **Anthropic (Claude)**: OAuth tokens in auth profiles.
|
||||
|
||||
@@ -130,6 +130,8 @@ WhatsApp runs through the gateway's web channel (Baileys Web). It starts automat
|
||||
}
|
||||
```
|
||||
|
||||
- Top-level `bindings[]` entries with `type: "acp"` configure persistent ACP bindings for WhatsApp DMs and groups. Use an E.164 direct number or WhatsApp group JID in `match.peer.id`. Field semantics are shared in [ACP Agents](/tools/acp-agents#persistent-channel-bindings).
|
||||
|
||||
<Accordion title="Multi-account WhatsApp">
|
||||
|
||||
```json5
|
||||
|
||||
@@ -339,7 +339,7 @@ Configures inbound media understanding (image/audio/video):
|
||||
|
||||
- `capabilities`: optional list (`image`, `audio`, `video`). Defaults: `openai`/`anthropic`/`minimax` → image, `google` → image+audio+video, `groq` → audio.
|
||||
- `prompt`, `maxChars`, `maxBytes`, `timeoutSeconds`, `language`: per-entry overrides.
|
||||
- `tools.media.image.timeoutSeconds` and matching image model `timeoutSeconds` entries also apply when the agent calls the explicit `image` tool.
|
||||
- `tools.media.image.timeoutSeconds` and matching image model `timeoutSeconds` entries also apply when the agent calls the explicit `image` tool. For image understanding, this timeout applies to the request itself and is not reduced by earlier preparation work.
|
||||
- Failures fall back to the next entry.
|
||||
|
||||
Provider auth follows standard order: `auth-profiles.json` → env vars → `models.providers.*.apiKey`.
|
||||
|
||||
@@ -493,6 +493,8 @@ example `~/.agents/skills/manager -> ~/Projects/manager/skills`.
|
||||
- `extraDirs` scans the sibling repo as an explicit skill root.
|
||||
- `allowSymlinkTargets` lets symlinked skill folders resolve into that trusted
|
||||
real target root without allowing arbitrary symlink escapes.
|
||||
- To let Skill Workshop apply write through the same trusted symlink target,
|
||||
set `skills.workshop.allowSymlinkTargetWrites: true`.
|
||||
|
||||
## Common patterns
|
||||
|
||||
|
||||
@@ -200,6 +200,9 @@ See [MCP](/cli/mcp#openclaw-as-an-mcp-client-registry) and
|
||||
nodeManager: "npm", // npm | pnpm | yarn | bun
|
||||
allowUploadedArchives: false,
|
||||
},
|
||||
workshop: {
|
||||
allowSymlinkTargetWrites: false,
|
||||
},
|
||||
entries: {
|
||||
"image-lab": {
|
||||
apiKey: { source: "env", provider: "default", id: "GEMINI_API_KEY" }, // or plaintext string
|
||||
@@ -216,6 +219,8 @@ See [MCP](/cli/mcp#openclaw-as-an-mcp-client-registry) and
|
||||
- `load.extraDirs`: extra shared skill roots (lowest precedence).
|
||||
- `load.allowSymlinkTargets`: trusted real target roots that skill symlinks may
|
||||
resolve into when the link lives outside its configured source root.
|
||||
- `workshop.allowSymlinkTargetWrites`: allows Skill Workshop apply to write
|
||||
through already-trusted symlink targets (default: false).
|
||||
- `install.preferBrew`: when true, prefer Homebrew installers when `brew` is
|
||||
available before falling back to other installer kinds.
|
||||
- `install.nodeManager`: node installer preference for `metadata.openclaw.install`
|
||||
|
||||
@@ -42,6 +42,21 @@ health commands above for live connectivity checks.
|
||||
- `channels.<provider>.accounts.<accountId>.healthMonitor.enabled`: multi-account override that wins over the channel-level setting.
|
||||
- These per-channel overrides apply to the built-in channel monitors that expose them today: Discord, Google Chat, iMessage, Microsoft Teams, Signal, Slack, Telegram, and WhatsApp.
|
||||
|
||||
## Uptime monitoring
|
||||
|
||||
External uptime monitoring services should use the dedicated `/health` endpoint, not `/v1/chat/completions`.
|
||||
|
||||
- **DO use:** `GET /health` — instant response, no session created, no LLM call, returns `{"ok":true,"status":"live"}`
|
||||
- **DON'T use:** `/v1/chat/completions` for health checks — each request creates a full agent session with skill snapshot, context assembly, and LLM calls
|
||||
|
||||
When no `x-openclaw-session-key` header or `user` field is provided, `/v1/chat/completions` generates a new random session for each request. Monitoring services that ping every 15 minutes create ~96 sessions/day, each consuming 4–22KB. Over time this causes session store bloat and can lead to context window overflow.
|
||||
|
||||
### Monitoring service setup examples
|
||||
|
||||
- **BetterStack:** Set health check URL to `https://<your-gateway-host>:<port>/health`
|
||||
- **UptimeRobot:** Add a new HTTP monitor with URL `https://<your-gateway-host>:<port>/health`
|
||||
- **Generic:** Any HTTP GET to `/health` returns 200 with `{"ok":true}` when the gateway is healthy
|
||||
|
||||
## When something fails
|
||||
|
||||
- `logged out` or status 409–515 → relink with `openclaw channels logout` then `openclaw channels login`.
|
||||
|
||||
@@ -75,6 +75,7 @@ Auth matrix:
|
||||
- honor `x-openclaw-scopes` when the header is present
|
||||
- fall back to the normal operator default scope set when the header is absent
|
||||
- only lose owner semantics when the caller explicitly narrows scopes and omits `operator.admin`
|
||||
- require `operator.admin` for owner-level request controls such as `x-openclaw-model`
|
||||
|
||||
See [Security](/gateway/security) and [Remote access](/gateway/remote).
|
||||
|
||||
@@ -96,7 +97,7 @@ OpenClaw treats the OpenAI `model` field as an **agent target**, not a raw provi
|
||||
|
||||
Optional request headers:
|
||||
|
||||
- `x-openclaw-model: <provider/model-or-bare-id>` overrides the backend model for the selected agent.
|
||||
- `x-openclaw-model: <provider/model-or-bare-id>` overrides the backend model for the selected agent. Shared-secret bearer callers can use this header. Identity-bearing callers, such as trusted-proxy or private no-auth ingress requests with `x-openclaw-scopes`, need `operator.admin`; write-only callers get `403 missing scope: operator.admin`.
|
||||
- `x-openclaw-agent-id: <agentId>` remains supported as a compatibility override.
|
||||
- `x-openclaw-session-key: <sessionKey>` fully controls session routing.
|
||||
- `x-openclaw-message-channel: <channel>` sets the synthetic ingress channel context for channel-aware prompts and policies.
|
||||
@@ -178,7 +179,7 @@ This is the highest-leverage compatibility set for self-hosted frontends and too
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="How do I override the backend model?">
|
||||
Use `x-openclaw-model`.
|
||||
Use `x-openclaw-model`. This is an owner-level override: it works with the Gateway shared-secret bearer token/password path, and it requires `operator.admin` on identity-bearing HTTP paths such as trusted proxy auth.
|
||||
|
||||
Examples:
|
||||
`x-openclaw-model: openai/gpt-5.4`
|
||||
@@ -191,7 +192,7 @@ This is the highest-leverage compatibility set for self-hosted frontends and too
|
||||
`/v1/embeddings` uses the same agent-target `model` ids.
|
||||
|
||||
Use `model: "openclaw/default"` or `model: "openclaw/<agentId>"`.
|
||||
When you need a specific embedding model, send it in `x-openclaw-model`.
|
||||
When you need a specific embedding model, send it in `x-openclaw-model` from a shared-secret caller or an identity-bearing caller with `operator.admin`.
|
||||
Without that header, the request passes through to the selected agent's normal embedding setup.
|
||||
|
||||
</Accordion>
|
||||
@@ -285,7 +286,7 @@ Expected behavior:
|
||||
|
||||
- `GET /v1/models` should list `openclaw/default`
|
||||
- Open WebUI should use `openclaw/default` as the chat model id
|
||||
- If you want a specific backend provider/model for that agent, set the agent's normal default model or send `x-openclaw-model`
|
||||
- If you want a specific backend provider/model for that agent, set the agent's normal default model or send `x-openclaw-model` from a shared-secret caller or an identity-bearing caller with `operator.admin`
|
||||
|
||||
Quick smoke:
|
||||
|
||||
@@ -370,7 +371,7 @@ Notes:
|
||||
|
||||
- `/v1/models` returns OpenClaw agent targets, not raw provider catalogs.
|
||||
- `openclaw/default` is always present so one stable id works across environments.
|
||||
- Backend provider/model overrides belong in `x-openclaw-model`, not the OpenAI `model` field.
|
||||
- Backend provider/model overrides belong in `x-openclaw-model`, not the OpenAI `model` field. On identity-bearing HTTP auth paths, this header requires `operator.admin`.
|
||||
- `/v1/embeddings` supports `input` as a string or array of strings.
|
||||
|
||||
## Related
|
||||
|
||||
@@ -951,7 +951,7 @@ Important boundary note:
|
||||
- Treat credentials that can call `/v1/chat/completions`, `/v1/responses`, plugin routes such as `/api/v1/admin/rpc`, or `/api/channels/*` as full-access operator secrets for that gateway.
|
||||
- On the OpenAI-compatible HTTP surface, shared-secret bearer auth restores the full default operator scopes (`operator.admin`, `operator.approvals`, `operator.pairing`, `operator.read`, `operator.talk.secrets`, `operator.write`) and owner semantics for agent turns; narrower `x-openclaw-scopes` values do not reduce that shared-secret path.
|
||||
- Per-request scope semantics on HTTP only apply when the request comes from an identity-bearing mode such as trusted proxy auth, or from an explicitly no-auth private ingress.
|
||||
- In those identity-bearing modes, omitting `x-openclaw-scopes` falls back to the normal operator default scope set; send the header explicitly when you want a narrower scope set.
|
||||
- In those identity-bearing modes, omitting `x-openclaw-scopes` falls back to the normal operator default scope set; send the header explicitly when you want a narrower scope set. Owner-level OpenAI-compatible headers such as `x-openclaw-model` require `operator.admin` when scopes are narrowed.
|
||||
- `/tools/invoke` and HTTP session history endpoints follow the same shared-secret rule: token/password bearer auth is treated as full operator access there too, while identity-bearing modes still honor declared scopes.
|
||||
- Do not share these credentials with untrusted callers; prefer separate gateways per trust boundary.
|
||||
|
||||
|
||||
@@ -154,6 +154,10 @@ Do not use broad targets such as `~`, `/`, or a whole synced project folder.
|
||||
Keep `allowSymlinkTargets` scoped to the real skill root that contains trusted
|
||||
`SKILL.md` directories.
|
||||
|
||||
If Skill Workshop apply should also write through those trusted symlinked
|
||||
workspace skill paths, enable `skills.workshop.allowSymlinkTargetWrites`. Keep
|
||||
it disabled for read-only shared skill roots.
|
||||
|
||||
Related:
|
||||
|
||||
- [Skills config](/tools/skills-config#symlinked-sibling-repos)
|
||||
|
||||
@@ -73,7 +73,7 @@ Live tests are split into two layers so we can isolate failures:
|
||||
- `pnpm test:live` (or `OPENCLAW_LIVE_TEST=1` if invoking Vitest directly)
|
||||
- Set `OPENCLAW_LIVE_MODELS=modern`, `small`, or `all` (alias for modern) to actually run this suite; otherwise it skips to keep `pnpm test:live` focused on gateway smoke
|
||||
- How to select models:
|
||||
- `OPENCLAW_LIVE_MODELS=modern` to run the modern allowlist (Opus/Sonnet 4.6+, GPT-5.2 + Codex, Gemini 3, DeepSeek V4, GLM 4.7, MiniMax M3, Grok 4.3)
|
||||
- `OPENCLAW_LIVE_MODELS=modern` to run the modern allowlist (Opus/Sonnet 4.6+, GPT-5.2 + Codex, Gemini 3, DeepSeek V4, GLM 5.1, MiniMax M3, Grok 4.3)
|
||||
- `OPENCLAW_LIVE_MODELS=small` to run the constrained small-model allowlist (Qwen 8B/9B local-compatible routes, Ollama Gemma, OpenRouter Qwen/GLM, and Z.AI GLM)
|
||||
- `OPENCLAW_LIVE_MODELS=all` is an alias for the modern allowlist
|
||||
- or `OPENCLAW_LIVE_MODELS="openai/gpt-5.5,anthropic/claude-opus-4-6,..."` (comma allowlist)
|
||||
@@ -357,6 +357,9 @@ Narrow, explicit allowlists are fastest and least flaky:
|
||||
- Tool calling across several providers:
|
||||
- `OPENCLAW_LIVE_GATEWAY_MODELS="openai/gpt-5.5,anthropic/claude-opus-4-6,google/gemini-3-flash-preview,deepseek/deepseek-v4-flash,zai/glm-5.1,minimax/MiniMax-M3" pnpm test:live src/gateway/gateway-models.profiles.live.test.ts`
|
||||
|
||||
- Z.AI Coding Plan GLM-5.2 direct smoke:
|
||||
- `ZAI_CODING_LIVE_TEST=1 pnpm test:live src/agents/zai.live.test.ts`
|
||||
|
||||
- Google focus (Gemini API key + Antigravity):
|
||||
- Gemini (API key): `OPENCLAW_LIVE_GATEWAY_MODELS="google/gemini-3-flash-preview" pnpm test:live src/gateway/gateway-models.profiles.live.test.ts`
|
||||
- Antigravity (OAuth): `OPENCLAW_LIVE_GATEWAY_MODELS="google-antigravity/claude-opus-4-6-thinking,google-antigravity/gemini-3-pro-high" pnpm test:live src/gateway/gateway-models.profiles.live.test.ts`
|
||||
@@ -388,7 +391,7 @@ This is the "common models" run we expect to keep working:
|
||||
- Google (Gemini API): `google/gemini-3.1-pro-preview` and `google/gemini-3-flash-preview` (avoid older Gemini 2.x models)
|
||||
- Google (Antigravity): `google-antigravity/claude-opus-4-6-thinking` and `google-antigravity/gemini-3-flash`
|
||||
- DeepSeek: `deepseek/deepseek-v4-flash` and `deepseek/deepseek-v4-pro`
|
||||
- Z.AI (GLM): `zai/glm-5.1`
|
||||
- Z.AI (GLM): `zai/glm-5.1` (general API) or `zai/glm-5.2` (Coding Plan)
|
||||
- MiniMax: `minimax/MiniMax-M3`
|
||||
|
||||
Run gateway smoke with tools + image:
|
||||
@@ -402,7 +405,7 @@ Pick at least one per provider family:
|
||||
- Anthropic: `anthropic/claude-opus-4-6` (or `anthropic/claude-sonnet-4-6`)
|
||||
- Google: `google/gemini-3-flash-preview` (or `google/gemini-3.1-pro-preview`)
|
||||
- DeepSeek: `deepseek/deepseek-v4-flash`
|
||||
- Z.AI (GLM): `zai/glm-5.1`
|
||||
- Z.AI (GLM): `zai/glm-5.1` (general API) or `zai/glm-5.2` (Coding Plan)
|
||||
- MiniMax: `minimax/MiniMax-M3`
|
||||
|
||||
Optional additional coverage (nice to have):
|
||||
|
||||
@@ -218,17 +218,27 @@ inside every shard.
|
||||
`OPENCLAW_NPM_TELEGRAM_PACKAGE_TGZ=/path/to/openclaw-current.tgz` or
|
||||
`OPENCLAW_CURRENT_PACKAGE_TGZ` to test a resolved local tarball instead of
|
||||
installing from the registry.
|
||||
- Emits repeated RTT timing in `qa-evidence.json` by default with
|
||||
`OPENCLAW_NPM_TELEGRAM_RTT_SAMPLES=20`. Override
|
||||
`OPENCLAW_NPM_TELEGRAM_RTT_SAMPLES`,
|
||||
`OPENCLAW_NPM_TELEGRAM_RTT_TIMEOUT_MS`, or
|
||||
`OPENCLAW_NPM_TELEGRAM_RTT_MAX_FAILURES` to tune the RTT run.
|
||||
`OPENCLAW_NPM_TELEGRAM_RTT_CHECKS` accepts a comma-separated list of
|
||||
Telegram QA check IDs to sample; when unset, the default RTT-capable check
|
||||
is `telegram-mentioned-message-reply`.
|
||||
- Uses the same Telegram env credentials or Convex credential source as
|
||||
`pnpm openclaw qa telegram`. For CI/release automation, set
|
||||
`OPENCLAW_NPM_TELEGRAM_CREDENTIAL_SOURCE=convex` plus
|
||||
`OPENCLAW_QA_CONVEX_SITE_URL` and the role secret. If
|
||||
`OPENCLAW_QA_CONVEX_SITE_URL` and a role secret. If
|
||||
`OPENCLAW_QA_CONVEX_SITE_URL` and a Convex role secret are present in CI,
|
||||
the Docker wrapper selects Convex automatically.
|
||||
- The wrapper validates Telegram or Convex credential env on the host before
|
||||
Docker build/install work. Set `OPENCLAW_NPM_TELEGRAM_SKIP_CREDENTIAL_PREFLIGHT=1`
|
||||
only when deliberately debugging pre-credential setup.
|
||||
- `OPENCLAW_NPM_TELEGRAM_CREDENTIAL_ROLE=ci|maintainer` overrides the shared
|
||||
`OPENCLAW_QA_CREDENTIAL_ROLE` for this lane only.
|
||||
`OPENCLAW_QA_CREDENTIAL_ROLE` for this lane only. When Convex credentials
|
||||
are selected and no role is set, the wrapper uses `ci` in CI and
|
||||
`maintainer` outside CI.
|
||||
- GitHub Actions exposes this lane as the manual maintainer workflow
|
||||
`NPM Telegram Beta E2E`. It does not run on merge. The workflow uses the
|
||||
`qa-live-shared` environment and Convex CI credential leases.
|
||||
@@ -344,11 +354,11 @@ gh workflow run package-acceptance.yml --ref main \
|
||||
want artifacts without a failing exit code.
|
||||
- Requires two distinct bots in the same private group, with the SUT bot exposing a Telegram username.
|
||||
- For stable bot-to-bot observation, enable Bot-to-Bot Communication Mode in `@BotFather` for both bots and ensure the driver bot can observe group bot traffic.
|
||||
- Writes a Telegram QA report, summary, and observed-messages artifact under `.artifacts/qa-e2e/...`. Replying scenarios include RTT from driver send request to observed SUT reply.
|
||||
- Writes a Telegram QA report, summary, and `qa-evidence.json` under `.artifacts/qa-e2e/...`. Replying scenarios include RTT from driver send request to observed SUT reply.
|
||||
|
||||
`Mantis Telegram Live` is the PR-evidence wrapper around this lane. It runs the
|
||||
candidate ref with Convex-leased Telegram credentials, renders the redacted
|
||||
observed-message transcript in a Crabbox desktop browser, records MP4 evidence,
|
||||
candidate ref with Convex-leased Telegram credentials, renders the redacted QA
|
||||
report/evidence bundle in a Crabbox desktop browser, records MP4 evidence,
|
||||
generates a motion-trimmed GIF, uploads the artifact bundle, and posts inline PR
|
||||
evidence through the Mantis GitHub App when `pr_number` is set. Maintainers can
|
||||
start it from the Actions UI through `Mantis Scenario` (`scenario_id:
|
||||
|
||||
@@ -214,6 +214,59 @@ permission boundary. Dangerous plugin node commands still require explicit
|
||||
After a node changes its declared command list, reject the old device pairing
|
||||
and approve the new request so the gateway stores the updated command snapshot.
|
||||
|
||||
## Config (`openclaw.json`)
|
||||
|
||||
Node-related settings live under `gateway.nodes` and `tools.exec`:
|
||||
|
||||
```json5
|
||||
{
|
||||
gateway: {
|
||||
nodes: {
|
||||
// Auto-approve first-time node pairing from trusted networks (CIDR list).
|
||||
// Disabled when unset. Only applies to first-time role:node requests
|
||||
// with no requested scopes; does not auto-approve upgrades.
|
||||
pairing: {
|
||||
autoApproveCidrs: ["192.168.1.0/24"],
|
||||
},
|
||||
// Opt into dangerous/privacy-heavy node commands (camera.snap, etc.).
|
||||
allowCommands: ["camera.snap", "screen.record"],
|
||||
// Block exact command names even if defaults or allowCommands include them.
|
||||
denyCommands: ["camera.clip"],
|
||||
},
|
||||
},
|
||||
tools: {
|
||||
exec: {
|
||||
// Default exec host: "node" routes all exec calls to a paired node.
|
||||
host: "node",
|
||||
// Security mode for node exec: allow only approved/allowlisted commands.
|
||||
security: "allowlist",
|
||||
// Pin exec to a specific node (id or name). Omit to allow any node.
|
||||
node: "build-node",
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Use exact node command names. `denyCommands` removes a command even when a
|
||||
platform default or `allowCommands` entry would otherwise allow it. See
|
||||
[Gateway configuration reference](/gateway/configuration-reference#gateway-field-details)
|
||||
for gateway node pairing and command-policy field details.
|
||||
|
||||
Per-agent exec node override:
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
list: [
|
||||
{
|
||||
id: "main",
|
||||
tools: { exec: { node: "build-node" } },
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Screenshots (canvas snapshots)
|
||||
|
||||
If the node is showing the Canvas (WebView), `canvas.snapshot` returns `{ format, base64 }`.
|
||||
|
||||
@@ -18,11 +18,13 @@ most Linux-compatible Gateway runtime.
|
||||
Windows Hub is the native WinUI companion app for Windows 10 20H2+ and Windows 11. It installs without administrator privileges and is published with signed
|
||||
x64 and ARM64 installers on OpenClaw releases.
|
||||
|
||||
Download the latest stable installer:
|
||||
Download the latest stable installer from the [OpenClaw releases page](https://github.com/openclaw/openclaw/releases):
|
||||
|
||||
- [OpenClawCompanion-Setup-x64.exe](https://github.com/openclaw/openclaw/releases/latest/download/OpenClawCompanion-Setup-x64.exe)
|
||||
- [OpenClawCompanion-Setup-arm64.exe](https://github.com/openclaw/openclaw/releases/latest/download/OpenClawCompanion-Setup-arm64.exe)
|
||||
- [Checksums](https://github.com/openclaw/openclaw/releases/latest/download/OpenClawCompanion-SHA256SUMS.txt)
|
||||
- [OpenClawCompanion-Setup-x64.exe](https://github.com/openclaw/openclaw/releases/download/v2026.6.5/OpenClawCompanion-Setup-x64.exe)
|
||||
- [OpenClawCompanion-Setup-arm64.exe](https://github.com/openclaw/openclaw/releases/download/v2026.6.5/OpenClawCompanion-Setup-arm64.exe)
|
||||
- [Checksums](https://github.com/openclaw/openclaw/releases/download/v2026.6.5/OpenClawCompanion-SHA256SUMS.txt)
|
||||
|
||||
If a download link above returns a 404, visit the [releases page](https://github.com/openclaw/openclaw/releases) and look for the `OpenClawCompanion-Setup-*` assets on the latest release.
|
||||
|
||||
After install, launch **OpenClaw Companion** from the Start menu or the system
|
||||
tray. The installer also adds shortcuts for Gateway Setup, Chat, Settings,
|
||||
|
||||
@@ -197,22 +197,30 @@ only for behavior that really belongs to the backend.
|
||||
|
||||
`CliBackendPlugin` can also define:
|
||||
|
||||
| Hook | Use |
|
||||
| ---------------------------------- | ------------------------------------------------------ |
|
||||
| `normalizeConfig(config, context)` | Rewrite legacy user config after merge |
|
||||
| `resolveExecutionArgs(ctx)` | Add request-scoped flags such as thinking effort |
|
||||
| `prepareExecution(ctx)` | Create temporary auth or config bridges before launch |
|
||||
| `transformSystemPrompt(ctx)` | Apply a final CLI-specific system prompt transform |
|
||||
| `textTransforms` | Bidirectional prompt/output replacements |
|
||||
| `defaultAuthProfileId` | Prefer a specific OpenClaw auth profile |
|
||||
| `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 |
|
||||
| Hook | Use |
|
||||
| ---------------------------------- | --------------------------------------------------------------------------- |
|
||||
| `normalizeConfig(config, context)` | Rewrite legacy user config after merge |
|
||||
| `resolveExecutionArgs(ctx)` | Add request-scoped flags such as thinking effort or side-question isolation |
|
||||
| `prepareExecution(ctx)` | Create temporary auth or config bridges before launch |
|
||||
| `transformSystemPrompt(ctx)` | Apply a final CLI-specific system prompt transform |
|
||||
| `textTransforms` | Bidirectional prompt/output replacements |
|
||||
| `defaultAuthProfileId` | Prefer a specific OpenClaw auth profile |
|
||||
| `authEpochMode` | Decide how auth changes invalidate stored CLI sessions |
|
||||
| `nativeToolMode` | Declare whether the CLI has always-on native tools |
|
||||
| `sideQuestionToolMode` | Declare disabled native tools for `/btw` side questions |
|
||||
| `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.
|
||||
|
||||
`ctx.executionMode` is `"agent"` for normal turns and `"side-question"` for
|
||||
ephemeral `/btw` calls. Use it when the CLI needs different one-shot flags, such
|
||||
as disabling native tools, session persistence, or resume behavior for BTW. If a
|
||||
backend normally has `nativeToolMode: "always-on"` but its side-question argv
|
||||
reliably disables those tools, also set `sideQuestionToolMode: "disabled"`;
|
||||
otherwise OpenClaw fails closed when BTW requires a no-tools CLI run.
|
||||
|
||||
### `ownsNativeCompaction`: opting out of OpenClaw compaction
|
||||
|
||||
If your backend runs an agent that compacts its **own** transcript, set
|
||||
|
||||
@@ -313,9 +313,13 @@ available timeout in this order:
|
||||
- For `image_generate` without a configured timeout, the 120 second
|
||||
image-generation default.
|
||||
- For the media-understanding `image` tool, `tools.media.image.timeoutSeconds`
|
||||
converted to milliseconds, or the 60 second media default.
|
||||
converted to milliseconds, or the 60 second media default. For image
|
||||
understanding, this applies to the request itself and is not reduced by
|
||||
earlier preparation work.
|
||||
- The 90 second dynamic-tool default.
|
||||
|
||||
This watchdog is the outer dynamic `item/tool/call` budget. Provider-specific
|
||||
request timeouts run inside that call and keep their own timeout semantics.
|
||||
Dynamic tool budgets are capped at 600000 ms. On timeout, OpenClaw aborts the
|
||||
tool signal where supported and returns a failed dynamic-tool response to Codex
|
||||
so the turn can continue instead of leaving the session in `processing`.
|
||||
|
||||
@@ -143,12 +143,39 @@ The native Codex app-server harness supports context engines that require
|
||||
pre-prompt assembly. Generic CLI backends, including `codex-cli`, do not provide
|
||||
that host capability.
|
||||
|
||||
Codex thread bindings live in OpenClaw's SQLite plugin state and use the stable
|
||||
agent-scoped OpenClaw session key, or an opaque conversation-binding id, as
|
||||
their owner. Physical session ids fence delayed cleanup but may rotate without
|
||||
losing the Codex thread. Context-engine compaction adopts the successor id
|
||||
before continuing native Codex compaction. The bounded store rejects a new
|
||||
binding at its safety limit instead of evicting an existing thread's continuity
|
||||
record.
|
||||
Conversation binds create or resume their Codex thread on the first bound
|
||||
message after channel approval; an abandoned approval consumes no thread row.
|
||||
That first message carries the prepared thread directly into its turn.
|
||||
Subsequent messages use a metadata-only resume to subscribe the shared client,
|
||||
then unsubscribe after the turn completes.
|
||||
The runtime does not poll transcript-adjacent binding files. Upgrades from
|
||||
releases that used `*.jsonl.codex-app-server.json` sidecars migrate them during
|
||||
normal startup preflight. `openclaw doctor --fix` can run the same migration
|
||||
manually.
|
||||
Successfully matched sidecars are archived before the new runtime resumes their
|
||||
threads. Migration imports durable thread ownership only; it does not infer
|
||||
Codex context usage from OpenClaw counters or crawl Codex rollout files. For
|
||||
agent-session harness bindings, the next resume attempts to restore a cached
|
||||
native snapshot when Codex has one, and ongoing turns persist the current-context
|
||||
usage reported by app-server notifications, not the cumulative thread lifetime
|
||||
total. Conversation bindings
|
||||
keep metadata-only resumes and leave continuity and compaction with the native
|
||||
Codex thread. Conflicting or ambiguous sidecars stay in place with a warning for
|
||||
operator review.
|
||||
|
||||
For Codex-backed agents, `/compact` starts native Codex app-server compaction on
|
||||
the bound thread. OpenClaw does not wait for completion, impose an OpenClaw
|
||||
timeout, restart the shared app-server, or fall back to a context-engine or
|
||||
public OpenAI summarizer. If the native Codex thread binding is missing or
|
||||
stale, the command fails closed so the operator sees the real runtime boundary
|
||||
instead of silently switching compaction backends.
|
||||
the bound thread. OpenClaw bounds the request-acceptance RPC but does not wait
|
||||
for compaction completion, restart the shared app-server, or fall back to a
|
||||
context-engine or public OpenAI summarizer. If the native Codex thread binding
|
||||
is missing or stale, the command fails closed so the operator sees the real
|
||||
runtime boundary instead of silently switching compaction backends.
|
||||
|
||||
```json5
|
||||
{
|
||||
@@ -557,10 +584,14 @@ or shortens that specific tool budget. The `image_generate` tool uses
|
||||
`agents.defaults.imageGenerationModel.timeoutMs` when the tool call does not
|
||||
provide its own timeout, or a 120 second image-generation default otherwise.
|
||||
The media-understanding `image` tool uses
|
||||
`tools.media.image.timeoutSeconds` or its 60 second media default. Dynamic tool
|
||||
budgets are capped at 600000 ms. On timeout, OpenClaw aborts the tool signal
|
||||
`tools.media.image.timeoutSeconds` or its 60 second media default. For image
|
||||
understanding, that timeout applies to the request itself and is not
|
||||
reduced by earlier preparation work. Dynamic tool budgets are
|
||||
capped at 600000 ms. On timeout, OpenClaw aborts the tool signal
|
||||
where supported and returns a failed dynamic-tool response to Codex so the turn
|
||||
can continue instead of leaving the session in `processing`.
|
||||
This watchdog is the outer dynamic `item/tool/call` budget; provider-specific
|
||||
request timeouts run inside that call and keep their own timeout semantics.
|
||||
|
||||
After Codex accepts a turn, and after OpenClaw responds to a turn-scoped
|
||||
app-server request, the harness expects Codex to make current-turn progress and
|
||||
|
||||
@@ -128,6 +128,10 @@ Current compatibility records include:
|
||||
- legacy runtime aliases such as `api.runtime.taskFlow`,
|
||||
`api.runtime.subagent.getSession`, `api.runtime.stt`, and deprecated
|
||||
`api.runtime.config.loadConfig()` / `api.runtime.config.writeConfigFile(...)`
|
||||
- WhatsApp `WebInboundMessage` flat callback fields such as `body`, `chatId`,
|
||||
`reply(...)`, and `mediaPath` while callback consumers migrate to the nested
|
||||
`WebInboundCallbackMessage` `event`, `payload`, `quote`, `group`, and
|
||||
`platform` contexts
|
||||
- legacy memory-plugin split registration while memory plugins move to
|
||||
`registerMemoryCapability`
|
||||
- legacy memory-specific embedding provider registration while embedding
|
||||
@@ -160,6 +164,33 @@ New plugin code should prefer the replacement listed in the registry and in the
|
||||
specific migration guide. Existing plugins can keep using a compatibility path
|
||||
until the docs, diagnostics, and release notes announce a removal window.
|
||||
|
||||
### WhatsApp Inbound Callback Flat Aliases
|
||||
|
||||
WhatsApp runtime callbacks deliver `WebInboundMessage`: the canonical nested
|
||||
`event`, `payload`, `quote`, `group`, and `platform` contexts plus deprecated
|
||||
flat aliases for the shipped callback fields. New callback code should read the
|
||||
nested contexts. Code that constructs clean nested callback messages can use
|
||||
`WebInboundCallbackMessage`; compatibility listeners that still inject old flat
|
||||
test or plugin messages should use `LegacyFlatWebInboundMessage` or
|
||||
`WebInboundMessageInput`.
|
||||
|
||||
The flat aliases remain available until **2026-08-30**. That removal window
|
||||
applies only to flat alias access; the nested callback shape is the canonical
|
||||
runtime contract. The TypeScript `@deprecated` annotations on each flat alias
|
||||
name its exact nested replacement. Common examples:
|
||||
|
||||
- `id`, `timestamp`, and `isBatched` move under `event`.
|
||||
- `body`, `mediaPath`, `mediaType`, `mediaFileName`, `mediaUrl`, `location`, and
|
||||
`untrustedStructuredContext` move under `payload`.
|
||||
- `to`, `chatId`, sender/self fields, `sendComposing`, `reply(...)`, and
|
||||
`sendMedia(...)` move under `platform`.
|
||||
- `replyTo*` fields move under `quote`, and group subject/participant/mention
|
||||
fields move under `group`.
|
||||
|
||||
`payload.untrustedStructuredContext` is extracted from inbound provider payloads.
|
||||
Plugins should inspect the `label`, `source`, and `type` before treating its
|
||||
`payload` as authoritative.
|
||||
|
||||
## Release notes
|
||||
|
||||
Release notes should include upcoming plugin deprecations with target dates and
|
||||
|
||||
@@ -152,7 +152,8 @@ observation-only.
|
||||
- `gateway_start` / `gateway_stop` - start or stop plugin-owned services with the Gateway
|
||||
- `deactivate` - deprecated compatibility alias for `gateway_stop`; use `gateway_stop` in new plugins
|
||||
- `cron_changed` - observe gateway-owned cron lifecycle changes (added, updated, removed, started, finished, scheduled)
|
||||
- **`before_install`** - inspect skill or plugin install context and optionally block
|
||||
- **`before_install`** - inspect staged skill or plugin install material from a loaded
|
||||
plugin runtime
|
||||
|
||||
## Debug runtime hooks
|
||||
|
||||
@@ -425,6 +426,10 @@ even when the channel payload has no visible text/caption. Rewriting that
|
||||
`content` updates the hook-visible transcript only; it is not rendered as a
|
||||
media caption.
|
||||
|
||||
`reply_payload_sending` events may include `usageState`, a best-effort live
|
||||
per-turn model/usage/context snapshot. Durable delivery, recovered replay, and
|
||||
replies without exact run correlation omit it.
|
||||
|
||||
Message hook contexts expose stable correlation fields when available:
|
||||
`ctx.sessionKey`, `ctx.runId`, `ctx.messageId`, `ctx.senderId`, `ctx.trace`,
|
||||
`ctx.traceId`, `ctx.spanId`, `ctx.parentSpanId`, and `ctx.callDepth`. Inbound
|
||||
@@ -458,11 +463,19 @@ Decision rules:
|
||||
|
||||
## Install hooks
|
||||
|
||||
`before_install` runs after the operator-owned `security.installPolicy` check
|
||||
when one is configured. The `builtinScan` field remains in the event payload for
|
||||
compatibility, but OpenClaw no longer runs built-in install-time dangerous-code
|
||||
blocking, so it is an empty `ok` result. Return additional findings or
|
||||
`{ block: true, blockReason }` to stop the install.
|
||||
Use `security.installPolicy` for operator-owned allow/block decisions. That
|
||||
policy runs from OpenClaw config, covers CLI install and update paths, and fails
|
||||
closed when enabled but unavailable.
|
||||
|
||||
`before_install` is a plugin-runtime lifecycle hook. It runs after
|
||||
`security.installPolicy` only in the OpenClaw process where plugin hooks have
|
||||
already been loaded, such as Gateway-backed install flows. It is useful for
|
||||
plugin-owned observations, warnings, and compatibility checks, but it is not the
|
||||
primary enterprise or host security boundary for installs. The `builtinScan`
|
||||
field remains in the event payload for compatibility, but OpenClaw no longer
|
||||
runs built-in install-time dangerous-code blocking, so it is an empty `ok`
|
||||
result. Return additional findings or `{ block: true, blockReason }` to stop the
|
||||
install in that process.
|
||||
|
||||
`block: true` is terminal. `block: false` is treated as no decision.
|
||||
Handler failures block the install fail-closed.
|
||||
|
||||
@@ -25,6 +25,7 @@ less like a pile of Markdown files.
|
||||
- Page-level provenance, confidence, contradictions, and open questions
|
||||
- Compiled digests for agent/runtime consumers
|
||||
- Wiki-native search/get/apply/lint tools
|
||||
- Open Knowledge Format imports into compiled wiki concepts
|
||||
- Optional bridge mode that imports public artifacts from the active memory plugin
|
||||
- Optional Obsidian-friendly render mode and CLI integration
|
||||
|
||||
@@ -135,6 +136,34 @@ The main page groups are:
|
||||
- `syntheses/` for compiled summaries and maintained rollups
|
||||
- `reports/` for generated dashboards
|
||||
|
||||
## Open Knowledge Format imports
|
||||
|
||||
`memory-wiki` can import unpacked Open Knowledge Format bundles with:
|
||||
|
||||
```bash
|
||||
openclaw wiki okf import ./bundles/ga4
|
||||
```
|
||||
|
||||
This is the cleanest fit when a data catalog, documentation crawler, or
|
||||
enrichment agent already produces OKF: keep OKF as the portable exchange
|
||||
artifact, then let `memory-wiki` turn it into OpenClaw-native concept pages and
|
||||
compiled digests.
|
||||
|
||||
The importer follows the OKF v0.1 shape:
|
||||
|
||||
- non-reserved `.md` files are concept documents
|
||||
- each imported concept needs a non-empty `type` frontmatter field
|
||||
- unknown OKF `type` values are accepted
|
||||
- reserved `index.md` and `log.md` files are not imported as concepts
|
||||
- broken or external markdown links are preserved
|
||||
|
||||
Imported concept pages are flattened under `concepts/` so the existing compile,
|
||||
search, get, dashboard, and prompt-digest paths see them without adding a second
|
||||
wiki tree. Each page keeps the original OKF concept ID, source path, `type`,
|
||||
`resource`, `tags`, timestamp, and full producer frontmatter. Internal OKF links
|
||||
are rewritten to the generated wiki concept pages and also emitted as structured
|
||||
`relationships` entries with `kind: okf-link`.
|
||||
|
||||
## Structured claims and evidence
|
||||
|
||||
Pages can carry structured `claims` frontmatter, not just freeform text.
|
||||
|
||||
@@ -378,7 +378,10 @@ AI CLI backend such as `claude-cli` or `my-cli`.
|
||||
(for example normalizing old flag shapes).
|
||||
- Use `resolveExecutionArgs` for request-scoped argv rewrites that belong to
|
||||
the CLI dialect, such as mapping OpenClaw thinking levels to a native effort
|
||||
flag.
|
||||
flag. The hook receives `ctx.executionMode`; use `"side-question"` to add
|
||||
backend-native isolation flags for ephemeral `/btw` calls. If those flags
|
||||
reliably disable native tools for an otherwise always-on CLI, declare
|
||||
`sideQuestionToolMode: "disabled"` too.
|
||||
|
||||
For an end-to-end authoring guide, see
|
||||
[CLI backend plugins](/plugins/cli-backend-plugins).
|
||||
@@ -428,6 +431,10 @@ semantics.
|
||||
|
||||
### Hook decision semantics
|
||||
|
||||
`before_install` is a plugin-runtime lifecycle hook, not the operator install
|
||||
policy surface. Use `security.installPolicy` when an allow/block decision must
|
||||
cover CLI and Gateway-backed install or update paths.
|
||||
|
||||
- `before_tool_call`: returning `{ block: true }` is terminal. Once any handler sets it, lower-priority handlers are skipped.
|
||||
- `before_tool_call`: returning `{ block: false }` is treated as no decision (same as omitting `block`), not as an override.
|
||||
- `before_install`: returning `{ block: true }` is terminal. Once any handler sets it, lower-priority handlers are skipped.
|
||||
|
||||
@@ -515,6 +515,7 @@ API key auth, and dynamic model resolution.
|
||||
|
||||
- `openclaw/plugin-sdk/provider-model-shared` - `ProviderReplayFamily`, `buildProviderReplayFamilyHooks(...)`, and the raw replay builders (`buildOpenAICompatibleReplayPolicy`, `buildAnthropicReplayPolicyForModel`, `buildGoogleGeminiReplayPolicy`, `buildHybridAnthropicOrOpenAIReplayPolicy`). Also exports Gemini replay helpers (`sanitizeGoogleGeminiReplayHistory`, `resolveTaggedReasoningOutputMode`) and endpoint/model helpers (`resolveProviderEndpoint`, `normalizeProviderId`, `normalizeGooglePreviewModelId`).
|
||||
- `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-stream-shared` - lightweight payload and event wrappers for hot provider paths, including `createOpenAICompatibleCompletionsThinkingOffWrapper`, `createPayloadPatchStreamWrapper`, and `createPlainTextToolCallCompatWrapper`.
|
||||
- `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
|
||||
|
||||
@@ -164,7 +164,7 @@ and pairing-path families.
|
||||
| `plugin-sdk/provider-tools` | `ProviderToolCompatFamily`, `buildProviderToolCompatFamilyHooks`, and DeepSeek/Gemini/OpenAI schema cleanup + diagnostics |
|
||||
| `plugin-sdk/provider-usage` | Provider usage snapshot types, shared usage fetch helpers, and provider fetchers such as `fetchClaudeUsage` |
|
||||
| `plugin-sdk/provider-stream` | `ProviderStreamFamily`, `buildProviderStreamFamilyHooks`, `composeProviderStreamWrappers`, stream wrapper types, plain-text tool-call compat, and shared Anthropic/Bedrock/DeepSeek V4/Google/Kilocode/Moonshot/OpenAI/OpenRouter/Z.A.I/MiniMax/Copilot wrapper helpers |
|
||||
| `plugin-sdk/provider-stream-shared` | Public shared provider stream wrapper helpers including `composeProviderStreamWrappers`, `createPlainTextToolCallCompatWrapper`, `createPayloadPatchStreamWrapper`, `createToolStreamWrapper`, and Anthropic/DeepSeek/OpenAI-compatible stream utilities |
|
||||
| `plugin-sdk/provider-stream-shared` | Public shared provider stream wrapper helpers including `composeProviderStreamWrappers`, `createOpenAICompatibleCompletionsThinkingOffWrapper`, `createPlainTextToolCallCompatWrapper`, `createPayloadPatchStreamWrapper`, `createToolStreamWrapper`, and Anthropic/DeepSeek/OpenAI-compatible stream utilities |
|
||||
| `plugin-sdk/provider-transport-runtime` | Native provider transport helpers such as guarded fetch, transport message transforms, and writable transport event streams |
|
||||
| `plugin-sdk/provider-onboard` | Onboarding config patch helpers |
|
||||
| `plugin-sdk/global-singleton` | Process-local singleton/map/cache helpers |
|
||||
@@ -236,6 +236,7 @@ usage endpoint failed or returned no usable usage data.
|
||||
| `plugin-sdk/config-contracts` | Focused type-only config surface for plugin config shapes such as `OpenClawConfig` and channel/provider config types |
|
||||
| `plugin-sdk/plugin-config-runtime` | Runtime plugin-config lookup helpers such as `requireRuntimeConfig`, `resolvePluginConfigObject`, and `resolveLivePluginConfigObject` |
|
||||
| `plugin-sdk/config-mutation` | Transactional config mutation helpers such as `mutateConfigFile`, `replaceConfigFile`, and `logConfigUpdated` |
|
||||
| `plugin-sdk/message-tool-delivery-hints` | Shared message-tool delivery metadata hint strings |
|
||||
| `plugin-sdk/runtime-config-snapshot` | Current process config snapshot helpers such as `getRuntimeConfig`, `getRuntimeConfigSnapshot`, and test snapshot setters |
|
||||
| `plugin-sdk/telegram-command-config` | Telegram command-name/description normalization and duplicate/conflict checks, even when the bundled Telegram contract surface is unavailable |
|
||||
| `plugin-sdk/text-autolink-runtime` | File-reference autolink detection without the broad text barrel |
|
||||
|
||||
@@ -22,6 +22,7 @@ Moonshot and Kimi Coding are **separate providers**. Keys are not interchangeabl
|
||||
| Model ref | Name | Reasoning | Input | Context | Max output |
|
||||
| --------------------------------- | ---------------------- | --------- | ----------- | ------- | ---------- |
|
||||
| `moonshot/kimi-k2.6` | Kimi K2.6 | No | text, image | 262,144 | 262,144 |
|
||||
| `moonshot/kimi-k2.7-code` | Kimi K2.7 Code | Always on | text, image | 262,144 | 262,144 |
|
||||
| `moonshot/kimi-k2.5` | Kimi K2.5 | No | text, image | 262,144 | 262,144 |
|
||||
| `moonshot/kimi-k2-thinking` | Kimi K2 Thinking | Yes | text | 262,144 | 262,144 |
|
||||
| `moonshot/kimi-k2-thinking-turbo` | Kimi K2 Thinking Turbo | Yes | text | 262,144 | 262,144 |
|
||||
@@ -30,11 +31,18 @@ Moonshot and Kimi Coding are **separate providers**. Keys are not interchangeabl
|
||||
[//]: # "moonshot-kimi-k2-ids:end"
|
||||
|
||||
Bundled cost estimates for current Moonshot-hosted K2 models use Moonshot's
|
||||
published pay-as-you-go rates: Kimi K2.6 is $0.16/MTok cache hit,
|
||||
published pay-as-you-go rates: Kimi K2.7 Code is $0.19/MTok cache hit,
|
||||
$0.95/MTok input, and $4.00/MTok output; Kimi K2.6 is $0.16/MTok cache hit,
|
||||
$0.95/MTok input, and $4.00/MTok output; Kimi K2.5 is $0.10/MTok cache hit,
|
||||
$0.60/MTok input, and $3.00/MTok output. Other legacy catalog entries keep
|
||||
zero-cost placeholders unless you override them in config.
|
||||
|
||||
Kimi K2.7 Code always uses native thinking. OpenClaw exposes only the `on`
|
||||
thinking state for this model and omits outbound `thinking` and
|
||||
`reasoning_effort` controls, as required by Moonshot. OpenClaw also omits
|
||||
sampling overrides that K2.7 fixes to provider defaults. Kimi K2.6 remains the
|
||||
onboarding default.
|
||||
|
||||
## Getting started
|
||||
|
||||
Choose your provider and follow the setup steps.
|
||||
@@ -109,6 +117,7 @@ Choose your provider and follow the setup steps.
|
||||
models: {
|
||||
// moonshot-kimi-k2-aliases:start
|
||||
"moonshot/kimi-k2.6": { alias: "Kimi K2.6" },
|
||||
"moonshot/kimi-k2.7-code": { alias: "Kimi K2.7 Code" },
|
||||
"moonshot/kimi-k2.5": { alias: "Kimi K2.5" },
|
||||
"moonshot/kimi-k2-thinking": { alias: "Kimi K2 Thinking" },
|
||||
"moonshot/kimi-k2-thinking-turbo": { alias: "Kimi K2 Thinking Turbo" },
|
||||
@@ -135,6 +144,15 @@ Choose your provider and follow the setup steps.
|
||||
contextWindow: 262144,
|
||||
maxTokens: 262144,
|
||||
},
|
||||
{
|
||||
id: "kimi-k2.7-code",
|
||||
name: "Kimi K2.7 Code",
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: { input: 0.95, output: 4, cacheRead: 0.19, cacheWrite: 0 },
|
||||
contextWindow: 262144,
|
||||
maxTokens: 262144,
|
||||
},
|
||||
{
|
||||
id: "kimi-k2.5",
|
||||
name: "Kimi K2.5",
|
||||
@@ -288,7 +306,13 @@ Config lives under `plugins.entries.moonshot.config.webSearch`:
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Native thinking mode">
|
||||
Moonshot Kimi supports binary native thinking:
|
||||
Kimi K2.7 Code always uses native thinking. Moonshot requires clients to
|
||||
omit the `thinking` field for this model, so OpenClaw exposes only `on` and
|
||||
ignores stale `off` settings. K2.7 also fixes `temperature`, `top_p`, `n`,
|
||||
`presence_penalty`, and `frequency_penalty`; OpenClaw omits configured
|
||||
overrides for those fields.
|
||||
|
||||
Other Moonshot Kimi models support binary native thinking:
|
||||
|
||||
- `thinking: { type: "enabled" }`
|
||||
- `thinking: { type: "disabled" }`
|
||||
@@ -311,7 +335,7 @@ Config lives under `plugins.entries.moonshot.config.webSearch`:
|
||||
}
|
||||
```
|
||||
|
||||
OpenClaw also maps runtime `/think` levels for Moonshot:
|
||||
OpenClaw maps runtime `/think` levels for those models:
|
||||
|
||||
| `/think` level | Moonshot behavior |
|
||||
| -------------------- | -------------------------- |
|
||||
@@ -319,14 +343,16 @@ Config lives under `plugins.entries.moonshot.config.webSearch`:
|
||||
| Any non-off level | `thinking.type=enabled` |
|
||||
|
||||
<Warning>
|
||||
When Moonshot thinking is enabled, `tool_choice` must be `auto` or `none`. OpenClaw normalizes incompatible `tool_choice` values to `auto` for compatibility.
|
||||
When Moonshot thinking is enabled, `tool_choice` must be `auto` or `none`. OpenClaw normalizes incompatible values to `auto`. This includes Kimi K2.7 Code, whose thinking mode cannot be disabled to preserve a pinned tool choice.
|
||||
</Warning>
|
||||
|
||||
Kimi K2.6 also accepts an optional `thinking.keep` field that controls
|
||||
multi-turn retention of `reasoning_content`. Set it to `"all"` to keep full
|
||||
reasoning across turns; omit it (or leave it `null`) to use the server
|
||||
default strategy. OpenClaw only forwards `thinking.keep` for
|
||||
`moonshot/kimi-k2.6` and strips it from other models.
|
||||
`moonshot/kimi-k2.6` and strips it from other models. Kimi K2.7 Code
|
||||
preserves full reasoning history by default while OpenClaw omits the entire
|
||||
`thinking` field.
|
||||
|
||||
```json5
|
||||
{
|
||||
@@ -347,7 +373,7 @@ Config lives under `plugins.entries.moonshot.config.webSearch`:
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Tool call id sanitization">
|
||||
Moonshot Kimi serves tool_call ids shaped like `functions.<name>:<index>`. OpenClaw preserves them unchanged so multi-turn tool calls keep working.
|
||||
Moonshot Kimi serves native tool_call ids shaped like `functions.<name>:<index>`. For the OpenAI-completions transport, OpenClaw preserves the first occurrence of each native Kimi id and rewrites later duplicates to deterministic OpenAI-style `call_*` ids. Matching tool results are remapped with the same id so replay remains unique without stripping Kimi's first native id.
|
||||
|
||||
To force strict sanitization on a custom OpenAI-compatible provider, set `sanitizeToolCallIds: true`:
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ OpenClaw uses the `zai` provider with a Z.AI API key.
|
||||
## GLM models
|
||||
|
||||
GLM is a model family, not a separate provider. In OpenClaw, GLM models use
|
||||
refs such as `zai/glm-5.1`: provider `zai`, model id `glm-5.1`.
|
||||
refs such as `zai/glm-5.2`: provider `zai`, model id `glm-5.2`.
|
||||
|
||||
## Getting started
|
||||
|
||||
@@ -85,12 +85,12 @@ you want to force a specific Coding Plan or general API surface.
|
||||
models: {
|
||||
providers: {
|
||||
zai: {
|
||||
// Example value. Onboarding writes the matching baseUrl for your endpoint.
|
||||
baseUrl: "https://api.z.ai/api/paas/v4",
|
||||
// GLM-5.2 uses the Coding Plan endpoint.
|
||||
baseUrl: "https://api.z.ai/api/coding/paas/v4",
|
||||
},
|
||||
},
|
||||
},
|
||||
agents: { defaults: { model: { primary: "zai/glm-5.1" } } },
|
||||
agents: { defaults: { model: { primary: "zai/glm-5.2" } } },
|
||||
}
|
||||
```
|
||||
|
||||
@@ -105,28 +105,31 @@ openclaw models list --all --provider zai
|
||||
|
||||
The manifest-backed catalog currently includes:
|
||||
|
||||
| Model ref | Notes |
|
||||
| -------------------- | ------------- |
|
||||
| `zai/glm-5.1` | Default model |
|
||||
| `zai/glm-5` | |
|
||||
| `zai/glm-5-turbo` | |
|
||||
| `zai/glm-5v-turbo` | |
|
||||
| `zai/glm-4.7` | |
|
||||
| `zai/glm-4.7-flash` | |
|
||||
| `zai/glm-4.7-flashx` | |
|
||||
| `zai/glm-4.6` | |
|
||||
| `zai/glm-4.6v` | |
|
||||
| `zai/glm-4.5` | |
|
||||
| `zai/glm-4.5-air` | |
|
||||
| `zai/glm-4.5-flash` | |
|
||||
| `zai/glm-4.5v` | |
|
||||
| Model ref | Notes |
|
||||
| -------------------- | ------------------------------- |
|
||||
| `zai/glm-5.2` | Coding Plan default; 1M context |
|
||||
| `zai/glm-5.1` | General API default |
|
||||
| `zai/glm-5` | |
|
||||
| `zai/glm-5-turbo` | |
|
||||
| `zai/glm-5v-turbo` | |
|
||||
| `zai/glm-4.7` | |
|
||||
| `zai/glm-4.7-flash` | |
|
||||
| `zai/glm-4.7-flashx` | |
|
||||
| `zai/glm-4.6` | |
|
||||
| `zai/glm-4.6v` | |
|
||||
| `zai/glm-4.5` | |
|
||||
| `zai/glm-4.5-air` | |
|
||||
| `zai/glm-4.5-flash` | |
|
||||
| `zai/glm-4.5v` | |
|
||||
|
||||
<Tip>
|
||||
GLM models are available as `zai/<model>` (example: `zai/glm-5`).
|
||||
</Tip>
|
||||
|
||||
<Note>
|
||||
The default bundled model ref is `zai/glm-5.1`. GLM versions and availability
|
||||
Coding Plan setup defaults to `zai/glm-5.2`; general API setup keeps
|
||||
`zai/glm-5.1`. Endpoint auto-detection falls back to `glm-5.1` or `glm-4.7`
|
||||
when the selected plan does not expose GLM-5.2. GLM versions and availability
|
||||
can change; run `openclaw models list --all --provider zai` to see the catalog
|
||||
known to your installed version.
|
||||
</Note>
|
||||
@@ -173,7 +176,7 @@ known to your installed version.
|
||||
agents: {
|
||||
defaults: {
|
||||
models: {
|
||||
"zai/glm-5.1": {
|
||||
"zai/glm-5.2": {
|
||||
params: { preserveThinking: true },
|
||||
},
|
||||
},
|
||||
|
||||
@@ -99,10 +99,14 @@ the maintainer-only release runbook.
|
||||
file, lane, workflow job, package profile, provider, or model allowlist that
|
||||
proves the fix. Rerun the full umbrella only when the changed surface makes
|
||||
prior evidence stale.
|
||||
9. For beta, tag `vYYYY.M.PATCH-beta.N`, then run `pnpm release:candidate -- --tag
|
||||
vYYYY.M.PATCH-beta.N` from the matching `release/YYYY.M.PATCH` branch. The helper runs
|
||||
the local generated-release checks, dispatches or verifies the full release
|
||||
validation and npm preflight evidence, runs Parallels and Telegram package
|
||||
9. For a tagged beta candidate, run
|
||||
`pnpm release:candidate -- --tag vYYYY.M.PATCH-beta.N` from the matching
|
||||
`release/YYYY.M.PATCH` branch. For stable, pass the required Windows source
|
||||
release too:
|
||||
`pnpm release:candidate -- --tag vYYYY.M.PATCH --windows-node-tag vX.Y.Z`.
|
||||
The helper runs the local generated-release checks, dispatches or verifies
|
||||
the full release validation and npm preflight evidence, runs Parallels
|
||||
fresh/update proof against the exact prepared tarball plus Telegram package
|
||||
proof, records plugin npm and ClawHub plans, and prints the exact
|
||||
`OpenClaw Release Publish` command only after the evidence bundle is green.
|
||||
`OpenClaw Release Publish` dispatches the selected or all-publishable plugin
|
||||
@@ -142,9 +146,12 @@ vYYYY.M.PATCH-beta.N` from the matching `release/YYYY.M.PATCH` branch. The helpe
|
||||
direct push, it opens or updates an appcast PR. Stable Windows Hub
|
||||
readiness requires the signed `OpenClawCompanion-Setup-x64.exe`,
|
||||
`OpenClawCompanion-Setup-arm64.exe`, and
|
||||
`OpenClawCompanion-SHA256SUMS.txt` assets on the OpenClaw GitHub release;
|
||||
promote them with the `Windows Node Release` workflow after the matching
|
||||
`openclaw/openclaw-windows-node` release has passed its signing workflow.
|
||||
`OpenClawCompanion-SHA256SUMS.txt` assets on the OpenClaw GitHub release.
|
||||
Pass the exact signed `openclaw/openclaw-windows-node` release tag as
|
||||
`windows_node_tag` and its candidate-approved installer digest map as
|
||||
`windows_node_installer_digests`; `OpenClaw Release Publish` keeps the
|
||||
release draft, dispatches `Windows Node Release`, and verifies all three
|
||||
assets before publication.
|
||||
11. After publish, run the npm post-publish verifier, optional standalone
|
||||
published-npm Telegram E2E when you need post-publish channel proof,
|
||||
dist-tag promotion when needed, verify the generated GitHub release page,
|
||||
@@ -253,21 +260,36 @@ vYYYY.M.PATCH-beta.N` from the matching `release/YYYY.M.PATCH` branch. The helpe
|
||||
to the GitHub release as `openclaw-<version>-dependency-evidence.zip`.
|
||||
- Run `OpenClaw Release Publish` for the mutating publish sequence after the
|
||||
tag exists. Dispatch it from `release/YYYY.M.PATCH` (or `main` when publishing a
|
||||
main-reachable tag), pass the release tag and successful OpenClaw npm
|
||||
`preflight_run_id`, and keep the default plugin publish scope
|
||||
`all-publishable` unless you are deliberately running a focused repair. The
|
||||
workflow serializes plugin npm publish, plugin ClawHub publish, and OpenClaw
|
||||
npm publish so the core package is not published before its externalized
|
||||
plugins.
|
||||
- Run the manual `Windows Node Release` workflow for stable releases after the
|
||||
matching `openclaw/openclaw-windows-node` release exists. It downloads the
|
||||
signed Windows Hub installers from the companion repo, verifies their
|
||||
Authenticode signatures on a Windows runner, writes a SHA-256 manifest, and
|
||||
uploads the installers plus manifest onto the canonical OpenClaw GitHub
|
||||
release. Website download links should target exact OpenClaw release asset
|
||||
URLs for the current stable release, or `releases/latest/download/...` only
|
||||
after verifying GitHub's latest redirect points at that same release; do not
|
||||
link only to the companion repo release page.
|
||||
main-reachable tag), pass the release tag, successful OpenClaw npm
|
||||
`preflight_run_id`, and successful `full_release_validation_run_id`, and keep
|
||||
the default plugin publish scope `all-publishable` unless you are deliberately
|
||||
running a focused repair. The workflow serializes plugin npm publish, plugin
|
||||
ClawHub publish, and OpenClaw npm publish so the core package is not published
|
||||
before its externalized plugins.
|
||||
- Stable `OpenClaw Release Publish` requires an exact `windows_node_tag` after
|
||||
the matching non-prerelease `openclaw/openclaw-windows-node` release exists.
|
||||
It also requires the candidate-approved `windows_node_installer_digests` map.
|
||||
Before dispatching any publish child, it verifies that source release is
|
||||
published, non-prerelease, contains the required x64/ARM64 installers, and
|
||||
still matches that approved map. It then dispatches `Windows Node Release`
|
||||
while the OpenClaw release is still a draft, carrying the pinned installer
|
||||
digest map unchanged. The child
|
||||
workflow downloads the signed Windows Hub installers from that exact tag,
|
||||
matches them against the pinned digests, verifies their Authenticode
|
||||
signatures use the expected OpenClaw Foundation signer on a Windows runner,
|
||||
writes a SHA-256 manifest, and uploads the installers plus manifest onto the
|
||||
canonical OpenClaw GitHub release, then re-downloads the promoted assets and
|
||||
verifies the manifest membership and hashes. The parent verifies the current
|
||||
x64, ARM64, and checksum asset contract before publication. Direct recovery
|
||||
rejects unexpected `OpenClawCompanion-*` asset names before replacing the
|
||||
expected contract assets with the pinned source bytes. Manually dispatch
|
||||
`Windows Node Release` only for recovery, and always pass an exact tag, never
|
||||
`latest`, plus the explicit `expected_installer_digests` JSON map from the
|
||||
approved source release. Website download links should target exact OpenClaw
|
||||
release asset URLs for the current stable release, or
|
||||
`releases/latest/download/...` only after verifying GitHub's latest redirect
|
||||
points at that same release; do not link only to the companion repo release
|
||||
page.
|
||||
- Release checks now run in a separate manual workflow:
|
||||
`OpenClaw Release Checks`
|
||||
- `OpenClaw Release Checks` also runs the QA Lab mock parity lane plus the fast
|
||||
@@ -697,7 +719,12 @@ orchestrates the trusted-publisher workflows in the order the release needs:
|
||||
`ref=<release-sha>`.
|
||||
5. Dispatch `Plugin ClawHub Release` with the same scope and SHA.
|
||||
6. Dispatch `OpenClaw NPM Release` with the release tag, npm dist-tag, and
|
||||
saved `preflight_run_id`.
|
||||
saved `preflight_run_id` after verifying the saved
|
||||
`full_release_validation_run_id`.
|
||||
7. For stable releases, create or update the GitHub release as a draft, dispatch
|
||||
`Windows Node Release` with the explicit `windows_node_tag` and
|
||||
candidate-approved `windows_node_installer_digests`, and verify the canonical
|
||||
installer/checksum assets before publishing the draft.
|
||||
|
||||
Beta publish example:
|
||||
|
||||
@@ -706,6 +733,7 @@ gh workflow run openclaw-release-publish.yml \
|
||||
--ref release/YYYY.M.PATCH \
|
||||
-f tag=vYYYY.M.PATCH-beta.N \
|
||||
-f preflight_run_id=<successful-openclaw-npm-preflight-run-id> \
|
||||
-f full_release_validation_run_id=<successful-full-release-validation-run-id> \
|
||||
-f npm_dist_tag=beta
|
||||
```
|
||||
|
||||
@@ -715,7 +743,10 @@ Stable publish to the default beta dist-tag:
|
||||
gh workflow run openclaw-release-publish.yml \
|
||||
--ref release/YYYY.M.PATCH \
|
||||
-f tag=vYYYY.M.PATCH \
|
||||
-f windows_node_tag=vX.Y.Z \
|
||||
-f windows_node_installer_digests='{"OpenClawCompanion-Setup-x64.exe":"sha256:<approved-x64-sha256>","OpenClawCompanion-Setup-arm64.exe":"sha256:<approved-arm64-sha256>"}' \
|
||||
-f preflight_run_id=<successful-openclaw-npm-preflight-run-id> \
|
||||
-f full_release_validation_run_id=<successful-full-release-validation-run-id> \
|
||||
-f npm_dist_tag=beta
|
||||
```
|
||||
|
||||
@@ -725,7 +756,10 @@ Stable promotion directly to `latest` is explicit:
|
||||
gh workflow run openclaw-release-publish.yml \
|
||||
--ref release/YYYY.M.PATCH \
|
||||
-f tag=vYYYY.M.PATCH \
|
||||
-f windows_node_tag=vX.Y.Z \
|
||||
-f windows_node_installer_digests='{"OpenClawCompanion-Setup-x64.exe":"sha256:<approved-x64-sha256>","OpenClawCompanion-Setup-arm64.exe":"sha256:<approved-arm64-sha256>"}' \
|
||||
-f preflight_run_id=<successful-openclaw-npm-preflight-run-id> \
|
||||
-f full_release_validation_run_id=<successful-full-release-validation-run-id> \
|
||||
-f npm_dist_tag=latest
|
||||
```
|
||||
|
||||
@@ -755,6 +789,13 @@ package cannot ship without every publishable official plugin, including
|
||||
- `tag`: required release tag; must already exist
|
||||
- `preflight_run_id`: successful `OpenClaw NPM Release` preflight run id;
|
||||
required when `publish_openclaw_npm=true`
|
||||
- `full_release_validation_run_id`: successful `Full Release Validation` run
|
||||
id; required when `publish_openclaw_npm=true`
|
||||
- `windows_node_tag`: exact non-prerelease `openclaw/openclaw-windows-node`
|
||||
release tag; required for stable OpenClaw publish
|
||||
- `windows_node_installer_digests`: candidate-approved compact JSON map of the
|
||||
current Windows installer names to their pinned `sha256:` digests; required
|
||||
for stable OpenClaw publish
|
||||
- `npm_dist_tag`: npm target tag for the OpenClaw package
|
||||
- `plugin_publish_scope`: defaults to `all-publishable`; use `selected` only
|
||||
for focused plugin-only repair work with `publish_openclaw_npm=false`
|
||||
@@ -800,14 +841,21 @@ When cutting a stable npm release:
|
||||
Matrix, and Telegram coverage from one manual workflow
|
||||
4. If you intentionally only need the deterministic normal test graph, run the
|
||||
manual `CI` workflow on the release ref instead
|
||||
5. Save the successful `preflight_run_id`
|
||||
6. Run `OpenClaw Release Publish` with the same `tag`, the same `npm_dist_tag`,
|
||||
and the saved `preflight_run_id`; it publishes externalized plugins to npm
|
||||
and ClawHub before promoting the OpenClaw npm package
|
||||
7. If the release landed on `beta`, use the
|
||||
5. Select the exact non-prerelease `openclaw/openclaw-windows-node` release tag
|
||||
whose signed x64 and ARM64 installers should ship. Save it as
|
||||
`windows_node_tag`, and save their validated digest map as
|
||||
`windows_node_installer_digests`. The release-candidate helper records both
|
||||
and includes them in its generated publish command.
|
||||
6. Save the successful `preflight_run_id` and `full_release_validation_run_id`
|
||||
7. Run `OpenClaw Release Publish` with the same `tag`, the same `npm_dist_tag`,
|
||||
the selected `windows_node_tag`, its saved `windows_node_installer_digests`,
|
||||
the saved `preflight_run_id`, and the saved `full_release_validation_run_id`;
|
||||
it publishes externalized plugins to npm and ClawHub before promoting the
|
||||
OpenClaw npm package
|
||||
8. If the release landed on `beta`, use the
|
||||
`openclaw/releases/.github/workflows/openclaw-npm-dist-tags.yml`
|
||||
workflow to promote that stable version from `beta` to `latest`
|
||||
8. If the release intentionally published directly to `latest` and `beta`
|
||||
9. If the release intentionally published directly to `latest` and `beta`
|
||||
should follow the same stable build immediately, use that same release
|
||||
workflow to point both dist-tags at the stable version, or let its scheduled
|
||||
self-healing sync move `beta` later
|
||||
|
||||
@@ -20,6 +20,7 @@ Scope includes:
|
||||
- Thinking signature cleanup
|
||||
- Image payload sanitization
|
||||
- Blank text-block cleanup before provider replay
|
||||
- Incomplete reasoning-only length-turn cleanup before provider replay
|
||||
- User-input provenance tagging (for inter-session routed prompts)
|
||||
- Empty assistant error-turn repair for Bedrock Converse replay
|
||||
|
||||
@@ -91,6 +92,21 @@ Implementation:
|
||||
|
||||
---
|
||||
|
||||
## Global rule: incomplete reasoning-only turns
|
||||
|
||||
Assistant turns that hit the provider output limit with only thinking or
|
||||
redacted-thinking content are omitted from the in-memory replay copy. Such turns
|
||||
contain incomplete provider state and may carry a partial thinking signature.
|
||||
|
||||
Empty length turns remain unchanged, as do length turns with visible text, tool
|
||||
calls, or unknown content blocks. Stored transcripts are not rewritten.
|
||||
|
||||
Implementation:
|
||||
|
||||
- `normalizeAssistantReplayContent` in `src/agents/embedded-agent-runner/replay-history.ts`
|
||||
|
||||
---
|
||||
|
||||
## Global rule: inter-session input provenance
|
||||
|
||||
When an agent sends a prompt into another session via `sessions_send` (including
|
||||
|
||||
@@ -336,6 +336,7 @@ top-level `bindings[]` entries.
|
||||
- **Discord channel/thread:** `match.channel="discord"` + `match.peer.id="<channelOrThreadId>"`
|
||||
- **Slack channel/DM:** `match.channel="slack"` + `match.peer.id="<channelId|channel:<channelId>|#<channelId>|userId|user:<userId>|slack:<userId>|<@userId>>"`. Prefer stable Slack ids; channel bindings also match replies inside that channel's threads.
|
||||
- **Telegram forum topic:** `match.channel="telegram"` + `match.peer.id="<chatId>:topic:<topicId>"`
|
||||
- **WhatsApp DM/group:** `match.channel="whatsapp"` + `match.peer.id="<E.164|group JID>"`. Use E.164 numbers such as `+15555550123` for direct chats and WhatsApp group JIDs such as `120363424282127706@g.us` for groups.
|
||||
- **iMessage DM/group:** `match.channel="imessage"` + `match.peer.id="<handle|chat_id:*|chat_guid:*|chat_identifier:*>"`. Prefer `chat_id:*` for stable group bindings.
|
||||
|
||||
</ParamField>
|
||||
@@ -453,8 +454,9 @@ Use `agents.list[].runtime` to define ACP defaults once per agent:
|
||||
|
||||
### Behavior
|
||||
|
||||
- OpenClaw ensures the configured ACP session exists before use.
|
||||
- Messages in that channel or topic route to the configured ACP session.
|
||||
- OpenClaw ensures the configured ACP session exists after channel-specific admission and before use.
|
||||
- Messages in that channel, topic, or chat route to the configured ACP session.
|
||||
- Configured ACP bindings own their session route. Channel broadcast fan-out does not replace the configured ACP session for a matched binding.
|
||||
- In bound conversations, `/new` and `/reset` reset the same ACP session key in place.
|
||||
- Temporary runtime bindings (for example created by thread-focus flows) still apply where present.
|
||||
- For cross-agent ACP spawns without an explicit `cwd`, OpenClaw inherits the target agent workspace from agent config.
|
||||
|
||||
@@ -13,7 +13,12 @@ CLI, and scripting patterns (snapshots, refs, waits, debug flows).
|
||||
|
||||
## Control API (optional)
|
||||
|
||||
For local integrations only, the Gateway exposes a small loopback HTTP API:
|
||||
For local integrations only, the Gateway exposes a small loopback HTTP API.
|
||||
This standalone server is opt-in — set the environment variable
|
||||
`OPENCLAW_EAGER_BROWSER_CONTROL_SERVER=1` in the gateway service environment
|
||||
and restart the gateway before the HTTP endpoints become available. Without
|
||||
this variable the browser control runtime still works through the CLI and
|
||||
agent tools, but nothing listens on the loopback control port.
|
||||
|
||||
- Status/start/stop: `GET /`, `POST /start`, `POST /stop`
|
||||
- Tabs: `GET /tabs`, `POST /tabs/open`, `POST /tabs/focus`, `DELETE /tabs/:targetId`
|
||||
@@ -258,7 +263,14 @@ Snapshot flags at a glance:
|
||||
- `--format aria`: accessibility tree with `axN` refs. When Playwright is available, OpenClaw binds refs with backend DOM ids to the live page so follow-up actions can use them; otherwise treat the output as inspection-only.
|
||||
- `--efficient` (or `--mode efficient`): compact role snapshot preset. Set `browser.snapshotDefaults.mode: "efficient"` to make this the default (see [Gateway configuration](/gateway/configuration-reference#browser)).
|
||||
- `--interactive`, `--compact`, `--depth`, `--selector` force a role snapshot with `ref=e12` refs. `--frame "<iframe>"` scopes role snapshots to an iframe.
|
||||
- `--labels` adds a viewport-only screenshot with overlayed ref labels and prints the saved path.
|
||||
- With Playwright, `--labels` adds a screenshot with overlayed ref labels
|
||||
(prints `MEDIA:<path>`) plus an `annotations` array with each ref's bounding
|
||||
box. On `screenshot`, Playwright-backed labels work with `--full-page`,
|
||||
`--ref`, and `--element`; on `snapshot`, the accompanying screenshot remains
|
||||
viewport-only. Existing-session/chrome-mcp profiles render overlay labels on
|
||||
page screenshots but do not return `annotations` or use the Playwright
|
||||
full-page/ref/element projection helper. Without Playwright or chrome-mcp,
|
||||
labeled screenshots are not available.
|
||||
- `--urls` appends discovered link destinations to AI snapshots.
|
||||
|
||||
## Snapshots and refs
|
||||
@@ -274,7 +286,9 @@ OpenClaw supports two "snapshot" styles:
|
||||
- Output: a role-based list/tree with `[ref=e12]` (and optional `[nth=1]`).
|
||||
- Actions: `openclaw browser click e12`, `openclaw browser highlight e12`.
|
||||
- Internally, the ref is resolved via `getByRole(...)` (plus `nth()` for duplicates).
|
||||
- Add `--labels` to include a viewport screenshot with overlayed `e12` labels.
|
||||
- Add `--labels` to include a screenshot with overlayed `e12` labels. On
|
||||
Playwright-backed profiles this also returns per-ref bounding-box metadata
|
||||
(`annotations[]`).
|
||||
- Add `--urls` when link text is ambiguous and the agent needs concrete
|
||||
navigation targets.
|
||||
|
||||
|
||||
@@ -42,8 +42,14 @@ app-server thread as an ephemeral side thread. That keeps Codex OAuth and native
|
||||
thread behavior intact while still isolating the side answer from the parent
|
||||
transcript. Like Codex `/side`, the side thread keeps the current Codex
|
||||
permissions and native tool surface, with guardrails that tell the model not to
|
||||
treat inherited parent-thread work as active instructions. Non-Codex runtimes
|
||||
keep the older direct one-shot path.
|
||||
treat inherited parent-thread work as active instructions.
|
||||
|
||||
For CLI runtime aliases, BTW uses the owning CLI backend in side-question mode
|
||||
instead of falling back to a direct provider call. OpenClaw seeds sanitized
|
||||
conversation context into a fresh one-shot CLI invocation, disables OpenClaw MCP
|
||||
tool bundling and reusable CLI session state for that invocation, and lets the
|
||||
backend add any CLI-native no-resume or no-tools flags it supports. Direct
|
||||
non-CLI runtimes keep the direct one-shot path.
|
||||
|
||||
## What it does not do
|
||||
|
||||
|
||||
@@ -147,10 +147,12 @@ such as `@beta` stay pinned to the selected package and fail when incompatible.
|
||||
|
||||
Configure `security.installPolicy` to run a trusted local policy command before
|
||||
plugin install or update proceeds. The policy receives metadata plus the staged
|
||||
source path and can allow or block the install. It runs before plugin
|
||||
`before_install` hooks. The deprecated `--dangerously-force-unsafe-install`
|
||||
flag is accepted for compatibility but does not bypass install policy, hooks, or
|
||||
OpenClaw's built-in plugin dependency denylist.
|
||||
source path and can allow or block the install. It covers CLI and Gateway-backed
|
||||
plugin install/update paths. Plugin `before_install` hooks run later only in
|
||||
OpenClaw processes where plugin hooks are loaded, so use `security.installPolicy`
|
||||
for operator-owned install decisions. The deprecated
|
||||
`--dangerously-force-unsafe-install` flag is accepted for compatibility but does
|
||||
not bypass install policy or OpenClaw's built-in plugin dependency denylist.
|
||||
|
||||
See [Skills config](/tools/skills-config#operator-install-policy-securityinstallpolicy)
|
||||
for the shared `security.installPolicy` exec schema used by both skills and
|
||||
|
||||
@@ -190,6 +190,7 @@ agent session or the CLI.
|
||||
autonomous: {
|
||||
enabled: false,
|
||||
},
|
||||
allowSymlinkTargetWrites: false,
|
||||
approvalPolicy: "pending",
|
||||
maxPending: 50,
|
||||
maxSkillBytes: 40000,
|
||||
@@ -200,6 +201,9 @@ agent session or the CLI.
|
||||
|
||||
- `autonomous.enabled`: allows OpenClaw to create pending proposals from durable
|
||||
conversation signals after successful turns. Default: `false`.
|
||||
- `allowSymlinkTargetWrites`: allows apply to write through workspace skill
|
||||
symlinks whose real target is listed in `skills.load.allowSymlinkTargets`.
|
||||
Default: `false`.
|
||||
- `approvalPolicy: "pending"`: requires an approval prompt before
|
||||
agent-initiated `apply`, `reject`, or `quarantine`.
|
||||
- `approvalPolicy: "auto"`: skips that approval prompt. The agent must still
|
||||
@@ -265,6 +269,7 @@ Default state directory: `~/.openclaw`.
|
||||
| `Skill proposal content is too large` | Shorten the proposal body or raise `skills.workshop.maxSkillBytes`. |
|
||||
| `Target skill changed after proposal creation` | Revise the proposal against the current target, or create a new proposal. |
|
||||
| `Proposal scan failed` | Inspect scanner findings, then revise or quarantine the proposal. |
|
||||
| `untrusted symlink target` | Configure `skills.load.allowSymlinkTargets` and enable `skills.workshop.allowSymlinkTargetWrites` only for intentional shared skill roots. |
|
||||
| `Support file paths must be under one of...` | Move support files under `assets/`, `examples/`, `references/`, `scripts/`, or `templates/`. |
|
||||
| Proposal does not show in list | Check the selected `--agent` workspace and `OPENCLAW_STATE_DIR`. |
|
||||
| Agent cannot call `skill_workshop` | Check the active tool policy and run mode. `coding` includes the tool; restrictive `tools.allow` policies must list it explicitly, and sandboxed runs must use a normal host-side agent session or the CLI. |
|
||||
|
||||
@@ -29,6 +29,7 @@ Most skills configuration lives under `skills` in
|
||||
},
|
||||
workshop: {
|
||||
autonomous: { enabled: false },
|
||||
allowSymlinkTargetWrites: false,
|
||||
approvalPolicy: "pending",
|
||||
maxPending: 50,
|
||||
maxSkillBytes: 40000,
|
||||
@@ -333,6 +334,13 @@ different visible skill set per agent.
|
||||
quarantine. `auto` allows those actions without approval.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="skills.workshop.allowSymlinkTargetWrites" type="boolean" default="false">
|
||||
Allow Skill Workshop apply to write through workspace skill symlinks whose
|
||||
real target is already trusted by `skills.load.allowSymlinkTargets`. Keep this
|
||||
disabled unless generated proposal applies should mutate that shared skill
|
||||
root.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="skills.workshop.maxPending" type="number" default="50">
|
||||
Maximum pending and quarantined proposals retained per workspace.
|
||||
</ParamField>
|
||||
@@ -365,6 +373,23 @@ With this config, `<workspace>/skills/manager -> ~/Projects/manager/skills` is
|
||||
accepted after realpath resolution. `extraDirs` scans the sibling repo directly;
|
||||
`allowSymlinkTargets` preserves the symlinked path for existing layouts.
|
||||
|
||||
Skill Workshop apply does not write through those symlinks by default. To let
|
||||
Workshop apply mutate skills under already-trusted symlink targets, opt in
|
||||
separately:
|
||||
|
||||
```json5
|
||||
{
|
||||
skills: {
|
||||
load: {
|
||||
allowSymlinkTargets: ["~/Projects/manager/skills"],
|
||||
},
|
||||
workshop: {
|
||||
allowSymlinkTargetWrites: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Managed `~/.openclaw/skills` and personal `~/.agents/skills` directories
|
||||
already accept skill-directory symlinks (per-skill `SKILL.md` containment still
|
||||
applies).
|
||||
|
||||
@@ -204,6 +204,8 @@ publish and sync.
|
||||
Workspace, project-agent, and extra-dir skill discovery only accepts skill
|
||||
roots whose resolved realpath stays inside the configured root, unless
|
||||
`skills.load.allowSymlinkTargets` explicitly trusts a target root.
|
||||
Skill Workshop writes through those trusted targets only when
|
||||
`skills.workshop.allowSymlinkTargetWrites` is enabled.
|
||||
Managed `~/.openclaw/skills` and personal `~/.agents/skills` may contain
|
||||
symlinked skill folders, but every `SKILL.md` realpath must still stay
|
||||
inside its resolved skill directory.
|
||||
@@ -533,6 +535,8 @@ aligned.
|
||||
Use `allowSymlinkTargets` for intentional symlinked layouts where a skill
|
||||
root symlink points outside the configured root, for example
|
||||
`<workspace>/skills/manager -> ~/Projects/manager/skills`.
|
||||
Enable `skills.workshop.allowSymlinkTargetWrites` only when Skill Workshop
|
||||
should also apply proposals through those trusted symlinked paths.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="Remote macOS nodes (Linux gateway)">
|
||||
|
||||
@@ -35,7 +35,7 @@ title: "Thinking levels"
|
||||
- Google Gemini maps `/think adaptive` to Gemini's provider-owned dynamic thinking. Gemini 3 requests omit a fixed `thinkingLevel`, while Gemini 2.5 requests send `thinkingBudget: -1`; fixed levels still map to the closest Gemini `thinkingLevel` or budget for that model family.
|
||||
- MiniMax M2.x (`minimax/MiniMax-M2*`) on the Anthropic-compatible streaming path defaults to `thinking: { type: "disabled" }` unless you explicitly set thinking in model params or request params. This avoids leaked `reasoning_content` deltas from M2.x's non-native Anthropic stream format. MiniMax-M3 (and M3.x) is exempt: M3 emits proper Anthropic thinking blocks and returns empty content when thinking is disabled, so OpenClaw keeps M3 on the provider's omitted/adaptive thinking path.
|
||||
- Z.AI (`zai/*`) only supports binary thinking (`on`/`off`). Any non-`off` level is treated as `on` (mapped to `low`).
|
||||
- Moonshot (`moonshot/*`) maps `/think off` to `thinking: { type: "disabled" }` and any non-`off` level to `thinking: { type: "enabled" }`. When thinking is enabled, Moonshot only accepts `tool_choice` `auto|none`; OpenClaw normalizes incompatible values to `auto`.
|
||||
- Moonshot Kimi K2.7 Code (`moonshot/kimi-k2.7-code`) always thinks. Its profile exposes only `on`, and OpenClaw omits the outbound `thinking` field as required by Moonshot. Other `moonshot/*` models map `/think off` to `thinking: { type: "disabled" }` and any non-`off` level to `thinking: { type: "enabled" }`. When thinking is enabled, Moonshot only accepts `tool_choice` `auto|none`; OpenClaw normalizes incompatible values to `auto`.
|
||||
|
||||
## Resolution order
|
||||
|
||||
|
||||
@@ -16,9 +16,9 @@ search or dynamic-tools surface. Codex-native code mode, tool search, deferred
|
||||
dynamic tools, and nested tool calls are stable Codex harness surfaces and do
|
||||
not depend on `tools.toolSearch`.
|
||||
|
||||
When enabled for OpenClaw runs, the model receives one `tool_search_code` tool by default.
|
||||
That tool runs a short JavaScript body in an isolated Node subprocess with an
|
||||
`openclaw.tools` bridge:
|
||||
When enabled for OpenClaw runs, the model receives one `tool_search_code` tool
|
||||
by default. That tool runs a short JavaScript body in an isolated Node
|
||||
subprocess with an `openclaw.tools` bridge:
|
||||
|
||||
```js
|
||||
const hits = await openclaw.tools.search("create a GitHub issue");
|
||||
@@ -49,8 +49,8 @@ run:
|
||||
3. List eligible MCP tools through the session MCP runtime.
|
||||
4. Add eligible client tools supplied for the current run.
|
||||
5. Index compact descriptors for search.
|
||||
6. Expose either the OpenClaw code bridge or the structured fallback tools to the
|
||||
model.
|
||||
6. Expose the OpenClaw code bridge, the structured fallback tools, or the
|
||||
compact directory surface to the model.
|
||||
|
||||
At execution time every real tool call returns to OpenClaw. The isolated Node
|
||||
runtime does not hold plugin implementations, MCP client objects, or secrets.
|
||||
@@ -59,18 +59,26 @@ normal policy, approval, hook, logging, and result handling still apply.
|
||||
|
||||
## Modes
|
||||
|
||||
`tools.toolSearch` has two model-facing modes:
|
||||
`tools.toolSearch` has three model-facing modes:
|
||||
|
||||
- `code`: exposes `tool_search_code`, the default compact JavaScript bridge.
|
||||
- `tools`: exposes `tool_search`, `tool_describe`, and `tool_call` as plain
|
||||
structured tools for providers that should not receive code.
|
||||
- `directory`: exposes `tool_search`, `tool_describe`, and `tool_call` plus a
|
||||
bounded prompt directory of available tool names and descriptions for
|
||||
providers that should see tool names without every full schema. OpenClaw can
|
||||
also expose a small bounded set of likely or required tool schemas directly
|
||||
for the current turn.
|
||||
|
||||
Both modes use the same catalog and execution path. The only difference is the
|
||||
shape the model sees. If the current runtime cannot launch the isolated Node
|
||||
code-mode child process, the default `code` mode falls back to `tools` before
|
||||
catalog compaction.
|
||||
All modes use the same policy-filtered catalog and normal OpenClaw execution
|
||||
path. If the current runtime cannot launch the isolated Node code-mode child
|
||||
process, the default `code` mode falls back to `tools` before catalog
|
||||
compaction. In `directory` mode, client-provided tools stay directly visible
|
||||
for the current run while OpenClaw tools, plugin tools, and MCP tools can be
|
||||
compacted behind the directory catalog. A direct call to an exact hidden
|
||||
directory name is hydrated from that same authorized catalog before execution.
|
||||
|
||||
Both modes are experimental. Prefer direct tool exposure for small OpenClaw tool
|
||||
All modes are experimental. Prefer direct tool exposure for small OpenClaw tool
|
||||
catalogs, and prefer the Codex-native stable surfaces for Codex harness runs.
|
||||
|
||||
There is no separate source-selection config. When Tool Search is enabled, the
|
||||
@@ -90,7 +98,10 @@ Tool Search changes the shape:
|
||||
contract
|
||||
- Tool Search tools mode: the model sees three compact structured fallback
|
||||
tools
|
||||
- during the turn: the model loads only the tool schemas it actually needs
|
||||
- Tool Search directory mode: the model sees a bounded directory plus
|
||||
search/describe/call controls and a small bounded set of likely or required
|
||||
schemas
|
||||
- during the turn: the model can load remaining schemas as needed
|
||||
|
||||
Direct tool exposure is still the right default for small catalogs. Tool Search
|
||||
is best when one run can see many tools, especially from MCP servers or
|
||||
@@ -132,6 +143,20 @@ The structured fallback mode exposes the same operations as tools:
|
||||
- `tool_describe`
|
||||
- `tool_call`
|
||||
|
||||
Directory mode exposes:
|
||||
|
||||
- `tool_search`
|
||||
- `tool_describe`
|
||||
- `tool_call`
|
||||
|
||||
It also keeps client-provided tools directly visible and may expose a small
|
||||
bounded set of likely or required catalog tool schemas directly for the current
|
||||
turn. If the bounded directory omits entries, use `tool_search` to find them. If
|
||||
the model requests an exact hidden directory tool name directly, OpenClaw
|
||||
hydrates it from the authorized catalog before normal execution.
|
||||
Directory-mode client tool names must not collide with OpenClaw, plugin, or MCP
|
||||
tool names because exact deferred dispatch uses those names.
|
||||
|
||||
## Runtime boundary
|
||||
|
||||
The code bridge runs in a short-lived Node subprocess. The subprocess starts
|
||||
@@ -186,6 +211,18 @@ Use the structured fallback tools instead for OpenClaw runs:
|
||||
}
|
||||
```
|
||||
|
||||
Use the compact directory surface instead for OpenClaw runs:
|
||||
|
||||
```json5
|
||||
{
|
||||
tools: {
|
||||
toolSearch: {
|
||||
mode: "directory",
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Tune code-mode timeout and search result limits:
|
||||
|
||||
```json5
|
||||
|
||||
218
extensions/acpx/npm-shrinkwrap.json
generated
218
extensions/acpx/npm-shrinkwrap.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@openclaw/acpx",
|
||||
"version": "2026.6.6",
|
||||
"version": "2026.6.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@openclaw/acpx",
|
||||
"version": "2026.6.6",
|
||||
"version": "2026.6.2",
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/claude-agent-acp": "0.39.0",
|
||||
"@zed-industries/codex-acp": "0.15.0",
|
||||
@@ -224,9 +224,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz",
|
||||
"integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz",
|
||||
"integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -240,9 +240,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz",
|
||||
"integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz",
|
||||
"integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -256,9 +256,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -272,9 +272,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -288,9 +288,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -304,9 +304,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -320,9 +320,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -336,9 +336,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -352,9 +352,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz",
|
||||
"integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz",
|
||||
"integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -368,9 +368,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -384,9 +384,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz",
|
||||
"integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz",
|
||||
"integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -400,9 +400,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz",
|
||||
"integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz",
|
||||
"integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
@@ -416,9 +416,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz",
|
||||
"integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz",
|
||||
"integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
@@ -432,9 +432,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz",
|
||||
"integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz",
|
||||
"integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -448,9 +448,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz",
|
||||
"integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz",
|
||||
"integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -464,9 +464,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz",
|
||||
"integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz",
|
||||
"integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
@@ -480,9 +480,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -496,9 +496,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -512,9 +512,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -528,9 +528,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -544,9 +544,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -560,9 +560,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openharmony-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -576,9 +576,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -592,9 +592,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -608,9 +608,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz",
|
||||
"integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz",
|
||||
"integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -624,9 +624,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1208,9 +1208,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz",
|
||||
"integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz",
|
||||
"integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
@@ -1220,32 +1220,32 @@
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.28.0",
|
||||
"@esbuild/android-arm": "0.28.0",
|
||||
"@esbuild/android-arm64": "0.28.0",
|
||||
"@esbuild/android-x64": "0.28.0",
|
||||
"@esbuild/darwin-arm64": "0.28.0",
|
||||
"@esbuild/darwin-x64": "0.28.0",
|
||||
"@esbuild/freebsd-arm64": "0.28.0",
|
||||
"@esbuild/freebsd-x64": "0.28.0",
|
||||
"@esbuild/linux-arm": "0.28.0",
|
||||
"@esbuild/linux-arm64": "0.28.0",
|
||||
"@esbuild/linux-ia32": "0.28.0",
|
||||
"@esbuild/linux-loong64": "0.28.0",
|
||||
"@esbuild/linux-mips64el": "0.28.0",
|
||||
"@esbuild/linux-ppc64": "0.28.0",
|
||||
"@esbuild/linux-riscv64": "0.28.0",
|
||||
"@esbuild/linux-s390x": "0.28.0",
|
||||
"@esbuild/linux-x64": "0.28.0",
|
||||
"@esbuild/netbsd-arm64": "0.28.0",
|
||||
"@esbuild/netbsd-x64": "0.28.0",
|
||||
"@esbuild/openbsd-arm64": "0.28.0",
|
||||
"@esbuild/openbsd-x64": "0.28.0",
|
||||
"@esbuild/openharmony-arm64": "0.28.0",
|
||||
"@esbuild/sunos-x64": "0.28.0",
|
||||
"@esbuild/win32-arm64": "0.28.0",
|
||||
"@esbuild/win32-ia32": "0.28.0",
|
||||
"@esbuild/win32-x64": "0.28.0"
|
||||
"@esbuild/aix-ppc64": "0.28.1",
|
||||
"@esbuild/android-arm": "0.28.1",
|
||||
"@esbuild/android-arm64": "0.28.1",
|
||||
"@esbuild/android-x64": "0.28.1",
|
||||
"@esbuild/darwin-arm64": "0.28.1",
|
||||
"@esbuild/darwin-x64": "0.28.1",
|
||||
"@esbuild/freebsd-arm64": "0.28.1",
|
||||
"@esbuild/freebsd-x64": "0.28.1",
|
||||
"@esbuild/linux-arm": "0.28.1",
|
||||
"@esbuild/linux-arm64": "0.28.1",
|
||||
"@esbuild/linux-ia32": "0.28.1",
|
||||
"@esbuild/linux-loong64": "0.28.1",
|
||||
"@esbuild/linux-mips64el": "0.28.1",
|
||||
"@esbuild/linux-ppc64": "0.28.1",
|
||||
"@esbuild/linux-riscv64": "0.28.1",
|
||||
"@esbuild/linux-s390x": "0.28.1",
|
||||
"@esbuild/linux-x64": "0.28.1",
|
||||
"@esbuild/netbsd-arm64": "0.28.1",
|
||||
"@esbuild/netbsd-x64": "0.28.1",
|
||||
"@esbuild/openbsd-arm64": "0.28.1",
|
||||
"@esbuild/openbsd-x64": "0.28.1",
|
||||
"@esbuild/openharmony-arm64": "0.28.1",
|
||||
"@esbuild/sunos-x64": "0.28.1",
|
||||
"@esbuild/win32-arm64": "0.28.1",
|
||||
"@esbuild/win32-ia32": "0.28.1",
|
||||
"@esbuild/win32-x64": "0.28.1"
|
||||
}
|
||||
},
|
||||
"node_modules/escape-html": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/acpx",
|
||||
"version": "2026.6.6",
|
||||
"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.6"
|
||||
"pluginApi": ">=2026.6.2"
|
||||
},
|
||||
"build": {
|
||||
"openclawVersion": "2026.6.6",
|
||||
"openclawVersion": "2026.6.2",
|
||||
"staticAssets": [
|
||||
{
|
||||
"source": "./src/runtime-internals/mcp-proxy.mjs",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -123,11 +123,12 @@
|
||||
"help": "Optional explicit denylist of chat/user IDs. Sessions whose resolved conversation id matches the list are skipped even when the chat type is allowed. Applied after allowedChatIds."
|
||||
},
|
||||
"timeoutMs": {
|
||||
"label": "Timeout (ms)"
|
||||
"label": "Timeout (ms)",
|
||||
"help": "Recall work budget on the main lane. Before recall, the hook allows up to 1500 ms for session/config preflight. After recall starts, it reserves another fixed 1500 ms only for abort settlement and transcript recovery."
|
||||
},
|
||||
"setupGraceTimeoutMs": {
|
||||
"label": "Setup Grace Timeout (ms)",
|
||||
"help": "Advanced: extra blocking budget for cold embedded-run setup before the recall timeout is considered exhausted. Defaults to 0 so timeoutMs remains the main-lane hook budget unless you opt in."
|
||||
"help": "Advanced: extra recall-work budget for cold embedded-run setup. Defaults to 0. The separate 1500 ms preflight cap and 1500 ms post-recall completion allowance still apply."
|
||||
},
|
||||
"queryMode": {
|
||||
"label": "Query Mode",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/admin-http-rpc",
|
||||
"version": "2026.6.6",
|
||||
"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.6",
|
||||
"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.6",
|
||||
"version": "2026.6.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@openclaw/amazon-bedrock-mantle-provider",
|
||||
"version": "2026.6.6",
|
||||
"version": "2026.6.2",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "0.100.1",
|
||||
"@aws/bedrock-token-generator": "1.1.0"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user