Compare commits

..

1 Commits

Author SHA1 Message Date
Peter Steinberger
de436322ff ci(qa): disable slack live lane 2026-05-04 18:36:06 +01:00
229 changed files with 984 additions and 7067 deletions

View File

@@ -208,19 +208,12 @@ jobs:
RELEASE_PROFILE_INPUT: ${{ inputs.release_profile }}
RELEASE_RERUN_GROUP_INPUT: ${{ inputs.rerun_group }}
RELEASE_LIVE_SUITE_FILTER_INPUT: ${{ inputs.live_suite_filter }}
RELEASE_QA_SLACK_LIVE_CI_ENABLED: ${{ vars.OPENCLAW_QA_SLACK_LIVE_CI_ENABLED || 'false' }}
RELEASE_PACKAGE_ACCEPTANCE_PACKAGE_SPEC_INPUT: ${{ inputs.package_acceptance_package_spec }}
run: |
set -euo pipefail
qa_live_matrix_enabled=true
qa_live_telegram_enabled=true
qa_live_slack_enabled=false
qa_live_slack_ci_enabled="$(printf '%s' "$RELEASE_QA_SLACK_LIVE_CI_ENABLED" | tr '[:upper:]' '[:lower:]')"
if [[ "$qa_live_slack_ci_enabled" != "true" && "$qa_live_slack_ci_enabled" != "1" && "$qa_live_slack_ci_enabled" != "yes" ]]; then
qa_live_slack_ci_enabled=false
else
qa_live_slack_ci_enabled=true
fi
filter="$(printf '%s' "$RELEASE_LIVE_SUITE_FILTER_INPUT" | tr '[:upper:]' '[:lower:]')"
if [[ -n "${filter// }" ]]; then
@@ -256,7 +249,7 @@ jobs:
;;
qa-live-slack|qa-slack|slack)
qa_filter_seen=true
slack_selected="$qa_live_slack_ci_enabled"
echo "Slack live QA is disabled; ignoring ${token}." >&2
;;
esac
done
@@ -306,7 +299,7 @@ jobs:
if [[ -n "${RELEASE_LIVE_SUITE_FILTER// }" ]]; then
echo "- Live suite filter: \`${RELEASE_LIVE_SUITE_FILTER}\`"
fi
echo "- QA live lanes: Matrix \`${{ steps.inputs.outputs.qa_live_matrix_enabled }}\`, Telegram \`${{ steps.inputs.outputs.qa_live_telegram_enabled }}\`, Slack \`${{ steps.inputs.outputs.qa_live_slack_enabled }}\`"
echo "- QA live lanes: Matrix \`${{ steps.inputs.outputs.qa_live_matrix_enabled }}\`, Telegram \`${{ steps.inputs.outputs.qa_live_telegram_enabled }}\`, Slack \`${{ steps.inputs.outputs.qa_live_slack_enabled }}\` (disabled)"
if [[ -n "${PACKAGE_ACCEPTANCE_PACKAGE_SPEC// }" ]]; then
echo "- Package Acceptance package spec: \`${PACKAGE_ACCEPTANCE_PACKAGE_SPEC}\`"
else
@@ -889,7 +882,7 @@ jobs:
qa_live_slack_release_checks:
name: Run QA Lab live Slack lane
needs: [resolve_target]
if: contains(fromJSON('["all","qa","qa-live"]'), needs.resolve_target.outputs.rerun_group) && needs.resolve_target.outputs.qa_live_slack_enabled == 'true' && vars.OPENCLAW_QA_SLACK_LIVE_CI_ENABLED == 'true'
if: ${{ false }}
runs-on: blacksmith-8vcpu-ubuntu-2404
timeout-minutes: 60
permissions:

View File

@@ -241,7 +241,7 @@ jobs:
echo "$RUNNER_TEMP" >> "$GITHUB_PATH"
- name: Verify package-local runtime build
run: node scripts/check-plugin-npm-runtime-builds.mjs --package "${{ matrix.plugin.packageDir }}"
run: pnpm release:plugins:npm:runtime:check --package "${{ matrix.plugin.packageDir }}"
- name: Preview publish command
env:

View File

@@ -562,7 +562,7 @@ jobs:
run_live_slack:
name: Run Slack live QA lane with Convex leases
needs: [authorize_actor, validate_selected_ref]
if: vars.OPENCLAW_QA_SLACK_LIVE_CI_ENABLED == 'true'
if: ${{ false }}
runs-on: blacksmith-8vcpu-ubuntu-2404
timeout-minutes: 60
environment: qa-live-shared

1
.gitignore vendored
View File

@@ -219,4 +219,3 @@ extensions/**/.openclaw-runtime-deps-stamp.json
# Output dir for scripts/run-opengrep.sh (local opengrep scans)
/.opengrep-out/
/.crabbox-artifacts

View File

@@ -10,10 +10,8 @@ Docs: https://docs.openclaw.ai
### Changes
- Dependencies: refresh runtime and provider packages including Pi 0.73.0, ACPX adapters, OpenAI, Anthropic, Slack, and TypeScript native preview, while keeping the Bedrock runtime installer override pinned below the Windows ARM Node 24 npm resolver failure.
- Plugins/active-memory: skip session-store channel entries that contain `:` when resolving the recall subagent's channel, so QQ c2c agent IDs (e.g. `c2c:10D4F7C2…`) and other scoped conversation IDs do not reach bundled-plugin `dirName` validation and crash the recall run. The same guard already applied to explicit `channelId` params (#76704); this extends it to store-derived channels. (#77396) Thanks @hclsys.
- Secrets/external channel contracts: also look in `<rootDir>/dist/` when resolving the `secret-contract-api` sidecar, so npm-published externalized channel plugins (e.g. `@openclaw/discord` since 2026.5.2) whose compiled artifacts live under `dist/` actually contribute their channel SecretRef contracts to the runtime snapshot. Without this, env-backed `channels.discord.token` SecretRefs silently failed to resolve at gateway start on 2026.5.3, leaving the channel `not configured` even though #76449 had landed the generic external-contract loader. Thanks @mogglemoss.
- Models/auth: add `openclaw models auth list [--provider <id>] [--json]` so users can inspect saved per-agent auth profiles without dumping secrets or hitting the old “too many arguments” path. Thanks @vincentkoc.
- QA/Slack: disable the Slack live QA lane in scheduled all-lanes and release-check workflows while leaving the source CLI available for manual recovery.
- Control UI/header: show the active agent name in dashboard breadcrumbs without adding the current session key, keeping non-chat views oriented without crowding the topbar.
- Control UI/cron: make the New Job sidebar collapsible so the jobs list can reclaim space while keeping the form one click away. Thanks @BunsDev.
- Gateway/startup: keep model-catalog test helpers, run-session lookup code, QR pairing helpers, and TypeBox memory-tool schema construction out of hot startup import paths, reducing default gateway benchmark plugin-load and memory pressure.
@@ -44,42 +42,16 @@ Docs: https://docs.openclaw.ai
- Discord/status: add degraded Discord transport and gateway event-loop starvation signals to `openclaw channels status`, `openclaw status --deep`, and fetch-timeout logs so intermittent socket resets do not look like a healthy running channel. (#76327) Thanks @joshavant.
- Providers/OpenRouter: add opt-in response caching params that send OpenRouter's `X-OpenRouter-Cache`, `X-OpenRouter-Cache-TTL`, and cache-clear headers only on verified OpenRouter routes. Thanks @vincentkoc.
- Providers/OpenRouter: expand app-attribution categories so OpenClaw advertises coding, programming, writing, chat, and personal-agent usage on verified OpenRouter routes. Thanks @vincentkoc.
- Plugins/update: make package upgrades swap pnpm/npm-prefix installs cleanly, keep legacy plugin install runtime chunks working, and on the beta channel fall back default-line npm plugins to default/latest when plugin beta releases are missing or fail install validation. Thanks @vincentkoc and @joshavant.
- Channels/WhatsApp: support explicit WhatsApp Channel/Newsletter `@newsletter` outbound message targets with channel session metadata instead of DM routing. Fixes #13417; carries forward the narrow outbound target idea from #13424. Thanks @vincentkoc and @agentz-manfred.
- Exec approvals: add a tree-sitter-backed shell command explainer for future approval and command-review surfaces. (#75004) Thanks @jesse-merhi.
- Agents/sandbox: store sandbox container and browser registry entries as per-runtime shard files, reducing unrelated session lock contention while `openclaw doctor --fix` migrates legacy monolithic registry files. (#74831) Thanks @luckylhb90.
- Plugins/ClawHub: annotate 429 errors from ClawHub with the reset window from `RateLimit-Reset`/`Retry-After` and append a `Sign in for higher rate limits.` hint when the request was unauthenticated, so users can see when downloads will recover and how to lift the cap. Thanks @romneyda.
- Plugins/runtime state: add `registerIfAbsent` for atomic keyed-store dedupe claims that return whether a plugin successfully claimed a key without overwriting an existing live value. Thanks @amknight.
- Plugin SDK: add plugin-owned `SessionEntry` slot projection and scoped trusted-policy session extension reads. (#75609; replaces part of #73384/#74483) Thanks @100yenadmin.
### Fixes
- Agents/OpenAI: default direct OpenAI Responses models to the SSE transport instead of WebSocket auto-selection, preventing pi runtime chat turns from hanging on servers where the WebSocket path stalls while the OpenAI HTTP stream works. Thanks @vincentkoc.
- Doctor/config: report missing catalog-backed channel plugin entries as repairable installs, so upgrades with Discord or WhatsApp config point to `openclaw doctor --fix` instead of telling users to remove valid plugin config. Fixes #77483.
- CLI/update: disable and skip plugins that fail package-update plugin sync, so a broken npm/ClawHub/git/marketplace plugin cannot turn a successful OpenClaw package update into a failed update result. Thanks @vincentkoc.
- CLI/update: use an absolute POSIX npm script shell during package-manager updates, so restricted PATH environments can still run dependency lifecycle scripts while updating from `--tag main`. Fixes #77530. Thanks @PeterTremonti.
- Diagnostics: grant the internal diagnostics event bus to official installed diagnostics exporter plugins, so npm-installed `@openclaw/diagnostics-prometheus` can emit metrics without broadening the capability to arbitrary global plugins. Fixes #76628. Thanks @RayWoo.
- Browser: enforce strict SSRF current-URL checks before existing-session screenshots, matching existing-session snapshot handling. Thanks @vincentkoc.
- Active Memory: give timeout partial transcript recovery enough abort-settle headroom so temporary recall summaries are returned before cleanup. Thanks @vincentkoc.
- Gateway/chat: clear the active reply-run guard before draining queued same-session follow-up turns, so sequential `chat.send` calls no longer trip `ReplyRunAlreadyActiveError` every other request. Fixes #77485. Thanks @bws14email.
- Agents/media: avoid sending generated image, video, and music attachments twice when streamed reply text arrives before the final `MEDIA:` directive.
- CLI/sessions: cap `openclaw sessions` output to the newest 100 rows by default and add `--limit <n|all>` plus JSON pagination metadata, so repeated machine polling of large session stores cannot fan out into unbounded per-row enrichment/output work. Fixes #77500. Thanks @Kaotic3.
- Doctor/config: restore legacy group chat config migrations for `routing.allowFrom`, `routing.groupChat.*`, and `channels.telegram.requireMention` so upgrades keep WhatsApp, Telegram, and iMessage group mention gates and history settings instead of leaving configs invalid or silently blocked. Thanks @scoootscooob.
- CLI/update: make package-update follow-up processes write completion results and exit explicitly, so Windows packaged upgrades do not hang after the new package finishes post-core plugin work. Thanks @vincentkoc.
- Release validation: skip Slack live QA unless Slack credentials are explicitly configured, so release gates can keep proving non-Slack surfaces while Slack is still local and credential-gated. Thanks @vincentkoc.
- Plugins/update: treat OpenClaw CalVer correction versions like `2026.5.3-1` as satisfying base plugin API ranges, so correction builds can install plugins that require the base runtime API. Fixes #77293. (#77450) Thanks @p3nchan.
- fix(gateway): clamp unbound websocket auth scopes [AI]. (#77413) Thanks @pgondhi987.
- Gate zalouser startup name matching [AI]. (#77411) Thanks @pgondhi987.
- Active Memory: send a bounded latest-message search query to the recall worker so channel/runtime metadata does not become the memory search string. Fixes #65309. Thanks @joeykrug, @westley3601, @pimenov, and @tasi333.
- fix(device-pair): require pairing scope for pair command [AI]. (#76377) Thanks @pgondhi987.
- Providers/OpenRouter: keep DeepSeek V4 `reasoning_effort` on OpenRouter-supported values, mapping stale `max` thinking overrides to `xhigh` so `openrouter/deepseek/deepseek-v4-pro` no longer fails with OpenRouter's invalid-effort 400. Fixes #77350. (#77423) Thanks @krllagent, @mushuiyu886, and @sallyom.
- fix(qqbot): keep private commands off framework surface [AI]. (#77212) Thanks @pgondhi987.
- Claude CLI: honor non-off `/think` levels by passing Claude Code's session-scoped `--effort` flag through the CLI backend seam, so chat bridges no longer show an inert thinking control. Fixes #77303. Thanks @Petr1t.
- Agents/subagents: refresh deferred final-delivery payloads when same-session completion output changes, so retried parent notifications use the final child summary instead of stale progress text. Thanks @vincentkoc.
- active-memory: skip the memory sub-agent gracefully instead of logging a confusing allowlist error when no memory plugin (`memory-core` or `memory-lancedb`) is loaded, so active-memory with no memory backend no longer produces misleading "No callable tools remain" warnings in the gateway log. Fixes #77506. Thanks @hclsys.
- Memory/wiki: preserve representation from both corpora in `corpus=all` searches while backfilling unused result capacity, so memory hits are not starved by numerically higher wiki integer scores. Fixes #77337. Thanks @hclsys.
- Telegram: clean up tool-only draft previews after assistant message boundaries so transient `Surfacing...` tool-status bubbles do not linger when no matching final preview arrives. Thanks @BunsDev.
- Telegram: let explicit forum-topic `requireMention` settings override persisted `/activate` and `/deactivate` state, so per-topic mention gates work consistently. Fixes #49864. Thanks @Panniantong.
- Cron: surface failed isolated-run diagnostics in `cron show`, status, and run history when requested tools are unavailable, so blocked cron runs report the actual tool-policy failure instead of a misleading green result. Fixes #75763. Thanks @RyanSandoval.
- TUI/escape abort: track the in-flight runId after `chat.send` resolves so pressing Esc during the gap before the first gateway event aborts the run instead of repeatedly printing `no active run`. Fixes #1296. Thanks @Lukavyi and @romneyda.
- TUI/render: stop the long-token sanitizer from injecting literal spaces inside inline code spans, fenced code blocks, table borders, and bare hyphenated/dotted identifiers, so copied package names, entity IDs, and shell line-continuations stay byte-for-byte intact while narrow-terminal protection still chunks unidentifiable long prose tokens. Fixes #48432, #39505. Thanks @DocOellerson, @xeusoc, @CCcassiusdjs, @akramcodez, @brokemac79, @romneyda.
@@ -87,7 +59,6 @@ Docs: https://docs.openclaw.ai
- Gateway/status: label Linux managed gateway services as `systemd user`, making status output explicit about the user-service scope instead of implying a system-level unit. Thanks @vincentkoc.
- Plugins/install: remove the previous managed plugin directory when a reinstall switches sources, so stale ClawHub and npm copies no longer keep duplicate plugin ids in discovery after the new install wins. Thanks @vincentkoc.
- Plugins/install: let official plugin reinstall recovery repair source-only installed runtime shadows, so `openclaw plugins install npm:@openclaw/discord --force` can replace the bad package instead of stopping at stale config validation. Thanks @vincentkoc.
- CLI/update: stage pnpm-detected npm-layout global package updates through a clean npm prefix swap, keep plugin install runtime imports behind a stable alias, and ship legacy install-runtime aliases back to `2026.3.22`, preventing stale overlay chunks from breaking plugin post-update sync. Thanks @vincentkoc.
- Plugins/commands: allow the official ClawHub Codex plugin package to keep reserved `/codex` command ownership, matching the existing npm-managed Codex package behavior. Thanks @vincentkoc.
- Auth/OpenAI Codex: rewrite invalidated per-agent Codex auth-order and session profile overrides toward a healthy relogin profile, so revoked OAuth accounts do not stay pinned after signing in again. Thanks @BunsDev.
- Plugins/commands: scope QQBot framework slash commands to the QQBot channel so `/bot-*` command handlers and native specs do not leak onto unrelated chat surfaces. Thanks @vincentkoc.
@@ -96,7 +67,6 @@ Docs: https://docs.openclaw.ai
- Plugins/discovery: ignore managed npm plugin packages that only expose TypeScript source entries without compiled runtime output, so stale/broken installs cannot hide a working bundled or reinstallable channel plugin during setup. Thanks @vincentkoc.
- CLI/update: treat OpenClaw stable correction versions like `2026.5.3-1` as newer than their base stable release, so package updates no longer ask for downgrade confirmation. Thanks @vincentkoc.
- Plugins/install: suppress dangerous-pattern scanner warnings for trusted official OpenClaw npm installs, so installing `@openclaw/discord` no longer prints credential-harvesting warnings for the official package. Thanks @vincentkoc.
- Plugins/commands: suppress dangerous-pattern scanner warnings for trusted catalog npm installs from owner-gated `/plugins install` commands, so chat-driven installs match the CLI install trust path. Thanks @vincentkoc.
- Plugins/release: make the published npm runtime verifier reject blank `openclaw.runtimeExtensions` entries instead of treating them as absent and passing via inferred outputs. Thanks @vincentkoc.
- Plugins/security: ignore inline and block comments when matching source-rule context in plugin install scans, so comment-only `fetch`/`post` references near environment defaults do not block clean plugins. Thanks @vincentkoc.
- Doctor/plugins: remove stale managed install records for bundled plugins even when the bundled plugin is not explicitly configured, so doctor cleanup cannot leave orphaned install metadata behind. Thanks @vincentkoc.
@@ -234,12 +204,6 @@ Docs: https://docs.openclaw.ai
- Agents/subagents: detect prefix-only completion announce replies and fall back to the captured child result so requester chats no longer lose most of long sub-agent reports silently. Fixes #76412. Thanks @inxaos and @davemorin.
- TUI: replace the stale-response watchdog notice with plain user-facing copy so stalled replies no longer surface backend or streaming internals. (#77120) Thanks @davemorin.
- Security/Windows: validate `SystemRoot`/`WINDIR` env values through the Windows install-root validator and add them to the dangerous-host-env policy when resolving `icacls.exe`/`whoami.exe` for `openclaw security audit`, so workspace `.env` overrides and bare command names cannot redirect Windows ACL helpers to attacker-controlled binaries. (#74458) Thanks @mmaps.
- Security/Windows: pin Windows registry-probe `reg.exe` resolution to the canonical Windows install root in install-root probing, so `SystemRoot`/`WINDIR` env overrides cannot redirect registry queries during Windows host detection. (#74454) Thanks @mmaps.
- QQBot: preserve the framework command authorization decision when converting framework command contexts into engine slash command contexts, so downstream slash handlers see `commandAuthorized` matching the channel's resolved `isAuthorizedSender` instead of a hardcoded `true`. (#77453) Thanks @drobison00.
- Security/Windows: block `LOCALAPPDATA` from workspace `.env` and resolve Windows update-flow portable Git path prepends from the trusted process-local `LOCALAPPDATA` only, so workspace-supplied values cannot redirect `git` discovery during `openclaw update`. (#77470) Thanks @drobison00.
- Browser/SSRF: enforce the existing current-tab URL navigation policy before tab-scoped debug, export, and read routes (console, page errors, network requests, trace start/stop, response body, screenshot, snapshot, storage, etc.) collect from an already-selected tab, so blocked tabs return a policy error instead of being read first and redacted only at response time. (#75731) Thanks @eleqtrizit.
- Security/Windows: route the `.cmd`/`.bat` process wrapper through the shared Windows install-root resolver instead of `process.env.ComSpec`, so workspace dotenv-blocked `SystemRoot`/`WINDIR` overrides and unsafe values like UNC paths or path-lists cannot redirect `cmd.exe` selection on Windows. (#77472) Thanks @drobison00.
- Agents/bootstrap: honor `BOOTSTRAP.md` content injected by `agent:bootstrap` hooks when deciding whether bootstrap is pending, so hook-provided required setup instructions are included in the system prompt. (#77501) Thanks @ificator.
## 2026.5.3-1
@@ -356,7 +320,6 @@ Docs: https://docs.openclaw.ai
- CLI/message: exit cleanly with a nonzero status when message-command plugin registry loading fails before dispatch, preventing `openclaw-message` children from staying alive after plugin load errors. Fixes #76168.
- Plugins/config: report configured plugins that are present but blocked by path-safety checks as blocked instead of stale `plugin not found` entries, and deduplicate repeated blocked-candidate warnings during discovery. Fixes #76144. Thanks @mayank6136.
- Gateway/update: recover an installed-but-unloaded macOS LaunchAgent after package updates, rerun Gateway health/version/channel readiness checks, and print restart, reinstall, and rollback guidance before reporting update failure. (#76790) Thanks @jonathanlindsay.
- Codex/runtime: preserve native Codex thread bindings across dynamic-tool reorder and no-tool maintenance turns, and project mirrored history when a legacy Codex run must start without a native binding, preventing follow-up requests from losing conversation context. (#76824) Thanks @VACInc.
- CLI/plugins: explain when a missing plugin command alias belongs to a bundled plugin that is disabled by default, including the `openclaw plugins enable <plugin>` repair command. (#76835)
- Gateway/Bonjour: auto-start LAN multicast discovery only on macOS hosts while preserving explicit `openclaw plugins enable bonjour` startup elsewhere, so Linux servers and containers that do not need LAN discovery avoid default mDNS probing and watchdog churn. Refs #74209.
- Gateway/macOS: stop `doctor` and LaunchAgent recovery from running `launchctl kickstart -k` after a fresh bootstrap, avoiding an immediate SIGTERM of the just-started gateway while still nudging already-loaded launchd jobs. Fixes #76261. Thanks @solosage1.
@@ -493,7 +456,6 @@ Docs: https://docs.openclaw.ai
- Status/update: resolve beta update-channel checks from the installed version when config still says `stable`, and let `status --deep` reuse live gateway channel credential state instead of warning on command-path-only token misses.
- Doctor/plugins: preserve unmanaged third-party plugin `node_modules` during `doctor --fix`, while still pruning OpenClaw-managed runtime dependency caches.
- Gateway/restart: add `openclaw gateway restart --force` and `--wait <duration>`, log active task run IDs before restart deferral timers, and report timeout restarts as explicit forced restarts.
- Gateway/restart: align `gateway.restart.safe` preflight with scheduled restart deferral by counting only active restart blockers (running non-ended tasks), so queued task records no longer keep "safe" restarts deferred indefinitely. (#76923) Thanks @NikolaFC.
- Discord: persist slash-command deploy hashes across process restarts so unchanged command sets skip redeploy and avoid restart-loop 429s.
- Providers/LM Studio: normalize binary `off`/`on` reasoning metadata from Gemma 4 and other local models to LM Studio's accepted OpenAI-compatible `reasoning_effort` values.
- Plugins/externalization: keep official external install docs, update examples, and live Codex npm checks on default npm tags instead of `@beta`. Thanks @vincentkoc.
@@ -501,7 +463,6 @@ Docs: https://docs.openclaw.ai
- Plugins/ClawHub: fall back to version metadata when the artifact resolver route is missing and keep the Docker ClawHub fixture aligned with npm-pack artifact resolution, avoiding false version-not-found failures during plugin install validation. Thanks @vincentkoc.
- Providers/openai-codex: honor `providerConfig.baseUrl` in the dynamic-model synthesis fallback so codex providers configured with a custom upstream (for example a forwarding proxy) no longer silently bypass the configured URL when the registry has no template row to clone for the requested model id. (#76428) Thanks @arniesaha.
- Status/channels: show configured channels in `openclaw status` and config-only `openclaw channels status` output even when the Gateway is unreachable, avoiding empty Channels tables on WSL and other no-Gateway paths. Thanks @vincentkoc.
- Agents/main-session: keep pending final delivery markers until the final reply is actually routed or queued, so restart and heartbeat recovery can retry failed delivery. Refs #65037. (#75280) Thanks @MertBasar0.
- Plugins/ClawHub: explain unavailable explicit ClawHub ClawPack artifact downloads with a temporary npm install hint while ClawHub artifact routing rolls out. Thanks @vincentkoc.
- Media: accept home-relative `MEDIA:~/...` attachment paths while preserving existing file-read policy, traversal checks, and media type validation. Fixes #73796. Thanks @fabkury.
- Onboarding/search: install official external web-search plugins such as Brave before saving provider config, and make doctor repair reconcile selected external search providers whose npm payload is missing. Thanks @vincentkoc.

View File

@@ -1,2 +1,2 @@
a7116e6c0cae4c7b9ee7cd6dc48f2978812f4b5be647f3e36eee91ec9a81d85e plugin-sdk-api-baseline.json
2b6c9883d701379761724e21946d417399c1247e6a244d6b00c4a982c8ef5968 plugin-sdk-api-baseline.jsonl
f8495c07213012748f099b12ddb02847ffd4eaa1b46f2ae9dfa574fa0ef3299a plugin-sdk-api-baseline.json
815ac868dda35d0af88b9c522233d6065c3eeb70775e19c111162b80390733fa plugin-sdk-api-baseline.jsonl

View File

@@ -36,7 +36,7 @@ openclaw daemon uninstall
- `status`: `--url`, `--token`, `--password`, `--timeout`, `--no-probe`, `--require-rpc`, `--deep`, `--json`
- `install`: `--port`, `--runtime <node|bun>`, `--token`, `--force`, `--json`
- `restart`: `--safe`, `--force`, `--wait <duration>`, `--json`
- `restart`: `--force`, `--wait <duration>`, `--json`
- lifecycle (`uninstall|start|stop`): `--json`
Notes:
@@ -53,7 +53,6 @@ Notes:
- If both `gateway.auth.token` and `gateway.auth.password` are configured and `gateway.auth.mode` is unset, install is blocked until mode is set explicitly.
- On macOS, `install` keeps LaunchAgent plists owner-only and loads managed service environment values through an owner-only file and wrapper instead of serializing API keys or auth-profile env refs into `EnvironmentVariables`.
- If you intentionally run multiple gateways on one host, isolate ports, config/state, and workspaces; see [/gateway#multiple-gateways-same-host](/gateway#multiple-gateways-same-host).
- `restart --safe` asks the running Gateway to preflight active work and schedule one coalesced restart after active work drains. Plain `restart` keeps the existing service-manager behavior; `--force` remains the immediate override path.
## Prefer

View File

@@ -105,16 +105,6 @@ openclaw gateway run
Raw stream jsonl path.
</ParamField>
## Restart the Gateway
```bash
openclaw gateway restart
openclaw gateway restart --safe
openclaw gateway restart --force
```
`openclaw gateway restart --safe` asks the running Gateway to preflight active OpenClaw work before restarting. If queued operations, reply delivery, embedded runs, or task runs are active, the Gateway reports the blockers, coalesces duplicate safe restart requests, and restarts once the active work drains. Plain `restart` keeps the existing service-manager behavior for compatibility. Use `--force` only when you explicitly want the immediate override path.
<Warning>
Inline `--password` can be exposed in local process listings. Prefer `--password-file`, env, or a SecretRef-backed `gateway.auth.password`.
</Warning>

View File

@@ -16,19 +16,17 @@ until a message is processed. Use `openclaw channels status --probe`,
`openclaw status --deep`, or `openclaw health --verbose` when you need live
channel connectivity.
`openclaw sessions` and Gateway `sessions.list` responses are bounded by
default so large long-lived stores cannot monopolize the CLI process or Gateway
event loop. The CLI returns the newest 100 sessions by default; pass
`--limit <n>` for a smaller/larger window or `--limit all` when you intentionally
need the full store. JSON responses include `totalCount`, `limitApplied`, and
`hasMore` when callers need to show that more rows exist.
Gateway `sessions.list` responses are bounded by default so large long-lived
stores cannot monopolize the Gateway event loop. Pass an explicit positive
`limit` from RPC clients when a different result window is needed; responses
include `totalCount`, `limitApplied`, and `hasMore` when callers need to show
that more rows exist.
```bash
openclaw sessions
openclaw sessions --agent work
openclaw sessions --all-agents
openclaw sessions --active 120
openclaw sessions --limit 25
openclaw sessions --verbose
openclaw sessions --json
```
@@ -40,7 +38,6 @@ Scope selection:
- `--agent <id>`: one configured agent store
- `--all-agents`: aggregate all configured agent stores
- `--store <path>`: explicit store path (cannot be combined with `--agent` or `--all-agents`)
- `--limit <n|all>`: max rows to output (default `100`; `all` restores full output)
Export a trajectory bundle for a stored session:
@@ -72,9 +69,6 @@ JSON examples:
],
"allAgents": true,
"count": 2,
"totalCount": 2,
"limitApplied": 100,
"hasMore": false,
"activeMinutes": null,
"sessions": [
{ "agentId": "main", "key": "agent:main:main", "model": "gpt-5" },

View File

@@ -168,9 +168,8 @@ manually.
On the beta update channel, tracked npm and ClawHub plugin installs that follow
the default/latest line try a plugin `@beta` release first. If the plugin has no
beta release, OpenClaw falls back to the recorded default/latest spec. For npm
plugins, OpenClaw also falls back when the beta package exists but fails install
validation. Exact versions and explicit tags are not rewritten.
beta release, OpenClaw falls back to the recorded default/latest spec. Exact
versions and explicit tags are not rewritten.
<Warning>
If an exact pinned npm plugin update resolves to an artifact whose integrity differs from the stored install record, `openclaw update` aborts that plugin artifact update instead of installing it. Reinstall or update the plugin explicitly only after verifying that you trust the new artifact.

View File

@@ -116,6 +116,7 @@ For transport-real Telegram, Discord, and Slack smoke lanes:
```bash
pnpm openclaw qa telegram
pnpm openclaw qa discord
# Slack live QA is currently parked from scheduled/release workflows.
pnpm openclaw qa slack
```

View File

@@ -178,12 +178,6 @@ that agent. To force a different Claude mode, set explicit raw backend args
such as `--permission-mode default` or `--permission-mode acceptEdits` under
`agents.defaults.cliBackends.claude-cli.args` and matching `resumeArgs`.
The bundled Anthropic `claude-cli` backend also maps OpenClaw `/think` levels
to Claude Code's native `--effort` flag for non-off levels. `minimal` and
`low` map to `low`, `adaptive` and `medium` map to `medium`, and `high`,
`xhigh`, and `max` map directly. Other CLI backends need their owning plugin to
declare an equivalent argv mapper before `/think` can affect the spawned CLI.
Before OpenClaw can use the bundled `claude-cli` backend, Claude Code itself
must already be logged in on the same host:

View File

@@ -189,7 +189,6 @@ That stages grounded durable candidates into the short-term dreaming store while
- `routing.groupChat.requireMention` → `channels.whatsapp/telegram/imessage.groups."*".requireMention`
- `routing.groupChat.historyLimit` → `messages.groupChat.historyLimit`
- `routing.groupChat.mentionPatterns` → `messages.groupChat.mentionPatterns`
- `channels.telegram.requireMention` → `channels.telegram.groups."*".requireMention`
- configured-channel configs missing visible reply policy → `messages.groupChat.visibleReplies: "message_tool"`
- `routing.queue` → `messages.queue`
- `routing.bindings` → top-level `bindings`

View File

@@ -123,8 +123,8 @@ pnpm test:docker:published-upgrade-survivor
```
Available scenarios are `base`, `feishu-channel`, `bootstrap-persona`,
`plugin-deps-cleanup`, `configured-plugin-installs`,
`stale-source-plugin-shadow`, `tilde-log-path`, and `versioned-runtime-deps`. In aggregate runs,
`plugin-deps-cleanup`, `configured-plugin-installs`, `tilde-log-path`, and
`versioned-runtime-deps`. In aggregate runs,
`OPENCLAW_UPGRADE_SURVIVOR_SCENARIOS=reported-issues` expands to all reported
issue-shaped scenarios, including the configured-plugin install migration.

View File

@@ -144,14 +144,6 @@ inside every shard.
`aimock` starts a local AIMock-backed provider server for experimental
fixture and protocol-mock coverage without replacing the scenario-aware
`mock-openai` lane.
- `pnpm test:plugins:kitchen-sink-live`
- Runs the live OpenAI Kitchen Sink plugin gauntlet through QA Lab. It
installs the external Kitchen Sink package, verifies the plugin SDK surface
inventory, probes `/healthz` and `/readyz`, records gateway CPU/RSS
evidence, runs a live OpenAI turn, and checks adversarial diagnostics.
Requires live OpenAI auth such as `OPENAI_API_KEY`. In hydrated Testbox
sessions it automatically sources the Testbox live-auth profile when the
`openclaw-testbox-env` helper is present.
- `pnpm test:gateway:cpu-scenarios`
- Runs the gateway startup bench plus a small mock QA Lab scenario pack
(`channel-chat-baseline`, `memory-failure-fallback`,

View File

@@ -92,9 +92,7 @@ when it was previously pinned to an exact version or tag.
When `openclaw update` runs on the beta channel, default-line npm and ClawHub
plugin records try the matching plugin `@beta` release first. If that beta
release does not exist, OpenClaw falls back to the recorded default/latest spec.
For npm plugins, OpenClaw also falls back when the beta package exists but fails
install validation. Exact versions and explicit tags such as `@rc` or `@beta`
are preserved.
Exact versions and explicit tags such as `@rc` or `@beta` are preserved.
## Uninstall plugins

View File

@@ -257,9 +257,6 @@ AI CLI backend such as `codex-cli`.
plugin default before running the CLI.
- Use `normalizeConfig` when a backend needs compatibility rewrites after merge
(for example normalizing old flag shapes).
- Use `resolveExecutionArgs` for request-scoped argv rewrites that belong to
the CLI dialect, such as mapping OpenClaw thinking levels to a native effort
flag.
### Exclusive slots

View File

@@ -211,9 +211,7 @@ does **not** inject those OpenRouter-specific headers or Anthropic cache markers
On verified OpenRouter routes, `openrouter/deepseek/deepseek-v4-flash` and
`openrouter/deepseek/deepseek-v4-pro` fill missing `reasoning_content` on
replayed assistant turns so thinking/tool conversations keep DeepSeek V4's
required follow-up shape. OpenClaw sends OpenRouter-supported
`reasoning_effort` values for these routes; `xhigh` is the highest advertised
level, and stale `max` overrides are mapped to `xhigh`.
required follow-up shape.
</Accordion>
<Accordion title="OpenAI-only request shaping">

View File

@@ -44,7 +44,7 @@ title: "Tests"
- `pnpm test:docker:openwebui`: Starts Dockerized OpenClaw + Open WebUI, signs in through Open WebUI, checks `/api/models`, then runs a real proxied chat through `/api/chat/completions`. Requires a usable live model key (for example OpenAI in `~/.profile`), pulls an external Open WebUI image, and is not expected to be CI-stable like the normal unit/e2e suites.
- `pnpm test:docker:mcp-channels`: Starts a seeded Gateway container and a second client container that spawns `openclaw mcp serve`, then verifies routed conversation discovery, transcript reads, attachment metadata, live event queue behavior, outbound send routing, and Claude-style channel + permission notifications over the real stdio bridge. The Claude notification assertion reads the raw stdio MCP frames directly so the smoke reflects what the bridge actually emits.
- `pnpm test:docker:upgrade-survivor`: Installs the packed OpenClaw tarball over a dirty old-user fixture, runs package update plus non-interactive doctor without live provider or channel keys, then starts a loopback Gateway and checks that agents, channel config, plugin allowlists, workspace/session files, stale legacy plugin dependency state, startup, and RPC status survive.
- `pnpm test:docker:published-upgrade-survivor`: Installs `openclaw@latest` by default, seeds realistic existing-user files without live provider or channel keys, configures that baseline with a baked `openclaw config set` command recipe, updates that published install to the packed OpenClaw tarball, runs non-interactive doctor, writes `.artifacts/upgrade-survivor/summary.json`, then starts a loopback Gateway and checks that configured intents, workspace/session files, stale plugin config and legacy dependency state, startup, `/healthz`, `/readyz`, and RPC status survive or repair cleanly. Override one baseline with `OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC`, expand an exact matrix with `OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPECS` such as `all-since-2026.4.23`, or add scenario fixtures with `OPENCLAW_UPGRADE_SURVIVOR_SCENARIOS=reported-issues`; the reported-issues set includes `configured-plugin-installs` to verify configured external OpenClaw plugins install automatically during upgrade and `stale-source-plugin-shadow` to keep source-only plugin shadows from breaking startup. Package Acceptance exposes those as `published_upgrade_survivor_baseline`, `published_upgrade_survivor_baselines`, and `published_upgrade_survivor_scenarios`.
- `pnpm test:docker:published-upgrade-survivor`: Installs `openclaw@latest` by default, seeds realistic existing-user files without live provider or channel keys, configures that baseline with a baked `openclaw config set` command recipe, updates that published install to the packed OpenClaw tarball, runs non-interactive doctor, writes `.artifacts/upgrade-survivor/summary.json`, then starts a loopback Gateway and checks that configured intents, workspace/session files, stale plugin config and legacy dependency state, startup, `/healthz`, `/readyz`, and RPC status survive or repair cleanly. Override one baseline with `OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC`, expand an exact matrix with `OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPECS` such as `all-since-2026.4.23`, or add scenario fixtures with `OPENCLAW_UPGRADE_SURVIVOR_SCENARIOS=reported-issues`; the reported-issues set includes `configured-plugin-installs` to verify configured external OpenClaw plugins install automatically during upgrade. Package Acceptance exposes those as `published_upgrade_survivor_baseline`, `published_upgrade_survivor_baselines`, and `published_upgrade_survivor_scenarios`.
- `pnpm test:docker:update-migration`: Runs the published-upgrade survivor harness in the cleanup-heavy `plugin-deps-cleanup` scenario, starting at `openclaw@2026.4.23` by default. The separate `Update Migration` workflow expands this lane with `baselines=all-since-2026.4.23` so every stable published package from `.23` onward updates to the candidate and proves configured-plugin dependency cleanup outside Full Release CI.
- `pnpm test:docker:plugins`: Runs install/update smoke for local path, `file:`, npm registry packages with hoisted dependencies, git moving refs, ClawHub fixtures, marketplace updates, and Claude-bundle enable/inspect.

View File

@@ -26,8 +26,7 @@ title: "Thinking levels"
- Anthropic Claude Opus 4.7 does not default to adaptive thinking. Its API effort default remains provider-owned unless you explicitly set a thinking level.
- Anthropic Claude Opus 4.7 maps `/think xhigh` to adaptive thinking plus `output_config.effort: "xhigh"`, because `/think` is a thinking directive and `xhigh` is the Opus 4.7 effort setting.
- Anthropic Claude Opus 4.7 also exposes `/think max`; it maps to the same provider-owned max effort path.
- Direct DeepSeek V4 models expose `/think xhigh|max`; both map to DeepSeek `reasoning_effort: "max"` while lower non-off levels map to `high`.
- OpenRouter-routed DeepSeek V4 models expose `/think xhigh` and send OpenRouter-supported `reasoning_effort` values. Stored `max` overrides fall back to `xhigh`.
- DeepSeek V4 models expose `/think xhigh|max`; both map to DeepSeek `reasoning_effort: "max"` while lower non-off levels map to `high`.
- Ollama thinking-capable models expose `/think low|medium|high|max`; `max` maps to native `think: "high"` because Ollama's native API accepts `low`, `medium`, and `high` effort strings.
- OpenAI GPT models map `/think` through model-specific Responses API effort support. `/think off` sends `reasoning.effort: "none"` only when the target model supports it; otherwise OpenClaw omits the disabled reasoning payload instead of sending an unsupported value.
- Custom OpenAI-compatible catalog entries can opt into `/think xhigh` by setting `models.providers.<provider>.models[].compat.supportedReasoningEfforts` to include `"xhigh"`. This uses the same compat metadata that maps outbound OpenAI reasoning effort payloads, so menus, session validation, agent CLI, and `llm-task` agree with transport behavior.
@@ -55,7 +54,6 @@ title: "Thinking levels"
## Application by agent
- **Embedded Pi**: the resolved level is passed to the in-process Pi agent runtime.
- **Claude CLI backend**: non-off levels are passed to Claude Code as `--effort` when using `claude-cli`; see [CLI backends](/gateway/cli-backends).
## Fast mode (/fast)

View File

@@ -8,8 +8,8 @@
},
"type": "module",
"dependencies": {
"@agentclientprotocol/claude-agent-acp": "0.32.0",
"@zed-industries/codex-acp": "0.13.0",
"@agentclientprotocol/claude-agent-acp": "0.31.4",
"@zed-industries/codex-acp": "0.12.0",
"acpx": "0.6.1"
},
"devDependencies": {

View File

@@ -211,8 +211,8 @@ ${ACPX_CMD} codex sessions close oc-codex-<conversationId>
Defaults are:
- `openclaw -> openclaw acp`
- `claude -> bundled @agentclientprotocol/claude-agent-acp@0.32.0`
- `codex -> bundled @zed-industries/codex-acp@0.13.0 through OpenClaw's isolated CODEX_HOME wrapper`
- `claude -> npx -y @agentclientprotocol/claude-agent-acp@^0.31.0`
- `codex -> bundled @zed-industries/codex-acp@0.12.0 through OpenClaw's isolated CODEX_HOME wrapper`
- `copilot -> copilot --acp --stdio`
- `cursor -> cursor-agent acp`
- `droid -> droid exec --output-format acp`

View File

@@ -163,7 +163,7 @@ describe("prepareAcpxCodexAuthConfig", () => {
});
const wrapper = await fs.readFile(generated.wrapperPath, "utf8");
expect(wrapper).toContain('"@zed-industries/codex-acp@0.13.0"');
expect(wrapper).toContain('"@zed-industries/codex-acp@^0.12.0"');
expect(wrapper).toContain('"--", "codex-acp"');
expect(wrapper).not.toContain("@zed-industries/codex-acp@^0.11.1");
});
@@ -184,7 +184,7 @@ describe("prepareAcpxCodexAuthConfig", () => {
});
const wrapper = await fs.readFile(generated.wrapperPath, "utf8");
expect(wrapper).toContain('"@agentclientprotocol/claude-agent-acp@0.32.0"');
expect(wrapper).toContain('"@agentclientprotocol/claude-agent-acp@0.31.4"');
expect(wrapper).toContain('"--", "claude-agent-acp"');
expect(wrapper).not.toContain("@agentclientprotocol/claude-agent-acp@^0.31.0");
expect(wrapper).not.toContain("@agentclientprotocol/claude-agent-acp@0.31.0");

View File

@@ -4,8 +4,10 @@ import path from "node:path";
import type { ResolvedAcpxPluginConfig } from "./config.js";
const CODEX_ACP_PACKAGE = "@zed-industries/codex-acp";
const CODEX_ACP_PACKAGE_RANGE = "^0.12.0";
const CODEX_ACP_BIN = "codex-acp";
const CLAUDE_ACP_PACKAGE = "@agentclientprotocol/claude-agent-acp";
const CLAUDE_ACP_PACKAGE_VERSION = "0.31.4";
const CLAUDE_ACP_BIN = "claude-agent-acp";
const RUN_CONFIGURED_COMMAND_SENTINEL = "--openclaw-run-configured";
const requireFromHere = createRequire(import.meta.url);
@@ -13,22 +15,8 @@ const requireFromHere = createRequire(import.meta.url);
type PackageManifest = {
name?: unknown;
bin?: unknown;
dependencies?: Record<string, unknown>;
};
const selfManifest = requireFromHere("../package.json") as PackageManifest;
function readManifestDependencyVersion(packageName: string): string {
const version = selfManifest.dependencies?.[packageName];
if (typeof version !== "string" || version.trim() === "") {
throw new Error(`Missing ${packageName} dependency version in @openclaw/acpx manifest`);
}
return version;
}
const CODEX_ACP_PACKAGE_VERSION = readManifestDependencyVersion(CODEX_ACP_PACKAGE);
const CLAUDE_ACP_PACKAGE_VERSION = readManifestDependencyVersion(CLAUDE_ACP_PACKAGE);
function quoteCommandPart(value: string): string {
return JSON.stringify(value);
}
@@ -217,7 +205,7 @@ child.on("exit", (code, signal) => {
function buildCodexAcpWrapperScript(installedBinPath?: string): string {
return buildAdapterWrapperScript({
displayName: "Codex",
packageSpec: `${CODEX_ACP_PACKAGE}@${CODEX_ACP_PACKAGE_VERSION}`,
packageSpec: `${CODEX_ACP_PACKAGE}@${CODEX_ACP_PACKAGE_RANGE}`,
binName: CODEX_ACP_BIN,
installedBinPath,
envSetup: `const codexHome = fileURLToPath(new URL("./codex-home/", import.meta.url));

View File

@@ -13,8 +13,8 @@ describe("acpx package manifest", () => {
) as AcpxPackageManifest;
expect(packageJson.dependencies?.acpx).toBeDefined();
expect(packageJson.dependencies?.["@zed-industries/codex-acp"]).toBe("0.13.0");
expect(packageJson.dependencies?.["@agentclientprotocol/claude-agent-acp"]).toBe("0.32.0");
expect(packageJson.dependencies?.["@zed-industries/codex-acp"]).toBe("0.12.0");
expect(packageJson.dependencies?.["@agentclientprotocol/claude-agent-acp"]).toBe("0.31.4");
expect(packageJson.devDependencies?.["@agentclientprotocol/claude-agent-acp"]).toBeUndefined();
});
});

View File

@@ -9,7 +9,7 @@ type TestSessionStore = {
const DOCUMENTED_OPENCLAW_BRIDGE_COMMAND =
"env OPENCLAW_HIDE_BANNER=1 OPENCLAW_SUPPRESS_NOTES=1 openclaw acp --url ws://127.0.0.1:18789 --token-file ~/.openclaw/gateway.token --session agent:main:main";
const CODEX_ACP_COMMAND = "npx @zed-industries/codex-acp@0.13.0";
const CODEX_ACP_COMMAND = "npx @zed-industries/codex-acp@^0.12.0";
const CODEX_ACP_WRAPPER_COMMAND = `node "/tmp/openclaw/acpx/codex-acp-wrapper.mjs"`;
function makeRuntime(
@@ -226,7 +226,7 @@ describe("AcpxRuntime fresh reset wrapper", () => {
reasoningEffort: "medium",
}),
).toBe(
"npx @zed-industries/codex-acp@0.13.0 -c model=gpt-5.4 -c model_reasoning_effort=medium",
"npx @zed-industries/codex-acp@^0.12.0 -c model=gpt-5.4 -c model_reasoning_effort=medium",
);
expect(__testing.isCodexAcpCommand("openclaw acp")).toBe(false);
});

View File

@@ -125,23 +125,6 @@ describe("active-memory plugin", () => {
"utf8",
);
};
const makeMemoryToolAllowlistError = (
reason: string,
sources = "runtime toolsAllow: memory_recall, memory_search, memory_get",
) =>
new Error(
`No callable tools remain after resolving explicit tool allowlist ` +
`(${sources}); ${reason}. ` +
`Fix the allowlist or enable the plugin that registers the requested tool.`,
);
const hasDebugLine = (needle: string) =>
vi
.mocked(api.logger.debug)
.mock.calls.some((call: unknown[]) => String(call[0]).includes(needle));
const hasWarnLine = (needle: string) =>
vi
.mocked(api.logger.warn)
.mock.calls.some((call: unknown[]) => String(call[0]).includes(needle));
beforeEach(async () => {
vi.clearAllMocks();
@@ -1091,12 +1074,9 @@ describe("active-memory plugin", () => {
"Your job is to search memory and return only the most relevant memory context for that model.",
);
expect(runParams?.prompt).toContain(
"You receive a bounded search query plus conversation context, including the user's latest message.",
"You receive conversation context, including the user's latest message.",
);
expect(runParams?.prompt).toContain("Use only the available memory tools.");
expect(runParams?.prompt).toContain(
"Use the bounded search query as the memory_search or memory_recall query.",
);
expect(runParams?.prompt).toContain("Prefer memory_recall when available.");
expect(runParams?.prompt).toContain(
"If memory_recall is unavailable, use memory_search and memory_get.",
@@ -1663,133 +1643,6 @@ describe("active-memory plugin", () => {
expect(result).toBeUndefined();
});
it("skips the recall subagent when no registered memory tools match", async () => {
const sessionKey = "agent:main:missing-memory-tools";
hoisted.sessionStore[sessionKey] = {
sessionId: "s-missing-memory-tools",
updatedAt: 0,
};
const error = makeMemoryToolAllowlistError("no registered tools matched");
expect(__testing.isMissingRegisteredMemoryToolsError(error)).toBe(true);
runEmbeddedPiAgent.mockRejectedValueOnce(error);
const result = await hooks.before_prompt_build(
{ prompt: "what wings should i order? missing memory tools", messages: [] },
{ agentId: "main", trigger: "user", sessionKey, messageProvider: "webchat" },
);
expect(result).toBeUndefined();
expect(hasDebugLine("no memory tools registered")).toBe(true);
expect(hasWarnLine("No callable tools remain")).toBe(false);
const lines = getActiveMemoryLines(sessionKey);
expect(lines).toEqual([expect.stringContaining("🧩 Active Memory: status=empty")]);
expect(lines.join("\n")).not.toContain("status=unavailable");
});
it("skips missing memory tools when the allowlist error includes inherited sources", async () => {
const sessionKey = "agent:main:missing-memory-tools-with-policy-source";
hoisted.sessionStore[sessionKey] = {
sessionId: "s-missing-memory-tools-with-policy-source",
updatedAt: 0,
};
const error = makeMemoryToolAllowlistError(
"no registered tools matched",
"tools.allow: *, lobster; runtime toolsAllow: memory_recall, memory_search, memory_get",
);
expect(__testing.isMissingRegisteredMemoryToolsError(error)).toBe(true);
runEmbeddedPiAgent.mockRejectedValueOnce(error);
const result = await hooks.before_prompt_build(
{ prompt: "what wings should i order? missing memory tools with policy", messages: [] },
{ agentId: "main", trigger: "user", sessionKey, messageProvider: "webchat" },
);
expect(result).toBeUndefined();
expect(hasDebugLine("no memory tools registered")).toBe(true);
expect(hasWarnLine("No callable tools remain")).toBe(false);
expect(getActiveMemoryLines(sessionKey)).toEqual([
expect.stringContaining("🧩 Active Memory: status=empty"),
]);
});
it("keeps memory-tool allowlist errors visible when upstream policy can filter memory tools", async () => {
const sessionKey = "agent:main:memory-tools-filtered-by-policy";
hoisted.sessionStore[sessionKey] = {
sessionId: "s-memory-tools-filtered-by-policy",
updatedAt: 0,
};
const error = makeMemoryToolAllowlistError(
"no registered tools matched",
"tools.allow: read, exec; runtime toolsAllow: memory_recall, memory_search, memory_get",
);
expect(__testing.isMissingRegisteredMemoryToolsError(error)).toBe(false);
runEmbeddedPiAgent.mockRejectedValueOnce(error);
const result = await hooks.before_prompt_build(
{ prompt: "what wings should i order? memory tools filtered by policy", messages: [] },
{ agentId: "main", trigger: "user", sessionKey, messageProvider: "webchat" },
);
expect(result).toBeUndefined();
expect(hasDebugLine("no memory tools registered")).toBe(false);
expect(hasWarnLine("No callable tools remain")).toBe(true);
expect(getActiveMemoryLines(sessionKey)).toEqual([
expect.stringContaining("🧩 Active Memory: status=unavailable"),
]);
});
it.each([
["disabled tools", "tools are disabled for this run"],
["models without tool support", "the selected model does not support tools"],
])("keeps allowlist errors for %s visible", async (_label, reason) => {
const sessionKey = `agent:main:${reason.replace(/\W+/g, "-")}`;
hoisted.sessionStore[sessionKey] = {
sessionId: `s-${reason.replace(/\W+/g, "-")}`,
updatedAt: 0,
};
const error = makeMemoryToolAllowlistError(reason);
expect(__testing.isMissingRegisteredMemoryToolsError(error)).toBe(false);
runEmbeddedPiAgent.mockRejectedValueOnce(error);
const result = await hooks.before_prompt_build(
{ prompt: `what wings should i order? ${reason}`, messages: [] },
{ agentId: "main", trigger: "user", sessionKey, messageProvider: "webchat" },
);
expect(result).toBeUndefined();
expect(hasDebugLine("no memory tools registered")).toBe(false);
expect(hasWarnLine(reason)).toBe(true);
expect(getActiveMemoryLines(sessionKey)).toEqual([
expect.stringContaining("🧩 Active Memory: status=unavailable"),
]);
});
it("does not skip missing memory-tool allowlist errors after abort", async () => {
const sessionKey = "agent:main:missing-memory-tools-after-abort";
hoisted.sessionStore[sessionKey] = {
sessionId: "s-missing-memory-tools-after-abort",
updatedAt: 0,
};
runEmbeddedPiAgent.mockImplementationOnce(async (params: { abortSignal?: AbortSignal }) => {
Object.defineProperty(params.abortSignal as AbortSignal, "aborted", {
configurable: true,
value: true,
});
throw makeMemoryToolAllowlistError("no registered tools matched");
});
const result = await hooks.before_prompt_build(
{ prompt: "what wings should i order? missing memory tools after abort", messages: [] },
{ agentId: "main", trigger: "user", sessionKey, messageProvider: "webchat" },
);
expect(result).toBeUndefined();
expect(hasDebugLine("no memory tools registered")).toBe(false);
expect(getActiveMemoryLines(sessionKey)).toEqual([
expect.stringContaining("🧩 Active Memory: status=timeout"),
]);
});
it("returns partial transcript text on timeout when the subagent has already written assistant output", async () => {
__testing.setMinimumTimeoutMsForTests(1);
__testing.setSetupGraceTimeoutMsForTests(0);
@@ -2900,33 +2753,6 @@ describe("active-memory plugin", () => {
});
});
it("skips colon-containing session-store channels for embedded recall (#77396)", async () => {
hoisted.sessionStore["agent:main:qqbot:direct:12345"] = {
sessionId: "session-a",
updatedAt: 25,
channel: "c2c:10D4F7C2",
origin: {
provider: "qqbot",
},
};
await hooks.before_prompt_build(
{ prompt: "what wings should i order? scoped stored channel", messages: [] },
{
agentId: "main",
trigger: "user",
sessionKey: "agent:main:qqbot:direct:12345",
messageProvider: "qqbot",
channelId: "qqbot",
},
);
expect(runEmbeddedPiAgent.mock.calls.at(-1)?.[0]).toMatchObject({
messageChannel: "qqbot",
messageProvider: "qqbot",
});
});
it("preserves an explicit real channel hint over a stale stored wrapper channel", async () => {
hoisted.sessionStore["agent:main:telegram:direct:12345"] = {
sessionId: "session-a",
@@ -3041,54 +2867,10 @@ describe("active-memory plugin", () => {
);
const prompt = runEmbeddedPiAgent.mock.calls.at(-1)?.[0]?.prompt;
expect(prompt).toContain("Bounded memory search query:\nwhat should i grab on the way?");
expect(prompt).toContain("Conversation context:\nwhat should i grab on the way?");
expect(prompt).not.toContain("Recent conversation tail:");
});
it("sends a bounded latest-message query instead of channel metadata to memory search", async () => {
api.pluginConfig = {
agents: ["main"],
queryMode: "recent",
};
plugin.register(api as unknown as OpenClawPluginApi);
await hooks.before_prompt_build(
{
prompt: [
"Conversation info:",
"Sender: discord:user-123",
"Untrusted Discord message body",
"---",
"do you remember my flight preferences?",
].join("\n"),
messages: [
{ role: "user", content: "i have a flight tomorrow" },
{ role: "assistant", content: "got it" },
],
},
{
agentId: "main",
trigger: "user",
sessionKey: "agent:main:main",
messageProvider: "webchat",
},
);
const prompt = runEmbeddedPiAgent.mock.calls.at(-1)?.[0]?.prompt;
expect(prompt).toContain(
"Bounded memory search query:\ndo you remember my flight preferences?",
);
expect(prompt).toContain(
"Do not use channel metadata, provider metadata, debug output, or the full conversation context as the memory tool query.",
);
expect(prompt).toContain("Conversation context:");
expect(prompt).toContain("Conversation info:");
expect(prompt).not.toContain("Bounded memory search query:\nConversation info:");
expect(prompt).not.toContain("Bounded memory search query:\nSender:");
expect(prompt).not.toContain("Bounded memory search query:\nUntrusted Discord message body");
});
it("supports full mode by sending the whole conversation", async () => {
api.pluginConfig = {
agents: ["main"],
@@ -3427,6 +3209,7 @@ describe("active-memory plugin", () => {
`^${escapeRegExp(expectedDir)}${escapeRegExp(path.sep)}active-memory-[a-z0-9]+-[a-f0-9]{8}\\.jsonl$`,
),
);
expect(rmSpy).not.toHaveBeenCalled();
expect(
vi
.mocked(api.logger.info)
@@ -3434,7 +3217,6 @@ describe("active-memory plugin", () => {
String(call[0]).includes(`transcript=${expectedDir}${path.sep}`),
),
).toBe(true);
expect(rmSpy.mock.calls.some(([target]) => String(target).startsWith(expectedDir))).toBe(false);
});
it("falls back to the default transcript directory when transcriptDir is unsafe", async () => {

View File

@@ -41,13 +41,11 @@ const DEFAULT_QMD_SEARCH_MODE = "search" as const;
const DEFAULT_TRANSCRIPT_DIR = "active-memory";
const DEFAULT_CIRCUIT_BREAKER_MAX_TIMEOUTS = 3;
const DEFAULT_CIRCUIT_BREAKER_COOLDOWN_MS = 60_000;
const ACTIVE_MEMORY_TOOL_ALLOWLIST = ["memory_recall", "memory_search", "memory_get"] as const;
const TOGGLE_STATE_FILE = "session-toggles.json";
const DEFAULT_PARTIAL_TRANSCRIPT_MAX_CHARS = 32_000;
const DEFAULT_TRANSCRIPT_READ_MAX_LINES = 2_000;
const DEFAULT_TRANSCRIPT_READ_MAX_BYTES = 50 * 1024 * 1024;
const TIMEOUT_PARTIAL_DATA_GRACE_MS = 500;
const MAX_ACTIVE_MEMORY_SEARCH_QUERY_CHARS = 480;
const TIMEOUT_PARTIAL_DATA_GRACE_MS = 50;
const TERMINAL_MEMORY_SEARCH_POLL_INTERVAL_MS = 25;
const NO_RECALL_VALUES = new Set([
@@ -495,38 +493,6 @@ function normalizeOptionalString(value: unknown): string | undefined {
return typeof value === "string" && value.trim() ? value.trim() : undefined;
}
function isMissingRegisteredMemoryToolsError(error: unknown): boolean {
if (!(error instanceof Error)) {
return false;
}
const message = error.message.trim();
const prefix = "No callable tools remain after resolving explicit tool allowlist (";
const suffix =
"); no registered tools matched. Fix the allowlist or enable the plugin that registers the requested tool.";
if (!message.startsWith(prefix) || !message.endsWith(suffix)) {
return false;
}
const sources = message.slice(prefix.length, -suffix.length);
const runtimeSource = `runtime toolsAllow: ${ACTIVE_MEMORY_TOOL_ALLOWLIST.join(", ")}`;
const sourceParts = sources
.split(";")
.map((source) => source.trim())
.filter(Boolean);
if (!sourceParts.includes(runtimeSource)) {
return false;
}
return sourceParts.every((source) => {
if (source === runtimeSource) {
return true;
}
const entries = source
.slice(source.indexOf(":") + 1)
.split(",")
.map((entry) => entry.trim());
return entries.includes("*");
});
}
function resolveRecallRunChannelContext(params: {
api: OpenClawPluginApi;
agentId: string;
@@ -594,17 +560,9 @@ function resolveRecallRunChannelContext(params: {
store,
sessionKey: resolvedSessionKey,
}).existing;
const rawStrongEntryChannel =
const strongEntryChannel =
normalizeOptionalString(sessionEntry?.lastChannel) ??
normalizeOptionalString(sessionEntry?.channel);
// Channel IDs containing ":" are scoped conversation IDs (e.g. QQ c2c
// "c2c:10D4F7C2..."), not runnable channel names. The same guard that
// applies to explicit channelId (#76704) must also apply to channels
// read from the session store (#77396).
const strongEntryChannel =
rawStrongEntryChannel && !rawStrongEntryChannel.includes(":")
? rawStrongEntryChannel
: undefined;
const weakEntryChannel = normalizeOptionalString(sessionEntry?.origin?.provider);
return resolveReturnValue({
resolvedChannel: strongEntryChannel ?? weakEntryChannel,
@@ -974,16 +932,13 @@ function buildPromptStyleLines(style: ActiveMemoryPromptStyle): string[] {
function buildRecallPrompt(params: {
config: ResolvedActiveRecallPluginConfig;
query: string;
searchQuery: string;
}): string {
const defaultInstructions = [
"You are a memory search agent.",
"Another model is preparing the final user-facing answer.",
"Your job is to search memory and return only the most relevant memory context for that model.",
"You receive a bounded search query plus conversation context, including the user's latest message.",
"You receive conversation context, including the user's latest message.",
"Use only the available memory tools.",
"Use the bounded search query as the memory_search or memory_recall query.",
"Do not use channel metadata, provider metadata, debug output, or the full conversation context as the memory tool query.",
"Prefer memory_recall when available.",
"If memory_recall is unavailable, use memory_search and memory_get.",
"When searching for preference or habit recall, use a permissive recall limit or memory_search threshold before deciding that no useful memory exists.",
@@ -1035,11 +990,7 @@ function buildRecallPrompt(params: {
]
.filter((section) => section.length > 0)
.join("\n\n");
return [
instructionBlock,
`Bounded memory search query:\n${params.searchQuery}`,
`Conversation context:\n${params.query}`,
].join("\n\n");
return `${instructionBlock}\n\nConversation context:\n${params.query}`;
}
function isEnabledForAgent(
@@ -2097,83 +2048,6 @@ function buildQuery(params: {
].join("\n");
}
function stripExternalUntrustedBlocks(text: string): string {
return text.replace(
/<<<EXTERNAL_UNTRUSTED_CONTENT\b[^>]*>>>[\s\S]*?<<<END_EXTERNAL_UNTRUSTED_CONTENT\b[^>]*>>>/g,
" ",
);
}
function stripJsonFences(text: string): string {
return text.replace(/```(?:json)?\s*[\s\S]*?```/gi, " ");
}
function stripActiveMemoryXmlBlocks(text: string): string {
return text.replace(/<active_memory_plugin>[\s\S]*?<\/active_memory_plugin>/gi, " ");
}
function normalizeSearchQueryText(text: string): string {
return text
.split("\n")
.map((line) => line.trim())
.filter((line) => {
if (!line) {
return false;
}
if (/^(conversation info|sender|untrusted context)\b/i.test(line)) {
return false;
}
if (/^(source: external|---|untrusted discord message body)$/i.test(line)) {
return false;
}
if (/^⚠️?\s*Agent couldn't generate a response/i.test(line)) {
return false;
}
if (/^Please try again\.?$/i.test(line)) {
return false;
}
return true;
})
.join(" ")
.replace(/\s+/g, " ")
.trim();
}
function clampSearchQuery(text: string): string {
const normalized = text.replace(/\s+/g, " ").trim();
return normalized.length > MAX_ACTIVE_MEMORY_SEARCH_QUERY_CHARS
? normalized.slice(0, MAX_ACTIVE_MEMORY_SEARCH_QUERY_CHARS).trim()
: normalized;
}
function buildSearchQuery(params: {
latestUserMessage: string;
recentTurns?: ActiveRecallRecentTurn[];
}): string {
const latest = clampSearchQuery(
normalizeSearchQueryText(
stripActiveMemoryXmlBlocks(
stripJsonFences(stripExternalUntrustedBlocks(params.latestUserMessage)),
),
),
);
if (latest.length >= 12 || !params.recentTurns?.length) {
return latest || clampSearchQuery(params.latestUserMessage);
}
const previousUser = [...params.recentTurns]
.toReversed()
.find((turn) => turn.role === "user" && turn.text.trim() !== params.latestUserMessage.trim());
if (!previousUser) {
return latest || clampSearchQuery(params.latestUserMessage);
}
const context = clampSearchQuery(
normalizeSearchQueryText(stripRecalledContextNoise(previousUser.text)),
)
.slice(0, 120)
.trim();
return clampSearchQuery(context ? `${context} ${latest}` : latest);
}
function extractTextContent(content: unknown): string {
if (typeof content === "string") {
return content;
@@ -2342,7 +2216,6 @@ async function runRecallSubagent(params: {
messageProvider?: string;
channelId?: string;
query: string;
searchQuery: string;
currentModelProviderId?: string;
currentModelId?: string;
modelRef?: { provider: string; model: string };
@@ -2397,7 +2270,6 @@ async function runRecallSubagent(params: {
const prompt = buildRecallPrompt({
config: params.config,
query: params.query,
searchQuery: params.searchQuery,
});
const { messageChannel, messageProvider } = resolveRecallRunChannelContext({
api: params.api,
@@ -2427,7 +2299,7 @@ async function runRecallSubagent(params: {
timeoutMs: embeddedTimeoutMs,
runId: subagentSessionId,
trigger: "manual",
toolsAllow: [...ACTIVE_MEMORY_TOOL_ALLOWLIST],
toolsAllow: ["memory_recall", "memory_search", "memory_get"],
disableMessageTool: true,
allowGatewaySubagentBinding: true,
bootstrapContextMode: "lightweight",
@@ -2470,12 +2342,6 @@ async function runRecallSubagent(params: {
const searchDebug = partialReply ? await readActiveMemorySearchDebug(sessionFile) : undefined;
attachPartialTimeoutData(error, partialReply, searchDebug);
}
if (!params.abortSignal?.aborted && isMissingRegisteredMemoryToolsError(error)) {
params.api.logger.debug?.(
`active-memory: no memory tools registered (memory-core or memory-lancedb required); skipping sub-agent`,
);
return { rawReply: "NONE" };
}
throw error;
} finally {
if (tempDir) {
@@ -2493,7 +2359,6 @@ async function maybeResolveActiveRecall(params: {
messageProvider?: string;
channelId?: string;
query: string;
searchQuery: string;
currentModelProviderId?: string;
currentModelId?: string;
}): Promise<ActiveRecallResult> {
@@ -2571,9 +2436,7 @@ async function maybeResolveActiveRecall(params: {
if (params.config.logging) {
params.api.logger.info?.(
`${logPrefix} start timeoutMs=${String(params.config.timeoutMs)} queryChars=${String(
params.query.length,
)} searchQueryChars=${String(params.searchQuery.length)}`,
`${logPrefix} start timeoutMs=${String(params.config.timeoutMs)} queryChars=${String(params.query.length)}`,
);
}
@@ -2942,16 +2805,11 @@ export default definePluginEntry({
});
return undefined;
}
const recentTurns = extractRecentTurns(event.messages);
const query = buildQuery({
latestUserMessage: event.prompt,
recentTurns,
recentTurns: extractRecentTurns(event.messages),
config,
});
const searchQuery = buildSearchQuery({
latestUserMessage: event.prompt,
recentTurns,
});
const result = await maybeResolveActiveRecall({
api,
config,
@@ -2961,7 +2819,6 @@ export default definePluginEntry({
messageProvider: ctx.messageProvider,
channelId: ctx.channelId,
query,
searchQuery,
currentModelProviderId: ctx.modelProviderId,
currentModelId: ctx.modelId,
});
@@ -2998,7 +2855,6 @@ const testing = {
buildPromptPrefix,
getCachedResult,
isCircuitBreakerOpen,
isMissingRegisteredMemoryToolsError,
normalizePluginConfig,
readActiveMemorySearchDebug,
readPartialAssistantText,

View File

@@ -5,9 +5,9 @@
"description": "OpenClaw Amazon Bedrock Mantle (OpenAI-compatible) provider plugin",
"type": "module",
"dependencies": {
"@anthropic-ai/sdk": "0.93.0",
"@anthropic-ai/sdk": "0.92.0",
"@aws/bedrock-token-generator": "^1.1.0",
"@mariozechner/pi-ai": "0.73.0"
"@mariozechner/pi-ai": "0.71.1"
},
"devDependencies": {
"@openclaw/plugin-sdk": "workspace:*"

View File

@@ -5,8 +5,8 @@
"description": "OpenClaw Amazon Bedrock provider plugin",
"type": "module",
"dependencies": {
"@aws-sdk/client-bedrock": "3.1042.0",
"@aws-sdk/client-bedrock-runtime": "3.1042.0",
"@aws-sdk/client-bedrock": "3.1041.0",
"@aws-sdk/client-bedrock-runtime": "3.1041.0",
"@aws-sdk/credential-provider-node": "3.972.39"
},
"devDependencies": {

View File

@@ -6,8 +6,8 @@
"type": "module",
"dependencies": {
"@anthropic-ai/vertex-sdk": "^0.16.0",
"@mariozechner/pi-agent-core": "0.73.0",
"@mariozechner/pi-ai": "0.73.0"
"@mariozechner/pi-agent-core": "0.71.1",
"@mariozechner/pi-ai": "0.71.1"
},
"devDependencies": {
"@openclaw/plugin-sdk": "workspace:*"

View File

@@ -10,7 +10,6 @@ import {
CLAUDE_CLI_MODEL_ALIASES,
CLAUDE_CLI_SESSION_ID_FIELDS,
normalizeClaudeBackendConfig,
resolveClaudeCliExecutionArgs,
} from "./cli-shared.js";
export function buildAnthropicCliBackend(): CliBackendPlugin {
@@ -77,6 +76,5 @@ export function buildAnthropicCliBackend(): CliBackendPlugin {
serialize: true,
},
normalizeConfig: normalizeClaudeBackendConfig,
resolveExecutionArgs: resolveClaudeCliExecutionArgs,
};
}

View File

@@ -6,7 +6,6 @@ import {
normalizeClaudePermissionArgs,
normalizeClaudeSettingSourcesArgs,
resolveClaudePermissionMode,
resolveClaudeCliExecutionArgs,
} from "./cli-shared.js";
describe("normalizeClaudePermissionArgs", () => {
@@ -76,67 +75,6 @@ describe("normalizeClaudeSettingSourcesArgs", () => {
});
});
describe("resolveClaudeCliExecutionArgs", () => {
it("omits effort args when thinking is off", () => {
expect(
resolveClaudeCliExecutionArgs({
workspaceDir: "/tmp",
provider: "claude-cli",
modelId: "claude-sonnet-4-6",
thinkingLevel: "off",
useResume: false,
baseArgs: ["-p", "--output-format", "stream-json"],
}),
).toEqual(["-p", "--output-format", "stream-json"]);
});
it("maps OpenClaw thinking levels to Claude effort args", () => {
expect(
resolveClaudeCliExecutionArgs({
workspaceDir: "/tmp",
provider: "claude-cli",
modelId: "claude-opus-4-7",
thinkingLevel: "minimal",
useResume: false,
baseArgs: ["-p"],
}),
).toEqual(["-p", "--effort", "low"]);
expect(
resolveClaudeCliExecutionArgs({
workspaceDir: "/tmp",
provider: "claude-cli",
modelId: "claude-opus-4-7",
thinkingLevel: "adaptive",
useResume: false,
baseArgs: ["-p"],
}),
).toEqual(["-p", "--effort", "medium"]);
expect(
resolveClaudeCliExecutionArgs({
workspaceDir: "/tmp",
provider: "claude-cli",
modelId: "claude-opus-4-7",
thinkingLevel: "xhigh",
useResume: true,
baseArgs: ["-p", "--resume", "{sessionId}"],
}),
).toEqual(["-p", "--resume", "{sessionId}", "--effort", "xhigh"]);
});
it("replaces static effort args when a session thinking level is active", () => {
expect(
resolveClaudeCliExecutionArgs({
workspaceDir: "/tmp",
provider: "claude-cli",
modelId: "claude-opus-4-7",
thinkingLevel: "max",
useResume: false,
baseArgs: ["-p", "--effort", "low", "--effort=high"],
}),
).toEqual(["-p", "--effort", "max"]);
});
});
describe("normalizeClaudeBackendConfig", () => {
it("normalizes both args and resumeArgs for custom overrides", () => {
const normalized = normalizeClaudeBackendConfig({
@@ -258,7 +196,6 @@ describe("normalizeClaudeBackendConfig", () => {
expect(normalized?.resumeArgs).toContain("--permission-mode");
expect(normalized?.resumeArgs).toContain("bypassPermissions");
expect(normalized?.liveSession).toBe("claude-stdio");
expect(backend.resolveExecutionArgs).toBe(resolveClaudeCliExecutionArgs);
});
it("leaves claude cli subscription-managed, restricts setting sources, and clears inherited env overrides", () => {

View File

@@ -1,7 +1,6 @@
import type {
CliBackendConfig,
CliBackendNormalizeConfigContext,
CliBackendResolveExecutionArgsContext,
} from "openclaw/plugin-sdk/cli-backend";
import { normalizeOptionalLowercaseString } from "openclaw/plugin-sdk/text-runtime";
import { CLAUDE_CLI_BACKEND_ID } from "./cli-constants.js";
@@ -61,12 +60,9 @@ export const CLAUDE_CLI_CLEAR_ENV = [
const CLAUDE_LEGACY_SKIP_PERMISSIONS_ARG = "--dangerously-skip-permissions";
const CLAUDE_PERMISSION_MODE_ARG = "--permission-mode";
const CLAUDE_SETTING_SOURCES_ARG = "--setting-sources";
const CLAUDE_EFFORT_ARG = "--effort";
const CLAUDE_SAFE_SETTING_SOURCES = "user";
const CLAUDE_BYPASS_PERMISSION_MODE = "bypassPermissions";
type ClaudeCliEffort = "low" | "medium" | "high" | "xhigh" | "max";
export function isClaudeCliProvider(providerId: string): boolean {
return normalizeOptionalLowercaseString(providerId) === CLAUDE_CLI_BACKEND_ID;
}
@@ -172,60 +168,6 @@ export function normalizeClaudeSettingSourcesArgs(args?: string[]): string[] | u
return normalized;
}
export function mapClaudeCliThinkingLevelToEffort(
thinkingLevel?: string | null,
): ClaudeCliEffort | undefined {
switch (normalizeOptionalLowercaseString(thinkingLevel)) {
case "minimal":
case "low":
return "low";
case "adaptive":
case "medium":
return "medium";
case "high":
return "high";
case "xhigh":
return "xhigh";
case "max":
return "max";
default:
return undefined;
}
}
function stripClaudeEffortArgs(args: readonly string[]): string[] {
const normalized: string[] = [];
for (let i = 0; i < args.length; i += 1) {
const arg = args[i] ?? "";
if (arg === CLAUDE_EFFORT_ARG) {
const maybeValue = args[i + 1];
if (
typeof maybeValue === "string" &&
maybeValue.trim().length > 0 &&
!maybeValue.startsWith("-")
) {
i += 1;
}
continue;
}
if (arg.startsWith(`${CLAUDE_EFFORT_ARG}=`)) {
continue;
}
normalized.push(arg);
}
return normalized;
}
export function resolveClaudeCliExecutionArgs(
context: CliBackendResolveExecutionArgsContext,
): string[] {
const effort = mapClaudeCliThinkingLevelToEffort(context.thinkingLevel);
if (!effort) {
return [...context.baseArgs];
}
return [...stripClaudeEffortArgs(context.baseArgs), CLAUDE_EFFORT_ARG, effort];
}
export function normalizeClaudeBackendConfig(
config: CliBackendConfig,
context?: CliBackendNormalizeConfigContext,

View File

@@ -5,7 +5,7 @@
"description": "OpenClaw Anthropic provider plugin",
"type": "module",
"dependencies": {
"@mariozechner/pi-ai": "0.73.0"
"@mariozechner/pi-ai": "0.71.1"
},
"devDependencies": {
"@openclaw/plugin-sdk": "workspace:*"

View File

@@ -4,7 +4,7 @@
"description": "OpenClaw Bonjour/mDNS gateway discovery",
"type": "module",
"dependencies": {
"@homebridge/ciao": "^1.3.8"
"@homebridge/ciao": "^1.3.7"
},
"devDependencies": {
"@openclaw/plugin-sdk": "workspace:*"

View File

@@ -14,7 +14,7 @@
},
"devDependencies": {
"@openclaw/plugin-sdk": "workspace:*",
"undici": "8.2.0"
"undici": "8.1.0"
},
"openclaw": {
"extensions": [

View File

@@ -695,7 +695,6 @@ export function registerBrowserAgentActRoutes(
res,
ctx,
targetId,
enforceCurrentUrlAllowed: true,
run: async ({ profileCtx, cdpUrl, tab, resolveTabUrl }) => {
if (getBrowserProfileCapabilities(profileCtx.profile).usesChromeMcp) {
return jsonError(res, 501, EXISTING_SESSION_LIMITS.responseBody);

View File

@@ -29,7 +29,6 @@ export function registerBrowserAgentDebugRoutes(
ctx,
targetId,
feature: "console messages",
enforceCurrentUrlAllowed: true,
run: async ({ cdpUrl, tab, pw, resolveTabUrl }) => {
const messages = await pw.getConsoleMessagesViaPlaywright({
cdpUrl,
@@ -55,7 +54,6 @@ export function registerBrowserAgentDebugRoutes(
ctx,
targetId,
feature: "page errors",
enforceCurrentUrlAllowed: true,
run: async ({ cdpUrl, tab, pw, resolveTabUrl }) => {
const result = await pw.getPageErrorsViaPlaywright({
cdpUrl,
@@ -82,7 +80,6 @@ export function registerBrowserAgentDebugRoutes(
ctx,
targetId,
feature: "network requests",
enforceCurrentUrlAllowed: true,
run: async ({ cdpUrl, tab, pw, resolveTabUrl }) => {
const result = await pw.getNetworkRequestsViaPlaywright({
cdpUrl,
@@ -112,7 +109,6 @@ export function registerBrowserAgentDebugRoutes(
ctx,
targetId,
feature: "trace start",
enforceCurrentUrlAllowed: true,
run: async ({ cdpUrl, tab, pw, resolveTabUrl }) => {
await pw.traceStartViaPlaywright({
cdpUrl,
@@ -141,7 +137,6 @@ export function registerBrowserAgentDebugRoutes(
ctx,
targetId,
feature: "trace stop",
enforceCurrentUrlAllowed: true,
run: async ({ cdpUrl, tab, pw, resolveTabUrl }) => {
const id = crypto.randomUUID();
const tracePath = await resolveWritableOutputPathOrRespond({

View File

@@ -1,13 +1,10 @@
import { describe, expect, it, vi } from "vitest";
import type { BrowserRouteContext, ProfileContext } from "../server-context.js";
import { describe, expect, it } from "vitest";
import {
readBody,
resolveSafeRouteTabUrl,
resolveTargetIdFromBody,
resolveTargetIdFromQuery,
withRouteTabContext,
} from "./agent.shared.js";
import { createBrowserRouteResponse } from "./test-helpers.js";
import type { BrowserRequest } from "./types.js";
function requestWithBody(body: unknown): BrowserRequest {
@@ -39,31 +36,6 @@ function profileContext(tabs: Array<{ targetId: string; url: string }>) {
};
}
function routeContextForTab(url: string): BrowserRouteContext {
const profileCtx = {
profile: {
cdpUrl: "http://127.0.0.1:9222",
name: "default",
},
ensureTabAvailable: vi.fn(async () => ({
targetId: "tab-1",
title: "Tab",
url,
type: "page",
})),
} as unknown as ProfileContext;
return {
forProfile: () => profileCtx,
state: () => ({
resolved: {
ssrfPolicy: {},
},
}),
mapTabError: () => null,
} as unknown as BrowserRouteContext;
}
describe("browser route shared helpers", () => {
describe("readBody", () => {
it("returns object bodies", () => {
@@ -128,44 +100,4 @@ describe("browser route shared helpers", () => {
).resolves.toBeUndefined();
});
});
describe("withRouteTabContext", () => {
it("does not enforce current-tab URL policy unless requested", async () => {
const response = createBrowserRouteResponse();
const run = vi.fn(async () => {
response.res.json({ ok: true });
});
await withRouteTabContext({
req: requestWithBody({}),
res: response.res,
ctx: routeContextForTab("http://127.0.0.1:8080/admin"),
run,
});
expect(run).toHaveBeenCalledOnce();
expect(response.body).toEqual({ ok: true });
});
it("blocks guarded routes before running on a disallowed current tab", async () => {
const response = createBrowserRouteResponse();
const run = vi.fn(async () => {
response.res.json({ ok: true });
});
await withRouteTabContext({
req: requestWithBody({}),
res: response.res,
ctx: routeContextForTab("http://127.0.0.1:8080/admin"),
enforceCurrentUrlAllowed: true,
run,
});
expect(run).not.toHaveBeenCalled();
expect(response.statusCode).toBe(400);
expect(response.body).toMatchObject({ error: expect.any(String) });
const body = response.body as { error?: unknown };
expect(body.error).not.toBe("");
});
});
});

View File

@@ -107,11 +107,6 @@ type RouteWithTabParams<T> = {
res: BrowserResponse;
ctx: BrowserRouteContext;
targetId?: string;
/**
* Set for routes that read from or return data scoped to the selected tab.
* Leave false only for routes that navigate, activate, close, or otherwise manage the tab.
*/
enforceCurrentUrlAllowed?: boolean;
run: (ctx: RouteTabContext) => Promise<T>;
};
@@ -124,17 +119,6 @@ export async function withRouteTabContext<T>(
}
try {
const tab = await profileCtx.ensureTabAvailable(params.targetId);
if (params.enforceCurrentUrlAllowed) {
await assertBrowserNavigationResultAllowed({
url: tab.url,
...withBrowserNavigationPolicy(params.ctx.state().resolved.ssrfPolicy, {
browserProxyMode: resolveBrowserNavigationProxyMode({
resolved: params.ctx.state().resolved,
profile: profileCtx.profile,
}),
}),
});
}
return await params.run({
profileCtx,
tab,
@@ -153,10 +137,6 @@ export async function withRouteTabContext<T>(
}
}
/**
* Response-only URL redaction. This swallows policy failures and must not be used as
* an execution gate; use enforceCurrentUrlAllowed on the route helper instead.
*/
export async function resolveSafeRouteTabUrl(params: {
ctx: BrowserRouteContext;
profileCtx: ProfileContext;
@@ -191,11 +171,6 @@ type RouteWithPwParams<T> = {
ctx: BrowserRouteContext;
targetId?: string;
feature: string;
/**
* Set for routes that read from or return data scoped to the selected tab.
* Leave false only for routes that navigate, activate, close, or otherwise manage the tab.
*/
enforceCurrentUrlAllowed?: boolean;
run: (ctx: RouteTabPwContext) => Promise<T>;
};
@@ -207,7 +182,6 @@ export async function withPlaywrightRouteContext<T>(
res: params.res,
ctx: params.ctx,
targetId: params.targetId,
enforceCurrentUrlAllowed: params.enforceCurrentUrlAllowed,
run: async ({ profileCtx, tab, cdpUrl, resolveTabUrl }) => {
const pw = await requirePwAi(params.res, params.feature);
if (!pw) {

View File

@@ -318,7 +318,6 @@ export function registerBrowserAgentSnapshotRoutes(
ctx,
targetId,
feature: "pdf",
enforceCurrentUrlAllowed: true,
run: async ({ cdpUrl, tab, pw }) => {
const pdf = await pw.pdfViaPlaywright({
cdpUrl,
@@ -362,19 +361,18 @@ export function registerBrowserAgentSnapshotRoutes(
res,
ctx,
targetId,
enforceCurrentUrlAllowed: true,
run: async ({ profileCtx, tab, cdpUrl }) => {
if (getBrowserProfileCapabilities(profileCtx.profile).usesChromeMcp) {
const ssrfPolicyOpts = browserNavigationPolicyForProfile(ctx, profileCtx);
if (element) {
return jsonError(res, 400, EXISTING_SESSION_LIMITS.snapshot.screenshotElement);
}
if (ssrfPolicyOpts.ssrfPolicy) {
await assertBrowserNavigationResultAllowed({
url: tab.url,
...ssrfPolicyOpts,
});
}
if (element) {
return jsonError(res, 400, EXISTING_SESSION_LIMITS.snapshot.screenshotElement);
}
if (labels) {
const snapshot = await takeChromeMcpSnapshot({
profileName: profileCtx.profile.name,

View File

@@ -85,7 +85,6 @@ export function registerBrowserAgentStorageRoutes(
ctx,
targetId,
feature: "cookies",
enforceCurrentUrlAllowed: true,
run: async ({ cdpUrl, tab, pw }) => {
const result = await pw.cookiesGetViaPlaywright({
cdpUrl,
@@ -110,7 +109,6 @@ export function registerBrowserAgentStorageRoutes(
return jsonError(res, 400, "cookie is required");
}
// Intentional: mutation routes are outside the tab-scoped read/export guard scope.
await withPlaywrightRouteContext({
req,
res,
@@ -150,7 +148,6 @@ export function registerBrowserAgentStorageRoutes(
const body = readBody(req);
const targetId = resolveTargetIdFromBody(body);
// Intentional: mutation routes are outside the tab-scoped read/export guard scope.
await withPlaywrightRouteContext({
req,
res,
@@ -184,7 +181,6 @@ export function registerBrowserAgentStorageRoutes(
ctx,
targetId,
feature: "storage get",
enforceCurrentUrlAllowed: true,
run: async ({ cdpUrl, tab, pw }) => {
const result = await pw.storageGetViaPlaywright({
cdpUrl,
@@ -211,7 +207,6 @@ export function registerBrowserAgentStorageRoutes(
}
const value = typeof mutation.body.value === "string" ? mutation.body.value : "";
// Intentional: mutation routes are outside the tab-scoped read/export guard scope.
await withPlaywrightRouteContext({
req,
res,
@@ -240,7 +235,6 @@ export function registerBrowserAgentStorageRoutes(
return;
}
// Intentional: mutation routes are outside the tab-scoped read/export guard scope.
await withPlaywrightRouteContext({
req,
res,
@@ -269,7 +263,6 @@ export function registerBrowserAgentStorageRoutes(
return jsonError(res, 400, "offline is required");
}
// Intentional: mutation routes are outside the tab-scoped read/export guard scope.
await withPlaywrightRouteContext({
req,
res,
@@ -308,7 +301,6 @@ export function registerBrowserAgentStorageRoutes(
}
}
// Intentional: mutation routes are outside the tab-scoped read/export guard scope.
await withPlaywrightRouteContext({
req,
res,

View File

@@ -1,9 +1,4 @@
import { vi } from "vitest";
import {
assertBrowserNavigationResultAllowed,
withBrowserNavigationPolicy,
} from "../navigation-guard.js";
import type { BrowserRouteContext } from "../server-context.js";
import type { BrowserRequest } from "./types.js";
export const existingSessionRouteState = {
@@ -42,33 +37,14 @@ export function createExistingSessionAgentSharedModule() {
typeof body.targetId === "string" ? body.targetId : undefined,
),
withPlaywrightRouteContext: vi.fn(),
withRouteTabContext: vi.fn(
async ({
ctx,
enforceCurrentUrlAllowed,
run,
}: {
ctx: BrowserRouteContext;
enforceCurrentUrlAllowed?: boolean;
run: (args: unknown) => Promise<void>;
}) => {
if (enforceCurrentUrlAllowed) {
const ssrfPolicyOpts = withBrowserNavigationPolicy(ctx.state().resolved.ssrfPolicy);
if (ssrfPolicyOpts.ssrfPolicy) {
await assertBrowserNavigationResultAllowed({
url: existingSessionRouteState.tab.url,
...ssrfPolicyOpts,
});
}
}
await run({
profileCtx: existingSessionRouteState.profileCtx,
cdpUrl: "http://127.0.0.1:18800",
tab: existingSessionRouteState.tab,
resolveTabUrl: vi.fn(async (fallbackUrl?: string) => fallbackUrl ?? routeStateUrl()),
});
},
),
withRouteTabContext: vi.fn(async ({ run }: { run: (args: unknown) => Promise<void> }) => {
await run({
profileCtx: existingSessionRouteState.profileCtx,
cdpUrl: "http://127.0.0.1:18800",
tab: existingSessionRouteState.tab,
resolveTabUrl: vi.fn(async (fallbackUrl?: string) => fallbackUrl ?? routeStateUrl()),
});
}),
};
}

View File

@@ -11,8 +11,6 @@ import {
import {
getBrowserControlServerTestState,
getPwMocks,
setBrowserControlServerSsrFPolicy,
setBrowserControlServerTabUrl,
} from "./server.control-server.test-harness.js";
import { getBrowserTestFetch, type BrowserTestFetch } from "./test-support/fetch.js";
@@ -20,81 +18,6 @@ const state = getBrowserControlServerTestState();
const pwMocks = getPwMocks();
const realFetch: BrowserTestFetch = (input, init) => getBrowserTestFetch()(input, init);
type GuardedCurrentTabRouteCase = {
method: "GET" | "POST";
path: string;
body?: Record<string, unknown>;
mockName:
| "cookiesGetViaPlaywright"
| "pdfViaPlaywright"
| "getConsoleMessagesViaPlaywright"
| "getPageErrorsViaPlaywright"
| "getNetworkRequestsViaPlaywright"
| "responseBodyViaPlaywright"
| "storageGetViaPlaywright"
| "takeScreenshotViaPlaywright"
| "traceStartViaPlaywright"
| "traceStopViaPlaywright";
};
const guardedCurrentTabRouteCases: readonly GuardedCurrentTabRouteCase[] = [
{
method: "GET",
path: "/console?targetId=abcd1234",
mockName: "getConsoleMessagesViaPlaywright",
},
{
method: "GET",
path: "/errors?targetId=abcd1234",
mockName: "getPageErrorsViaPlaywright",
},
{
method: "GET",
path: "/requests?targetId=abcd1234",
mockName: "getNetworkRequestsViaPlaywright",
},
{
method: "POST",
path: "/pdf",
body: { targetId: "abcd1234" },
mockName: "pdfViaPlaywright",
},
{
method: "POST",
path: "/screenshot",
body: { targetId: "abcd1234" },
mockName: "takeScreenshotViaPlaywright",
},
{
method: "POST",
path: "/response/body",
body: { targetId: "abcd1234", url: "**/api/data" },
mockName: "responseBodyViaPlaywright",
},
{
method: "GET",
path: "/cookies?targetId=abcd1234",
mockName: "cookiesGetViaPlaywright",
},
{
method: "GET",
path: "/storage/local?targetId=abcd1234",
mockName: "storageGetViaPlaywright",
},
{
method: "POST",
path: "/trace/start",
body: { targetId: "abcd1234" },
mockName: "traceStartViaPlaywright",
},
{
method: "POST",
path: "/trace/stop",
body: { targetId: "abcd1234" },
mockName: "traceStopViaPlaywright",
},
] as const;
async function withSymlinkPathEscape<T>(params: {
rootDir: string;
run: (relativePath: string) => Promise<T>;
@@ -516,25 +439,6 @@ describe("browser control server", () => {
);
});
it.each(guardedCurrentTabRouteCases)(
"blocks $method $path on disallowed current tab URLs",
async (routeCase) => {
setBrowserControlServerSsrFPolicy({ allowPrivateNetwork: false });
setBrowserControlServerTabUrl("http://127.0.0.1:8080/admin");
const base = await startServerAndBase();
const res = await realFetch(`${base}${routeCase.path}`, {
method: routeCase.method,
headers: routeCase.body ? { "Content-Type": "application/json" } : undefined,
body: routeCase.body ? JSON.stringify(routeCase.body) : undefined,
});
expect(res.status).toBe(400);
const body = (await res.json()) as { error?: unknown };
expect(body.error).toEqual(expect.stringMatching(/(blocked|denied|not allowed|policy)/i));
expect(pwMocks[routeCase.mockName]).not.toHaveBeenCalled();
},
);
it("wait/download rejects traversal path outside downloads dir", async () => {
const base = await startServerAndBase();
const waitRes = await postJson<{ error?: string }>(`${base}/wait/download`, {

View File

@@ -1,6 +1,5 @@
import { afterEach, beforeEach, vi } from "vitest";
import { deriveDefaultBrowserCdpPortRange } from "../config/port-defaults.js";
import type { SsrFPolicy } from "../infra/net/ssrf.js";
import type { MockFn } from "../test-utils/vitest-mock-fn.js";
import { installChromeUserDataDirHooks } from "./chrome-user-data-dir.test-harness.js";
import { getFreePort } from "./test-port.js";
@@ -11,7 +10,6 @@ type HarnessState = {
reachable: boolean;
cfgAttachOnly: boolean;
cfgEvaluateEnabled: boolean;
cfgSsrfPolicy: SsrFPolicy | undefined;
cfgDefaultProfile: string;
cfgProfiles: Record<
string,
@@ -23,7 +21,6 @@ type HarnessState = {
attachOnly?: boolean;
}
>;
tabUrl: string;
prevGatewayPort: string | undefined;
prevGatewayToken: string | undefined;
prevGatewayPassword: string | undefined;
@@ -35,10 +32,8 @@ const state: HarnessState = {
reachable: false,
cfgAttachOnly: false,
cfgEvaluateEnabled: true,
cfgSsrfPolicy: undefined,
cfgDefaultProfile: "openclaw",
cfgProfiles: {},
tabUrl: "https://example.com",
prevGatewayPort: undefined,
prevGatewayToken: undefined,
prevGatewayPassword: undefined,
@@ -64,18 +59,10 @@ export function setBrowserControlServerEvaluateEnabled(enabled: boolean): void {
state.cfgEvaluateEnabled = enabled;
}
export function setBrowserControlServerSsrFPolicy(policy: SsrFPolicy | undefined): void {
state.cfgSsrfPolicy = policy;
}
export function setBrowserControlServerReachable(reachable: boolean): void {
state.reachable = reachable;
}
export function setBrowserControlServerTabUrl(url: string): void {
state.tabUrl = url;
}
export function setBrowserControlServerProfiles(
profiles: HarnessState["cfgProfiles"],
defaultProfile = Object.keys(profiles)[0] ?? "openclaw",
@@ -165,7 +152,6 @@ const pwMocks = vi.hoisted(() => ({
clickViaPlaywright: vi.fn(async (_opts?: unknown) => {}),
closePageViaPlaywright: vi.fn(async (_opts?: unknown) => {}),
closePlaywrightBrowserConnection: vi.fn(async () => {}),
cookiesGetViaPlaywright: vi.fn(async () => ({ cookies: [] })),
downloadViaPlaywright: vi.fn(async () => ({
url: "https://example.com/report.pdf",
suggestedFilename: "report.pdf",
@@ -175,8 +161,6 @@ const pwMocks = vi.hoisted(() => ({
evaluateViaPlaywright: vi.fn(async (_opts?: unknown) => "ok"),
fillFormViaPlaywright: vi.fn(async (_opts?: unknown) => {}),
getConsoleMessagesViaPlaywright: vi.fn(async () => []),
getNetworkRequestsViaPlaywright: vi.fn(async () => ({ requests: [] })),
getPageErrorsViaPlaywright: vi.fn(async () => ({ errors: [] })),
hoverViaPlaywright: vi.fn(async (_opts?: unknown) => {}),
scrollIntoViewViaPlaywright: vi.fn(async (_opts?: unknown) => {}),
navigateViaPlaywright: vi.fn(async () => ({ url: "https://example.com" })),
@@ -197,9 +181,7 @@ const pwMocks = vi.hoisted(() => ({
refs: { e1: { role: "button", name: "Role" } },
stats: { lines: 1, chars: 24, refs: 1, interactive: 1 },
})),
storageGetViaPlaywright: vi.fn(async () => ({ values: {} })),
storeAriaSnapshotRefsViaPlaywright: vi.fn(async () => {}),
traceStartViaPlaywright: vi.fn(async () => {}),
traceStopViaPlaywright: vi.fn(async () => {}),
takeScreenshotViaPlaywright: vi.fn(async () => ({
buffer: Buffer.from("png"),
@@ -411,13 +393,13 @@ vi.mock("../config/config.js", async () => {
evaluateEnabled: state.cfgEvaluateEnabled,
color: "#FF4500",
attachOnly: state.cfgAttachOnly,
ssrfPolicy: state.cfgSsrfPolicy ?? { dangerouslyAllowPrivateNetwork: true },
headless: true,
defaultProfile: state.cfgDefaultProfile,
profiles:
Object.keys(state.cfgProfiles).length > 0
? state.cfgProfiles
: defaultProfilesForState(state.testPort),
ssrfPolicy: { dangerouslyAllowPrivateNetwork: true },
},
};
};
@@ -531,10 +513,8 @@ export async function resetBrowserControlServerTestContext(): Promise<void> {
state.reachable = false;
state.cfgAttachOnly = false;
state.cfgEvaluateEnabled = true;
state.cfgSsrfPolicy = undefined;
state.cfgDefaultProfile = "openclaw";
state.cfgProfiles = defaultProfilesForState(state.testPort);
state.tabUrl = "https://example.com";
mockClearAll(pwMocks);
mockClearAll(cdpMocks);
@@ -600,7 +580,7 @@ export function installBrowserControlServerHooks() {
{
id: "abcd1234",
title: "Tab",
url: state.tabUrl,
url: "https://example.com",
webSocketDebuggerUrl: "ws://127.0.0.1/devtools/page/abcd1234",
type: "page",
},

View File

@@ -8,11 +8,11 @@
},
"type": "module",
"dependencies": {
"@mariozechner/pi-coding-agent": "0.73.0",
"@mariozechner/pi-coding-agent": "0.71.1",
"@openai/codex": "0.128.0",
"ajv": "^8.20.0",
"ws": "^8.20.0",
"zod": "^4.4.3"
"zod": "^4.4.1"
},
"devDependencies": {
"@openclaw/plugin-sdk": "workspace:*"

View File

@@ -146,14 +146,6 @@ function assistantMessage(text: string, timestamp: number) {
};
}
function userMessage(text: string, timestamp: number) {
return {
role: "user" as const,
content: [{ type: "text" as const, text }],
timestamp,
};
}
function createAppServerHarness(
requestImpl: (method: string, params: unknown) => Promise<unknown>,
options: {
@@ -760,34 +752,6 @@ describe("runCodexAppServerAttempt", () => {
);
});
it("projects mirrored history when starting Codex without a native thread binding", async () => {
const sessionFile = path.join(tempDir, "session.jsonl");
const workspaceDir = path.join(tempDir, "workspace");
const sessionManager = SessionManager.open(sessionFile);
sessionManager.appendMessage(userMessage("we are fixing the Opik default project", Date.now()));
sessionManager.appendMessage(assistantMessage("Opik default project context", Date.now() + 1));
const harness = createStartedThreadHarness();
const params = createParams(sessionFile, workspaceDir);
params.prompt = "make the default webpage openclaw";
const run = runCodexAppServerAttempt(params);
await harness.waitForMethod("turn/start");
await new Promise<void>((resolve) => setImmediate(resolve));
await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" });
await run;
const turnStart = harness.requests.find((request) => request.method === "turn/start");
const inputText =
(turnStart?.params as { input?: Array<{ text?: string }> } | undefined)?.input?.[0]?.text ??
"";
expect(inputText).toContain("OpenClaw assembled context for this turn:");
expect(inputText).toContain("we are fixing the Opik default project");
expect(inputText).toContain("Opik default project context");
expect(inputText).toContain("Current user request:");
expect(inputText).toContain("make the default webpage openclaw");
});
it("passes OpenClaw bootstrap files through Codex config instructions", async () => {
const sessionFile = path.join(tempDir, "session.jsonl");
const workspaceDir = path.join(tempDir, "workspace");
@@ -2084,90 +2048,6 @@ describe("runCodexAppServerAttempt", () => {
expect(request.mock.calls.map(([method]) => method)).toEqual(["thread/start", "thread/resume"]);
});
it("resumes a bound Codex thread when dynamic tools are reordered", async () => {
const sessionFile = path.join(tempDir, "session.jsonl");
const workspaceDir = path.join(tempDir, "workspace");
const params = createParams(sessionFile, workspaceDir);
const appServer = createThreadLifecycleAppServerOptions();
const request = vi.fn(async (method: string) => {
if (method === "thread/start") {
return threadStartResult("thread-existing");
}
if (method === "thread/resume") {
return threadStartResult("thread-existing");
}
throw new Error(`unexpected method: ${method}`);
});
await startOrResumeThread({
client: { request } as never,
params,
cwd: workspaceDir,
dynamicTools: [createNamedDynamicTool("wiki_status"), createNamedDynamicTool("diffs")],
appServer,
});
const binding = await startOrResumeThread({
client: { request } as never,
params,
cwd: workspaceDir,
dynamicTools: [createNamedDynamicTool("diffs"), createNamedDynamicTool("wiki_status")],
appServer,
});
expect(binding.threadId).toBe("thread-existing");
expect(request.mock.calls.map(([method]) => method)).toEqual(["thread/start", "thread/resume"]);
});
it("keeps the previous dynamic tool fingerprint for transient no-tool maintenance turns", async () => {
const sessionFile = path.join(tempDir, "session.jsonl");
const workspaceDir = path.join(tempDir, "workspace");
const params = createParams(sessionFile, workspaceDir);
const appServer = createThreadLifecycleAppServerOptions();
let nextThread = 1;
const request = vi.fn(async (method: string) => {
if (method === "thread/start") {
return threadStartResult(`thread-${nextThread++}`);
}
if (method === "thread/resume") {
return threadStartResult("thread-1");
}
throw new Error(`unexpected method: ${method}`);
});
await startOrResumeThread({
client: { request } as never,
params,
cwd: workspaceDir,
dynamicTools: [createMessageDynamicTool("Send and manage messages.")],
appServer,
});
const fingerprint = (await readCodexAppServerBinding(sessionFile))?.dynamicToolsFingerprint;
await startOrResumeThread({
client: { request } as never,
params,
cwd: workspaceDir,
dynamicTools: [],
appServer,
});
await startOrResumeThread({
client: { request } as never,
params,
cwd: workspaceDir,
dynamicTools: [createMessageDynamicTool("Send and manage messages.")],
appServer,
});
await expect(readCodexAppServerBinding(sessionFile)).resolves.toMatchObject({
dynamicToolsFingerprint: fingerprint,
threadId: "thread-1",
});
expect(request.mock.calls.map(([method]) => method)).toEqual([
"thread/start",
"thread/start",
"thread/resume",
]);
});
it("preserves the binding when the app-server closes during thread resume", async () => {
const sessionFile = path.join(tempDir, "session.jsonl");
const workspaceDir = path.join(tempDir, "workspace");

View File

@@ -88,10 +88,8 @@ import { readCodexAppServerBinding, type CodexAppServerThreadBinding } from "./s
import { readCodexMirroredSessionHistoryMessages } from "./session-history.js";
import { clearSharedCodexAppServerClientIfCurrent } from "./shared-client.js";
import {
areCodexDynamicToolFingerprintsCompatible,
buildDeveloperInstructions,
buildTurnStartParams,
codexDynamicToolsFingerprint,
startOrResumeThread,
} from "./thread-lifecycle.js";
import {
@@ -502,20 +500,6 @@ export async function runCodexAppServerAttempt(
error: formatErrorMessage(assembleErr),
});
}
} else if (
shouldProjectMirroredHistoryForCodexStart({
startupBinding,
dynamicToolsFingerprint: codexDynamicToolsFingerprint(toolBridge.specs),
historyMessages,
})
) {
const projection = projectContextEngineAssemblyForCodex({
assembledMessages: historyMessages,
originalHistoryMessages: historyMessages,
prompt: params.prompt,
});
promptText = projection.promptText;
prePromptMessageCount = projection.prePromptMessageCount;
}
const promptBuild = await resolveAgentHarnessBeforePromptBuildResult({
prompt: promptText,
@@ -1562,23 +1546,6 @@ async function buildDynamicTools(input: DynamicToolBuildParams) {
});
}
function shouldProjectMirroredHistoryForCodexStart(params: {
startupBinding: CodexAppServerThreadBinding | undefined;
dynamicToolsFingerprint: string;
historyMessages: AgentMessage[];
}): boolean {
if (!params.historyMessages.some((message) => message.role === "user")) {
return false;
}
if (!params.startupBinding?.threadId) {
return true;
}
return !areCodexDynamicToolFingerprintsCompatible({
previous: params.startupBinding.dynamicToolsFingerprint,
next: params.dynamicToolsFingerprint,
});
}
async function withCodexStartupTimeout<T>(params: {
timeoutMs: number;
timeoutFloorMs?: number;

View File

@@ -47,37 +47,20 @@ export async function startOrResumeThread(params: {
agentDir: params.params.agentDir,
config: params.params.config,
});
let preserveExistingBinding = false;
if (binding?.threadId) {
// `/codex resume <thread>` writes a binding before the next turn can know
// the dynamic tool catalog, so only invalidate fingerprints we actually have.
if (
binding.dynamicToolsFingerprint &&
!areDynamicToolFingerprintsCompatible(
binding.dynamicToolsFingerprint,
dynamicToolsFingerprint,
)
binding.dynamicToolsFingerprint !== dynamicToolsFingerprint
) {
preserveExistingBinding = shouldStartTransientNoToolThread({
previous: binding.dynamicToolsFingerprint,
next: dynamicToolsFingerprint,
});
if (preserveExistingBinding) {
embeddedAgentLog.debug(
"codex app-server dynamic tools unavailable for turn; starting transient thread",
{
threadId: binding.threadId,
},
);
} else {
embeddedAgentLog.debug(
"codex app-server dynamic tool catalog changed; starting a new thread",
{
threadId: binding.threadId,
},
);
await clearCodexAppServerBinding(params.params.sessionFile);
}
embeddedAgentLog.debug(
"codex app-server dynamic tool catalog changed; starting a new thread",
{
threadId: binding.threadId,
},
);
await clearCodexAppServerBinding(params.params.sessionFile);
} else {
try {
const authProfileId = params.params.authProfileId ?? binding.authProfileId;
@@ -159,25 +142,23 @@ export async function startOrResumeThread(params: {
config: params.params.config,
});
const createdAt = new Date().toISOString();
if (!preserveExistingBinding) {
await writeCodexAppServerBinding(
params.params.sessionFile,
{
threadId: response.thread.id,
cwd: params.cwd,
authProfileId: params.params.authProfileId,
model: response.model ?? params.params.modelId,
modelProvider: response.modelProvider ?? modelProvider,
dynamicToolsFingerprint,
createdAt,
},
{
authProfileStore: params.params.authProfileStore,
agentDir: params.params.agentDir,
config: params.params.config,
},
);
}
await writeCodexAppServerBinding(
params.params.sessionFile,
{
threadId: response.thread.id,
cwd: params.cwd,
authProfileId: params.params.authProfileId,
model: response.model ?? params.params.modelId,
modelProvider: response.modelProvider ?? modelProvider,
dynamicToolsFingerprint,
createdAt,
},
{
authProfileStore: params.params.authProfileStore,
agentDir: params.params.agentDir,
config: params.params.config,
},
);
return {
schemaVersion: 1,
threadId: response.thread.id,
@@ -303,21 +284,8 @@ function buildHeartbeatCollaborationInstructions(): string {
].join("\n\n");
}
export function codexDynamicToolsFingerprint(dynamicTools: CodexDynamicToolSpec[]): string {
return fingerprintDynamicTools(dynamicTools);
}
export function areCodexDynamicToolFingerprintsCompatible(params: {
previous?: string;
next: string;
}): boolean {
return areDynamicToolFingerprintsCompatible(params.previous, params.next);
}
function fingerprintDynamicTools(dynamicTools: CodexDynamicToolSpec[]): string {
return JSON.stringify(
dynamicTools.map(fingerprintDynamicToolSpec).toSorted(compareJsonFingerprint),
);
return JSON.stringify(dynamicTools.map(fingerprintDynamicToolSpec));
}
function fingerprintDynamicToolSpec(tool: JsonValue): JsonValue {
@@ -352,27 +320,6 @@ function stabilizeJsonValue(value: JsonValue): JsonValue {
return stable;
}
const EMPTY_DYNAMIC_TOOLS_FINGERPRINT = JSON.stringify([]);
function areDynamicToolFingerprintsCompatible(previous: string | undefined, next: string): boolean {
return !previous || previous === next;
}
function shouldStartTransientNoToolThread(params: {
previous: string | undefined;
next: string;
}): boolean {
return Boolean(
params.previous &&
params.previous !== EMPTY_DYNAMIC_TOOLS_FINGERPRINT &&
params.next === EMPTY_DYNAMIC_TOOLS_FINGERPRINT,
);
}
function compareJsonFingerprint(left: JsonValue, right: JsonValue): number {
return JSON.stringify(left).localeCompare(JSON.stringify(right));
}
export function buildDeveloperInstructions(params: EmbeddedRunAttemptParams): string {
const promptOverlay = renderCodexRuntimePromptOverlay(params);
const sections = [

View File

@@ -13,7 +13,7 @@
"https-proxy-agent": "^9.0.0",
"opusscript": "^0.1.1",
"typebox": "1.1.37",
"undici": "8.2.0",
"undici": "8.1.0",
"ws": "^8.20.0"
},
"devDependencies": {

View File

@@ -5,7 +5,7 @@
"description": "OpenClaw Fireworks provider plugin",
"type": "module",
"dependencies": {
"@mariozechner/pi-ai": "0.73.0"
"@mariozechner/pi-ai": "0.71.1"
},
"devDependencies": {
"@openclaw/plugin-sdk": "workspace:*"

View File

@@ -8,7 +8,7 @@
"@clack/prompts": "^1.3.0"
},
"devDependencies": {
"@mariozechner/pi-ai": "0.73.0",
"@mariozechner/pi-ai": "0.71.1",
"@openclaw/plugin-sdk": "workspace:*"
},
"openclaw": {

View File

@@ -6,7 +6,7 @@
"type": "module",
"dependencies": {
"@google/genai": "^1.51.0",
"@mariozechner/pi-ai": "0.73.0"
"@mariozechner/pi-ai": "0.71.1"
},
"devDependencies": {
"@openclaw/plugin-sdk": "workspace:*"

View File

@@ -10,7 +10,7 @@
"dependencies": {
"gaxios": "7.1.4",
"google-auth-library": "10.6.2",
"zod": "^4.4.3"
"zod": "^4.4.1"
},
"devDependencies": {
"@openclaw/plugin-sdk": "workspace:*",

View File

@@ -5,7 +5,7 @@
"description": "OpenClaw Kimi provider plugin",
"type": "module",
"dependencies": {
"@mariozechner/pi-ai": "0.73.0"
"@mariozechner/pi-ai": "0.71.1"
},
"devDependencies": {
"@openclaw/plugin-sdk": "workspace:*"

View File

@@ -5,7 +5,7 @@
"description": "OpenClaw LM Studio provider plugin",
"type": "module",
"dependencies": {
"@mariozechner/pi-ai": "0.73.0"
"@mariozechner/pi-ai": "0.71.1"
},
"openclaw": {
"extensions": [

View File

@@ -10,7 +10,7 @@
"dependencies": {
"@lancedb/lancedb": "^0.27.2",
"apache-arrow": "18.1.0",
"openai": "^6.36.0",
"openai": "^6.35.0",
"typebox": "1.1.37"
},
"devDependencies": {

View File

@@ -6,7 +6,7 @@
"type": "module",
"dependencies": {
"typebox": "1.1.37",
"yaml": "^2.8.4"
"yaml": "^2.8.3"
},
"devDependencies": {
"@openclaw/plugin-sdk": "workspace:*",

View File

@@ -5,7 +5,7 @@
"description": "Hermes to OpenClaw migration provider",
"type": "module",
"dependencies": {
"yaml": "^2.8.4"
"yaml": "^2.8.3"
},
"devDependencies": {
"@openclaw/plugin-sdk": "workspace:*",

View File

@@ -8,7 +8,7 @@
},
"type": "module",
"dependencies": {
"zod": "^4.4.3"
"zod": "^4.4.1"
},
"devDependencies": {
"@openclaw/plugin-sdk": "workspace:*",

View File

@@ -9,7 +9,7 @@
"type": "module",
"dependencies": {
"nostr-tools": "^2.23.3",
"zod": "^4.4.3"
"zod": "^4.4.1"
},
"devDependencies": {
"@openclaw/plugin-sdk": "workspace:*",

View File

@@ -5,7 +5,7 @@
"description": "OpenClaw Ollama provider plugin",
"type": "module",
"dependencies": {
"@mariozechner/pi-ai": "0.73.0",
"@mariozechner/pi-ai": "0.71.1",
"typebox": "1.1.37"
},
"devDependencies": {

View File

@@ -508,9 +508,9 @@ describe("buildOpenAIProvider", () => {
});
expect(extraParams).toMatchObject({
transport: "sse",
transport: "auto",
openaiWsWarmup: true,
});
expect(extraParams?.openaiWsWarmup).toBeUndefined();
expect(result.payload.store).toBe(true);
expect(result.payload.context_management).toEqual([
{ type: "compaction", compact_threshold: 140_000 },

View File

@@ -227,7 +227,7 @@ export function buildOpenAIProvider(): ProviderPlugin {
shouldUseOpenAIResponsesTransport({ provider, api, baseUrl })
? { api: "openai-responses", baseUrl }
: undefined,
...buildOpenAIResponsesProviderHooks({ transport: "sse" }),
...buildOpenAIResponsesProviderHooks({ openaiWsWarmup: true }),
matchesContextOverflowError: ({ errorMessage }) =>
/content_filter.*(?:prompt|input).*(?:too long|exceed)/i.test(errorMessage),
resolveReasoningOutputMode: () => "native",

View File

@@ -60,7 +60,7 @@ function providerWizardByKey() {
describe("OpenAI plugin manifest", () => {
it("keeps runtime dependencies in the package manifest", () => {
expect(packageJson.dependencies?.["@mariozechner/pi-ai"]).toBe("0.73.0");
expect(packageJson.dependencies?.["@mariozechner/pi-ai"]).toBe("0.71.1");
expect(packageJson.dependencies?.ws).toBe("^8.20.0");
});

View File

@@ -5,7 +5,7 @@
"description": "OpenClaw OpenAI provider plugins",
"type": "module",
"dependencies": {
"@mariozechner/pi-ai": "0.73.0",
"@mariozechner/pi-ai": "0.71.1",
"ws": "^8.20.0"
},
"devDependencies": {

View File

@@ -50,11 +50,10 @@ function hasSupportedOpenAIResponsesTransport(
function defaultOpenAIResponsesExtraParams(
extraParams: Record<string, unknown> | undefined,
options?: { openaiWsWarmup?: boolean; transport?: "auto" | "sse" | "websocket" },
options?: { openaiWsWarmup?: boolean },
): Record<string, unknown> | undefined {
const hasSupportedTransport = hasSupportedOpenAIResponsesTransport(extraParams?.transport);
const hasExplicitWarmup = typeof extraParams?.openaiWsWarmup === "boolean";
const defaultTransport = options?.transport ?? "auto";
const shouldDefaultWarmup = options?.openaiWsWarmup === true;
if (hasSupportedTransport && (!shouldDefaultWarmup || hasExplicitWarmup)) {
return extraParams;
@@ -62,7 +61,7 @@ function defaultOpenAIResponsesExtraParams(
return {
...extraParams,
...(hasSupportedTransport ? {} : { transport: defaultTransport }),
...(hasSupportedTransport ? {} : { transport: "auto" }),
...(shouldDefaultWarmup && !hasExplicitWarmup ? { openaiWsWarmup: true } : {}),
};
}
@@ -94,7 +93,6 @@ const wrapOpenAIResponsesProviderStreamFn: NonNullable<
export function buildOpenAIResponsesProviderHooks(options?: {
openaiWsWarmup?: boolean;
transport?: "auto" | "sse" | "websocket";
}): OpenAIResponsesProviderHooks {
return {
buildReplayPolicy: buildOpenAIReplayPolicy,

View File

@@ -73,7 +73,7 @@ describe("openrouter provider hooks", () => {
it("advertises xhigh thinking for OpenRouter-routed DeepSeek V4 models", async () => {
const provider = await registerSingleProviderPlugin(openrouterPlugin);
const expectedV4Levels = ["off", "minimal", "low", "medium", "high", "xhigh"];
const expectedV4Levels = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
expect(
provider
@@ -309,7 +309,7 @@ describe("openrouter provider hooks", () => {
expect(capturedPayload).toMatchObject({
thinking: { type: "enabled" },
reasoning_effort: "xhigh",
reasoning_effort: "max",
messages: [
{ role: "user", content: "read file" },
{
@@ -324,50 +324,6 @@ describe("openrouter provider hooks", () => {
expect(baseStreamFn).toHaveBeenCalledOnce();
});
it("keeps OpenRouter DeepSeek V4 reasoning_effort within OpenRouter values", async () => {
const provider = await registerSingleProviderPlugin(openrouterPlugin);
const payloads: Array<Record<string, unknown>> = [];
const baseStreamFn = vi.fn(
(
...args: Parameters<import("@mariozechner/pi-agent-core").StreamFn>
): ReturnType<import("@mariozechner/pi-agent-core").StreamFn> => {
const payload = { messages: [] };
void args[2]?.onPayload?.(payload, args[0]);
payloads.push(payload);
return { async *[Symbol.asyncIterator]() {} } as never;
},
);
for (const thinkingLevel of ["minimal", "low", "medium", "high", "xhigh", "max"] as const) {
const wrapped = provider.wrapStreamFn?.({
provider: "openrouter",
modelId: "openrouter/deepseek/deepseek-v4-pro",
streamFn: baseStreamFn,
thinkingLevel,
} as never);
void wrapped?.(
{
provider: "openrouter",
api: "openai-completions",
id: "openrouter/deepseek/deepseek-v4-pro",
baseUrl: "https://openrouter.ai/api/v1",
compat: {},
} as never,
{ messages: [] } as never,
{},
);
}
expect(payloads.map((payload) => payload.reasoning_effort)).toEqual([
"minimal",
"low",
"medium",
"high",
"xhigh",
"xhigh",
]);
});
it("recognizes full OpenRouter DeepSeek V4 refs but skips custom proxy routes", async () => {
const provider = await registerSingleProviderPlugin(openrouterPlugin);
const payloads: Array<Record<string, unknown>> = [];

View File

@@ -3,8 +3,6 @@ import type { ProviderWrapStreamFnContext } from "openclaw/plugin-sdk/plugin-ent
import { OPENROUTER_THINKING_STREAM_HOOKS } from "openclaw/plugin-sdk/provider-stream-family";
import {
createDeepSeekV4OpenAICompatibleThinkingWrapper,
type DeepSeekV4ReasoningEffort,
type DeepSeekV4ThinkingLevel,
createPayloadPatchStreamWrapper,
stripTrailingAssistantPrefillMessages,
} from "openclaw/plugin-sdk/provider-stream-shared";
@@ -57,27 +55,6 @@ function shouldPatchDeepSeekV4OpenRouterPayload(model: Parameters<StreamFn>[0]):
);
}
function resolveOpenRouterDeepSeekV4ReasoningEffort(
thinkingLevel: DeepSeekV4ThinkingLevel,
): DeepSeekV4ReasoningEffort {
switch (thinkingLevel) {
case "minimal":
case "low":
case "medium":
case "high":
case "xhigh":
return thinkingLevel;
case "max":
return "xhigh";
case "adaptive":
return "medium";
case "off":
case undefined:
return "high";
}
return "high";
}
function isEnabledReasoningValue(value: unknown): boolean {
if (value === undefined || value === null || value === false) {
return false;
@@ -148,7 +125,6 @@ function createOpenRouterDeepSeekV4ThinkingWrapper(
baseStreamFn,
thinkingLevel,
shouldPatchModel: shouldPatchDeepSeekV4OpenRouterPayload,
resolveReasoningEffort: resolveOpenRouterDeepSeekV4ReasoningEffort,
});
}
@@ -180,3 +156,12 @@ export function wrapOpenRouterProviderStream(
createOpenRouterDeepSeekV4ThinkingWrapper(wrappedStreamFn, ctx.thinkingLevel),
);
}
export const __testing = {
isOpenRouterDeepSeekV4ModelId,
isOpenRouterAnthropicModelId,
isOpenRouterReasoningPayloadEnabled,
isVerifiedOpenRouterRoute,
shouldPatchDeepSeekV4OpenRouterPayload,
shouldPatchAnthropicOpenRouterPayload,
};

View File

@@ -8,6 +8,7 @@ const OPENROUTER_DEEPSEEK_V4_THINKING_LEVEL_IDS = [
"medium",
"high",
"xhigh",
"max",
] as const;
function buildOpenRouterDeepSeekV4ThinkingLevel(

View File

@@ -5,11 +5,11 @@
"description": "OpenClaw QA lab plugin with private debugger UI and scenario runner",
"type": "module",
"dependencies": {
"@copilotkit/aimock": "1.17.0",
"@copilotkit/aimock": "1.16.4",
"@modelcontextprotocol/sdk": "1.29.0",
"playwright-core": "1.59.1",
"yaml": "^2.8.4",
"zod": "^4.4.3"
"yaml": "^2.8.3",
"zod": "^4.4.1"
},
"devDependencies": {
"@openclaw/discord": "workspace:*",

View File

@@ -407,44 +407,6 @@ describe("buildQaRuntimeEnv", () => {
});
});
it("stages live env API-key profiles for isolated QA workers", async () => {
const stateDir = await mkdtemp(path.join(os.tmpdir(), "qa-live-api-key-state-"));
cleanups.push(async () => {
await rm(stateDir, { recursive: true, force: true });
});
const cfg = await __testing.stageQaLiveApiKeyProfiles({
cfg: {},
stateDir,
providerIds: ["openai"],
env: {
OPENAI_API_KEY: "qa-live-not-a-real-key",
},
});
expect(cfg.auth?.profiles?.["qa-live-openai-env"]).toMatchObject({
provider: "openai",
mode: "api_key",
displayName: "QA live openai env credential",
});
for (const agentId of ["main", "qa"]) {
const storeRaw = await readFile(
path.join(stateDir, "agents", agentId, "agent", "auth-profiles.json"),
"utf8",
);
expect(JSON.parse(storeRaw)).toMatchObject({
profiles: {
"qa-live-openai-env": {
type: "api_key",
provider: "openai",
key: "qa-live-not-a-real-key",
},
},
});
}
});
it("stages placeholder mock auth profiles per agent dir so mock-openai runs can resolve credentials", async () => {
const stateDir = await mkdtemp(path.join(os.tmpdir(), "qa-mock-auth-"));
cleanups.push(async () => {

View File

@@ -34,7 +34,6 @@ import { DEFAULT_QA_PROVIDER_MODE, getQaProvider } from "./providers/index.js";
import {
QA_LIVE_ANTHROPIC_SETUP_TOKEN_ENV,
QA_LIVE_SETUP_TOKEN_VALUE_ENV,
stageQaLiveApiKeyProfiles,
stageQaLiveAnthropicSetupToken,
} from "./providers/live-frontier/auth.js";
import { stageQaMockAuthProfiles } from "./providers/shared/mock-auth.js";
@@ -315,7 +314,6 @@ export const __testing = {
redactQaGatewayDebugText,
readQaLiveProviderConfigOverrides,
resolveQaGatewayChildProviderMode,
stageQaLiveApiKeyProfiles,
stageQaLiveAnthropicSetupToken,
stageQaMockAuthProfiles,
resolveQaLiveCliAuthEnv,
@@ -575,11 +573,6 @@ export async function startQaGatewayChild(params: {
});
const buildStagedGatewayConfig = async (gatewayPort: number) => {
let cfg = buildGatewayConfig(gatewayPort);
cfg = await stageQaLiveApiKeyProfiles({
cfg,
stateDir,
providerIds: liveProviderIds,
});
cfg = await stageQaLiveAnthropicSetupToken({
cfg,
stateDir,

View File

@@ -1,7 +1,6 @@
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
import {
applyAuthProfileConfig,
resolveEnvApiKey,
validateAnthropicSetupToken,
} from "openclaw/plugin-sdk/provider-auth";
import { resolveQaAgentAuthDir, writeQaAuthProfiles } from "../shared/auth-store.js";
@@ -10,11 +9,6 @@ export const QA_LIVE_ANTHROPIC_SETUP_TOKEN_ENV = "OPENCLAW_QA_LIVE_ANTHROPIC_SET
export const QA_LIVE_SETUP_TOKEN_VALUE_ENV = "OPENCLAW_LIVE_SETUP_TOKEN_VALUE";
const QA_LIVE_ANTHROPIC_SETUP_TOKEN_PROFILE_ENV = "OPENCLAW_QA_LIVE_ANTHROPIC_SETUP_TOKEN_PROFILE";
const QA_LIVE_ANTHROPIC_SETUP_TOKEN_PROFILE_ID = "anthropic:qa-setup-token";
const QA_LIVE_API_KEY_AGENT_IDS = Object.freeze(["main", "qa"] as const);
function buildQaLiveApiKeyProfileId(provider: string): string {
return `qa-live-${provider.replaceAll(/[^a-z0-9_-]/giu, "-")}-env`;
}
function resolveQaLiveAnthropicSetupToken(env: NodeJS.ProcessEnv = process.env) {
const token = (
@@ -61,59 +55,3 @@ export async function stageQaLiveAnthropicSetupToken(params: {
displayName: "QA setup-token",
});
}
export async function stageQaLiveApiKeyProfiles(params: {
cfg: OpenClawConfig;
stateDir: string;
providerIds: readonly string[];
env?: NodeJS.ProcessEnv;
agentIds?: readonly string[];
}): Promise<OpenClawConfig> {
const env = params.env ?? process.env;
const providerIds = [...new Set(params.providerIds.map((providerId) => providerId.trim()))]
.filter((providerId) => providerId.length > 0)
.toSorted();
const profiles: Record<
string,
{
type: "api_key";
provider: string;
key: string;
displayName: string;
}
> = {};
let next = params.cfg;
for (const providerId of providerIds) {
const resolved = resolveEnvApiKey(providerId, env, { config: next });
if (!resolved?.apiKey) {
continue;
}
const profileId = buildQaLiveApiKeyProfileId(providerId);
const displayName = `QA live ${providerId} env credential`;
profiles[profileId] = {
type: "api_key",
provider: providerId,
key: resolved.apiKey,
displayName,
};
next = applyAuthProfileConfig(next, {
profileId,
provider: providerId,
mode: "api_key",
displayName,
});
}
if (Object.keys(profiles).length === 0) {
return next;
}
const agentIds = [...new Set(params.agentIds ?? QA_LIVE_API_KEY_AGENT_IDS)];
await Promise.all(
agentIds.map((agentId) =>
writeQaAuthProfiles({
agentDir: resolveQaAgentAuthDir({ stateDir: params.stateDir, agentId }),
profiles,
}),
),
);
return next;
}

View File

@@ -22,15 +22,10 @@ export async function writeQaAuthProfiles(params: {
agentDir: string;
profiles: Record<string, QaAuthProfileCredential>;
}): Promise<void> {
const authPath = path.join(params.agentDir, "auth-profiles.json");
const existing = await fs
.readFile(authPath, "utf8")
.then((raw) => JSON.parse(raw) as { profiles?: Record<string, QaAuthProfileCredential> })
.catch(() => ({ profiles: {} }));
await fs.mkdir(params.agentDir, { recursive: true });
await fs.writeFile(
authPath,
`${JSON.stringify({ version: 1, profiles: { ...existing.profiles, ...params.profiles } }, null, 2)}\n`,
path.join(params.agentDir, "auth-profiles.json"),
`${JSON.stringify({ version: 1, profiles: params.profiles }, null, 2)}\n`,
"utf8",
);
}

View File

@@ -187,7 +187,6 @@ describe("qa scenario catalog", () => {
pluginId?: string;
pluginPersonality?: string;
adversarialPersonality?: string;
expectedSurfaceIds?: Record<string, string[]>;
expectedAdversarialDiagnostics?: string[];
}
| undefined;
@@ -199,22 +198,9 @@ describe("qa scenario catalog", () => {
expect(config?.pluginId).toBe("openclaw-kitchen-sink-fixture");
expect(config?.pluginPersonality).toBe("conformance");
expect(config?.adversarialPersonality).toBe("adversarial");
expect(config?.expectedSurfaceIds?.webSearchProviderIds).toContain(
"kitchen-sink-web-search-provider",
);
expect(config?.expectedSurfaceIds?.realtimeVoiceProviderIds).toContain(
"kitchen-sink-realtime-voice-provider",
);
expect(config?.expectedAdversarialDiagnostics).toContain(
"only bundled plugins can register agent tool result middleware",
);
expect(config?.expectedAdversarialDiagnostics).toContain(
"control UI descriptor registration requires id, surface, label, and valid optional fields",
);
expect(
config?.expectedAdversarialDiagnostics?.every((entry) => typeof entry === "string"),
).toBe(true);
expect(JSON.stringify(scenario.execution.flow)).toContain("--runtime");
expect(scenario.execution.flow?.steps.map((step) => step.name)).toEqual([
"installs and inspects the Kitchen Sink plugin",
"restarts gateway with Kitchen Sink configured",

View File

@@ -51,8 +51,6 @@ import {
import { createTempDirHarness } from "./temp-dir.test-helper.js";
const { cleanup, makeTempDir } = createTempDirHarness();
const repoRoot = "/repo/openclaw";
const gatewayTempRoot = "/tmp/openclaw-qa-runtime";
afterEach(cleanup);
@@ -113,14 +111,12 @@ describe("qa suite runtime agent tools helpers", () => {
callPluginToolsMcp({
env: {
gateway: {
tempRoot: gatewayTempRoot,
runtimeEnv: {
PATH: "/usr/bin",
OPENCLAW_KEY: "1",
EMPTY: undefined,
},
},
repoRoot,
} as never,
toolName: "plugin.echo",
args: { text: "hello" },
@@ -131,13 +127,8 @@ describe("qa suite runtime agent tools helpers", () => {
expect(stdioTransportMock).toHaveBeenCalledWith({
command: "/usr/bin/node",
args: [
"--import",
expect.stringContaining(path.join("node_modules", "tsx")),
path.join(repoRoot, "src", "mcp", "plugin-tools-serve.ts"),
],
args: ["--import", "tsx", "src/mcp/plugin-tools-serve.ts"],
stderr: "pipe",
cwd: gatewayTempRoot,
env: {
PATH: "/usr/bin",
OPENCLAW_KEY: "1",
@@ -149,31 +140,4 @@ describe("qa suite runtime agent tools helpers", () => {
});
expect(closeMock).toHaveBeenCalled();
});
it("reports available plugin-tools MCP names when the requested tool is missing", async () => {
listToolsMock.mockResolvedValueOnce({
tools: [{ name: "plugin.beta" }, { name: "plugin.alpha" }] as never[],
});
await expect(
callPluginToolsMcp({
env: {
gateway: {
tempRoot: gatewayTempRoot,
runtimeEnv: {
PATH: "/usr/bin",
},
},
repoRoot,
} as never,
toolName: "plugin.missing",
args: {},
}),
).rejects.toThrow(
"MCP tool missing: plugin.missing; available tools: plugin.alpha, plugin.beta",
);
expect(callToolMock).not.toHaveBeenCalled();
expect(closeMock).toHaveBeenCalled();
});
});

View File

@@ -1,5 +1,4 @@
import fs from "node:fs/promises";
import { createRequire } from "node:module";
import path from "node:path";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
@@ -12,8 +11,6 @@ import type {
QaTransportActionName,
} from "./suite-runtime-types.js";
const requireFromHere = createRequire(import.meta.url);
function findSkill(skills: QaSkillStatusEntry[], name: string) {
return skills.find((skill) => skill.name === name);
}
@@ -31,7 +28,7 @@ async function writeWorkspaceSkill(params: {
}
async function callPluginToolsMcp(params: {
env: Pick<QaSuiteRuntimeEnv, "gateway" | "repoRoot">;
env: Pick<QaSuiteRuntimeEnv, "gateway">;
toolName: string;
args: Record<string, unknown>;
}) {
@@ -43,13 +40,8 @@ async function callPluginToolsMcp(params: {
const nodeExecPath = await resolveQaNodeExecPath();
const transport = new StdioClientTransport({
command: nodeExecPath,
args: [
"--import",
requireFromHere.resolve("tsx"),
path.join(params.env.repoRoot, "src/mcp/plugin-tools-serve.ts"),
],
args: ["--import", "tsx", "src/mcp/plugin-tools-serve.ts"],
stderr: "pipe",
cwd: params.env.gateway.tempRoot,
env: transportEnv,
});
const client = new Client({ name: "openclaw-qa-suite", version: "0.0.0" }, {});
@@ -58,13 +50,7 @@ async function callPluginToolsMcp(params: {
const listed = await client.listTools();
const tool = listed.tools.find((entry) => entry.name === params.toolName);
if (!tool) {
const availableTools = listed.tools
.map((entry) => entry.name)
.filter((name): name is string => typeof name === "string" && name.length > 0)
.toSorted();
throw new Error(
`MCP tool missing: ${params.toolName}; available tools: ${availableTools.join(", ") || "<none>"}`,
);
throw new Error(`MCP tool missing: ${params.toolName}`);
}
return await client.callTool({
name: params.toolName,

View File

@@ -5,7 +5,7 @@
"description": "OpenClaw Matrix QA runner plugin",
"type": "module",
"dependencies": {
"undici": "8.2.0"
"undici": "8.1.0"
},
"devDependencies": {
"@openclaw/matrix": "workspace:*",

View File

@@ -13,7 +13,7 @@
"mpg123-decoder": "^1.0.3",
"silk-wasm": "^3.7.1",
"ws": "^8.20.0",
"zod": "^4.4.3"
"zod": "^4.4.1"
},
"devDependencies": {
"@openclaw/plugin-sdk": "workspace:*",

View File

@@ -1,55 +0,0 @@
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
import type { PluginCommandContext } from "openclaw/plugin-sdk/plugin-entry";
import { describe, expect, it } from "vitest";
import { buildFrameworkSlashContext } from "./framework-context-adapter.js";
function createCommandContext(isAuthorizedSender: boolean): PluginCommandContext {
return {
senderId: "SENDER_OPENID",
channel: "qqbot",
isAuthorizedSender,
args: "on",
commandBody: "/bot-streaming on",
config: {} as OpenClawConfig,
from: "qqbot:c2c:SENDER_OPENID",
requestConversationBinding: async () => undefined,
detachConversationBinding: async () => ({ removed: false }),
getCurrentConversationBinding: async () => null,
} as unknown as PluginCommandContext;
}
describe("buildFrameworkSlashContext", () => {
it("preserves the framework authorization decision in the slash context", () => {
const authorized = buildFrameworkSlashContext({
ctx: createCommandContext(true),
account: {
accountId: "default",
enabled: true,
appId: "app",
clientSecret: "secret",
secretSource: "config",
markdownSupport: true,
config: {},
},
from: { msgType: "c2c", targetType: "c2c", targetId: "SENDER_OPENID" },
commandName: "bot-streaming",
});
const unauthorized = buildFrameworkSlashContext({
ctx: createCommandContext(false),
account: {
accountId: "default",
enabled: true,
appId: "app",
clientSecret: "secret",
secretSource: "config",
markdownSupport: true,
config: {},
},
from: { msgType: "c2c", targetType: "c2c", targetId: "SENDER_OPENID" },
commandName: "bot-streaming",
});
expect(authorized.commandAuthorized).toBe(true);
expect(unauthorized.commandAuthorized).toBe(false);
});
});

View File

@@ -54,7 +54,7 @@ export function buildFrameworkSlashContext({
accountId: account.accountId,
appId: account.appId,
accountConfig: account.config as unknown as Record<string, unknown>,
commandAuthorized: ctx.isAuthorizedSender,
commandAuthorized: true,
queueSnapshot: { ...DEFAULT_QUEUE_SNAPSHOT },
};
}

View File

@@ -6,8 +6,8 @@
"type": "module",
"dependencies": {
"@slack/bolt": "^4.7.2",
"@slack/types": "^2.21.0",
"@slack/web-api": "^7.15.2",
"@slack/types": "^2.20.1",
"@slack/web-api": "^7.15.1",
"https-proxy-agent": "^9.0.0"
},
"devDependencies": {

View File

@@ -8,7 +8,7 @@
},
"type": "module",
"dependencies": {
"zod": "^4.4.3"
"zod": "^4.4.1"
},
"devDependencies": {
"@openclaw/plugin-sdk": "workspace:*"

View File

@@ -9,7 +9,7 @@
"@grammyjs/transformer-throttler": "^1.2.1",
"grammy": "^1.42.0",
"typebox": "1.1.37",
"undici": "8.2.0"
"undici": "8.1.0"
},
"devDependencies": {
"@openclaw/plugin-sdk": "workspace:*"

View File

@@ -1,112 +0,0 @@
import { getRuntimeConfig } from "openclaw/plugin-sdk/runtime-config-snapshot";
import { beforeEach, describe, expect, it, vi } from "vitest";
const { defaultRouteConfig } = vi.hoisted(() => ({
defaultRouteConfig: {
agents: {
list: [{ id: "main", default: true }],
},
channels: { telegram: {} },
messages: { groupChat: { mentionPatterns: [] } },
},
}));
vi.mock("openclaw/plugin-sdk/runtime-config-snapshot", async () => {
const actual = await vi.importActual<
typeof import("openclaw/plugin-sdk/runtime-config-snapshot")
>("openclaw/plugin-sdk/runtime-config-snapshot");
return {
...actual,
getRuntimeConfig: vi.fn(() => defaultRouteConfig),
};
});
const { buildTelegramMessageContextForTest } =
await import("./bot-message-context.test-harness.js");
describe("buildTelegramMessageContext requireMention precedence", () => {
function buildForumMessage(threadId = 99) {
return {
message_id: 1,
chat: {
id: -1001234567890,
type: "supergroup" as const,
title: "Forum",
is_forum: true,
},
date: 1_700_000_000,
text: "hello everyone",
message_thread_id: threadId,
from: { id: 42, first_name: "Alice" },
};
}
beforeEach(() => {
vi.mocked(getRuntimeConfig).mockReturnValue(defaultRouteConfig as never);
});
it("lets explicit topic requireMention=false override group requireMention=true", async () => {
const ctx = await buildTelegramMessageContextForTest({
message: buildForumMessage(),
resolveGroupActivation: () => undefined,
resolveGroupRequireMention: () => true,
resolveTelegramGroupConfig: () => ({
groupConfig: { requireMention: true },
topicConfig: { requireMention: false },
}),
});
expect(ctx).not.toBeNull();
});
it("lets explicit topic requireMention=false override mention activation", async () => {
const resolveGroupActivation = vi.fn(() => true);
const ctx = await buildTelegramMessageContextForTest({
message: buildForumMessage(),
resolveGroupActivation,
resolveGroupRequireMention: () => true,
resolveTelegramGroupConfig: () => ({
groupConfig: { requireMention: true },
topicConfig: { requireMention: false },
}),
});
expect(ctx).not.toBeNull();
expect(resolveGroupActivation).toHaveBeenCalledWith(
expect.objectContaining({
chatId: -1001234567890,
messageThreadId: 99,
sessionKey: "agent:main:telegram:group:-1001234567890:topic:99",
}),
);
});
it("lets explicit topic requireMention=true override always activation", async () => {
const ctx = await buildTelegramMessageContextForTest({
message: buildForumMessage(),
resolveGroupActivation: () => false,
resolveGroupRequireMention: () => false,
resolveTelegramGroupConfig: () => ({
groupConfig: { requireMention: false },
topicConfig: { requireMention: true },
}),
});
expect(ctx).toBeNull();
});
it("keeps activation fallback when no topic requireMention is configured", async () => {
const ctx = await buildTelegramMessageContextForTest({
message: buildForumMessage(),
resolveGroupActivation: () => false,
resolveGroupRequireMention: () => true,
resolveTelegramGroupConfig: () => ({
groupConfig: { requireMention: true },
topicConfig: { agentId: "main" },
}),
});
expect(ctx).not.toBeNull();
});
});

View File

@@ -411,8 +411,8 @@ export const buildTelegramMessageContext = async ({
});
const baseRequireMention = resolveGroupRequireMention(chatId);
const requireMention = firstDefined(
topicConfig?.requireMention,
activationOverride,
topicConfig?.requireMention,
telegramGroupConfig?.requireMention,
baseRequireMention,
);

View File

@@ -8,8 +8,8 @@
},
"type": "module",
"dependencies": {
"@aws-sdk/client-s3": "3.1042.0",
"@aws-sdk/s3-request-presigner": "3.1042.0",
"@aws-sdk/client-s3": "3.1041.0",
"@aws-sdk/s3-request-presigner": "3.1041.0",
"@tloncorp/tlon-skill": "0.3.5",
"@urbit/aura": "^3.0.0"
},

View File

@@ -5,7 +5,7 @@
"description": "OpenClaw webhook bridge plugin",
"type": "module",
"dependencies": {
"zod": "^4.4.3"
"zod": "^4.4.1"
},
"devDependencies": {
"@openclaw/plugin-sdk": "workspace:*"

View File

@@ -12,7 +12,7 @@
"https-proxy-agent": "^9.0.0",
"jimp": "^1.6.1",
"typebox": "1.1.37",
"undici": "8.2.0"
"undici": "8.1.0"
},
"devDependencies": {
"@openclaw/plugin-sdk": "workspace:*",

View File

@@ -142,9 +142,7 @@ describe("loginWeb coverage", () => {
restartOpts?.onQr?.("restart-qr");
await flushTasks();
expect(runtime.log).toHaveBeenCalledWith(
"Open the WhatsApp app, go to Linked Devices, then scan this QR:",
);
expect(runtime.log).toHaveBeenCalledWith("Scan this QR in WhatsApp (Linked Devices):");
expect(runtime.log).toHaveBeenCalledWith("terminal:initial-qr");
expect(runtime.log).toHaveBeenCalledWith("terminal:restart-qr");
expect(renderQrTerminalMock).toHaveBeenCalledWith("initial-qr", { small: true });

View File

@@ -21,7 +21,7 @@ export async function loginWeb(
const socketTiming = resolveWhatsAppSocketTiming(cfg);
const restoredFromBackup = await restoreCredsFromBackupIfNeeded(account.authDir);
const onQr = (qr: string) => {
runtime.log("Open the WhatsApp app, go to Linked Devices, then scan this QR:");
runtime.log("Scan this QR in WhatsApp (Linked Devices):");
void renderQrTerminal(qr, { small: true })
.then((output) => {
runtime.log(output.endsWith("\n") ? output.slice(0, -1) : output);

View File

@@ -192,7 +192,7 @@ export async function createWaSocket(
if (qr) {
opts.onQr?.(qr);
if (printQr) {
console.log("Open the WhatsApp app, go to Linked Devices, then scan this QR:");
console.log("Scan this QR in WhatsApp (Linked Devices):");
void printTerminalQr(qr).catch((err) => {
sessionLogger.warn({ error: String(err) }, "failed rendering WhatsApp QR");
});

View File

@@ -5,7 +5,7 @@
"description": "OpenClaw xAI plugin",
"type": "module",
"dependencies": {
"@mariozechner/pi-ai": "0.73.0",
"@mariozechner/pi-ai": "0.71.1",
"typebox": "1.1.37"
},
"devDependencies": {

View File

@@ -8,7 +8,7 @@
},
"type": "module",
"dependencies": {
"undici": "8.2.0"
"undici": "8.1.0"
},
"devDependencies": {
"@openclaw/plugin-sdk": "workspace:*",

View File

@@ -1614,7 +1614,6 @@
"test:perf:profile:main": "node scripts/run-vitest-profile.mjs main",
"test:perf:profile:runner": "node scripts/run-vitest-profile.mjs runner",
"test:plugins:gateway-gauntlet": "node scripts/check-plugin-gateway-gauntlet.mjs",
"test:plugins:kitchen-sink-live": "bash -lc 'if [ -x \"$HOME/.local/bin/openclaw-testbox-env\" ]; then exec \"$HOME/.local/bin/openclaw-testbox-env\" pnpm openclaw qa suite --provider-mode live-frontier --scenario kitchen-sink-live-openai; fi; exec pnpm openclaw qa suite --provider-mode live-frontier --scenario kitchen-sink-live-openai'",
"test:sectriage": "OPENCLAW_GATEWAY_PROJECT_SHARDS=1 node scripts/run-vitest.mjs run --config test/vitest/vitest.gateway.config.ts && node scripts/run-vitest.mjs run --config test/vitest/vitest.unit.config.ts --exclude src/daemon/launchd.integration.test.ts --exclude src/process/exec.test.ts",
"test:serial": "OPENCLAW_TEST_PROJECTS_SERIAL=1 OPENCLAW_VITEST_MAX_WORKERS=1 node scripts/test-projects.mjs",
"test:stability:gateway": "OPENCLAW_VITEST_MAX_WORKERS=1 node scripts/run-vitest.mjs run --config test/vitest/vitest.gateway.config.ts src/gateway/gateway-stability.test.ts && OPENCLAW_VITEST_MAX_WORKERS=1 node scripts/run-vitest.mjs run --config test/vitest/vitest.logging.config.ts src/logging/diagnostic-stability-bundle.test.ts && OPENCLAW_VITEST_MAX_WORKERS=1 node scripts/run-vitest.mjs run --config test/vitest/vitest.infra.config.ts src/infra/fatal-error-hooks.test.ts",
@@ -1663,27 +1662,27 @@
},
"dependencies": {
"@agentclientprotocol/sdk": "0.21.0",
"@anthropic-ai/sdk": "0.93.0",
"@anthropic-ai/sdk": "0.92.0",
"@anthropic-ai/vertex-sdk": "^0.16.0",
"@aws-sdk/client-bedrock": "3.1042.0",
"@aws-sdk/client-bedrock-runtime": "3.1042.0",
"@aws-sdk/client-bedrock": "3.1041.0",
"@aws-sdk/client-bedrock-runtime": "3.1041.0",
"@aws-sdk/credential-provider-node": "3.972.39",
"@aws/bedrock-token-generator": "^1.1.0",
"@clack/prompts": "^1.3.0",
"@google/genai": "^1.51.0",
"@grammyjs/runner": "^2.0.3",
"@grammyjs/transformer-throttler": "^1.2.1",
"@homebridge/ciao": "^1.3.8",
"@homebridge/ciao": "^1.3.7",
"@lydell/node-pty": "1.2.0-beta.12",
"@mariozechner/pi-agent-core": "0.73.0",
"@mariozechner/pi-ai": "0.73.0",
"@mariozechner/pi-coding-agent": "0.73.0",
"@mariozechner/pi-tui": "0.73.0",
"@mariozechner/pi-agent-core": "0.71.1",
"@mariozechner/pi-ai": "0.71.1",
"@mariozechner/pi-coding-agent": "0.71.1",
"@mariozechner/pi-tui": "0.71.1",
"@modelcontextprotocol/sdk": "1.29.0",
"@mozilla/readability": "^0.6.0",
"@slack/bolt": "^4.7.2",
"@slack/types": "^2.21.0",
"@slack/web-api": "^7.15.2",
"@slack/types": "^2.20.1",
"@slack/web-api": "^7.15.1",
"ajv": "^8.20.0",
"chalk": "^5.6.2",
"chokidar": "^5.0.0",
@@ -1695,7 +1694,7 @@
"global-agent": "^4.1.3",
"grammy": "^1.42.0",
"https-proxy-agent": "^9.0.0",
"ipaddr.js": "^2.4.0",
"ipaddr.js": "^2.3.0",
"jiti": "^2.6.1",
"json5": "^2.2.3",
"jszip": "^3.10.1",
@@ -1703,7 +1702,7 @@
"markdown-it": "14.1.1",
"minimatch": "10.2.5",
"node-edge-tts": "^1.2.10",
"openai": "^6.36.0",
"openai": "^6.35.0",
"openshell": "0.1.0",
"pdfjs-dist": "^5.7.284",
"playwright-core": "1.59.1",
@@ -1714,15 +1713,15 @@
"tree-sitter-bash": "^0.25.1",
"tslog": "^4.10.2",
"typebox": "1.1.37",
"undici": "8.2.0",
"undici": "8.1.0",
"web-push": "^3.6.7",
"web-tree-sitter": "^0.26.8",
"ws": "^8.20.0",
"yaml": "^2.8.4",
"zod": "^4.4.3"
"yaml": "^2.8.3",
"zod": "^4.4.1"
},
"devDependencies": {
"@copilotkit/aimock": "1.17.0",
"@copilotkit/aimock": "1.16.4",
"@grammyjs/types": "^3.26.0",
"@lit-labs/signals": "^0.2.0",
"@lit/context": "^1.1.6",
@@ -1731,7 +1730,7 @@
"@types/markdown-it": "^14.1.2",
"@types/node": "25.6.0",
"@types/ws": "^8.18.1",
"@typescript/native-preview": "7.0.0-dev.20260504.1",
"@typescript/native-preview": "7.0.0-dev.20260501.1",
"@vitest/coverage-v8": "^4.1.5",
"jscpd": "4.0.9",
"jsdom": "^29.1.1",
@@ -1761,7 +1760,7 @@
"packageManager": "pnpm@10.33.2+sha512.a90faf6feeab71ad6c6e57f94e0fe1a12f5dcc22cd754db40ae9593eb6a3e0b6b12e3540218bb37ae083404b1f2ce6db2a4121e979829b4aff94b99f49da1cf8",
"pnpm": {
"overrides": {
"@anthropic-ai/sdk": "0.93.0",
"@anthropic-ai/sdk": "0.92.0",
"hono": "4.12.14",
"@hono/node-server": "1.19.14",
"@aws-sdk/client-bedrock-runtime": "3.1024.0",
@@ -1818,7 +1817,7 @@
},
"patchedDependencies": {
"@whiskeysockets/baileys@7.0.0-rc.9": "patches/@whiskeysockets__baileys@7.0.0-rc.9.patch",
"@agentclientprotocol/claude-agent-acp@0.32.0": "patches/@agentclientprotocol__claude-agent-acp@0.32.0.patch"
"@agentclientprotocol/claude-agent-acp@0.31.4": "patches/@agentclientprotocol__claude-agent-acp@0.31.4.patch"
}
}
}

View File

@@ -1,8 +1,8 @@
diff --git a/dist/acp-agent.js b/dist/acp-agent.js
index e1d9aa9f0815f57ea2fd299a7f2b8ef0917ca191..875fdfb25fbfa905ca80728355d25a17e6d89148 100644
index 0a8f5e3c57ed05189cba546bd65fc18143744d09..a8522d86a5a2f1bbcdd446d222cb9b7b5acb79ca 100644
--- a/dist/acp-agent.js
+++ b/dist/acp-agent.js
@@ -436,6 +436,7 @@ export class ClaudeAcpAgent {
@@ -421,6 +421,7 @@ export class ClaudeAcpAgent {
session.promptRunning = true;
let handedOff = false;
let stopReason = "end_turn";
@@ -10,7 +10,7 @@ index e1d9aa9f0815f57ea2fd299a7f2b8ef0917ca191..875fdfb25fbfa905ca80728355d25a17
try {
while (true) {
const { value: message, done } = await session.query.next();
@@ -443,6 +444,9 @@ export class ClaudeAcpAgent {
@@ -428,6 +429,9 @@ export class ClaudeAcpAgent {
if (session.cancelled) {
return { stopReason: "cancelled" };
}
@@ -20,7 +20,7 @@ index e1d9aa9f0815f57ea2fd299a7f2b8ef0917ca191..875fdfb25fbfa905ca80728355d25a17
break;
}
if (session.emitRawSDKMessages &&
@@ -499,7 +503,7 @@ export class ClaudeAcpAgent {
@@ -496,7 +500,7 @@ export class ClaudeAcpAgent {
break;
}
case "session_state_changed": {
@@ -29,7 +29,7 @@ index e1d9aa9f0815f57ea2fd299a7f2b8ef0917ca191..875fdfb25fbfa905ca80728355d25a17
return { stopReason, usage: sessionUsage(session) };
}
break;
@@ -621,6 +625,7 @@ export class ClaudeAcpAgent {
@@ -601,6 +605,7 @@ export class ClaudeAcpAgent {
unreachable(message, this.logger);
break;
}

811
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -49,44 +49,12 @@ execution:
- kitchen_sink_text
- kitchen_sink_search
- kitchen_sink_image_job
expectedSurfaceIds:
speechProviderIds:
- kitchen-sink-speech
- kitchen-sink-speech-provider
realtimeTranscriptionProviderIds:
- kitchen-sink-realtime-transcription
- kitchen-sink-realtime-transcription-provider
realtimeVoiceProviderIds:
- kitchen-sink-realtime-voice
- kitchen-sink-realtime-voice-provider
mediaUnderstandingProviderIds:
- kitchen-sink-media
- kitchen-sink-media-understanding-provider
imageGenerationProviderIds:
- kitchen-sink-image
- kitchen-sink-image-generation-provider
videoGenerationProviderIds:
- kitchen-sink-video
- kitchen-sink-video-generation-provider
musicGenerationProviderIds:
- kitchen-sink-music
- kitchen-sink-music-generation-provider
webFetchProviderIds:
- kitchen-sink-fetch
- kitchen-sink-web-fetch-provider
webSearchProviderIds:
- kitchen-sink-search
- kitchen-sink-web-search-provider
migrationProviderIds:
- kitchen-sink-migration-providers
- kitchen-sink-migration-provider
maxGatewayCpuCoreRatio: 1.5
maxGatewayRssMiB: 2048
agentTurnTimeoutMs: 120000
outboundTimeoutMs: 60000
livePrompt: "Kitchen Sink OpenAI marker. Reply exactly: KITCHEN-SINK-OPENAI-OK"
expectedAdversarialDiagnostics:
- agent event subscription registration requires id and handle
- only bundled plugins can register agent tool result middleware
- agent harness "kitchen-sink-agent-harness" registration missing required runtime methods
- channel "kitchen-sink-channel-probe" registration missing required config helpers
@@ -94,16 +62,9 @@ execution:
- only bundled plugins can register Codex app-server extension factories
- compaction provider "kitchen-sink-compaction-provider" registration missing summarize
- context engine registration missing id
- control UI descriptor registration requires id, surface, label, and valid optional fields
- "http route registration missing or invalid auth: /kitchen-sink/http-route"
- http route registration missing or invalid auth: /kitchen-sink/http-route
- "plugin must own memory slot or declare contracts.memoryEmbeddingProviders for adapter: kitchen-sink-memory-embedding-provider"
- memory prompt supplement registration missing builder
- node invoke policy registration missing commands
- session extension registration requires namespace and description
- session scheduler job registration requires unique id, sessionKey, and kind
- "plugin must declare contracts.tools for: kitchen-sink-tool"
- tool metadata registration missing toolName
- only bundled plugins can register trusted tool policies
```
```yaml qa-flow
@@ -149,10 +110,6 @@ steps:
...(cfg.channels || {}),
[config.channelId]: { enabled: true, token: "kitchen-sink-qa" },
};
cfg.tools = {
...(cfg.tools || {}),
alsoAllow: [...new Set([...(cfg.tools?.alsoAllow || []), ...config.expectedToolAny])],
};
await fs.writeFile(env.gateway.configPath, `${JSON.stringify(cfg, null, 2)}\n`, "utf8");
return env.gateway.configPath;
})()
@@ -172,7 +129,6 @@ steps:
- - plugins
- inspect
- expr: config.pluginId
- --runtime
- --json
- json: true
timeoutMs: 60000
@@ -192,22 +148,9 @@ steps:
channels: [...new Set([...(plugin.channelIds ?? []), ...(plugin.channels ?? [])])],
providers: [...new Set([...(plugin.providerIds ?? []), ...(plugin.providers ?? [])])],
tools: [...new Set([...namesFromTools, ...(contracts.tools ?? [])])],
commands: inspect.commands ?? [],
services: inspect.services ?? [],
typedHookCount: Array.isArray(inspect.typedHooks) ? inspect.typedHooks.length : 0,
hookCount: plugin.hookCount ?? 0,
surfaceIds: Object.fromEntries(
Object.keys(config.expectedSurfaceIds ?? {})
.map((field) => [field, Array.isArray(plugin[field]) ? plugin[field] : []])
),
agentHarnessIds: plugin.agentHarnessIds ?? [],
diagnostics: [...(pluginList.diagnostics ?? []), ...(inspect.diagnostics ?? [])]
.filter((entry) => entry?.level === "error")
.map((entry) => String(entry.message ?? "")),
unexpectedDiagnostics: [...new Set([...(pluginList.diagnostics ?? []), ...(inspect.diagnostics ?? [])]
.filter((entry) => entry?.level === "error")
.map((entry) => String(entry.message ?? ""))
.filter((message) => !config.expectedAdversarialDiagnostics.includes(message)))],
};
})()
- assert:
@@ -227,25 +170,9 @@ steps:
message:
expr: "`Kitchen Sink tools missing from inspect output: ${JSON.stringify(inspectFacts.tools)}`"
- assert:
expr: "Object.entries(config.expectedSurfaceIds).every(([field, expected]) => expected.some((id) => (inspectFacts.surfaceIds[field] ?? []).includes(id)))"
expr: "inspectFacts.diagnostics.length === 0"
message:
expr: "`Kitchen Sink SDK provider surface missing from inspect output: ${JSON.stringify(inspectFacts.surfaceIds)}`"
- assert:
expr: "inspectFacts.commands.includes('kitchen') && inspectFacts.services.includes('kitchen-sink-service')"
message:
expr: "`Kitchen Sink command/service surfaces missing: ${JSON.stringify({ commands: inspectFacts.commands, services: inspectFacts.services })}`"
- assert:
expr: "inspectFacts.hookCount >= 30 && inspectFacts.typedHookCount >= 30"
message:
expr: "`Kitchen Sink hook surfaces missing: ${JSON.stringify({ hookCount: inspectFacts.hookCount, typedHookCount: inspectFacts.typedHookCount })}`"
- assert:
expr: "!inspectFacts.agentHarnessIds.includes('kitchen-sink-agent-harness')"
message:
expr: "`External Kitchen Sink plugin unexpectedly registered bundled-only agent harness: ${JSON.stringify(inspectFacts.agentHarnessIds)}`"
- assert:
expr: "inspectFacts.unexpectedDiagnostics.length === 0"
message:
expr: "`Kitchen Sink conformance personality emitted unexpected diagnostics: ${JSON.stringify(inspectFacts.unexpectedDiagnostics)}`"
expr: "`Kitchen Sink conformance personality emitted diagnostics: ${JSON.stringify(inspectFacts.diagnostics)}`"
detailsExpr: inspectFacts
- name: restarts gateway with Kitchen Sink configured
@@ -281,32 +208,12 @@ steps:
...(cfg.channels || {}),
[config.channelId]: { enabled: true, token: "kitchen-sink-qa" },
};
cfg.tools = {
...(cfg.tools || {}),
alsoAllow: [...new Set([...(cfg.tools?.alsoAllow || []), ...config.expectedToolAny])],
};
await fs.writeFile(ctx.configPath, `${JSON.stringify(cfg, null, 2)}\n`, "utf8");
})()
- call: waitForGatewayHealthy
args:
- ref: env
- 120000
- call: fetchJson
saveAs: healthz
args:
- expr: "`${env.gateway.baseUrl}/healthz`"
- call: fetchJson
saveAs: readyz
args:
- expr: "`${env.gateway.baseUrl}/readyz`"
- assert:
expr: "healthz?.ok === true && healthz?.status === 'live'"
message:
expr: "`/healthz did not report live: ${JSON.stringify(healthz)}`"
- assert:
expr: "readyz?.ready === true"
message:
expr: "`/readyz did not report ready: ${JSON.stringify(readyz)}`"
- call: waitForQaChannelReady
args:
- ref: env
@@ -334,7 +241,7 @@ steps:
expr: "kitchenChannelAccount?.running === true && kitchenChannelAccount?.configured === true"
message:
expr: "`Kitchen Sink channel did not report running+configured: ${JSON.stringify(kitchenChannelAccount)}`"
detailsExpr: "{ healthz, readyz, kitchenChannelAccount }"
detailsExpr: kitchenChannelAccount
- name: exercises command inventory and MCP tool surfaces
actions:
@@ -483,7 +390,6 @@ steps:
- - plugins
- inspect
- expr: config.pluginId
- --runtime
- --json
- json: true
timeoutMs: 60000

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