mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 14:01:24 +08:00
Compare commits
27 Commits
codex/8606
...
v2026.5.12
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fdb6e92ff5 | ||
|
|
7f0fc0bab4 | ||
|
|
8f212d0b6f | ||
|
|
b86c387d6c | ||
|
|
23dc2bfcd8 | ||
|
|
985bc40711 | ||
|
|
eab66220f8 | ||
|
|
22a6717e11 | ||
|
|
a4743ad180 | ||
|
|
1df4df6eed | ||
|
|
ca8bc5500d | ||
|
|
930046df04 | ||
|
|
03e4b035f1 | ||
|
|
6115eada6d | ||
|
|
84a2060a64 | ||
|
|
bc6090502c | ||
|
|
d3a8a45119 | ||
|
|
b12cd4358d | ||
|
|
1824464bf2 | ||
|
|
6eebba3920 | ||
|
|
7b544a7976 | ||
|
|
441041f92d | ||
|
|
7284608461 | ||
|
|
56d96b3b8d | ||
|
|
41bf26ede3 | ||
|
|
6820d18160 | ||
|
|
e6fb7aa1a8 |
@@ -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: |
|
||||
|
||||
@@ -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`.
|
||||
|
||||
13
Dockerfile
13
Dockerfile
@@ -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 ──────────────────────────────────────────
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/acpx",
|
||||
"version": "2026.5.12-beta.1",
|
||||
"version": "2026.5.12-beta.2",
|
||||
"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.2"
|
||||
},
|
||||
"build": {
|
||||
"openclawVersion": "2026.5.12-beta.1",
|
||||
"openclawVersion": "2026.5.12-beta.2",
|
||||
"staticAssets": [
|
||||
{
|
||||
"source": "./src/runtime-internals/mcp-proxy.mjs",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/alibaba-provider",
|
||||
"version": "2026.5.12-beta.1",
|
||||
"version": "2026.5.12-beta.2",
|
||||
"private": true,
|
||||
"description": "OpenClaw Alibaba Model Studio video provider plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/amazon-bedrock-mantle-provider",
|
||||
"version": "2026.5.12-beta.1",
|
||||
"version": "2026.5.12-beta.2",
|
||||
"private": true,
|
||||
"description": "OpenClaw Amazon Bedrock Mantle (OpenAI-compatible) provider plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/amazon-bedrock-provider",
|
||||
"version": "2026.5.12-beta.1",
|
||||
"version": "2026.5.12-beta.2",
|
||||
"private": true,
|
||||
"description": "OpenClaw Amazon Bedrock provider plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/anthropic-vertex-provider",
|
||||
"version": "2026.5.12-beta.1",
|
||||
"version": "2026.5.12-beta.2",
|
||||
"private": true,
|
||||
"description": "OpenClaw Anthropic Vertex provider plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/anthropic-provider",
|
||||
"version": "2026.5.12-beta.1",
|
||||
"version": "2026.5.12-beta.2",
|
||||
"private": true,
|
||||
"description": "OpenClaw Anthropic provider plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/arcee-provider",
|
||||
"version": "2026.5.12-beta.1",
|
||||
"version": "2026.5.12-beta.2",
|
||||
"private": true,
|
||||
"description": "OpenClaw Arcee provider plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/azure-speech",
|
||||
"version": "2026.5.12-beta.1",
|
||||
"version": "2026.5.12-beta.2",
|
||||
"private": true,
|
||||
"description": "OpenClaw Azure Speech plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/bonjour",
|
||||
"version": "2026.5.12-beta.1",
|
||||
"version": "2026.5.12-beta.2",
|
||||
"description": "OpenClaw Bonjour/mDNS gateway discovery",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/brave-plugin",
|
||||
"version": "2026.5.12-beta.1",
|
||||
"version": "2026.5.12-beta.2",
|
||||
"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.2"
|
||||
},
|
||||
"build": {
|
||||
"openclawVersion": "2026.5.12-beta.1"
|
||||
"openclawVersion": "2026.5.12-beta.2"
|
||||
},
|
||||
"release": {
|
||||
"publishToClawHub": true,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/browser-plugin",
|
||||
"version": "2026.5.12-beta.1",
|
||||
"version": "2026.5.12-beta.2",
|
||||
"private": true,
|
||||
"description": "OpenClaw browser tool plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/byteplus-provider",
|
||||
"version": "2026.5.12-beta.1",
|
||||
"version": "2026.5.12-beta.2",
|
||||
"private": true,
|
||||
"description": "OpenClaw BytePlus provider plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/canvas-plugin",
|
||||
"version": "2026.5.12-beta.1",
|
||||
"version": "2026.5.12-beta.2",
|
||||
"private": true,
|
||||
"description": "OpenClaw Canvas plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1 +1 @@
|
||||
6f42494c638de9f72ce783550b4a7c62912c26d47293641e588625afa06db370
|
||||
5c117c22ffc4a4bf3d82f985b24634211f61999b4fc321670bcaad8d00aa3064
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/cerebras-provider",
|
||||
"version": "2026.5.12-beta.1",
|
||||
"version": "2026.5.12-beta.2",
|
||||
"private": true,
|
||||
"description": "OpenClaw Cerebras provider plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/chutes-provider",
|
||||
"version": "2026.5.12-beta.1",
|
||||
"version": "2026.5.12-beta.2",
|
||||
"private": true,
|
||||
"description": "OpenClaw Chutes.ai provider plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/clickclack",
|
||||
"version": "2026.5.12-beta.1",
|
||||
"version": "2026.5.12-beta.2",
|
||||
"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.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"openclaw": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/cloudflare-ai-gateway-provider",
|
||||
"version": "2026.5.12-beta.1",
|
||||
"version": "2026.5.12-beta.2",
|
||||
"private": true,
|
||||
"description": "OpenClaw Cloudflare AI Gateway provider plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/codex",
|
||||
"version": "2026.5.12-beta.1",
|
||||
"version": "2026.5.12-beta.2",
|
||||
"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.2"
|
||||
},
|
||||
"build": {
|
||||
"openclawVersion": "2026.5.12-beta.1"
|
||||
"openclawVersion": "2026.5.12-beta.2"
|
||||
},
|
||||
"release": {
|
||||
"publishToClawHub": true,
|
||||
|
||||
@@ -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" }));
|
||||
|
||||
@@ -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,
|
||||
|
||||
74
extensions/codex/src/app-server/plugin-app-cache-key.ts
Normal file
74
extensions/codex/src/app-server/plugin-app-cache-key.ts
Normal 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")}`;
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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] : [],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/comfy-provider",
|
||||
"version": "2026.5.12-beta.1",
|
||||
"version": "2026.5.12-beta.2",
|
||||
"private": true,
|
||||
"description": "OpenClaw ComfyUI provider plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/copilot-proxy",
|
||||
"version": "2026.5.12-beta.1",
|
||||
"version": "2026.5.12-beta.2",
|
||||
"private": true,
|
||||
"description": "OpenClaw Copilot Proxy provider plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/deepgram-provider",
|
||||
"version": "2026.5.12-beta.1",
|
||||
"version": "2026.5.12-beta.2",
|
||||
"private": true,
|
||||
"description": "OpenClaw Deepgram media-understanding provider",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/deepinfra-provider",
|
||||
"version": "2026.5.12-beta.1",
|
||||
"version": "2026.5.12-beta.2",
|
||||
"private": true,
|
||||
"description": "OpenClaw DeepInfra provider plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/deepseek-provider",
|
||||
"version": "2026.5.12-beta.1",
|
||||
"version": "2026.5.12-beta.2",
|
||||
"private": true,
|
||||
"description": "OpenClaw DeepSeek provider plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/diagnostics-otel",
|
||||
"version": "2026.5.12-beta.1",
|
||||
"version": "2026.5.12-beta.2",
|
||||
"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.2"
|
||||
},
|
||||
"build": {
|
||||
"openclawVersion": "2026.5.12-beta.1"
|
||||
"openclawVersion": "2026.5.12-beta.2"
|
||||
},
|
||||
"release": {
|
||||
"publishToClawHub": true,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/diagnostics-prometheus",
|
||||
"version": "2026.5.12-beta.1",
|
||||
"version": "2026.5.12-beta.2",
|
||||
"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.2"
|
||||
},
|
||||
"build": {
|
||||
"openclawVersion": "2026.5.12-beta.1"
|
||||
"openclawVersion": "2026.5.12-beta.2"
|
||||
},
|
||||
"release": {
|
||||
"publishToClawHub": true,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/diffs",
|
||||
"version": "2026.5.12-beta.1",
|
||||
"version": "2026.5.12-beta.2",
|
||||
"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.2"
|
||||
},
|
||||
"build": {
|
||||
"openclawVersion": "2026.5.12-beta.1",
|
||||
"openclawVersion": "2026.5.12-beta.2",
|
||||
"staticAssets": [
|
||||
{
|
||||
"source": "./assets/viewer-runtime.js",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/discord",
|
||||
"version": "2026.5.12-beta.1",
|
||||
"version": "2026.5.12-beta.2",
|
||||
"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.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"openclaw": {
|
||||
@@ -65,10 +65,10 @@
|
||||
"allowInvalidConfigRecovery": true
|
||||
},
|
||||
"compat": {
|
||||
"pluginApi": ">=2026.5.12-beta.1"
|
||||
"pluginApi": ">=2026.5.12-beta.2"
|
||||
},
|
||||
"build": {
|
||||
"openclawVersion": "2026.5.12-beta.1"
|
||||
"openclawVersion": "2026.5.12-beta.2"
|
||||
},
|
||||
"release": {
|
||||
"publishToClawHub": true,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/document-extract-plugin",
|
||||
"version": "2026.5.12-beta.1",
|
||||
"version": "2026.5.12-beta.2",
|
||||
"private": true,
|
||||
"description": "OpenClaw local document extraction plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/duckduckgo-plugin",
|
||||
"version": "2026.5.12-beta.1",
|
||||
"version": "2026.5.12-beta.2",
|
||||
"private": true,
|
||||
"description": "OpenClaw DuckDuckGo plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/elevenlabs-speech",
|
||||
"version": "2026.5.12-beta.1",
|
||||
"version": "2026.5.12-beta.2",
|
||||
"private": true,
|
||||
"description": "OpenClaw ElevenLabs speech plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/exa-plugin",
|
||||
"version": "2026.5.12-beta.1",
|
||||
"version": "2026.5.12-beta.2",
|
||||
"private": true,
|
||||
"description": "OpenClaw Exa plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/fal-provider",
|
||||
"version": "2026.5.12-beta.1",
|
||||
"version": "2026.5.12-beta.2",
|
||||
"private": true,
|
||||
"description": "OpenClaw fal provider plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/feishu",
|
||||
"version": "2026.5.12-beta.1",
|
||||
"version": "2026.5.12-beta.2",
|
||||
"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.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"openclaw": {
|
||||
@@ -48,10 +48,10 @@
|
||||
"minHostVersion": ">=2026.4.25"
|
||||
},
|
||||
"compat": {
|
||||
"pluginApi": ">=2026.5.12-beta.1"
|
||||
"pluginApi": ">=2026.5.12-beta.2"
|
||||
},
|
||||
"build": {
|
||||
"openclawVersion": "2026.5.12-beta.1"
|
||||
"openclawVersion": "2026.5.12-beta.2"
|
||||
},
|
||||
"release": {
|
||||
"publishToClawHub": true,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/file-transfer",
|
||||
"version": "2026.5.12-beta.1",
|
||||
"version": "2026.5.12-beta.2",
|
||||
"description": "OpenClaw file transfer plugin (file_fetch, dir_list, dir_fetch, file_write)",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/firecrawl-plugin",
|
||||
"version": "2026.5.12-beta.1",
|
||||
"version": "2026.5.12-beta.2",
|
||||
"private": true,
|
||||
"description": "OpenClaw Firecrawl plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/fireworks-provider",
|
||||
"version": "2026.5.12-beta.1",
|
||||
"version": "2026.5.12-beta.2",
|
||||
"private": true,
|
||||
"description": "OpenClaw Fireworks provider plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/github-copilot-provider",
|
||||
"version": "2026.5.12-beta.1",
|
||||
"version": "2026.5.12-beta.2",
|
||||
"private": true,
|
||||
"description": "OpenClaw GitHub Copilot provider plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/google-meet",
|
||||
"version": "2026.5.12-beta.1",
|
||||
"version": "2026.5.12-beta.2",
|
||||
"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.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"openclaw": {
|
||||
@@ -33,10 +33,10 @@
|
||||
"minHostVersion": ">=2026.4.20"
|
||||
},
|
||||
"compat": {
|
||||
"pluginApi": ">=2026.5.12-beta.1"
|
||||
"pluginApi": ">=2026.5.12-beta.2"
|
||||
},
|
||||
"build": {
|
||||
"openclawVersion": "2026.5.12-beta.1"
|
||||
"openclawVersion": "2026.5.12-beta.2"
|
||||
},
|
||||
"release": {
|
||||
"publishToClawHub": true,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/google-plugin",
|
||||
"version": "2026.5.12-beta.1",
|
||||
"version": "2026.5.12-beta.2",
|
||||
"private": true,
|
||||
"description": "OpenClaw Google plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/googlechat",
|
||||
"version": "2026.5.12-beta.1",
|
||||
"version": "2026.5.12-beta.2",
|
||||
"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.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"openclaw": {
|
||||
@@ -75,10 +75,10 @@
|
||||
"minHostVersion": ">=2026.4.10"
|
||||
},
|
||||
"compat": {
|
||||
"pluginApi": ">=2026.5.12-beta.1"
|
||||
"pluginApi": ">=2026.5.12-beta.2"
|
||||
},
|
||||
"build": {
|
||||
"openclawVersion": "2026.5.12-beta.1"
|
||||
"openclawVersion": "2026.5.12-beta.2"
|
||||
},
|
||||
"release": {
|
||||
"publishToClawHub": true,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/gradium-speech",
|
||||
"version": "2026.5.12-beta.1",
|
||||
"version": "2026.5.12-beta.2",
|
||||
"private": true,
|
||||
"description": "OpenClaw Gradium speech plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/groq-provider",
|
||||
"version": "2026.5.12-beta.1",
|
||||
"version": "2026.5.12-beta.2",
|
||||
"private": true,
|
||||
"description": "OpenClaw Groq media-understanding provider",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/huggingface-provider",
|
||||
"version": "2026.5.12-beta.1",
|
||||
"version": "2026.5.12-beta.2",
|
||||
"private": true,
|
||||
"description": "OpenClaw Hugging Face provider plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/image-generation-core",
|
||||
"version": "2026.5.12-beta.1",
|
||||
"version": "2026.5.12-beta.2",
|
||||
"private": true,
|
||||
"description": "OpenClaw image generation runtime package",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/imessage",
|
||||
"version": "2026.5.12-beta.1",
|
||||
"version": "2026.5.12-beta.2",
|
||||
"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.2"
|
||||
},
|
||||
"build": {
|
||||
"openclawVersion": "2026.5.12-beta.1"
|
||||
"openclawVersion": "2026.5.12-beta.2"
|
||||
}
|
||||
},
|
||||
"pluginInspector": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/inworld-speech",
|
||||
"version": "2026.5.12-beta.1",
|
||||
"version": "2026.5.12-beta.2",
|
||||
"private": true,
|
||||
"description": "OpenClaw Inworld speech plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/irc",
|
||||
"version": "2026.5.12-beta.1",
|
||||
"version": "2026.5.12-beta.2",
|
||||
"description": "OpenClaw IRC channel plugin",
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/kilocode-provider",
|
||||
"version": "2026.5.12-beta.1",
|
||||
"version": "2026.5.12-beta.2",
|
||||
"private": true,
|
||||
"description": "OpenClaw Kilo Gateway provider plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/kimi-provider",
|
||||
"version": "2026.5.12-beta.1",
|
||||
"version": "2026.5.12-beta.2",
|
||||
"private": true,
|
||||
"description": "OpenClaw Kimi provider plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/line",
|
||||
"version": "2026.5.12-beta.1",
|
||||
"version": "2026.5.12-beta.2",
|
||||
"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.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"openclaw": {
|
||||
@@ -46,10 +46,10 @@
|
||||
"minHostVersion": ">=2026.4.10"
|
||||
},
|
||||
"compat": {
|
||||
"pluginApi": ">=2026.5.12-beta.1"
|
||||
"pluginApi": ">=2026.5.12-beta.2"
|
||||
},
|
||||
"build": {
|
||||
"openclawVersion": "2026.5.12-beta.1"
|
||||
"openclawVersion": "2026.5.12-beta.2"
|
||||
},
|
||||
"release": {
|
||||
"publishToClawHub": true,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/litellm-provider",
|
||||
"version": "2026.5.12-beta.1",
|
||||
"version": "2026.5.12-beta.2",
|
||||
"private": true,
|
||||
"description": "OpenClaw LiteLLM provider plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/llm-task",
|
||||
"version": "2026.5.12-beta.1",
|
||||
"version": "2026.5.12-beta.2",
|
||||
"private": true,
|
||||
"description": "OpenClaw JSON-only LLM task plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/lmstudio-provider",
|
||||
"version": "2026.5.12-beta.1",
|
||||
"version": "2026.5.12-beta.2",
|
||||
"private": true,
|
||||
"description": "OpenClaw LM Studio provider plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/lobster",
|
||||
"version": "2026.5.12-beta.1",
|
||||
"version": "2026.5.12-beta.2",
|
||||
"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.2"
|
||||
},
|
||||
"build": {
|
||||
"openclawVersion": "2026.5.12-beta.1"
|
||||
"openclawVersion": "2026.5.12-beta.2"
|
||||
},
|
||||
"release": {
|
||||
"publishToClawHub": true,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/matrix",
|
||||
"version": "2026.5.12-beta.1",
|
||||
"version": "2026.5.12-beta.2",
|
||||
"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.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"openclaw": {
|
||||
@@ -87,10 +87,10 @@
|
||||
"allowInvalidConfigRecovery": true
|
||||
},
|
||||
"compat": {
|
||||
"pluginApi": ">=2026.5.12-beta.1"
|
||||
"pluginApi": ">=2026.5.12-beta.2"
|
||||
},
|
||||
"build": {
|
||||
"openclawVersion": "2026.5.12-beta.1"
|
||||
"openclawVersion": "2026.5.12-beta.2"
|
||||
},
|
||||
"release": {
|
||||
"publishToClawHub": true,
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/mattermost",
|
||||
"version": "2026.5.12-beta.1",
|
||||
"version": "2026.5.12-beta.2",
|
||||
"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.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"openclaw": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/media-understanding-core",
|
||||
"version": "2026.5.12-beta.1",
|
||||
"version": "2026.5.12-beta.2",
|
||||
"private": true,
|
||||
"description": "OpenClaw media understanding runtime package",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/memory-core",
|
||||
"version": "2026.5.12-beta.1",
|
||||
"version": "2026.5.12-beta.2",
|
||||
"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.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"openclaw": {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 () => [] });
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/memory-lancedb",
|
||||
"version": "2026.5.12-beta.1",
|
||||
"version": "2026.5.12-beta.2",
|
||||
"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.2"
|
||||
},
|
||||
"build": {
|
||||
"openclawVersion": "2026.5.12-beta.1"
|
||||
"openclawVersion": "2026.5.12-beta.2"
|
||||
},
|
||||
"release": {
|
||||
"publishToClawHub": true,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/memory-wiki",
|
||||
"version": "2026.5.12-beta.1",
|
||||
"version": "2026.5.12-beta.2",
|
||||
"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.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"openclaw": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/microsoft-foundry",
|
||||
"version": "2026.5.12-beta.1",
|
||||
"version": "2026.5.12-beta.2",
|
||||
"private": true,
|
||||
"description": "OpenClaw Microsoft Foundry provider plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/microsoft-speech",
|
||||
"version": "2026.5.12-beta.1",
|
||||
"version": "2026.5.12-beta.2",
|
||||
"private": true,
|
||||
"description": "OpenClaw Microsoft speech plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/migrate-claude",
|
||||
"version": "2026.5.12-beta.1",
|
||||
"version": "2026.5.12-beta.2",
|
||||
"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.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"openclaw": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/migrate-hermes",
|
||||
"version": "2026.5.12-beta.1",
|
||||
"version": "2026.5.12-beta.2",
|
||||
"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.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"openclaw": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/minimax-provider",
|
||||
"version": "2026.5.12-beta.1",
|
||||
"version": "2026.5.12-beta.2",
|
||||
"private": true,
|
||||
"description": "OpenClaw MiniMax provider and OAuth plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/mistral-provider",
|
||||
"version": "2026.5.12-beta.1",
|
||||
"version": "2026.5.12-beta.2",
|
||||
"private": true,
|
||||
"description": "OpenClaw Mistral provider plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/moonshot-provider",
|
||||
"version": "2026.5.12-beta.1",
|
||||
"version": "2026.5.12-beta.2",
|
||||
"private": true,
|
||||
"description": "OpenClaw Moonshot provider plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/msteams",
|
||||
"version": "2026.5.12-beta.1",
|
||||
"version": "2026.5.12-beta.2",
|
||||
"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.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"openclaw": {
|
||||
@@ -58,10 +58,10 @@
|
||||
"minHostVersion": ">=2026.4.10"
|
||||
},
|
||||
"compat": {
|
||||
"pluginApi": ">=2026.5.12-beta.1"
|
||||
"pluginApi": ">=2026.5.12-beta.2"
|
||||
},
|
||||
"build": {
|
||||
"openclawVersion": "2026.5.12-beta.1"
|
||||
"openclawVersion": "2026.5.12-beta.2"
|
||||
},
|
||||
"release": {
|
||||
"publishToClawHub": true,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/nextcloud-talk",
|
||||
"version": "2026.5.12-beta.1",
|
||||
"version": "2026.5.12-beta.2",
|
||||
"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.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"openclaw": {
|
||||
@@ -44,10 +44,10 @@
|
||||
"minHostVersion": ">=2026.4.10"
|
||||
},
|
||||
"compat": {
|
||||
"pluginApi": ">=2026.5.12-beta.1"
|
||||
"pluginApi": ">=2026.5.12-beta.2"
|
||||
},
|
||||
"build": {
|
||||
"openclawVersion": "2026.5.12-beta.1"
|
||||
"openclawVersion": "2026.5.12-beta.2"
|
||||
},
|
||||
"release": {
|
||||
"publishToClawHub": true,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/nostr",
|
||||
"version": "2026.5.12-beta.1",
|
||||
"version": "2026.5.12-beta.2",
|
||||
"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.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"openclaw": {
|
||||
@@ -54,10 +54,10 @@
|
||||
"minHostVersion": ">=2026.4.10"
|
||||
},
|
||||
"compat": {
|
||||
"pluginApi": ">=2026.5.12-beta.1"
|
||||
"pluginApi": ">=2026.5.12-beta.2"
|
||||
},
|
||||
"build": {
|
||||
"openclawVersion": "2026.5.12-beta.1"
|
||||
"openclawVersion": "2026.5.12-beta.2"
|
||||
},
|
||||
"release": {
|
||||
"publishToClawHub": true,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/nvidia-provider",
|
||||
"version": "2026.5.12-beta.1",
|
||||
"version": "2026.5.12-beta.2",
|
||||
"private": true,
|
||||
"description": "OpenClaw NVIDIA provider plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/oc-path",
|
||||
"version": "2026.5.12-beta.1",
|
||||
"version": "2026.5.12-beta.2",
|
||||
"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.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"openclaw": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/ollama-provider",
|
||||
"version": "2026.5.12-beta.1",
|
||||
"version": "2026.5.12-beta.2",
|
||||
"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
Reference in New Issue
Block a user