Compare commits

...

108 Commits

Author SHA1 Message Date
Peter Steinberger
2cdd69a303 test(plugins): fix scoped config fixture types 2026-05-13 18:28:23 +01:00
Peter Steinberger
f084cf2335 fix(plugins): keep scoped config compat guard-clean 2026-05-13 18:21:25 +01:00
Peter Steinberger
b6c34ab055 fix(plugins): satisfy release lint 2026-05-13 18:12:39 +01:00
Peter Steinberger
95582f03de fix(agents): wire subagent announce gateway dependency 2026-05-13 18:03:37 +01:00
Peter Steinberger
d3950fbd64 chore(canvas): refresh a2ui bundle hash 2026-05-13 16:32:19 +01:00
Peter Steinberger
2906fb833f chore(release): refresh plugin sdk api baseline 2026-05-13 16:30:43 +01:00
Peter Steinberger
a6d878376b fix(tui): emit v4 embedded chat deltas 2026-05-13 16:27:38 +01:00
Peter Steinberger
38cb3d0041 docs(changelog): note v4 chat delta protocol 2026-05-13 16:22:41 +01:00
Peter Steinberger
bdbc224841 fix(gateway): avoid duplicate v4 deltas 2026-05-13 16:22:20 +01:00
Peter Steinberger
02ae92cade fix(gateway): require v4 chat deltas 2026-05-13 16:22:20 +01:00
samzong
9375a72d56 fix(sdk): preserve replayed chat snapshots 2026-05-13 16:22:20 +01:00
samzong
7e8b2d48ce fix(gateway): add incremental chat delta payloads 2026-05-13 16:22:20 +01:00
Vincent Koc
a56727d8fe fix(plugins): prune managed peers on uninstall
(cherry picked from commit 2a67a7f65e)
2026-05-13 22:54:31 +08:00
Peter Steinberger
4165893843 fix(plugins): attribute runtime config deprecations (#81425) (thanks @BKF-Gitty)
Co-authored-by: BKF-Gitty <bandark@mac.com>
2026-05-13 15:49:36 +01:00
Shakker
bf0a5fe5dc fix: preserve owned plugin dependencies during peer repair 2026-05-13 15:33:18 +01:00
Shakker
033d74ce2e fix: harden managed plugin peer recovery 2026-05-13 15:33:18 +01:00
Shakker
d0796a9b13 fix: avoid rescanning repaired plugin peers 2026-05-13 15:33:18 +01:00
Shakker
1b572f2fe2 fix: preserve managed plugin peer dependencies 2026-05-13 15:33:18 +01:00
Pavan Kumar Gondhi
1415c06fc4 fix(plugins): scan installed dependency runtime code [AI] (#81066)
* fix: scan installed plugin dependency code

* addressing review-skill

* addressing review-skill

* addressing codex review

* addressing codex review

* addressing codex review

* addressing codex review

* addressing codex review

* addressing codex review

* addressing codex review

* addressing codex review

* addressing ci

* addressing ci

* docs: add changelog entry for PR merge
2026-05-13 15:32:35 +01:00
Pavan Kumar Gondhi
8b840b28e5 fix: scan plugin runtime entries during install [AI] (#80998)
* fix: scan plugin runtime entries during install

* addressing review-skill

* addressing claude review

* docs: add changelog entry for PR merge
2026-05-13 15:31:34 +01:00
Peter Steinberger
c949a35534 test: keep release queue settings expectations 2026-05-13 14:45:50 +01:00
Peter Steinberger
159d6a610b test: fix queue settings session fixtures 2026-05-13 14:44:23 +01:00
Peter Steinberger
2d088b2c55 test: add live subagent steering proof 2026-05-13 14:43:42 +01:00
Peter Steinberger
e0c744869c fix: use in-process subagent announce handoff 2026-05-13 14:42:47 +01:00
brokemac79
d513271bb6 fix(channel): refresh wecom onboarding install 2026-05-13 14:25:15 +01:00
Peter Steinberger
0f24a78b24 fix(channel): refresh wecom onboarding install (#80390) (thanks @brokemac79) 2026-05-13 14:07:05 +01:00
Peter Steinberger
70bce0d9ed style(agents): format array schema normalization 2026-05-13 14:05:41 +01:00
Peter Steinberger
a50d65f63a fix(config): normalize gemini subagent model writes 2026-05-13 14:04:22 +01:00
Peter Steinberger
6a64f05bcc fix(cli): normalize Gemini config mutation refs 2026-05-13 14:04:17 +01:00
Peter Steinberger
32487b4906 fix(config): normalize per-agent Gemini preview refs 2026-05-13 14:04:11 +01:00
JARVIS-Glasses
798ba972ea fix(agents): normalize array tool schemas 2026-05-13 14:03:34 +01:00
Sarah Fortune
a8f03295c4 fix(plugins): raise default install scan file limit to 25k (#81361) 2026-05-13 14:00:29 +01:00
Jason
552c575a5c Allow pnpm source updates to build OpenClaw (#81294)
Merged via squash.

Prepared head SHA: 4815d5a8c9
Co-authored-by: fuller-stack-dev <263060202+fuller-stack-dev@users.noreply.github.com>
Co-authored-by: altaywtf <9790196+altaywtf@users.noreply.github.com>
Reviewed-by: @altaywtf
2026-05-13 13:59:54 +01:00
Peter Steinberger
f96e0ff23c fix(installer): honor git install versions 2026-05-13 13:59:25 +01:00
Gio Della-Libera
2873631873 fix(update): suppress handoff newer-config warning (#81235)
Merged via squash.

Prepared head SHA: 61a5c975bf
Co-authored-by: giodl73-repo <giodl@microsoft.com>
Co-authored-by: galiniliev <5711535+galiniliev@users.noreply.github.com>
Reviewed-by: @galiniliev
2026-05-13 13:48:17 +01:00
Vincent Koc
9ca2f075c4 revert(cli): remove global root refusal (#81370) 2026-05-13 11:37:33 +01:00
Peter Steinberger
6a8dac800d build(whatsapp): keep audio deps external 2026-05-13 11:27:59 +01:00
Peter Steinberger
e427846225 build(pnpm): restore exotic subdependency blocking 2026-05-13 11:27:52 +01:00
Peter Steinberger
a57b646af4 build(whatsapp): externalize whatsapp plugin 2026-05-13 11:27:28 +01:00
Peter Steinberger
0e324f6d2b build(pnpm): restore exotic subdependency blocking 2026-05-13 11:06:03 +01:00
Sarah Fortune
3c62ebddb6 chore(release): bump beta 5 2026-05-13 01:07:10 -07:00
Peter Steinberger
e88f8476c1 ci(release): extend npm telegram e2e timeout 2026-05-13 08:58:37 +01:00
Sarah Fortune
daff6d8797 cherry-pick #81219 onto release/2026.5.12 for beta.5 (#81327)
* feat(migrate): suppress plan log on embedding + add "Accept recommended" affordance (#81219)

Two related improvements to the interactive `openclaw migrate <provider>`
flow, both surfaced by the onboarding post-install migration prompt that
landed in #81192.

1. `suppressPlanLog?: boolean` on `MigrateCommonOptions`
   (`src/commands/migrate/types.ts`). When set, `migratePlanCommand`
   skips the up-front `runtime.log(formatMigrationPlan(plan))` dump.
   The interactive Codex selection picker and the "Apply this migration
   now?" confirm still run. Wired from the wizard helper at
   `src/wizard/setup.post-install-migration.ts` so that path no longer
   shows the plan dump after the user has already confirmed at the
   wizard prompt.

2. New "Accept recommended" sentinel row at the top of both Codex
   selection pickers, with "Toggle all on" and "Toggle all off" moved
   to the bottom. The cursor starts on "Accept recommended" so pressing
   Enter at the default position submits the picker's `initialValues`
   (the recommended set) — matching the visual state of the checkboxes.

   Implemented in `skill-selection-prompt.ts`:
   - Enter on the Accept sentinel sets `prompt.value` to
     `opts.initialValues` and lets clack submit.
   - Space on the Accept sentinel snaps `prompt.value` to
     `opts.initialValues` so the visible checkboxes flip to the
     recommended state. The user can then Enter to commit or continue
     toggling individual rows. The Accept row itself is never persisted
     in the submitted value list.

   The existing Enter handler for "Toggle all on" / "Toggle all off"
   stays unchanged.

3. Removed the "Skip for now" sentinel entirely. It was a single-
   keystroke trap: with the picker cursor wrapping from Accept to Skip
   via up-arrow (or via accidental down-arrows), Enter on Skip wiped
   `prompt.value` to `[MIGRATION_SELECTION_SKIP]` and abandoned the
   whole migration — including any items the user had already
   confirmed in the previous picker. To exit without migrating, users
   now navigate to "Toggle all off" (or use the `a` / `i` keyboard
   shortcuts) to clear the selection; the apply phase then sees no
   planned work and skips itself via the existing
   `shouldSkipCodexApplyAfterInteractiveSelection` path.

   Cleanup spans `migrate/selection.ts` (constants, `{ action: "skip" }`
   variant, and the reconcile/resolve SKIP branches),
   `migrate.ts` (the picker option rows and the
   `if (selection.action === "skip")` handler blocks in both pickers),
   and the corresponding tests.

4. Plugin selection hint relabelled from "Activate every recommended
   plugin" to "Migrate every recommended plugin" so it matches the
   skill hint and the prompt's own verb ("Migrate ... into this agent
   now?").

Tests:

- `src/commands/migrate/skill-selection-prompt.test.ts` — Accept
  sentinel cases (Enter and Space + Enter both submit initialValues);
  Skip-related test removed; Skip row dropped from the picker fixture.
- `src/commands/migrate/selection.test.ts` — Skip-related sub-
  assertions trimmed from the resolve/reconcile tests; the
  "skip + toggle-off precedence" test renamed to "toggle-off precedence
  over toggle-on" and Skip cases removed.
- `src/commands/migrate.test.ts` — four Skip-driven scenarios removed
  (plugin-only skip, both-pickers skip, skip-skills-continue-to-plugins,
  Codex subscription warning + skip).
- `src/wizard/setup.post-install-migration.test.ts` — call-args
  assertion expects the new `suppressPlanLog` option.

Verification:

- `pnpm lint` clean
- `pnpm tsgo:core` + `pnpm tsgo:core:test` clean
- Touched test suites green (migrate 32/32, selection 17/17,
  skill-selection-prompt 6/6, setup.post-install-migration 10/10).

(cherry picked from commit a197e31abb)

* test(migrate): reconcile beta selection tests
2026-05-13 00:52:36 -07:00
Peter Steinberger
6926481646 test(ci): update install smoke pull retry guard 2026-05-13 07:46:54 +01:00
Peter Steinberger
55b7d0c181 test(docker): stabilize update channel fixture install 2026-05-13 07:39:49 +01:00
Peter Steinberger
05b23f8fb8 ci(release): retry root smoke image pulls 2026-05-13 07:22:27 +01:00
Peter Steinberger
b27f0899e4 test(telegram): stabilize topic serialization test 2026-05-13 07:04:25 +01:00
Peter Steinberger
d1246255d0 chore(release): bump beta 4 2026-05-13 05:52:14 +01:00
Peter Steinberger
bdbdd17b90 build(canvas): refresh a2ui bundle hash 2026-05-13 05:52:10 +01:00
Peter Steinberger
4acf72cf03 fix(plugins): recognize managed codex runtime package 2026-05-13 05:44:39 +01:00
Peter Steinberger
e126e74a58 test(models): restore provider catalog discovery helper 2026-05-13 05:43:44 +01:00
rendrag-git
a2abb63f6e fix: resolve custom provider env markers
(cherry picked from commit 8831754f5c)
2026-05-13 05:42:42 +01:00
rendrag-git
a25650e044 fix: list self-hosted runtime wildcard models
(cherry picked from commit 2269ec727f)
2026-05-13 05:42:42 +01:00
rendrag-git
13c44a2432 fix: discover self-hosted provider wildcards
(cherry picked from commit 3b361cf51c)
2026-05-13 05:42:42 +01:00
pashpashpash
1ffb4126dc Trust installed Codex for its private task runtime (#81206)
* fix(codex): trust installed codex task runtime

* fix(codex): keep private runtime alias packaged

(cherry picked from commit 3688c47f1f)
2026-05-13 05:42:14 +01:00
Peter Steinberger
2b7e1ab1e4 fix: normalize per-agent gemini config refs
(cherry picked from commit 38bab38ce7)
2026-05-13 05:41:20 +01:00
Peter Steinberger
738a8f574e fix: normalize nested gemini preview config ids
(cherry picked from commit 9147a53274)
2026-05-13 05:41:20 +01:00
pashpashpash
410e009d54 Keep Codex media tools backed by auth profiles (#81059)
* fix(codex): pass auth profiles to dynamic tools

* fix: bump protobufjs past advisory range

(cherry picked from commit 36b9da5c91)
2026-05-13 05:40:50 +01:00
Peter Steinberger
80c9015b56 fix: canonicalize qualified gemini pro preview refs
(cherry picked from commit fad2484477)
2026-05-13 05:40:03 +01:00
Peter Steinberger
c612dd9529 fix: normalize oauth auth-result config patches
(cherry picked from commit 88714d6803)
2026-05-13 05:40:03 +01:00
Super Zheng
a97399a25c fix: enable native require fast path on Windows for plugin-sdk root alias (#80878)
Merged via squash.

Prepared head SHA: 87446445b6
Co-authored-by: medns <1575008+medns@users.noreply.github.com>
Co-authored-by: odysseus0 <8635094+odysseus0@users.noreply.github.com>
Reviewed-by: @odysseus0

(cherry picked from commit 4223dd2886)
2026-05-13 05:40:03 +01:00
pashpashpash
87d6b868b7 docs(changelog): note telegram html rendering backport 2026-05-12 20:57:43 -07:00
Ayaan Zaidi
e309a8732e fix(telegram): simplify context boundary plumbing
(cherry picked from commit 8c40481076)
2026-05-12 20:43:25 -07:00
VACInc
b28a70369d fix telegram context session start boundary
(cherry picked from commit 6648b20b65)
2026-05-12 20:43:25 -07:00
VACInc
c5140a09e1 fix telegram context reset boundary
(cherry picked from commit 991f89af04)
2026-05-12 20:43:25 -07:00
Ayaan Zaidi
3c3cef1785 fix(telegram): keep durable html unsanitized
(cherry picked from commit 78b57ae201)
2026-05-12 20:43:17 -07:00
Ayaan Zaidi
7c606f834c fix(telegram): preserve supported html replies
(cherry picked from commit 716a4cf412)
2026-05-12 20:43:17 -07:00
WhatsSkiLL
fec979cf96 fix(gateway): clarify invalid config recovery hints
Closes #40652.

Thanks @JARVIS-Glasses.

(cherry picked from commit e0f6f78b02)
2026-05-12 20:27:41 -07:00
Marcus Castro
b258035a8d fix(whatsapp): drain debounced inbound before close (#81246)
* fix(whatsapp): drain debounced inbound before close

* docs(changelog): note WhatsApp debounce close drain

(cherry picked from commit 81a3de1d9d)
2026-05-12 20:18:14 -07:00
Rubén Cuevas
f6c919cb9a fix(plugins): retry npm alias override installs (#80539)
* fix(plugins): retry npm alias override installs

* fix(onboarding): space install retry warning

* fix(onboarding): shorten retry progress label

* docs(changelog): note npm alias install retry

---------

Co-authored-by: pashpashpash <nik@vault77.ai>
(cherry picked from commit d4998d7b88)
2026-05-12 20:18:13 -07:00
Sarah Fortune
f810103509 feat(onboard): add --skip-hooks flag (#81220)
(cherry picked from commit e7c9e84a42)
2026-05-12 20:18:13 -07:00
Sarah Fortune
2aa4dc03c1 feat(onboard): offer codex migration after harness install (#81192)
Add a post-install seam so the wizard can prompt the user to import their
existing Codex CLI state (skills, archived config/hooks, advisory cached
plugins) through the existing `openclaw migrate codex` flow once the
harness plugin is in place. Fires on both fresh installs and repair runs;
the user can decline at any time.

Trigger sites, both routing through one helper:

- src/plugins/provider-auth-choice.ts: after
  `ensureCodexRuntimePluginForModelSelection` reports `installed: true`,
  dynamically import `offerPostInstallMigrations` and call it before the
  wizard moves on.
- src/commands/onboard-non-interactive/local/auth-choice.plugin-providers.ts:
  same call shape with `nonInteractive: true`, so the helper emits a hint
  line only and never mutates state.

Helper (src/wizard/setup.post-install-migration.ts) is generic, not
Codex-hardcoded — it resolves migration providers via the manifest
`migrationProviders` contract, filters to providers owned by plugins the
caller flags as installed in this onboarding step, runs `provider.detect`,
and on TTY hands accepted runs to `migrateDefaultCommand`. All detect,
prompt, and migrate failures are swallowed so onboarding never aborts on
this optional offer.

Also harden the Codex app-server subprocess lifecycle now that `detect()`
runs from a hotter onboarding path: isolate the plugin-install
`plugin/read` call (extensions/codex/src/migration/apply.ts) and have the
isolated request wait for child exit with a SIGKILL fallback
(extensions/codex/src/app-server/request.ts) so parents are not held open
by an orphaned codex binary.

Tests:

- src/wizard/setup.post-install-migration.test.ts (new, 10 cases)
- src/commands/onboard-non-interactive/local/auth-choice.plugin-providers.test.ts
  extended with hint-call assertions and a not-required-no-offer case.

(cherry picked from commit 48529f1a96)
2026-05-12 20:18:13 -07:00
Kevin Lin
a0a2eeefb9 fix: improve Codex migration selector enter
(cherry picked from commit bc44f3824f)
2026-05-12 20:18:13 -07:00
Sarah Fortune
4182fbaad0 fix(install): don't abort install.ps1 when git writes to stderr (#80834)
PowerShell 7+ honors $ErrorActionPreference=Stop for native commands,
so git's normal progress line ("From https://...") on stderr during
`git pull --rebase` would turn into a terminating error and abort the
installer immediately after a fresh clone — before pnpm install/build
ever runs. The existing `2>$null` redirects the display but the error
record is still generated.

Wrap the git status / pull calls in try/catch so the pull stays
best-effort and the rest of the installer can proceed. Reproduced on
Windows 11 ARM under PowerShell 7.x with -InstallMethod git.

(cherry picked from commit d06f0a0ee7)
2026-05-12 20:15:46 -07:00
Peter Steinberger
ab9893a4f5 fix(update): make pnpm preflight resolution deterministic 2026-05-13 03:01:05 +01:00
Peter Steinberger
d7472ab015 ci: avoid pnpm prompts in live docker tests 2026-05-13 02:35:10 +01:00
Peter Steinberger
95434cd497 test(gateway): tolerate google live tool nonce misses 2026-05-13 01:08:51 +01:00
Peter Steinberger
a90a5fc4d1 ci: track live provider workflow rename 2026-05-13 00:52:41 +01:00
Peter Steinberger
cc46ca9bee chore(release): bump beta 3 2026-05-13 00:04:20 +01:00
Peter Steinberger
9fd79d7b69 fix(plugins): keep codex runtime alias in packages 2026-05-12 23:48:21 +01:00
Peter Steinberger
b251a74b1c fix(plugins): alias codex native runtime for managed installs 2026-05-12 23:34:57 +01:00
Peter Steinberger
fdb6e92ff5 test(auth): type model check auth profile mocks 2026-05-12 22:44:11 +01:00
Peter Steinberger
7f0fc0bab4 build(canvas): refresh a2ui bundle hash 2026-05-12 22:37:43 +01:00
Peter Steinberger
8f212d0b6f chore(release): bump beta 2 2026-05-12 22:37:43 +01:00
y471823206
b86c387d6c Handle generic provider internal errors (#49401)
Merged via squash.

Prepared head SHA: 492caa49a9
Co-authored-by: y471823206 <2311651347@qq.com>
Co-authored-by: altaywtf <9790196+altaywtf@users.noreply.github.com>
Reviewed-by: @altaywtf
2026-05-12 22:37:43 +01:00
Galin Iliev
23dc2bfcd8 fix(azure):Drain split provider stream frames (#80927)
Merged via squash.

Prepared head SHA: 03a7e1fec3
Co-authored-by: galiniliev <5711535+galiniliev@users.noreply.github.com>
Co-authored-by: galiniliev <5711535+galiniliev@users.noreply.github.com>
Reviewed-by: @galiniliev
2026-05-12 22:37:42 +01:00
Bob
985bc40711 fix: surface silent model fallback failures (#80917)
Merged via squash.

Prepared head SHA: 59be6e2db5
Co-authored-by: dutifulbob <261991368+dutifulbob@users.noreply.github.com>
Co-authored-by: osolmaz <2453968+osolmaz@users.noreply.github.com>
Reviewed-by: @osolmaz
2026-05-12 22:37:42 +01:00
clawsweeper[bot]
eab66220f8 fix(gateway): wire max_completion_tokens/max_tokens through openai-http (#81013)
Summary:
- The branch adds Chat Completions token-cap fields to the Gateway request type, forwards them as agent stream parameters, and documents/tests the behavior.
- Reproducibility: yes. Source inspection gives a high-confidence current-main path: send `max_completion_toke ... tokens` to `/v1/chat/completions` and observe that the current handler never sets `streamParams.maxTokens`.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(gateway): wire max_completion_tokens/max_tokens through openai-http

Validation:
- ClawSweeper review passed for head a9c39f7d4a.
- Required merge gates passed before the squash merge.

Prepared head SHA: a9c39f7d4a
Review: https://github.com/openclaw/openclaw/pull/81013#issuecomment-4430303959

Co-authored-by: Bingsen <dingheng.huang@urbanic.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
2026-05-12 22:37:42 +01:00
pashpashpash
22a6717e11 Keep Codex media tools backed by auth profiles (#81059)
* fix(codex): pass auth profiles to dynamic tools

* fix: bump protobufjs past advisory range
2026-05-12 22:37:42 +01:00
Kevin Lin
a4743ad180 fix(codex): gate migration on app readiness (#80815)
* fix(codex): gate migration on app readiness

* fix(codex): preserve source auth during migration

* fix(codex): isolate migration source app probes

* docs(codex): align migration readiness reasons

* docs(codex): remove stale auth-required source reason

* fix(codex): narrow native auth profile resolver input

* fix: clarify codex migration subscription gating

* refactor: simplify codex migration subscription gate

* fix: make codex app verification optional

* docs: clarify codex app inventory cache

* test: avoid map spread in migration test
2026-05-12 22:37:42 +01:00
Ayaan Zaidi
1df4df6eed fix(docker): persist auth profile key mount 2026-05-12 22:37:42 +01:00
Rubén Cuevas
ca8bc5500d fix(onboard): short-circuit model auth check 2026-05-12 22:37:42 +01:00
Rubén Cuevas
930046df04 fix(onboard): accept Codex auth in model check 2026-05-12 22:37:42 +01:00
kinjitakabe
03e4b035f1 fix(matrix): stop runtime npm install from parent-derived cwd
`ensureMatrixSdkInstalled` previously derived an install `cwd` via fixed
two-segment traversal from `import.meta.url` and spawned `npm install`
(or `pnpm install`) when Matrix packages were missing. Under the
externalized plugin layout the derived path is a scope directory like
`<config>/npm/node_modules/@openclaw`, so npm walks up to the managed
project root and prunes undeclared siblings. Under the legacy bundled
layout it would target `<global-prefix>/lib/node_modules` and could
delete unrelated global CLIs.

Matrix is now a pure availability check: if any required package fails
to resolve, it throws an actionable error pointing the operator at the
supported repair commands (`openclaw plugins update matrix`,
`openclaw doctor --fix`). This matches extensions/AGENTS.md:
"Runtime never installs deps; install/update/doctor are repair points."

The exported signature stays backwards-compatible (all params optional;
`confirm` and `runtime` are accepted but ignored). `resolveMissingMatrixPackages`
gains an optional `resolveFn` seam for testability, mirroring the existing
`ensureMatrixCryptoRuntime` injection pattern.

Fixes #80758.
2026-05-12 22:37:42 +01:00
Peter Steinberger
6115eada6d docs: note baileys install policy 2026-05-12 22:37:42 +01:00
pashpashpash
84a2060a64 fix(codex): backport auth profiles to dynamic tools 2026-05-12 14:31:47 -07:00
Peter Steinberger
bc6090502c ci: allow focused media video live reruns 2026-05-12 21:09:16 +01:00
Peter Steinberger
d3a8a45119 ci: run advisory live wrappers with bash 2026-05-12 20:59:12 +01:00
Peter Steinberger
b12cd4358d ci: keep Docker CLI backend live test noninteractive 2026-05-12 19:47:54 +01:00
Peter Steinberger
1824464bf2 fix(docker): avoid runtime prune hang 2026-05-12 16:15:39 +01:00
Peter Steinberger
6eebba3920 test(openai): relax live tts latency budget 2026-05-12 15:28:55 +01:00
Peter Steinberger
7b544a7976 fix(release): unblock docker release validation 2026-05-12 14:34:32 +01:00
Peter Steinberger
441041f92d test(release): harden beta validation flakes 2026-05-12 13:25:18 +01:00
Peter Steinberger
7284608461 fix(release): unblock update validation gates 2026-05-12 12:50:02 +01:00
Peter Steinberger
56d96b3b8d fix(release): unblock beta docker validation 2026-05-12 12:07:28 +01:00
Peter Steinberger
41bf26ede3 test(release): refresh beta validation expectations 2026-05-12 11:33:20 +01:00
Peter Steinberger
6820d18160 test(docker): align runtime prune assertion 2026-05-12 11:14:58 +01:00
Peter Steinberger
e6fb7aa1a8 fix(docker): allow runtime prune to hydrate cache 2026-05-12 11:06:35 +01:00
365 changed files with 12211 additions and 1619 deletions

View File

@@ -316,7 +316,19 @@ jobs:
- name: Pull root Dockerfile smoke image
env:
IMAGE_REF: ${{ needs.root_dockerfile_image.outputs.image_ref }}
run: timeout 600s docker pull "$IMAGE_REF"
run: |
set -euo pipefail
for attempt in 1 2; do
if timeout 1200s docker pull "$IMAGE_REF"; then
exit 0
fi
status=$?
if [ "$attempt" = "2" ]; then
exit "$status"
fi
docker image rm "$IMAGE_REF" >/dev/null 2>&1 || true
sleep 30
done
- name: Run root Dockerfile CLI smoke
env:
@@ -421,7 +433,19 @@ jobs:
- name: Pull root Dockerfile smoke image
env:
IMAGE_REF: ${{ needs.root_dockerfile_image.outputs.image_ref }}
run: timeout 600s docker pull "$IMAGE_REF"
run: |
set -euo pipefail
for attempt in 1 2; do
if timeout 1200s docker pull "$IMAGE_REF"; then
exit 0
fi
status=$?
if [ "$attempt" = "2" ]; then
exit "$status"
fi
docker image rm "$IMAGE_REF" >/dev/null 2>&1 || true
sleep 30
done
- name: Set up Blacksmith Docker Builder
uses: useblacksmith/setup-docker-builder@722e97d12b1d06a961800dd6c05d79d951ad3c80 # v1
@@ -488,7 +512,19 @@ jobs:
- name: Pull root Dockerfile smoke image
env:
IMAGE_REF: ${{ needs.root_dockerfile_image.outputs.image_ref }}
run: timeout 600s docker pull "$IMAGE_REF"
run: |
set -euo pipefail
for attempt in 1 2; do
if timeout 1200s docker pull "$IMAGE_REF"; then
exit 0
fi
status=$?
if [ "$attempt" = "2" ]; then
exit "$status"
fi
docker image rm "$IMAGE_REF" >/dev/null 2>&1 || true
sleep 30
done
- name: Setup Node environment for Bun smoke
uses: ./.github/actions/setup-node-env

View File

@@ -100,7 +100,7 @@ jobs:
run_package_telegram_e2e:
name: Run package Telegram E2E
runs-on: blacksmith-32vcpu-ubuntu-2404
timeout-minutes: 60
timeout-minutes: 120
environment: qa-live-shared
permissions:
actions: read

View File

@@ -433,6 +433,10 @@ jobs:
add_profile_suite native-live-extensions-media-music-google "full"
add_profile_suite native-live-extensions-media-music-minimax "full"
add_profile_suite native-live-extensions-media-video "full"
add_profile_suite native-live-extensions-media-video-a "full"
add_profile_suite native-live-extensions-media-video-b "full"
add_profile_suite native-live-extensions-media-video-c "full"
add_profile_suite native-live-extensions-media-video-d "full"
fi
fi
@@ -2198,6 +2202,7 @@ jobs:
- name: Run ${{ matrix.label }}
if: contains(matrix.profiles, inputs.release_test_profile) && (inputs.live_suite_filter == '' || inputs.live_suite_filter == matrix.suite_id || (inputs.live_suite_filter == 'native-live-src-gateway-profiles-anthropic' && startsWith(matrix.suite_id, 'native-live-src-gateway-profiles-anthropic-')) || (inputs.live_suite_filter == 'native-live-src-gateway-profiles-opencode-go' && startsWith(matrix.suite_id, 'native-live-src-gateway-profiles-opencode-go-')))
shell: bash
env:
OPENCLAW_LIVE_COMMAND: ${{ matrix.command }}
OPENCLAW_LIVE_SUITE_ADVISORY: ${{ matrix.advisory }}
@@ -2414,6 +2419,7 @@ jobs:
- name: Run ${{ matrix.label }}
if: contains(matrix.profiles, inputs.release_test_profile) && (inputs.live_suite_filter == '' || inputs.live_suite_filter == matrix.suite_id || (inputs.live_suite_filter == 'live-gateway-advisory-docker' && startsWith(matrix.suite_id, 'live-gateway-advisory-docker-')))
shell: bash
env:
OPENCLAW_LIVE_COMMAND: ${{ matrix.command }}
OPENCLAW_LIVE_SUITE_ADVISORY: ${{ matrix.advisory }}
@@ -2602,6 +2608,7 @@ jobs:
- name: Run ${{ matrix.label }}
if: contains(matrix.profiles, inputs.release_test_profile) && (inputs.live_suite_filter == '' || inputs.live_suite_filter == matrix.suite_id || (inputs.live_suite_filter == 'native-live-extensions-media-video' && startsWith(matrix.suite_id, 'native-live-extensions-media-video-')))
shell: bash
env:
OPENCLAW_LIVE_SUITE_ADVISORY: ${{ matrix.advisory }}
run: |

View File

@@ -6,17 +6,76 @@ Docs: https://docs.openclaw.ai
### Fixes
- gateway: pass Talk session scope to resolver [AI]. (#81379) Thanks @pgondhi987.
- Gateway protocol: require v4 clients and stream explicit chat `deltaText`/`replace` frames so SDK clients can consume assistant updates without local diffing. (#80725) Thanks @samzong.
- GitHub Copilot: exchange OAuth tokens for Copilot API tokens on image understanding requests and route Gemini image payloads through Chat Completions, fixing Copilot Gemini image descriptions. (#80393, #80442) Thanks @afunnyhy.
- Gateway: hide pending Node pairing commands, capabilities, and permissions until approval, and refresh the live approved surface when pairings change. (#80741) Thanks @samzong.
- Plugins/Feishu/WhatsApp/Line: enforce inbound media size caps while reading download streams, avoiding full buffering of oversized attachments. (#81044, #81050) Thanks @samzong.
- Config: serialize and retry semantic config mutations centrally, so concurrent commands can rebase safe changes instead of clobbering or hand-rolling command-local retry loops. (#76601)
- Require approval for setup-code device pairing [AI]. (#81292) Thanks @pgondhi987.
- Plugins/install: preserve third-party peer dependencies in the managed npm root when later plugin installs or updates recalculate the shared dependency tree. Thanks @shakkernerd.
- Plugins/uninstall: prune managed third-party peer dependencies after their owning npm plugin is removed, without blocking plugin cleanup on peer-prune failures.
- Docker: pin setup-time container paths so stale host `.env` OpenClaw paths cannot leak into Linux containers. Fixes #80381. (#81105) Thanks @brokemac79.
- Channels/WeCom: refresh the official onboarding install to `@wecom/wecom-openclaw-plugin@2026.5.7` and update existing managed npm installs instead of failing on the package directory. Fixes #79884. (#80390) Thanks @brokemac79.
- Anthropic: reseed Claude CLI fresh-session retries from bounded OpenClaw transcript history after session rotation, preventing conversation amnesia. Fixes #80905. (#80934) Thanks @bitloi.
- Require explicit browser device pairing [AI]. (#81289) Thanks @pgondhi987.
- Require Control UI pairing before proxy-scoped access [AI]. (#81288) Thanks @pgondhi987.
- Installer: honor `--version` for git installs and install from the checked-in lockfile, preventing recent dependency pins from tripping pnpm's minimum-release-age gate during tag installs.
- Agents: deliver same-process subagent completion handoffs through the in-process agent dispatcher instead of opening a Gateway RPC loopback.
- Harden trusted-proxy source validation [AI]. (#81290) Thanks @pgondhi987.
- Agents: add permissive item schemas to array tool parameters before provider submission, preventing OpenAI-compatible schema validation from rejecting plugin tools that omit `items`. Fixes #81175. (#81217) Thanks @JARVIS-Glasses.
- Agents: escalate LLM idle watchdog timeouts through profile rotation and configured model fallback instead of leaving agent turns stuck after a silent model stream. Fixes #76877. (#80449) Thanks @jimdawdy-hub.
- Discord voice: treat OpenAI Realtime startup auth failures as fatal, suppress duplicate realtime error logs, and stop autoJoin from retrying the same broken voice channel until credentials are fixed.
- ACPX: stop forwarding unsupported timeout config options to Claude ACP while preserving OpenClaw's own turn timeout. (#80812) Thanks @sxxtony.
- Session transcripts: redact sensitive message content in the centralized JSONL append path so CLI turns, gateway transcript injection, transcript mirrors, and guarded tool results use the same configured redaction behavior. Fixes #73565. Refs #73563. (#79645) Thanks @Ziy1-Tan.
- Channels/iMessage: ignore Apple link-preview plugin payload attachments when users paste URLs, keeping the URL text while avoiding phantom media context. (#79374) Thanks @homer-byte.
- Telegram: detect polling stalls from `getUpdates` liveness only, so outbound API calls no longer mask dead inbound polling; log polling-cycle starts after transport rebuilds. Fixes #78473.
- fix: scan plugin runtime entries during install [AI]. (#80998) Thanks @pgondhi987.
- fix(plugins): scan installed dependency runtime code [AI]. (#81066) Thanks @pgondhi987.
- Inherit tool restrictions for delegated sessions [AI]. (#80979) Thanks @pgondhi987.
- Telegram: discard legacy long-poll update offsets that cannot be tied to the current bot token, so token rotation no longer leaves bots silently skipping new messages. (#80671) Thanks @sxxtony.
- browser: enforce navigation checks for act interactions [AI]. (#81070) Thanks @pgondhi987.
- Validate node exec event provenance [AI]. (#81071) Thanks @pgondhi987.
- Limit hook CLI tool authority [AI]. (#81065) Thanks @pgondhi987.
- Require admin scope for node device token management [AI]. (#81067) Thanks @pgondhi987.
- Restrict chat sender allowlist matching [AI]. (#80898) Thanks @pgondhi987.
- Update: suppress the false newer-config warning during restart health probing after an update handoff, while keeping future-version mutation guards intact. (#78652)
- Sessions: redact persisted tool result detail metadata before writing transcripts so diagnostic secrets do not survive tool output redaction. (#80444) Thanks @nimbleenigma.
- Codex runtime: allow the official installed `@openclaw/codex` package to use its private task-runtime SDK helper, fixing `MODULE_NOT_FOUND` during migrated OpenAI/Codex beta runs.
- Codex migration: make Enter activate the highlighted checkbox row before continuing, so `Skip for now` and bulk-selection rows work even when planned items start preselected.
- Codex harness: keep auth-profile-backed media tools such as `image_generate` available when OpenAI auth lives in the agent's auth-profile store instead of environment variables.
- WhatsApp/install: allow Baileys' pinned libsignal git subdependency under pnpm 11 so source installs and local checks can complete.
- fix(memory-wiki): require admin scope for ingest [AI]. (#80897) Thanks @pgondhi987.
- memory-wiki: require write scope for Obsidian search [AI]. (#80904) Thanks @pgondhi987.
- WhatsApp/install: allow Baileys' pinned libsignal git subdependency under pnpm 11 so source installs and local checks can complete.
- WhatsApp: externalize the channel as a ClawHub/npm plugin outside the core npm runtime bundle, and bump Baileys to `7.0.0-rc11` so libsignal resolves from the registry instead of a GitHub tarball.
- WhatsApp: keep optional audio decoding dependencies local to the external plugin so the core npm install no longer pulls WhatsApp-only media helpers.
- Build: skip copied metadata for bundled plugins that are excluded from build entries, preventing update/status rebuilds from advertising missing QQ Bot runtime files. (#80925)
- Control UI/sessions: nest subagent sessions under their parent session in the session picker dropdown using a visual `└─ ` prefix, making the parent-child relationship clear. Fixes #77628. (#78623) Thanks @chinar-amrutkar.
- Auto-reply: surface a visible error when the configured model backend fails and fallback produces no visible reply, while preserving intentional silent turns and side-effect-only deliveries. (#80917) Thanks @dutifulbob.
- Agents/exec: skip redundant heartbeat wake-ups for subagent session exec completions, preventing spurious LLM invocations on parent sessions. Fixes #66748. (#66749) Thanks @ggzeng.
- Provider streams: keep OpenAI-compatible SSE and JSON fallback streams draining across split chunks and fail Azure Responses streams with a bounded first-event diagnostic instead of stalling. Refs #80926. (#80927) Thanks @galiniliev and @CaptainTimon.
- Agents: rewrite generic provider internal errors with support request IDs into user-friendly transient error copy. (#49401) Thanks @y471823206.
- WhatsApp: finish handling pending debounced inbound messages before closing the socket. (#81246) Thanks @mcaxtr.
- CLI/commitments: write `--json` output to stdout instead of diagnostic logs so automation can parse commitment list and dismiss results. (#81215) Thanks @giodl73-repo.
- Update: allow pnpm GitHub-source OpenClaw updates to approve the OpenClaw package build, so source installs complete their prepare/prepack lifecycle. (#81294) Thanks @fuller-stack-dev.
- Telegram: preserve supported HTML tags in visible replies and durable mirrors so formatted messages render correctly instead of degrading to escaped text. (#80977) Thanks @obviyus.
- Plugins/runtime: attribute deprecated runtime config load/write warnings to the plugin id and source that triggered them so logs and plugin doctor runs are actionable. Refs #81394. (#81425) Thanks @BKF-Gitty.
### Changes
- Gateway/OpenAI HTTP: honor `max_completion_tokens` and `max_tokens` on inbound `/v1/chat/completions` requests so client-provided token caps reach the upstream provider via `streamParams.maxTokens`, with `max_completion_tokens` taking precedence when both are sent. Thanks @Lellansin.
- Models/OpenAI CLI auth: make `openclaw models auth login --provider openai` start the ChatGPT/Codex account login by default, while `--method api-key` remains the explicit OpenAI API-key setup path.
- Google/Gemini: normalize retired Gemini 3 Pro Preview ids inside explicit SDK OAuth auth-result config patches, so provider helpers emit `google/gemini-3.1-pro-preview` for Gemini 3.1 testing.
- Google/Gemini: normalize retired Gemini 3 Pro Preview ids inside SDK OAuth auth-result default config patches, so helper-built provider auth flows emit `google/gemini-3.1-pro-preview` for Gemini 3.1 testing.
- Google/Gemini: normalize retired Gemini 3 Pro Preview ids returned by direct `openclaw models auth login --set-default` provider auth flows before writing config, so Gemini testing targets `google/gemini-3.1-pro-preview`.
- Google/Gemini: normalize retired Gemini 3 Pro Preview ids in per-agent config defaults and auth patches, so agent-specific emitted config keeps targeting `google/gemini-3.1-pro-preview`.
- Google/Gemini: normalize retired Gemini 3 Pro Preview ids in provider catalog rows when API-key onboarding only reapplies the agent default, so emitted config keeps testing `google/gemini-3.1-pro-preview`.
- Google/Gemini: normalize retired Gemini 3 Pro Preview ids in `config set` mutation output for agent overrides and provider catalog rows, so current config emits `google/gemini-3.1-pro-preview`.
- Google/Gemini: canonicalize provider-qualified retired Gemini 3 Pro Preview refs during Google forward-compatible model resolution, so emitted config uses `google/gemini-3.1-pro-preview` for Gemini 3.1 testing.
- Google/Gemini: normalize proxy-prefixed retired Gemini 3 Pro Preview catalog rows, so emitted configs use `google/gemini-3.1-pro-preview` for Gemini 3.1 testing.
- Google/Gemini: normalize retired Gemini 3 Pro Preview ids inside per-agent model overrides before writing config, so agent-specific config emits `google/gemini-3.1-pro-preview` for Gemini 3.1 testing.
- Google/Gemini: normalize retired Gemini 3 Pro Preview ids in subagent, heartbeat, compaction, and subagent-tool model config during writes, so current config keeps emitting `google/gemini-3.1-pro-preview`.
- Docs/subagents: document `agents.defaults.subagents.announceTimeoutMs` in the sub-agent and configuration references. (#75509) Thanks @akrimm702.
- Cron: add direct `cron.get`, `openclaw cron get <id>`, and agent-tool `get` support for inspecting one stored cron job by id. (#75117) Thanks @samzong.
- Agents/tools: add per-sender tool policies with canonical channel-scoped sender keys, so operators can restrict dangerous tools by requester identity across global, agent, group, core, bundled, and plugin tool surfaces. (#66933) Thanks @JerranC.
@@ -85,6 +144,7 @@ Docs: https://docs.openclaw.ai
- Agents/compaction: read post-compaction AGENTS.md refresh context from the queued run workspace instead of the runner process cwd, so CLI-backed follow-up turns re-inject the correct workspace startup rules after compaction. Fixes #70541. (#75532) Thanks @vyctorbrzezowski.
- Agents/read tool: treat positive offsets beyond EOF as empty ranges instead of surfacing the upstream read error, so stale pagination cursors no longer crash tool calls while unrelated read failures still fail loud. Fixes #62466. (#75536) Thanks @vyctorbrzezowski.
- Google/Gemini: normalize retired Gemini 3 Pro Preview refs left in Google API-key onboarding model allowlists and fallbacks, so setup-emitted config keeps testing `google/gemini-3.1-pro-preview` instead of `google/gemini-3-pro-preview`.
- Telegram/context: bound selected topic context to the active session so messages from before `/new` or `/reset` are not replayed into later turns. (#80848) Thanks @VACInc.
- Google/Gemini: normalize retired nested Gemini 3 Pro Preview ids when resolving exact configured proxy-provider refs, so `kilocode/google/gemini-3-pro-preview` resolves to `kilocode/google/gemini-3.1-pro-preview` for Gemini 3.1 testing.
- CLI: strip generic OSC terminal escape payloads from sanitized output fields, preventing clipboard/title escape bodies from leaking into commitment tables and other terminal-safe text. Thanks @shakkernerd.
- Codex app-server: match connector-backed plugin approval elicitations by stable connector id so enabled destructive actions no longer fall through to display-name-only rejection.
@@ -221,6 +281,7 @@ Docs: https://docs.openclaw.ai
- Control UI/config: remove plugin allowlist entries that the form auto-added when a plugin enable toggle is reverted before saving, so reverting the visible toggle clears dirty state without persisting unintended allowlist changes. (#78329) Thanks @samzong.
- Gateway/mobile: reuse bootstrap-issued device-token scopes on handoff reconnects and surface device-token scope mismatches separately from token mismatches while preserving full shared-token dashboard/native sessions. Fixes #79292. Thanks @BunsDev.
- Media/host-read: allow buffer-verified gzip, tar, and 7z archives in the shared host-local media validator alongside ZIP and document attachments.
- Plugins/install: retry managed npm plugin installs without npm alias overrides after npm's `Invalid comparator: npm:` failure, so older npm versions can install official plugins instead of aborting. (#80539) Thanks @rubencu.
- Plugins/doctor: invalidate persisted plugin registry snapshots when plugin diagnostics point at deleted source paths, so `openclaw doctor` stops repeating stale warnings after a local extension is replaced by a managed npm plugin. Fixes #80087. (#80134) Thanks @hclsys.
- Doctor/OpenAI Codex: preserve Codex auth intent when auto-repairing legacy `openai-codex/*` model refs to canonical `openai/*` by adding provider/model-scoped Codex runtime policy, preventing repaired configs from falling through to direct OpenAI API-key auth. Fixes #78533 and #78570. Thanks @superck110 and @Azmodump.
- CLI/agents: surface durable message delivery status from `sendDurableMessageBatch` in `deliverAgentCommandResult` and `openclaw agent --json --deliver`, preserving suppressed hook outcomes as terminal no-retry results while exposing partial and failed sends for automation. Supersedes #53961 and #57755. Thanks @Kaspre.
@@ -322,6 +383,7 @@ Docs: https://docs.openclaw.ai
- Agents: abort generic repeated no-progress tool loops at the critical threshold when identical calls keep returning identical outcomes. (#80668) Thanks @frankekn.
- Exec approvals: omit generated command highlights for non-POSIX Windows and shell-wrapper approval commands until those command languages have native highlighting support. (#80566) Thanks @jesse-merhi.
- Telegram: keep verbose tool progress and result drafts separate from the final assistant answer so tool output no longer blends into the final Telegram message. (#80294) Thanks @jalehman.
- Plugin SDK/Windows: enable the native require fast path for root `openclaw/plugin-sdk` dist aliases instead of forcing Jiti transforms. (#80878) Thanks @medns.
## 2026.5.9
@@ -579,7 +641,7 @@ Docs: https://docs.openclaw.ai
- Control UI/chat: hide retired and non-public Google Gemini model IDs from chat model catalogs and route the bare `gemini-3-pro` alias to Gemini 3.1 Pro Preview instead of the shut-down Gemini 3 Pro Preview. Thanks @BunsDev.
- CLI/infer: canonicalize case-only catalog model refs in `infer model run --model` so mixed-case provider/model strings resolve to the canonical catalog entry instead of failing with `Unknown model`. (#78940) Thanks @ai-hpc.
- CLI/infer: allow explicit local `infer model run --model <provider/model>` probes to use exact bundled static catalog rows before the provider is written to config, surfacing missing credentials as auth errors instead of `Unknown model`.
- CLI/install: refuse state-mutating OpenClaw CLI runs as root by default, keep an explicit `OPENCLAW_ALLOW_ROOT=1` escape hatch for intentional root/container use, and update DigitalOcean setup guidance to run OpenClaw as a non-root user. Fixes #67478. Thanks @Jerry-Xin and @natechicago.
- CLI/install: revert the beta-only global root-refusal guard so existing root-managed VPS installs keep working; the DigitalOcean split-brain protection will move to a narrower image/install-specific path. Refs #67478 and #67509. Thanks @vincentkoc.
- Auto-reply/media: resolve `scp` from `PATH` when staging sandbox media so nonstandard OpenSSH installs can copy remote attachments.
- Agents/PI: route PI-native OpenAI-compatible default streams through OpenClaw boundary-aware transports so local-compatible model runs keep API-key injection and transport policy.
- Gateway/media: require authenticated owner or admin context for managed outgoing image bytes instead of trusting requester-session headers.

View File

@@ -116,20 +116,25 @@ ENV OPENCLAW_PREFER_PNPM=1
RUN pnpm_config_verify_deps_before_run=false pnpm ui:build
RUN pnpm_config_verify_deps_before_run=false pnpm qa:lab:build
# Prune dev dependencies and strip build-only metadata before copying
# Reinstall production dependencies and strip build-only metadata before copying
# runtime assets into the final image.
FROM build AS runtime-assets
ARG OPENCLAW_EXTENSIONS
ARG OPENCLAW_BUNDLED_PLUGIN_DIR
RUN --mount=type=cache,id=openclaw-pnpm-store,target=/root/.local/share/pnpm/store,sharing=locked \
CI=true pnpm prune --prod \
--config.offline=true \
RUN --mount=type=cache,id=openclaw-pnpm-runtime-store,target=/root/.local/share/pnpm/store,sharing=locked \
echo "==> runtime-assets: install prod dependencies" && \
rm -rf node_modules && \
NODE_OPTIONS=--max-old-space-size=2048 pnpm install --prod --frozen-lockfile --ignore-scripts \
--config.supportedArchitectures.os=linux \
--config.supportedArchitectures.cpu="$(node -p 'process.arch')" \
--config.supportedArchitectures.libc=glibc && \
echo "==> runtime-assets: refresh bundled plugin registry" && \
node scripts/postinstall-bundled-plugins.mjs && \
echo "==> runtime-assets: prune non-selected plugin dist" && \
OPENCLAW_EXTENSIONS="$OPENCLAW_EXTENSIONS" node scripts/prune-docker-plugin-dist.mjs && \
echo "==> runtime-assets: remove dist type and sourcemap files" && \
find dist -type f \( -name '*.d.ts' -o -name '*.d.mts' -o -name '*.d.cts' -o -name '*.map' \) -delete && \
echo "==> runtime-assets: check package dist imports" && \
node scripts/check-package-dist-imports.mjs /app
# ── Runtime base image ──────────────────────────────────────────

View File

@@ -1,4 +1,4 @@
package ai.openclaw.app.gateway
const val GATEWAY_PROTOCOL_VERSION = 4
const val GATEWAY_MIN_PROTOCOL_VERSION = 3
const val GATEWAY_MIN_PROTOCOL_VERSION = 4

View File

@@ -3,7 +3,7 @@
import Foundation
public let GATEWAY_PROTOCOL_VERSION = 4
public let GATEWAY_MIN_PROTOCOL_VERSION = 3
public let GATEWAY_MIN_PROTOCOL_VERSION = 4
private struct GatewayAnyCodingKey: CodingKey, Hashable {
let stringValue: String
@@ -6224,12 +6224,138 @@ public struct ChatInjectParams: Codable, Sendable {
}
}
public struct ChatEvent: Codable, Sendable {
public struct ChatDeltaEvent: Codable, Sendable {
public let runid: String
public let sessionkey: String
public let spawnedby: String?
public let seq: Int
public let state: AnyCodable
public let state: String
public let message: AnyCodable?
public let deltatext: String
public let replace: Bool?
public let usage: AnyCodable?
public init(
runid: String,
sessionkey: String,
spawnedby: String?,
seq: Int,
state: String,
message: AnyCodable?,
deltatext: String,
replace: Bool?,
usage: AnyCodable?)
{
self.runid = runid
self.sessionkey = sessionkey
self.spawnedby = spawnedby
self.seq = seq
self.state = state
self.message = message
self.deltatext = deltatext
self.replace = replace
self.usage = usage
}
private enum CodingKeys: String, CodingKey {
case runid = "runId"
case sessionkey = "sessionKey"
case spawnedby = "spawnedBy"
case seq
case state
case message
case deltatext = "deltaText"
case replace
case usage
}
}
public struct ChatFinalEvent: Codable, Sendable {
public let runid: String
public let sessionkey: String
public let spawnedby: String?
public let seq: Int
public let state: String
public let message: AnyCodable?
public let usage: AnyCodable?
public let stopreason: String?
public init(
runid: String,
sessionkey: String,
spawnedby: String?,
seq: Int,
state: String,
message: AnyCodable?,
usage: AnyCodable?,
stopreason: String?)
{
self.runid = runid
self.sessionkey = sessionkey
self.spawnedby = spawnedby
self.seq = seq
self.state = state
self.message = message
self.usage = usage
self.stopreason = stopreason
}
private enum CodingKeys: String, CodingKey {
case runid = "runId"
case sessionkey = "sessionKey"
case spawnedby = "spawnedBy"
case seq
case state
case message
case usage
case stopreason = "stopReason"
}
}
public struct ChatAbortedEvent: Codable, Sendable {
public let runid: String
public let sessionkey: String
public let spawnedby: String?
public let seq: Int
public let state: String
public let message: AnyCodable?
public let stopreason: String?
public init(
runid: String,
sessionkey: String,
spawnedby: String?,
seq: Int,
state: String,
message: AnyCodable?,
stopreason: String?)
{
self.runid = runid
self.sessionkey = sessionkey
self.spawnedby = spawnedby
self.seq = seq
self.state = state
self.message = message
self.stopreason = stopreason
}
private enum CodingKeys: String, CodingKey {
case runid = "runId"
case sessionkey = "sessionKey"
case spawnedby = "spawnedBy"
case seq
case state
case message
case stopreason = "stopReason"
}
}
public struct ChatErrorEvent: Codable, Sendable {
public let runid: String
public let sessionkey: String
public let spawnedby: String?
public let seq: Int
public let state: String
public let message: AnyCodable?
public let errormessage: String?
public let errorkind: AnyCodable?
@@ -6241,7 +6367,7 @@ public struct ChatEvent: Codable, Sendable {
sessionkey: String,
spawnedby: String?,
seq: Int,
state: AnyCodable,
state: String,
message: AnyCodable?,
errormessage: String?,
errorkind: AnyCodable?,
@@ -6373,6 +6499,43 @@ public enum PluginsSessionActionResult: Codable, Sendable {
}
}
public enum ChatEvent: Codable, Sendable {
case delta(ChatDeltaEvent)
case final(ChatFinalEvent)
case aborted(ChatAbortedEvent)
case error(ChatErrorEvent)
private enum CodingKeys: String, CodingKey {
case discriminator = "state"
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let discriminator = try container.decode(String.self, forKey: .discriminator)
switch discriminator {
case "delta": self = try .delta(ChatDeltaEvent(from: decoder))
case "final": self = try .final(ChatFinalEvent(from: decoder))
case "aborted": self = try .aborted(ChatAbortedEvent(from: decoder))
case "error": self = try .error(ChatErrorEvent(from: decoder))
default:
throw DecodingError.dataCorruptedError(
forKey: .discriminator,
in: container,
debugDescription: "Unknown ChatEvent discriminator value"
)
}
}
public func encode(to encoder: Encoder) throws {
switch self {
case .delta(let value): try value.encode(to: encoder)
case .final(let value): try value.encode(to: encoder)
case .aborted(let value): try value.encode(to: encoder)
case .error(let value): try value.encode(to: encoder)
}
}
}
public enum GatewayFrame: Codable, Sendable {
case req(RequestFrame)
case res(ResponseFrame)

View File

@@ -66,7 +66,6 @@ const rootBundledPluginRuntimeDependencies = [
"@slack/bolt",
"@slack/types",
"@slack/web-api",
"audio-decode",
"grammy",
"linkedom",
"minimatch",

View File

@@ -38,6 +38,7 @@ services:
volumes:
- ${OPENCLAW_CONFIG_DIR:-${HOME:-/tmp}/.openclaw}:/home/node/.openclaw
- ${OPENCLAW_WORKSPACE_DIR:-${HOME:-/tmp}/.openclaw/workspace}:/home/node/.openclaw/workspace
- ${OPENCLAW_AUTH_PROFILE_SECRET_DIR:-${HOME:-/tmp}/.openclaw-auth-profile-secrets}:/home/node/.config/openclaw
## Uncomment the lines below to enable sandbox isolation
## (agents.defaults.sandbox). Requires Docker CLI in the image
## (build with --build-arg OPENCLAW_INSTALL_DOCKER_CLI=1) or use
@@ -112,6 +113,7 @@ services:
volumes:
- ${OPENCLAW_CONFIG_DIR:-${HOME:-/tmp}/.openclaw}:/home/node/.openclaw
- ${OPENCLAW_WORKSPACE_DIR:-${HOME:-/tmp}/.openclaw/workspace}:/home/node/.openclaw/workspace
- ${OPENCLAW_AUTH_PROFILE_SECRET_DIR:-${HOME:-/tmp}/.openclaw-auth-profile-secrets}:/home/node/.config/openclaw
stdin_open: true
tty: true
init: true

View File

@@ -1,2 +1,2 @@
f26833e053032e3da94025c8a5a8cb62dcddd275797b527440a19be5886a4783 plugin-sdk-api-baseline.json
429fe1d6d119379b914bf84b15705233dc8d2d9e1a8131bb28ea19b19afbe6a0 plugin-sdk-api-baseline.jsonl
754a25f82ddb9c924167a739b2b8963ac99166e05d13aee0a279df29ab9ad38c plugin-sdk-api-baseline.json
819bbc3e4bf9d16438f1b7da2d3254dcdd93aebd76643241ee91df6c9ba18658 plugin-sdk-api-baseline.jsonl

View File

@@ -16,8 +16,8 @@ Text is supported everywhere; media and reactions vary by channel.
- Slack multi-person DMs route as group chats, so group policy, mention
behavior, and group-session rules apply to MPIM conversations.
- WhatsApp setup is install-on-demand: onboarding can show the setup flow before
the plugin package is installed, and the Gateway loads the WhatsApp runtime
only when the channel is actually active.
the plugin package is installed, and the Gateway loads the external
ClawHub/npm plugin only when the channel is actually active.
## Supported channels

View File

@@ -395,7 +395,7 @@ curl "https://api.telegram.org/bot<bot_token>/getUpdates"
Outbound text uses Telegram `parse_mode: "HTML"`.
- Markdown-ish text is rendered to Telegram-safe HTML.
- Raw model HTML is escaped to reduce Telegram parse failures.
- Supported Telegram HTML tags are preserved; unsupported HTML is escaped.
- If Telegram rejects parsed HTML, OpenClaw retries as plain text.
Link previews are enabled by default and can be disabled with `channels.telegram.linkPreview: false`.

View File

@@ -14,27 +14,19 @@ Status: production-ready via WhatsApp Web (Baileys). Gateway owns linked session
- `openclaw channels login --channel whatsapp` also offers the install flow when
the plugin is not present yet.
- Dev channel + git checkout: defaults to the local plugin path.
- Stable/Beta: uses the npm package `@openclaw/whatsapp` on the current official
release tag.
- Stable/Beta: installs the official `@openclaw/whatsapp` plugin from ClawHub
first, with npm as the fallback.
- The WhatsApp runtime is distributed outside the core OpenClaw npm package so
WhatsApp-specific runtime dependencies stay with the external plugin.
Manual install stays available:
```bash
openclaw plugins install @openclaw/whatsapp
openclaw plugins install clawhub:@openclaw/whatsapp
```
Use the bare package to follow the current official release tag. Pin an exact
version only when you need a reproducible install.
On Windows, the WhatsApp plugin needs Git on `PATH` during npm install because
one of its Baileys/libsignal dependencies is fetched from a git URL. Install
Git for Windows, then restart the shell and rerun the install:
```powershell
winget install --id Git.Git -e
```
Portable Git also works if its `bin` directory is on `PATH`.
Use the bare npm package (`@openclaw/whatsapp`) only when you need the registry
fallback. Pin an exact version only when you need a reproducible install.
<CardGroup cols={3}>
<Card title="Pairing" icon="link" href="/channels/pairing">

View File

@@ -22,6 +22,7 @@ openclaw migrate claude --dry-run
openclaw migrate codex --dry-run
openclaw migrate codex --skill gog-vault77-google-workspace
openclaw migrate codex --plugin google-calendar --dry-run
openclaw migrate codex --plugin google-calendar --verify-plugin-apps --dry-run
openclaw migrate hermes --dry-run
openclaw migrate hermes
openclaw migrate apply codex --yes --skill gog-vault77-google-workspace
@@ -59,6 +60,9 @@ openclaw onboard --import-from hermes --import-source ~/.hermes
<ParamField path="--plugin <name>" type="string">
Select one Codex plugin install item by plugin name or item id. Repeat the flag to migrate multiple Codex plugins. When omitted, interactive Codex migrations show a native Codex plugin checkbox selector and non-interactive migrations keep all planned plugins. This only applies to source-installed `openai-curated` Codex plugins discovered by the Codex app-server inventory.
</ParamField>
<ParamField path="--verify-plugin-apps" type="boolean">
Codex only. Force a fresh source Codex app-server `app/list` traversal before planning native plugin activation. Off by default to keep migration planning fast.
</ParamField>
<ParamField path="--no-backup" type="boolean">
Skip the pre-apply backup. Requires `--force` when local OpenClaw state exists.
</ParamField>
@@ -125,7 +129,8 @@ your personal Codex CLI state by default.
Running `openclaw migrate codex` in an interactive terminal previews the full
plan, then opens checkbox selectors before the final apply confirmation. Skill
copy items are prompted first. Use `Toggle all on` or `Toggle all off` for bulk
selection; planned skills start checked, conflict skills start unchecked, and
selection. Press Space to toggle rows, or press Enter to activate the highlighted
row and continue. Planned skills start checked, conflict skills start unchecked, and
`Skip for now` skips skill copies for this run while still continuing to plugin
selection. When source-installed curated Codex plugins are migratable and
`--plugin` was not supplied, migration then prompts for native Codex plugin
@@ -156,17 +161,36 @@ openclaw migrate apply codex --yes --plugin google-calendar
- Personal AgentSkills under `$HOME/.agents/skills`, copied into the current
OpenClaw agent workspace when you want per-agent ownership.
- Source-installed `openai-curated` Codex plugins discovered through Codex
app-server `plugin/list`. Apply calls app-server `plugin/install` for each
selected plugin, even if the target app-server already reports that plugin as
installed and enabled. Migrated Codex plugins are usable only in sessions that
select the native Codex harness; they are not exposed to Pi, normal OpenAI
provider runs, ACP conversation bindings, or other harnesses.
app-server `plugin/list`. Planning reads `plugin/read` for each enabled
installed plugin. App-backed plugins require the source Codex app-server
account response to be a ChatGPT subscription account; non-ChatGPT or missing
account responses are skipped with `codex_subscription_required`. By default,
migration does not call source `app/list`, so app-backed plugins that pass the
account gate are planned without source app accessibility verification, and
account lookup transport failures skip with `codex_account_unavailable`. Pass
`--verify-plugin-apps` when you want migration to force a fresh source
`app/list` snapshot and require every owned app to be present, enabled, and
accessible before planning native activation. In that mode, account lookup
transport failures fall through to source app inventory verification. The
source app inventory snapshot is kept in memory for the current process; it
is not written to migration output or target config. Disabled plugins,
unreadable plugin details, subscription-gated source accounts, and, when
verification is requested, missing apps, disabled apps, inaccessible apps, or
source app inventory failures become manual skipped items with typed reasons
instead of target config entries.
Apply calls app-server `plugin/install` for each selected eligible plugin,
even if the target app-server already reports that plugin as installed and
enabled. Migrated Codex plugins are usable only in sessions that select the
native Codex harness; they are not exposed to Pi, normal OpenAI provider runs,
ACP conversation bindings, or other harnesses.
### Manual-review Codex state
Codex `config.toml`, native `hooks/hooks.json`, non-curated marketplaces, and
cached plugin bundles that are not source-installed curated plugins are not
activated automatically. They are copied or reported in the migration report for
Codex `config.toml`, native `hooks/hooks.json`, non-curated marketplaces, cached
plugin bundles that are not source-installed curated plugins, and source-installed
plugins that fail the source subscription gate are not activated automatically.
When `--verify-plugin-apps` is set, plugins that fail the source app-inventory
gate are also skipped. They are copied or reported in the migration report for
manual review.
For migrated source-installed curated plugins, apply writes:
@@ -178,7 +202,13 @@ For migrated source-installed curated plugins, apply writes:
`pluginName` for each selected plugin
Migration never writes `plugins["*"]` and never stores local marketplace cache
paths. Auth-required installs are reported on the affected plugin item with
paths. Source-side subscription failures are reported on manual items with typed
reasons such as `codex_subscription_required`, `codex_account_unavailable`,
`plugin_disabled`, or `plugin_read_unavailable`. With `--verify-plugin-apps`,
source app-inventory failures can also appear as `app_inaccessible`,
`app_disabled`, `app_missing`, or `app_inventory_unavailable`. Skipped plugins
are not written to target config.
Target-side auth-required installs are reported on the affected plugin item with
`status: "skipped"`, `reason: "auth_required"`, and sanitized app identifiers.
Their explicit config entries are written disabled until you reauthorize and
enable them. Other install failures are item-scoped `error` results.

View File

@@ -201,6 +201,10 @@ Set `stream: true` to receive Server-Sent Events (SSE):
- `tool_choice`: `"auto"`, `"none"`
- `messages[*].role: "tool"` follow-up turns
- `messages[*].tool_call_id` for binding tool results back to a prior tool call
- `max_completion_tokens`: number; per-call cap for total completion tokens (reasoning tokens included). Current OpenAI Chat Completions field name; preferred when both `max_completion_tokens` and `max_tokens` are sent.
- `max_tokens`: number; legacy alias accepted for backwards compatibility. Ignored when `max_completion_tokens` is also present.
When either field is set, the value is forwarded to the upstream provider via the agent stream-param channel. The actual wire field name sent to the upstream provider is chosen by the provider transport: `max_completion_tokens` for OpenAI-family endpoints, and `max_tokens` for providers that only accept the legacy name (such as Mistral and Chutes).
### Unsupported variants

View File

@@ -476,7 +476,9 @@ enumeration of `src/gateway/server-methods/*.ts`.
### Common event families
- `chat`: UI chat updates such as `chat.inject` and other transcript-only chat
events.
events. In protocol v4, delta payloads carry `deltaText`; `message` remains
the cumulative assistant snapshot. Non-prefix replacements set `replace=true`
and use `deltaText` as the replacement text.
- `session.message` and `session.tool`: transcript/event-stream updates for a
subscribed session.
- `sessions.changed`: session index or metadata changed.
@@ -632,8 +634,8 @@ terminal summary, and sanitized error text.
- `PROTOCOL_VERSION` lives in `src/gateway/protocol/version.ts`.
- Clients send `minProtocol` + `maxProtocol`; the server rejects ranges that
do not include its current protocol. Native clients use a v3 lower bound so
additive v4 clients can still reach v3 gateways.
do not include its current protocol. Current clients and servers require
protocol v4.
- Schemas + models are generated from TypeBox definitions:
- `pnpm protocol:gen`
- `pnpm protocol:gen:swift`
@@ -647,7 +649,7 @@ stable across protocol v4 and are the expected baseline for third-party clients.
| Constant | Default | Source |
| ----------------------------------------- | ----------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| `PROTOCOL_VERSION` | `4` | `src/gateway/protocol/version.ts` |
| `MIN_CLIENT_PROTOCOL_VERSION` | `3` | `src/gateway/protocol/version.ts` |
| `MIN_CLIENT_PROTOCOL_VERSION` | `4` | `src/gateway/protocol/version.ts` |
| Request timeout (per RPC) | `30_000` ms | `src/gateway/client.ts` (`requestTimeoutMs`) |
| Preauth / connect-challenge timeout | `15_000` ms | `src/gateway/handshake-timeouts.ts` (config/env can raise the paired server/client budget) |
| Initial reconnect backoff | `1_000` ms | `src/gateway/client.ts` (`backoffMs`) |

View File

@@ -39,6 +39,13 @@ Preview migration from the source Codex home:
openclaw migrate codex --dry-run
```
Use strict source app verification when you want migration to check source app
accessibility before planning native plugin activation:
```bash
openclaw migrate codex --dry-run --verify-plugin-apps
```
Apply the migration when the plan looks right:
```bash
@@ -87,8 +94,19 @@ The integration has three separate states:
- Accessible: Codex app-server confirms the plugin's app entries are available
for the active account and can be mapped to the migrated plugin identity.
Migration is the durable install/eligibility step. Runtime app inventory is the
accessibility check. Codex harness session setup then computes a restrictive
Migration is the durable install/eligibility step. During planning, OpenClaw
reads source Codex `plugin/read` details and checks that the source Codex
app-server account response is a ChatGPT subscription account. Non-ChatGPT or
missing account responses skip app-backed plugins with
`codex_subscription_required`. By default, migration does not call source
`app/list`; app-backed source plugins that pass the account gate are planned
without source app accessibility verification, and account lookup transport
failures skip with `codex_account_unavailable`. With `--verify-plugin-apps`,
migration takes a fresh source `app/list` snapshot and requires every owned app
to be present, enabled, and accessible before planning native activation. In
that mode, account lookup transport failures fall through to the source
app-inventory gate. Runtime app inventory is the target-session accessibility
check after migration. Codex harness session setup then computes a restrictive
thread app config for the enabled and accessible plugin apps.
Thread app config is computed when OpenClaw establishes a Codex harness session
@@ -100,6 +118,12 @@ V1 is intentionally narrow:
- Only `openai-curated` plugins that were already installed in the source Codex
app-server inventory are migration-eligible.
- App-backed source plugins must pass the migration-time subscription gate.
`--verify-plugin-apps` adds the source app-inventory gate. Subscription-gated
accounts plus, in verification mode, inaccessible, disabled, missing source
apps or source app-inventory refresh failures are reported as skipped manual
items instead of enabled config entries. Unreadable plugin details are skipped
before the source app-inventory gate.
- Migration writes explicit plugin identities with `marketplaceName` and
`pluginName`; it does not write local `marketplacePath` cache paths.
- `codexPlugins.enabled` is the global enablement switch.
@@ -111,7 +135,18 @@ V1 is intentionally narrow:
## App inventory and ownership
OpenClaw reads Codex app inventory through app-server `app/list`, caches it for
one hour, and refreshes stale or missing entries asynchronously.
one hour, and refreshes stale or missing entries asynchronously. The cache is
in memory only; restarting the CLI or gateway drops it, and OpenClaw rebuilds it
from the next `app/list` read.
Migration and runtime use separate cache keys:
- Source migration verification uses the source Codex home and source app-server
start options. This runs only when `--verify-plugin-apps` is set, and it
forces a fresh source `app/list` traversal for that planning run.
- Target runtime setup uses the target agent's Codex app-server identity when it
builds the Codex thread app config. Plugin activation invalidates that target
cache key and then force-refreshes it after `plugin/install`.
A plugin app is exposed only when OpenClaw can map it back to the migrated
plugin through stable ownership:
@@ -161,6 +196,27 @@ plugins, while unsafe schemas and ambiguous ownership still fail closed:
needs authentication. The explicit plugin entry is written disabled until you
reauthorize and enable it.
**`app_inaccessible`, `app_disabled`, or `app_missing`:**
migration did not install the plugin because the source Codex app inventory did
not show all owned apps as present, enabled, and accessible while
`--verify-plugin-apps` was set. Reauthorize or enable the app in Codex, then
rerun migration with `--verify-plugin-apps`.
**`app_inventory_unavailable`:** migration did not install the plugin because
strict source app verification was requested and source Codex app inventory
refresh failed. Fix source Codex app-server access or retry without
`--verify-plugin-apps` if you accept the faster account-gated plan.
**`codex_subscription_required`:** migration did not install the app-backed
plugin because the source Codex app-server account was not logged in with a
ChatGPT subscription account. Log in to the Codex app with subscription auth,
then rerun migration.
**`codex_account_unavailable`:** migration did not install the app-backed plugin
because the source Codex app-server account could not be read. Fix source Codex
app-server auth or rerun with `--verify-plugin-apps` if you want source app
inventory to decide eligibility when account lookup fails.
**`marketplace_missing` or `plugin_missing`:** the target Codex app-server
cannot see the expected `openai-curated` marketplace or plugin. Rerun migration
against the target runtime or inspect Codex app-server plugin status.

View File

@@ -166,7 +166,7 @@ uninstall, and publishing commands.
| [tlon](/plugins/reference/tlon) | Adds the Tlon channel surface for sending and receiving OpenClaw messages. | `@openclaw/tlon`<br />npm; ClawHub | channels: tlon; contracts: tools; skills |
| [twitch](/plugins/reference/twitch) | Adds the Twitch channel surface for sending and receiving OpenClaw messages. | `@openclaw/twitch`<br />npm; ClawHub | channels: twitch |
| [voice-call](/plugins/reference/voice-call) | Adds agent-callable tools. | `@openclaw/voice-call`<br />npm; ClawHub | contracts: tools |
| [whatsapp](/plugins/reference/whatsapp) | Adds the WhatsApp channel surface for sending and receiving OpenClaw messages. | `@openclaw/whatsapp`<br />npm; ClawHub | channels: whatsapp |
| [whatsapp](/plugins/reference/whatsapp) | Adds the WhatsApp channel surface for sending and receiving OpenClaw messages. | `@openclaw/whatsapp`<br />ClawHub: `clawhub:@openclaw/whatsapp`; npm | channels: whatsapp |
| [zalo](/plugins/reference/zalo) | Adds the Zalo channel surface for sending and receiving OpenClaw messages. | `@openclaw/zalo`<br />npm; ClawHub | channels: zalo |
| [zalouser](/plugins/reference/zalouser) | Adds the Zalo Personal channel surface for sending and receiving OpenClaw messages. | `@openclaw/zalouser`<br />npm; ClawHub | channels: zalouser; contracts: tools |

View File

@@ -128,7 +128,7 @@ pnpm plugins:inventory:gen
| [vydra](/plugins/reference/vydra) | Adds Vydra model provider support to OpenClaw. | `@openclaw/vydra-provider`<br />included in OpenClaw | providers: vydra; contracts: imageGenerationProviders, speechProviders, videoGenerationProviders |
| [web-readability](/plugins/reference/web-readability) | Extract readable article content from local HTML web fetch responses. | `@openclaw/web-readability-plugin`<br />included in OpenClaw | contracts: webContentExtractors |
| [webhooks](/plugins/reference/webhooks) | Authenticated inbound webhooks that bind external automation to OpenClaw TaskFlows. | `@openclaw/webhooks`<br />included in OpenClaw | plugin |
| [whatsapp](/plugins/reference/whatsapp) | Adds the WhatsApp channel surface for sending and receiving OpenClaw messages. | `@openclaw/whatsapp`<br />npm; ClawHub | channels: whatsapp |
| [whatsapp](/plugins/reference/whatsapp) | Adds the WhatsApp channel surface for sending and receiving OpenClaw messages. | `@openclaw/whatsapp`<br />ClawHub: `clawhub:@openclaw/whatsapp`; npm | channels: whatsapp |
| [xai](/plugins/reference/xai) | Adds xAI model provider support to OpenClaw. | `@openclaw/xai-plugin`<br />included in OpenClaw | providers: xai; contracts: imageGenerationProviders, mediaUnderstandingProviders, realtimeTranscriptionProviders, speechProviders, tools, videoGenerationProviders, webSearchProviders |
| [xiaomi](/plugins/reference/xiaomi) | Adds Xiaomi model provider support to OpenClaw. | `@openclaw/xiaomi-provider`<br />included in OpenClaw | providers: xiaomi; contracts: speechProviders |
| [zai](/plugins/reference/zai) | Adds Z.AI model provider support to OpenClaw. | `@openclaw/zai-provider`<br />included in OpenClaw | providers: zai; contracts: mediaUnderstandingProviders |

View File

@@ -12,22 +12,12 @@ Adds the WhatsApp channel surface for sending and receiving OpenClaw messages.
## Distribution
- Package: `@openclaw/whatsapp`
- Install route: npm; ClawHub
- Install route: ClawHub: `clawhub:@openclaw/whatsapp`; npm
## Surface
channels: whatsapp
## Windows install note
On Windows, the WhatsApp plugin needs Git on `PATH` during npm install because one of its Baileys/libsignal dependencies is fetched from a git URL. Install Git for Windows, then restart the shell and rerun the install:
```powershell
winget install --id Git.Git -e
```
Portable Git also works if its `bin` directory is on `PATH`.
## Related docs
- [whatsapp](/channels/whatsapp)

View File

@@ -20,7 +20,7 @@ SGLang serves open-weight models via an OpenAI-compatible HTTP API. OpenClaw con
| Streaming usage | Yes (`supportsStreamingUsage: true`) |
| Pricing | Marked external-free (`modelPricing.external: false`) |
OpenClaw also **auto-discovers** available models from SGLang when you opt in with `SGLANG_API_KEY` and you do not define an explicit `models.providers.sglang` entry — see [Model discovery (implicit provider)](#model-discovery-implicit-provider) below.
OpenClaw also **auto-discovers** available models from SGLang when you opt in with `SGLANG_API_KEY`. Use `sglang/*` in `agents.defaults.models` to keep discovery dynamic when you also configure a custom SGLang base URL. See [Model discovery (implicit provider)](#model-discovery-implicit-provider) below.
## Getting started
@@ -71,8 +71,10 @@ define `models.providers.sglang`, OpenClaw will query:
and convert the returned IDs into model entries.
<Note>
If you set `models.providers.sglang` explicitly, auto-discovery is skipped and
you must define models manually.
If you set `models.providers.sglang` explicitly, OpenClaw uses your declared
models by default. Add `"sglang/*": {}` to `agents.defaults.models` when you
want OpenClaw to query that configured provider's `/models` endpoint and include
all advertised SGLang models.
</Note>
## Explicit configuration (manual models)

View File

@@ -8,7 +8,7 @@ title: "vLLM"
vLLM can serve open-source (and some custom) models via an **OpenAI-compatible** HTTP API. OpenClaw connects to vLLM using the `openai-completions` API.
OpenClaw can also **auto-discover** available models from vLLM when you opt in with `VLLM_API_KEY` (any value works if your server does not enforce auth) and you do not define an explicit `models.providers.vllm` entry.
OpenClaw can also **auto-discover** available models from vLLM when you opt in with `VLLM_API_KEY` (any value works if your server does not enforce auth). Use `vllm/*` in `agents.defaults.models` to keep discovery dynamic when you also configure a custom vLLM base URL.
OpenClaw treats `vllm` as a local OpenAI-compatible provider that supports
streamed usage accounting, so status/context token counts can update from
@@ -72,7 +72,7 @@ GET http://127.0.0.1:8000/v1/models
and converts the returned IDs into model entries.
<Note>
If you set `models.providers.vllm` explicitly, auto-discovery is skipped and you must define models manually.
If you set `models.providers.vllm` explicitly, OpenClaw uses your declared models by default. Add `"vllm/*": {}` to `agents.defaults.models` when you want OpenClaw to query that configured provider's `/models` endpoint and include all advertised vLLM models.
</Note>
## Explicit configuration (manual models)
@@ -111,6 +111,21 @@ Use explicit config when:
}
```
To keep this provider dynamic without manually listing every model, add a provider
wildcard to the visible model catalog:
```json5
{
agents: {
defaults: {
models: {
"vllm/*": {},
},
},
},
}
```
## Advanced configuration
<AccordionGroup>
@@ -331,7 +346,7 @@ Use explicit config when:
</Accordion>
<Accordion title="No models discovered">
Auto-discovery requires `VLLM_API_KEY` to be set **and** no explicit `models.providers.vllm` config entry. If you have defined the provider manually, OpenClaw skips discovery and uses only your declared models.
Auto-discovery requires `VLLM_API_KEY` to be set. If you have defined `models.providers.vllm`, OpenClaw uses only your declared models unless `agents.defaults.models` includes `"vllm/*": {}`.
</Accordion>
<Accordion title="Tools render as raw text">

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/acpx",
"version": "2026.5.12-beta.1",
"version": "2026.5.12-beta.5",
"description": "OpenClaw ACP runtime backend",
"repository": {
"type": "git",
@@ -26,10 +26,10 @@
"minHostVersion": ">=2026.4.25"
},
"compat": {
"pluginApi": ">=2026.5.12-beta.1"
"pluginApi": ">=2026.5.12-beta.5"
},
"build": {
"openclawVersion": "2026.5.12-beta.1",
"openclawVersion": "2026.5.12-beta.5",
"staticAssets": [
{
"source": "./src/runtime-internals/mcp-proxy.mjs",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/alibaba-provider",
"version": "2026.5.12-beta.1",
"version": "2026.5.12-beta.5",
"private": true,
"description": "OpenClaw Alibaba Model Studio video provider plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/amazon-bedrock-mantle-provider",
"version": "2026.5.12-beta.1",
"version": "2026.5.12-beta.5",
"private": true,
"description": "OpenClaw Amazon Bedrock Mantle (OpenAI-compatible) provider plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/amazon-bedrock-provider",
"version": "2026.5.12-beta.1",
"version": "2026.5.12-beta.5",
"private": true,
"description": "OpenClaw Amazon Bedrock provider plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/anthropic-vertex-provider",
"version": "2026.5.12-beta.1",
"version": "2026.5.12-beta.5",
"private": true,
"description": "OpenClaw Anthropic Vertex provider plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/anthropic-provider",
"version": "2026.5.12-beta.1",
"version": "2026.5.12-beta.5",
"private": true,
"description": "OpenClaw Anthropic provider plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/arcee-provider",
"version": "2026.5.12-beta.1",
"version": "2026.5.12-beta.5",
"private": true,
"description": "OpenClaw Arcee provider plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/azure-speech",
"version": "2026.5.12-beta.1",
"version": "2026.5.12-beta.5",
"private": true,
"description": "OpenClaw Azure Speech plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/bonjour",
"version": "2026.5.12-beta.1",
"version": "2026.5.12-beta.5",
"description": "OpenClaw Bonjour/mDNS gateway discovery",
"type": "module",
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/brave-plugin",
"version": "2026.5.12-beta.1",
"version": "2026.5.12-beta.5",
"description": "OpenClaw Brave plugin",
"repository": {
"type": "git",
@@ -20,10 +20,10 @@
"minHostVersion": ">=2026.4.10"
},
"compat": {
"pluginApi": ">=2026.5.12-beta.1"
"pluginApi": ">=2026.5.12-beta.5"
},
"build": {
"openclawVersion": "2026.5.12-beta.1"
"openclawVersion": "2026.5.12-beta.5"
},
"release": {
"publishToClawHub": true,

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/browser-plugin",
"version": "2026.5.12-beta.1",
"version": "2026.5.12-beta.5",
"private": true,
"description": "OpenClaw browser tool plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/byteplus-provider",
"version": "2026.5.12-beta.1",
"version": "2026.5.12-beta.5",
"private": true,
"description": "OpenClaw BytePlus provider plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/canvas-plugin",
"version": "2026.5.12-beta.1",
"version": "2026.5.12-beta.5",
"private": true,
"description": "OpenClaw Canvas plugin",
"type": "module",

View File

@@ -1 +1 @@
6f42494c638de9f72ce783550b4a7c62912c26d47293641e588625afa06db370
d71c027b0bfedc047d1cc1950615b9b9ef2e56e232e9918e30c54113229dc792

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/cerebras-provider",
"version": "2026.5.12-beta.1",
"version": "2026.5.12-beta.5",
"private": true,
"description": "OpenClaw Cerebras provider plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/chutes-provider",
"version": "2026.5.12-beta.1",
"version": "2026.5.12-beta.5",
"private": true,
"description": "OpenClaw Chutes.ai provider plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/clickclack",
"version": "2026.5.12-beta.1",
"version": "2026.5.12-beta.5",
"private": true,
"description": "OpenClaw ClickClack channel plugin",
"type": "module",
@@ -18,7 +18,7 @@
"openclaw": "workspace:*"
},
"peerDependencies": {
"openclaw": ">=2026.5.12-beta.1"
"openclaw": ">=2026.5.12-beta.5"
},
"peerDependenciesMeta": {
"openclaw": {

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/cloudflare-ai-gateway-provider",
"version": "2026.5.12-beta.1",
"version": "2026.5.12-beta.5",
"private": true,
"description": "OpenClaw Cloudflare AI Gateway provider plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/codex",
"version": "2026.5.12-beta.1",
"version": "2026.5.12-beta.5",
"description": "OpenClaw Codex harness and model provider plugin",
"repository": {
"type": "git",
@@ -27,10 +27,10 @@
"minHostVersion": ">=2026.5.1-beta.1"
},
"compat": {
"pluginApi": ">=2026.5.12-beta.1"
"pluginApi": ">=2026.5.12-beta.5"
},
"build": {
"openclawVersion": "2026.5.12-beta.1"
"openclawVersion": "2026.5.12-beta.5"
},
"release": {
"publishToClawHub": true,

View File

@@ -550,6 +550,25 @@ describe("bridgeCodexAppServerStartOptions", () => {
}
});
it("leaves native app-server auth untouched when auth bridging is disabled", async () => {
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-codex-app-server-"));
const request = vi.fn(async () => ({ requiresOpenaiAuth: true }));
try {
vi.stubEnv("OPENAI_API_KEY", "env-api-key");
await applyCodexAppServerAuthProfile({
client: { request } as never,
agentDir,
authProfileId: null,
startOptions: createStartOptions(),
});
expect(request).not.toHaveBeenCalled();
} finally {
await fs.rm(agentDir, { recursive: true, force: true });
}
});
it("applies a normal OpenAI API-key profile as a Codex app-server backup", async () => {
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-codex-app-server-"));
const request = vi.fn(async () => ({ type: "apiKey" }));

View File

@@ -42,7 +42,7 @@ type AuthProfileOrderConfig = Parameters<typeof resolveAuthProfileOrder>[0]["cfg
export async function bridgeCodexAppServerStartOptions(params: {
startOptions: CodexAppServerStartOptions;
agentDir: string;
authProfileId?: string;
authProfileId?: string | null;
config?: AuthProfileOrderConfig;
}): Promise<CodexAppServerStartOptions> {
if (params.startOptions.transport !== "stdio") {
@@ -52,6 +52,9 @@ export async function bridgeCodexAppServerStartOptions(params: {
params.startOptions,
params.agentDir,
);
if (params.authProfileId === null) {
return isolatedStartOptions;
}
const store = ensureCodexAppServerAuthProfileStore({
agentDir: params.agentDir,
authProfileId: params.authProfileId,
@@ -291,10 +294,13 @@ function withoutClearedCodexIsolationEnv(clearEnv: string[] | undefined): string
export async function applyCodexAppServerAuthProfile(params: {
client: CodexAppServerClient;
agentDir: string;
authProfileId?: string;
authProfileId?: string | null;
startOptions?: CodexAppServerStartOptions;
config?: AuthProfileOrderConfig;
}): Promise<void> {
if (params.authProfileId === null) {
return;
}
const loginParams = await resolveCodexAppServerAuthProfileLoginParams({
agentDir: params.agentDir,
authProfileId: params.authProfileId,

View File

@@ -0,0 +1,74 @@
import { createHash } from "node:crypto";
import {
buildCodexAppInventoryCacheKey,
type CodexAppInventoryCacheKeyInput,
} from "./app-inventory-cache.js";
import { resolveCodexAppServerHomeDir } from "./auth-bridge.js";
import type { CodexAppServerRuntimeOptions, CodexAppServerStartOptions } from "./config.js";
export type CodexPluginAppCacheKeyParams = Omit<
CodexAppInventoryCacheKeyInput,
"codexHome" | "endpoint"
> & {
appServer: Pick<CodexAppServerRuntimeOptions, "start">;
agentDir?: string;
};
export function buildCodexPluginAppCacheKey(params: CodexPluginAppCacheKeyParams): string {
return buildCodexAppInventoryCacheKey({
codexHome: resolveCodexPluginAppCacheCodexHome(params.appServer, params.agentDir),
endpoint: resolveCodexPluginAppCacheEndpoint(params.appServer),
authProfileId: params.authProfileId,
accountId: params.accountId,
envApiKeyFingerprint: params.envApiKeyFingerprint,
appServerVersion: params.appServerVersion,
});
}
export function resolveCodexPluginAppCacheEndpoint(
appServer: Pick<CodexAppServerRuntimeOptions, "start">,
): string {
return JSON.stringify({
transport: appServer.start.transport,
command: appServer.start.command,
args: appServer.start.args,
url: appServer.start.url ?? null,
credentialFingerprint: fingerprintCodexPluginAppCacheCredentials(appServer.start),
});
}
export function resolveCodexPluginAppCacheCodexHome(
appServer: Pick<CodexAppServerRuntimeOptions, "start">,
agentDir?: string,
): string | undefined {
const configuredCodexHome = appServer.start.env?.CODEX_HOME?.trim();
if (configuredCodexHome) {
return configuredCodexHome;
}
return appServer.start.transport === "stdio" && agentDir
? resolveCodexAppServerHomeDir(agentDir)
: undefined;
}
function fingerprintCodexPluginAppCacheCredentials(
startOptions: CodexAppServerStartOptions,
): string | null {
const authToken = startOptions.authToken ?? "";
const headers = Object.entries(startOptions.headers)
.map(([key, value]) => [key.toLowerCase(), value] as const)
.toSorted(([left], [right]) => left.localeCompare(right));
if (!authToken && headers.length === 0) {
return null;
}
const hash = createHash("sha256");
hash.update("openclaw:codex:plugin-app-cache-credentials:v1");
hash.update("\0");
hash.update(authToken);
for (const [key, value] of headers) {
hash.update("\0");
hash.update(key);
hash.update("\0");
hash.update(value);
}
return `sha256:${hash.digest("hex")}`;
}

View File

@@ -17,7 +17,8 @@ export async function requestCodexAppServerJson<M extends CodexAppServerRequestM
requestParams: CodexAppServerRequestParams<M>;
timeoutMs?: number;
startOptions?: CodexAppServerStartOptions;
authProfileId?: string;
authProfileId?: string | null;
agentDir?: string;
config?: Parameters<typeof resolveCodexAppServerAuthProfileIdForAgent>[0]["config"];
isolated?: boolean;
}): Promise<CodexAppServerRequestResult<M>>;
@@ -26,7 +27,8 @@ export async function requestCodexAppServerJson<T = JsonValue | undefined>(param
requestParams?: unknown;
timeoutMs?: number;
startOptions?: CodexAppServerStartOptions;
authProfileId?: string;
authProfileId?: string | null;
agentDir?: string;
config?: Parameters<typeof resolveCodexAppServerAuthProfileIdForAgent>[0]["config"];
isolated?: boolean;
}): Promise<T>;
@@ -35,7 +37,8 @@ export async function requestCodexAppServerJson<T = JsonValue | undefined>(param
requestParams?: unknown;
timeoutMs?: number;
startOptions?: CodexAppServerStartOptions;
authProfileId?: string;
authProfileId?: string | null;
agentDir?: string;
config?: Parameters<typeof resolveCodexAppServerAuthProfileIdForAgent>[0]["config"];
isolated?: boolean;
}): Promise<T> {
@@ -48,13 +51,19 @@ export async function requestCodexAppServerJson<T = JsonValue | undefined>(param
startOptions: params.startOptions,
timeoutMs,
authProfileId: params.authProfileId,
agentDir: params.agentDir,
config: params.config,
});
try {
return await client.request<T>(params.method, params.requestParams, { timeoutMs });
} finally {
if (params.isolated) {
client.close();
// Wait for the child to actually exit (with a SIGKILL fallback) so
// the parent process doesn't hang on an orphaned codex app-server.
// The stdio bin shim does not always propagate stdin EOF to the
// underlying codex binary, so the unref'd close() path can leave
// the child running and keep the parent's event loop alive.
await client.closeAndWait({ exitTimeoutMs: 2_000, forceKillDelayMs: 250 });
}
}
})(),

View File

@@ -25,20 +25,18 @@ function queueActiveRunMessageForTest(
return queueAgentHarnessMessage(...args);
}
import { CODEX_GPT5_BEHAVIOR_CONTRACT } from "../../prompt-overlay.js";
import {
buildCodexAppInventoryCacheKey,
defaultCodexAppInventoryCache,
} from "./app-inventory-cache.js";
import {
resolveCodexAppServerEnvApiKeyCacheKey,
resolveCodexAppServerHomeDir,
} from "./auth-bridge.js";
import { defaultCodexAppInventoryCache } from "./app-inventory-cache.js";
import { resolveCodexAppServerEnvApiKeyCacheKey } from "./auth-bridge.js";
import { readCodexPluginConfig, resolveCodexAppServerRuntimeOptions } from "./config.js";
import {
CODEX_OPENCLAW_DYNAMIC_TOOL_NAMESPACE,
createCodexDynamicToolBridge,
} from "./dynamic-tools.js";
import * as elicitationBridge from "./elicitation-bridge.js";
import {
buildCodexPluginAppCacheKey,
resolveCodexPluginAppCacheEndpoint,
} from "./plugin-app-cache-key.js";
import type { CodexServerNotification } from "./protocol.js";
import { rememberCodexRateLimits, resetCodexRateLimitCacheForTests } from "./rate-limit-cache.js";
import { runCodexAppServerAttempt, __testing } from "./run-attempt.js";
@@ -683,6 +681,47 @@ describe("runCodexAppServerAttempt", () => {
}
});
it("passes auth profiles into Codex dynamic tool construction", async () => {
const sessionFile = path.join(tempDir, "session.jsonl");
const workspaceDir = path.join(tempDir, "workspace");
const params = createParams(sessionFile, workspaceDir);
const authProfileStore = {
version: 1,
profiles: {
"openai:api-key-backup": {
provider: "openai",
type: "api_key",
key: "not-a-real-key",
},
},
} satisfies EmbeddedRunAttemptParams["authProfileStore"];
params.disableTools = false;
params.authProfileStore = authProfileStore;
const factoryOptions: unknown[] = [];
__testing.setOpenClawCodingToolsFactoryForTests((options) => {
factoryOptions.push(options);
return [];
});
await __testing.buildDynamicTools({
params,
resolvedWorkspace: workspaceDir,
effectiveWorkspace: workspaceDir,
sandboxSessionKey: params.sessionKey!,
sandbox: null as never,
runAbortController: new AbortController(),
sessionAgentId: "main",
pluginConfig: {},
onYieldDetected: () => undefined,
});
expect(factoryOptions).toHaveLength(1);
expect(factoryOptions[0]).toMatchObject({
authProfileStore,
});
});
it("normalizes Codex dynamic toolsAllow entries before filtering", () => {
const tools = ["exec", "apply_patch", "read", "message"].map((name) => ({ name }));
@@ -3685,9 +3724,9 @@ describe("runCodexAppServerAttempt", () => {
});
defaultCodexAppInventoryCache.clear();
await defaultCodexAppInventoryCache.refreshNow({
key: buildCodexAppInventoryCacheKey({
codexHome: resolveCodexAppServerHomeDir(agentDir),
endpoint: __testing.resolveCodexPluginAppCacheEndpoint(appServer),
key: buildCodexPluginAppCacheKey({
appServer,
agentDir,
}),
request: async () => ({
data: [
@@ -3887,9 +3926,9 @@ describe("runCodexAppServerAttempt", () => {
});
defaultCodexAppInventoryCache.clear();
await defaultCodexAppInventoryCache.refreshNow({
key: buildCodexAppInventoryCacheKey({
codexHome: resolveCodexAppServerHomeDir(agentDir),
endpoint: __testing.resolveCodexPluginAppCacheEndpoint(appServer),
key: buildCodexPluginAppCacheKey({
appServer,
agentDir,
authProfileId,
accountId: "account-work",
}),
@@ -4028,9 +4067,9 @@ describe("runCodexAppServerAttempt", () => {
});
defaultCodexAppInventoryCache.clear();
await defaultCodexAppInventoryCache.refreshNow({
key: buildCodexAppInventoryCacheKey({
codexHome: resolveCodexAppServerHomeDir(agentDir),
endpoint: __testing.resolveCodexPluginAppCacheEndpoint(appServer),
key: buildCodexPluginAppCacheKey({
appServer,
agentDir,
envApiKeyFingerprint: resolveCodexAppServerEnvApiKeyCacheKey({
startOptions: appServer.start,
baseEnv: { CODEX_API_KEY: "old-codex-env-key" },
@@ -5198,7 +5237,7 @@ describe("runCodexAppServerAttempt", () => {
});
it("keys plugin app inventory by websocket credentials without exposing them", () => {
const first = __testing.resolveCodexPluginAppCacheEndpoint({
const first = resolveCodexPluginAppCacheEndpoint({
start: {
transport: "websocket",
command: "codex",
@@ -5207,13 +5246,8 @@ describe("runCodexAppServerAttempt", () => {
authToken: "token-first",
headers: { Authorization: "Bearer first" },
},
requestTimeoutMs: 60_000,
turnCompletionIdleTimeoutMs: 5,
approvalPolicy: "never",
approvalsReviewer: "user",
sandbox: "workspace-write",
});
const second = __testing.resolveCodexPluginAppCacheEndpoint({
const second = resolveCodexPluginAppCacheEndpoint({
start: {
transport: "websocket",
command: "codex",
@@ -5222,11 +5256,6 @@ describe("runCodexAppServerAttempt", () => {
authToken: "token-second",
headers: { Authorization: "Bearer second" },
},
requestTimeoutMs: 60_000,
turnCompletionIdleTimeoutMs: 5,
approvalPolicy: "never",
approvalsReviewer: "user",
sandbox: "workspace-write",
});
expect(first).not.toEqual(second);

View File

@@ -41,16 +41,12 @@ import {
import { markAuthProfileBlockedUntil, resolveAgentDir } from "openclaw/plugin-sdk/agent-runtime";
import { emitTrustedDiagnosticEvent } from "openclaw/plugin-sdk/diagnostic-runtime";
import { pathExists } from "openclaw/plugin-sdk/security-runtime";
import {
buildCodexAppInventoryCacheKey,
defaultCodexAppInventoryCache,
} from "./app-inventory-cache.js";
import { defaultCodexAppInventoryCache } from "./app-inventory-cache.js";
import { handleCodexAppServerApprovalRequest } from "./approval-bridge.js";
import {
refreshCodexAppServerAuthTokens,
resolveCodexAppServerAuthAccountCacheKey,
resolveCodexAppServerEnvApiKeyCacheKey,
resolveCodexAppServerHomeDir,
resolveCodexAppServerAuthProfileId,
resolveCodexAppServerAuthProfileIdForAgent,
} from "./auth-bridge.js";
@@ -87,6 +83,7 @@ import {
buildCodexNativeHookRelayConfig,
CODEX_NATIVE_HOOK_RELAY_EVENTS,
} from "./native-hook-relay.js";
import { buildCodexPluginAppCacheKey } from "./plugin-app-cache-key.js";
import {
buildCodexPluginThreadConfig,
buildCodexPluginThreadConfigInputFingerprint,
@@ -391,50 +388,6 @@ function toCodexTextInput(text: string): CodexUserInput {
return { type: "text", text, text_elements: [] };
}
function resolveCodexPluginAppCacheEndpoint(appServer: CodexAppServerRuntimeOptions): string {
return JSON.stringify({
transport: appServer.start.transport,
command: appServer.start.command,
args: appServer.start.args,
url: appServer.start.url ?? null,
credentialFingerprint: fingerprintCodexPluginAppCacheCredentials(appServer.start),
});
}
function fingerprintCodexPluginAppCacheCredentials(
startOptions: CodexAppServerRuntimeOptions["start"],
): string | null {
const authToken = startOptions.authToken ?? "";
const headers = Object.entries(startOptions.headers)
.map(([key, value]) => [key.toLowerCase(), value] as const)
.toSorted(([left], [right]) => left.localeCompare(right));
if (!authToken && headers.length === 0) {
return null;
}
const hash = createHash("sha256");
hash.update("openclaw:codex:plugin-app-cache-credentials:v1");
hash.update("\0");
hash.update(authToken);
for (const [key, value] of headers) {
hash.update("\0");
hash.update(key);
hash.update("\0");
hash.update(value);
}
return `sha256:${hash.digest("hex")}`;
}
function resolveCodexPluginAppCacheCodexHome(
appServer: CodexAppServerRuntimeOptions,
agentDir: string,
): string | undefined {
const configuredCodexHome = appServer.start.env?.CODEX_HOME?.trim();
if (configuredCodexHome) {
return configuredCodexHome;
}
return appServer.start.transport === "stdio" ? resolveCodexAppServerHomeDir(agentDir) : undefined;
}
function restrictCodexAppServerSandboxForOpenClawSandbox(
appServer: CodexAppServerRuntimeOptions,
sandbox: Awaited<ReturnType<typeof resolveSandboxContext>>,
@@ -734,9 +687,9 @@ export async function runCodexAppServerAttempt(
: undefined;
const threadConfig = nativeHookRelayConfig;
const pluginThreadConfigEnabled = shouldBuildCodexPluginThreadConfig(pluginConfig);
const pluginAppCacheKey = buildCodexAppInventoryCacheKey({
codexHome: resolveCodexPluginAppCacheCodexHome(appServer, agentDir),
endpoint: resolveCodexPluginAppCacheEndpoint(appServer),
const pluginAppCacheKey = buildCodexPluginAppCacheKey({
appServer,
agentDir,
authProfileId: startupAuthProfileId,
accountId: startupAuthAccountCacheKey,
envApiKeyFingerprint: startupEnvApiKeyCacheKey,
@@ -2229,6 +2182,7 @@ async function buildDynamicTools(input: DynamicToolBuildParams) {
resolvedWorkspace: input.resolvedWorkspace,
}),
config: params.config,
authProfileStore: params.authProfileStore,
abortSignal: input.runAbortController.signal,
modelProvider: params.model.provider,
modelId: params.modelId,
@@ -3155,7 +3109,6 @@ export const __testing = {
filterToolsForVisionInputs,
handleDynamicToolCallWithTimeout,
resolveDynamicToolCallTimeoutMs,
resolveCodexPluginAppCacheEndpoint,
restrictCodexAppServerSandboxForOpenClawSandbox,
resolveOpenClawCodingToolsSessionKeys,
shouldForceMessageTool,

View File

@@ -39,6 +39,7 @@ let listCodexAppServerModels: typeof import("./models.js").listCodexAppServerMod
let clearSharedCodexAppServerClient: typeof import("./shared-client.js").clearSharedCodexAppServerClient;
let clearSharedCodexAppServerClientIfCurrent: typeof import("./shared-client.js").clearSharedCodexAppServerClientIfCurrent;
let createIsolatedCodexAppServerClient: typeof import("./shared-client.js").createIsolatedCodexAppServerClient;
let getSharedCodexAppServerClient: typeof import("./shared-client.js").getSharedCodexAppServerClient;
let resetSharedCodexAppServerClientForTests: typeof import("./shared-client.js").resetSharedCodexAppServerClientForTests;
async function sendInitializeResult(
@@ -63,6 +64,7 @@ describe("shared Codex app-server client", () => {
clearSharedCodexAppServerClient,
clearSharedCodexAppServerClientIfCurrent,
createIsolatedCodexAppServerClient,
getSharedCodexAppServerClient,
resetSharedCodexAppServerClientForTests,
} = await import("./shared-client.js"));
});
@@ -151,6 +153,39 @@ describe("shared Codex app-server client", () => {
expect(applyCall?.authProfileId).toBe("openai-codex:work");
});
it("skips target auth resolution when native source auth is requested", async () => {
const harness = createClientHarness();
vi.spyOn(CodexAppServerClient, "start").mockReturnValue(harness.client);
const config = { auth: { order: { "openai-codex": ["openai-codex:target"] } } };
const clientPromise = getSharedCodexAppServerClient({
timeoutMs: 1000,
authProfileId: null,
agentDir: "/tmp/openclaw-target-agent",
config,
});
await sendInitializeResult(harness, "openclaw/0.125.0 (macOS; test)");
await expect(clientPromise).resolves.toBe(harness.client);
expect(mocks.resolveCodexAppServerAuthProfileIdForAgent).not.toHaveBeenCalled();
const [bridgeCall] = mocks.bridgeCodexAppServerStartOptions.mock.calls[0] ?? [];
expect(bridgeCall).toEqual(
expect.objectContaining({
agentDir: "/tmp/openclaw-target-agent",
authProfileId: null,
config,
}),
);
const [applyCall] = mocks.applyCodexAppServerAuthProfile.mock.calls[0] ?? [];
expect(applyCall).toEqual(
expect.objectContaining({
agentDir: "/tmp/openclaw-target-agent",
authProfileId: null,
config,
}),
);
});
it("resolves the configured implicit auth profile before sharing a client", async () => {
const harness = createClientHarness();
vi.spyOn(CodexAppServerClient, "start").mockReturnValue(harness.client);

View File

@@ -32,29 +32,34 @@ function getSharedCodexAppServerClientState(): SharedCodexAppServerClientState {
export async function getSharedCodexAppServerClient(options?: {
startOptions?: CodexAppServerStartOptions;
timeoutMs?: number;
authProfileId?: string;
authProfileId?: string | null;
agentDir?: string;
config?: Parameters<typeof resolveCodexAppServerAuthProfileIdForAgent>[0]["config"];
}): Promise<CodexAppServerClient> {
const state = getSharedCodexAppServerClientState();
const agentDir = options?.agentDir ?? resolveDefaultAgentDir(options?.config ?? {});
const authProfileId = resolveCodexAppServerAuthProfileIdForAgent({
authProfileId: options?.authProfileId,
agentDir,
config: options?.config,
});
const usesNativeAuth = options?.authProfileId === null;
const requestedAuthProfileId =
options?.authProfileId === null ? undefined : options?.authProfileId;
const authProfileId = usesNativeAuth
? undefined
: resolveCodexAppServerAuthProfileIdForAgent({
authProfileId: requestedAuthProfileId,
agentDir,
config: options?.config,
});
const requestedStartOptions =
options?.startOptions ?? resolveCodexAppServerRuntimeOptions().start;
const managedStartOptions = await resolveManagedCodexAppServerStartOptions(requestedStartOptions);
const startOptions = await bridgeCodexAppServerStartOptions({
startOptions: managedStartOptions,
agentDir,
authProfileId,
authProfileId: usesNativeAuth ? null : authProfileId,
config: options?.config,
});
const key = codexAppServerStartOptionsKey(startOptions, {
authProfileId,
agentDir,
agentDir: usesNativeAuth ? undefined : agentDir,
});
if (state.key && state.key !== key) {
clearSharedCodexAppServerClient();
@@ -71,7 +76,7 @@ export async function getSharedCodexAppServerClient(options?: {
await applyCodexAppServerAuthProfile({
client,
agentDir,
authProfileId,
authProfileId: usesNativeAuth ? null : authProfileId,
startOptions,
config: options?.config,
});
@@ -100,23 +105,28 @@ export async function getSharedCodexAppServerClient(options?: {
export async function createIsolatedCodexAppServerClient(options?: {
startOptions?: CodexAppServerStartOptions;
timeoutMs?: number;
authProfileId?: string;
authProfileId?: string | null;
agentDir?: string;
config?: Parameters<typeof resolveCodexAppServerAuthProfileIdForAgent>[0]["config"];
}): Promise<CodexAppServerClient> {
const agentDir = options?.agentDir ?? resolveDefaultAgentDir(options?.config ?? {});
const authProfileId = resolveCodexAppServerAuthProfileIdForAgent({
authProfileId: options?.authProfileId,
agentDir,
config: options?.config,
});
const usesNativeAuth = options?.authProfileId === null;
const requestedAuthProfileId =
options?.authProfileId === null ? undefined : options?.authProfileId;
const authProfileId = usesNativeAuth
? undefined
: resolveCodexAppServerAuthProfileIdForAgent({
authProfileId: requestedAuthProfileId,
agentDir,
config: options?.config,
});
const requestedStartOptions =
options?.startOptions ?? resolveCodexAppServerRuntimeOptions().start;
const managedStartOptions = await resolveManagedCodexAppServerStartOptions(requestedStartOptions);
const startOptions = await bridgeCodexAppServerStartOptions({
startOptions: managedStartOptions,
agentDir,
authProfileId,
authProfileId: usesNativeAuth ? null : authProfileId,
config: options?.config,
});
const client = CodexAppServerClient.start(startOptions);
@@ -126,7 +136,7 @@ export async function createIsolatedCodexAppServerClient(options?: {
await applyCodexAppServerAuthProfile({
client,
agentDir,
authProfileId,
authProfileId: usesNativeAuth ? null : authProfileId,
startOptions,
config: options?.config,
});

View File

@@ -21,14 +21,22 @@ import type {
MigrationProviderContext,
} from "openclaw/plugin-sdk/plugin-entry";
import { defaultCodexAppInventoryCache } from "../app-server/app-inventory-cache.js";
import {
resolveCodexAppServerAuthAccountCacheKey,
resolveCodexAppServerAuthProfileIdForAgent,
resolveCodexAppServerEnvApiKeyCacheKey,
} from "../app-server/auth-bridge.js";
import {
CODEX_PLUGINS_MARKETPLACE_NAME,
readCodexPluginConfig,
resolveCodexAppServerRuntimeOptions,
type ResolvedCodexPluginPolicy,
} from "../app-server/config.js";
import {
ensureCodexPluginActivation,
type CodexPluginActivationResult,
} from "../app-server/plugin-activation.js";
import { buildCodexPluginAppCacheKey } from "../app-server/plugin-app-cache-key.js";
import type { v2 } from "../app-server/protocol.js";
import { requestCodexAppServerJson } from "../app-server/request.js";
import { buildCodexMigrationPlan } from "./plan.js";
@@ -40,6 +48,7 @@ import {
readCodexPluginMigrationConfigEntry,
type CodexPluginMigrationConfigEntry,
} from "./plan.js";
import { resolveCodexMigrationTargets } from "./targets.js";
const CODEX_PLUGIN_AUTH_REQUIRED_REASON = "auth_required";
const CODEX_PLUGIN_NOT_SELECTED_REASON = "not selected for migration";
@@ -104,6 +113,8 @@ async function applyCodexPluginInstallItem(
};
}
try {
const appCacheKey = await buildTargetCodexPluginAppCacheKey(ctx);
const appServer = resolveTargetCodexAppServer(ctx);
const result = await ensureCodexPluginActivation({
identity: policy,
installEvenIfActive: true,
@@ -112,10 +123,14 @@ async function applyCodexPluginInstallItem(
method,
requestParams,
timeoutMs: 60_000,
startOptions: appServer.start,
agentDir: resolveCodexMigrationTargets(ctx).agentDir,
config: ctx.config,
isolated: true,
}),
appCache: defaultCodexAppInventoryCache,
appCacheKey,
});
defaultCodexAppInventoryCache.clear();
const baseDetails = {
...item.details,
code: result.reason,
@@ -162,6 +177,38 @@ async function applyCodexPluginInstallItem(
}
}
function resolveTargetCodexAppServer(ctx: MigrationProviderContext) {
return resolveCodexAppServerRuntimeOptions({
pluginConfig: readCodexPluginConfig(ctx.config),
});
}
async function buildTargetCodexPluginAppCacheKey(ctx: MigrationProviderContext): Promise<string> {
const targets = resolveCodexMigrationTargets(ctx);
const appServer = resolveTargetCodexAppServer(ctx);
const authProfileId = resolveCodexAppServerAuthProfileIdForAgent({
agentDir: targets.agentDir,
config: ctx.config,
});
const accountId = await resolveCodexAppServerAuthAccountCacheKey({
authProfileId,
agentDir: targets.agentDir,
config: ctx.config,
});
const envApiKeyFingerprint = authProfileId
? undefined
: resolveCodexAppServerEnvApiKeyCacheKey({
startOptions: appServer.start,
});
return buildCodexPluginAppCacheKey({
appServer,
agentDir: targets.agentDir,
authProfileId,
accountId,
envApiKeyFingerprint,
});
}
async function applyCodexPluginConfigItem(
ctx: MigrationProviderContext,
item: MigrationItem,

View File

@@ -15,6 +15,7 @@ import type {
import { CODEX_PLUGINS_MARKETPLACE_NAME } from "../app-server/config.js";
import { exists, sanitizeName } from "./helpers.js";
import {
codexPluginMigrationSubscriptionWarning,
discoverCodexSource,
hasCodexSource,
type CodexPluginSource,
@@ -33,6 +34,7 @@ const CODEX_PLUGIN_NATIVE_CONFIG_PATH = [
"codexPlugins",
] as const;
const MIGRATION_REASON_PLUGIN_EXISTS = "plugin exists";
const CODEX_PLUGIN_SOURCE_APP_VERIFICATION_UNVERIFIED = "not_run";
export type CodexPluginMigrationConfigEntry = {
configKey: string;
@@ -40,6 +42,13 @@ export type CodexPluginMigrationConfigEntry = {
enabled: boolean;
};
type CodexPluginMigrationBlockSkipDetails = {
pluginName: string;
marketplaceName: typeof CODEX_PLUGINS_MARKETPLACE_NAME;
apps?: NonNullable<CodexPluginSource["migrationBlock"]>["apps"];
error?: string;
};
function uniqueSkillName(skill: CodexSkillSource, counts: Map<string, number>): string {
const base = sanitizeName(skill.name) || "codex-skill";
if ((counts.get(base) ?? 0) <= 1) {
@@ -173,6 +182,9 @@ function buildPluginItems(
pluginName: plugin.pluginName,
sourceInstalled: plugin.installed === true,
sourceEnabled: plugin.enabled === true,
...(plugin.apps && plugin.apps.length > 0 && !shouldVerifyPluginApps(ctx)
? { sourceAppVerification: CODEX_PLUGIN_SOURCE_APP_VERIFICATION_UNVERIFIED }
: {}),
},
}),
);
@@ -180,6 +192,29 @@ function buildPluginItems(
}
manualIndex += 1;
if (plugin.migrationBlock && plugin.pluginName) {
const details: CodexPluginMigrationBlockSkipDetails = {
pluginName: plugin.pluginName,
marketplaceName: CODEX_PLUGINS_MARKETPLACE_NAME,
...(plugin.migrationBlock.apps ? { apps: plugin.migrationBlock.apps } : {}),
...(plugin.migrationBlock.error ? { error: plugin.migrationBlock.error } : {}),
};
items.push(
createMigrationItem({
id: `plugin:${sanitizeName(plugin.name) || sanitizeName(path.basename(plugin.source))}:${manualIndex}`,
kind: "manual",
action: "manual",
source: plugin.source,
status: "skipped",
reason: plugin.migrationBlock.code,
message:
plugin.message ??
`Codex native plugin "${plugin.name}" was found but not activated automatically.`,
details: { ...details },
}),
);
continue;
}
items.push(
createMigrationManualItem({
id: `plugin:${sanitizeName(plugin.name) || sanitizeName(path.basename(plugin.source))}:${manualIndex}`,
@@ -195,6 +230,10 @@ function buildPluginItems(
return items;
}
function shouldVerifyPluginApps(ctx: MigrationProviderContext): boolean {
return ctx.providerOptions?.verifyPluginApps === true;
}
export function readCodexPluginMigrationConfigEntry(
item: MigrationItem,
enabled: boolean,
@@ -345,13 +384,17 @@ function buildPluginConfigItem(
export async function buildCodexMigrationPlan(
ctx: MigrationProviderContext,
): Promise<MigrationPlan> {
const source = await discoverCodexSource(ctx.source);
const targets = resolveCodexMigrationTargets(ctx);
const source = await discoverCodexSource({
input: ctx.source,
evaluatePluginMigrationEligibility: true,
verifyPluginApps: shouldVerifyPluginApps(ctx),
});
if (!hasCodexSource(source)) {
throw new Error(
`Codex state was not found at ${source.root}. Pass --from <path> if it lives elsewhere.`,
);
}
const targets = resolveCodexMigrationTargets(ctx);
const items: MigrationItem[] = [];
items.push(
...(await buildSkillItems({
@@ -386,16 +429,31 @@ export async function buildCodexMigrationPlan(
"Conflicts were found. Re-run with --overwrite to replace conflicting migration targets after item-level backups.",
]
: []),
...(source.plugins.length > 0
...(source.plugins.some((plugin) => plugin.migratable)
? [
"Codex source-installed openai-curated plugins are planned for native activation; cached plugin bundles remain manual-review only.",
]
: []),
...(source.plugins.some(
(plugin) => plugin.migratable && plugin.apps && plugin.apps.length > 0,
) && !shouldVerifyPluginApps(ctx)
? [
"Codex app-backed plugins were planned without source app accessibility verification. Re-run with --verify-plugin-apps to force a fresh source app/list check before planning native plugin activation.",
]
: []),
...(source.plugins.some((plugin) => plugin.sourceKind === "cache")
? ["Codex cached plugin bundles remain manual-review only."]
: []),
...(source.pluginDiscoveryError
? [
`Codex app-server plugin inventory discovery failed: ${source.pluginDiscoveryError}. Cached plugin bundles, if any, are advisory only.`,
]
: []),
...(source.plugins.some(
(plugin) => plugin.migrationBlock?.code === "codex_subscription_required",
)
? [codexPluginMigrationSubscriptionWarning()]
: []),
...(source.archivePaths.length > 0
? [
"Codex config and hook files are archive-only. They are preserved in the migration report, not loaded into OpenClaw automatically.",

View File

@@ -3,8 +3,10 @@ import os from "node:os";
import path from "node:path";
import type { MigrationProviderContext } from "openclaw/plugin-sdk/plugin-entry";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { defaultCodexAppInventoryCache } from "../app-server/app-inventory-cache.js";
import { CODEX_PLUGINS_MARKETPLACE_NAME } from "../app-server/config.js";
import type { v2 } from "../app-server/protocol.js";
import { buildCodexPluginAppCacheKey } from "../app-server/plugin-app-cache-key.js";
import type { CodexGetAccountResponse, v2 } from "../app-server/protocol.js";
import { buildCodexMigrationProvider } from "./provider.js";
const appServerRequest = vi.hoisted(() => vi.fn());
@@ -38,6 +40,7 @@ function makeContext(params: {
stateDir: string;
workspaceDir: string;
overwrite?: boolean;
verifyPluginApps?: boolean;
reportDir?: string;
config?: MigrationProviderContext["config"];
runtime?: MigrationProviderContext["runtime"];
@@ -56,6 +59,7 @@ function makeContext(params: {
source: params.source,
stateDir: params.stateDir,
overwrite: params.overwrite,
providerOptions: params.verifyPluginApps ? { verifyPluginApps: true } : undefined,
reportDir: params.reportDir,
logger,
};
@@ -69,6 +73,14 @@ function findItem(items: readonly { id?: string }[], id: string) {
return item as Record<string, unknown>;
}
function findItemByReason(items: readonly { reason?: string }[], reason: string) {
const item = items.find((entry) => entry.reason === reason);
if (!item) {
throw new Error(`Expected migration item reason ${reason}`);
}
return item as Record<string, unknown>;
}
function expectRecordFields(record: unknown, expected: Record<string, unknown>) {
if (!record || typeof record !== "object") {
throw new Error("Expected record");
@@ -122,9 +134,28 @@ async function createCodexFixture(): Promise<{
return { root, homeDir, codexHome, stateDir, workspaceDir };
}
function sourceAppCacheKey(fixture: { codexHome: string }): string {
return buildCodexPluginAppCacheKey({
appServer: {
start: {
transport: "stdio",
command: "codex",
commandSource: "config",
args: ["app-server", "--listen", "stdio://"],
headers: {},
env: {
CODEX_HOME: fixture.codexHome,
HOME: path.dirname(fixture.codexHome),
},
},
},
});
}
afterEach(async () => {
vi.unstubAllEnvs();
appServerRequest.mockReset();
defaultCodexAppInventoryCache.clear();
for (const root of tempRoots) {
await fs.rm(root, { recursive: true, force: true });
}
@@ -145,6 +176,7 @@ describe("buildCodexMigrationProvider", () => {
source: fixture.codexHome,
stateDir: fixture.stateDir,
workspaceDir: fixture.workspaceDir,
verifyPluginApps: true,
}),
);
@@ -185,9 +217,15 @@ describe("buildCodexMigrationProvider", () => {
it("plans source-installed curated plugins without installing during dry-run", async () => {
const fixture = await createCodexFixture();
appServerRequest.mockResolvedValueOnce(
pluginList([pluginSummary("google-calendar", { installed: true, enabled: true })]),
);
appServerRequest.mockImplementation(async ({ method }: { method: string }) => {
if (method === "plugin/list") {
return pluginList([pluginSummary("google-calendar", { installed: true, enabled: true })]);
}
if (method === "plugin/read") {
return pluginRead("google-calendar");
}
throw new Error(`unexpected request ${method}`);
});
const provider = buildCodexMigrationProvider();
const plan = await provider.plan(
@@ -195,10 +233,11 @@ describe("buildCodexMigrationProvider", () => {
source: fixture.codexHome,
stateDir: fixture.stateDir,
workspaceDir: fixture.workspaceDir,
verifyPluginApps: true,
}),
);
expect(appServerRequest).toHaveBeenCalledTimes(1);
expect(appServerRequest).toHaveBeenCalledTimes(2);
expectRecordFields(mockCallArg(appServerRequest), {
method: "plugin/list",
requestParams: { cwds: [] },
@@ -226,6 +265,588 @@ describe("buildCodexMigrationProvider", () => {
});
});
it("skips source-installed plugins whose owned apps are inaccessible", async () => {
const fixture = await createCodexFixture();
appServerRequest.mockImplementation(
async ({ method, requestParams }: { method: string; requestParams?: unknown }) => {
if (method === "plugin/list") {
return pluginList([pluginSummary("readwise", { installed: true, enabled: true })]);
}
if (method === "plugin/read") {
return pluginRead("readwise", [
pluginApp("asdk_app_readwise", { name: "Readwise", needsAuth: false }),
]);
}
if (method === "account/read") {
return chatGptAccount();
}
if (method === "app/list") {
expectRecordFields(requestParams, { forceRefetch: true });
return appsList([
appInfo("asdk_app_readwise", {
name: "Readwise",
isAccessible: false,
isEnabled: true,
}),
]);
}
throw new Error(`unexpected request ${method}`);
},
);
const provider = buildCodexMigrationProvider();
const plan = await provider.plan(
makeContext({
source: fixture.codexHome,
stateDir: fixture.stateDir,
workspaceDir: fixture.workspaceDir,
verifyPluginApps: true,
}),
);
expect(plan.items.some((item) => item.id === "plugin:readwise")).toBe(false);
expect(plan.items.some((item) => item.id === "config:codex-plugins")).toBe(false);
const manualItem = findItemByReason(plan.items, "app_inaccessible");
expectRecordFields(manualItem, {
kind: "manual",
action: "manual",
status: "skipped",
reason: "app_inaccessible",
});
const details = expectRecordFields(manualItem.details, {
pluginName: "readwise",
marketplaceName: CODEX_PLUGINS_MARKETPLACE_NAME,
});
expect(details).not.toHaveProperty("code");
expect(details.apps).toEqual([
{
id: "asdk_app_readwise",
name: "Readwise",
isAccessible: false,
isEnabled: true,
needsAuth: false,
},
]);
expect(appServerRequest.mock.calls.filter(([arg]) => arg.method === "app/list")).toHaveLength(
1,
);
});
it("plans app-backed plugins without source app/list by default", async () => {
const fixture = await createCodexFixture();
appServerRequest.mockImplementation(async ({ method }: { method: string }) => {
if (method === "plugin/list") {
return pluginList([pluginSummary("gmail", { installed: true, enabled: true })]);
}
if (method === "plugin/read") {
return pluginRead("gmail", [pluginApp("app-gmail", { name: "Gmail", needsAuth: true })]);
}
if (method === "account/read") {
return chatGptAccount();
}
throw new Error(`unexpected request ${method}`);
});
const provider = buildCodexMigrationProvider();
const plan = await provider.plan(
makeContext({
source: fixture.codexHome,
stateDir: fixture.stateDir,
workspaceDir: fixture.workspaceDir,
}),
);
expectRecordFields(findItem(plan.items, "plugin:gmail"), {
kind: "plugin",
action: "install",
status: "planned",
});
expectRecordFields(findItem(plan.items, "config:codex-plugins"), {
kind: "config",
action: "merge",
status: "planned",
});
expect(plan.warnings).toEqual(
expect.arrayContaining([
expect.stringContaining(
"Codex app-backed plugins were planned without source app accessibility verification",
),
]),
);
expect(appServerRequest.mock.calls.filter(([arg]) => arg.method === "app/list")).toHaveLength(
0,
);
});
it("warns and skips app-backed plugins when source Codex account is not ChatGPT subscription auth", async () => {
const fixture = await createCodexFixture();
appServerRequest.mockImplementation(async ({ method }: { method: string }) => {
if (method === "plugin/list") {
return pluginList([pluginSummary("gmail", { installed: true, enabled: true })]);
}
if (method === "plugin/read") {
return pluginRead("gmail", [pluginApp("app-gmail", { name: "Gmail", needsAuth: true })]);
}
if (method === "account/read") {
return {
account: { type: "apiKey" },
requiresOpenaiAuth: true,
} satisfies CodexGetAccountResponse;
}
throw new Error(`unexpected request ${method}`);
});
const provider = buildCodexMigrationProvider();
const plan = await provider.plan(
makeContext({
source: fixture.codexHome,
stateDir: fixture.stateDir,
workspaceDir: fixture.workspaceDir,
}),
);
expect(plan.items.some((item) => item.id === "plugin:gmail")).toBe(false);
expect(plan.items.some((item) => item.id === "config:codex-plugins")).toBe(false);
const manualItem = findItemByReason(plan.items, "codex_subscription_required");
expectRecordFields(manualItem, {
kind: "manual",
action: "manual",
status: "skipped",
reason: "codex_subscription_required",
});
const details = expectRecordFields(manualItem.details, {
pluginName: "gmail",
marketplaceName: CODEX_PLUGINS_MARKETPLACE_NAME,
});
expect(details).not.toHaveProperty("code");
expect(details.apps).toEqual([
{
id: "app-gmail",
name: "Gmail",
needsAuth: true,
},
]);
expect(plan.warnings).toEqual(
expect.arrayContaining([
expect.stringContaining(
"Codex app-backed plugin migration requires the Codex app-server source account",
),
]),
);
expect(plan.warnings).not.toEqual(
expect.arrayContaining([expect.stringContaining("planned for native activation")]),
);
expect(appServerRequest.mock.calls.filter(([arg]) => arg.method === "app/list")).toHaveLength(
0,
);
});
it("warns and skips app-backed plugins when source Codex account is missing", async () => {
const fixture = await createCodexFixture();
appServerRequest.mockImplementation(async ({ method }: { method: string }) => {
if (method === "plugin/list") {
return pluginList([pluginSummary("gmail", { installed: true, enabled: true })]);
}
if (method === "plugin/read") {
return pluginRead("gmail", [pluginApp("app-gmail", { name: "Gmail", needsAuth: true })]);
}
if (method === "account/read") {
return {
account: null,
requiresOpenaiAuth: true,
} satisfies CodexGetAccountResponse;
}
throw new Error(`unexpected request ${method}`);
});
const provider = buildCodexMigrationProvider();
const plan = await provider.plan(
makeContext({
source: fixture.codexHome,
stateDir: fixture.stateDir,
workspaceDir: fixture.workspaceDir,
}),
);
expect(plan.items.some((item) => item.id === "plugin:gmail")).toBe(false);
expect(plan.items.some((item) => item.id === "config:codex-plugins")).toBe(false);
expectRecordFields(findItemByReason(plan.items, "codex_subscription_required"), {
reason: "codex_subscription_required",
status: "skipped",
});
expect(appServerRequest.mock.calls.filter(([arg]) => arg.method === "app/list")).toHaveLength(
0,
);
});
it("falls through to app inventory when source account read fails and app verification is requested", async () => {
const fixture = await createCodexFixture();
appServerRequest.mockImplementation(async ({ method }: { method: string }) => {
if (method === "plugin/list") {
return pluginList([pluginSummary("gmail", { installed: true, enabled: true })]);
}
if (method === "plugin/read") {
return pluginRead("gmail", [pluginApp("app-gmail", { name: "Gmail", needsAuth: true })]);
}
if (method === "account/read") {
throw new Error("account unavailable");
}
if (method === "app/list") {
return appsList([appInfo("app-gmail")]);
}
throw new Error(`unexpected request ${method}`);
});
const provider = buildCodexMigrationProvider();
const plan = await provider.plan(
makeContext({
source: fixture.codexHome,
stateDir: fixture.stateDir,
workspaceDir: fixture.workspaceDir,
verifyPluginApps: true,
}),
);
expectRecordFields(findItem(plan.items, "plugin:gmail"), {
kind: "plugin",
action: "install",
status: "planned",
});
expect(appServerRequest.mock.calls.filter(([arg]) => arg.method === "app/list")).toHaveLength(
1,
);
});
it("skips app-backed plugins by default when source account read fails", async () => {
const fixture = await createCodexFixture();
appServerRequest.mockImplementation(async ({ method }: { method: string }) => {
if (method === "plugin/list") {
return pluginList([pluginSummary("gmail", { installed: true, enabled: true })]);
}
if (method === "plugin/read") {
return pluginRead("gmail", [pluginApp("app-gmail", { name: "Gmail", needsAuth: true })]);
}
if (method === "account/read") {
throw new Error("account unavailable");
}
throw new Error(`unexpected request ${method}`);
});
const provider = buildCodexMigrationProvider();
const plan = await provider.plan(
makeContext({
source: fixture.codexHome,
stateDir: fixture.stateDir,
workspaceDir: fixture.workspaceDir,
}),
);
expect(plan.items.some((item) => item.id === "plugin:gmail")).toBe(false);
expect(plan.items.some((item) => item.id === "config:codex-plugins")).toBe(false);
const manualItem = findItemByReason(plan.items, "codex_account_unavailable");
expectRecordFields(manualItem, {
kind: "manual",
action: "manual",
reason: "codex_account_unavailable",
status: "skipped",
});
expectRecordFields(manualItem.details, { error: "account unavailable" });
expect(appServerRequest.mock.calls.filter(([arg]) => arg.method === "app/list")).toHaveLength(
0,
);
});
it("reads source plugin readiness with native source auth instead of target agent auth", async () => {
const fixture = await createCodexFixture();
appServerRequest.mockImplementation(async ({ method }: { method: string }) => {
if (method === "plugin/list") {
return pluginList([pluginSummary("google-calendar", { installed: true, enabled: true })]);
}
if (method === "plugin/read") {
return pluginRead("google-calendar", [
pluginApp("app-google-calendar", { name: "Google Calendar", needsAuth: false }),
]);
}
if (method === "account/read") {
return chatGptAccount();
}
if (method === "app/list") {
return appsList([appInfo("app-google-calendar")]);
}
throw new Error(`unexpected request ${method}`);
});
const provider = buildCodexMigrationProvider();
await provider.plan(
makeContext({
source: fixture.codexHome,
stateDir: fixture.stateDir,
workspaceDir: fixture.workspaceDir,
verifyPluginApps: true,
config: {
agents: {
defaults: {
workspace: fixture.workspaceDir,
},
},
auth: {
order: {
"openai-codex": ["openai-codex:target"],
},
},
} as MigrationProviderContext["config"],
}),
);
expect(appServerRequest).toHaveBeenCalledTimes(4);
for (const [arg] of appServerRequest.mock.calls) {
expect(arg).toEqual(
expect.objectContaining({
authProfileId: null,
isolated: true,
startOptions: expect.objectContaining({
env: {
CODEX_HOME: fixture.codexHome,
HOME: path.dirname(fixture.codexHome),
},
}),
}),
);
expect(arg).not.toHaveProperty("agentDir");
expect(arg).not.toHaveProperty("config");
}
});
it("reports inaccessible before missing when multiple owned apps are blocked", async () => {
const fixture = await createCodexFixture();
appServerRequest.mockImplementation(async ({ method }: { method: string }) => {
if (method === "plugin/list") {
return pluginList([pluginSummary("readwise", { installed: true, enabled: true })]);
}
if (method === "plugin/read") {
return pluginRead("readwise", [
pluginApp("asdk_app_readwise", { name: "Readwise", needsAuth: false }),
pluginApp("asdk_app_reader", { name: "Reader", needsAuth: false }),
]);
}
if (method === "account/read") {
return chatGptAccount();
}
if (method === "app/list") {
return appsList([
appInfo("asdk_app_readwise", {
name: "Readwise",
isAccessible: false,
isEnabled: true,
}),
]);
}
throw new Error(`unexpected request ${method}`);
});
const provider = buildCodexMigrationProvider();
const plan = await provider.plan(
makeContext({
source: fixture.codexHome,
stateDir: fixture.stateDir,
workspaceDir: fixture.workspaceDir,
verifyPluginApps: true,
}),
);
const manualItem = findItemByReason(plan.items, "app_inaccessible");
expectRecordFields(manualItem, {
reason: "app_inaccessible",
status: "skipped",
});
const details = expectRecordFields(manualItem.details, {
pluginName: "readwise",
marketplaceName: CODEX_PLUGINS_MARKETPLACE_NAME,
});
expect(details).not.toHaveProperty("code");
expect(details.apps).toEqual([
{
id: "asdk_app_reader",
name: "Reader",
needsAuth: false,
},
{
id: "asdk_app_readwise",
name: "Readwise",
isAccessible: false,
isEnabled: true,
needsAuth: false,
},
]);
});
it("force-refreshes source app inventory once for app-backed plugins sharing a cache key", async () => {
const fixture = await createCodexFixture();
await defaultCodexAppInventoryCache.refreshNow({
key: sourceAppCacheKey(fixture),
request: async () => appsList([appInfo("app-google-calendar", { isAccessible: false })]),
});
appServerRequest.mockImplementation(
async ({ method, requestParams }: { method: string; requestParams?: unknown }) => {
if (method === "plugin/list") {
return pluginList([
pluginSummary("google-calendar", { installed: true, enabled: true }),
pluginSummary("gmail", { installed: true, enabled: true }),
]);
}
if (method === "plugin/read") {
const pluginName = (requestParams as v2.PluginReadParams).pluginName;
return pluginRead(pluginName, [pluginApp(`app-${pluginName}`)]);
}
if (method === "account/read") {
return chatGptAccount();
}
if (method === "app/list") {
expectRecordFields(requestParams, { forceRefetch: true });
return appsList([appInfo("app-google-calendar"), appInfo("app-gmail")]);
}
throw new Error(`unexpected request ${method}`);
},
);
const provider = buildCodexMigrationProvider();
const plan = await provider.plan(
makeContext({
source: fixture.codexHome,
stateDir: fixture.stateDir,
workspaceDir: fixture.workspaceDir,
verifyPluginApps: true,
}),
);
expectRecordFields(findItem(plan.items, "plugin:google-calendar"), { status: "planned" });
expectRecordFields(findItem(plan.items, "plugin:gmail"), { status: "planned" });
expect(appServerRequest.mock.calls.filter(([arg]) => arg.method === "app/list")).toHaveLength(
1,
);
});
it("fails closed for disabled plugins and plugin/read failures", async () => {
const fixture = await createCodexFixture();
appServerRequest.mockImplementation(
async ({ method, requestParams }: { method: string; requestParams?: unknown }) => {
if (method === "plugin/list") {
return pluginList([
pluginSummary("readwise", { installed: true, enabled: false }),
pluginSummary("gmail", { installed: true, enabled: true }),
]);
}
if (method === "plugin/read") {
expectRecordFields(requestParams, { pluginName: "gmail" });
throw new Error("detail unavailable");
}
throw new Error(`unexpected request ${method}`);
},
);
const provider = buildCodexMigrationProvider();
const plan = await provider.plan(
makeContext({
source: fixture.codexHome,
stateDir: fixture.stateDir,
workspaceDir: fixture.workspaceDir,
verifyPluginApps: true,
}),
);
expectRecordFields(findItemByReason(plan.items, "plugin_disabled"), {
reason: "plugin_disabled",
status: "skipped",
});
expectRecordFields(findItemByReason(plan.items, "plugin_read_unavailable"), {
reason: "plugin_read_unavailable",
status: "skipped",
});
expect(plan.items.some((item) => item.id === "config:codex-plugins")).toBe(false);
expect(appServerRequest.mock.calls.filter(([arg]) => arg.method === "app/list")).toHaveLength(
0,
);
});
it("fails closed when app inventory refresh fails for app-backed plugins", async () => {
const fixture = await createCodexFixture();
appServerRequest.mockImplementation(async ({ method }: { method: string }) => {
if (method === "plugin/list") {
return pluginList([pluginSummary("readwise", { installed: true, enabled: true })]);
}
if (method === "plugin/read") {
return pluginRead("readwise", [pluginApp("asdk_app_readwise", { name: "Readwise" })]);
}
if (method === "account/read") {
return chatGptAccount();
}
if (method === "app/list") {
throw new Error("app inventory unavailable");
}
throw new Error(`unexpected request ${method}`);
});
const provider = buildCodexMigrationProvider();
const plan = await provider.plan(
makeContext({
source: fixture.codexHome,
stateDir: fixture.stateDir,
workspaceDir: fixture.workspaceDir,
verifyPluginApps: true,
}),
);
expectRecordFields(findItemByReason(plan.items, "app_inventory_unavailable"), {
reason: "app_inventory_unavailable",
status: "skipped",
});
expect(plan.items.some((item) => item.id === "plugin:readwise")).toBe(false);
});
it("treats auth-required source apps as ready when app inventory says they are accessible", async () => {
const fixture = await createCodexFixture();
appServerRequest.mockImplementation(async ({ method }: { method: string }) => {
if (method === "plugin/list") {
return pluginList([pluginSummary("reader", { installed: true, enabled: true })]);
}
if (method === "plugin/read") {
return pluginRead("reader", [
pluginApp("ready-app", { name: "Ready App", needsAuth: false }),
pluginApp("auth-app", { name: "Auth App", needsAuth: true }),
]);
}
if (method === "account/read") {
return chatGptAccount();
}
if (method === "app/list") {
return appsList([appInfo("ready-app"), appInfo("auth-app")]);
}
throw new Error(`unexpected request ${method}`);
});
const provider = buildCodexMigrationProvider();
const plan = await provider.plan(
makeContext({
source: fixture.codexHome,
stateDir: fixture.stateDir,
workspaceDir: fixture.workspaceDir,
verifyPluginApps: true,
}),
);
const pluginItem = findItem(plan.items, "plugin:reader");
expectRecordFields(pluginItem, {
kind: "plugin",
action: "install",
status: "planned",
});
expectRecordFields(pluginItem.details, {
configKey: "reader",
pluginName: "reader",
marketplaceName: CODEX_PLUGINS_MARKETPLACE_NAME,
});
});
it("copies planned skills and archives native config during apply", async () => {
const fixture = await createCodexFixture();
const reportDir = path.join(fixture.root, "report");
@@ -275,6 +896,9 @@ describe("buildCodexMigrationProvider", () => {
if (method === "plugin/list") {
return pluginList([pluginSummary("google-calendar", { installed: true, enabled: true })]);
}
if (method === "plugin/read") {
return pluginRead("google-calendar");
}
if (method === "plugin/install") {
return { authPolicy: "ON_USE", appsNeedingAuth: [] } satisfies v2.PluginInstallResponse;
}
@@ -287,6 +911,9 @@ describe("buildCodexMigrationProvider", () => {
if (method === "config/mcpServer/reload") {
return {};
}
if (method === "app/list") {
return appsList([]);
}
throw new Error(`unexpected request ${method}`);
});
const provider = buildCodexMigrationProvider({
@@ -375,6 +1002,9 @@ describe("buildCodexMigrationProvider", () => {
pluginSummary("gmail", { installed: true, enabled: true }),
]);
}
if (method === "plugin/read") {
return pluginRead("google-calendar");
}
throw new Error(`unexpected request ${method}`);
});
const provider = buildCodexMigrationProvider();
@@ -415,6 +1045,9 @@ describe("buildCodexMigrationProvider", () => {
if (method === "plugin/list") {
return pluginList([pluginSummary("google-calendar", { installed: true, enabled: true })]);
}
if (method === "plugin/read") {
return pluginRead("google-calendar");
}
if (method === "plugin/install") {
return { authPolicy: "ON_USE", appsNeedingAuth: [] } satisfies v2.PluginInstallResponse;
}
@@ -427,6 +1060,9 @@ describe("buildCodexMigrationProvider", () => {
if (method === "config/mcpServer/reload") {
return {};
}
if (method === "app/list") {
return appsList([]);
}
throw new Error(`unexpected request ${method}`);
});
const provider = buildCodexMigrationProvider({
@@ -449,6 +1085,11 @@ describe("buildCodexMigrationProvider", () => {
it("merges migrated plugin config with existing Codex plugins when entries do not conflict", async () => {
const fixture = await createCodexFixture();
const sourceKey = sourceAppCacheKey(fixture);
await defaultCodexAppInventoryCache.refreshNow({
key: sourceKey,
request: async () => appsList([appInfo("source-only-app")]),
});
const configState: MigrationProviderContext["config"] = {
plugins: {
entries: {
@@ -476,6 +1117,9 @@ describe("buildCodexMigrationProvider", () => {
if (method === "plugin/list") {
return pluginList([pluginSummary("google-calendar", { installed: true, enabled: true })]);
}
if (method === "plugin/read") {
return pluginRead("google-calendar");
}
if (method === "plugin/install") {
return { authPolicy: "ON_USE", appsNeedingAuth: [] } satisfies v2.PluginInstallResponse;
}
@@ -488,6 +1132,9 @@ describe("buildCodexMigrationProvider", () => {
if (method === "config/mcpServer/reload") {
return {};
}
if (method === "app/list") {
return appsList([]);
}
throw new Error(`unexpected request ${method}`);
});
const provider = buildCodexMigrationProvider({
@@ -504,6 +1151,14 @@ describe("buildCodexMigrationProvider", () => {
);
expectRecordFields(findItem(result.items, "config:codex-plugins"), { status: "migrated" });
const sourceCacheRead = defaultCodexAppInventoryCache.read({
key: sourceKey,
request: async () => {
throw new Error("source app cache was cleared");
},
});
expect(sourceCacheRead.state).toBe("fresh");
expect(sourceCacheRead.snapshot?.apps.map((app) => app.id)).toEqual(["source-only-app"]);
expect(configState.plugins?.entries?.codex?.config?.codexPlugins).toEqual({
allow_destructive_actions: true,
plugins: {
@@ -545,6 +1200,9 @@ describe("buildCodexMigrationProvider", () => {
if (method === "plugin/list") {
return pluginList([pluginSummary("google-calendar", { installed: true, enabled: true })]);
}
if (method === "plugin/read") {
return pluginRead("google-calendar");
}
if (method === "plugin/install") {
return { authPolicy: "ON_USE", appsNeedingAuth: [] } satisfies v2.PluginInstallResponse;
}
@@ -557,6 +1215,9 @@ describe("buildCodexMigrationProvider", () => {
if (method === "config/mcpServer/reload") {
return {};
}
if (method === "app/list") {
return appsList([]);
}
throw new Error(`unexpected request ${method}`);
});
const provider = buildCodexMigrationProvider({
@@ -596,6 +1257,9 @@ describe("buildCodexMigrationProvider", () => {
if (method === "plugin/list") {
return pluginList([pluginSummary("google-calendar", { installed: true, enabled: true })]);
}
if (method === "plugin/read") {
return pluginRead("google-calendar");
}
if (method === "plugin/install") {
return {
authPolicy: "ON_USE",
@@ -619,6 +1283,9 @@ describe("buildCodexMigrationProvider", () => {
if (method === "config/mcpServer/reload") {
return {};
}
if (method === "app/list") {
return appsList([]);
}
throw new Error(`unexpected request ${method}`);
});
const provider = buildCodexMigrationProvider({
@@ -671,6 +1338,9 @@ describe("buildCodexMigrationProvider", () => {
if (method === "plugin/list") {
return pluginList([pluginSummary("google-calendar", { installed: true, enabled: true })]);
}
if (method === "plugin/read") {
return pluginRead("google-calendar");
}
if (method === "plugin/install") {
throw new Error("install failed");
}
@@ -777,6 +1447,61 @@ function pluginList(plugins: v2.PluginSummary[]): v2.PluginListResponse {
};
}
function pluginRead(pluginName: string, apps: v2.AppSummary[] = []): v2.PluginReadResponse {
return {
plugin: {
marketplaceName: CODEX_PLUGINS_MARKETPLACE_NAME,
marketplacePath: "/marketplaces/openai-curated",
summary: pluginSummary(pluginName, { installed: true, enabled: true }),
description: null,
skills: [],
apps,
mcpServers: [],
},
};
}
function pluginApp(id: string, overrides: Partial<v2.AppSummary> = {}): v2.AppSummary {
return {
id,
name: id,
description: null,
installUrl: null,
needsAuth: false,
...overrides,
};
}
function appInfo(id: string, overrides: Partial<v2.AppInfo> = {}): v2.AppInfo {
return {
id,
name: id,
description: null,
logoUrl: null,
logoUrlDark: null,
distributionChannel: null,
branding: null,
appMetadata: null,
labels: null,
installUrl: null,
isAccessible: true,
isEnabled: true,
pluginDisplayNames: [],
...overrides,
};
}
function appsList(apps: v2.AppInfo[]): v2.AppsListResponse {
return { data: apps, nextCursor: null };
}
function chatGptAccount(): CodexGetAccountResponse {
return {
account: { type: "chatgpt", email: "codex@example.test", planType: "plus" },
requiresOpenaiAuth: false,
};
}
function pluginSummary(id: string, overrides: Partial<v2.PluginSummary> = {}): v2.PluginSummary {
return {
id,

View File

@@ -18,7 +18,9 @@ export function buildCodexMigrationProvider(
description:
"Inventory and promote Codex CLI skills while keeping Codex native plugins and hooks explicit.",
async detect(ctx) {
const source = await discoverCodexSource(ctx.source);
const source = await discoverCodexSource({
input: ctx.source,
});
const found = hasCodexSource(source);
return {
found,

View File

@@ -1,8 +1,18 @@
import type { Dirent } from "node:fs";
import fs from "node:fs/promises";
import path from "node:path";
import {
defaultCodexAppInventoryCache,
type CodexAppInventoryRequest,
} from "../app-server/app-inventory-cache.js";
import { CODEX_PLUGINS_MARKETPLACE_NAME } from "../app-server/config.js";
import type { v2 } from "../app-server/protocol.js";
import type { CodexAppServerStartOptions } from "../app-server/config.js";
import { buildCodexPluginAppCacheKey } from "../app-server/plugin-app-cache-key.js";
import {
pluginReadParams,
type CodexPluginMarketplaceRef,
} from "../app-server/plugin-inventory.js";
import type { CodexGetAccountResponse, v2 } from "../app-server/protocol.js";
import { requestCodexAppServerJson } from "../app-server/request.js";
import {
exists,
@@ -32,9 +42,35 @@ export type CodexPluginSource = {
pluginName?: string;
installed?: boolean;
enabled?: boolean;
apps?: CodexPluginMigrationAppFact[];
migrationBlock?: CodexPluginMigrationBlock;
message?: string;
};
export type CodexPluginMigrationBlockCode =
| "plugin_disabled"
| "codex_subscription_required"
| "codex_account_unavailable"
| "plugin_read_unavailable"
| "app_inventory_unavailable"
| "app_inaccessible"
| "app_disabled"
| "app_missing";
export type CodexPluginMigrationAppFact = {
id: string;
name: string;
needsAuth?: boolean;
isAccessible?: boolean;
isEnabled?: boolean;
};
export type CodexPluginMigrationBlock = {
code: CodexPluginMigrationBlockCode;
apps?: CodexPluginMigrationAppFact[];
error?: string;
};
type CodexArchiveSource = {
id: string;
path: string;
@@ -56,6 +92,26 @@ type CodexSource = {
archivePaths: CodexArchiveSource[];
};
type CodexSourceDiscoveryOptions = {
input?: string;
evaluatePluginMigrationEligibility?: boolean;
verifyPluginApps?: boolean;
};
type SourceAppServerRequestOptions = {
startOptions: CodexAppServerStartOptions;
};
type PluginReadResult =
| {
ok: true;
detail: v2.PluginDetail;
}
| {
ok: false;
error: string;
};
function defaultCodexHome(): string {
return resolveHomePath(process.env.CODEX_HOME?.trim() || "~/.codex");
}
@@ -137,27 +193,19 @@ async function discoverPluginDirs(codexHome: string): Promise<CodexPluginSource[
return [...discovered.values()].toSorted((a, b) => a.source.localeCompare(b.source));
}
async function discoverInstalledCuratedPlugins(codexHome: string): Promise<{
async function discoverInstalledCuratedPlugins(
codexHome: string,
options: CodexSourceDiscoveryOptions = {},
): Promise<{
plugins: CodexPluginSource[];
error?: string;
}> {
const startOptions = sourceCodexAppServerStartOptions(codexHome);
const requestOptions = { startOptions };
try {
const response = await requestCodexAppServerJson<v2.PluginListResponse>({
const response = await requestSourceCodexAppServerJson<v2.PluginListResponse>(requestOptions, {
method: "plugin/list",
requestParams: { cwds: [] } satisfies v2.PluginListParams,
timeoutMs: 60_000,
isolated: true,
startOptions: {
transport: "stdio",
command: "codex",
commandSource: "config",
args: ["app-server", "--listen", "stdio://"],
headers: {},
env: {
CODEX_HOME: codexHome,
HOME: path.dirname(codexHome),
},
},
});
const marketplace = response.marketplaces.find(
(entry) => entry.name === CODEX_PLUGINS_MARKETPLACE_NAME,
@@ -170,25 +218,21 @@ async function discoverInstalledCuratedPlugins(codexHome: string): Promise<{
}
const plugins = marketplace.plugins
.filter((plugin) => plugin.installed)
.map((plugin): CodexPluginSource | undefined => {
const pluginName = pluginNameFromSummary(plugin);
if (!pluginName) {
return undefined;
}
return {
name: plugin.name,
pluginName,
marketplaceName: CODEX_PLUGINS_MARKETPLACE_NAME,
source: `${CODEX_PLUGINS_MARKETPLACE_NAME}/${pluginName}`,
sourceKind: "app-server",
migratable: true,
installed: plugin.installed,
enabled: plugin.enabled,
};
})
.filter((plugin): plugin is CodexPluginSource => plugin !== undefined)
.toSorted((a, b) => (a.pluginName ?? a.name).localeCompare(b.pluginName ?? b.name));
return { plugins };
.map((plugin) => buildInstalledPluginSource(plugin))
.filter((plugin): plugin is CodexPluginSource => plugin !== undefined);
const withEligibility =
options.evaluatePluginMigrationEligibility === true
? await withPluginMigrationEligibility({
plugins,
marketplace: marketplaceRef(marketplace),
requestOptions,
verifyPluginApps: options.verifyPluginApps === true,
})
: plugins;
const sorted = withEligibility.toSorted((a, b) =>
(a.pluginName ?? a.name).localeCompare(b.pluginName ?? b.name),
);
return { plugins: sorted };
} catch (error) {
return {
plugins: [],
@@ -197,6 +241,308 @@ async function discoverInstalledCuratedPlugins(codexHome: string): Promise<{
}
}
function sourceCodexAppServerStartOptions(codexHome: string): CodexAppServerStartOptions {
return {
transport: "stdio",
command: "codex",
commandSource: "config",
args: ["app-server", "--listen", "stdio://"],
headers: {},
env: {
CODEX_HOME: codexHome,
HOME: path.dirname(codexHome),
},
};
}
async function requestSourceCodexAppServerJson<T>(
options: SourceAppServerRequestOptions,
params: {
method: string;
requestParams?: unknown;
},
): Promise<T> {
return await requestCodexAppServerJson<T>({
method: params.method,
requestParams: params.requestParams,
timeoutMs: 60_000,
startOptions: options.startOptions,
authProfileId: null,
isolated: true,
});
}
function buildInstalledPluginSource(plugin: v2.PluginSummary): CodexPluginSource | undefined {
const pluginName = pluginNameFromSummary(plugin);
if (!pluginName) {
return undefined;
}
return {
name: plugin.name,
pluginName,
marketplaceName: CODEX_PLUGINS_MARKETPLACE_NAME,
source: `${CODEX_PLUGINS_MARKETPLACE_NAME}/${pluginName}`,
sourceKind: "app-server",
migratable: true,
installed: plugin.installed,
enabled: plugin.enabled,
};
}
function marketplaceRef(marketplace: v2.PluginMarketplaceEntry): CodexPluginMarketplaceRef {
return {
name: CODEX_PLUGINS_MARKETPLACE_NAME,
...(marketplace.path ? { path: marketplace.path } : {}),
...(!marketplace.path ? { remoteMarketplaceName: marketplace.name } : {}),
};
}
async function withPluginMigrationEligibility(params: {
plugins: CodexPluginSource[];
marketplace: CodexPluginMarketplaceRef;
requestOptions: SourceAppServerRequestOptions;
verifyPluginApps: boolean;
}): Promise<CodexPluginSource[]> {
const pending: Array<{ plugin: CodexPluginSource; apps: CodexPluginMigrationAppFact[] }> = [];
const evaluated: CodexPluginSource[] = [];
for (const plugin of params.plugins) {
if (plugin.enabled !== true) {
evaluated.push({
...plugin,
migratable: false,
migrationBlock: { code: "plugin_disabled" },
message: `Codex plugin "${plugin.pluginName ?? plugin.name}" is installed in Codex but disabled; enable it in Codex before migrating it to OpenClaw.`,
});
continue;
}
const detail = await readPluginDetail(params.requestOptions, params.marketplace, plugin);
if (!detail.ok) {
evaluated.push({
...plugin,
migratable: false,
migrationBlock: { code: "plugin_read_unavailable", error: detail.error },
message: `Codex plugin "${plugin.pluginName ?? plugin.name}" detail could not be read: ${detail.error}`,
});
continue;
}
if (detail.detail.apps.length === 0) {
evaluated.push({
...plugin,
migratable: true,
});
continue;
}
const apps = detail.detail.apps
.map(sourcePluginAppFact)
.toSorted((left, right) => left.id.localeCompare(right.id));
pending.push({ plugin, apps });
}
if (pending.length === 0) {
return evaluated;
}
let sourceAccount: Awaited<ReturnType<typeof readSourceCodexAccount>> | undefined;
try {
sourceAccount = await readSourceCodexAccount(params.requestOptions);
} catch (error) {
if (!params.verifyPluginApps) {
const message = error instanceof Error ? error.message : String(error);
for (const { plugin, apps } of pending) {
evaluated.push({
...plugin,
migratable: false,
migrationBlock: { code: "codex_account_unavailable", apps, error: message },
message: `Codex plugin "${plugin.pluginName ?? plugin.name}" owns apps, but the source Codex app-server account could not be read: ${message}`,
});
}
return evaluated;
}
}
if (sourceAccount && sourceAccount !== "chatgpt") {
for (const { plugin, apps } of pending) {
evaluated.push({
...plugin,
migratable: false,
migrationBlock: { code: "codex_subscription_required", apps },
message: codexSubscriptionRequiredMessage(plugin),
});
}
return evaluated;
}
if (!params.verifyPluginApps) {
for (const { plugin, apps } of pending) {
evaluated.push({
...plugin,
apps,
migratable: true,
});
}
return evaluated;
}
const snapshot = await refreshSourceAppInventory(params.requestOptions).catch((error) => {
const message = error instanceof Error ? error.message : String(error);
for (const { plugin, apps } of pending) {
evaluated.push({
...plugin,
migratable: false,
migrationBlock: {
code: "app_inventory_unavailable",
apps,
error: message,
},
message: `Codex plugin "${plugin.pluginName ?? plugin.name}" owns apps, but source app inventory could not be read: ${message}`,
});
}
return undefined;
});
if (!snapshot) {
return evaluated;
}
const appInfoById = new Map(snapshot.apps.map((app) => [app.id, app] as const));
for (const { plugin, apps: declaredApps } of pending) {
const apps = declaredApps
.map((app) => sourcePluginAppFactWithInventory(app, appInfoById.get(app.id)))
.toSorted((left, right) => left.id.localeCompare(right.id));
const blockCode = migrationBlockCodeForApps(apps);
if (!blockCode) {
evaluated.push({ ...plugin, apps, migratable: true });
continue;
}
evaluated.push({
...plugin,
migratable: false,
migrationBlock: { code: blockCode, apps },
message: appInventoryBlockMessage(plugin, apps, blockCode),
});
}
return evaluated;
}
async function readSourceCodexAccount(
options: SourceAppServerRequestOptions,
): Promise<"chatgpt" | "non_chatgpt" | "missing"> {
const response = await requestSourceCodexAppServerJson<CodexGetAccountResponse>(options, {
method: "account/read",
requestParams: { refreshToken: false },
});
if (
!response.account ||
typeof response.account !== "object" ||
Array.isArray(response.account)
) {
return "missing";
}
const type = response.account.type;
return type === "chatgpt" ? "chatgpt" : "non_chatgpt";
}
async function readPluginDetail(
options: SourceAppServerRequestOptions,
marketplace: CodexPluginMarketplaceRef,
plugin: CodexPluginSource,
): Promise<PluginReadResult> {
try {
const response = await requestSourceCodexAppServerJson<v2.PluginReadResponse>(options, {
method: "plugin/read",
requestParams: pluginReadParams(marketplace, plugin.pluginName ?? plugin.name),
});
return { ok: true, detail: response.plugin };
} catch (error) {
return { ok: false, error: error instanceof Error ? error.message : String(error) };
}
}
async function refreshSourceAppInventory(
options: SourceAppServerRequestOptions,
): Promise<Awaited<ReturnType<typeof defaultCodexAppInventoryCache.refreshNow>>> {
const key = buildCodexPluginAppCacheKey({
appServer: { start: options.startOptions },
});
const request: CodexAppInventoryRequest = async (method, requestParams) =>
await requestSourceCodexAppServerJson<v2.AppsListResponse>(options, {
method,
requestParams,
});
return await defaultCodexAppInventoryCache.refreshNow({
key,
request,
forceRefetch: true,
});
}
function sourcePluginAppFact(app: v2.AppSummary): CodexPluginMigrationAppFact {
return {
id: app.id,
name: app.name,
needsAuth: app.needsAuth,
};
}
function sourcePluginAppFactWithInventory(
app: CodexPluginMigrationAppFact,
info: v2.AppInfo | undefined,
): CodexPluginMigrationAppFact {
if (!info) {
return app;
}
return {
...app,
isAccessible: info.isAccessible,
isEnabled: info.isEnabled,
};
}
function migrationBlockCodeForApps(
apps: readonly CodexPluginMigrationAppFact[],
): CodexPluginMigrationBlockCode | undefined {
if (apps.some((app) => app.isAccessible === false)) {
return "app_inaccessible";
}
if (apps.some((app) => app.isEnabled === false)) {
return "app_disabled";
}
if (apps.some((app) => app.isAccessible === undefined || app.isEnabled === undefined)) {
return "app_missing";
}
return undefined;
}
function appInventoryBlockMessage(
plugin: CodexPluginSource,
apps: readonly CodexPluginMigrationAppFact[],
code: CodexPluginMigrationBlockCode,
): string {
const status =
code === "app_inaccessible" ? "inaccessible" : code === "app_disabled" ? "disabled" : "missing";
const blocking =
apps.find((app) =>
code === "app_inaccessible"
? app.isAccessible === false
: code === "app_disabled"
? app.isEnabled === false
: app.isAccessible === undefined || app.isEnabled === undefined,
) ?? apps[0];
const appLabel = blocking ? ` app "${blocking.name}"` : " an owned app";
return `Codex plugin "${plugin.pluginName ?? plugin.name}" owns${appLabel} but the source app inventory reports it is ${status}; authenticate or enable the app in Codex before migrating it to OpenClaw.`;
}
export function codexPluginMigrationSubscriptionWarning(): string {
return "Codex app-backed plugin migration requires the Codex app-server source account to be logged in with a ChatGPT subscription account. Log in to the Codex app with subscription auth; OpenClaw auth or API-key auth does not satisfy Codex app connector access.";
}
function codexSubscriptionRequiredMessage(plugin: CodexPluginSource): string {
return `Codex plugin "${plugin.pluginName ?? plugin.name}" owns apps, but ${codexPluginMigrationSubscriptionWarning()}`;
}
function pluginNameFromSummary(summary: v2.PluginSummary): string | undefined {
const candidates = [summary.id, summary.name];
for (const candidate of candidates) {
@@ -216,8 +562,14 @@ function pluginNameFromSummary(summary: v2.PluginSummary): string | undefined {
return undefined;
}
export async function discoverCodexSource(input?: string): Promise<CodexSource> {
const codexHome = resolveHomePath(input?.trim() || defaultCodexHome());
export async function discoverCodexSource(
inputOrOptions?: string | CodexSourceDiscoveryOptions,
): Promise<CodexSource> {
const options =
typeof inputOrOptions === "string" || inputOrOptions === undefined
? { input: inputOrOptions }
: inputOrOptions;
const codexHome = resolveHomePath(options.input?.trim() || defaultCodexHome());
const codexSkillsDir = path.join(codexHome, "skills");
const agentsSkillsDir = personalAgentsSkillsDir();
const configPath = path.join(codexHome, "config.toml");
@@ -231,7 +583,7 @@ export async function discoverCodexSource(input?: string): Promise<CodexSource>
root: agentsSkillsDir,
sourceLabel: "personal AgentSkill",
});
const sourcePluginDiscovery = await discoverInstalledCuratedPlugins(codexHome);
const sourcePluginDiscovery = await discoverInstalledCuratedPlugins(codexHome, options);
const sourcePluginNames = new Set(
sourcePluginDiscovery.plugins.flatMap((plugin) =>
plugin.pluginName ? [plugin.pluginName] : [],

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/comfy-provider",
"version": "2026.5.12-beta.1",
"version": "2026.5.12-beta.5",
"private": true,
"description": "OpenClaw ComfyUI provider plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/copilot-proxy",
"version": "2026.5.12-beta.1",
"version": "2026.5.12-beta.5",
"private": true,
"description": "OpenClaw Copilot Proxy provider plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/deepgram-provider",
"version": "2026.5.12-beta.1",
"version": "2026.5.12-beta.5",
"private": true,
"description": "OpenClaw Deepgram media-understanding provider",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/deepinfra-provider",
"version": "2026.5.12-beta.1",
"version": "2026.5.12-beta.5",
"private": true,
"description": "OpenClaw DeepInfra provider plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/deepseek-provider",
"version": "2026.5.12-beta.1",
"version": "2026.5.12-beta.5",
"private": true,
"description": "OpenClaw DeepSeek provider plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/diagnostics-otel",
"version": "2026.5.12-beta.1",
"version": "2026.5.12-beta.5",
"description": "OpenClaw diagnostics OpenTelemetry exporter",
"repository": {
"type": "git",
@@ -34,10 +34,10 @@
"minHostVersion": ">=2026.4.25"
},
"compat": {
"pluginApi": ">=2026.5.12-beta.1"
"pluginApi": ">=2026.5.12-beta.5"
},
"build": {
"openclawVersion": "2026.5.12-beta.1"
"openclawVersion": "2026.5.12-beta.5"
},
"release": {
"publishToClawHub": true,

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/diagnostics-prometheus",
"version": "2026.5.12-beta.1",
"version": "2026.5.12-beta.5",
"description": "OpenClaw diagnostics Prometheus exporter",
"repository": {
"type": "git",
@@ -21,10 +21,10 @@
"minHostVersion": ">=2026.4.25"
},
"compat": {
"pluginApi": ">=2026.5.12-beta.1"
"pluginApi": ">=2026.5.12-beta.5"
},
"build": {
"openclawVersion": "2026.5.12-beta.1"
"openclawVersion": "2026.5.12-beta.5"
},
"release": {
"publishToClawHub": true,

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/diffs",
"version": "2026.5.12-beta.1",
"version": "2026.5.12-beta.5",
"description": "OpenClaw diff viewer plugin",
"repository": {
"type": "git",
@@ -31,10 +31,10 @@
"minHostVersion": ">=2026.4.30"
},
"compat": {
"pluginApi": ">=2026.5.12-beta.1"
"pluginApi": ">=2026.5.12-beta.5"
},
"build": {
"openclawVersion": "2026.5.12-beta.1",
"openclawVersion": "2026.5.12-beta.5",
"staticAssets": [
{
"source": "./assets/viewer-runtime.js",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/discord",
"version": "2026.5.12-beta.1",
"version": "2026.5.12-beta.5",
"description": "OpenClaw Discord channel plugin",
"repository": {
"type": "git",
@@ -21,7 +21,7 @@
"openclaw": "workspace:*"
},
"peerDependencies": {
"openclaw": ">=2026.5.12-beta.1"
"openclaw": ">=2026.5.12-beta.5"
},
"peerDependenciesMeta": {
"openclaw": {
@@ -65,10 +65,10 @@
"allowInvalidConfigRecovery": true
},
"compat": {
"pluginApi": ">=2026.5.12-beta.1"
"pluginApi": ">=2026.5.12-beta.5"
},
"build": {
"openclawVersion": "2026.5.12-beta.1"
"openclawVersion": "2026.5.12-beta.5"
},
"release": {
"publishToClawHub": true,

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/document-extract-plugin",
"version": "2026.5.12-beta.1",
"version": "2026.5.12-beta.5",
"private": true,
"description": "OpenClaw local document extraction plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/duckduckgo-plugin",
"version": "2026.5.12-beta.1",
"version": "2026.5.12-beta.5",
"private": true,
"description": "OpenClaw DuckDuckGo plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/elevenlabs-speech",
"version": "2026.5.12-beta.1",
"version": "2026.5.12-beta.5",
"private": true,
"description": "OpenClaw ElevenLabs speech plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/exa-plugin",
"version": "2026.5.12-beta.1",
"version": "2026.5.12-beta.5",
"private": true,
"description": "OpenClaw Exa plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/fal-provider",
"version": "2026.5.12-beta.1",
"version": "2026.5.12-beta.5",
"private": true,
"description": "OpenClaw fal provider plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/feishu",
"version": "2026.5.12-beta.1",
"version": "2026.5.12-beta.5",
"description": "OpenClaw Feishu/Lark channel plugin (community maintained by @m1heng)",
"repository": {
"type": "git",
@@ -17,7 +17,7 @@
"openclaw": "workspace:*"
},
"peerDependencies": {
"openclaw": ">=2026.5.12-beta.1"
"openclaw": ">=2026.5.12-beta.5"
},
"peerDependenciesMeta": {
"openclaw": {
@@ -48,10 +48,10 @@
"minHostVersion": ">=2026.4.25"
},
"compat": {
"pluginApi": ">=2026.5.12-beta.1"
"pluginApi": ">=2026.5.12-beta.5"
},
"build": {
"openclawVersion": "2026.5.12-beta.1"
"openclawVersion": "2026.5.12-beta.5"
},
"release": {
"publishToClawHub": true,

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/file-transfer",
"version": "2026.5.12-beta.1",
"version": "2026.5.12-beta.5",
"description": "OpenClaw file transfer plugin (file_fetch, dir_list, dir_fetch, file_write)",
"type": "module",
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/firecrawl-plugin",
"version": "2026.5.12-beta.1",
"version": "2026.5.12-beta.5",
"private": true,
"description": "OpenClaw Firecrawl plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/fireworks-provider",
"version": "2026.5.12-beta.1",
"version": "2026.5.12-beta.5",
"private": true,
"description": "OpenClaw Fireworks provider plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/github-copilot-provider",
"version": "2026.5.12-beta.1",
"version": "2026.5.12-beta.5",
"private": true,
"description": "OpenClaw GitHub Copilot provider plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/google-meet",
"version": "2026.5.12-beta.1",
"version": "2026.5.12-beta.5",
"description": "OpenClaw Google Meet participant plugin",
"repository": {
"type": "git",
@@ -16,7 +16,7 @@
"openclaw": "workspace:*"
},
"peerDependencies": {
"openclaw": ">=2026.5.12-beta.1"
"openclaw": ">=2026.5.12-beta.5"
},
"peerDependenciesMeta": {
"openclaw": {
@@ -33,10 +33,10 @@
"minHostVersion": ">=2026.4.20"
},
"compat": {
"pluginApi": ">=2026.5.12-beta.1"
"pluginApi": ">=2026.5.12-beta.5"
},
"build": {
"openclawVersion": "2026.5.12-beta.1"
"openclawVersion": "2026.5.12-beta.5"
},
"release": {
"publishToClawHub": true,

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/google-plugin",
"version": "2026.5.12-beta.1",
"version": "2026.5.12-beta.5",
"private": true,
"description": "OpenClaw Google plugin",
"type": "module",

View File

@@ -118,6 +118,24 @@ describe("resolveGoogleGeminiForwardCompatModel", () => {
});
});
it("canonicalizes provider-qualified retired Gemini 3 Pro preview requests", () => {
const model = resolveGoogleGeminiForwardCompatModel({
providerId: "google",
ctx: createContext({
provider: "google",
modelId: "google/gemini-3-pro-preview",
models: [createTemplateModel("google", "gemini-3.1-pro-preview")],
}),
});
expectModelFields(model, {
provider: "google",
id: "google/gemini-3.1-pro-preview",
api: "google-generative-ai",
reasoning: true,
});
});
it("keeps Gemini CLI 3.1 clones sourced from CLI templates when both catalogs exist", () => {
const model = resolveGoogleGeminiForwardCompatModel({
providerId: "google-gemini-cli",

View File

@@ -30,14 +30,24 @@ const GEMINI_3_FLASH_ANTIGRAVITY_TEMPLATE_IDS = ["gemini-3-flash"] as const;
// Gemma uses the Gemini flash template as a forward-compat approximation
// until a dedicated Gemma template is registered in the catalog.
const GEMMA_TEMPLATE_IDS = GEMINI_3_1_FLASH_TEMPLATE_IDS;
const GOOGLE_PROVIDER_PREFIX = "google/";
function normalizeGeminiProRequestId(id: string): string {
if (id.startsWith(GOOGLE_PROVIDER_PREFIX)) {
const modelId = id.slice(GOOGLE_PROVIDER_PREFIX.length);
const normalizedModelId = normalizeGeminiProRequestId(modelId);
return normalizedModelId === modelId ? id : `${GOOGLE_PROVIDER_PREFIX}${normalizedModelId}`;
}
if (id === "gemini-3-pro" || id === "gemini-3-pro-preview" || id === "gemini-3.1-pro") {
return "gemini-3.1-pro-preview";
}
return id;
}
function googleFamilyModelId(id: string): string {
return id.startsWith(GOOGLE_PROVIDER_PREFIX) ? id.slice(GOOGLE_PROVIDER_PREFIX.length) : id;
}
type GoogleForwardCompatFamily = {
googleTemplateIds: readonly string[];
cliTemplateIds: readonly string[];
@@ -130,7 +140,7 @@ export function resolveGoogleGeminiForwardCompatModel(params: {
ctx: ProviderResolveDynamicModelContext;
}): ProviderRuntimeModel | undefined {
const trimmed = normalizeGeminiProRequestId(params.ctx.modelId.trim());
const lower = normalizeOptionalLowercaseString(trimmed) ?? "";
const lower = normalizeOptionalLowercaseString(googleFamilyModelId(trimmed)) ?? "";
let family: GoogleForwardCompatFamily;
let patch: Partial<ProviderRuntimeModel> | undefined;

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/googlechat",
"version": "2026.5.12-beta.1",
"version": "2026.5.12-beta.5",
"description": "OpenClaw Google Chat channel plugin",
"repository": {
"type": "git",
@@ -17,7 +17,7 @@
"openclaw": "workspace:*"
},
"peerDependencies": {
"openclaw": ">=2026.5.12-beta.1"
"openclaw": ">=2026.5.12-beta.5"
},
"peerDependenciesMeta": {
"openclaw": {
@@ -75,10 +75,10 @@
"minHostVersion": ">=2026.4.10"
},
"compat": {
"pluginApi": ">=2026.5.12-beta.1"
"pluginApi": ">=2026.5.12-beta.5"
},
"build": {
"openclawVersion": "2026.5.12-beta.1"
"openclawVersion": "2026.5.12-beta.5"
},
"release": {
"publishToClawHub": true,

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/gradium-speech",
"version": "2026.5.12-beta.1",
"version": "2026.5.12-beta.5",
"private": true,
"description": "OpenClaw Gradium speech plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/groq-provider",
"version": "2026.5.12-beta.1",
"version": "2026.5.12-beta.5",
"private": true,
"description": "OpenClaw Groq media-understanding provider",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/huggingface-provider",
"version": "2026.5.12-beta.1",
"version": "2026.5.12-beta.5",
"private": true,
"description": "OpenClaw Hugging Face provider plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/image-generation-core",
"version": "2026.5.12-beta.1",
"version": "2026.5.12-beta.5",
"private": true,
"description": "OpenClaw image generation runtime package",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/imessage",
"version": "2026.5.12-beta.1",
"version": "2026.5.12-beta.5",
"private": true,
"description": "OpenClaw iMessage channel plugin using imsg on a signed-in Mac",
"type": "module",
@@ -40,10 +40,10 @@
]
},
"compat": {
"pluginApi": ">=2026.5.12-beta.1"
"pluginApi": ">=2026.5.12-beta.5"
},
"build": {
"openclawVersion": "2026.5.12-beta.1"
"openclawVersion": "2026.5.12-beta.5"
}
},
"pluginInspector": {

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/inworld-speech",
"version": "2026.5.12-beta.1",
"version": "2026.5.12-beta.5",
"private": true,
"description": "OpenClaw Inworld speech plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/irc",
"version": "2026.5.12-beta.1",
"version": "2026.5.12-beta.5",
"description": "OpenClaw IRC channel plugin",
"type": "module",
"devDependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/kilocode-provider",
"version": "2026.5.12-beta.1",
"version": "2026.5.12-beta.5",
"private": true,
"description": "OpenClaw Kilo Gateway provider plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/kimi-provider",
"version": "2026.5.12-beta.1",
"version": "2026.5.12-beta.5",
"private": true,
"description": "OpenClaw Kimi provider plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/line",
"version": "2026.5.12-beta.1",
"version": "2026.5.12-beta.5",
"description": "OpenClaw LINE channel plugin",
"repository": {
"type": "git",
@@ -16,7 +16,7 @@
"openclaw": "workspace:*"
},
"peerDependencies": {
"openclaw": ">=2026.5.12-beta.1"
"openclaw": ">=2026.5.12-beta.5"
},
"peerDependenciesMeta": {
"openclaw": {
@@ -46,10 +46,10 @@
"minHostVersion": ">=2026.4.10"
},
"compat": {
"pluginApi": ">=2026.5.12-beta.1"
"pluginApi": ">=2026.5.12-beta.5"
},
"build": {
"openclawVersion": "2026.5.12-beta.1"
"openclawVersion": "2026.5.12-beta.5"
},
"release": {
"publishToClawHub": true,

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/litellm-provider",
"version": "2026.5.12-beta.1",
"version": "2026.5.12-beta.5",
"private": true,
"description": "OpenClaw LiteLLM provider plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/llm-task",
"version": "2026.5.12-beta.1",
"version": "2026.5.12-beta.5",
"private": true,
"description": "OpenClaw JSON-only LLM task plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/lmstudio-provider",
"version": "2026.5.12-beta.1",
"version": "2026.5.12-beta.5",
"private": true,
"description": "OpenClaw LM Studio provider plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/lobster",
"version": "2026.5.12-beta.1",
"version": "2026.5.12-beta.5",
"description": "Lobster workflow tool plugin (typed pipelines + resumable approvals)",
"repository": {
"type": "git",
@@ -25,10 +25,10 @@
"minHostVersion": ">=2026.4.25"
},
"compat": {
"pluginApi": ">=2026.5.12-beta.1"
"pluginApi": ">=2026.5.12-beta.5"
},
"build": {
"openclawVersion": "2026.5.12-beta.1"
"openclawVersion": "2026.5.12-beta.5"
},
"release": {
"publishToClawHub": true,

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/matrix",
"version": "2026.5.12-beta.1",
"version": "2026.5.12-beta.5",
"description": "OpenClaw Matrix channel plugin",
"repository": {
"type": "git",
@@ -22,7 +22,7 @@
"openclaw": "workspace:*"
},
"peerDependencies": {
"openclaw": ">=2026.5.12-beta.1"
"openclaw": ">=2026.5.12-beta.5"
},
"peerDependenciesMeta": {
"openclaw": {
@@ -87,10 +87,10 @@
"allowInvalidConfigRecovery": true
},
"compat": {
"pluginApi": ">=2026.5.12-beta.1"
"pluginApi": ">=2026.5.12-beta.5"
},
"build": {
"openclawVersion": "2026.5.12-beta.1"
"openclawVersion": "2026.5.12-beta.5"
},
"release": {
"publishToClawHub": true,

View File

@@ -2,7 +2,7 @@ import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { describe, expect, it, vi } from "vitest";
import { ensureMatrixCryptoRuntime } from "./deps.js";
import { ensureMatrixCryptoRuntime, ensureMatrixSdkInstalled } from "./deps.js";
const logStub = vi.fn();
@@ -160,3 +160,47 @@ describe("ensureMatrixCryptoRuntime", () => {
);
});
});
describe("ensureMatrixSdkInstalled", () => {
it("returns without error when all required packages resolve", async () => {
const resolveFn = vi.fn((_id: string) => "/fake/path");
await expect(ensureMatrixSdkInstalled({ resolveFn })).resolves.toBeUndefined();
expect(resolveFn).toHaveBeenCalled();
});
it("throws actionable repair error listing every missing package", async () => {
const resolveFn = vi.fn((_id: string) => {
throw new Error("Cannot find module");
});
await expect(ensureMatrixSdkInstalled({ resolveFn })).rejects.toThrow(
/matrix-js-sdk.*@matrix-org\/matrix-sdk-crypto-nodejs.*@matrix-org\/matrix-sdk-crypto-wasm/s,
);
await expect(ensureMatrixSdkInstalled({ resolveFn })).rejects.toThrow(
/openclaw plugins update matrix/,
);
await expect(ensureMatrixSdkInstalled({ resolveFn })).rejects.toThrow(/openclaw doctor --fix/);
});
it("lists only the packages that fail to resolve", async () => {
const resolveFn = vi.fn((id: string) => {
if (id === "@matrix-org/matrix-sdk-crypto-wasm") {
throw new Error("Cannot find module");
}
return "/fake/path";
});
await expect(ensureMatrixSdkInstalled({ resolveFn })).rejects.toThrow(
/Matrix plugin dependencies are missing: @matrix-org\/matrix-sdk-crypto-wasm\./,
);
});
it("does not invoke the install confirm prompt when packages are missing (regression: #80758)", async () => {
const confirm = vi.fn(async () => true);
const resolveFn = vi.fn((_id: string) => {
throw new Error("Cannot find module");
});
await expect(ensureMatrixSdkInstalled({ resolveFn, confirm })).rejects.toThrow(
/Matrix plugin dependencies are missing/,
);
expect(confirm).not.toHaveBeenCalled();
});
});

View File

@@ -2,7 +2,6 @@ import { spawn } from "node:child_process";
import fs from "node:fs";
import { createRequire } from "node:module";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime";
@@ -26,12 +25,12 @@ type MatrixCryptoRuntimeDeps = {
log?: (message: string) => void;
};
function resolveMissingMatrixPackages(): string[] {
function resolveMissingMatrixPackages(resolveFn?: (id: string) => string): string[] {
try {
const req = createRequire(import.meta.url);
const resolve = resolveFn ?? defaultResolveFn;
return REQUIRED_MATRIX_PACKAGES.filter((pkg) => {
try {
req.resolve(pkg);
resolve(pkg);
return false;
} catch {
return true;
@@ -46,9 +45,12 @@ export function isMatrixSdkAvailable(): boolean {
return resolveMissingMatrixPackages().length === 0;
}
function resolvePluginRoot(): string {
const currentDir = path.dirname(fileURLToPath(import.meta.url));
return path.resolve(currentDir, "..", "..");
function buildMatrixDepsMissingMessage(missing: string[]): string {
const packages = missing.length > 0 ? missing.join(", ") : REQUIRED_MATRIX_PACKAGES.join(", ");
return [
`Matrix plugin dependencies are missing: ${packages}.`,
"Repair this plugin with `openclaw plugins update matrix` or run `openclaw doctor --fix`.",
].join(" ");
}
type CommandResult = {
@@ -299,47 +301,14 @@ async function ensureMatrixCryptoRuntimeOnce(params: MatrixCryptoRuntimeDeps): P
requireFn("@matrix-org/matrix-sdk-crypto-nodejs");
}
export async function ensureMatrixSdkInstalled(params: {
runtime: RuntimeEnv;
export async function ensureMatrixSdkInstalled(params?: {
runtime?: RuntimeEnv;
confirm?: (message: string) => Promise<boolean>;
resolveFn?: (id: string) => string;
}): Promise<void> {
if (isMatrixSdkAvailable()) {
const missing = resolveMissingMatrixPackages(params?.resolveFn);
if (missing.length === 0) {
return;
}
const confirm = params.confirm;
if (confirm) {
const ok = await confirm(
"Matrix requires matrix-js-sdk, @matrix-org/matrix-sdk-crypto-nodejs, and @matrix-org/matrix-sdk-crypto-wasm. Install now?",
);
if (!ok) {
throw new Error(
"Matrix requires matrix-js-sdk, @matrix-org/matrix-sdk-crypto-nodejs, and @matrix-org/matrix-sdk-crypto-wasm (install dependencies first).",
);
}
}
const root = resolvePluginRoot();
const command = fs.existsSync(path.join(root, "pnpm-lock.yaml"))
? ["pnpm", "install"]
: ["npm", "install", "--omit=dev", "--silent"];
params.runtime.log?.(`matrix: installing dependencies via ${command[0]} (${root})…`);
const result = await runFixedCommandWithTimeout({
argv: command,
cwd: root,
timeoutMs: 300_000,
env: { COREPACK_ENABLE_DOWNLOAD_PROMPT: "0" },
});
if (result.code !== 0) {
throw new Error(
result.stderr.trim() || result.stdout.trim() || "Matrix dependency install failed.",
);
}
if (!isMatrixSdkAvailable()) {
const missing = resolveMissingMatrixPackages();
throw new Error(
missing.length > 0
? `Matrix dependency install completed but required packages are still missing: ${missing.join(", ")}`
: "Matrix dependency install completed but Matrix dependencies are still missing.",
);
}
throw new Error(buildMatrixDepsMissingMessage(missing));
}

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/mattermost",
"version": "2026.5.12-beta.1",
"version": "2026.5.12-beta.5",
"description": "OpenClaw Mattermost channel plugin",
"repository": {
"type": "git",
@@ -16,7 +16,7 @@
"openclaw": "workspace:*"
},
"peerDependencies": {
"openclaw": ">=2026.5.12-beta.1"
"openclaw": ">=2026.5.12-beta.5"
},
"peerDependenciesMeta": {
"openclaw": {

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/media-understanding-core",
"version": "2026.5.12-beta.1",
"version": "2026.5.12-beta.5",
"private": true,
"description": "OpenClaw media understanding runtime package",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/memory-core",
"version": "2026.5.12-beta.1",
"version": "2026.5.12-beta.5",
"private": true,
"description": "OpenClaw core memory search plugin",
"type": "module",
@@ -14,7 +14,7 @@
"openclaw": "workspace:*"
},
"peerDependencies": {
"openclaw": ">=2026.5.12-beta.1"
"openclaw": ">=2026.5.12-beta.5"
},
"peerDependenciesMeta": {
"openclaw": {

View File

@@ -1,3 +1,4 @@
import { stringEnum } from "openclaw/plugin-sdk/channel-actions";
import {
listMemoryCorpusSupplements,
resolveMemorySearchConfig,
@@ -31,23 +32,14 @@ export const MemorySearchSchema = Type.Object({
query: Type.String(),
maxResults: Type.Optional(Type.Number()),
minScore: Type.Optional(Type.Number()),
corpus: Type.Optional(
Type.Union([
Type.Literal("memory"),
Type.Literal("wiki"),
Type.Literal("all"),
Type.Literal("sessions"),
]),
),
corpus: Type.Optional(stringEnum(["memory", "wiki", "all", "sessions"])),
});
export const MemoryGetSchema = Type.Object({
path: Type.String(),
from: Type.Optional(Type.Number()),
lines: Type.Optional(Type.Number()),
corpus: Type.Optional(
Type.Union([Type.Literal("memory"), Type.Literal("wiki"), Type.Literal("all")]),
),
corpus: Type.Optional(stringEnum(["memory", "wiki", "all"])),
});
function resolveMemoryToolContext(options: MemoryToolOptions) {

View File

@@ -7,6 +7,7 @@ import {
setMemorySearchImpl,
} from "./memory-tool-manager-mock.js";
import { createMemorySearchTool } from "./tools.js";
import { MemoryGetSchema, MemorySearchSchema } from "./tools.shared.js";
import {
asOpenClawConfig,
createMemorySearchToolOrThrow,
@@ -33,6 +34,24 @@ vi.mock("openclaw/plugin-sdk/session-transcript-hit", async (importOriginal) =>
};
});
describe("memory tool schemas", () => {
it("uses flat corpus enums for provider tool compatibility", () => {
const searchCorpus = MemorySearchSchema.properties.corpus as {
anyOf?: unknown;
enum?: unknown;
};
const getCorpus = MemoryGetSchema.properties.corpus as {
anyOf?: unknown;
enum?: unknown;
};
expect(searchCorpus.anyOf).toBeUndefined();
expect(searchCorpus.enum).toEqual(["memory", "wiki", "all", "sessions"]);
expect(getCorpus.anyOf).toBeUndefined();
expect(getCorpus.enum).toEqual(["memory", "wiki", "all"]);
});
});
describe("memory_search unavailable payloads", () => {
beforeEach(() => {
resetMemoryToolMockState({ searchImpl: async () => [] });

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