Compare commits

..

206 Commits

Author SHA1 Message Date
Vincent Koc
9ff7abc898 test(ci): read sparse android guard files from git 2026-06-23 23:50:51 +08:00
Vincent Koc
dc9c11be91 fix(qa): reject duplicate plugin gauntlet controls 2026-06-23 17:37:53 +02:00
Vincent Koc
58552f6d7c ci: make release maturity scorecard opt-in 2026-06-23 23:32:45 +08:00
Vincent Koc
b8811b7dde fix(qa): reject duplicate gateway cpu controls 2026-06-23 17:30:37 +02:00
Vincent Koc
0850d83de1 fix(qa): reject duplicate model resolution perf controls 2026-06-23 17:24:41 +02:00
Jesse Merhi
92c10d4edc Fix WebChat dispatch failure session status (#84352)
Merged via squash.

Prepared head SHA: 562f2ac5a8
Co-authored-by: jesse-merhi <79823012+jesse-merhi@users.noreply.github.com>
Reviewed-by: @jesse-merhi
2026-06-24 01:19:21 +10:00
Vincent Koc
b22ae2a4da fix(qa): reject duplicate model bench controls 2026-06-23 17:18:47 +02:00
Vincent Koc
a822c9abaa fix(qa): reject duplicate gateway restart controls 2026-06-23 17:13:39 +02:00
Vincent Koc
c308295cd3 fix(qa): reject duplicate gateway startup controls 2026-06-23 17:08:08 +02:00
Vincent Koc
524e19726f fix(qa): reject duplicate cli bench controls 2026-06-23 17:01:42 +02:00
Vincent Koc
bc243568e7 chore(acpx): bump bundled client to 0.11.2 (#96124) 2026-06-23 22:54:51 +08:00
Vincent Koc
2cbb4e70cc fix(qa): reject duplicate telegram proof controls 2026-06-23 16:54:31 +02:00
Vincent Koc
e9b017d9dc fix(qa): reject duplicate abort leak controls 2026-06-23 16:46:39 +02:00
Vincent Koc
bde5be874a fix(qa): reject duplicate sqlite bench controls 2026-06-23 16:41:01 +02:00
Vincent Koc
8c09419f20 fix(qa): reject unknown docker timing options 2026-06-23 16:26:42 +02:00
Tony Wei
71f84f910a fix(acpx): detect wrapper orphan on any PPID change, not just init reparenting (#96032)
* fix(acpx): detect wrapper orphan on any PPID change, not just init reparenting

The codex / claude adapter wrapper's orphan watcher (emitted by
buildAdapterWrapperScript) skipped cleanup when `process.ppid !== 1`,
intending to wait for the kernel to reparent the orphaned wrapper to
PID 1 (init). This only works on bare-metal hosts without an active
user-session manager.

On systemd-managed deployments (EC2 user services, most container
runtimes), an orphaned process is reparented to the user-session
manager or container init — not to init itself. The watcher therefore
never fires, and when the gateway exits, the adapter wrapper survives
and holds its child process group (codex-acp.js + native binary)
running indefinitely.

Real-world symptom: each gateway restart accumulates 3-process trees of
leftover codex adapters. Subsequent ACP spawns then contend with these
orphans, the main event loop is starved by acpx-runtime reap attempts,
and new sessions stall at "waiting for tool execution" for minutes.

Fix: trigger orphan cleanup as soon as PPID changes from the recorded
original, regardless of what the new PPID is. The killChildTree path
already covers process-group cleanup via `kill(-pid, SIGTERM)`, so
once the watcher fires, grandchildren are reaped correctly.

Adds a regression test asserting the wrapper template does not
re-introduce the `process.ppid !== 1` guard.

* test: document maturity ref handoff

---------

Co-authored-by: t2wei <t2wei@me.com>
Co-authored-by: Vincent Koc <25068+vincentkoc@users.noreply.github.com>
2026-06-23 22:24:29 +08:00
Vincent Koc
8e6624cb6c fix(qa): reject duplicate sibling bench cases 2026-06-23 16:20:46 +02:00
Vincent Koc
273eed4c51 fix(harness): recover Copilot native subagent tasks 2026-06-23 22:13:59 +08:00
Vincent Koc
0bc5fb86a8 feat(copilot): mirror native plan and subagent events 2026-06-23 22:13:59 +08:00
Vincent Koc
7bde374c47 fix(qa): reject duplicate startup bench cases 2026-06-23 16:11:44 +02:00
Vincent Koc
fa263affd5 test(extensions): use real chutes response mocks 2026-06-23 22:00:32 +08:00
Vincent Koc
fa0427347a test(extensions): use real provider response mocks 2026-06-23 22:00:32 +08:00
Vincent Koc
aad78d399c test(extensions): use real response mocks 2026-06-23 22:00:32 +08:00
Vincent Koc
928607ac4a fix(qa): reject missing memory fd args 2026-06-23 15:58:33 +02:00
Vincent Koc
010c7f7110 fix(qa): disable pnpm verify in cpu scenarios 2026-06-23 15:50:39 +02:00
Vincent Koc
69891cf2ac fix(maint): protect pending hosted CI reruns 2026-06-23 21:49:36 +08:00
Vincent Koc
541f9b25d2 fix(maint): choose latest hosted CI run 2026-06-23 21:49:36 +08:00
Vincent Koc
c045fbf8ec fix(maint): use rebase PR landing 2026-06-23 21:49:36 +08:00
Shakker
e63d11ea24 test: scope transcript reader env setup 2026-06-23 14:46:20 +01:00
Shakker
fed369085f fix: restore task state env through helper 2026-06-23 14:36:55 +01:00
Shakker
6834a2d47b test: scope send state env helper 2026-06-23 14:33:32 +01:00
Vincent Koc
52251261ca fix(qa): reject duplicate gauntlet selectors 2026-06-23 15:32:12 +02:00
Shakker
e94deea4f2 fix: simplify Fly Machine env cleanup 2026-06-23 14:27:02 +01:00
Shakker
b827629418 test: route network runtime env setup 2026-06-23 14:25:43 +01:00
Vincent Koc
02556f9caf fix(qa): reject polluted Tool Search proof lanes 2026-06-23 15:25:27 +02:00
Vincent Koc
3f2b205dde fix(qa): require MCP API list evidence 2026-06-23 15:20:29 +02:00
Vincent Koc
3d2c52c935 fix(qa): reject duplicate RPC RTT methods 2026-06-23 15:14:40 +02:00
Shakker
e11539234b fix: route shared auth secret env writes 2026-06-23 14:10:02 +01:00
Vincent Koc
720e295cff fix(qa): require Telegram proof report before publish 2026-06-23 15:09:04 +02:00
Shakker
20d1dc8f0a test: route shared token reload env writes 2026-06-23 14:02:24 +01:00
Vincent Koc
d3ac8e3caa fix(qa): reject duplicate Parallels platforms 2026-06-23 15:01:33 +02:00
Shakker
93cfd59dd6 fix: clear config secret refs through env helper 2026-06-23 13:55:23 +01:00
Vincent Koc
5078ffdeb4 fix(qa): reject duplicate gateway smoke options 2026-06-23 14:52:37 +02:00
Josh Lehman
475252453b refactor: add transcript update identity contract (#89912) 2026-06-23 05:52:08 -07:00
Vincent Koc
d38fb7456a fix(qa): reject duplicate otel smoke options 2026-06-23 14:46:43 +02:00
Vincent Koc
08f8de3aee fix(qa): reject duplicate ux evidence options 2026-06-23 14:41:32 +02:00
Vincent Koc
a02a8cca79 fix(qa): reject duplicate hosted gate options 2026-06-23 14:35:37 +02:00
Vincent Koc
c638f2beda fix(release): reject duplicate candidate checklist options 2026-06-23 14:30:29 +02:00
Vincent Koc
34d2d54d6c fix(plugin-sdk): refresh api baseline hash 2026-06-23 20:27:52 +08:00
Vincent Koc
7cc0879d0e fix(qa): reject duplicate docker package options 2026-06-23 14:24:41 +02:00
Vincent Koc
2af06042c2 fix(qa): reject duplicate package candidate options 2026-06-23 14:19:42 +02:00
Vincent Koc
8cda4399d0 fix(qa): reject duplicate dependency evidence options 2026-06-23 14:14:43 +02:00
Vincent Koc
3d6127f7e4 fix(qa): reject duplicate test report controls 2026-06-23 14:06:41 +02:00
Vincent Koc
72816124c9 fix(qa): reject duplicate single-value flags 2026-06-23 14:01:29 +02:00
Vincent Koc
0e091482a3 fix(qa): reject ambiguous dependency report inputs 2026-06-23 13:43:44 +02:00
Vincent Koc
d51582a936 fix(qa): reject duplicate report artifacts 2026-06-23 13:38:40 +02:00
Vincent Koc
7374ecc777 fix(qa): reject duplicate qa e2e outputs 2026-06-23 13:34:20 +02:00
Vincent Koc
e856a24754 fix(qa): bound docker e2e log replay 2026-06-23 13:26:09 +02:00
Vincent Koc
9dbdefd43c fix(ci): keep release QA evidence branch-compatible 2026-06-23 19:24:27 +08:00
Vincent Koc
0177521375 fix(ci): pass resolved ref to maturity QA evidence 2026-06-23 19:18:13 +08:00
Vincent Koc
c714bfd8b6 fix(ci): allow release QA evidence workflow calls 2026-06-23 19:06:49 +08:00
Vincent Koc
d980f2555a fix(qa): preserve active mac restart locks 2026-06-23 13:02:39 +02:00
Vincent Koc
300b09b33f fix(acpx): consume acpx 0.11.1 model capability errors
* fix(acpx): consume acpx 0.11.1 model capability errors

* fix(acpx): refresh npm shrinkwrap for 0.11.1

* test: include workflow checks in tooling plan
2026-06-23 18:55:46 +08:00
Vincent Koc
2429585046 fix(qa): isolate docker rerun artifact downloads 2026-06-23 12:55:05 +02:00
Vincent Koc
a972855150 fix(qa): avoid extension memory report collisions 2026-06-23 12:48:16 +02:00
Vincent Koc
306f0ec37f test(ci): update tooling route expectation 2026-06-23 18:40:54 +08:00
Vincent Koc
cb6b15f782 fix(qa): avoid vitest report path collisions 2026-06-23 12:40:25 +02:00
Vincent Koc
44d77de0c5 fix(qa): isolate parallels plugin temp script 2026-06-23 12:34:05 +02:00
Vincent Koc
bd9f2a5e2e fix(ci): refresh dependency audit locks 2026-06-23 18:28:29 +08:00
Vincent Koc
b3b210b706 fix(qa): allow web search smoke gateway port override 2026-06-23 12:26:07 +02:00
Vincent Koc
536b437454 fix(docs): keep maturity taxonomy renderer formatted 2026-06-23 18:10:26 +08:00
ooiuuii
dd055c4f7c fix: npm plugin updates break running gateway imports (#95589)
Merged via squash.

Prepared head SHA: 74ecbbbb98
Co-authored-by: ooiuuii <169449607+ooiuuii@users.noreply.github.com>
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Reviewed-by: @vincentkoc
2026-06-23 18:07:57 +08:00
Vincent Koc
f484bf9985 fix(qa): avoid plugin update registry port collisions 2026-06-23 12:01:34 +02:00
Vincent Koc
04575a97b6 fix(qa): avoid telegram proof artifact collisions 2026-06-23 11:57:34 +02:00
Vincent Koc
318f95417a docs: place maturity pages under release reference (#96061)
Merged via squash.

Prepared head SHA: 7ab898262e
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Reviewed-by: @vincentkoc
2026-06-23 17:55:58 +08:00
Vincent Koc
1aad7d4e50 fix(ci): repair maturity docs checks 2026-06-23 17:50:47 +08:00
Vincent Koc
c313642ae2 fix(qa-lab): use scoped crabline package 2026-06-23 17:47:07 +08:00
Vincent Koc
932b58b94b fix(qa): avoid matrix qa artifact collisions 2026-06-23 11:45:29 +02:00
Vincent Koc
d37300f357 fix(anthropic): narrow stream block index guard 2026-06-23 17:45:15 +08:00
Vincent Koc
9e63323388 perf(anthropic): index active stream blocks 2026-06-23 17:45:15 +08:00
Vincent Koc
00f8b10567 perf(usage): bound session log retention 2026-06-23 17:42:28 +08:00
Vincent Koc
4dac8f47ed perf(agents): index displaced tool results 2026-06-23 17:40:21 +08:00
Vincent Koc
9089a8ab32 docs: redesign maturity scorecard pages (#96057)
Merged via squash.

Prepared head SHA: d2c680a48e
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Reviewed-by: @vincentkoc
2026-06-23 17:37:58 +08:00
Vincent Koc
67b26126ce fix(qa): avoid direct smoke artifact collisions 2026-06-23 11:29:24 +02:00
Vincent Koc
307300ac97 fix(ci): report missing workflow pre-commit runtime 2026-06-23 17:27:32 +08:00
Dallin Romney
7e0083ce0b ci: add release QA profile evidence (#95094)
* ci: add release qa profile evidence

* ci: simplify release qa profile evidence

* ci: reuse qa profile evidence workflow

* ci: remove inherited secrets lint comment

* ci: pass qa profile evidence secret explicitly

* ci: run maturity scorecard in release checks

* ci: declare maturity scorecard reusable secret
2026-06-23 02:27:00 -07:00
maweibin
740578b596 fix: assistant reply lost between compaction summary and first kept user in successor transcript (#95484)
Merged via squash.

Prepared head SHA: eff5894fb8
Co-authored-by: maweibin <18023423+maweibin@users.noreply.github.com>
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Reviewed-by: @vincentkoc
2026-06-23 17:25:45 +08:00
Vincent Koc
0a986f893a fix(daemon): type Windows task env fixture 2026-06-23 17:21:05 +08:00
Sliverp
9535b102d3 Gate private QQBot group commands (#92154)
* fix: gate private qqbot group commands

* fix(qqbot): keep authorized stop urgent in groups

* fix(qqbot): preserve omitted group command level

* fix(qqbot): preserve ignore-other-mentions gate

* test(qqbot): avoid unbound mention gate mock

* fix(qqbot): close strict command visibility gaps

* fix(qqbot): gate private group commands and close strict command visibility gaps (#92154) (thanks @sliverp)
2026-06-23 17:20:50 +08:00
Dallin Romney
1ce8eb3993 docs: rename top maturity tier (#96044) 2026-06-23 02:19:45 -07:00
Vincent Koc
f354889efa fix(crabbox): share Windows hydrate handoff path 2026-06-23 11:16:13 +02:00
Vincent Koc
cdf35e83f3 fix(qa): avoid live artifact directory collisions 2026-06-23 11:11:47 +02:00
Vincent Koc
8a8c6b2a27 fix(copilot): preserve compaction metadata 2026-06-23 17:11:24 +08:00
mikasa
f5148aff25 fix #89231: [Bug]: Windows installer-created scheduled task launches gateway.cmd with visible console — should use windowless launcher (#95480)
Merged via squash.

Prepared head SHA: 8b57b0377a
Co-authored-by: mikasa0818 <244515412+mikasa0818@users.noreply.github.com>
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Reviewed-by: @vincentkoc
2026-06-23 17:05:23 +08:00
Alex Knight
7bec91c8d8 fix(mattermost): block-bodied promise executor in participation test (oxlint) 2026-06-23 18:58:38 +10:00
Alex Knight
64c81f25c0 fix(mattermost): record thread participation on preview-finalized replies; document thread mention exception 2026-06-23 18:58:38 +10:00
Alex Knight
13ecb5c55e feat(mattermost): persist participated threads for mention-free follow-ups 2026-06-23 18:58:38 +10:00
Dallin Romney
db212e572e test(qa): gate maturity docs on passing evidence (#96017)
* docs: refresh maturity scorecard evidence

* test(qa): gate maturity docs on passing evidence

* test(qa): ensure UX matrix video dependencies

* test(qa): simplify maturity evidence result text

* test: align maturity docs test routing
2026-06-23 01:58:34 -07:00
Vincent Koc
5738cfb6df fix(qa): avoid default artifact directory collisions 2026-06-23 10:53:46 +02:00
Vincent Koc
33b8b72ad3 fix(qa): avoid self-check report clobbering 2026-06-23 10:46:25 +02:00
Moeed Ahmed
e998986889 fix(auto-reply): keep drain/restart-abort reply paths silent (#95431)
Merged via squash.

Prepared head SHA: edb75a944f
Co-authored-by: moeedahmed <5780040+moeedahmed@users.noreply.github.com>
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Reviewed-by: @vincentkoc
2026-06-23 16:46:04 +08:00
Yuval Dinodia
9549545dd0 fix(reply): suppress per-message finals across multi-message block streaming (#95432)
Merged via squash.

Prepared head SHA: 7d7c61f3d7
Co-authored-by: yetval <102706514+yetval@users.noreply.github.com>
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Reviewed-by: @vincentkoc
2026-06-23 16:44:02 +08:00
Yuval Dinodia
9f0d2427cd fix(agents): keep post-compaction user re-issue of a kept-tail prompt during compaction rotation (#94328)
Merged via squash.

Prepared head SHA: 05981b6c9f
Co-authored-by: yetval <102706514+yetval@users.noreply.github.com>
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Reviewed-by: @vincentkoc
2026-06-23 16:41:12 +08:00
Vincent Koc
a59b2f2958 fix(scripts): catch namespace plugin sdk wildcard exports 2026-06-23 10:35:28 +02:00
Vincent Koc
c061373ede fix(release): track CommonJS package dist imports 2026-06-23 10:31:05 +02:00
Vincent Koc
ea0330963c fix(release): surface installed extension manifest errors 2026-06-23 10:26:10 +02:00
tangtaizong666
43890ebc3b fix(heartbeat): skip reasoning payloads when selecting heartbeat reply (#92356)
Merged via squash.

Prepared head SHA: 5885fbba0c
Co-authored-by: tangtaizong666 <212687958+tangtaizong666@users.noreply.github.com>
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Reviewed-by: @vincentkoc
2026-06-23 16:24:15 +08:00
Dallin Romney
e2bcde9b1c ci: add codex maturity scorecard agent (#95919) 2026-06-23 01:22:21 -07:00
Rohit
695cea68f5 Fix recent session resume with long headers (#94578)
Merged via squash.

Prepared head SHA: 8102961184
Co-authored-by: rohitjavvadi <76606932+rohitjavvadi@users.noreply.github.com>
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Reviewed-by: @vincentkoc
2026-06-23 16:21:15 +08:00
SunnyShu0925
dd76fdceb6 fix(memory-wiki): exclude durable reference pages from stale report (#94369)
Merged via squash.

Prepared head SHA: c2dca7ed9b
Co-authored-by: SunnyShu0925 <265248434+SunnyShu0925@users.noreply.github.com>
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Reviewed-by: @vincentkoc
2026-06-23 16:19:36 +08:00
Dallin Romney
32dc664b4b fix(qa-lab): avoid duplicate child evidence files (#96030) 2026-06-23 01:18:24 -07:00
Vincent Koc
3d8d45fb0d fix(qa): reject out-of-range lab CLI ports 2026-06-23 10:16:45 +02:00
Coder
d63a73a1b8 fix(model-usage): coerce numeric-string costs and ignore non-finite values (#87861)
Merged via squash.

Prepared head SHA: 11bb5719ca
Co-authored-by: coder999999999 <83845889+coder999999999@users.noreply.github.com>
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Reviewed-by: @vincentkoc
2026-06-23 16:16:31 +08:00
Vincent Koc
ca5905eb90 fix(crabbox): reclaim sparse reused leases 2026-06-23 10:09:38 +02:00
Vincent Koc
023394000c fix(release): reject malformed candidate API timeouts 2026-06-23 10:04:15 +02:00
Vincent Koc
a0f93cf88f fix(agents): gate subagent stream suppression 2026-06-23 15:54:57 +08:00
chenhaoqiang
1876e3e1c1 perf: skip per-chunk live parsing for subagents
Subagent runs do not have a live stream consumer; their result is delivered from
the terminal message path after the child run finishes. The intermediate
message_update stream work only feeds live preview output.

Thread suppressLiveStreamOutput from the subagent lane into the embedded runner
subscription and return from handleMessageUpdate after accumulating the raw
chunk. This keeps final delivery unchanged while skipping per-chunk visible text
and reasoning stream parsing for subagents, which reduces event-loop pressure
when multiple child agents stream long answers in parallel.

Interactive and Control UI runs keep the existing live preview path.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
(cherry picked from commit e0382c2c58c3eabdf64638777ec82cb1e68514e9)
2026-06-23 15:54:57 +08:00
Jason (Json)
6f63140902 fix: avoid false macOS update failures during gateway shutdown (#95886)
Merged via squash.

Prepared head SHA: 400e87c937
Co-authored-by: fuller-stack-dev <263060202+fuller-stack-dev@users.noreply.github.com>
Reviewed-by: @fuller-stack-dev
2026-06-23 01:53:38 -06:00
Vincent Koc
d0f591893b fix(release): validate DMG resize slack 2026-06-23 09:51:38 +02:00
Vincent Koc
da32c7fe53 chore(sdk): update public surface budget 2026-06-23 15:48:27 +08:00
Vincent Koc
d3019e6127 fix(copilot): tighten harness sdk boundaries 2026-06-23 15:48:27 +08:00
Vincent Koc
21d67b168a feat(copilot): wire harness parity helpers 2026-06-23 15:48:27 +08:00
Vincent Koc
2824c02a42 fix(qa): avoid lab artifact directory collisions 2026-06-23 09:39:04 +02:00
youngting520
4c9c6f5116 fix(sessions): honor configured store for transcript mirrors (#95782) 2026-06-23 07:35:36 +00:00
Del
90d4aa7a8e fix(xiaomi): correct mimo-v2.5 and mimo-v2.5-pro max output tokens to 128K (#95934)
* fix(xiaomi): correct mimo-v2.5 and mimo-v2.5-pro max output tokens to 131072

* docs(xiaomi): correct mimo-v2.5 and mimo-v2.5-pro max output tokens to 131072
2026-06-23 07:35:10 +00:00
Yuval Dinodia
f826a665a2 fix(compaction): trim prefix when transcript ends in an oversized tool result (#95860)
findCutPoint defaulted cutIndex to the earliest valid cut (cutPoints[0],
keep everything) and only moved it forward to a cut point at or after the
backward token cursor. When the final entry is a toolResult whose estimate
alone meets keepRecentTokens, the cursor stops at that trailing toolResult
index, no valid cut point sits at or after it (toolResult entries are not
valid cut points), and the default stuck at keep-everything. Compaction then
summarized zero messages, so preflight and overflow compaction silently
no-op and the session loops on a context it cannot shrink.

Default cutIndex to the most recent valid cut before the forward search.
When a cut point exists at or after the cursor the search still finds it and
behavior is unchanged; only the trailing-tool-result case now keeps the
recent tail and summarizes the prefix.
2026-06-23 07:34:33 +00:00
Vincent Koc
add9f3c6d3 fix(test): reject pathological Docker E2E limits 2026-06-23 09:26:54 +02:00
Vincent Koc
603b250125 fix(qa): omit local temp roots from gateway artifacts 2026-06-23 09:18:41 +02:00
joshavant
19ddaa28b9 fix: harden ios screenshot uploads 2026-06-23 02:14:15 -05:00
Dallin Romney
f6b2a5ffb4 test(qa): harden all-profile evidence scenarios (#96003) 2026-06-23 00:07:51 -07:00
Vincent Koc
78a8caef38 fix(release): require postpublish evidence artifact 2026-06-23 14:53:14 +08:00
Chunyue Wang
e0d7776fff fix(context-engine): forward abortSignal through delegation bridge to runtime compaction (#89886)
Merged via squash.

Prepared head SHA: ff5a439d76
Co-authored-by: openperf <80630709+openperf@users.noreply.github.com>
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Reviewed-by: @vincentkoc
2026-06-23 14:41:08 +08:00
joshavant
8efed50c4e fix: gate ios push enrollment on notification consent 2026-06-23 01:38:24 -05:00
Vincent Koc
3e84836b01 fix(ci): require release QA evidence artifacts 2026-06-23 14:30:11 +08:00
mushuiyu886
01abe0a33d fix(agents): suggest recovery for unknown tool ids (#93374)
Merged via squash.

Prepared head SHA: bee84e4eb8
Co-authored-by: mushuiyu886 <266724580+mushuiyu886@users.noreply.github.com>
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Reviewed-by: @vincentkoc
2026-06-23 14:20:09 +08:00
Sunjae Kim
f0a2ba0584 Fix Gemini day freshness time range handling (#95682)
Merged via squash.

Prepared head SHA: f6038b3a33
Co-authored-by: Sunjae-k <52808029+Sunjae-k@users.noreply.github.com>
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Reviewed-by: @vincentkoc
2026-06-23 14:19:25 +08:00
Vincent Koc
f24b1a9c0c test(qa): relax heartbeat target none startup probe 2026-06-23 08:18:55 +02:00
Eden Kang
7c60379589 CLI: escape zsh completion descriptions (#64490)
* CLI: escape zsh completion descriptions

* Update src/cli/completion-cli.ts

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* CLI: use parser-safe zsh completion escaping

* CLI: escape zsh completion descriptions

---------

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: openclaw-clownfish[bot] <280122609+openclaw-clownfish[bot]@users.noreply.github.com>
2026-06-23 14:12:27 +08:00
Vincent Koc
0fed6402be fix(ci): require OpenGrep SARIF artifacts 2026-06-23 14:08:20 +08:00
Vincent Koc
a13e2b92b3 perf(ci): widen main test fanout and move codeql off blacksmith (#95967)
* perf(ci): widen main test fanout and move codeql off blacksmith

* test(ci): update fanout guard
2026-06-23 13:56:29 +08:00
Alix-007
e583e62190 fix(cron): normalize run-log jobId on write to match read-side validation (#93567)
Merged via squash.

Prepared head SHA: ee41f84b53
Co-authored-by: Alix-007 <267018309+Alix-007@users.noreply.github.com>
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Reviewed-by: @vincentkoc
2026-06-23 13:55:57 +08:00
Vincent Koc
fe5c098fd7 test(ios): remove host zip dependency from IPA validator fixture 2026-06-23 13:54:42 +08:00
Vincent Koc
28a5b0a212 fix(canvas): guard native A2UI resources 2026-06-23 13:44:14 +08:00
Vincent Koc
53f9b6a36b test(qa): align release memory scenario assertions 2026-06-23 07:43:06 +02:00
joshavant
eae53595b0 fix: unblock ios release upload metadata 2026-06-23 00:39:45 -05:00
Andy Ye
ca2f4c0d67 Warn on generated wrapper overwrites and status diagnostics (#90537)
Merged via squash.

Prepared head SHA: c6b6589e6d
Co-authored-by: TurboTheTurtle <35905412+TurboTheTurtle@users.noreply.github.com>
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Reviewed-by: @vincentkoc
2026-06-23 13:39:20 +08:00
Patrick Erichsen
f66e83154b docs: update ClawHub skill route references
Update OpenClaw ClawHub docs and user-facing copy for canonical owner-qualified skill routes.\n\nEvidence:\n- pnpm docs:list\n- pnpm test src/plugins/clawhub.test.ts src/cli/plugins-cli.install.test.ts src/gateway/server-methods/skills.clawhub.test.ts ui/src/ui/views/skills.test.ts\n- pnpm exec oxfmt --check --threads=1 docs/clawhub/cli.md docs/clawhub/publishing.md docs/cli/skills.md docs/help/faq.md docs/start/showcase.md docs/tools/creating-skills.md docs/tools/skills.md src/gateway/server-methods/skills.clawhub.test.ts src/plugins/clawhub.test.ts src/plugins/clawhub.ts ui/src/ui/views/skills.test.ts\n- git diff --check\n- exact-head hosted CI passed for 8530374388d8a73235b2ac8444b95a4a4c7d0f1c\n\nNote: repo-native scripts/pr prepare-run was attempted; local broad pnpm test was stopped after unrelated existing failures in agent/media/provider shards, while hosted exact-head CI and targeted ClawHub route/copy validation were green.
2026-06-22 22:27:57 -07:00
Vincent Koc
1479078a25 fix(ci): require iOS Periphery evidence artifact 2026-06-23 13:17:42 +08:00
Patrick Erichsen
0a97f73402 feat: add bundled plugin icon manifest URLs (#95845) 2026-06-22 22:14:18 -07:00
Vincent Koc
7668a72843 fix(qa): allow evidence-free maturity input checks 2026-06-23 13:05:20 +08:00
joshavant
10d850b39c chore: make ios testflight upload path canonical 2026-06-23 00:01:20 -05:00
joshavant
d4f666874f feat: harden ios app store push release mode 2026-06-23 00:01:20 -05:00
Dallin Romney
606706492f ci: fail qa profile evidence on qa failures (#95971) 2026-06-22 22:00:30 -07:00
Vincent Koc
cc1b3a8550 fix(install): skip llama cpp native build by default 2026-06-23 12:58:41 +08:00
Dallin Romney
438f208a76 perf(qa-lab): speed up unified QA suites (#95944)
* perf(qa-lab): speed up smoke ci suite

* fix(qa-lab): satisfy suite scheduler lint

* fix(qa-lab): settle unified partitions before retry

* fix(qa-lab): preserve isolated suite safeguards

* refactor(qa-lab): make suite isolation explicit

* fix(qa-lab): preserve channel-driver suite serialization

* fix(qa-lab): narrow flow-only isolation metadata
2026-06-22 21:55:54 -07:00
Jason O'Neal
b8f1961aae fix(model-fallback): classify Codex usage-limit payloads (#95400)
* fix(model-fallback): classify Codex usage-limit payloads

* test: add real behavior proof for Codex usage-limit fallback

Adds a permanent real behavior proof test that exercises the production
classifyEmbeddedAgentRunResultForModelFallback() classifier with the exact
Codex subscription usage-limit error text.

Covers:
- Primary path: isError payload with usage-limit text -> rate_limit fallback
- Non-error payload: same text as normal assistant output -> no fallback
- Visible output already delivered -> no fallback
- Cross-provider: same text via openrouter -> rate_limit fallback

* fix(fallback-classifier): guard on finalAssistantVisibleText delivery evidence

When finalAssistantVisibleText contains real visible output (non-empty,
non-silent-reply), the agent already delivered a response to the user.
The classifier must not trigger model fallback in that case, because the
user already has their answer and rotating models would only burn quota
without improving the outcome.

Adds a guard in classifyEmbeddedAgentRunResultForModelFallback() that
checks finalAssistantVisibleText after committed outbound delivery
evidence and before the hook_block check. Uses the existing
isSilentReplyPayloadText() helper to avoid suppressing NO_REPLY and
similar intentional silent tokens.

This fixes the already-delivered-output test case in the Codex
usage-limit real behavior proof test.

* fix(test): use toEqual for cross-provider proof test type safety

The ModelFallbackResultClassification union includes { error: unknown },
so accessing .reason/.code after not.toBeNull() fails type checking.
Use toEqual with the full expected object instead, matching the pattern
used in result-fallback-classifier.test.ts.

* fix(model-fallback): refresh usage-limit fallback

Signed-off-by: sallyom <somalley@redhat.com>

---------

Signed-off-by: sallyom <somalley@redhat.com>
Co-authored-by: sallyom <somalley@redhat.com>
2026-06-23 00:55:17 -04:00
Vincent Koc
495a4f9b8e test(qa): accept verified live fanout completions 2026-06-23 06:46:40 +02:00
Vincent Koc
381cec0051 fix(ci): require live proof evidence artifacts
Require live Mantis and Telegram proof artifact uploads to fail when evidence is missing and guard the workflow invariant.
2026-06-23 12:43:09 +08:00
Joe Pahuchi
b27ac78d4d fix(plugins): make empty-allowlist actionable for new users (#78105)
* fix(plugins): make empty-allowlist warning actionable for first-time users

* fix(plugins): make empty-allowlist warnings actionable

* fix(plugins): make empty-allowlist warnings actionable

* fix(plugins): make empty-allowlist actionable for new users

---------

Co-authored-by: openclaw-clownfish[bot] <280122609+openclaw-clownfish[bot]@users.noreply.github.com>
2026-06-23 04:41:40 +00:00
Dallin Romney
d3dc7aaa87 docs: update maturity scorecard (#95933)
* docs: update maturity scorecard

* docs: rerender maturity scorecard from all evidence
2026-06-22 21:37:03 -07:00
Shakker
1f1cb5f2cb test: contain bundle mcp home env 2026-06-23 05:33:12 +01:00
ooiuuii
2ea0e8807a fix(cli): show working commands for pinned plugin drift (#95541)
Merged via squash.

Prepared head SHA: d41b9b5b25
Co-authored-by: ooiuuii <169449607+ooiuuii@users.noreply.github.com>
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Reviewed-by: @vincentkoc
2026-06-23 12:32:22 +08:00
Vincent Koc
77d0deedf2 improve: speed up provider tool-call streaming (#95957)
Merged via squash.

Prepared head SHA: d8f510757b
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Reviewed-by: @vincentkoc
2026-06-23 12:28:24 +08:00
Shakker
e253568d52 fix: scope bundle command home env 2026-06-23 05:24:48 +01:00
Stellar鱼
a64e270ae7 fix(agents): infer runtime provider from qualified model ids (#91724)
Merged via squash.

Prepared head SHA: 9b544a23d7
Co-authored-by: yu-xin-c <175149126+yu-xin-c@users.noreply.github.com>
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Reviewed-by: @vincentkoc
2026-06-23 12:23:52 +08:00
Andy Ye
33b23214d9 Fix memory-wiki bridge self-import loop (#95666)
Merged via squash.

Prepared head SHA: 0f74629547
Co-authored-by: TurboTheTurtle <35905412+TurboTheTurtle@users.noreply.github.com>
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Reviewed-by: @vincentkoc
2026-06-23 12:21:57 +08:00
Shakker
667e5bf67e test: isolate plugin update home env 2026-06-23 05:16:21 +01:00
Vincent Koc
c8ca44739a fix(qa): exclude archived categories from all profile 2026-06-23 12:08:33 +08:00
Vincent Koc
cfff6b2ac6 fix(ci): require QA live evidence artifacts
Require QA live artifact uploads to fail when evidence is missing and guard the workflow invariant.
2026-06-23 12:07:48 +08:00
Vincent Koc
81f0e93881 docs(copilot): refresh harness parity notes 2026-06-23 12:07:30 +08:00
Vincent Koc
035cfa1470 fix(apps): remove stale native A2UI assets 2026-06-23 12:05:41 +08:00
Vincent Koc
68a1e00b73 fix(agents): retry silent subagent completion handoffs 2026-06-23 06:04:16 +02:00
Vincent Koc
54b2243de3 test(qa): tighten release profile scenario waits 2026-06-23 06:04:16 +02:00
兰之
bd479958c0 feat(plugin-sdk): add extensible channel identity hook context (#91903)
Merged via squash.

Prepared head SHA: 90f51eafd5
Co-authored-by: lanzhi-lee <36190508+lanzhi-lee@users.noreply.github.com>
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Reviewed-by: @vincentkoc
2026-06-23 11:56:49 +08:00
Dallin Romney
4460fa78c3 feat(qa): add "all" taxonomy profile (#95947)
* qa: add all maturity profile

* test: update qa coverage profile expectations
2026-06-22 20:49:08 -07:00
Vincent Koc
ca0eb62c87 fix(ci): finalize Windows Testbox after setup failures
Ensure the Windows Testbox workflow runs its lifecycle loop after setup failures and guard the shared Testbox finalization invariant.
2026-06-23 11:47:10 +08:00
Gio Della-Libera
67ee0dee00 Doctor: expose extra gateway service findings (#84340)
Merged via squash.

Prepared head SHA: f0bda85907
Co-authored-by: giodl73-repo <235387111+giodl73-repo@users.noreply.github.com>
Co-authored-by: giodl73-repo <235387111+giodl73-repo@users.noreply.github.com>
Reviewed-by: @giodl73-repo
2026-06-22 20:45:19 -07:00
Marcus Castro
02387e747d fix(whatsapp): resolve approval reactions across jid drift (#95935) 2026-06-23 00:44:50 -03:00
Vincent Koc
ad3b2f4b88 fix(agents): align OpenRouter model scan body cap 2026-06-23 11:28:29 +08:00
Alix-007
91b0567e89 fix(agents): bound Google prompt cache response reads (#95417)
The Google embedded-agent prompt-cache helpers parsed cachedContents
metadata with an unbounded `await response.json()` in both
createGooglePromptCache and updateGooglePromptCacheTtl. A buggy or
hostile Generative Language endpoint returning a 200 with a large or
never-ending body (especially with no Content-Length) would be fully
buffered into memory before parsing, with the existing
cancelUnreadResponseBody guard firing too late (json() already drained
the body).

Route both reads through the shared streaming byte-cap reader
(readResponseWithLimit) under a 1 MiB cap, cancelling the stream on
overflow instead of buffering it, then JSON.parse the bounded buffer.
This is the symmetric Google-endpoint counterpart to the Anthropic
error-stream and gateway pricing-catalog bounds.

Adds regressions that stream an oversized no-Content-Length body through
the real create and TTL-refresh paths and assert the body is cancelled.
2026-06-22 23:26:37 -04:00
Vincent Koc
f80d9b6eae fix(ci): finalize testbox sessions after setup failures
Ensure Testbox wrapper workflows finalize backend sessions even when setup fails, align the check timeout fallback with the documented 120-minute default, and guard the workflow invariants.
2026-06-23 11:26:30 +08:00
joshavant
f2b8668a54 feat: add ios push relay diagnostics 2026-06-22 22:17:04 -05:00
Vincent Koc
a9024741c2 test(qa): pin live artifact scenario contracts 2026-06-23 05:13:35 +02:00
Vincent Koc
d1b268f7f7 fix(qa): normalize completed wait envelopes 2026-06-23 05:13:35 +02:00
Alix-007
06ca1235ef fix(agents): bound OpenRouter model-scan catalog success body (#95418)
The OpenRouter /models catalog read in fetchOpenRouterModels hardened only
the error/early-return path (dbd5689 cancels the body when res.bodyUsed is
false), but the success branch still buffered the whole body with an
unbounded `await res.json()`. The response is a provider-controlled,
runtime-fetched body, so a faulty or hostile provider can stream an
effectively unbounded JSON document and exhaust process memory before the
parse completes; the finally-cancel is a no-op once .json() has drained.

Read the success body through the canonical byte-cap reader
(readResponseWithLimit) under a 4 MiB ceiling before JSON.parse, cancelling
the stream on overflow and bounding idle stalls with the call's existing
timeout. This is the symmetric success-path counterpart to the bounded-stream
hardening landed in #95103 (pricing catalog) and #95108 (Anthropic error
streams), reusing the same helper rather than a new abstraction.
2026-06-22 23:10:15 -04:00
Alix-007
3da4280caf fix(agents): bound OpenRouter model catalog response reads (#95420)
* fix(agents): bound OpenRouter model catalog response reads

The runtime OpenRouter model-capability detector fetched the full
/models catalog with an unbounded `await response.json()`, so a
compromised or misbehaving endpoint could stream an arbitrarily large
body and force the process to buffer the whole payload before parsing.

Read the body through the shared bounded reader instead, capping it at
16 MiB (matching the sibling pricing-cache endpoint hardened in #95103)
and cancelling the stream on overflow. This mirrors the symmetric
bound-stream fixes in #95103 and #95108.

Adds coverage that an oversized streamed catalog is cancelled instead of
buffered and that an under-cap chunked body still reassembles, parses,
and round-trips through the SQLite cache on a fresh import.

* fix(agents): avoid OpenRouter refetch after capped catalog miss

---------

Co-authored-by: sallyom <somalley@redhat.com>
2026-06-22 23:00:17 -04:00
mikasa
aa0bdb901f fix #95489: [Bug]: claude-cli out-of-credits error bypasses model fallback chain — error text delivered as final response (#95508)
* fix(agents): fallback on generic cli failure text

* fix(agents): guard generic cli failure payload visibility

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(agents): use exported generic failure text

Signed-off-by: sallyom <somalley@redhat.com>

---------

Signed-off-by: sallyom <somalley@redhat.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: sallyom <somalley@redhat.com>
2026-06-22 22:55:35 -04:00
Vincent Koc
c48dd3cdd1 fix(ci): align maturity score source with taxonomy 2026-06-23 10:46:07 +08:00
Vincent Koc
ace3fe969b fix(ci): honor reusable QA evidence failure policy
Make QA Profile Evidence failure handling explicit for direct and reusable callers. Direct manual runs still fail on non-zero QA profiles by default, while maturity scorecard reusable calls can collect failed QA evidence for parent rendering. Verified with actionlint, diff check, Testbox changed gate, PR CI, and CodeQL.
2026-06-23 10:44:12 +08:00
Dallin Romney
b71ddbf1b4 ci: simplify maturity scorecard QA evidence inputs (#95898)
* ci: simplify maturity scorecard evidence inputs

* ci: keep maturity renderer defaults runnable

* ci: validate maturity evidence source

* ci: split maturity scorecard codex agent

* ci: remove codex copy from maturity evidence workflow

* ci: narrow maturity evidence workflow secrets
2026-06-22 19:24:43 -07:00
Sean Sun
1d013c219b plugins: clarify allowlist warning when entries don't match discovered ids (#68389)
* plugins: clarify allowlist warning when entries don't match discovered ids

When plugins.allow contains entries that do not match any discovered
plugin id (for example a channel id like feishu instead of the real
plugin id openclaw-lark), stop emitting the misleading "plugins.allow
is empty" warning. Emit a specific mismatch warning that lists the
unknown allow entries alongside the discovered plugin ids and points
users at the plugin id rather than a channel id or npm package name.

Refs #68352

* plugins: treat bundled plugin ids as valid allow entries

Codex P2 on #68389: warnWhenAllowlistIsOpen computed allowHasMatch
against the auto-discoverable (workspace + global) subset only, so
a legitimate bundled-only allowlist like plugins.allow=['telegram']
would trip the new mismatch warning whenever any non-bundled plugin
happened to be discoverable alongside it.

Compare allow entries to every discovered plugin id (bundled +
workspace + global) for both the short-circuit and the unmatched-
entries computation. The warning text stays scoped to non-bundled
auto-discoverable plugins; we just stop flagging bundled ids as
'does not match any discovered plugin ids'. Add a regression test
that covers the bundled-only allowlist + non-bundled workspace
plugin combination.

Refs #68352

* chore: drop release-owned CHANGELOG entry (AGENTS.md: changelog is release-generated)

* plugins: clarify allowlist warning when entries do not match plugin ids

---------

Co-authored-by: Sean Sun <lyfuci11@gmail.com>
Co-authored-by: openclaw-clownfish[bot] <280122609+openclaw-clownfish[bot]@users.noreply.github.com>
2026-06-23 02:20:17 +00:00
Vincent Koc
33206ee583 fix(ci): use available Android SDK platform
Restores Android CI to a publicly installable SDK platform and keeps Gradle compileSdk aligned with the workflow install/cache key. Rolls back the API-37-only AndroidX core slice until Android 37 is available to hosted CI, while preserving the unrelated Kotlin dependency bump.

Verification:
- Google SDK repository index check: android-36 exists; android-37/android-37.0 do not.
- git diff --check
- Testbox changed gate: tbx_01kvs3r1bc925pxya94zey23c8
- PR CI: 68 successful, 12 skipped, 0 failing, 0 pending; Android build/play and both Android unit-test lanes passed.
2026-06-23 10:18:28 +08:00
wangjieweb3-design
a84d3b6853 docs: document local avatar file size limit (#78884)
* docs: document local avatar file size limit

* docs: update docs/gateway/config-agents.md

* docs: document local avatar file size limit

---------

Co-authored-by: openclaw-clownfish[bot] <280122609+openclaw-clownfish[bot]@users.noreply.github.com>
2026-06-23 02:17:04 +00:00
Vincent Koc
19627c7dd9 fix(memory): improve node:sqlite unavailable guidance (#95916)
Co-authored-by: openclaw-clownfish[bot] <280122609+openclaw-clownfish[bot]@users.noreply.github.com>
Co-authored-by: Song Luo <133665654+rrrrrredy@users.noreply.github.com>
2026-06-23 02:12:47 +00:00
Vincent Koc
abd8a46b0a improve: reduce hot-path linear scans and redundant I/O (#95697)
Merged via squash.

Prepared head SHA: 67f2678a34
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Reviewed-by: @vincentkoc
2026-06-23 10:11:18 +08:00
Marcus Castro
ce391dc382 fix(whatsapp): preserve durable reply target (#95914) 2026-06-22 23:05:35 -03:00
Vincent Koc
2205f50016 test(qa): satisfy history reply lint 2026-06-23 04:01:11 +02:00
Vincent Koc
7fc4bbc0bc fix(agents): wake active parents for subagent completions 2026-06-23 04:01:11 +02:00
Vincent Koc
d716dfd532 test(qa): wait for live history replies in flow scenarios 2026-06-23 04:01:11 +02:00
Vincent Koc
5822e8074d test(qa): accept completed agent wait status 2026-06-23 04:01:11 +02:00
Dallin Romney
27711b500c ci: add maturity scorecard renderer (#94272) (#95901)
* ci: add maturity scorecard renderer

* ci: render qa scorecard evidence

* ci: type maturity docs renderer

* ci: tighten maturity artifact inputs

* ci: move maturity renderer under qa scripts

* ci: share maturity score schema

* ci: centralize maturity taxonomy validation

* ci: move maturity scores under qa

* ci: remove docs maturity score source

* docs: simplify maturity scorecard output

* docs: commit generated maturity scorecard

* docs: group maturity pages

* docs: simplify maturity scorecard dates

* docs: promote maturity nav tab

* docs: clean up maturity pages

* docs: remove maturity outline page

* docs: filter maturity taxonomy doc links

* docs: simplify maturity taxonomy tables

* docs: keep artifact taxonomy links

* docs: simplify lts scorecard display

* docs: clarify maturity score definitions

* docs: derive maturity coverage from evidence

* docs: hide maturity scorecard until evidence

* docs: remove placeholder maturity pages

* docs: keep maturity scores out of pr

* ci: open maturity scorecard docs pr
2026-06-22 18:55:06 -07:00
Vincent Koc
1252378018 fix(installer): unblock Windows source installs 2026-06-23 09:48:43 +08:00
Vincent Koc
def4b51485 fix(qa): gate smoke profile scenarios by channel driver 2026-06-23 09:34:52 +08:00
ly-wang19
d84a8b1506 fix(discord): reserve closing-fence space on fence-closing lines (#95661)
`chunkDiscordText` reserved closing-fence space from the post-line fence state
(`nextOpenFence`), but a flush during a line's segment loop appends the closing
fence based on the still-open `openFence`, which is only advanced after the
line. On a line that closes a fence yet carries trailing text, `reserveChars`
was 0 while `flush()` still appended a `` ``` ``, producing a chunk of
`maxChars + 4` (e.g. 2004 > 2000) that Discord rejects with HTTP 400.

Reserve against `nextOpenFence ?? openFence` so whichever fence a flush can
close is accounted for, keeping a fence-closing line's chunk within `maxChars`.

Co-authored-by: ly-wang19 <ly-wang19@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 01:28:14 +00:00
Vincent Koc
1658fb6c14 fix(ci): restore QA workflow gates (#95890) 2026-06-23 09:26:35 +08:00
Mehraz Morshed
dd9706e902 Fix: plural agreement in VISION.md File (#78715)
Change "optional capability" to "optional capabilities" to better match the plural "plugins" in the same sentence.

No functional changes. Documentation only.
2026-06-23 01:23:35 +00:00
669 changed files with 35657 additions and 8916 deletions

View File

@@ -15,7 +15,7 @@ committed `inventory/` report tree.
This skill owns the operational workflow for:
- `taxonomy.yaml`
- `docs/maturity-scores.yaml`
- `qa/maturity-scores.yaml`
- `docs/concepts/qa-e2e-automation.md`
- `qa/scenarios/index.yaml`
@@ -37,28 +37,35 @@ out of this repo. If a score needs private evidence, use the redacted
coverage IDs. Do not promote generic IDs into standalone feature names.
- Avoid duplicate coverage-ID bundles under different feature names in one
category.
- `docs/maturity-scores.yaml` is the aggregate score source committed in this
repo. It is the only committed score data; do not add generated inventory
directories.
- There is no committed maturity-doc renderer or `pnpm maturity:*` script in
this repo. Do not invent generated scorecard files; update the source YAML
and current docs directly.
- `qa-evidence.json` artifacts provide per-run QA scorecard evidence. They can
enrich generated artifact docs, but they are not committed as inventory.
- `qa/maturity-scores.yaml` is the committed aggregate source for Quality,
Completeness, and LTS review state.
- `extensions/qa-lab/src/scorecard-taxonomy.ts` exports
`qaMaturityScoresSchema` and `readValidatedQaMaturityScoreSources`; use those
QA Lab utilities to validate score output.
- Generated public docs are `docs/maturity/scorecard.md` and
`docs/maturity/taxonomy.md`; both come from `pnpm maturity:render`. Do not
hand-edit generated Markdown to change score results.
- `qa-evidence.json` artifacts provide per-run QA scorecard evidence. Release
profile artifacts are the source of truth for Coverage. They can enrich
generated artifact docs, but they are not committed as inventory.
## Commands
Run from the openclaw repo root.
Validate YAML structure after source edits:
Validate taxonomy YAML structure and the maturity score schema after source
edits:
```bash
node <<'NODE'
const fs = require("node:fs");
const YAML = require("yaml");
for (const file of ["taxonomy.yaml", "docs/maturity-scores.yaml", "qa/scenarios/index.yaml"]) {
node --import tsx --input-type=module <<'NODE'
import fs from "node:fs";
import YAML from "yaml";
import { readValidatedQaMaturityScoreSources } from "./extensions/qa-lab/src/scorecard-taxonomy.ts";
for (const file of ["taxonomy.yaml", "qa/scenarios/index.yaml"]) {
YAML.parse(fs.readFileSync(file, "utf8"));
}
readValidatedQaMaturityScoreSources();
NODE
```
@@ -83,17 +90,17 @@ When asked to score or refresh a surface:
`.agents/skills/claw-score/references/completeness/`.
3. Gather public repo evidence from docs, source, tests, and QA scenario
metadata.
4. Prefer existing `qa-evidence.json` artifacts for executed proof. Do not use
discrawl or unredacted private archives.
5. Update `docs/maturity-scores.yaml` only when the score change is backed by
public or redacted artifact evidence.
6. Run the YAML validation command from this skill.
4. Prefer existing release profile `qa-evidence.json` artifacts for executed
proof.
5. Update `qa/maturity-scores.yaml` only for Quality, Completeness, and LTS
review state backed by public or redacted artifact evidence.
6. Run the schema validation command from this skill.
7. Run `pnpm check:docs` if docs prose changed, and focused QA coverage checks
if coverage IDs or profile membership changed.
For subjective score changes, make the smallest defensible edit and leave the
evidence path in the PR or task summary. Keep manual prose in current docs and
keep score data in `docs/maturity-scores.yaml`.
keep score data in `qa/maturity-scores.yaml`.
## Default Completeness Process
@@ -139,7 +146,7 @@ Default guidance:
Default Completeness bands:
- `Lovable` (95-100): complete across expected workflows, variants, and
- `Clawesome` (95-100): complete across expected workflows, variants, and
recovery branches, with only minor polish gaps.
- `Stable` (80-95): the expected workflow set is broadly present, with only
bounded missing branches.
@@ -152,19 +159,20 @@ Default Completeness bands:
## Score Semantics
- Coverage: public or redacted proof that the feature is exercised by docs,
tests, QA scenarios, live lanes, or release evidence.
- Coverage: deterministic release validation coverage derived from the release
profile `qa-evidence.json.scorecard` feature fulfillment data.
- Quality: reliability, maintainability, operator safety, and regression
confidence for the category.
- Completeness: how much of the intended operator-visible workflow exists for
the category. Use the default completeness process plus any surface-specific
variation before changing this score.
- LTS: derived from score thresholds and `human_lts_override`; do not hand-edit
generated Markdown to change LTS status.
- LTS: derived from Quality, release-evidence Coverage, and
`human_lts_override`; do not hand-edit generated Markdown to change LTS
status.
Bands:
- `Lovable`: 95-100
- `Clawesome`: 95-100
- `Stable`: 80-95
- `Beta`: 70-80
- `Alpha`: 50-70

View File

@@ -0,0 +1,23 @@
# OpenClaw Maturity Scorecard Agent
You are refreshing the OpenClaw maturity score source for a release scorecard.
Goal: use the `$claw-score` skill to refresh `qa/maturity-scores.yaml` for every active surface in `taxonomy.yaml`, using the current repository and the release evidence artifacts in `.artifacts/maturity-evidence`.
Allowed tracked paths:
- `qa/maturity-scores.yaml`
Hard limits:
- Do not edit generated docs, taxonomy, workflows, scripts, package metadata, lockfiles, tests, or application code.
- Do not render docs. The workflow renders docs after validating the score source.
- Keep the score source schema valid for QA Lab maturity score validation.
Required workflow:
1. Use the `$claw-score` skill before editing.
2. Read `taxonomy.yaml`, any existing maturity score file, and the release evidence artifacts.
3. Refresh scores for every active surface in `taxonomy.yaml`.
4. Run the QA Lab maturity score validation used by this repository.
5. If no defensible score update is possible, leave a valid `qa/maturity-scores.yaml` and explain the uncertainty in the final message.

View File

@@ -261,6 +261,6 @@ jobs:
- name: Run Testbox
uses: useblacksmith/run-testbox@3f60ff9ceb2c10c3feefa87dc0c6490cffae059d
if: success()
if: always()
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"

View File

@@ -179,6 +179,6 @@ jobs:
- name: Run Testbox
uses: useblacksmith/run-testbox@5ca05834db1d3813554d1dd109e5f2087a8d7cbc
if: success()
if: always()
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"

View File

@@ -33,7 +33,7 @@ jobs:
contents: read
name: "check"
runs-on: blacksmith-32vcpu-ubuntu-2404
timeout-minutes: ${{ fromJSON(inputs.timeout_minutes || '30') }}
timeout-minutes: ${{ fromJSON(inputs.timeout_minutes || '120') }}
steps:
- name: Begin Testbox
uses: useblacksmith/begin-testbox@233448af4bfdc6fca509a7f0974411ac6d8a8043
@@ -168,6 +168,6 @@ jobs:
- name: Run Testbox
uses: useblacksmith/run-testbox@3f60ff9ceb2c10c3feefa87dc0c6490cffae059d
if: success()
if: always()
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"

View File

@@ -1177,7 +1177,9 @@ jobs:
timeout-minutes: ${{ matrix.timeout_minutes || 60 }}
strategy:
fail-fast: false
max-parallel: 12
# The canonical main path waits for the admission debounce above, so
# modestly widen this large matrix without recreating registration bursts.
max-parallel: 16
matrix: ${{ fromJson(needs.preflight.outputs.checks_node_core_nondist_matrix) }}
steps:
- name: Checkout
@@ -2233,7 +2235,7 @@ jobs:
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
with:
path: ~/.android-sdk
key: ${{ runner.os }}-android-sdk-v1-cmdline-14742923-platform-37.0-build-tools-36.0.0
key: ${{ runner.os }}-android-sdk-v1-cmdline-14742923-platform-36-build-tools-36.0.0
restore-keys: |
${{ runner.os }}-android-sdk-v1-
@@ -2263,7 +2265,7 @@ jobs:
yes | sdkmanager --sdk_root="${ANDROID_SDK_ROOT}" --licenses >/dev/null
sdkmanager --sdk_root="${ANDROID_SDK_ROOT}" --install \
"platform-tools" \
"platforms;android-37.0" \
"platforms;android-36" \
"build-tools;36.0.0"
- name: Run Android ${{ matrix.task }}

View File

@@ -22,12 +22,6 @@ on:
push:
branches:
- main
paths:
- ".github/actions/**"
- ".github/codeql/**"
- ".github/workflows/**"
- "packages/**"
- "src/**"
schedule:
- cron: "0 6 * * *"
@@ -55,32 +49,32 @@ jobs:
include:
- language: javascript-typescript
category: core-auth-secrets
runs_on: blacksmith-8vcpu-ubuntu-2404
runs_on: ubuntu-24.04
timeout_minutes: 25
config_file: ./.github/codeql/codeql-core-auth-secrets-critical-security.yml
- language: javascript-typescript
category: channel-runtime-boundary
runs_on: blacksmith-8vcpu-ubuntu-2404
runs_on: ubuntu-24.04
timeout_minutes: 25
config_file: ./.github/codeql/codeql-channel-runtime-boundary-critical-security.yml
- language: javascript-typescript
category: network-ssrf-boundary
runs_on: blacksmith-4vcpu-ubuntu-2404
runs_on: ubuntu-24.04
timeout_minutes: 25
config_file: ./.github/codeql/codeql-network-ssrf-boundary-critical-security.yml
- language: javascript-typescript
category: mcp-process-tool-boundary
runs_on: blacksmith-4vcpu-ubuntu-2404
runs_on: ubuntu-24.04
timeout_minutes: 25
config_file: ./.github/codeql/codeql-mcp-process-tool-boundary-critical-security.yml
- language: javascript-typescript
category: plugin-trust-boundary
runs_on: blacksmith-4vcpu-ubuntu-2404
runs_on: ubuntu-24.04
timeout_minutes: 25
config_file: ./.github/codeql/codeql-plugin-trust-boundary-critical-security.yml
- language: actions
category: actions
runs_on: blacksmith-8vcpu-ubuntu-2404
runs_on: ubuntu-24.04
timeout_minutes: 10
config_file: ./.github/codeql/codeql-actions-critical-security.yml
steps:

View File

@@ -490,7 +490,7 @@ jobs:
if (-not $env:CRABBOX_ID -or $env:CRABBOX_ID -notmatch '^[A-Za-z0-9._-]+$') {
Write-Error "Invalid crabbox_id"
}
$actionsRoot = Join-Path $HOME ".crabbox\actions"
$actionsRoot = "C:\ProgramData\crabbox\actions"
New-Item -ItemType Directory -Force $actionsRoot | Out-Null
$state = Join-Path $actionsRoot "$env:CRABBOX_ID.env"
$envFile = Join-Path $actionsRoot "$env:CRABBOX_ID.env.ps1"
@@ -546,7 +546,7 @@ jobs:
if ($env:CRABBOX_KEEP_ALIVE_MINUTES -match '^[0-9]+$') {
$minutes = [int]$env:CRABBOX_KEEP_ALIVE_MINUTES
}
$stop = Join-Path $HOME ".crabbox\actions\$env:CRABBOX_ID.stop"
$stop = Join-Path "C:\ProgramData\crabbox\actions" "$env:CRABBOX_ID.stop"
$deadline = (Get-Date).AddMinutes($minutes)
while ((Get-Date) -lt $deadline) {
if (Test-Path $stop) {

View File

@@ -27,10 +27,8 @@ jobs:
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
with:
script: |
const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");
const childProcess = require("node:child_process");
const zlib = require("node:zlib");
const marker = "<!-- openclaw-ios-periphery-dead-code -->";
const run = context.payload.workflow_run;
@@ -126,10 +124,7 @@ jobs:
archive_format: "zip",
});
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "ios-periphery-"));
const archivePath = path.join(dir, "artifact.zip");
const archiveBuffer = Buffer.from(archive.data);
fs.writeFileSync(archivePath, archiveBuffer);
const allowedArtifactFiles = new Set([
"periphery.json",
@@ -240,19 +235,59 @@ jobs:
return;
}
entries.set(name, { uncompressedSize });
entries.set(name, {
compressedSize,
compressionMethod,
localHeaderOffset: readUInt32(offset + 42),
uncompressedSize,
});
offset = nextOffset;
}
const readZipEntry = (name, entry) => {
const localHeaderOffset = entry.localHeaderOffset;
if (
localHeaderOffset + 30 > archiveBuffer.length ||
readUInt32(localHeaderOffset) !== 0x04034b50
) {
throw new Error(`${name} has an invalid local header.`);
}
const localNameLength = readUInt16(localHeaderOffset + 26);
const localExtraLength = readUInt16(localHeaderOffset + 28);
const dataStart = localHeaderOffset + 30 + localNameLength + localExtraLength;
const dataEnd = dataStart + entry.compressedSize;
if (dataEnd > archiveBuffer.length) {
throw new Error(`${name} exceeds archive bounds.`);
}
const compressed = archiveBuffer.subarray(dataStart, dataEnd);
let contents;
if (entry.compressionMethod === 0) {
contents = compressed;
} else {
try {
contents = zlib.inflateRawSync(compressed, { maxOutputLength: maxEntryBytes });
} catch (error) {
if (error && error.code === "ERR_BUFFER_TOO_LARGE") {
throw new Error(`${name} exceeded the per-file size limit while reading.`);
}
throw error;
}
}
if (contents.length !== entry.uncompressedSize || contents.length > maxEntryBytes) {
throw new Error(`${name} exceeded the per-file size limit while reading.`);
}
return contents.toString("utf8");
};
const files = new Map();
for (const [name, entry] of entries) {
const contents = childProcess.execFileSync("unzip", ["-p", archivePath, name], {
encoding: "utf8",
maxBuffer: Math.max(1, entry.uncompressedSize + 1024),
timeout: 5000,
});
if (Buffer.byteLength(contents, "utf8") > maxEntryBytes) {
core.warning(`Skipping ${artifactName}; ${name} exceeded the per-file size limit while reading.`);
let contents;
try {
contents = readZipEntry(name, entry);
} catch (error) {
core.warning(`Skipping ${artifactName}; ${error instanceof Error ? error.message : String(error)}`);
return;
}
files.set(name, contents);

View File

@@ -220,7 +220,7 @@ jobs:
with:
name: ios-periphery-dead-code-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ runner.temp }}/ios-periphery
if-no-files-found: warn
if-no-files-found: error
retention-days: 14
- name: Fail on dead code

View File

@@ -171,4 +171,4 @@ jobs:
name: mantis-discord-smoke-${{ github.run_id }}-${{ github.run_attempt }}
path: .artifacts/qa-e2e/mantis/
retention-days: 14
if-no-files-found: warn
if-no-files-found: error

View File

@@ -540,7 +540,7 @@ jobs:
name: mantis-discord-status-reactions-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ steps.run_mantis.outputs.output_dir }}
retention-days: 14
if-no-files-found: warn
if-no-files-found: error
- name: Create Mantis GitHub App token
id: mantis_app_token

View File

@@ -547,7 +547,7 @@ jobs:
with:
name: mantis-discord-thread-attachment-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ steps.run_mantis.outputs.output_dir }}
if-no-files-found: warn
if-no-files-found: error
retention-days: 14
- name: Create Mantis GitHub App token

View File

@@ -458,7 +458,7 @@ jobs:
name: mantis-slack-desktop-smoke-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ steps.run_mantis.outputs.output_dir }}
retention-days: 14
if-no-files-found: warn
if-no-files-found: error
- name: Create Mantis GitHub App token
id: mantis_app_token

View File

@@ -556,7 +556,7 @@ jobs:
name: mantis-telegram-desktop-proof-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ steps.inspect.outputs.output_dir }}
retention-days: 14
if-no-files-found: warn
if-no-files-found: error
- name: Create Mantis GitHub App token
id: mantis_app_token

View File

@@ -506,7 +506,7 @@ jobs:
name: mantis-telegram-live-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ steps.run_mantis.outputs.output_dir }}
retention-days: 14
if-no-files-found: warn
if-no-files-found: error
- name: Create Mantis GitHub App token
id: mantis_app_token

464
.github/workflows/maturity-scorecard.yml vendored Normal file
View File

@@ -0,0 +1,464 @@
name: Maturity scorecard
on:
workflow_dispatch:
inputs:
qa_evidence_run_id:
description: Optional workflow run id containing qa-evidence.json
required: false
type: string
ref:
description: OpenClaw branch, tag, or SHA containing the maturity score source
required: true
default: main
type: string
expected_sha:
description: Optional full SHA that ref must resolve to
required: false
default: ""
type: string
workflow_call:
inputs:
qa_evidence_run_id:
description: Optional workflow run id containing qa-evidence.json
required: false
default: ""
type: string
ref:
description: OpenClaw branch, tag, or SHA containing the maturity score source
required: true
type: string
expected_sha:
description: Optional full SHA that ref must resolve to
required: false
default: ""
type: string
secrets:
OPENAI_API_KEY:
description: OpenAI API key used by live QA profile scenarios
required: true
OPENCLAW_MATURITY_SCORECARD_AGENT_OPENAI_API_KEY:
description: Optional OpenAI API key used by maturity scorecard agent steps
required: false
GH_APP_PRIVATE_KEY:
description: Optional GitHub App private key for generated docs PR creation
required: false
GH_APP_PRIVATE_KEY_FALLBACK:
description: Optional fallback GitHub App private key for generated docs PR creation
required: false
permissions:
actions: read
contents: read
concurrency:
group: ${{ format('{0}-{1}-{2}', github.workflow, inputs.ref, inputs.qa_evidence_run_id || github.run_id) }}
cancel-in-progress: true
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
NODE_VERSION: "24.x"
jobs:
validate_selected_ref:
name: Validate selected ref
runs-on: ubuntu-24.04
outputs:
selected_revision: ${{ steps.validate.outputs.selected_revision }}
trusted_reason: ${{ steps.validate.outputs.trusted_reason }}
steps:
- name: Checkout selected ref
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: false
ref: ${{ inputs.ref }}
fetch-depth: 0
- name: Validate selected ref
id: validate
env:
EXPECTED_SHA: ${{ inputs.expected_sha }}
INPUT_REF: ${{ inputs.ref }}
shell: bash
run: |
set -euo pipefail
selected_revision="$(git rev-parse HEAD)"
expected_sha="${EXPECTED_SHA,,}"
trusted_reason=""
if [[ -n "${expected_sha// }" && ! "$expected_sha" =~ ^[0-9a-f]{40}$ ]]; then
echo "expected_sha must be a full 40-character SHA; got: ${EXPECTED_SHA}" >&2
exit 1
fi
if [[ -n "${expected_sha// }" && "${selected_revision,,}" != "$expected_sha" ]]; then
echo "Ref '${INPUT_REF}' resolved to ${selected_revision}, expected ${EXPECTED_SHA}." >&2
exit 1
fi
git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main
if git merge-base --is-ancestor "$selected_revision" refs/remotes/origin/main; then
trusted_reason="main-ancestor"
elif git tag --points-at "$selected_revision" | grep -Eq '^v'; then
trusted_reason="release-tag"
elif [[ "$INPUT_REF" =~ ^release/[0-9]{4}\.[0-9]+\.[0-9]+$ ]]; then
git fetch --no-tags origin "+refs/heads/${INPUT_REF}:refs/remotes/origin/${INPUT_REF}"
release_branch_sha="$(git rev-parse "refs/remotes/origin/${INPUT_REF}")"
if [[ "$selected_revision" == "$release_branch_sha" ]]; then
trusted_reason="release-branch-head"
fi
fi
if [[ -z "$trusted_reason" ]]; then
echo "Ref '${INPUT_REF}' resolved to $selected_revision, which is not trusted for this secret-bearing maturity scorecard run." >&2
echo "Allowed refs must be on main, point to a release tag, or match a release branch head." >&2
exit 1
fi
echo "selected_revision=$selected_revision" >> "$GITHUB_OUTPUT"
echo "trusted_reason=$trusted_reason" >> "$GITHUB_OUTPUT"
{
echo "### Target"
echo
echo "- Requested ref: \`${INPUT_REF}\`"
echo "- Resolved SHA: \`$selected_revision\`"
echo "- Trust reason: \`$trusted_reason\`"
} >> "$GITHUB_STEP_SUMMARY"
generate_qa_evidence:
name: Generate full taxonomy QA evidence
needs: validate_selected_ref
if: ${{ inputs.qa_evidence_run_id == '' }}
uses: ./.github/workflows/qa-profile-evidence.yml
with:
ref: ${{ inputs.ref }}
expected_sha: ${{ needs.validate_selected_ref.outputs.selected_revision }}
qa_profile: release
secrets:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
publish:
name: Publish maturity docs PR
needs:
- validate_selected_ref
- generate_qa_evidence
if: ${{ always() && needs.validate_selected_ref.result == 'success' && (inputs.qa_evidence_run_id != '' || needs.generate_qa_evidence.result == 'success') }}
runs-on: ubuntu-24.04
timeout-minutes: 30
permissions:
actions: read
contents: read
steps:
- name: Checkout selected ref
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ needs.validate_selected_ref.outputs.selected_revision }}
fetch-depth: 1
fetch-tags: false
persist-credentials: false
submodules: false
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
install-bun: "false"
- name: Download provided QA evidence artifact
if: ${{ inputs.qa_evidence_run_id != '' }}
env:
GH_TOKEN: ${{ github.token }}
QA_EVIDENCE_RUN_ID: ${{ inputs.qa_evidence_run_id }}
run: |
set -euo pipefail
mkdir -p .artifacts/maturity-evidence
gh run download "$QA_EVIDENCE_RUN_ID" \
--repo "$GITHUB_REPOSITORY" \
--dir .artifacts/maturity-evidence
- name: Download generated QA evidence artifact
if: ${{ inputs.qa_evidence_run_id == '' }}
env:
GENERATED_ARTIFACT_NAME: ${{ needs.generate_qa_evidence.outputs.artifact_name }}
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
if [[ -z "${GENERATED_ARTIFACT_NAME:-}" ]]; then
echo "Generated QA evidence workflow did not expose an artifact name." >&2
exit 1
fi
mkdir -p .artifacts/maturity-evidence
gh run download "$GITHUB_RUN_ID" \
--repo "$GITHUB_REPOSITORY" \
--name "$GENERATED_ARTIFACT_NAME" \
--dir .artifacts/maturity-evidence
- name: Require one QA evidence file
id: evidence
env:
QA_EVIDENCE_RUN_ID: ${{ inputs.qa_evidence_run_id }}
run: |
set -euo pipefail
mapfile -t evidence_paths < <(find .artifacts/maturity-evidence -type f -name qa-evidence.json | sort)
if [[ "${#evidence_paths[@]}" -eq 0 ]]; then
echo "Expected a qa-evidence.json file in the downloaded QA evidence artifact." >&2
exit 1
fi
if [[ "${#evidence_paths[@]}" -gt 1 ]]; then
echo "Expected exactly one qa-evidence.json file, found ${#evidence_paths[@]}:" >&2
printf '%s\n' "${evidence_paths[@]}" >&2
exit 1
fi
echo "qa_evidence_path=${evidence_paths[0]}" >> "$GITHUB_OUTPUT"
{
echo "### QA evidence"
echo
echo "- Evidence path: \`${evidence_paths[0]}\`"
echo "- Evidence source run: \`${QA_EVIDENCE_RUN_ID:-$GITHUB_RUN_ID}\`"
} >> "$GITHUB_STEP_SUMMARY"
- name: Validate QA evidence manifest
env:
QA_EVIDENCE_PATH: ${{ steps.evidence.outputs.qa_evidence_path }}
TARGET_SHA: ${{ needs.validate_selected_ref.outputs.selected_revision }}
run: |
set -euo pipefail
node --input-type=module <<'NODE'
import fs from "node:fs";
import path from "node:path";
const evidencePath = process.env.QA_EVIDENCE_PATH;
const targetSha = process.env.TARGET_SHA;
if (!evidencePath) {
throw new Error("QA_EVIDENCE_PATH is required");
}
if (!targetSha) {
throw new Error("TARGET_SHA is required");
}
const evidence = JSON.parse(fs.readFileSync(evidencePath, "utf8"));
if (evidence.profile !== "release") {
throw new Error(`qa-evidence.json profile must be release, got ${JSON.stringify(evidence.profile)}`);
}
const artifactDir = path.dirname(evidencePath);
const manifestNames = fs
.readdirSync(artifactDir)
.filter((name) => name.endsWith("qa-profile-evidence-manifest.json"))
.sort();
if (manifestNames.length !== 1) {
throw new Error(
`Expected exactly one QA profile evidence manifest next to qa-evidence.json, found ${manifestNames.length}`,
);
}
const manifestPath = path.join(artifactDir, manifestNames[0]);
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
const manifestProfile = manifest.qaProfile ?? evidence.profile;
if (manifestProfile !== "release") {
throw new Error(`QA evidence manifest profile must be release, got ${JSON.stringify(manifestProfile)}`);
}
if (manifest.targetSha !== targetSha) {
throw new Error(`QA evidence manifest targetSha ${manifest.targetSha} does not match selected ref ${targetSha}`);
}
NODE
- name: Ensure maturity scorecard agent key exists
env:
OPENAI_API_KEY: ${{ secrets.OPENCLAW_MATURITY_SCORECARD_AGENT_OPENAI_API_KEY || secrets.OPENAI_API_KEY }}
run: |
set -euo pipefail
if [[ -z "${OPENAI_API_KEY:-}" ]]; then
echo "Missing OPENCLAW_MATURITY_SCORECARD_AGENT_OPENAI_API_KEY or OPENAI_API_KEY secret." >&2
exit 1
fi
- name: Run Codex maturity scorecard agent
uses: openai/codex-action@e0fdf01220eb9a88167c4898839d273e3f2609d1
env:
MATURITY_EVIDENCE_DIR: .artifacts/maturity-evidence
MATURITY_SCORES_PATH: qa/maturity-scores.yaml
MATURITY_TAXONOMY_PATH: taxonomy.yaml
with:
openai-api-key: ${{ secrets.OPENCLAW_MATURITY_SCORECARD_AGENT_OPENAI_API_KEY || secrets.OPENAI_API_KEY }}
prompt-file: .github/codex/prompts/maturity-scorecard-agent.md
model: ${{ vars.OPENCLAW_CI_OPENAI_MODEL_BARE }}
effort: high
sandbox: workspace-write
safety-strategy: drop-sudo
- name: Enforce focused maturity score patch
run: |
set -euo pipefail
git restore --staged :/
allowed='^qa/maturity-scores\.yaml$'
bad_tracked="$(
git diff --name-only HEAD -- | while IFS= read -r path; do
if [[ ! "$path" =~ $allowed ]]; then
printf '%s\n' "$path"
fi
done
)"
if [[ -n "$bad_tracked" ]]; then
echo "Maturity scorecard agent touched forbidden tracked paths:"
printf '%s\n' "$bad_tracked"
exit 1
fi
bad_untracked="$(
git ls-files --others --exclude-standard | while IFS= read -r path; do
if [[ "$path" != "qa/maturity-scores.yaml" ]]; then
printf '%s\n' "$path"
fi
done
)"
if [[ -n "$bad_untracked" ]]; then
echo "Maturity scorecard agent created forbidden untracked paths:"
printf '%s\n' "$bad_untracked"
exit 1
fi
if [[ ! -f qa/maturity-scores.yaml ]]; then
echo "Maturity scorecard agent must produce qa/maturity-scores.yaml." >&2
exit 1
fi
- name: Validate maturity score sources
run: |
node --import tsx --input-type=module <<'NODE'
import { readValidatedQaMaturityScoreSources } from "./extensions/qa-lab/src/scorecard-taxonomy.ts";
const { warnings } = readValidatedQaMaturityScoreSources({
scoresPath: "qa/maturity-scores.yaml",
taxonomyPath: "taxonomy.yaml",
});
for (const warning of warnings) {
console.error(`warning: ${warning}`);
}
NODE
- name: Render artifact docs
run: |
set -euo pipefail
pnpm maturity:render -- \
--output-dir .artifacts/maturity-docs \
--static-assets-dir .artifacts/maturity-docs/assets/maturity \
--scores qa/maturity-scores.yaml \
--evidence-dir .artifacts/maturity-evidence \
--strict-inputs
{
echo "### Maturity scorecard docs"
echo
echo "- Source validation: passed"
echo "- Artifact docs: \`.artifacts/maturity-docs\`"
echo "- Strict inputs: \`true\`"
echo "- QA evidence: included"
} >> "$GITHUB_STEP_SUMMARY"
- name: Render committed docs preview
run: |
set -euo pipefail
pnpm maturity:render -- \
--output-dir docs \
--scores qa/maturity-scores.yaml \
--evidence-dir .artifacts/maturity-evidence \
--strict-inputs
- name: Create generated docs PR app token
if: ${{ github.event_name == 'workflow_dispatch' }}
id: app-token
continue-on-error: true
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3
with:
app-id: "2729701"
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
permission-contents: write
permission-pull-requests: write
- name: Create generated docs PR fallback app token
if: ${{ github.event_name == 'workflow_dispatch' && steps.app-token.outcome == 'failure' }}
id: app-token-fallback
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3
with:
app-id: "2971289"
private-key: ${{ secrets.GH_APP_PRIVATE_KEY_FALLBACK }}
permission-contents: write
permission-pull-requests: write
- name: Open generated docs PR
if: ${{ github.event_name == 'workflow_dispatch' }}
env:
GH_TOKEN: ${{ steps.app-token.outputs.token || steps.app-token-fallback.outputs.token }}
QA_EVIDENCE_RUN_ID: ${{ inputs.qa_evidence_run_id }}
REF_INPUT: ${{ inputs.ref }}
run: |
set -euo pipefail
if [[ -z "${GH_TOKEN:-}" ]]; then
echo "Maturity scorecard PR creation requires the OpenClaw GitHub App token secrets." >&2
exit 1
fi
if [[ -z "$(git status --porcelain -- qa/maturity-scores.yaml docs/maturity/scorecard.md docs/maturity/taxonomy.md)" ]]; then
{
echo
echo "- Pull request: skipped; generated scorecard matches selected ref"
} >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
evidence_run_id="${QA_EVIDENCE_RUN_ID:-$GITHUB_RUN_ID}"
branch="automation/maturity-scorecard-${evidence_run_id}"
base_branch="${REF_INPUT:-main}"
if ! git ls-remote --exit-code --heads origin "$base_branch" >/dev/null 2>&1; then
base_branch="main"
fi
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
gh auth setup-git
git fetch --no-tags --depth=1 origin "refs/heads/${branch}:refs/remotes/origin/${branch}" || true
git switch -C "$branch"
git add qa/maturity-scores.yaml docs/maturity/scorecard.md docs/maturity/taxonomy.md
git commit -m "docs: update maturity scorecard"
git push --force-with-lease origin "$branch"
body_file=".artifacts/maturity-scorecard-pr-body.md"
mkdir -p "$(dirname "$body_file")"
cat > "$body_file" <<BODY
## Summary
- render maturity scorecard docs from \`qa/maturity-scores.yaml\` and release QA evidence
- maturity source ref: ${REF_INPUT}
- QA evidence run: ${evidence_run_id}
## Verification
- QA Lab maturity score validation passed
- Maturity scorecard workflow rendered docs from release profile qa-evidence.json artifacts with strict inputs
BODY
pr_url="$(gh pr list --head "$branch" --state open --json url --jq '.[0].url // ""')"
if [[ -n "$pr_url" ]]; then
gh pr edit "$pr_url" \
--title "docs: update maturity scorecard" \
--body-file "$body_file"
else
pr_url="$(gh pr create \
--base "$base_branch" \
--head "$branch" \
--title "docs: update maturity scorecard" \
--body-file "$body_file")"
fi
{
echo
echo "- Pull request: ${pr_url}"
} >> "$GITHUB_STEP_SUMMARY"
- name: Upload maturity docs artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: maturity-scorecard-docs-${{ github.run_id }}-${{ github.run_attempt }}
path: .artifacts/maturity-docs/
retention-days: 30
if-no-files-found: error

View File

@@ -273,4 +273,4 @@ jobs:
name: npm-telegram-beta-e2e-${{ github.run_id }}-${{ github.run_attempt }}
path: .artifacts/qa-e2e/
retention-days: 14
if-no-files-found: warn
if-no-files-found: error

View File

@@ -44,6 +44,11 @@ on:
required: false
default: false
type: boolean
run_maturity_scorecard:
description: Render advisory maturity scorecard release docs; default release checks rely on dedicated package, QA, live, and E2E gates
required: false
default: false
type: boolean
rerun_group:
description: Release check group to run
required: false
@@ -106,6 +111,7 @@ jobs:
mode: ${{ steps.inputs.outputs.mode }}
release_profile: ${{ steps.inputs.outputs.release_profile }}
run_release_soak: ${{ steps.inputs.outputs.run_release_soak }}
run_maturity_scorecard: ${{ steps.inputs.outputs.run_maturity_scorecard }}
rerun_group: ${{ steps.inputs.outputs.rerun_group }}
live_suite_filter: ${{ steps.inputs.outputs.live_suite_filter }}
cross_os_suite_filter: ${{ steps.inputs.outputs.cross_os_suite_filter }}
@@ -279,6 +285,7 @@ jobs:
RELEASE_MODE_INPUT: ${{ inputs.mode }}
RELEASE_PROFILE_INPUT: ${{ inputs.release_profile }}
RELEASE_RUN_RELEASE_SOAK_INPUT: ${{ inputs.run_release_soak }}
RELEASE_RUN_MATURITY_SCORECARD_INPUT: ${{ inputs.run_maturity_scorecard }}
RELEASE_RERUN_GROUP_INPUT: ${{ inputs.rerun_group }}
RELEASE_LIVE_SUITE_FILTER_INPUT: ${{ inputs.live_suite_filter }}
RELEASE_CROSS_OS_SUITE_FILTER_INPUT: ${{ inputs.cross_os_suite_filter }}
@@ -319,6 +326,12 @@ jobs:
else
run_release_soak=true
fi
run_maturity_scorecard="$(printf '%s' "$RELEASE_RUN_MATURITY_SCORECARD_INPUT" | tr '[:upper:]' '[:lower:]')"
if [[ "$run_maturity_scorecard" != "true" && "$run_maturity_scorecard" != "1" && "$run_maturity_scorecard" != "yes" ]]; then
run_maturity_scorecard=false
else
run_maturity_scorecard=true
fi
release_profile="$RELEASE_PROFILE_INPUT"
if [[ "$release_profile" == "minimum" ]]; then
release_profile=beta
@@ -422,6 +435,7 @@ jobs:
printf 'mode=%s\n' "$RELEASE_MODE_INPUT"
printf 'release_profile=%s\n' "$release_profile"
printf 'run_release_soak=%s\n' "$run_release_soak"
printf 'run_maturity_scorecard=%s\n' "$run_maturity_scorecard"
printf 'rerun_group=%s\n' "$RELEASE_RERUN_GROUP_INPUT"
printf 'live_suite_filter=%s\n' "$RELEASE_LIVE_SUITE_FILTER_INPUT"
printf 'cross_os_suite_filter=%s\n' "$RELEASE_CROSS_OS_SUITE_FILTER_INPUT"
@@ -444,6 +458,7 @@ jobs:
RELEASE_MODE: ${{ inputs.mode }}
RELEASE_PROFILE: ${{ steps.inputs.outputs.release_profile }}
RUN_RELEASE_SOAK: ${{ steps.inputs.outputs.run_release_soak }}
RUN_MATURITY_SCORECARD: ${{ steps.inputs.outputs.run_maturity_scorecard }}
RELEASE_RERUN_GROUP: ${{ inputs.rerun_group }}
RELEASE_LIVE_SUITE_FILTER: ${{ inputs.live_suite_filter }}
RELEASE_CROSS_OS_SUITE_FILTER: ${{ inputs.cross_os_suite_filter }}
@@ -461,6 +476,7 @@ jobs:
echo "- Cross-OS mode: \`${RELEASE_MODE}\`"
echo "- Release profile: \`${RELEASE_PROFILE}\`"
echo "- Release soak lanes: \`${RUN_RELEASE_SOAK}\`"
echo "- Maturity scorecard docs: \`${RUN_MATURITY_SCORECARD}\`"
echo "- Rerun group: \`${RELEASE_RERUN_GROUP}\`"
if [[ -n "${RELEASE_LIVE_SUITE_FILTER// }" ]]; then
echo "- Live suite filter: \`${RELEASE_LIVE_SUITE_FILTER}\`"
@@ -767,6 +783,20 @@ jobs:
OPENCLAW_QA_CONVEX_SITE_URL: ${{ secrets.OPENCLAW_QA_CONVEX_SITE_URL }}
OPENCLAW_QA_CONVEX_SECRET_CI: ${{ secrets.OPENCLAW_QA_CONVEX_SECRET_CI }}
maturity_scorecard_release_checks:
name: Render maturity scorecard release docs
needs: [resolve_target]
if: contains(fromJSON('["all","qa"]'), needs.resolve_target.outputs.rerun_group) && needs.resolve_target.outputs.run_maturity_scorecard == 'true'
permissions:
actions: read
contents: read
uses: ./.github/workflows/maturity-scorecard.yml
with:
ref: ${{ needs.resolve_target.outputs.ref }}
expected_sha: ${{ needs.resolve_target.outputs.revision }}
secrets:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
qa_lab_parity_lane_release_checks:
name: Run QA Lab parity lane (${{ matrix.lane }})
needs: [resolve_target]
@@ -853,7 +883,7 @@ jobs:
name: release-qa-parity-${{ matrix.lane }}-${{ needs.resolve_target.outputs.revision }}
path: .artifacts/qa-e2e/
retention-days: 14
if-no-files-found: warn
if-no-files-found: error
- name: Record advisory status
if: always()
@@ -959,7 +989,7 @@ jobs:
name: release-qa-parity-${{ needs.resolve_target.outputs.revision }}
path: .artifacts/qa-e2e/
retention-days: 14
if-no-files-found: warn
if-no-files-found: error
- name: Record advisory status
if: always()
@@ -1131,7 +1161,7 @@ jobs:
name: release-qa-runtime-parity-${{ needs.resolve_target.outputs.revision }}
path: .artifacts/qa-e2e/
retention-days: 14
if-no-files-found: warn
if-no-files-found: error
- name: Record advisory status
if: always()
@@ -1241,13 +1271,13 @@ jobs:
--output .artifacts/qa-e2e/runtime-parity-standard-report/qa-runtime-tool-coverage-report.md
- name: Upload runtime tool coverage artifacts
if: always()
if: ${{ always() && steps.verify_runtime_parity_status.outputs.ready == 'true' }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: release-qa-runtime-tool-coverage-${{ needs.resolve_target.outputs.revision }}
path: .artifacts/qa-e2e/runtime-parity-standard-report/
retention-days: 14
if-no-files-found: warn
if-no-files-found: error
qa_live_matrix_release_checks:
name: Run QA Lab live Matrix lane
@@ -1327,7 +1357,7 @@ jobs:
name: release-qa-live-matrix-${{ needs.resolve_target.outputs.revision }}
path: .artifacts/qa-e2e/
retention-days: 14
if-no-files-found: warn
if-no-files-found: error
- name: Record advisory status
if: always()
@@ -1467,7 +1497,7 @@ jobs:
name: release-qa-live-telegram-${{ needs.resolve_target.outputs.revision }}
path: .artifacts/qa-e2e/
retention-days: 14
if-no-files-found: warn
if-no-files-found: error
- name: Record advisory status
if: always()
@@ -1607,7 +1637,7 @@ jobs:
name: release-qa-live-discord-${{ needs.resolve_target.outputs.revision }}
path: .artifacts/qa-e2e/
retention-days: 14
if-no-files-found: warn
if-no-files-found: error
- name: Record advisory status
if: always()
@@ -1750,7 +1780,7 @@ jobs:
name: release-qa-live-whatsapp-${{ needs.resolve_target.outputs.revision }}
path: .artifacts/qa-e2e/
retention-days: 14
if-no-files-found: warn
if-no-files-found: error
- name: Record advisory status
if: always()
@@ -1890,7 +1920,7 @@ jobs:
name: release-qa-live-slack-${{ needs.resolve_target.outputs.revision }}
path: .artifacts/qa-e2e/
retention-days: 14
if-no-files-found: warn
if-no-files-found: error
- name: Record advisory status
if: always()
@@ -1946,6 +1976,7 @@ jobs:
- docker_e2e_release_checks
- package_acceptance_release_checks
- qa_lab_parity_lane_release_checks
- maturity_scorecard_release_checks
- qa_lab_parity_report_release_checks
- qa_lab_runtime_parity_release_checks
- runtime_tool_coverage_release_checks
@@ -2031,6 +2062,7 @@ jobs:
"docker_e2e_release_checks=${{ needs.docker_e2e_release_checks.result }}" \
"package_acceptance_release_checks=${{ needs.package_acceptance_release_checks.result }}" \
"qa_lab_parity_lane_release_checks=${{ needs.qa_lab_parity_lane_release_checks.result }}" \
"maturity_scorecard_release_checks=${{ needs.maturity_scorecard_release_checks.result }}" \
"qa_lab_parity_report_release_checks=${{ needs.qa_lab_parity_report_release_checks.result }}" \
"qa_lab_runtime_parity_release_checks=${{ needs.qa_lab_runtime_parity_release_checks.result }}" \
"runtime_tool_coverage_release_checks=${{ needs.runtime_tool_coverage_release_checks.result }}" \

View File

@@ -1466,9 +1466,9 @@ jobs:
fi
- name: Upload postpublish evidence
if: ${{ always() }}
if: ${{ always() && inputs.publish_openclaw_npm }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: openclaw-release-postpublish-evidence-${{ inputs.tag }}
path: ${{ runner.temp }}/openclaw-release-postpublish-evidence
if-no-files-found: ignore
if-no-files-found: error

View File

@@ -66,5 +66,5 @@ jobs:
with:
name: opengrep-full-sarif
path: .opengrep-out/precise.sarif
if-no-files-found: warn
if-no-files-found: error
retention-days: 30

View File

@@ -97,5 +97,5 @@ jobs:
with:
name: opengrep-pr-diff-sarif
path: .opengrep-out/precise.sarif
if-no-files-found: warn
if-no-files-found: error
retention-days: 30

View File

@@ -226,7 +226,7 @@ jobs:
name: qa-parity-${{ github.run_id }}-${{ github.run_attempt }}
path: .artifacts/qa-e2e/
retention-days: 14
if-no-files-found: warn
if-no-files-found: error
run_live_runtime_token_efficiency:
name: Run live runtime token-efficiency lane
@@ -315,7 +315,7 @@ jobs:
name: qa-live-runtime-token-efficiency-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ steps.run_lane.outputs.output_dir }}
retention-days: 14
if-no-files-found: warn
if-no-files-found: error
run_live_matrix:
name: Run Matrix live QA lane
@@ -391,7 +391,7 @@ jobs:
name: qa-live-matrix-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ steps.run_lane.outputs.output_dir }}
retention-days: 14
if-no-files-found: warn
if-no-files-found: error
run_live_matrix_sharded:
name: Run Matrix live QA lane (${{ matrix.profile }})
@@ -475,7 +475,7 @@ jobs:
name: qa-live-matrix-${{ matrix.profile }}-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ steps.run_lane.outputs.output_dir }}
retention-days: 14
if-no-files-found: warn
if-no-files-found: error
run_live_telegram:
name: Run Telegram live QA lane with Convex leases
@@ -570,7 +570,7 @@ jobs:
name: qa-live-telegram-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ steps.run_lane.outputs.output_dir }}
retention-days: 14
if-no-files-found: warn
if-no-files-found: error
run_live_discord:
name: Run Discord live QA lane with Convex leases
@@ -665,7 +665,7 @@ jobs:
name: qa-live-discord-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ steps.run_lane.outputs.output_dir }}
retention-days: 14
if-no-files-found: warn
if-no-files-found: error
run_live_whatsapp:
name: Run WhatsApp live QA lane with Convex leases
@@ -763,7 +763,7 @@ jobs:
name: qa-live-whatsapp-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ steps.run_lane.outputs.output_dir }}
retention-days: 14
if-no-files-found: warn
if-no-files-found: error
run_live_slack:
name: Run Slack live QA lane with Convex leases
@@ -859,4 +859,4 @@ jobs:
name: qa-live-slack-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ steps.run_lane.outputs.output_dir }}
retention-days: 14
if-no-files-found: warn
if-no-files-found: error

View File

@@ -16,7 +16,7 @@ on:
default: ""
type: string
qa_profile:
description: Taxonomy QA profile id to run
description: Taxonomy QA profile id to run (for example release or all)
required: true
default: release
type: string
@@ -35,11 +35,10 @@ on:
description: Taxonomy QA profile id to run
required: true
type: string
fail_on_qa_failure:
description: Fail the reusable workflow when the QA profile command exits non-zero
required: false
default: false
type: boolean
secrets:
OPENAI_API_KEY:
description: OpenAI API key used by live QA profile scenarios
required: true
outputs:
artifact_name:
description: Uploaded QA profile evidence artifact name
@@ -90,6 +89,13 @@ jobs:
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
with:
script: |
// Reusable workflow jobs inherit the caller event but run as
// github-actions[bot]; selected ref validation still gates secrets.
if (context.actor === "github-actions[bot]") {
core.info("Skipping manual actor permission check for a reusable workflow call.");
core.setOutput("authorized", "true");
return;
}
if (context.eventName !== "workflow_dispatch") {
core.info(`Skipping manual actor permission check for ${context.eventName}.`);
core.setOutput("authorized", "true");
@@ -244,6 +250,9 @@ jobs:
NODE_OPTIONS: --max-old-space-size=8192
run: node scripts/build-all.mjs qaRuntime
- name: Ensure Playwright Chromium
run: node scripts/ensure-playwright-chromium.mjs
- name: Run QA profile
id: run_profile
env:
@@ -358,8 +367,8 @@ jobs:
retention-days: 30
if-no-files-found: error
- name: Fail if configured QA gate failed
if: always() && (github.event_name == 'workflow_dispatch' || inputs.fail_on_qa_failure)
- name: Fail if QA profile failed
if: always()
env:
QA_EXIT_CODE: ${{ steps.run_profile.outputs.qa_exit_code }}
QA_PROFILE: ${{ steps.profile.outputs.profile }}

View File

@@ -150,6 +150,7 @@ jobs:
git --version
- name: Run Testbox
if: always()
shell: bash
run: |
set -euo pipefail

View File

@@ -37,6 +37,7 @@ This audited record covers the complete v2026.6.8..HEAD history: 423 merged PRs.
#### Pull requests
- **PR #92154** fix(qqbot): gate private group commands and close strict command visibility gaps. Thanks @sliverp.
- **PR #90463** refactor: add session accessor seam with gateway consumer. Thanks @jalehman.
- **PR #88656** Drop reasoning-only length turns from replay. Thanks @abel-zer0.
- **PR #92856** feat(webui): add session workspace rail. Thanks @Solvely-Colin.

View File

@@ -61,7 +61,7 @@ We prioritize secure defaults, but also expose clear knobs for trusted high-powe
## Plugins & Memory
OpenClaw has an extensive plugin API.
Core stays lean; optional capability should usually ship as plugins.
Core stays lean; optional capabilities should usually ship as plugins.
We are generally slimming down core while expanding what plugins can do.
If a useful feature cannot be built as a plugin yet, we welcome PRs and design discussions that extend the plugin API instead of adding one-off core behavior.

View File

@@ -59,7 +59,7 @@ plugins {
android {
namespace = "ai.openclaw.app"
compileSdk = 37
compileSdk = 36
// Release signing is local-only; keep the keystore path and passwords out of the repo.
signingConfigs {

View File

@@ -49,18 +49,8 @@ import java.util.concurrent.Executors
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
private fun createDnsResolver(context: Context): DnsResolver =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.CINNAMON_BUN) {
createContextDnsResolver(context)
} else {
createLegacyDnsResolver()
}
@RequiresApi(Build.VERSION_CODES.CINNAMON_BUN)
private fun createContextDnsResolver(context: Context): DnsResolver = DnsResolver(context, null)
@Suppress("DEPRECATION")
private fun createLegacyDnsResolver(): DnsResolver = DnsResolver.getInstance()
private fun createDnsResolver(): DnsResolver = DnsResolver.getInstance()
/**
* Watches local DNS-SD and optional wide-area DNS-SD for reachable OpenClaw gateways.
@@ -71,7 +61,7 @@ class GatewayDiscovery(
) {
private val nsd = context.getSystemService(NsdManager::class.java)
private val connectivity = context.getSystemService(ConnectivityManager::class.java)
private val dns = createDnsResolver(context)
private val dns = createDnsResolver()
private val serviceType = "_openclaw-gw._tcp."
private val wideAreaDomain = System.getenv("OPENCLAW_WIDE_AREA_DOMAIN")
private val logTag = "OpenClaw/GatewayDiscovery"

View File

@@ -5,7 +5,7 @@ plugins {
android {
namespace = "ai.openclaw.app.benchmark"
compileSdk = 37
compileSdk = 36
defaultConfig {
minSdk = 31

View File

@@ -4,7 +4,7 @@ androidx-activity = "1.13.0"
androidx-benchmark = "1.4.1"
androidx-camera = "1.6.0"
androidx-compose-bom = "2026.05.01"
androidx-core = "1.19.0"
androidx-core = "1.18.0"
androidx-exifinterface = "1.4.2"
androidx-lifecycle = "2.10.0"
androidx-security = "1.1.0"

View File

@@ -67,9 +67,9 @@ Release behavior:
- App Store release uses canonical `ai.openclawfoundation.app*` bundle IDs through a temporary generated xcconfig in `apps/ios/build/AppStoreRelease.xcconfig`.
- App Store release uses manual `Apple Distribution` signing with profile names pinned in `apps/ios/Config/AppStoreSigning.json`.
- Fastlane owns one-time Developer Portal setup, encrypted `match` signing sync to the repo/branch pinned in `apps/ios/Config/AppStoreSigning.json`, and release handling.
- App Store release also switches the app to `OpenClawPushTransport=relay`, `OpenClawPushDistribution=official`, `OpenClawPushAPNsEnvironment=production`, `OpenClawPushRelayProfile=production`, `OpenClawPushProofPolicy=appleStrict`, and the App-Attest-capable entitlement file.
- App Store release also switches the app to `OpenClawPushMode=appStore`, which derives relay transport, official distribution, the canonical production relay, production APNs, production relay profile, `appleStrict` proof, and the App-Attest-capable entitlement file.
- `pnpm ios:release:upload` generates App Store screenshots and uploads release notes before archiving and uploading the IPA.
- `pnpm ios:release` remains a compatibility alias for `pnpm ios:release:upload`; prefer the explicit upload command in new release docs and automation.
- The release archive is validated before upload by inspecting the exported IPA's signed entitlements, embedded App Store profile, and push mode. The upload fails if the IPA is not an App Store production relay build.
- App Review submission is manual in App Store Connect. The release lane uploads a build and metadata, but does not submit for review.
- The release flow does not modify `apps/ios/.local-signing.xcconfig` or `apps/ios/LocalSigning.xcconfig`.
- `apps/ios/version.json` is the pinned iOS release version source.
@@ -83,9 +83,8 @@ Release behavior:
Relay behavior for App Store builds:
- Release builds default to `https://ios-push-relay.openclaw.ai`.
- Optional custom relay override: `OPENCLAW_PUSH_RELAY_BASE_URL=https://relay.example.com`
This must be a plain `https://host[:port][/path]` base URL without whitespace, query params, fragments, or xcconfig metacharacters.
- App Store release builds use the canonical hosted relay at `https://ios-push-relay.openclaw.ai`.
- App Store release builds reject custom relay URL overrides. Future self-hosted relay support should use a separate explicit release path, not the public App Store build lane.
Signing setup commands:
@@ -162,25 +161,19 @@ This should create `apps/ios/fastlane/.env` with non-secret App Store Connect va
Use `pnpm ios:release:signing:setup` for the initial portal setup, then `MATCH_PASSWORD=... pnpm ios:release:signing:sync:push` to publish encrypted Fastlane match assets to the shared private repo.
4. Optional: set a custom official relay URL for the build. If unset, the release flow uses `https://ios-push-relay.openclaw.ai`.
```bash
export OPENCLAW_PUSH_RELAY_BASE_URL=https://relay.example.com
```
5. If you are starting a brand-new production release train, pin iOS to the current gateway version first:
4. If you are starting a brand-new production release train, pin iOS to the current gateway version first:
```bash
pnpm ios:version:pin -- --from-gateway
```
6. Upload the build:
5. Upload the build:
```bash
pnpm ios:release:upload
```
7. Expected behavior:
6. Expected behavior:
- Fastlane reads `apps/ios/version.json`
- verifies synced iOS versioning artifacts
- resolves the next App Store Connect build number for that short version
@@ -188,15 +181,16 @@ pnpm ios:release:upload
- uploads release notes and screenshots to the editable App Store version
- generates `apps/ios/build/AppStoreRelease.xcconfig`
- archives `OpenClaw`
- validates the exported IPA's push mode, signed entitlements, and embedded App Store profile
- uploads the IPA to App Store Connect for TestFlight/App Review use
- leaves App Review submission for a maintainer to complete manually
8. Expected outputs after a successful run:
7. Expected outputs after a successful run:
- `apps/ios/build/app-store/OpenClaw-<version>.ipa`
- `apps/ios/build/app-store/OpenClaw-<version>.app.dSYM.zip`
- Fastlane log line like `Uploaded iOS App Store build: version=<version> short=<short> build=<build>`
9. If this is a fresh clone on a maintainer machine that already works elsewhere, it is OK to copy the non-secret `apps/ios/fastlane/.env` from another trusted local clone on the same Mac. The Keychain-backed private key remains machine-local and is not stored in the repo.
8. If this is a fresh clone on a maintainer machine that already works elsewhere, it is OK to copy the non-secret `apps/ios/fastlane/.env` from another trusted local clone on the same Mac. The Keychain-backed private key remains machine-local and is not stored in the repo.
## iOS Versioning Workflow
@@ -246,13 +240,13 @@ See `apps/ios/VERSIONING.md` for the detailed spec.
- `apps/ios/Sources/OpenClaw.entitlements` derives `aps-environment` from the active build configuration/signing override.
- App Attest relay builds use `apps/ios/Sources/OpenClawAppAttest.entitlements`; local/direct builds do not require App Attest provisioning.
- APNs token registration to gateway happens only after gateway connection (`push.apns.register`).
- Local/manual builds default to `OpenClawPushTransport=direct`, `OpenClawPushDistribution=local`, and a development `aps-environment` entitlement.
- Local/manual Debug builds default to `OpenClawPushMode=localSandbox`, direct APNs registration, and a development `aps-environment` entitlement. Local/manual Release builds default to `OpenClawPushMode=localProduction` and direct production APNs registration.
- Your selected team/profile must support Push Notifications for the app bundle ID you are signing.
- If push capability or provisioning is wrong, APNs registration fails at runtime (check Xcode logs for `APNs registration failed`).
- The gateway host also needs direct APNs auth configured separately with `OPENCLAW_APNS_TEAM_ID`, `OPENCLAW_APNS_KEY_ID`, and either `OPENCLAW_APNS_PRIVATE_KEY_P8` or `OPENCLAW_APNS_PRIVATE_KEY_PATH`.
- Recommended gateway-host storage for the APNs `.p8` file is `~/.openclaw/credentials/apns/AuthKey_<KEYID>.p8` with restrictive permissions, then point `OPENCLAW_APNS_PRIVATE_KEY_PATH` at that file.
- `apps/ios/fastlane/.env` only covers App Store Connect / Fastlane auth; it does not provide gateway APNs credentials for local direct-push testing.
- Debug builds default to `OpenClawPushAPNsEnvironment=sandbox`; Release builds default to `production`.
- Debug builds default to sandbox APNs through `OpenClawPushMode=localSandbox`; Release builds default to production APNs through `OpenClawPushMode=localProduction`.
## APNs Expectations For Official Builds
@@ -261,7 +255,7 @@ See `apps/ios/VERSIONING.md` for the detailed spec.
- The relay registration is bound to the gateway identity fetched from `gateway.identity.get`, so another gateway cannot reuse that stored registration.
- The app persists the relay handle metadata locally so reconnects can republish the gateway registration without re-registering on every connect.
- If the relay base URL changes in a later build, the app refreshes the relay registration instead of reusing the old relay origin.
- Production relay mode uses the `production` relay profile, production APNs, App Attest, and a StoreKit app transaction JWS during registration.
- App Store release mode uses the internal `production` relay profile, production APNs, App Attest, and a StoreKit app transaction JWS during registration.
- Gateway-side relay sending is configured through `gateway.push.apns.relay.baseUrl` in `openclaw.json`. `OPENCLAW_APNS_RELAY_BASE_URL` remains a temporary env override only.
## Official Build Relay Trust Model

View File

@@ -152,6 +152,7 @@ extension SettingsProTab {
}
let notificationSettings = await UNUserNotificationCenter.current().notificationSettings()
self.applyNotificationStatus(notificationSettings.authorizationStatus)
self.registerForRemoteNotificationsIfEnrollmentReady()
let issueCount = SettingsDiagnostics.issueCount(
gatewayConnected: self.gatewayDiagnosticConnected,
@@ -417,6 +418,7 @@ extension SettingsProTab {
let status = settings.authorizationStatus
Task { @MainActor in
self.applyNotificationStatus(status)
self.registerForRemoteNotificationsIfEnrollmentReady()
}
}
}
@@ -437,6 +439,7 @@ extension SettingsProTab {
func requestNotificationAuthorizationFromSettings() {
guard !self.isRequestingNotificationAuthorization else { return }
PushEnrollmentConsent.markDisclosureAccepted()
self.isRequestingNotificationAuthorization = true
Task {
let granted = await (try? UNUserNotificationCenter.current().requestAuthorization(options: [
@@ -448,12 +451,19 @@ extension SettingsProTab {
await MainActor.run {
self.isRequestingNotificationAuthorization = false
self.notificationStatus = SettingsNotificationStatus(settings.authorizationStatus)
guard granted, self.notificationStatus.allowsNotifications else { return }
UIApplication.shared.registerForRemoteNotifications()
guard granted else { return }
self.registerForRemoteNotificationsIfEnrollmentReady()
}
}
}
@MainActor
func registerForRemoteNotificationsIfEnrollmentReady() {
guard PushEnrollmentConsent.disclosureAccepted else { return }
guard self.notificationStatus.allowsNotifications else { return }
UIApplication.shared.registerForRemoteNotifications()
}
@MainActor
func applyNotificationStatus(_ status: UNAuthorizationStatus) {
self.notificationStatus = SettingsNotificationStatus(status)

View File

@@ -443,12 +443,54 @@ enum GatewaySettingsStore {
}
enum GatewayDiagnostics {
struct ScopedLogger {
private let prefix: String
fileprivate init(prefix: String) {
self.prefix = prefix
}
func stage(_ message: String) {
GatewayDiagnostics.log("\(self.prefix): \(GatewayDiagnostics.sanitizeScopedMessage(message))")
}
func skipped(_ reason: String) {
self.stage("registration skipped reason=\(reason)")
}
func failed(_ stage: String, error: Error) {
let nsError = error as NSError
self
.stage(
"\(stage) failed errorType=\(String(reflecting: type(of: error))) domain=\(nsError.domain) code=\(nsError.code)")
}
}
private static let logger = Logger(subsystem: "ai.openclawfoundation.app", category: "GatewayDiag")
private static let queue = DispatchQueue(label: "ai.openclawfoundation.app.gateway.diagnostics")
private static let maxLogBytes: Int64 = 512 * 1024
private static let keepLogBytes: Int64 = 256 * 1024
private static let logSizeCheckEveryWrites = 50
private static let logWritesSinceCheck = OSAllocatedUnfairLock(initialState: 0)
private static let maxScopedMessageCharacters = 320
/// Keep relay diagnostics stage-based. Push tokens, relay grants, proofs,
/// receipts, signed payloads, and handles must never enter this cache log.
static let pushRelay = ScopedLogger(prefix: "push relay")
private static func sanitizeScopedMessage(_ value: String) -> String {
let collapsed = value
.replacingOccurrences(of: "\r", with: " ")
.replacingOccurrences(of: "\n", with: " ")
.replacingOccurrences(of: "\t", with: " ")
.trimmingCharacters(in: .whitespacesAndNewlines)
guard collapsed.count > self.maxScopedMessageCharacters else {
return collapsed
}
let end = collapsed.index(collapsed.startIndex, offsetBy: self.maxScopedMessageCharacters)
return String(collapsed[..<end]) + "..."
}
private static func isoTimestamp() -> String {
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]

View File

@@ -82,18 +82,10 @@
<string>$(OPENCLAW_APP_GROUP_ID)</string>
<key>OpenClawCanonicalVersion</key>
<string>$(OPENCLAW_IOS_VERSION)</string>
<key>OpenClawPushAPNsEnvironment</key>
<string>$(OPENCLAW_PUSH_APNS_ENVIRONMENT)</string>
<key>OpenClawPushDistribution</key>
<string>$(OPENCLAW_PUSH_DISTRIBUTION)</string>
<key>OpenClawPushProofPolicy</key>
<string>$(OPENCLAW_PUSH_PROOF_POLICY)</string>
<key>OpenClawPushMode</key>
<string>$(OPENCLAW_PUSH_MODE)</string>
<key>OpenClawPushRelayBaseURL</key>
<string>$(OPENCLAW_PUSH_RELAY_BASE_URL)</string>
<key>OpenClawPushRelayProfile</key>
<string>$(OPENCLAW_PUSH_RELAY_PROFILE)</string>
<key>OpenClawPushTransport</key>
<string>$(OPENCLAW_PUSH_TRANSPORT)</string>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>

View File

@@ -4102,42 +4102,87 @@ extension NodeAppModel {
}
private func registerAPNsTokenIfNeeded() async {
guard self.gatewayConnected else { return }
let usesRelayTransport = await self.pushRegistrationManager.usesRelayTransport
guard await self.canPublishAPNsRegistration(usesRelayTransport: usesRelayTransport) else {
return
}
guard self.gatewayConnected else {
if usesRelayTransport {
GatewayDiagnostics.pushRelay.skipped("gateway_offline")
}
return
}
guard let token = self.apnsDeviceTokenHex?.trimmingCharacters(in: .whitespacesAndNewlines),
!token.isEmpty
else {
if usesRelayTransport {
GatewayDiagnostics.pushRelay.skipped("missing_apns_token")
}
return
}
let usesRelayTransport = await self.pushRegistrationManager.usesRelayTransport
if !usesRelayTransport, token == self.apnsLastRegisteredTokenHex {
return
}
guard let topic = Bundle.main.bundleIdentifier?.trimmingCharacters(in: .whitespacesAndNewlines),
!topic.isEmpty
else {
if usesRelayTransport {
GatewayDiagnostics.pushRelay.skipped("missing_topic")
}
return
}
do {
let gatewayIdentity: PushRelayGatewayIdentity?
if usesRelayTransport {
guard self.operatorConnected else { return }
guard self.operatorConnected else {
GatewayDiagnostics.pushRelay.skipped("operator_offline")
return
}
GatewayDiagnostics.pushRelay.stage("gateway identity request start")
gatewayIdentity = try await self.fetchPushRelayGatewayIdentity()
GatewayDiagnostics.pushRelay.stage("gateway identity request complete")
} else {
gatewayIdentity = nil
}
if usesRelayTransport {
GatewayDiagnostics.pushRelay.stage("gateway registration payload start")
}
let payloadJSON = try await self.pushRegistrationManager.makeGatewayRegistrationPayload(
apnsTokenHex: token,
topic: topic,
gatewayIdentity: gatewayIdentity)
await self.nodeGateway.sendEvent(event: "push.apns.register", payloadJSON: payloadJSON)
self.apnsLastRegisteredTokenHex = token
if usesRelayTransport {
GatewayDiagnostics.pushRelay.stage("gateway registration event published")
}
} catch {
self.pushWakeLogger.error(
"APNs registration publish failed: \(error.localizedDescription, privacy: .public)")
if usesRelayTransport {
GatewayDiagnostics.pushRelay.failed("registration", error: error)
}
}
}
private func canPublishAPNsRegistration(usesRelayTransport: Bool) async -> Bool {
guard PushEnrollmentConsent.disclosureAccepted else {
if usesRelayTransport {
GatewayDiagnostics.pushRelay.skipped("enrollment_disclosure_not_accepted")
}
return false
}
let status = await self.notificationAuthorizationStatus()
guard Self.isNotificationAuthorizationAllowed(status) else {
if usesRelayTransport {
GatewayDiagnostics.pushRelay.skipped("notifications_not_authorized")
}
return false
}
return true
}
private func fetchPushRelayGatewayIdentity() async throws -> PushRelayGatewayIdentity {
let response = try await self.operatorGateway.request(
method: "gateway.identity.get",
@@ -5101,6 +5146,10 @@ extension NodeAppModel {
self.setOperatorConnected(connected)
}
func _test_canPublishAPNsRegistration(usesRelayTransport: Bool = true) async -> Bool {
await self.canPublishAPNsRegistration(usesRelayTransport: usesRelayTransport)
}
nonisolated static func _test_makeWatchChatItems(from raw: [OpenClawKit.AnyCodable]) -> [OpenClawWatchChatItem] {
self.makeWatchChatItems(from: raw)
}

View File

@@ -123,10 +123,30 @@ final class OpenClawAppDelegate: NSObject, UIApplicationDelegate, @preconcurrenc
let notificationCenter = UNUserNotificationCenter.current()
notificationCenter.delegate = self
ExecApprovalNotificationBridge.registerCategory(center: notificationCenter)
application.registerForRemoteNotifications()
Task { @MainActor in
await self.registerForRemoteNotificationsIfEnrollmentReady(application)
}
return true
}
private func registerForRemoteNotificationsIfEnrollmentReady(_ application: UIApplication) async {
guard PushEnrollmentConsent.disclosureAccepted else { return }
guard await Self.isNotificationAuthorizationAllowed() else { return }
application.registerForRemoteNotifications()
}
private static func isNotificationAuthorizationAllowed() async -> Bool {
let settings = await UNUserNotificationCenter.current().notificationSettings()
switch settings.authorizationStatus {
case .authorized, .provisional, .ephemeral:
return true
case .denied, .notDetermined:
return false
@unknown default:
return false
}
}
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
if let appModel = self.resolvedAppModel() {
Task { @MainActor in

View File

@@ -27,7 +27,16 @@ enum PushProofPolicy: String {
case internalSimulator
}
enum PushBuildMode: String {
case localSandbox
case localProduction
case appStore
case deviceSandbox
case simulatorSandbox
}
struct PushBuildConfig {
let mode: PushBuildMode
let transport: PushTransportMode
let distribution: PushDistributionMode
let relayBaseURL: URL?
@@ -54,31 +63,64 @@ struct PushBuildConfig {
}
init(bundle: Bundle = .main) {
self.transport = Self.readEnum(
bundle: bundle,
key: "OpenClawPushTransport",
fallback: .direct)
self.distribution = Self.readEnum(
bundle: bundle,
key: "OpenClawPushDistribution",
fallback: .local)
self.apnsEnvironment = Self.readEnum(
bundle: bundle,
key: "OpenClawPushAPNsEnvironment",
fallback: Self.defaultAPNsEnvironment)
self.relayProfile = Self.readEnum(
bundle: bundle,
key: "OpenClawPushRelayProfile",
fallback: Self.defaultRelayProfile(apnsEnvironment: self.apnsEnvironment))
self.proofPolicy = Self.readEnum(
bundle: bundle,
key: "OpenClawPushProofPolicy",
fallback: Self.defaultProofPolicy(relayProfile: self.relayProfile))
self.relayBaseURL = Self.readURL(bundle: bundle, key: "OpenClawPushRelayBaseURL")
self.init(readValue: { bundle.object(forInfoDictionaryKey: $0) })
}
private static func readURL(bundle: Bundle, key: String) -> URL? {
guard let raw = bundle.object(forInfoDictionaryKey: key) as? String else { return nil }
init(infoDictionary: [String: Any]) {
self.init(readValue: { infoDictionary[$0] })
}
private init(readValue: (String) -> Any?) {
self.mode = Self.readEnum(
readValue: readValue,
key: "OpenClawPushMode",
fallback: .localSandbox)
let relayBaseURLOverride = Self.readURL(
readValue: readValue,
key: "OpenClawPushRelayBaseURL")
switch self.mode {
case .localSandbox:
self.transport = .direct
self.distribution = .local
self.relayBaseURL = nil
self.apnsEnvironment = .sandbox
self.relayProfile = .deviceSandbox
self.proofPolicy = .appleDevelopment
case .localProduction:
self.transport = .direct
self.distribution = .local
self.relayBaseURL = nil
self.apnsEnvironment = .production
self.relayProfile = .production
self.proofPolicy = .appleStrict
case .appStore:
self.transport = .relay
self.distribution = .official
self.relayBaseURL = URL(string: "https://\(Self.openClawHostedRelayHost)")!
self.apnsEnvironment = .production
self.relayProfile = .production
self.proofPolicy = .appleStrict
case .deviceSandbox:
self.transport = .relay
self.distribution = .official
self.relayBaseURL = relayBaseURLOverride
?? URL(string: "https://\(Self.openClawSandboxRelayHost)")!
self.apnsEnvironment = .sandbox
self.relayProfile = .deviceSandbox
self.proofPolicy = .appleDevelopment
case .simulatorSandbox:
self.transport = .relay
self.distribution = .official
self.relayBaseURL = relayBaseURLOverride
?? URL(string: "https://\(Self.openClawSandboxRelayHost)")!
self.apnsEnvironment = .sandbox
self.relayProfile = .simulatorSandbox
self.proofPolicy = .internalSimulator
}
}
private static func readURL(readValue: (String) -> Any?, key: String) -> URL? {
guard let raw = readValue(key) as? String else { return nil }
let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return nil }
guard let components = URLComponents(string: trimmed),
@@ -96,29 +138,12 @@ struct PushBuildConfig {
}
private static func readEnum<T: RawRepresentable>(
bundle: Bundle,
readValue: (String) -> Any?,
key: String,
fallback: T)
-> T where T.RawValue == String {
guard let raw = bundle.object(forInfoDictionaryKey: key) as? String else { return fallback }
guard let raw = readValue(key) as? String else { return fallback }
let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
return T(rawValue: trimmed) ?? T(rawValue: trimmed.lowercased()) ?? fallback
}
private static let defaultAPNsEnvironment: PushAPNsEnvironment = .sandbox
private static func defaultRelayProfile(apnsEnvironment: PushAPNsEnvironment) -> PushRelayProfile {
apnsEnvironment == .production ? .production : .deviceSandbox
}
private static func defaultProofPolicy(relayProfile: PushRelayProfile) -> PushProofPolicy {
switch relayProfile {
case .production:
.appleStrict
case .deviceSandbox:
.appleDevelopment
case .simulatorSandbox:
.internalSimulator
}
}
}

View File

@@ -0,0 +1,19 @@
import Foundation
enum PushEnrollmentConsent {
static let disclosureAcceptedKey = "push.enrollment.disclosureAccepted"
static var disclosureAccepted: Bool {
UserDefaults.standard.bool(forKey: disclosureAcceptedKey)
}
static func markDisclosureAccepted() {
UserDefaults.standard.set(true, forKey: self.disclosureAcceptedKey)
}
#if DEBUG
static func reset() {
UserDefaults.standard.removeObject(forKey: self.disclosureAcceptedKey)
}
#endif
}

View File

@@ -69,12 +69,16 @@ actor PushRegistrationManager {
async throws -> String {
guard self.buildConfig.distribution == .official else {
throw PushRelayError.relayMisconfigured(
"Relay transport requires OpenClawPushDistribution=official")
"Relay transport requires an official push build mode")
}
try Self.validateRelayContract(
relayProfile: self.buildConfig.relayProfile,
apnsEnvironment: self.buildConfig.apnsEnvironment,
proofPolicy: self.buildConfig.proofPolicy)
GatewayDiagnostics.pushRelay.stage(
"contract validated apns=\(self.buildConfig.apnsEnvironment.rawValue) "
+ "profile=\(self.buildConfig.relayProfile.rawValue) "
+ "proof=\(self.buildConfig.proofPolicy.rawValue)")
guard let relayClient = self.relayClient else {
throw PushRelayError.relayBaseURLMissing
}
@@ -102,6 +106,7 @@ actor PushRegistrationManager {
stored.lastAPNsTokenHashHex == tokenHashHex,
!Self.isExpired(stored.relayHandleExpiresAtMs)
{
GatewayDiagnostics.pushRelay.stage("using cached relay registration")
return try Self.encodePayload(
RelayGatewayPushRegistrationPayload(
relayHandle: stored.relayHandle,
@@ -115,6 +120,7 @@ actor PushRegistrationManager {
tokenDebugSuffix: stored.tokenDebugSuffix))
}
GatewayDiagnostics.pushRelay.stage("relay registration cache miss")
let response = try await relayClient.register(PushRelayRegistrationInput(
installationId: installationId,
bundleId: bundleId,
@@ -139,6 +145,7 @@ actor PushRegistrationManager {
relayProfile: self.buildConfig.relayProfile.rawValue,
proofPolicy: self.buildConfig.proofPolicy.rawValue)
_ = PushRelayRegistrationStore.saveRegistrationState(registrationState)
GatewayDiagnostics.pushRelay.stage("stored relay registration hasExpiry=\(response.expiresAtMs != nil)")
return try Self.encodePayload(
RelayGatewayPushRegistrationPayload(
relayHandle: response.relayHandle,

View File

@@ -276,7 +276,20 @@ final class PushRelayClient: @unchecked Sendable {
}
func register(_ input: PushRelayRegistrationInput) async throws -> PushRelayRegisterResponse {
let challenge = try await self.fetchChallenge()
GatewayDiagnostics.pushRelay.stage(
"registration start origin=\(self.normalizedBaseURLString) "
+ "apns=\(input.environment.rawValue) "
+ "profile=\(input.relayProfile.rawValue) "
+ "proof=\(input.proofPolicy.rawValue)")
let challenge: PushRelayChallengeResponse
do {
GatewayDiagnostics.pushRelay.stage("challenge request start")
challenge = try await self.fetchChallenge()
GatewayDiagnostics.pushRelay.stage("challenge received")
} catch {
GatewayDiagnostics.pushRelay.failed("challenge request", error: error)
throw error
}
let signedPayload = PushRelayRegisterSignedPayload(
challengeId: challenge.challengeId,
installationId: input.installationId,
@@ -295,15 +308,38 @@ final class PushRelayClient: @unchecked Sendable {
apnsEnvironment: input.environment.rawValue,
relayProfile: input.relayProfile.rawValue,
proofPolicy: input.proofPolicy.rawValue)
let appAttest = try await self.createAppAttestProofIfNeeded(
proofPolicy: input.proofPolicy,
challenge: challenge.challenge,
signedPayloadData: signedPayloadData,
scope: appAttestScope)
let receipt = try await self.createReceiptIfNeeded(proofPolicy: input.proofPolicy)
let simulatorProof = try self.createSimulatorProofIfNeeded(
proofPolicy: input.proofPolicy,
signedPayloadData: signedPayloadData)
let appAttest: PushRelayAppAttestProof?
do {
GatewayDiagnostics.pushRelay.stage("app attest proof start")
appAttest = try await self.createAppAttestProofIfNeeded(
proofPolicy: input.proofPolicy,
challenge: challenge.challenge,
signedPayloadData: signedPayloadData,
scope: appAttestScope)
GatewayDiagnostics.pushRelay.stage("app attest proof complete included=\(appAttest != nil)")
} catch {
GatewayDiagnostics.pushRelay.failed("app attest proof", error: error)
throw error
}
let receipt: PushRelayReceiptPayload?
do {
GatewayDiagnostics.pushRelay.stage("receipt proof start")
receipt = try await self.createReceiptIfNeeded(proofPolicy: input.proofPolicy)
GatewayDiagnostics.pushRelay.stage("receipt proof complete included=\(receipt != nil)")
} catch {
GatewayDiagnostics.pushRelay.failed("receipt proof", error: error)
throw error
}
let simulatorProof: PushRelaySimulatorProofPayload?
do {
simulatorProof = try self.createSimulatorProofIfNeeded(
proofPolicy: input.proofPolicy,
signedPayloadData: signedPayloadData)
GatewayDiagnostics.pushRelay.stage("simulator proof complete included=\(simulatorProof != nil)")
} catch {
GatewayDiagnostics.pushRelay.failed("simulator proof", error: error)
throw error
}
let requestBody = PushRelayRegisterRequest(
challengeId: signedPayload.challengeId,
installationId: signedPayload.installationId,
@@ -334,8 +370,17 @@ final class PushRelayClient: @unchecked Sendable {
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = try self.jsonEncoder.encode(requestBody)
let (data, response) = try await self.session.data(for: request)
let data: Data
let response: URLResponse
do {
GatewayDiagnostics.pushRelay.stage("register request start")
(data, response) = try await self.session.data(for: request)
} catch {
GatewayDiagnostics.pushRelay.failed("register request", error: error)
throw error
}
let status = Self.statusCode(from: response)
GatewayDiagnostics.pushRelay.stage("register response status=\(status)")
guard (200..<300).contains(status) else {
if status == 401 {
// If the relay rejects registration, drop local App Attest state so the next
@@ -343,11 +388,20 @@ final class PushRelayClient: @unchecked Sendable {
_ = PushRelayRegistrationStore.clearAppAttestKeyID(scope: appAttestScope)
_ = PushRelayRegistrationStore.clearAttestedKeyID(scope: appAttestScope)
}
throw PushRelayError.requestFailed(
let relayError = PushRelayError.requestFailed(
status: status,
message: Self.decodeErrorMessage(data: data))
GatewayDiagnostics.pushRelay.stage("register response failed status=\(status)")
throw relayError
}
do {
let decoded = try self.decode(PushRelayRegisterResponse.self, from: data)
GatewayDiagnostics.pushRelay.stage("registration response decoded")
return decoded
} catch {
GatewayDiagnostics.pushRelay.failed("registration response decode", error: error)
throw error
}
return try self.decode(PushRelayRegisterResponse.self, from: data)
}
private func createAppAttestProofIfNeeded(

View File

@@ -76,6 +76,7 @@ Sources/Permissions/PermissionRequestBridge.swift
Sources/Push/ExecApprovalNotificationBridge.swift
Sources/Push/BackgroundAliveBeacon.swift
Sources/Push/PushBuildConfig.swift
Sources/Push/PushEnrollmentConsent.swift
Sources/Push/PushRegistrationManager.swift
Sources/Push/PushRelayClient.swift
Sources/Push/PushRelayKeychainStore.swift

View File

@@ -1377,6 +1377,24 @@ private final class MockBootstrapNotificationCenter: NotificationCentering, @unc
#expect(center.addCalls == 1)
}
@Test @MainActor func apnsRegistrationRequiresDisclosureAndNotificationAuthorization() async {
let center = MockBootstrapNotificationCenter()
center.status = .authorized
let appModel = NodeAppModel(notificationCenter: center)
PushEnrollmentConsent.reset()
defer { PushEnrollmentConsent.reset() }
#expect(await appModel._test_canPublishAPNsRegistration() == false)
#expect(await appModel._test_canPublishAPNsRegistration(usesRelayTransport: false) == false)
PushEnrollmentConsent.markDisclosureAccepted()
center.status = .notDetermined
#expect(await appModel._test_canPublishAPNsRegistration() == false)
center.status = .authorized
#expect(await appModel._test_canPublishAPNsRegistration())
}
@Test @MainActor func chatPushWithoutSpeechReturnsUnavailableWhenNotificationsOff() async throws {
let center = MockBootstrapNotificationCenter()
center.status = .notDetermined

View File

@@ -0,0 +1,50 @@
import Foundation
import Testing
@testable import OpenClaw
struct PushBuildConfigTests {
@Test func `app store mode derives production relay contract`() {
let config = PushBuildConfig(infoDictionary: [
"OpenClawPushMode": "appStore",
"OpenClawPushRelayBaseURL": "https://wrong.example.com",
])
#expect(config.mode == .appStore)
#expect(config.transport == .relay)
#expect(config.distribution == .official)
#expect(config.relayBaseURL?.absoluteString == "https://ios-push-relay.openclaw.ai")
#expect(config.apnsEnvironment == .production)
#expect(config.relayProfile == .production)
#expect(config.proofPolicy == .appleStrict)
}
@Test func `simulator sandbox mode derives internal proof contract`() {
let config = PushBuildConfig(infoDictionary: [
"OpenClawPushMode": "simulatorSandbox",
"OpenClawPushRelayBaseURL": "https://staging-relay.example.com",
])
#expect(config.mode == .simulatorSandbox)
#expect(config.transport == .relay)
#expect(config.distribution == .official)
#expect(config.relayBaseURL?.absoluteString == "https://staging-relay.example.com")
#expect(config.apnsEnvironment == .sandbox)
#expect(config.relayProfile == .simulatorSandbox)
#expect(config.proofPolicy == .internalSimulator)
}
@Test func `local release mode remains direct production push`() {
let config = PushBuildConfig(infoDictionary: [
"OpenClawPushMode": "localProduction",
"OpenClawPushRelayBaseURL": "https://ios-push-relay.openclaw.ai",
])
#expect(config.mode == .localProduction)
#expect(config.transport == .direct)
#expect(config.distribution == .local)
#expect(config.relayBaseURL == nil)
#expect(config.apnsEnvironment == .production)
#expect(config.relayProfile == .production)
#expect(config.proofPolicy == .appleStrict)
}
}

View File

@@ -550,6 +550,20 @@ struct RootTabsSourceGuardTests {
#expect(docsSource.contains(".accessibilityHint(\"Opens Settings / Gateway\")"))
}
@Test func `push enrollment stays behind notification disclosure flow`() throws {
let appSource = try String(contentsOf: Self.openClawAppSourceURL(), encoding: .utf8)
let actionsSource = try String(contentsOf: Self.settingsProTabActionsSourceURL(), encoding: .utf8)
let modelSource = try String(contentsOf: Self.nodeAppModelSourceURL(), encoding: .utf8)
#expect(appSource.contains("PushEnrollmentConsent.disclosureAccepted"))
#expect(appSource.contains("await Self.isNotificationAuthorizationAllowed()"))
#expect(actionsSource.contains("PushEnrollmentConsent.markDisclosureAccepted()"))
#expect(actionsSource.contains("self.registerForRemoteNotificationsIfEnrollmentReady()"))
#expect(modelSource.contains("PushEnrollmentConsent.disclosureAccepted"))
#expect(modelSource.contains("notifications_not_authorized"))
#expect(modelSource.contains("enrollment_disclosure_not_accepted"))
}
@Test func `gateway settings keeps pairing trust diagnostics and tailscale actions`() throws {
let settingsSource = try String(contentsOf: Self.settingsProTabSourceURL(), encoding: .utf8)
let sectionsSource = try String(contentsOf: Self.settingsProTabSectionsSourceURL(), encoding: .utf8)
@@ -786,6 +800,13 @@ struct RootTabsSourceGuardTests {
.appendingPathComponent("Sources/Design/SettingsProTab.swift")
}
private static func openClawAppSourceURL() -> URL {
URL(fileURLWithPath: #filePath)
.deletingLastPathComponent()
.deletingLastPathComponent()
.appendingPathComponent("Sources/OpenClawApp.swift")
}
private static func notificationPermissionGuidanceDialogSourceURL() -> URL {
URL(fileURLWithPath: #filePath)
.deletingLastPathComponent()

View File

@@ -15,13 +15,12 @@
import Foundation
import XCTest
var deviceLanguage = ""
var locale = ""
@MainActor
func setupSnapshot(_ app: XCUIApplication, waitForAnimations: Bool = true) {
Snapshot.setupSnapshot(app, waitForAnimations: waitForAnimations)
}
@MainActor
func snapshot(_ name: String, waitForLoadingIndicator: Bool) {
if waitForLoadingIndicator {
Snapshot.snapshot(name)
@@ -33,6 +32,7 @@ func snapshot(_ name: String, waitForLoadingIndicator: Bool) {
/// - Parameters:
/// - name: The name of the snapshot
/// - timeout: Amount of seconds to wait until the network loading indicator disappears. Pass `0` if you don't want to wait.
@MainActor
func snapshot(_ name: String, timeWaitingForIdle timeout: TimeInterval = 20) {
Snapshot.snapshot(name, timeWaitingForIdle: timeout)
}
@@ -52,6 +52,7 @@ enum SnapshotError: Error, CustomDebugStringConvertible {
}
@objcMembers
@MainActor
open class Snapshot: NSObject {
static var app: XCUIApplication?
static var waitForAnimations = true
@@ -59,6 +60,8 @@ open class Snapshot: NSObject {
static var screenshotsDirectory: URL? {
return cacheDirectory?.appendingPathComponent("screenshots", isDirectory: true)
}
static var deviceLanguage = ""
static var currentLocale = ""
open class func setupSnapshot(_ app: XCUIApplication, waitForAnimations: Bool = true) {
@@ -103,17 +106,17 @@ open class Snapshot: NSObject {
do {
let trimCharacterSet = CharacterSet.whitespacesAndNewlines
locale = try String(contentsOf: path, encoding: .utf8).trimmingCharacters(in: trimCharacterSet)
currentLocale = try String(contentsOf: path, encoding: .utf8).trimmingCharacters(in: trimCharacterSet)
} catch {
NSLog("Couldn't detect/set locale...")
}
if locale.isEmpty && !deviceLanguage.isEmpty {
locale = Locale(identifier: deviceLanguage).identifier
if currentLocale.isEmpty && !deviceLanguage.isEmpty {
currentLocale = Locale(identifier: deviceLanguage).identifier
}
if !locale.isEmpty {
app.launchArguments += ["-AppleLocale", "\"\(locale)\""]
if !currentLocale.isEmpty {
app.launchArguments += ["-AppleLocale", "\"\(currentLocale)\""]
}
}
@@ -165,7 +168,7 @@ open class Snapshot: NSObject {
}
let screenshot = XCUIScreen.main.screenshot()
#if os(iOS)
#if os(iOS) && !targetEnvironment(macCatalyst)
let image = XCUIDevice.shared.orientation.isLandscape ? fixLandscapeOrientation(image: screenshot.image) : screenshot.image
#else
let image = screenshot.image
@@ -181,7 +184,7 @@ open class Snapshot: NSObject {
let path = screenshotsDir.appendingPathComponent("\(simulator)-\(name).png")
#if swift(<5.0)
UIImagePNGRepresentation(image)?.write(to: path, options: .atomic)
try UIImagePNGRepresentation(image)?.write(to: path, options: .atomic)
#else
try image.pngData()?.write(to: path, options: .atomic)
#endif
@@ -281,6 +284,7 @@ private extension XCUIElementQuery {
return self.containing(isNetworkLoadingIndicator)
}
@MainActor
var deviceStatusBars: XCUIElementQuery {
guard let app = Snapshot.app else {
fatalError("XCUIApplication is not set. Please call setupSnapshot(app) before snapshot().")
@@ -306,4 +310,4 @@ private extension CGFloat {
// Please don't remove the lines below
// They are used to detect outdated configuration files
// SnapshotHelperVersion [1.27]
// SnapshotHelperVersion [1.30]

View File

@@ -10,7 +10,24 @@ default_platform(:ios)
APP_STORE_APP_IDENTIFIER = "ai.openclawfoundation.app"
DEFAULT_APP_STORE_CONNECT_KEYCHAIN_SERVICE = "openclaw-app-store-connect-key"
DEFAULT_SNAPSHOT_DEVICES = ["iPhone 16 Pro Max", "iPad Pro 13-inch (M4)"].freeze
DEFAULT_SNAPSHOT_DEVICE_FAMILIES = [
{
label: "iPhone",
patterns: [
/\AiPhone .* Pro Max\z/,
/\AiPhone .* Plus\z/,
/\AiPhone .*\z/
]
},
{
label: "13-inch iPad",
patterns: [
/\AiPad Pro 13-inch/,
/\AiPad Air 13-inch/,
/\AiPad .*13-inch/
]
}
].freeze
DEFAULT_WATCH_SNAPSHOT_DEVICE = "Apple Watch Ultra 3 (49mm)"
WATCH_SCREENSHOT_MODE_DEFAULTS_KEY = "openclaw.watch.screenshotMode"
WATCH_SNAPSHOT_STATUS_BAR_TIME = "09:41"
@@ -77,11 +94,23 @@ end
def snapshot_devices
raw = ENV["OPENCLAW_SNAPSHOT_DEVICES"].to_s.strip
return DEFAULT_SNAPSHOT_DEVICES if raw.empty?
return default_snapshot_devices if raw.empty?
raw.split(",").map(&:strip).reject(&:empty?)
end
def default_snapshot_devices
names = available_simulator_devices.map { |device| device["name"].to_s }.reject(&:empty?).uniq
DEFAULT_SNAPSHOT_DEVICE_FAMILIES.map do |family|
match = family.fetch(:patterns).filter_map do |pattern|
names.find { |name| name.match?(pattern) }
end.first
UI.user_error!("No available #{family.fetch(:label)} simulator found for App Store screenshots.") if match.nil?
match
end
end
def watch_snapshot_device
raw = ENV["OPENCLAW_WATCH_SNAPSHOT_DEVICE"].to_s.strip
raw.empty? ? DEFAULT_WATCH_SNAPSHOT_DEVICE : raw
@@ -113,6 +142,51 @@ def resolve_simulator_device(name)
fallback
end
def install_ready_for_review_edit_state_lookup!
require "spaceship"
app_class = Spaceship::ConnectAPI::App
app_class.class_eval do
unless method_defined?(:openclaw_get_edit_app_store_version_without_ready_for_review)
alias_method :openclaw_get_edit_app_store_version_without_ready_for_review, :get_edit_app_store_version
end
unless method_defined?(:openclaw_fetch_edit_app_info_without_ready_for_review)
alias_method :openclaw_fetch_edit_app_info_without_ready_for_review, :fetch_edit_app_info
end
def get_edit_app_store_version(client: nil, platform: nil, includes: Spaceship::ConnectAPI::AppStoreVersion::ESSENTIAL_INCLUDES)
version = openclaw_get_edit_app_store_version_without_ready_for_review(client: client, platform: platform, includes: includes)
return version if version
# First public releases can leave the only version in READY_FOR_REVIEW.
# Fastlane 2.236.1 excludes that state and then tries to create an illegal
# second version; use the existing review-ready version as the edit target.
client ||= Spaceship::ConnectAPI
platform ||= Spaceship::ConnectAPI::Platform::IOS
filter = {
appVersionState: Spaceship::ConnectAPI::AppStoreVersion::AppVersionState::READY_FOR_REVIEW,
platform: platform
}
get_app_store_versions(client: client, filter: filter, includes: includes)
.sort_by { |candidate| Gem::Version.new(candidate.version_string) }
.last
end
def fetch_edit_app_info(client: nil, includes: Spaceship::ConnectAPI::AppInfo::ESSENTIAL_INCLUDES)
app_info = openclaw_fetch_edit_app_info_without_ready_for_review(client: client, includes: includes)
return app_info if app_info
client ||= Spaceship::ConnectAPI
client
.get_app_infos(app_id: id, includes: includes)
.to_models
.find { |candidate| candidate.state == Spaceship::ConnectAPI::AppInfo::State::READY_FOR_REVIEW }
end
end
end
def bundle_identifier_for_product(product_path)
info_plist_path = File.join(product_path, "Info.plist")
UI.user_error!("Expected Info.plist at #{info_plist_path}.") unless File.exist?(info_plist_path)
@@ -210,6 +284,7 @@ def normalize_watch_screenshot_status_bar(path)
script = <<~SWIFT
import AppKit
import Foundation
import ImageIO
let path = CommandLine.arguments[1]
let timeText = CommandLine.arguments[2]
@@ -221,36 +296,37 @@ def normalize_watch_screenshot_status_bar(path)
exit(2)
}
let width = CGFloat(cgImage.width)
let height = CGFloat(cgImage.height)
guard let bitmap = NSBitmapImageRep(
bitmapDataPlanes: nil,
pixelsWide: Int(width),
pixelsHigh: Int(height),
bitsPerSample: 8,
samplesPerPixel: 4,
hasAlpha: true,
isPlanar: false,
colorSpaceName: .deviceRGB,
bytesPerRow: 0,
bitsPerPixel: 0),
let graphicsContext = NSGraphicsContext(bitmapImageRep: bitmap)
let width = cgImage.width
let height = cgImage.height
let drawWidth = CGFloat(width)
let drawHeight = CGFloat(height)
let colorSpace = CGColorSpace(name: CGColorSpace.sRGB) ?? CGColorSpaceCreateDeviceRGB()
guard let bitmapContext = CGContext(
data: nil,
width: width,
height: height,
bitsPerComponent: 8,
bytesPerRow: width * 4,
space: colorSpace,
bitmapInfo: CGImageAlphaInfo.noneSkipLast.rawValue)
else {
fputs("Failed to create normalized screenshot bitmap at \\(path)\\n", stderr)
exit(3)
}
bitmap.size = NSSize(width: width, height: height)
let graphicsContext = NSGraphicsContext(cgContext: bitmapContext, flipped: false)
NSGraphicsContext.saveGraphicsState()
NSGraphicsContext.current = graphicsContext
NSColor.black.setFill()
NSBezierPath(rect: NSRect(x: 0, y: 0, width: drawWidth, height: drawHeight)).fill()
source.draw(
in: NSRect(x: 0, y: 0, width: width, height: height),
from: NSRect(x: 0, y: 0, width: width, height: height),
operation: .copy,
in: NSRect(x: 0, y: 0, width: drawWidth, height: drawHeight),
from: NSRect(x: 0, y: 0, width: drawWidth, height: drawHeight),
operation: .sourceOver,
fraction: 1.0)
NSColor.black.setFill()
NSBezierPath(rect: NSRect(x: width - 146, y: height - 92, width: 124, height: 70)).fill()
NSBezierPath(rect: NSRect(x: drawWidth - 146, y: drawHeight - 92, width: 124, height: 70)).fill()
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = .right
@@ -260,17 +336,26 @@ def normalize_watch_screenshot_status_bar(path)
.paragraphStyle: paragraphStyle,
]
timeText.draw(
in: NSRect(x: width - 134, y: height - 82, width: 102, height: 44),
in: NSRect(x: drawWidth - 134, y: drawHeight - 82, width: 102, height: 44),
withAttributes: attributes)
NSGraphicsContext.restoreGraphicsState()
guard let png = bitmap.representation(using: .png, properties: [:])
guard let output = bitmapContext.makeImage(),
let destination = CGImageDestinationCreateWithURL(
URL(fileURLWithPath: path) as CFURL,
"public.png" as CFString,
1,
nil)
else {
fputs("Failed to encode normalized screenshot at \\(path)\\n", stderr)
exit(4)
}
try png.write(to: URL(fileURLWithPath: path))
CGImageDestinationAddImage(destination, output, nil)
guard CGImageDestinationFinalize(destination) else {
fputs("Failed to write normalized screenshot at \\(path)\\n", stderr)
exit(5)
}
SWIFT
Tempfile.create(["openclaw-watch-status-bar", ".swift"]) do |file|
@@ -754,6 +839,11 @@ def prepare_app_store_release!(version:, build_number:)
release_xcconfig
end
def validate_app_store_ipa!(ipa_path)
script_path = File.join(repo_root, "scripts", "ios-validate-app-store-ipa.sh")
sh(shell_join(["bash", script_path, "--ipa", ipa_path]))
end
def build_app_store_release(context)
version = context[:version]
project_path = File.join(ios_root, "OpenClaw.xcodeproj")
@@ -804,6 +894,7 @@ def build_app_store_release(context)
UI.user_error!("xcodebuild export produced multiple IPAs in #{output_directory}: #{exported_ipas.join(", ")}") if exported_ipas.length > 1
exported_ipa = exported_ipas.first
FileUtils.mv(exported_ipa, expected_ipa_path) unless exported_ipa == expected_ipa_path
validate_app_store_ipa!(expected_ipa_path)
{
archive_path: archive_path,
@@ -923,25 +1014,12 @@ platform :ios do
ENV.delete("XCODE_XCCONFIG_FILE")
end
desc "Build + upload an App Store distribution build to App Store Connect"
lane :app_store do
context = prepare_app_store_context(require_api_key: true)
build = build_app_store_release(context)
upload_to_testflight(
api_key: context[:api_key],
ipa: build[:ipa_path],
skip_waiting_for_build_processing: true,
uses_non_exempt_encryption: false
)
UI.success("Uploaded iOS App Store build: version=#{build[:version]} short=#{build[:short_version]} build=#{build[:build_number]}")
ensure
ENV.delete("XCODE_XCCONFIG_FILE")
end
desc "Generate screenshots, update App Store version metadata, then upload an App Store build"
lane :release_upload do
unless ENV["OPENCLAW_IOS_RELEASE_WRAPPER"] == "1"
UI.user_error!("Use `pnpm ios:release:upload`; direct Fastlane TestFlight upload is disabled.")
end
release_signing_check!
preserve_local_signing do
screenshots
@@ -968,6 +1046,7 @@ platform :ios do
desc "Upload App Store metadata (and optionally screenshots)"
lane :metadata do
install_ready_for_review_edit_state_lookup!
sync_ios_versioning!
version_metadata = read_ios_version_metadata
api_key = app_store_connect_api_key_config

View File

@@ -104,7 +104,7 @@ Generate deterministic App Store screenshots:
pnpm ios:screenshots
```
The screenshot lane runs the app with `--openclaw-screenshot-mode`, which enters the built-in connected screenshot fixture instead of pairing with a live gateway. By default it captures the tab set on `iPhone 16 Pro Max` and `iPad Pro 13-inch (M4)`; override devices with a comma-separated `OPENCLAW_SNAPSHOT_DEVICES` value when the requested simulators exist locally.
The screenshot lane runs the app with `--openclaw-screenshot-mode`, which enters the built-in connected screenshot fixture instead of pairing with a live gateway. By default it chooses one available large iPhone simulator and one available 13-inch iPad simulator from the installed Xcode runtime; override devices with a comma-separated `OPENCLAW_SNAPSHOT_DEVICES` value when the requested simulators exist locally.
Upload to App Store Connect:
@@ -112,12 +112,9 @@ Upload to App Store Connect:
pnpm ios:release:upload
```
Direct Fastlane entry point:
```bash
cd apps/ios
fastlane ios release_upload
```
Direct Fastlane TestFlight upload is disabled. Use the package script so the
release wrapper, App Store push mode, and exported-IPA validation gate all run
in the same path.
Maintainer recovery path for a fresh clone on the same Mac:
@@ -144,13 +141,7 @@ fastlane ios auth_check
pnpm ios:version:pin -- --from-gateway
```
5. Set the official relay URL before release:
```bash
export OPENCLAW_PUSH_RELAY_BASE_URL=https://relay.example.com
```
6. Upload:
5. Upload:
```bash
pnpm ios:release:upload
@@ -159,6 +150,7 @@ pnpm ios:release:upload
Quick verification after upload:
- confirm `apps/ios/build/app-store/OpenClaw-<version>.ipa` exists
- confirm Fastlane validates the exported IPA before upload
- confirm Fastlane prints `Uploaded iOS App Store build: version=<version> short=<short> build=<build>`
- remember that App Store Connect/TestFlight processing can take a few minutes after the upload succeeds
@@ -175,5 +167,7 @@ Versioning rules:
- `pnpm ios:version:check` validates that checked-in iOS version artifacts are in sync
- The release flow regenerates `apps/ios/OpenClaw.xcodeproj` from `apps/ios/project.yml` before archiving
- Local App Store signing uses a temporary generated xcconfig with profile names from `apps/ios/Config/AppStoreSigning.json` and leaves local development signing overrides untouched
- App Store release uses `OpenClawPushMode=appStore`, which derives the canonical production hosted relay, production APNs, production relay profile, and `appleStrict` proof. The release lane rejects custom production relay URL overrides.
- The exported IPA is validated before upload by inspecting its push mode, signed entitlements, and embedded App Store profile.
- `pnpm ios:release:upload` generates and uploads screenshots and release notes before archiving, then uploads the IPA without submitting it for App Review
- See `apps/ios/VERSIONING.md` for the detailed workflow

View File

@@ -2,10 +2,9 @@ project("OpenClaw.xcodeproj")
scheme("OpenClawUITests")
configuration("Debug")
devices([
"iPhone 16 Pro Max",
"iPad Pro 13-inch (M4)",
])
# The Fastfile screenshot lane resolves concrete device names from the installed
# Xcode simulators. Fastlane validates Snapfile devices before lane overrides, so
# this file intentionally does not hardcode simulator model names.
languages([
"en-US",

View File

@@ -122,21 +122,13 @@ targets:
Debug:
OPENCLAW_CODE_SIGN_ENTITLEMENTS: Sources/OpenClaw.entitlements
OPENCLAW_APNS_ENTITLEMENT_ENVIRONMENT: development
OPENCLAW_PUSH_TRANSPORT: direct
OPENCLAW_PUSH_DISTRIBUTION: local
OPENCLAW_PUSH_MODE: localSandbox
OPENCLAW_PUSH_RELAY_BASE_URL: ""
OPENCLAW_PUSH_APNS_ENVIRONMENT: sandbox
OPENCLAW_PUSH_RELAY_PROFILE: deviceSandbox
OPENCLAW_PUSH_PROOF_POLICY: appleDevelopment
Release:
OPENCLAW_CODE_SIGN_ENTITLEMENTS: Sources/OpenClaw.entitlements
OPENCLAW_APNS_ENTITLEMENT_ENVIRONMENT: production
OPENCLAW_PUSH_TRANSPORT: direct
OPENCLAW_PUSH_DISTRIBUTION: local
OPENCLAW_PUSH_MODE: localProduction
OPENCLAW_PUSH_RELAY_BASE_URL: ""
OPENCLAW_PUSH_APNS_ENVIRONMENT: production
OPENCLAW_PUSH_RELAY_PROFILE: production
OPENCLAW_PUSH_PROOF_POLICY: appleStrict
info:
path: Sources/Info.plist
properties:
@@ -178,12 +170,8 @@ targets:
NSSpeechRecognitionUsageDescription: OpenClaw uses on-device speech recognition for talk mode and voice wake.
NSSupportsLiveActivities: true
ITSAppUsesNonExemptEncryption: false
OpenClawPushTransport: "$(OPENCLAW_PUSH_TRANSPORT)"
OpenClawPushDistribution: "$(OPENCLAW_PUSH_DISTRIBUTION)"
OpenClawPushMode: "$(OPENCLAW_PUSH_MODE)"
OpenClawPushRelayBaseURL: "$(OPENCLAW_PUSH_RELAY_BASE_URL)"
OpenClawPushAPNsEnvironment: "$(OPENCLAW_PUSH_APNS_ENVIRONMENT)"
OpenClawPushRelayProfile: "$(OPENCLAW_PUSH_RELAY_PROFILE)"
OpenClawPushProofPolicy: "$(OPENCLAW_PUSH_PROOF_POLICY)"
UISupportedInterfaceOrientations:
- UIInterfaceOrientationPortrait
- UIInterfaceOrientationPortraitUpsideDown

View File

@@ -1,4 +1,3 @@
// Bundled A2UI runtime resource embedded by OpenClawKit.
var __defProp$1 = Object.defineProperty;
var __exportAll = (all, no_symbols) => {
let target = {};
@@ -11936,6 +11935,10 @@ var __runInitializers = function(thisArg, initializers, value) {
};
return _classThis;
})();
/**
* Canvas A2UI browser bootstrap that installs theme overrides and native bridge
* helpers.
*/
const modalStyles = i$10`
dialog {
position: fixed;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 258 KiB

View File

@@ -1,2 +1,2 @@
8d38dc64627e6bfcebc25215d499a30841953799133dea16ffce902cf301273f plugin-sdk-api-baseline.json
d7927d51588fd006d743fc56cc22d779141b487f82655d88054d2be12f3093ff plugin-sdk-api-baseline.jsonl
57b3f65c9d8c4edddea6ffa86584756234e761cc1cdd561e4f57c8c072baaad2 plugin-sdk-api-baseline.json
1c20edb5599d0050382a32272ff3708e969f4605a2dca3db8b5cef9ab7680bd6 plugin-sdk-api-baseline.jsonl

View File

@@ -943,6 +943,14 @@
"source": "Matrix QA",
"target": "Matrix QA"
},
{
"source": "Maturity scorecard",
"target": "成熟度评分卡"
},
{
"source": "Maturity taxonomy",
"target": "成熟度分类法"
},
{
"source": "Matrix presentation metadata",
"target": "Matrix 呈现元数据"

View File

@@ -155,6 +155,7 @@ Notes:
- `onchar` still responds to explicit @mentions.
- `channels.mattermost.requireMention` is honored for legacy configs but `chatmode` is preferred.
- After the bot sends a visible reply in a channel thread, later messages in that same thread are answered without a new @mention or `onchar` prefix, so multi-turn thread conversations keep flowing. Participation is remembered for 7 days of thread inactivity (refreshed on each reply) and persists across gateway restarts. Threads the bot has only observed are unaffected; start a new top-level message to require an explicit mention again.
## Threading and sessions

View File

@@ -151,6 +151,7 @@ to a group, then mention it or configure the group to run without a mention.
groups: {
"*": {
requireMention: true,
commandLevel: "all",
historyLimit: 50,
tools: { deny: ["exec", "read", "write"] },
},
@@ -158,6 +159,7 @@ to a group, then mention it or configure the group to run without a mention.
name: "Release room",
requireMention: false,
ignoreOtherMentions: true,
commandLevel: "safety",
historyLimit: 20,
prompt: "Keep replies short and operational.",
},
@@ -172,6 +174,9 @@ to a group, then mention it or configure the group to run without a mention.
settings include:
- `requireMention`: require an @mention before the bot replies. Default: `true`.
- `commandLevel`: control which built-in slash commands can run in groups.
Default: `all`, which preserves the pre-existing QQBot group behavior when the
setting is omitted.
- `ignoreOtherMentions`: drop messages that mention someone else but not the bot.
- `historyLimit`: keep recent non-mention group messages as context for the next mentioned turn. Set `0` to disable.
- `tools`: allow/deny tools for the whole group.
@@ -179,6 +184,17 @@ settings include:
- `name`: friendly label used in logs and group context.
- `prompt`: per-group behavior prompt appended to the agent context.
`commandLevel` accepts:
- `all`: keep recognized built-in commands available as before. Some commands may
stay hidden from menus, but authorized users can still run them in the group.
- `safety`: allow common collaboration commands such as `/help`, `/btw`, and
`/stop`; ask users to run sensitive commands such as `/config`, `/tools`, and
`/bash` in private chat.
- `strict`: only allow the group-session controls needed for strict group
operation. `/stop` still stays urgent so an authorized sender can interrupt an
active run.
Old QQBot `toolPolicy` entries are retired. Run `openclaw doctor --fix` to migrate them to `tools`.
Activation modes are `mention` and `always`. `requireMention: true` maps to

View File

@@ -198,7 +198,7 @@ Every lane uploads GitHub artifacts. When `CLAWGRIT_REPORTS_TOKEN` is configured
## Full Release Validation
`Full Release Validation` is the manual umbrella workflow for "run everything before release." It accepts a branch, tag, or full commit SHA, dispatches the manual `CI` workflow with that target, dispatches `Plugin Prerelease` for release-only plugin/package/static/Docker proof, and dispatches `OpenClaw Release Checks` for install smoke, package acceptance, cross-OS package checks, QA Lab parity, Matrix, and Telegram lanes. Stable and full profiles always include exhaustive live/E2E and Docker release-path soak coverage; the beta profile can opt in with `run_release_soak=true`. The canonical package Telegram E2E runs inside Package Acceptance, so a full candidate does not start a duplicate live poller. After publishing, pass `release_package_spec` to reuse the shipped npm package across release checks, Package Acceptance, Docker, cross-OS, and Telegram without rebuilding. Use `npm_telegram_package_spec` only for a focused published-package Telegram rerun. The Codex plugin live package lane uses the same selected state by default: published `release_package_spec=openclaw@<tag>` derives `codex_plugin_spec=npm:@openclaw/codex@<tag>`, while SHA/artifact runs pack `extensions/codex` from the selected ref. Set `codex_plugin_spec` explicitly for custom plugin sources such as `npm:`, `npm-pack:`, or `git:` specs.
`Full Release Validation` is the manual umbrella workflow for "run everything before release." It accepts a branch, tag, or full commit SHA, dispatches the manual `CI` workflow with that target, dispatches `Plugin Prerelease` for release-only plugin/package/static/Docker proof, and dispatches `OpenClaw Release Checks` for install smoke, package acceptance, cross-OS package checks, maturity scorecard rendering from QA profile evidence, QA Lab parity, Matrix, and Telegram lanes. Stable and full profiles always include exhaustive live/E2E and Docker release-path soak coverage; the beta profile can opt in with `run_release_soak=true`. The canonical package Telegram E2E runs inside Package Acceptance, so a full candidate does not start a duplicate live poller. After publishing, pass `release_package_spec` to reuse the shipped npm package across release checks, Package Acceptance, Docker, cross-OS, and Telegram without rebuilding. Use `npm_telegram_package_spec` only for a focused published-package Telegram rerun. The Codex plugin live package lane uses the same selected state by default: published `release_package_spec=openclaw@<tag>` derives `codex_plugin_spec=npm:@openclaw/codex@<tag>`, while SHA/artifact runs pack `extensions/codex` from the selected ref. Set `codex_plugin_spec` explicitly for custom plugin sources such as `npm:`, `npm-pack:`, or `git:` specs.
See [Full release validation](/reference/full-release-validation) for the
stage matrix, exact workflow job names, profile differences, artifacts, and

View File

@@ -23,8 +23,8 @@ OpenClaw agent or Gateway.
```bash
openclaw skills search "calendar"
openclaw skills install <slug>
openclaw skills update <slug>
openclaw skills install @owner/<slug>
openclaw skills update @owner/<slug>
openclaw skills verify <slug>
openclaw plugins search "calendar"

View File

@@ -24,13 +24,13 @@ where you have publisher access.
Skills are published from a skill folder. The public page is:
```text
https://clawhub.ai/<owner>/<slug>
https://clawhub.ai/<owner>/skills/<slug>
```
Example:
```text
https://clawhub.ai/alice/review-helper
https://clawhub.ai/alice/skills/review-helper
```
The publish request includes the selected owner, slug, version, changelog, and

View File

@@ -212,6 +212,7 @@ Notes:
- `--agent` or `--workspace` can be used to select the target agent.
- If you rely on `--workspace` and multiple agents share that workspace, the command fails and asks you to pass `--agent`.
- Local workspace-relative avatar image files are limited to 2 MB. HTTP(S) URLs and `data:` URIs are not checked with the local file-size limit.
- When no explicit identity fields are provided, the command reads identity data from `IDENTITY.md`.
Load from `IDENTITY.md`:

View File

@@ -305,6 +305,16 @@ does not import plugin runtime code, run a package manager, or repair missing
dependencies.
</Note>
If startup logs `plugins.allow is empty; discovered non-bundled plugins may auto-load: ...`,
run `openclaw plugins list --enabled --verbose` or
`openclaw plugins inspect <id>` with a listed plugin id to confirm the plugin
ids and copy trusted ids into `plugins.allow` in `openclaw.json`. When the
warning can list every discovered plugin, it prints a ready-to-paste
`plugins.allow` snippet that already includes those ids. If a plugin loads
without install/load-path provenance, inspect that plugin id, then either pin
the trusted id in `plugins.allow` or reinstall the plugin from a trusted source
so OpenClaw records install provenance.
`plugins search` is a remote ClawHub catalog lookup. It does not inspect local
state, mutate config, install packages, or load plugin runtime code. Search
results include the ClawHub package name, family, channel, version, summary, and

View File

@@ -25,16 +25,16 @@ Related:
```bash
openclaw skills search "calendar"
openclaw skills search --limit 20 --json
openclaw skills install <slug>
openclaw skills install <slug> --version <version>
openclaw skills install @owner/<slug>
openclaw skills install @owner/<slug> --version <version>
openclaw skills install git:owner/repo
openclaw skills install git:owner/repo@main
openclaw skills install ./path/to/skill --as custom-name
openclaw skills install <slug> --force
openclaw skills install <slug> --agent <id>
openclaw skills install <slug> --global
openclaw skills update <slug>
openclaw skills update <slug> --global
openclaw skills install @owner/<slug> --force
openclaw skills install @owner/<slug> --agent <id>
openclaw skills install @owner/<slug> --global
openclaw skills update @owner/<slug>
openclaw skills update @owner/<slug> --global
openclaw skills update --all
openclaw skills update --all --agent <id>
openclaw skills update --all --global
@@ -64,8 +64,8 @@ openclaw skills workshop reject <proposal-id> --reason "Not reusable"
openclaw skills workshop quarantine <proposal-id> --reason "Needs security review"
```
`search`, `update`, and `verify` use ClawHub directly. `install <slug>` installs
a ClawHub skill, `install git:owner/repo[@ref]` clones a Git skill, and
`search`, `update`, and `verify` use ClawHub directly. `install @owner/<slug>`
installs a ClawHub skill, `install git:owner/repo[@ref]` clones a Git skill, and
`install ./path` copies a local skill directory. By default, `install`, `update`,
and `verify` target the active workspace `skills/` directory; with `--global`,
they target the shared managed skills directory. `list`/`info`/`check` still
@@ -94,15 +94,15 @@ Notes:
`SKILL.md`.
- `install --as <slug>` overrides the inferred slug for Git and local directory
installs.
- `install --version <version>` applies only to ClawHub skill slugs.
- `install --version <version>` applies only to ClawHub skill refs.
- `install --force` overwrites an existing workspace skill folder for the same
slug.
- `--global` targets the shared managed skills directory and cannot be combined
with `--agent <id>`.
- `--agent <id>` targets one configured agent workspace and overrides current
working directory inference.
- `update <slug>` updates a single tracked skill. Add `--global` to target the
shared managed skills directory instead of the workspace.
- `update @owner/<slug>` updates a single tracked skill. Add `--global` to
target the shared managed skills directory instead of the workspace.
- `update --all` updates tracked ClawHub installs in the selected workspace, or
in the shared managed skills directory when combined with `--global`.
- `verify <slug>` prints ClawHub's `clawhub.skill.verify.v1` JSON envelope by

View File

@@ -31,7 +31,7 @@ script aliases; both forms are supported.
| Command | Purpose |
| --------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `qa run` | Bundled QA self-check without `--qa-profile`; taxonomy-backed maturity profile runner with `--qa-profile smoke-ci` or `--qa-profile release`. |
| `qa run` | Bundled QA self-check without `--qa-profile`; taxonomy-backed maturity profile runner with `--qa-profile smoke-ci`, `--qa-profile release`, or `--qa-profile all`. |
| `qa suite` | Run repo-backed scenarios against the QA gateway lane. Aliases: `pnpm openclaw qa suite --runner multipass` for a disposable Linux VM. |
| `qa coverage` | Print the YAML scenario-coverage inventory (`--json` for machine output). |
| `qa parity-report` | Compare two `qa-suite-summary.json` files and write the agentic parity report, or use `--runtime-axis --token-efficiency` to write Codex-vs-OpenClaw runtime parity and token-efficiency reports from one runtime-pair summary. |
@@ -75,8 +75,10 @@ pnpm openclaw qa run \
Use `smoke-ci` for deterministic profile proof with mock model providers and
Crabline fake provider servers. Use `release` for Stable/LTS proof against live
channels. When a command also needs an OpenClaw root profile, put the root
profile before the QA command:
channels. Use `all` only for explicit full-taxonomy evidence runs; it selects
every active maturity category and can be dispatched through the `QA Profile
Evidence` workflow with `qa_profile=all`. When a command also needs an OpenClaw
root profile, put the root profile before the QA command:
```bash
pnpm openclaw --profile work qa run --qa-profile smoke-ci

View File

@@ -68,6 +68,14 @@
"source": "/reference/openclaw-sdk-api-design",
"destination": "/gateway/external-apps"
},
{
"source": "/reference/maturity-scorecard",
"destination": "/maturity/scorecard"
},
{
"source": "/reference/maturity-taxonomy",
"destination": "/maturity/taxonomy"
},
{
"source": "/mcp",
"destination": "/cli/mcp"
@@ -1852,6 +1860,8 @@
{
"group": "Release and CI",
"pages": [
"maturity/scorecard",
"maturity/taxonomy",
"reference/RELEASING",
"reference/full-release-validation",
"reference/release-performance-sweep",

View File

@@ -1103,6 +1103,7 @@ for provider examples and precedence.
- `models`: optional per-agent model catalog/runtime overrides keyed by full `provider/model` ids. Use `models["provider/model"].agentRuntime` for per-agent runtime exceptions.
- `runtime`: optional per-agent runtime descriptor. Use `type: "acp"` with `runtime.acp` defaults (`agent`, `backend`, `mode`, `cwd`) when the agent should default to ACP harness sessions.
- `identity.avatar`: workspace-relative path, `http(s)` URL, or `data:` URI.
- Local workspace-relative `identity.avatar` image files are limited to 2 MB. `http(s)` URLs and `data:` URIs are not checked with the local file-size limit.
- `identity` derives defaults: `ackReaction` from `emoji`, `mentionPatterns` from `name`/`emoji`.
- `subagents.allowAgents`: allowlist of configured agent ids for explicit `sessions_spawn.agentId` targets (`["*"]` = any configured target; default: same agent only). Include the requester id when self-targeted `agentId` calls should be allowed. Stale entries whose agent config was deleted are rejected by `sessions_spawn` and omitted from `agents_list`; run `openclaw doctor --fix` to clean them up, or add a minimal `agents.list[]` entry if that target should remain spawnable while inheriting defaults.
- Sandbox inheritance guard: if the requester session is sandboxed, `sessions_spawn` rejects targets that would run unsandboxed.

View File

@@ -602,7 +602,7 @@ See [Inferred commitments](/concepts/commitments).
- `remote.transport`: `ssh` (default) or `direct` (ws/wss). For `direct`, `remote.url` must be `wss://` for public hosts; plaintext `ws://` is accepted only for loopback, LAN, link-local, `.local`, `.ts.net`, and Tailscale CGNAT hosts.
- `remote.remotePort`: gateway port on the remote SSH host. Defaults to `18789`; use this when the local tunnel port differs from the remote gateway port.
- `gateway.remote.token` / `.password` are remote-client credential fields. They do not configure gateway auth by themselves.
- `gateway.push.apns.relay.baseUrl`: base HTTPS URL for the external APNs relay used by official/TestFlight iOS builds after they publish relay-backed registrations to the gateway. This URL must match the relay URL compiled into the iOS build.
- `gateway.push.apns.relay.baseUrl`: base HTTPS URL for the external APNs relay used after relay-backed iOS builds publish registrations to the gateway. Public App Store/TestFlight builds use the hosted OpenClaw relay. Custom relay URLs must match a deliberately separate iOS build/deployment path whose relay URL points at that relay.
- `gateway.push.apns.relay.timeoutMs`: gateway-to-relay send timeout in milliseconds. Defaults to `10000`.
- Relay-backed registrations are delegated to a specific gateway identity. The paired iOS app fetches `gateway.identity.get`, includes that identity in the relay registration, and forwards a registration-scoped send grant to the gateway. Another gateway cannot reuse that stored registration.
- `OPENCLAW_APNS_RELAY_BASE_URL` / `OPENCLAW_APNS_RELAY_TIMEOUT_MS`: temporary env overrides for the relay config above.

View File

@@ -337,9 +337,9 @@ candidate contains redacted secret placeholders such as `***`.
</Accordion>
<Accordion title="Enable relay-backed push for official iOS builds">
Relay-backed push uses the hosted OpenClaw relay by default: `https://ios-push-relay.openclaw.ai`.
Relay-backed push for public App Store/TestFlight builds uses the hosted OpenClaw relay: `https://ios-push-relay.openclaw.ai`.
To use a custom relay, set this in gateway config:
Custom relay deployments require a deliberately separate iOS build/deployment path whose relay URL matches the gateway relay URL. If you are using a custom relay build, set this in gateway config:
```json5
{
@@ -369,12 +369,12 @@ candidate contains redacted secret placeholders such as `***`.
- Uses a registration-scoped send grant forwarded by the paired iOS app. The gateway does not need a deployment-wide relay token.
- Binds each relay-backed registration to the gateway identity that the iOS app paired with, so another gateway cannot reuse the stored registration.
- Keeps local/manual iOS builds on direct APNs. Relay-backed sends apply only to official distributed builds that registered through the relay.
- Must match the relay base URL baked into the official/TestFlight iOS build, so registration and send traffic reach the same relay deployment.
- Must match the relay base URL baked into the iOS build, so registration and send traffic reach the same relay deployment.
End-to-end flow:
1. Install an official/TestFlight iOS build.
2. Optional: configure `gateway.push.apns.relay.baseUrl` on the gateway only when using a custom relay deployment.
2. Optional: configure `gateway.push.apns.relay.baseUrl` on the gateway only when using a deliberately separate custom relay build.
3. Pair the iOS app to the gateway and let both node and operator sessions connect.
4. The iOS app fetches the gateway identity, registers with the relay using App Attest plus the app receipt, and then publishes the relay-backed `push.apns.register` payload to the paired gateway.
5. The gateway stores the relay handle and send grant, then uses them for `push.test`, wake nudges, and reconnect wakes.
@@ -387,7 +387,7 @@ candidate contains redacted secret placeholders such as `***`.
Compatibility note:
- `OPENCLAW_APNS_RELAY_BASE_URL` and `OPENCLAW_APNS_RELAY_TIMEOUT_MS` still work as temporary env overrides.
- Custom gateway relay URLs must match the relay base URL baked into the official/TestFlight iOS build.
- Custom gateway relay URLs must match the relay base URL baked into the iOS build. The public App Store release lane rejects custom iOS relay URL overrides.
- `OPENCLAW_APNS_RELAY_ALLOW_HTTP=true` remains a loopback-only development escape hatch; do not persist HTTP relay URLs in config.
See [iOS App](/platforms/ios#relay-backed-push-for-official-builds) for the end-to-end flow and [Authentication and trust flow](/platforms/ios#authentication-and-trust-flow) for the relay security model.

View File

@@ -346,10 +346,10 @@ lives on the [First-run FAQ](/help/faq-first-run).
```bash
openclaw skills search "calendar"
openclaw skills search --limit 20
openclaw skills install <skill-slug>
openclaw skills install <skill-slug> --version <version>
openclaw skills install <skill-slug> --force
openclaw skills install <skill-slug> --global
openclaw skills install @owner/<skill-slug>
openclaw skills install @owner/<skill-slug> --version <version>
openclaw skills install @owner/<skill-slug> --force
openclaw skills install @owner/<skill-slug> --global
openclaw skills update --all
openclaw skills update --all --global
openclaw skills list --eligible
@@ -433,11 +433,11 @@ lives on the [First-run FAQ](/help/faq-first-run).
Install skills:
```bash
openclaw skills install <skill-slug>
openclaw skills install @owner/<skill-slug>
openclaw skills update --all
```
Native installs land in the active workspace `skills/` directory. For shared skills across all local agents, use `openclaw skills install <slug> --global` (or place them manually in `~/.openclaw/skills/<name>/SKILL.md`). If only some agents should see a shared install, configure `agents.defaults.skills` or `agents.list[].skills`. Some skills expect binaries installed via Homebrew; on Linux that means Linuxbrew (see the Homebrew Linux FAQ entry above). See [Skills](/tools/skills), [Skills config](/tools/skills-config), and [ClawHub](/tools/clawhub).
Native installs land in the active workspace `skills/` directory. For shared skills across all local agents, use `openclaw skills install @owner/<skill-slug> --global` (or place them manually in `~/.openclaw/skills/<name>/SKILL.md`). If only some agents should see a shared install, configure `agents.defaults.skills` or `agents.list[].skills`. Some skills expect binaries installed via Homebrew; on Linux that means Linuxbrew (see the Homebrew Linux FAQ entry above). See [Skills](/tools/skills), [Skills config](/tools/skills-config), and [ClawHub](/tools/clawhub).
</Accordion>

View File

@@ -190,7 +190,10 @@ inside every shard.
- When dispatched by `pnpm openclaw qa run --qa-profile <profile>`, embeds the
selected taxonomy profile scorecard in the same `qa-evidence.json`.
`smoke-ci` writes slim evidence, which sets `evidenceMode: "slim"` and omits
per-entry `execution`.
per-entry `execution`. `release` covers the curated release-readiness slice;
`all` selects every active maturity category and is intended for explicit QA
Profile Evidence workflow dispatches when a full scorecard artifact is
needed.
- Runs multiple selected scenarios in parallel by default with isolated
gateway workers. `qa-channel` defaults to concurrency 4 (bounded by the
selected scenario count). Use `--concurrency <count>` to tune the worker

View File

@@ -110,14 +110,18 @@ systemctl --user daemon-reload
### Windows (Scheduled Task)
Default task name is `OpenClaw Gateway` (or `OpenClaw Gateway (<profile>)`).
The task script lives under your state dir.
The task script lives under your state dir as `gateway.cmd`; current installs may
also create a windowless `gateway.vbs` launcher that Task Scheduler runs instead
of opening `gateway.cmd` directly.
```powershell
schtasks /Delete /F /TN "OpenClaw Gateway"
Remove-Item -Force "$env:USERPROFILE\.openclaw\gateway.cmd"
Remove-Item -Force "$env:USERPROFILE\.openclaw\gateway.cmd" -ErrorAction SilentlyContinue
Remove-Item -Force "$env:USERPROFILE\.openclaw\gateway.vbs" -ErrorAction SilentlyContinue
```
If you used a profile, delete the matching task name and `~\.openclaw-<profile>\gateway.cmd`.
If you used a profile, delete the matching task name and the `gateway.cmd` /
`gateway.vbs` files under `~\.openclaw-<profile>`.
## Normal install vs source checkout

File diff suppressed because it is too large Load Diff

3421
docs/maturity/scorecard.md Normal file

File diff suppressed because it is too large Load Diff

3781
docs/maturity/taxonomy.md Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -75,9 +75,9 @@ openclaw gateway call node.list --params "{}"
Official distributed iOS builds use the external push relay instead of publishing the raw APNs
token to the gateway.
By default, official/TestFlight builds and gateways use the hosted relay at `https://ios-push-relay.openclaw.ai`.
Official/TestFlight builds from the public App Store release lane use the hosted relay at `https://ios-push-relay.openclaw.ai`.
Custom relay deployments can override the gateway relay URL:
Custom relay deployments require a deliberately separate iOS build/deployment path whose relay URL matches the gateway relay URL. The public App Store release lane does not accept custom relay URL overrides. If you are using a custom relay build, set the matching gateway relay URL:
```json5
{
@@ -100,7 +100,7 @@ How the flow works:
- The iOS app fetches the paired gateway identity and includes it in relay registration, so the relay-backed registration is delegated to that specific gateway.
- The app forwards that relay-backed registration to the paired gateway with `push.apns.register`.
- The gateway uses that stored relay handle for `push.test`, background wakes, and wake nudges.
- Custom gateway relay URLs must match the relay URL baked into the official/TestFlight iOS build.
- Custom gateway relay URLs must match the relay URL baked into the iOS build.
- If the app later connects to a different gateway or a build with a different relay base URL, it refreshes the relay registration instead of reusing the old binding.
What the gateway does **not** need for this path:
@@ -111,7 +111,7 @@ What the gateway does **not** need for this path:
Expected operator flow:
1. Install the official/TestFlight iOS build.
2. Optional: set `gateway.push.apns.relay.baseUrl` on the gateway only when using a custom relay deployment.
2. Optional: set `gateway.push.apns.relay.baseUrl` on the gateway only when using a deliberately separate custom relay build.
3. Pair the app to the gateway and let it finish connecting.
4. The app publishes `push.apns.register` automatically after it has an APNs token, the operator session is connected, and relay registration succeeds.
5. After that, `push.test`, reconnect wakes, and wake nudges can use the stored relay-backed registration.
@@ -130,7 +130,7 @@ compatible but does not count as a durable last-seen update.
Compatibility note:
- `OPENCLAW_APNS_RELAY_BASE_URL` still works as a temporary env override for the gateway.
- `OPENCLAW_PUSH_RELAY_BASE_URL` still works as a temporary env override for official/TestFlight iOS builds.
- The public App Store release lane rejects `OPENCLAW_PUSH_RELAY_BASE_URL` for iOS builds.
## Authentication and trust flow

View File

@@ -124,8 +124,11 @@ openclaw gateway status --json
```
Native Windows CLI and Gateway flows are supported and continue to improve.
Managed startup uses Windows Scheduled Tasks when available and falls back to a
per-user Startup-folder login item if task creation is denied.
Managed startup uses Windows Scheduled Tasks when available. The task keeps the
readable `gateway.cmd` script in the OpenClaw state dir, but launches it through
a generated `gateway.vbs` WScript wrapper so the background Gateway does not open
a visible console window. If task creation is denied, OpenClaw falls back to a
per-user Startup-folder login item.
To install the Gateway service:

View File

@@ -150,6 +150,12 @@ the same directory), or `~/.openclaw/agents/<agentId>/copilot` otherwise.
Override with `copilotHome: <path>` on the attempt input when you need a
custom location (for example, a shared mount for migration).
Live harness tests use `OPENCLAW_COPILOT_AGENT_LIVE_TOKEN` when a direct token
is needed. The shared live-test setup intentionally scrubs `COPILOT_GITHUB_TOKEN`,
`GH_TOKEN`, and `GITHUB_TOKEN` after staging real auth profiles into the isolated
test home, so passing a `gh auth token` value through the dedicated live-test
variable avoids false skips without exposing the token to unrelated suites.
## Configuration surface
The harness reads its config from per-attempt input
@@ -253,14 +259,10 @@ under `describe("runSideQuestion")`.
fallback for whatever runtimes do not have a peer surface.
- PI session state is not migrated when an agent switches to `copilot`.
Selection is per attempt; existing PI sessions remain valid.
- **Interactive `ask_user` is not yet wired.** The SDK's
`onUserInputRequest` handler is intentionally not registered, which
per the SDK contract hides the `ask_user` tool from the model
entirely. Agents running under this harness make best-judgment
decisions from the initial prompt rather than asking clarifying
questions mid-turn. A follow-up will port the codex pattern at
`extensions/codex/src/app-server/user-input-bridge.ts` to route SDK
`UserInputRequest`s through the OpenClaw channel/TUI prompt path.
- `ask_user` uses the same OpenClaw prompt-and-reply path as the Codex
harness. When the Copilot SDK asks for user input, OpenClaw posts a
blocking prompt to the active channel/TUI and the next queued user
message resolves the SDK request.
## Permissions and ask_user
@@ -314,13 +316,23 @@ right scope, and `session_status: "current"` resolves to a stale
sandbox key. The bridge builder is in
`extensions/copilot/src/tool-bridge.ts` and mirrors the PI
authoritative call at
`src/agents/pi-embedded-runner/run/attempt.ts:1029-1117`. Two PI fields
are intentionally **not** forwarded at MVP and tracked as follow-ups:
`sandbox` (the harness does not yet route through `resolveSandboxContext`)
and the PI tool-search/code-mode machinery
(`toolSearchCatalogRef`, `includeCoreTools`,
`includeToolSearchControls`, `toolSearchCatalogExecutor`,
`toolConstructionPlan`), which has no analog at the SDK boundary.
`src/agents/pi-embedded-runner/run/attempt.ts:1029-1117`. `runAttempt`
already resolves sandbox context through the shared
`resolveSandboxContext` seam, passes the SDK an effective working
directory, and forwards `sandbox` plus the subagent-spawn workspace into
the tool bridge. The bridge also forwards the bounded tool-construction
controls it can enforce at the SDK boundary: `includeCoreTools`, the
runtime tool allowlist, and `toolConstructionPlan`.
The bridge also uses the shared harness tool-surface helper from
`openclaw/plugin-sdk/agent-harness-tool-runtime` for PI parity. When
tool-search is enabled, the SDK sees compact control tools plus a hidden
catalog executor instead of every OpenClaw tool schema. When code mode is
enabled, the helper builds the same code-mode control surface and catalog
lifecycle used by other agent harnesses. Local-model lean defaults,
runtime-compatible schema filtering, directory hydration, and catalog
cleanup all stay in the shared helper so Copilot and Codex-adjacent
harnesses do not drift.
### Session-level GitHub token
@@ -337,7 +349,10 @@ When the resolved mode is `useLoggedInUser`, the session-level field
is omitted so the SDK keeps deriving identity from the logged-in
identity.
`ask_user` is intentionally hidden — see Limitations above.
`ask_user` uses `SessionConfig.onUserInputRequest`. The bridge accepts
choice indexes or labels for fixed-choice requests, accepts free-form
answers when the SDK request allows them, and cancels a pending request
when the OpenClaw attempt is aborted.
## Related

View File

@@ -319,10 +319,56 @@ Cron-driven runs also expose `ctx.jobId` (the originating cron job id) so
plugin hooks can scope metrics, side effects, or state to a specific scheduled
job.
For channel-originated runs, `ctx.messageProvider` is the provider surface such
as `discord` or `telegram`, while `ctx.channelId` is the conversation target
identifier when OpenClaw can derive one from the session key or delivery
metadata.
For channel-originated runs, `ctx.channel` and `ctx.messageProvider` identify
the provider surface such as `discord` or `telegram`, while `ctx.channelId` is
the conversation target identifier when OpenClaw can derive one from the session
key or delivery metadata.
When sender identity is available, agent hook contexts also include:
- `ctx.senderId` — channel-scoped sender ID (e.g. Feishu `open_id`, Discord
user ID). Populated when the run originates from a user message with known
sender metadata.
- `ctx.chatId` — transport-native conversation identifier (e.g. Feishu
`chat_id`, Telegram `chat_id`). Populated when the originating channel
provides a native conversation ID.
- `ctx.channelContext.sender.id` — the same sender ID as `ctx.senderId`, under a
channel-owned object that plugins can extend with channel-specific fields.
- `ctx.channelContext.chat.id` — the same conversation ID as `ctx.chatId`, under a
channel-owned object that plugins can extend with channel-specific fields.
Core only defines the nested `id` fields. Channel plugins that pass richer
sender or chat metadata through the inbound helper can augment
`PluginHookChannelSenderContext` or `PluginHookChannelChatContext` from
`openclaw/plugin-sdk/channel-inbound`:
```ts
declare module "openclaw/plugin-sdk/channel-inbound" {
interface PluginHookChannelSenderContext {
unionId?: string;
userId?: string;
}
}
```
Channel plugins pass those fields through the inbound SDK helper:
```ts
buildChannelInboundEventContext({
// ...
channelContext: {
sender: { id: senderOpenId, unionId, userId },
chat: { id: chatId },
},
});
```
These fields are optional and absent for system-originated runs (heartbeat,
cron, exec-event).
`ctx.senderExternalId` remains as a deprecated source-compatibility field for
older plugins. Core does not populate it; new channel-specific sender identities
should live under `ctx.channelContext.sender` through module augmentation.
`agent_end` is an observation hook. Gateway and persistent harness paths run it
fire-and-forget after the turn, while short-lived one-shot CLI paths wait for the

View File

@@ -191,6 +191,7 @@ or npm install metadata. Those belong in your plugin code and `package.json`.
| `skills` | No | `string[]` | Skill directories to load, relative to the plugin root. |
| `name` | No | `string` | Human-readable plugin name. |
| `description` | No | `string` | Short summary shown in plugin surfaces. |
| `icon` | No | `string` | HTTPS image URL for marketplace/catalog cards. ClawHub accepts any valid `https://` URL and falls back to the default plugin icon when this is omitted or invalid. |
| `version` | No | `string` | Informational plugin version. |
| `uiHints` | No | `Record<string, object>` | UI labels, placeholders, and sensitivity hints for config fields. |

View File

@@ -196,6 +196,23 @@ finish. Both helpers accept the same `{ event, ctx }` payload as
`runAgentHarnessAgentEndHook(...)`; their failures do not alter the completed
attempt result.
### User input and tool surfaces
Native harnesses that expose a runtime-level user-input request should use the
user-input helpers from `openclaw/plugin-sdk/agent-harness-runtime` to format
the prompt, deliver it through OpenClaw's blocking reply path, and normalize
choice/free-form answers back into the runtime's native response shape. The
helper keeps channel/TUI presentation consistent while each harness keeps its
own protocol parsing and pending-request lifecycle.
Native harnesses that need PI-like compact tool routing should use
`createAgentHarnessToolSurfaceRuntime(...)` from
`openclaw/plugin-sdk/agent-harness-tool-runtime`. It owns
tool-search/code-mode control selection, local-model lean defaults,
runtime-compatible schema filtering, hidden catalog execution, directory
hydration, and catalog cleanup. Harnesses still own their SDK-specific tool
conversion and native execution callback.
### Native Codex harness mode
The bundled `codex` harness is the native Codex mode for embedded OpenClaw

View File

@@ -29,7 +29,10 @@ import {
```
- `buildChannelInboundEventContext(...)`: project normalized channel facts into
the prompt/session context.
the prompt/session context. Use `channelContext` to pass channel-owned
sender/chat metadata through to plugin hook `ctx.channelContext`; augment
`PluginHookChannelSenderContext` or `PluginHookChannelChatContext` from this
subpath for channel-specific fields.
- `runChannelInboundEvent(...)`: run ingest, classify, preflight, resolve,
record, dispatch, and finalize for one inbound platform event.
- `dispatchChannelInboundReply(...)`: record and dispatch an already assembled

View File

@@ -84,8 +84,8 @@ Choose the Token Plan auth choice that matches the regional base URL shown in Xi
| Model ref | Input | Context | Max output | Reasoning | Notes |
| --------------------------------- | ----------- | --------- | ---------- | --------- | ------------- |
| `xiaomi-token-plan/mimo-v2.5-pro` | text | 1,048,576 | 32,000 | Yes | Default model |
| `xiaomi-token-plan/mimo-v2.5` | text, image | 1,048,576 | 32,000 | Yes | Multimodal |
| `xiaomi-token-plan/mimo-v2.5-pro` | text | 1,048,576 | 131,072 | Yes | Default model |
| `xiaomi-token-plan/mimo-v2.5` | text, image | 1,048,576 | 131,072 | Yes | Multimodal |
<Tip>
Token Plan onboarding validates the key shape and warns when a `tp-...` key is entered into the pay-as-you-go path, or an `sk-...` key is entered into the Token Plan path.
@@ -222,7 +222,7 @@ Token Plan:
reasoning: true,
input: ["text"],
contextWindow: 1048576,
maxTokens: 32000,
maxTokens: 131072,
},
{
id: "mimo-v2.5",
@@ -230,7 +230,7 @@ Token Plan:
reasoning: true,
input: ["text", "image"],
contextWindow: 1048576,
maxTokens: 32000,
maxTokens: 131072,
},
],
},

View File

@@ -67,7 +67,7 @@ Wraps papla.media TTS and sends results as Telegram voice notes (no annoying aut
<img src="/assets/showcase/papla-tts.jpg" alt="Telegram voice note output from TTS" />
</Card>
<Card title="CodexMonitor" icon="eye" href="https://clawhub.ai/odrobnik/codexmonitor">
<Card title="CodexMonitor" icon="eye" href="https://clawhub.ai/odrobnik/skills/codexmonitor">
**@odrobnik** • `devtools` `codex` `brew`
Homebrew-installed helper to list, inspect, and watch local OpenAI Codex sessions (CLI + VS Code).
@@ -75,7 +75,7 @@ Homebrew-installed helper to list, inspect, and watch local OpenAI Codex session
<img src="/assets/showcase/codexmonitor.png" alt="CodexMonitor on ClawHub" />
</Card>
<Card title="Bambu 3D Printer Control" icon="print" href="https://clawhub.ai/tobiasbischoff/bambu-cli">
<Card title="Bambu 3D Printer Control" icon="print" href="https://clawhub.ai/tobiasbischoff/skills/bambu-cli">
**@tobiasbischoff** • `hardware` `3d-printing` `skill`
Control and troubleshoot BambuLab printers: status, jobs, camera, AMS, calibration, and more.
@@ -83,7 +83,7 @@ Control and troubleshoot BambuLab printers: status, jobs, camera, AMS, calibrati
<img src="/assets/showcase/bambu-cli.png" alt="Bambu CLI skill on ClawHub" />
</Card>
<Card title="Vienna transport (Wiener Linien)" icon="train" href="https://clawhub.ai/hjanuschka/wienerlinien">
<Card title="Vienna transport (Wiener Linien)" icon="train" href="https://clawhub.ai/hjanuschka/skills/wienerlinien">
**@hjanuschka** • `travel` `transport` `skill`
Real-time departures, disruptions, elevator status, and routing for Vienna's public transport.
@@ -97,7 +97,7 @@ Real-time departures, disruptions, elevator status, and routing for Vienna's pub
Automated UK school meal booking via ParentPay. Uses mouse coordinates for reliable table cell clicking.
</Card>
<Card title="R2 upload (Send Me My Files)" icon="cloud-arrow-up" href="https://clawhub.ai/skills/r2-upload">
<Card title="R2 upload (Send Me My Files)" icon="cloud-arrow-up" href="https://clawhub.ai/julianengel/skills/r2-upload">
**@julianengel** • `files` `r2` `presigned-urls`
Upload to Cloudflare R2/S3 and generate secure presigned download links. Useful for remote OpenClaw instances.
@@ -267,7 +267,7 @@ Speech-first entry points, phone bridges, and transcription-heavy workflows.
Vapi voice assistant to OpenClaw HTTP bridge. Near real-time phone calls with your agent.
</Card>
<Card title="OpenRouter transcription" icon="microphone" href="https://clawhub.ai/obviyus/openrouter-transcribe">
<Card title="OpenRouter transcription" icon="microphone" href="https://clawhub.ai/obviyus/skills/openrouter-transcribe">
**@obviyus** • `transcription` `multilingual` `skill`
Multi-lingual audio transcription via OpenRouter (Gemini, and more). Available on ClawHub.
@@ -289,8 +289,8 @@ Packaging, deployment, and integrations that make OpenClaw easier to run and ext
OpenClaw gateway running on Home Assistant OS with SSH tunnel support and persistent state.
</Card>
<Card title="Home Assistant skill" icon="toggle-on" href="https://clawhub.ai/skills/homeassistant">
**ClawHub**`homeassistant` `skill` `automation`
<Card title="Home Assistant skill" icon="toggle-on" href="https://clawhub.ai/homeofe/skills/openclaw-homeassistant">
**@homeofe** • `homeassistant` `skill` `automation`
Control and automate Home Assistant devices via natural language.
@@ -303,8 +303,8 @@ Control and automate Home Assistant devices via natural language.
Batteries-included nixified OpenClaw configuration for reproducible deployments.
</Card>
<Card title="CalDAV calendar" icon="calendar" href="https://clawhub.ai/skills/caldav-calendar">
**ClawHub**`calendar` `caldav` `skill`
<Card title="CalDAV calendar" icon="calendar" href="https://clawhub.ai/asleep123/skills/caldav-calendar">
**@asleep123** • `calendar` `caldav` `skill`
Calendar skill using khal and vdirsyncer. Self-hosted calendar integration.

View File

@@ -135,3 +135,753 @@ html.dark .nav-tabs-underline {
grid-template-columns: 1fr;
}
}
.maturity-hero {
display: grid;
gap: 14px;
margin: 10px 0 38px;
padding: 4px 0 26px 20px;
border-bottom: 1px solid color-mix(in oklab, rgb(var(--primary)) 22%, transparent);
border-left: 3px solid rgb(var(--primary));
}
.maturity-hero-compact {
margin-bottom: 34px;
}
.maturity-hero h2 {
max-width: 46rem;
margin: 0;
font-size: clamp(26px, 3vw, 38px);
line-height: 1.08;
letter-spacing: -0.01em;
}
.maturity-hero-title {
max-width: 46rem;
margin: 0;
font-size: clamp(26px, 3vw, 38px);
font-weight: 750;
line-height: 1.08;
letter-spacing: -0.01em;
}
.maturity-hero > p:not(.maturity-kicker):not(.maturity-jump-links) {
max-width: 58rem;
margin: 0;
font-size: 16px;
line-height: 1.65;
opacity: 0.76;
}
.maturity-kicker {
margin: 0;
color: rgb(var(--primary));
font-size: 10px;
font-weight: 750;
letter-spacing: 0.1em;
line-height: 1.3;
text-transform: uppercase;
}
.maturity-jump-links {
margin: 0;
font-size: 13px;
line-height: 1.5;
}
.maturity-jump-links a {
text-decoration: none;
}
.maturity-jump-links a:hover {
text-decoration: underline;
text-underline-offset: 3px;
}
.maturity-score-stable,
.maturity-band-stable {
color: #4ca574;
}
.maturity-score-beta,
.maturity-band-beta {
color: #849fd2;
}
.maturity-score-alpha,
.maturity-band-alpha {
color: #d39a4b;
}
.maturity-score-experimental,
.maturity-band-experimental {
color: #dc7669;
}
.maturity-score-clawesome,
.maturity-band-clawesome {
color: #46b59a;
}
.maturity-level-pill {
display: inline-flex;
align-items: center;
gap: 6px;
width: max-content;
max-width: 100%;
padding: 3px 8px;
border: 1px solid color-mix(in oklab, currentColor 32%, transparent);
border-radius: 999px;
background: color-mix(in oklab, currentColor 10%, transparent);
color: inherit;
font-size: 10px;
font-weight: 750;
line-height: 1.25;
white-space: nowrap;
}
.maturity-level-code {
font-size: 9px;
letter-spacing: 0.04em;
opacity: 0.72;
}
.maturity-level-experimental {
color: #dc7669;
}
.maturity-level-alpha {
color: #d39a4b;
}
.maturity-level-beta {
color: #849fd2;
}
.maturity-level-stable {
color: #4ca574;
}
.maturity-level-clawesome {
color: #46b59a;
}
.maturity-summary-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
margin: 14px 0 20px;
border-top: 1px solid color-mix(in oklab, rgb(var(--primary)) 18%, transparent);
border-bottom: 1px solid color-mix(in oklab, rgb(var(--primary)) 18%, transparent);
}
.maturity-summary-item {
display: grid;
gap: 10px;
min-width: 0;
padding: 18px 20px 18px 0;
}
.maturity-summary-item + .maturity-summary-item {
padding-left: 20px;
border-left: 1px solid color-mix(in oklab, rgb(var(--primary)) 14%, transparent);
}
.maturity-summary-heading {
display: flex;
align-items: baseline;
gap: 9px;
}
.maturity-summary-value {
display: inline-block;
font-size: 30px;
font-weight: 750;
letter-spacing: -0.04em;
}
.maturity-summary-heading > span:not(.maturity-summary-value) {
font-size: 13px;
font-weight: 700;
}
.maturity-summary-bar {
height: 7px;
overflow: hidden;
background: color-mix(in oklab, currentColor 14%, transparent);
}
.maturity-summary-bar span {
display: block;
width: calc(var(--score) * 1%);
height: 100%;
background: currentColor;
}
.maturity-summary-meta {
display: flex;
flex-wrap: wrap;
gap: 4px 10px;
font-size: 11px;
line-height: 1.4;
}
.maturity-summary-meta span:first-child {
font-weight: 700;
}
.maturity-summary-meta span:last-child {
opacity: 0.62;
}
.maturity-band-list {
display: flex;
margin: 12px 0 30px;
border-top: 1px solid color-mix(in oklab, rgb(var(--primary)) 16%, transparent);
border-bottom: 1px solid color-mix(in oklab, rgb(var(--primary)) 16%, transparent);
}
.maturity-band {
display: grid;
flex: 1 1 0;
gap: 3px;
padding: 10px 12px 11px 0;
}
.maturity-band + .maturity-band {
padding-left: 12px;
border-left: 1px solid color-mix(in oklab, rgb(var(--primary)) 12%, transparent);
}
.maturity-band-title {
font-size: 12px;
font-weight: 700;
}
.maturity-band-title + span {
color: inherit;
font-size: 11px;
opacity: 0.7;
}
.maturity-band > span:last-child {
color: inherit;
font-size: 11px;
opacity: 0.7;
}
.maturity-band span {
color: inherit;
font-size: 11px;
opacity: 0.7;
}
.maturity-band .maturity-level-pill {
font-size: 10px;
opacity: 1;
}
.maturity-band .maturity-level-pill span {
font-size: inherit;
opacity: inherit;
}
.maturity-score {
display: grid;
gap: 4px;
min-width: 0;
color: inherit;
font-size: 11px;
font-weight: 700;
}
.maturity-score-label {
display: flex;
justify-content: space-between;
gap: 6px;
line-height: 1.2;
}
.maturity-score-label > span:first-child {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.maturity-score-label > span:last-child {
flex: 0 0 auto;
}
.maturity-score-label .maturity-level-pill {
gap: 4px;
padding: 2px 6px;
font-size: 9px;
}
.maturity-score-label-text {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.maturity-summary-meta .maturity-level-pill {
opacity: 1;
}
.maturity-meter {
display: inline-block;
width: 100%;
height: 3px;
overflow: hidden;
background: color-mix(in oklab, currentColor 15%, transparent);
vertical-align: middle;
}
.maturity-meter > span {
display: block;
width: 0;
height: 100%;
background: currentColor;
}
.maturity-score-unscored,
.maturity-lts-none {
color: inherit;
opacity: 0.52;
}
.maturity-surface-table {
display: grid;
gap: 0;
margin: 8px 0 22px;
}
.maturity-surface-row {
display: grid;
grid-template-columns: minmax(190px, 1.55fr) repeat(3, minmax(110px, 1fr)) minmax(72px, 0.55fr);
gap: 12px;
align-items: center;
padding: 13px 0;
border-top: 1px solid color-mix(in oklab, currentColor 14%, transparent);
}
.maturity-surface-row-header {
padding: 0 0 9px;
border-top: 0;
color: inherit;
font-size: 10px;
font-weight: 750;
letter-spacing: 0.04em;
opacity: 0.56;
text-transform: uppercase;
}
.maturity-surface-name {
display: grid;
gap: 3px;
min-width: 0;
border-bottom: 0 !important;
text-decoration: none;
}
.maturity-surface-name:hover .maturity-surface-title {
color: rgb(var(--primary));
}
.maturity-surface-title {
overflow-wrap: anywhere;
font-size: 13px;
font-weight: 700;
line-height: 1.25;
}
.maturity-surface-meta {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 6px;
min-width: 0;
}
.maturity-surface-meta > span:not(.maturity-level-pill) {
font-size: 11px;
opacity: 0.62;
}
.maturity-surface-meta .maturity-level-pill {
opacity: 1;
}
.maturity-surface-metric {
min-width: 0;
}
.maturity-surface-metric-label {
display: none;
font-size: 10px;
opacity: 0.62;
}
.maturity-surface-support {
justify-self: start;
}
.maturity-lts {
display: inline-flex;
align-items: center;
gap: 5px;
color: inherit;
font-size: 11px;
font-weight: 700;
white-space: nowrap;
}
.maturity-lts::before {
width: 6px;
height: 6px;
border-radius: 50%;
background: currentColor;
content: "";
}
.maturity-lts-partial {
color: #d39a4b;
}
.maturity-lts-full {
color: #4ca574;
}
.maturity-evidence-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
margin: 14px 0 24px;
border-top: 1px solid color-mix(in oklab, rgb(var(--primary)) 16%, transparent);
border-bottom: 1px solid color-mix(in oklab, rgb(var(--primary)) 16%, transparent);
}
.maturity-evidence-card {
display: grid;
gap: 4px;
min-width: 0;
padding: 14px 16px 14px 0;
}
.maturity-evidence-card + .maturity-evidence-card {
padding-left: 16px;
border-left: 1px solid color-mix(in oklab, rgb(var(--primary)) 12%, transparent);
}
.maturity-evidence-title {
font-size: 13px;
font-weight: 700;
}
.maturity-evidence-card span {
font-size: 11px;
line-height: 1.4;
opacity: 0.68;
}
.maturity-readiness-summary {
margin: 0 0 12px;
font-size: 12px;
opacity: 0.65;
}
.maturity-readiness-list {
display: grid;
margin: 0;
border-top: 1px solid color-mix(in oklab, rgb(var(--primary)) 14%, transparent);
}
.maturity-readiness-row {
display: grid;
grid-template-columns: minmax(0, 1.7fr) minmax(120px, 0.8fr) minmax(110px, 0.7fr);
gap: 14px;
align-items: center;
padding: 11px 0;
border-bottom: 1px solid color-mix(in oklab, rgb(var(--primary)) 10%, transparent);
font-size: 11px;
}
.maturity-readiness-row-header {
padding: 8px 0;
border-bottom-color: color-mix(in oklab, rgb(var(--primary)) 14%, transparent);
font-size: 10px;
font-weight: 750;
letter-spacing: 0.04em;
opacity: 0.56;
text-transform: uppercase;
}
.maturity-readiness-area {
display: grid;
gap: 3px;
min-width: 0;
}
.maturity-readiness-title {
overflow-wrap: anywhere;
font-weight: 700;
}
.maturity-readiness-status {
font-size: 10px;
opacity: 0.62;
}
.maturity-readiness-status-ready {
color: #4ca574;
}
.maturity-readiness-status-partially-reviewed {
color: #d39a4b;
}
.maturity-readiness-status-needs-review {
color: #dc7669;
}
.maturity-category-list {
display: grid;
width: 100%;
margin-top: 4px;
overflow: hidden;
}
.maturity-category-row {
display: grid;
grid-template-columns: minmax(180px, 1.55fr) repeat(3, minmax(100px, 1fr)) minmax(140px, 1.2fr);
gap: 12px;
align-items: center;
padding: 12px 0;
border-top: 1px solid color-mix(in oklab, rgb(var(--primary)) 11%, transparent);
font-size: 11px;
}
.maturity-category-row-header {
padding: 8px 0;
border-top: 0;
color: inherit;
font-size: 10px;
font-weight: 750;
letter-spacing: 0.04em;
opacity: 0.56;
text-transform: uppercase;
}
.maturity-category-area {
display: grid;
gap: 3px;
min-width: 0;
}
.maturity-category-title {
overflow-wrap: anywhere;
font-weight: 700;
}
.maturity-category-area > span:last-child {
font-size: 10px;
opacity: 0.62;
}
.maturity-category-docs {
min-width: 0;
overflow-wrap: anywhere;
line-height: 1.4;
}
.maturity-category-docs a {
text-decoration: none;
}
.maturity-category-docs a:hover {
text-decoration: underline;
text-underline-offset: 3px;
}
.maturity-level-list {
display: grid;
margin: 12px 0 28px;
border-top: 1px solid color-mix(in oklab, rgb(var(--primary)) 16%, transparent);
border-bottom: 1px solid color-mix(in oklab, rgb(var(--primary)) 16%, transparent);
}
.maturity-level-row {
display: grid;
grid-template-columns: minmax(130px, 0.32fr) minmax(0, 1fr);
gap: 4px 14px;
padding: 13px 0;
border-top: 1px solid color-mix(in oklab, rgb(var(--primary)) 11%, transparent);
}
.maturity-level-row:first-child {
border-top: 0;
}
.maturity-level-title {
grid-row: span 2;
font-size: 13px;
font-weight: 700;
}
.maturity-level-title .maturity-level-pill {
opacity: 1;
}
.maturity-level-row span,
.maturity-level-promotion {
font-size: 12px;
line-height: 1.45;
opacity: 0.68;
}
.maturity-surface-link {
display: grid;
gap: 3px;
margin: 0;
padding: 11px 0;
border-bottom: 1px solid color-mix(in oklab, currentColor 14%, transparent);
text-decoration: none;
}
.maturity-surface-link:hover {
color: rgb(var(--primary));
}
.maturity-surface-link .maturity-surface-title {
font-size: 13px;
font-weight: 700;
}
.maturity-surface-link > .maturity-surface-meta {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 6px;
}
.maturity-surface-link > .maturity-surface-meta > span:not(.maturity-level-pill) {
font-size: 11px;
opacity: 0.68;
}
.maturity-surface-link > .maturity-surface-meta .maturity-level-pill {
opacity: 1;
}
.maturity-surface-rollup {
display: flex;
flex-wrap: wrap;
gap: 5px 14px;
margin: 0 0 14px;
padding: 9px 0;
border-top: 1px solid color-mix(in oklab, rgb(var(--primary)) 13%, transparent);
border-bottom: 1px solid color-mix(in oklab, rgb(var(--primary)) 13%, transparent);
}
.maturity-surface-rollup > span {
color: inherit;
font-size: 11px;
font-weight: 700;
}
#content table .maturity-score,
#content table .maturity-lts {
white-space: nowrap;
}
@media (max-width: 960px) {
.maturity-summary-grid,
.maturity-evidence-grid {
grid-template-columns: 1fr;
}
.maturity-summary-item,
.maturity-summary-item + .maturity-summary-item,
.maturity-evidence-card,
.maturity-evidence-card + .maturity-evidence-card {
padding: 14px 0;
border-left: 0;
}
.maturity-summary-item + .maturity-summary-item,
.maturity-evidence-card + .maturity-evidence-card {
border-top: 1px solid color-mix(in oklab, rgb(var(--primary)) 12%, transparent);
}
.maturity-surface-row {
grid-template-columns: minmax(160px, 1.35fr) repeat(3, minmax(98px, 1fr)) minmax(70px, 0.5fr);
gap: 8px;
}
.maturity-category-row {
grid-template-columns: minmax(160px, 1.35fr) repeat(3, minmax(86px, 1fr)) minmax(110px, 1fr);
gap: 8px;
}
}
@media (max-width: 640px) {
.maturity-hero {
padding-left: 14px;
}
.maturity-surface-row-header {
display: none;
}
.maturity-surface-row {
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
gap: 9px 12px;
}
.maturity-surface-metric {
display: grid;
grid-template-columns: auto 1fr;
align-items: center;
gap: 6px;
}
.maturity-surface-metric-label {
display: block;
}
.maturity-surface-support {
justify-self: end;
}
.maturity-readiness-row,
.maturity-category-row {
grid-template-columns: 1fr;
gap: 5px;
}
.maturity-readiness-row-header,
.maturity-category-row-header {
display: none;
}
.maturity-category-docs {
padding-top: 3px;
}
.maturity-band-list {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.maturity-band + .maturity-band {
padding-left: 0;
border-left: 0;
}
.maturity-level-row {
grid-template-columns: 1fr;
}
.maturity-level-title {
grid-row: auto;
}
}

View File

@@ -225,7 +225,7 @@ See [Skill Workshop](/tools/skill-workshop) for the full proposal lifecycle.
metadata:
```bash
openclaw skills install clawhub-publish
openclaw skills install @openclaw/clawhub-publish
```
</Step>

View File

@@ -92,6 +92,8 @@ Notes:
- Host execution (`gateway`/`node`) rejects `env.PATH` and loader overrides (`LD_*`/`DYLD_*`) to
prevent binary hijacking or injected code.
- OpenClaw sets `OPENCLAW_SHELL=exec` in the spawned command environment (including PTY and sandbox execution) so shell/profile rules can detect exec-tool context.
- For channel-origin runs, OpenClaw also exposes a narrow sender/chat identity JSON payload in
`OPENCLAW_CHANNEL_CONTEXT` when the channel provided those ids.
- `openclaw channels login` is blocked from `exec` because it is an interactive channel-auth flow; run it in a terminal on the gateway host, or use the channel-native login tool from chat when one exists.
- Important: sandboxing is **off by default**. If sandboxing is off, implicit `host=auto`
resolves to `gateway`. Explicit `host=sandbox` still fails closed instead of silently

View File

@@ -88,8 +88,9 @@ still returns one synthesized answer with citations rather than an N-result
list.
`freshness` accepts `day`, `week`, `month`, `year`, and the shared shortcuts
`pd`, `pw`, `pm`, and `py`. OpenClaw converts these values, or an explicit
`date_after`/`date_before` range, into Gemini Google Search grounding's
`pd`, `pw`, `pm`, and `py`. `day`/`pd` adds a recency instruction to the Gemini
query instead of a hard 24-hour range. `week`, `month`, `year`, and explicit
`date_after`/`date_before` ranges set Gemini Google Search grounding's
`timeRangeFilter`. `country`, `language`, and `domain_filter` are not supported.
## Model selection

View File

@@ -207,6 +207,19 @@ Key policy rules:
`codex` plugin owns Codex app-server runtime for canonical `openai/*` agent
refs, explicit `agentRuntime.id: "codex"`, and legacy `codex/*` refs.
When `plugins.allow` is unset and non-bundled plugins are auto-discovered from
the workspace or global plugin roots, startup logs
`plugins.allow is empty; discovered non-bundled plugins may auto-load: ...`.
The warning includes discovered plugin ids and, for short lists, a minimal
`plugins.allow` snippet. Run
[`openclaw plugins list --enabled --verbose`](/cli/plugins#list) or
[`openclaw plugins inspect <id>`](/cli/plugins#inspect) with the listed plugin
id before copying trusted plugins into `openclaw.json`. The same trust-pinning
guidance applies when diagnostics say a plugin loaded
`without install/load-path provenance`: inspect that plugin id, then pin the
trusted id in `plugins.allow` or reinstall from a trusted source so OpenClaw
records install provenance.
Run `openclaw doctor` or `openclaw doctor --fix` when config validation reports
stale plugin ids, allowlist/tool mismatches, or legacy bundled plugin paths.

View File

@@ -145,12 +145,12 @@ publish and sync.
| Action | Command |
| ---------------------------------- | ------------------------------------------------------ |
| Install a skill into the workspace | `openclaw skills install <slug>` |
| Install a skill into the workspace | `openclaw skills install @owner/<slug>` |
| Install from a Git repository | `openclaw skills install git:owner/repo@ref` |
| Install a local skill directory | `openclaw skills install ./path/to/skill --as my-tool` |
| Install for all local agents | `openclaw skills install <slug> --global` |
| Install for all local agents | `openclaw skills install @owner/<slug> --global` |
| Update all workspace skills | `openclaw skills update --all` |
| Update a shared managed skill | `openclaw skills update <slug> --global` |
| Update a shared managed skill | `openclaw skills update @owner/<slug> --global` |
| Update all shared managed skills | `openclaw skills update --all --global` |
| Verify a skill's trust envelope | `openclaw skills verify <slug>` |
| Print the generated Skill Card | `openclaw skills verify <slug> --card` |
@@ -179,7 +179,7 @@ publish and sync.
with detail pages for VirusTotal, ClawScan, and static analysis. The
command exits non-zero when ClawHub marks verification as failed. Publishers
recover false positives through the ClawHub dashboard or
`clawhub skill rescan <slug>`.
`clawhub skill rescan @owner/<slug>`.
</Accordion>
<Accordion title="Private archive installs">

View File

@@ -389,8 +389,8 @@ show the `x_search` prompt.
freshness ranges require both start and end dates.
Gemini, Grok, and Kimi return one synthesized answer with citations. They
accept `count` for shared-tool compatibility, but it does not change the
grounded answer shape. Gemini supports `freshness`, `date_after`, and
`date_before` by converting them to Google Search grounding time ranges.
grounded answer shape. Gemini treats `day` freshness as a recency hint; wider
freshness values and explicit dates set Google Search grounding time ranges.
Perplexity behaves the same way when you use the Sonar/OpenRouter
compatibility path (`plugins.entries.perplexity.config.webSearch.baseUrl` /
`model` or `OPENROUTER_API_KEY`).

View File

@@ -10,7 +10,7 @@
"dependencies": {
"@agentclientprotocol/claude-agent-acp": "0.39.0",
"@zed-industries/codex-acp": "0.15.0",
"acpx": "0.10.0",
"acpx": "0.11.2",
"zod": "4.4.3"
}
},
@@ -196,9 +196,9 @@
}
},
"node_modules/@clack/core": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/@clack/core/-/core-1.4.1.tgz",
"integrity": "sha512-FILJa1gGKEFTGZAJE9RpVhrjKz3c3h4ar60dSv6cGuDqufQ84YEIS3GAGvZiN+H6yaLbbvTFNejjCC4tXpZEuw==",
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@clack/core/-/core-1.3.1.tgz",
"integrity": "sha512-fT1qHVGAag4IEkrupZ6lRRbNCs1vS9P01KB/sG8zKgvUztbYtFBtQpjSITNwooDZ83tpsPzP0mRNs1/KVszCRA==",
"license": "MIT",
"dependencies": {
"fast-wrap-ansi": "^0.2.0",
@@ -209,12 +209,12 @@
}
},
"node_modules/@clack/prompts": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-1.5.1.tgz",
"integrity": "sha512-zccHj2z2oCCO4yrDiRSlFOxWerGqRiysP7a5jPK6uoI9URKAquwY42Dd/iUP8JWHxEzdRe4TlbvZCo8z1/mhrw==",
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-1.4.0.tgz",
"integrity": "sha512-S0My7XPGIgpRWMDG8uRqalbgT+a6FmCUdOW+HaIOVVpUPHOb7RrpvjTjiODadKp06fsrVDJZlIzc6yCTp4AnxA==",
"license": "MIT",
"dependencies": {
"@clack/core": "1.4.1",
"@clack/core": "1.3.1",
"fast-string-width": "^3.0.2",
"fast-wrap-ansi": "^0.2.0",
"sisteransi": "^1.0.5"
@@ -701,6 +701,7 @@
"version": "0.15.0",
"resolved": "https://registry.npmjs.org/@zed-industries/codex-acp/-/codex-acp-0.15.0.tgz",
"integrity": "sha512-eAv7sGBeiYrYkOulF729nrM51szS7WIhBtugRj5wWq6csRKZUhAZfoUZlF8xUWdHPtOIzd/eT6MNG6gMHu6z0w==",
"deprecated": "This package has been replaced by @agentclientprotocol/codex-acp. Please migrate to continue receiving updates.",
"license": "Apache-2.0",
"bin": {
"codex-acp": "bin/codex-acp.js"
@@ -721,6 +722,7 @@
"cpu": [
"arm64"
],
"deprecated": "This package has been replaced by @agentclientprotocol/codex-acp. Please migrate to continue receiving updates.",
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -737,6 +739,7 @@
"cpu": [
"x64"
],
"deprecated": "This package has been replaced by @agentclientprotocol/codex-acp. Please migrate to continue receiving updates.",
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -753,6 +756,7 @@
"cpu": [
"arm64"
],
"deprecated": "This package has been replaced by @agentclientprotocol/codex-acp. Please migrate to continue receiving updates.",
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -769,6 +773,7 @@
"cpu": [
"x64"
],
"deprecated": "This package has been replaced by @agentclientprotocol/codex-acp. Please migrate to continue receiving updates.",
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -785,6 +790,7 @@
"cpu": [
"arm64"
],
"deprecated": "This package has been replaced by @agentclientprotocol/codex-acp. Please migrate to continue receiving updates.",
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -801,6 +807,7 @@
"cpu": [
"x64"
],
"deprecated": "This package has been replaced by @agentclientprotocol/codex-acp. Please migrate to continue receiving updates.",
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -824,15 +831,15 @@
}
},
"node_modules/acpx": {
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/acpx/-/acpx-0.10.0.tgz",
"integrity": "sha512-hd48XV03gG3sd409T1lDrOKJTTz1ap4g0wrndXjxQ590tN85pBYlvfNLyerybvGRrtUGsZjNdt99r1jpIt6ukA==",
"version": "0.11.2",
"resolved": "https://registry.npmjs.org/acpx/-/acpx-0.11.2.tgz",
"integrity": "sha512-ksTmfJDVqUAJJXsNDamEno03AMZ/aAZzXk/h5nt61VsLc/jcpoDMfCVpErzuYNJjwCd0V6Zm5o6F8OoqxsjQWA==",
"license": "MIT",
"dependencies": {
"@agentclientprotocol/sdk": "^0.22.1",
"commander": "^14.0.3",
"skillflag": "^0.1.4",
"tsx": "^4.22.0",
"@agentclientprotocol/sdk": "^0.28.1",
"commander": "^15.0.0",
"skillflag": "^0.2.0",
"tsx": "^4.22.4",
"zod": "^4.4.3"
},
"bin": {
@@ -842,6 +849,15 @@
"node": ">=22.13.0"
}
},
"node_modules/acpx/node_modules/@agentclientprotocol/sdk": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@agentclientprotocol/sdk/-/sdk-0.28.1.tgz",
"integrity": "sha512-Z2Frs6YtPhnZZ+XwFXyQkRDXY0fn8FjCalEs0W4yUhQnY4TztmNq0/RnfzWdFN3vqT3h0jTz5klzYbZHGxCDyQ==",
"license": "Apache-2.0",
"peerDependencies": {
"zod": "^3.25.0 || ^4.0.0"
}
},
"node_modules/ajv": {
"version": "8.20.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
@@ -1043,12 +1059,12 @@
}
},
"node_modules/commander": {
"version": "14.0.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz",
"integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==",
"version": "15.0.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-15.0.0.tgz",
"integrity": "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==",
"license": "MIT",
"engines": {
"node": ">=20"
"node": ">=22.12.0"
}
},
"node_modules/content-disposition": {
@@ -2045,9 +2061,9 @@
"license": "MIT"
},
"node_modules/skillflag": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/skillflag/-/skillflag-0.1.4.tgz",
"integrity": "sha512-egFg+XCF5sloOWdtzxZivTX7n4UDj5pxQoY33wbT8h+YSDjMQJ76MZUg2rXQIBXmIDtlZhLgirS1g/3R5/qaHA==",
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/skillflag/-/skillflag-0.2.0.tgz",
"integrity": "sha512-7ZmEpBeEoPLc+hqZ/StAnCO/hulgEPANzPyZgOM/CZ5zc3b0ApSp3URavY5POM/OKyi5d9+UC/Q21OoiYC2kJw==",
"license": "MIT",
"dependencies": {
"@clack/prompts": "^1.0.1",

View File

@@ -10,7 +10,7 @@
"dependencies": {
"@agentclientprotocol/claude-agent-acp": "0.39.0",
"@zed-industries/codex-acp": "0.15.0",
"acpx": "0.10.0",
"acpx": "0.11.2",
"zod": "4.4.3"
},
"devDependencies": {

View File

@@ -251,6 +251,15 @@ describe("prepareAcpxCodexAuthConfig", () => {
expect(wrapper).not.toMatch(
/forceKillTimer = setTimeout\(\(\) => killChildTree\("SIGKILL"\), 1_500\);\s*forceKillTimer\.unref\?\.\(\);\s*process\.exit\(1\);/s,
);
// Orphan detection must trigger on any PPID change, not only when the new
// PPID is init (1). Systemd user services and container init reparent
// orphaned processes to a session manager or container init (PID != 1),
// and the older `process.ppid !== 1` guard would silently leak the codex
// adapter tree there.
expect(wrapper).not.toContain("process.ppid !== 1");
expect(wrapper).toMatch(
/setInterval\(\(\) => \{[\s\S]*?if \(process\.ppid === originalParentPid\) \{\s*return;\s*\}/,
);
});
it("uses the bundled Claude ACP dependency by default when it is installed", async () => {

View File

@@ -475,7 +475,13 @@ const parentWatcher =
process.platform === "win32"
? undefined
: setInterval(() => {
if (process.ppid === originalParentPid || process.ppid !== 1) {
// Orphan detection: parent PID changed means our original parent died.
// The new parent could be PID 1 (init) on bare-metal hosts, OR a
// systemd user-session manager, OR a container init, OR a session
// leader — depending on environment. Previously this only triggered
// on PPID == 1, which missed all systemd-managed deployments and
// leaked codex-acp adapter trees on every gateway restart.
if (process.ppid === originalParentPid) {
return;
}
if (orphanCleanupStarted) {

View File

@@ -2,6 +2,7 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { RequestedModelUnsupportedError } from "acpx/runtime";
import { beforeEach, describe, expect, it, vi } from "vitest";
import {
AcpRuntimeError,
@@ -708,6 +709,100 @@ describe("AcpxRuntime fresh reset wrapper", () => {
});
});
it("retries without a model when ACPX reports missing model capability", async () => {
const baseStore: TestSessionStore = {
load: vi.fn(async () => undefined),
save: vi.fn(async () => {}),
};
const { runtime, delegate } = makeRuntime(baseStore, {
agentRegistry: {
resolve: (agentName: string) => (agentName === "opencode" ? "opencode acp" : agentName),
list: () => ["opencode"],
},
});
const ensure = vi
.spyOn(delegate, "ensureSession")
.mockRejectedValueOnce(
new RequestedModelUnsupportedError(
"Cannot apply --model: the ACP agent did not advertise model support",
"missing-capability",
),
)
.mockResolvedValueOnce({
sessionKey: "agent:opencode:acp:test",
backend: "acpx",
runtimeSessionName: "opencode",
});
await runtime.ensureSession({
sessionKey: "agent:opencode:acp:test",
agent: "opencode",
mode: "persistent",
model: "openrouter/owl-alpha",
});
expect(ensure).toHaveBeenCalledTimes(2);
expect(readFirstEnsureSessionInput(ensure)).toMatchObject({
model: "openrouter/owl-alpha",
sessionOptions: { model: "openrouter/owl-alpha" },
});
const [, secondCall] = ensure.mock.calls;
expect(secondCall?.[0]).not.toHaveProperty("sessionOptions");
expect((secondCall?.[0] as { model?: string } | undefined)?.model).toBeUndefined();
});
it("does not retry when ACPX rejects an explicitly unsupported model id", async () => {
const baseStore: TestSessionStore = {
load: vi.fn(async () => undefined),
save: vi.fn(async () => {}),
};
const { runtime, delegate } = makeRuntime(baseStore, {
agentRegistry: {
resolve: (agentName: string) => (agentName === "opencode" ? "opencode acp" : agentName),
list: () => ["opencode"],
},
});
const ensure = vi
.spyOn(delegate, "ensureSession")
.mockRejectedValueOnce(
new RequestedModelUnsupportedError(
"Cannot apply --model: the ACP agent did not advertise that model",
"unadvertised-model",
),
);
await expect(
runtime.ensureSession({
sessionKey: "agent:opencode:acp:test",
agent: "opencode",
mode: "persistent",
model: "unknown/model",
}),
).rejects.toThrow("did not advertise that model");
expect(ensure).toHaveBeenCalledTimes(1);
});
it("does not retry an unrelated error with similar wording", async () => {
const baseStore: TestSessionStore = {
load: vi.fn(async () => undefined),
save: vi.fn(async () => {}),
};
const { runtime, delegate } = makeRuntime(baseStore);
const ensure = vi
.spyOn(delegate, "ensureSession")
.mockRejectedValueOnce(new Error("the ACP agent did not advertise model support"));
await expect(
runtime.ensureSession({
sessionKey: "agent:main:acp:test",
agent: "main",
mode: "persistent",
model: "openrouter/owl-alpha",
}),
).rejects.toThrow("did not advertise model support");
expect(ensure).toHaveBeenCalledTimes(1);
});
it("injects Codex ACP startup config into the scoped registry", () => {
expect(testing.isCodexAcpCommand(CODEX_ACP_COMMAND)).toBe(true);
expect(testing.isCodexAcpCommand(CODEX_ACP_WRAPPER_COMMAND)).toBe(true);

View File

@@ -13,6 +13,7 @@ import {
createFileSessionStore,
decodeAcpxRuntimeHandleState,
encodeAcpxRuntimeHandleState,
isRequestedModelUnsupportedError,
type AcpAgentRegistry,
type AcpRuntimeDoctorReport,
type AcpRuntimeEvent,
@@ -586,6 +587,26 @@ function withAcpxSessionOptions(input: OpenClawRuntimeEnsureInput): AcpxDelegate
} as AcpxDelegateEnsureInput;
}
function isAcpModelCapabilityMissingError(error: unknown): boolean {
return isRequestedModelUnsupportedError(error) && error.reason === "missing-capability";
}
// ACPX owns the distinction between missing model capability and an invalid model id.
// Retry only the former so explicit model mistakes remain visible to the caller.
async function ensureDelegateSessionWithModelFallback(
delegate: BaseAcpxRuntime,
input: OpenClawRuntimeEnsureInput,
): Promise<AcpRuntimeHandle> {
try {
return await delegate.ensureSession(withAcpxSessionOptions(input));
} catch (error) {
if (!input.model || !isAcpModelCapabilityMissingError(error)) {
throw error;
}
return await delegate.ensureSession(withAcpxSessionOptions({ ...input, model: undefined }));
}
}
function quoteShellArg(value: string): string {
if (/^[A-Za-z0-9_./:=@+-]+$/.test(value)) {
return value;
@@ -989,7 +1010,7 @@ export class AcpxRuntime implements AcpRuntime {
this.withCodexWrapperDiagnostics({
command: stableLaunchCommand,
fallbackCode: "ACP_SESSION_INIT_FAILED",
run: () => delegate.ensureSession(withAcpxSessionOptions(ensureInput)),
run: () => ensureDelegateSessionWithModelFallback(delegate, ensureInput),
}),
});
}

View File

@@ -1,5 +1,6 @@
{
"id": "alibaba",
"icon": "https://cdn.simpleicons.org/alibabacloud/111111",
"activation": {
"onStartup": false
},

View File

@@ -2,6 +2,7 @@
"id": "anthropic-vertex",
"name": "Anthropic Vertex",
"description": "OpenClaw Anthropic Vertex provider plugin for Claude models on Google Vertex AI.",
"icon": "https://cdn.simpleicons.org/anthropic/111111",
"activation": {
"onStartup": false
},

View File

@@ -1,5 +1,6 @@
{
"id": "anthropic",
"icon": "https://cdn.simpleicons.org/anthropic/111111",
"activation": {
"onStartup": false
},

Some files were not shown because too many files have changed in this diff Show More