Compare commits

...

30 Commits

Author SHA1 Message Date
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
195 changed files with 3799 additions and 549 deletions

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,13 +6,21 @@ Docs: https://docs.openclaw.ai
### Fixes
- 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.
- 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.
- fix(memory-wiki): require admin scope for ingest [AI]. (#80897) Thanks @pgondhi987.
- memory-wiki: require write scope for Obsidian search [AI]. (#80904) Thanks @pgondhi987.
- 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.
### 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 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`.

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

@@ -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
bc50c46011011a907e4a2e55f0608fe2624a8f34d2a9363853f3cc80c58f2971 plugin-sdk-api-baseline.json
fe317db84516318f0fdf885af7ce34783b7b6f47bd612f2557cb7aad46f493f3 plugin-sdk-api-baseline.jsonl

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>
@@ -156,17 +160,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 +201,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

@@ -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

@@ -1,6 +1,6 @@
{
"name": "@openclaw/acpx",
"version": "2026.5.12-beta.1",
"version": "2026.5.12-beta.3",
"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.3"
},
"build": {
"openclawVersion": "2026.5.12-beta.1",
"openclawVersion": "2026.5.12-beta.3",
"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.3",
"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.3",
"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.3",
"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.3",
"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.3",
"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.3",
"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.3",
"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.3",
"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.3",
"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.3"
},
"build": {
"openclawVersion": "2026.5.12-beta.1"
"openclawVersion": "2026.5.12-beta.3"
},
"release": {
"publishToClawHub": true,

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/browser-plugin",
"version": "2026.5.12-beta.1",
"version": "2026.5.12-beta.3",
"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.3",
"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.3",
"private": true,
"description": "OpenClaw Canvas plugin",
"type": "module",

View File

@@ -1 +1 @@
6f42494c638de9f72ce783550b4a7c62912c26d47293641e588625afa06db370
5c117c22ffc4a4bf3d82f985b24634211f61999b4fc321670bcaad8d00aa3064

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/cerebras-provider",
"version": "2026.5.12-beta.1",
"version": "2026.5.12-beta.3",
"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.3",
"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.3",
"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.3"
},
"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.3",
"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.3",
"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.3"
},
"build": {
"openclawVersion": "2026.5.12-beta.1"
"openclawVersion": "2026.5.12-beta.3"
},
"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,6 +51,7 @@ export async function requestCodexAppServerJson<T = JsonValue | undefined>(param
startOptions: params.startOptions,
timeoutMs,
authProfileId: params.authProfileId,
agentDir: params.agentDir,
config: params.config,
});
try {

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,13 @@ async function applyCodexPluginInstallItem(
method,
requestParams,
timeoutMs: 60_000,
startOptions: appServer.start,
agentDir: resolveCodexMigrationTargets(ctx).agentDir,
config: ctx.config,
}),
appCache: defaultCodexAppInventoryCache,
appCacheKey,
});
defaultCodexAppInventoryCache.clear();
const baseDetails = {
...item.details,
code: result.reason,
@@ -162,6 +176,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.3",
"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.3",
"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.3",
"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.3",
"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.3",
"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.3",
"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.3"
},
"build": {
"openclawVersion": "2026.5.12-beta.1"
"openclawVersion": "2026.5.12-beta.3"
},
"release": {
"publishToClawHub": true,

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/diagnostics-prometheus",
"version": "2026.5.12-beta.1",
"version": "2026.5.12-beta.3",
"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.3"
},
"build": {
"openclawVersion": "2026.5.12-beta.1"
"openclawVersion": "2026.5.12-beta.3"
},
"release": {
"publishToClawHub": true,

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/diffs",
"version": "2026.5.12-beta.1",
"version": "2026.5.12-beta.3",
"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.3"
},
"build": {
"openclawVersion": "2026.5.12-beta.1",
"openclawVersion": "2026.5.12-beta.3",
"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.3",
"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.3"
},
"peerDependenciesMeta": {
"openclaw": {
@@ -65,10 +65,10 @@
"allowInvalidConfigRecovery": true
},
"compat": {
"pluginApi": ">=2026.5.12-beta.1"
"pluginApi": ">=2026.5.12-beta.3"
},
"build": {
"openclawVersion": "2026.5.12-beta.1"
"openclawVersion": "2026.5.12-beta.3"
},
"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.3",
"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.3",
"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.3",
"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.3",
"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.3",
"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.3",
"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.3"
},
"peerDependenciesMeta": {
"openclaw": {
@@ -48,10 +48,10 @@
"minHostVersion": ">=2026.4.25"
},
"compat": {
"pluginApi": ">=2026.5.12-beta.1"
"pluginApi": ">=2026.5.12-beta.3"
},
"build": {
"openclawVersion": "2026.5.12-beta.1"
"openclawVersion": "2026.5.12-beta.3"
},
"release": {
"publishToClawHub": true,

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/file-transfer",
"version": "2026.5.12-beta.1",
"version": "2026.5.12-beta.3",
"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.3",
"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.3",
"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.3",
"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.3",
"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.3"
},
"peerDependenciesMeta": {
"openclaw": {
@@ -33,10 +33,10 @@
"minHostVersion": ">=2026.4.20"
},
"compat": {
"pluginApi": ">=2026.5.12-beta.1"
"pluginApi": ">=2026.5.12-beta.3"
},
"build": {
"openclawVersion": "2026.5.12-beta.1"
"openclawVersion": "2026.5.12-beta.3"
},
"release": {
"publishToClawHub": true,

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/googlechat",
"version": "2026.5.12-beta.1",
"version": "2026.5.12-beta.3",
"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.3"
},
"peerDependenciesMeta": {
"openclaw": {
@@ -75,10 +75,10 @@
"minHostVersion": ">=2026.4.10"
},
"compat": {
"pluginApi": ">=2026.5.12-beta.1"
"pluginApi": ">=2026.5.12-beta.3"
},
"build": {
"openclawVersion": "2026.5.12-beta.1"
"openclawVersion": "2026.5.12-beta.3"
},
"release": {
"publishToClawHub": true,

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/gradium-speech",
"version": "2026.5.12-beta.1",
"version": "2026.5.12-beta.3",
"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.3",
"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.3",
"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.3",
"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.3",
"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.3"
},
"build": {
"openclawVersion": "2026.5.12-beta.1"
"openclawVersion": "2026.5.12-beta.3"
}
},
"pluginInspector": {

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/inworld-speech",
"version": "2026.5.12-beta.1",
"version": "2026.5.12-beta.3",
"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.3",
"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.3",
"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.3",
"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.3",
"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.3"
},
"peerDependenciesMeta": {
"openclaw": {
@@ -46,10 +46,10 @@
"minHostVersion": ">=2026.4.10"
},
"compat": {
"pluginApi": ">=2026.5.12-beta.1"
"pluginApi": ">=2026.5.12-beta.3"
},
"build": {
"openclawVersion": "2026.5.12-beta.1"
"openclawVersion": "2026.5.12-beta.3"
},
"release": {
"publishToClawHub": true,

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/litellm-provider",
"version": "2026.5.12-beta.1",
"version": "2026.5.12-beta.3",
"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.3",
"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.3",
"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.3",
"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.3"
},
"build": {
"openclawVersion": "2026.5.12-beta.1"
"openclawVersion": "2026.5.12-beta.3"
},
"release": {
"publishToClawHub": true,

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/matrix",
"version": "2026.5.12-beta.1",
"version": "2026.5.12-beta.3",
"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.3"
},
"peerDependenciesMeta": {
"openclaw": {
@@ -87,10 +87,10 @@
"allowInvalidConfigRecovery": true
},
"compat": {
"pluginApi": ">=2026.5.12-beta.1"
"pluginApi": ">=2026.5.12-beta.3"
},
"build": {
"openclawVersion": "2026.5.12-beta.1"
"openclawVersion": "2026.5.12-beta.3"
},
"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.3",
"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.3"
},
"peerDependenciesMeta": {
"openclaw": {

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/media-understanding-core",
"version": "2026.5.12-beta.1",
"version": "2026.5.12-beta.3",
"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.3",
"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.3"
},
"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 () => [] });

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/memory-lancedb",
"version": "2026.5.12-beta.1",
"version": "2026.5.12-beta.3",
"description": "OpenClaw LanceDB-backed long-term memory plugin with auto-recall/capture",
"repository": {
"type": "git",
@@ -26,10 +26,10 @@
"minHostVersion": ">=2026.4.10"
},
"compat": {
"pluginApi": ">=2026.5.12-beta.1"
"pluginApi": ">=2026.5.12-beta.3"
},
"build": {
"openclawVersion": "2026.5.12-beta.1"
"openclawVersion": "2026.5.12-beta.3"
},
"release": {
"publishToClawHub": true,

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/microsoft-foundry",
"version": "2026.5.12-beta.1",
"version": "2026.5.12-beta.3",
"private": true,
"description": "OpenClaw Microsoft Foundry provider plugin",
"type": "module",

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/migrate-claude",
"version": "2026.5.12-beta.1",
"version": "2026.5.12-beta.3",
"private": true,
"description": "Claude to OpenClaw migration provider",
"type": "module",
@@ -9,7 +9,7 @@
"openclaw": "workspace:*"
},
"peerDependencies": {
"openclaw": ">=2026.5.12-beta.1"
"openclaw": ">=2026.5.12-beta.3"
},
"peerDependenciesMeta": {
"openclaw": {

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/migrate-hermes",
"version": "2026.5.12-beta.1",
"version": "2026.5.12-beta.3",
"private": true,
"description": "Hermes to OpenClaw migration provider",
"type": "module",
@@ -12,7 +12,7 @@
"openclaw": "workspace:*"
},
"peerDependencies": {
"openclaw": ">=2026.5.12-beta.1"
"openclaw": ">=2026.5.12-beta.3"
},
"peerDependenciesMeta": {
"openclaw": {

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/minimax-provider",
"version": "2026.5.12-beta.1",
"version": "2026.5.12-beta.3",
"private": true,
"description": "OpenClaw MiniMax provider and OAuth plugin",
"type": "module",

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/msteams",
"version": "2026.5.12-beta.1",
"version": "2026.5.12-beta.3",
"description": "OpenClaw Microsoft Teams channel plugin",
"repository": {
"type": "git",
@@ -22,7 +22,7 @@
"openclaw": "workspace:*"
},
"peerDependencies": {
"openclaw": ">=2026.5.12-beta.1"
"openclaw": ">=2026.5.12-beta.3"
},
"peerDependenciesMeta": {
"openclaw": {
@@ -58,10 +58,10 @@
"minHostVersion": ">=2026.4.10"
},
"compat": {
"pluginApi": ">=2026.5.12-beta.1"
"pluginApi": ">=2026.5.12-beta.3"
},
"build": {
"openclawVersion": "2026.5.12-beta.1"
"openclawVersion": "2026.5.12-beta.3"
},
"release": {
"publishToClawHub": true,

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/nextcloud-talk",
"version": "2026.5.12-beta.1",
"version": "2026.5.12-beta.3",
"description": "OpenClaw Nextcloud Talk channel plugin",
"repository": {
"type": "git",
@@ -12,7 +12,7 @@
"openclaw": "workspace:*"
},
"peerDependencies": {
"openclaw": ">=2026.5.12-beta.1"
"openclaw": ">=2026.5.12-beta.3"
},
"peerDependenciesMeta": {
"openclaw": {
@@ -44,10 +44,10 @@
"minHostVersion": ">=2026.4.10"
},
"compat": {
"pluginApi": ">=2026.5.12-beta.1"
"pluginApi": ">=2026.5.12-beta.3"
},
"build": {
"openclawVersion": "2026.5.12-beta.1"
"openclawVersion": "2026.5.12-beta.3"
},
"release": {
"publishToClawHub": true,

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/nostr",
"version": "2026.5.12-beta.1",
"version": "2026.5.12-beta.3",
"description": "OpenClaw Nostr channel plugin for NIP-04 encrypted DMs",
"repository": {
"type": "git",
@@ -16,7 +16,7 @@
"openclaw": "workspace:*"
},
"peerDependencies": {
"openclaw": ">=2026.5.12-beta.1"
"openclaw": ">=2026.5.12-beta.3"
},
"peerDependenciesMeta": {
"openclaw": {
@@ -54,10 +54,10 @@
"minHostVersion": ">=2026.4.10"
},
"compat": {
"pluginApi": ">=2026.5.12-beta.1"
"pluginApi": ">=2026.5.12-beta.3"
},
"build": {
"openclawVersion": "2026.5.12-beta.1"
"openclawVersion": "2026.5.12-beta.3"
},
"release": {
"publishToClawHub": true,

View File

@@ -39,7 +39,7 @@ describe("nvidia onboard", () => {
legacyModelName: "Custom",
});
expect(provider?.models.map((model) => model.id)).toEqual([
"custom-model",
"nvidia/custom-model",
"nvidia/nemotron-3-super-120b-a12b",
"moonshotai/kimi-k2.5",
"minimaxai/minimax-m2.5",

View File

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

View File

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

View File

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

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