Compare commits

..

21 Commits

Author SHA1 Message Date
Tideclaw
f6ac3f001b test: lazy load mcp channel package protocol 2026-06-11 03:18:31 +00:00
Tideclaw
de87ed8a4d chore: refresh alpha shrinkwrap versions 2026-06-11 02:44:03 +00:00
Tideclaw
da477e3e19 chore: prepare alpha 2026.6.7-alpha.3 2026-06-11 02:22:27 +00:00
Tideclaw
2e93bb330c test: isolate codex startup auth home
(cherry picked from commit cf743adeea)
2026-06-11 02:17:52 +00:00
Tideclaw
34fc3e88a5 test: tolerate stale mcp pairing approvals
(cherry picked from commit c3b3101890)
2026-06-11 02:17:52 +00:00
Tideclaw
939647a155 test: fix slack media mock typing
(cherry picked from commit f62d8f7472)
2026-06-11 02:17:52 +00:00
Tideclaw
98368bb241 test: stabilize slack pending media history
(cherry picked from commit cce8a14224)
2026-06-11 02:17:52 +00:00
Tideclaw
eb45f56698 test: prefer transcript runtime parity tools
(cherry picked from commit 02e5f4ad32)
2026-06-11 02:17:43 +00:00
Tideclaw
6a5034db29 test: retry codex startup temp cleanup
(cherry picked from commit 7d756f707e)
2026-06-11 02:17:06 +00:00
Tideclaw
8e3bd17f4e test: allow empty rem truth preview
(cherry picked from commit 8bd4714a75)
2026-06-11 02:17:05 +00:00
Tideclaw
6e0d469a92 test: align imessage echo cache options
(cherry picked from commit 9d9c70af0b)
2026-06-11 02:17:05 +00:00
Tideclaw
bb62fe721f test: stabilize task maintenance release shard
(cherry picked from commit f28ab293b1)
2026-06-11 02:17:05 +00:00
Tideclaw
51297fd266 test: stabilize imessage monitor release shard
(cherry picked from commit 1022e49adc)
(cherry picked from commit 9a32601492)
2026-06-11 02:17:05 +00:00
Tideclaw
7c9305e711 test: register matrix fixture for cross-agent acp spawn
(cherry picked from commit 1e74b8c081)
(cherry picked from commit ef66cd908d)
2026-06-11 02:17:05 +00:00
Tideclaw
2b8bd5f528 test: stabilize acp spawn channel fixtures
(cherry picked from commit 17a4a7f7d7)
(cherry picked from commit f149bfd196)
2026-06-11 02:17:05 +00:00
Tideclaw
49b3b737e3 test: use sqlite session stores in release fixtures
(cherry picked from commit 9afb3acff7)
(cherry picked from commit c1def265eb)
2026-06-11 02:17:05 +00:00
Tideclaw
23ad6ce609 test: seed telegram approval stores through sdk
(cherry picked from commit 6a06a36f26)
(cherry picked from commit 70e7d8bc77)
2026-06-11 02:17:05 +00:00
Tideclaw
6a93548215 test: seed remaining plugin stores through sdk
(cherry picked from commit aea32e6b6c)
(cherry picked from commit 9323c0aab4)
2026-06-11 02:17:05 +00:00
Tideclaw
34c83ea2d0 test: use valid slack session entries
(cherry picked from commit 08f4ff46da)
(cherry picked from commit 606fb0a6af)
2026-06-11 02:17:05 +00:00
Tideclaw
892946b9ce test: seed channel session stores through sdk
(cherry picked from commit 066ed9dd84)
(cherry picked from commit 3db224497b)
2026-06-11 02:17:05 +00:00
Tideclaw
a97c2eee58 fix: allow alpha focused publish proof
(cherry picked from commit 8d91342423)
(cherry picked from commit 54750b096a)
2026-06-11 02:17:05 +00:00
942 changed files with 7041 additions and 41449 deletions

View File

@@ -317,23 +317,6 @@ pnpm release:check
pnpm test:install:smoke
```
- Before tagging, diff publishable plugin package manifests against the last
reachable stable/beta release tag. For every newly publishable package
(`openclaw.release.publishToNpm: true` or `publishToClawHub: true`) whose
package name did not exist in the base tag, verify the target registry package
already exists in npm/ClawHub or stop and help the owner mint/prepublish the
package first. Do not hide or disable release surfaces just to unblock a
train unless the owner explicitly decides the plugin should not ship in that
release; first-package registry ownership is release prep, not product
rollback. The mint/prepublish path must either be the real release publish
path for the auto-bumped beta version, or a deliberately non-consuming
registry-prep step that cannot occupy the next beta version/tag. Confirm
registry owner, npm scope/package-creation permission, provenance path, and
first-package publish plan before the full release publish continues. Useful
npm probe:
`npm view <package-name> version dist-tags --json --prefer-online`; a 404 for
a package newly added to the release is a release-prep blocker, not something
to discover from the publish job.
- Use `pnpm qa:otel:smoke` when release validation needs telemetry coverage.
It starts a local OTLP/HTTP trace receiver, runs QA-lab's
`otel-trace-smoke`, and checks span names plus content/identifier redaction
@@ -579,11 +562,7 @@ node --import tsx scripts/openclaw-npm-postpublish-verify.ts <published-version>
- Use `NPM_TOKEN` only for explicit npm dist-tag management modes, because npm
does not support trusted publishing for `npm dist-tag add`.
- `@openclaw/*` plugin publishes use a separate maintainer-only flow.
- Publishable plugins that are new to npm require owner-led first-package
minting before the full release publish. Do not consume the next beta version
with an ad-hoc manual package publish; use the release-owned auto-bumped
version path, or a non-consuming registry setup/preflight step. Bundled
disk-tree-only plugins stay unpublished.
- Only publish plugins that already exist on npm; bundled disk-tree-only plugins stay unpublished.
## Fallback local mac publish
@@ -640,9 +619,7 @@ node --import tsx scripts/openclaw-npm-postpublish-verify.ts <published-version>
mac app, signing, notarization, and appcast path.
12. Confirm the target npm version is not already published.
13. Create and push the git tag from the release branch.
14. Do not create or publish the matching GitHub release page yet. The real
publish workflow creates or undrafts it only after postpublish verification
and release evidence upload pass.
14. Create or refresh the matching GitHub release.
15. Dispatch Actions > `QA-Lab - All Lanes` against the release tag and wait
for the mock parity, live Matrix, and live Telegram credentialed-channel
lanes to pass.
@@ -665,29 +642,20 @@ node --import tsx scripts/openclaw-npm-postpublish-verify.ts <published-version>
with `preflight_only=true` and wait for it to pass. Save that run id because
the real publish requires it to reuse the notarized mac artifacts.
21. If any preflight or validation run fails, fix the issue on a new commit,
delete the tag and any accidental draft/incomplete GitHub release, recreate
the tag from the fixed commit, and rerun all relevant preflights from
scratch before continuing. Never reuse old preflight results after the
commit changes. Once the npm version exists, do not rerun the publish
workflow for that same version; finalize the existing draft/evidence state
manually or cut a correction tag. For pushed or published beta tags, do not
delete/recreate; increment to the next beta tag. For preflight-only failures
where npm did not publish the beta version, delete/recreate the same beta
tag and any accidental draft/incomplete prerelease at the fixed commit
instead of skipping a prerelease number.
delete the tag and matching GitHub release, recreate them from the fixed
commit, and rerun all relevant preflights from scratch before continuing.
Never reuse old preflight results after the commit changes. For pushed or
published beta tags, do not delete/recreate; increment to the next beta tag.
For preflight-only failures where npm did not publish the beta version,
delete/recreate the same beta tag and prerelease at the fixed commit instead
of skipping a prerelease number.
22. Start `.github/workflows/openclaw-npm-release.yml` from the same branch with
the same tag for the real publish, choose `npm_dist_tag` (`beta` default,
`latest` only when you intentionally want direct stable publish), keep it
the same as the preflight run, and pass the successful npm
`preflight_run_id`.
23. Wait for `npm-release` approval from `@openclaw/openclaw-release-managers`.
24. Wait for the real publish workflow to run postpublish verification,
create or update the GitHub release as a draft, upload dependency evidence,
append release verification proof, and only then undraft/publish it. If a
waited plugin publish fails after OpenClaw npm succeeds, the workflow keeps
the release draft with OpenClaw npm evidence and exits red; do not undraft
until the plugin publish gap is repaired. The standalone verifier command
remains the recovery probe:
24. Run postpublish verification:
`node --import tsx scripts/openclaw-npm-postpublish-verify.ts <published-version>`.
25. Run the post-published beta verification roster. First scan current `main`
for critical fixes that landed after the release branch cut; backport only

View File

@@ -1,61 +0,0 @@
name: openclaw-codeql-process-exec-boundary-critical-security
disable-default-queries: true
queries:
- uses: security-extended
query-filters:
- include:
precision:
- high
- very-high
tags contain: security
security-severity: /([7-9]|10)\.(\d)+/
paths:
- src/process
- src/tui/tui-local-shell.ts
- src/tui/tui.ts
- src/plugin-sdk/windows-spawn.ts
- packages/agent-core/src/harness/env
- packages/memory-host-sdk/src/host
- extensions/acpx/src
- extensions/bonjour/src/advertiser.ts
- extensions/browser/src/browser/chrome-mcp.ts
- extensions/browser/src/browser/chrome.executables.ts
- extensions/browser/src/browser/chrome.ts
- extensions/codex/src/app-server/sandbox-exec-server
- extensions/codex/src/app-server/transport-stdio.ts
- extensions/codex/src/node-cli-sessions.ts
- extensions/codex-supervisor/src/json-rpc-client.ts
- extensions/file-transfer/src
- extensions/google-meet/src
- extensions/imessage/src
- extensions/memory-core/src/memory/qmd-manager.ts
- extensions/memory-wiki/src/obsidian.ts
- extensions/microsoft-foundry/cli.ts
- extensions/ollama/src/wsl2-crash-loop-check.ts
- extensions/qa-lab/src
- extensions/signal/src/daemon.ts
- extensions/tts-local-cli/speech-provider.ts
- extensions/voice-call/src
- scripts
paths-ignore:
- "**/node_modules"
- "**/coverage"
- "**/*.generated.ts"
- "**/*.bundle.js"
- "**/*-runtime.js"
- "**/*.test.ts"
- "**/*.test.tsx"
- "**/*.spec.ts"
- "**/*.spec.tsx"
- "**/*.e2e.test.ts"
- "**/*.e2e.test.tsx"
- "**/*test-support*"
- "**/*test-helper*"
- "**/*mock*"
- "**/*fixture*"
- "**/*bench*"

View File

@@ -17,28 +17,7 @@ on:
- ".github/actions/**"
- ".github/codeql/**"
- ".github/workflows/**"
- "extensions/acpx/src/**"
- "extensions/bonjour/src/advertiser.ts"
- "extensions/browser/src/browser/chrome-mcp.ts"
- "extensions/browser/src/browser/chrome.executables.ts"
- "extensions/browser/src/browser/chrome.ts"
- "extensions/codex/src/app-server/sandbox-exec-server/**"
- "extensions/codex/src/app-server/transport-stdio.ts"
- "extensions/codex/src/node-cli-sessions.ts"
- "extensions/codex-supervisor/src/json-rpc-client.ts"
- "extensions/file-transfer/src/**"
- "extensions/google-meet/src/**"
- "extensions/imessage/src/**"
- "extensions/memory-core/src/memory/qmd-manager.ts"
- "extensions/memory-wiki/src/obsidian.ts"
- "extensions/microsoft-foundry/cli.ts"
- "extensions/ollama/src/wsl2-crash-loop-check.ts"
- "extensions/qa-lab/src/**"
- "extensions/signal/src/daemon.ts"
- "extensions/tts-local-cli/speech-provider.ts"
- "extensions/voice-call/src/**"
- "packages/**"
- "scripts/**"
- "src/**"
push:
branches:
@@ -47,28 +26,7 @@ on:
- ".github/actions/**"
- ".github/codeql/**"
- ".github/workflows/**"
- "extensions/acpx/src/**"
- "extensions/bonjour/src/advertiser.ts"
- "extensions/browser/src/browser/chrome-mcp.ts"
- "extensions/browser/src/browser/chrome.executables.ts"
- "extensions/browser/src/browser/chrome.ts"
- "extensions/codex/src/app-server/sandbox-exec-server/**"
- "extensions/codex/src/app-server/transport-stdio.ts"
- "extensions/codex/src/node-cli-sessions.ts"
- "extensions/codex-supervisor/src/json-rpc-client.ts"
- "extensions/file-transfer/src/**"
- "extensions/google-meet/src/**"
- "extensions/imessage/src/**"
- "extensions/memory-core/src/memory/qmd-manager.ts"
- "extensions/memory-wiki/src/obsidian.ts"
- "extensions/microsoft-foundry/cli.ts"
- "extensions/ollama/src/wsl2-crash-loop-check.ts"
- "extensions/qa-lab/src/**"
- "extensions/signal/src/daemon.ts"
- "extensions/tts-local-cli/speech-provider.ts"
- "extensions/voice-call/src/**"
- "packages/**"
- "scripts/**"
- "src/**"
schedule:
- cron: "0 6 * * *"
@@ -115,11 +73,6 @@ jobs:
runs_on: blacksmith-4vcpu-ubuntu-2404
timeout_minutes: 25
config_file: ./.github/codeql/codeql-mcp-process-tool-boundary-critical-security.yml
- language: javascript-typescript
category: process-exec-boundary
runs_on: blacksmith-4vcpu-ubuntu-2404
timeout_minutes: 25
config_file: ./.github/codeql/codeql-process-exec-boundary-critical-security.yml
- language: javascript-typescript
category: plugin-trust-boundary
runs_on: blacksmith-4vcpu-ubuntu-2404

View File

@@ -437,17 +437,8 @@ jobs:
echo "::warning::Could not generate motion-trimmed desktop previews; continuing with screenshots and full MP4 links."
fi
read_discord_status_reaction_status() {
local lane="$1"
if [[ -f "$root/$lane/qa-evidence.json" ]]; then
jq -r '.entries[0].result.status' "$root/$lane/qa-evidence.json"
return
fi
jq -r '.scenarios[0].status' "$root/$lane/discord-qa-summary.json"
}
baseline_status="$(read_discord_status_reaction_status baseline)"
candidate_status="$(read_discord_status_reaction_status candidate)"
baseline_status="$(jq -r '.scenarios[0].status' "$root/baseline/discord-qa-summary.json")"
candidate_status="$(jq -r '.scenarios[0].status' "$root/candidate/discord-qa-summary.json")"
jq -n \
--arg baseline_status "$baseline_status" \

View File

@@ -451,17 +451,8 @@ jobs:
capture_candidate_discord_web
read_discord_thread_attachment_status() {
local lane="$1"
if [[ -f "$root/$lane/qa-evidence.json" ]]; then
jq -r '.entries[] | select(.test.id == "discord-thread-reply-filepath-attachment") | .result.status' "$root/$lane/qa-evidence.json"
return
fi
jq -r '.scenarios[] | select(.id == "discord-thread-reply-filepath-attachment") | .status' "$root/$lane/discord-qa-summary.json"
}
baseline_status="$(read_discord_thread_attachment_status baseline)"
candidate_status="$(read_discord_thread_attachment_status candidate)"
baseline_status="$(jq -r '.scenarios[] | select(.id == "discord-thread-reply-filepath-attachment") | .status' "$root/baseline/discord-qa-summary.json")"
candidate_status="$(jq -r '.scenarios[] | select(.id == "discord-thread-reply-filepath-attachment") | .status' "$root/candidate/discord-qa-summary.json")"
comparison_status="fail"
if [[ "$baseline_status" == "fail" && "$candidate_status" == "pass" ]]; then
comparison_status="pass"

View File

@@ -445,8 +445,8 @@ jobs:
telegram_exit=$?
set -e
if [[ ! -f "$root/qa-evidence.json" && ! -f "$root/telegram-qa-summary.json" ]]; then
echo "Telegram live QA did not produce an evidence summary." >&2
if [[ ! -f "$root/telegram-qa-summary.json" ]]; then
echo "Telegram live QA did not produce a summary." >&2
exit "$telegram_exit"
fi
echo "telegram_exit=${telegram_exit}" >> "$GITHUB_OUTPUT"

View File

@@ -1748,7 +1748,6 @@ jobs:
anthropic) require_any Anthropic ANTHROPIC_API_KEY ANTHROPIC_API_KEY_OLD ANTHROPIC_API_TOKEN ;;
google) require_any Google GEMINI_API_KEY GOOGLE_API_KEY ;;
minimax) require_any MiniMax MINIMAX_API_KEY ;;
moonshot) require_any Moonshot MOONSHOT_API_KEY KIMI_API_KEY ;;
openai) require_any OpenAI OPENAI_API_KEY ;;
opencode-go) require_any OpenCode OPENCODE_API_KEY OPENCODE_ZEN_API_KEY ;;
openrouter) require_any OpenRouter OPENROUTER_API_KEY ;;
@@ -1837,7 +1836,7 @@ jobs:
run: |
set -euo pipefail
all_providers=(anthropic google minimax moonshot openai opencode-go openrouter xai zai fireworks)
all_providers=(anthropic google minimax openai opencode-go openrouter xai zai fireworks)
normalize_provider() {
local value="${1,,}"
@@ -1923,7 +1922,6 @@ jobs:
anthropic) require_any Anthropic ANTHROPIC_API_KEY ANTHROPIC_API_KEY_OLD ANTHROPIC_API_TOKEN ;;
google) require_any Google GEMINI_API_KEY GOOGLE_API_KEY ;;
minimax) require_any MiniMax MINIMAX_API_KEY ;;
moonshot) require_any Moonshot MOONSHOT_API_KEY KIMI_API_KEY ;;
openai) require_any OpenAI OPENAI_API_KEY ;;
opencode-go) require_any OpenCode OPENCODE_API_KEY OPENCODE_ZEN_API_KEY ;;
openrouter) require_any OpenRouter OPENROUTER_API_KEY ;;

View File

@@ -661,6 +661,10 @@ jobs:
- name: Verify full release validation target
if: ${{ inputs.full_release_validation_run_id != '' }}
env:
RELEASE_TAG: ${{ inputs.tag }}
RELEASE_NPM_DIST_TAG: ${{ inputs.npm_dist_tag }}
WORKFLOW_REF_NAME: ${{ github.ref_name }}
run: |
set -euo pipefail
EXPECTED_RELEASE_SHA="$(git rev-parse HEAD)"
@@ -681,7 +685,11 @@ jobs:
echo "Full release validation target SHA mismatch: expected $EXPECTED_RELEASE_SHA, got $TARGET_SHA" >&2
exit 1
fi
if [[ "$RERUN_GROUP" != "all" ]]; then
tideclaw_alpha_focused_validation=false
if [[ "$RERUN_GROUP" == "install-smoke" && "$RELEASE_TAG" == *"-alpha."* && "$RELEASE_NPM_DIST_TAG" == "alpha" && "$WORKFLOW_REF_NAME" =~ ^tideclaw/alpha/[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{4}Z$ ]]; then
tideclaw_alpha_focused_validation=true
fi
if [[ "$RERUN_GROUP" != "all" && "$tideclaw_alpha_focused_validation" != "true" ]]; then
echo "Full release validation must run rerun_group=all before npm publish; got $RERUN_GROUP" >&2
exit 1
fi

View File

@@ -527,13 +527,6 @@ jobs:
cleanup_gateway
trap - EXIT
if node -e "const fs=require('node:fs'); const scripts=require('./package.json').scripts||{}; process.exit(scripts['test:sqlite:perf:smoke'] && fs.existsSync('scripts/bench-sqlite-state.ts') ? 0 : 1)"; then
pnpm test:sqlite:perf:smoke
cp .artifacts/sqlite-perf/smoke.json "$SOURCE_PERF_DIR/sqlite-perf-smoke.json"
else
echo "SQLite state smoke probe is not available in ${TESTED_REF}; continuing with the remaining source probes." >> "$GITHUB_STEP_SUMMARY"
fi
summary_args=(node "$PERFORMANCE_HELPER_DIR/scripts/openclaw-performance-source-summary.mjs" \
--source-dir "$SOURCE_PERF_DIR" \
--output "$SOURCE_PERF_DIR/index.md")
@@ -611,7 +604,7 @@ jobs:
## Source probes
Additional gateway boot, memory, plugin pressure, mock hello-loop, CLI startup, and SQLite state smoke numbers are in [source/index.md](source/index.md).
Additional gateway boot, memory, plugin pressure, mock hello-loop, and CLI startup numbers are in [source/index.md](source/index.md).
EOF
fi
fi

View File

@@ -268,6 +268,8 @@ jobs:
EXPECTED_SHA: ${{ steps.ref.outputs.sha }}
EXPECTED_RELEASE_PROFILE: ${{ inputs.release_profile }}
EXPECTED_WORKFLOW_BRANCH: ${{ github.ref_name }}
RELEASE_TAG: ${{ inputs.tag }}
RELEASE_NPM_DIST_TAG: ${{ inputs.npm_dist_tag }}
run: |
set -euo pipefail
RUN_JSON="$(gh run view "$FULL_RELEASE_VALIDATION_RUN_ID" --repo "$GITHUB_REPOSITORY" --json workflowName,headBranch,event,status,conclusion,url)"
@@ -295,7 +297,11 @@ jobs:
echo "Full release validation profile mismatch: expected $EXPECTED_RELEASE_PROFILE, got $release_profile" >&2
exit 1
fi
if [[ "$rerun_group" != "all" ]]; then
tideclaw_alpha_focused_validation=false
if [[ "$rerun_group" == "install-smoke" && "$RELEASE_TAG" == *"-alpha."* && "$RELEASE_NPM_DIST_TAG" == "alpha" && "$EXPECTED_WORKFLOW_BRANCH" =~ ^tideclaw/alpha/[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{4}Z$ ]]; then
tideclaw_alpha_focused_validation=true
fi
if [[ "$rerun_group" != "all" && "$tideclaw_alpha_focused_validation" != "true" ]]; then
echo "Full release validation must run rerun_group=all before npm publish; got $rerun_group" >&2
exit 1
fi
@@ -387,9 +393,7 @@ jobs:
run: |
set -euo pipefail
dispatch_workflow_at_ref() {
local workflow_ref="$1"
shift
dispatch_workflow() {
local workflow="$1"
shift
@@ -399,7 +403,7 @@ jobs:
-F per_page=100 \
--jq '[.workflow_runs[].id]')"
dispatch_output="$(gh workflow run --repo "$GITHUB_REPOSITORY" "$workflow" --ref "$workflow_ref" "$@" 2>&1)"
dispatch_output="$(gh workflow run --repo "$GITHUB_REPOSITORY" "$workflow" --ref "$CHILD_WORKFLOW_REF" "$@" 2>&1)"
printf '%s\n' "$dispatch_output" >&2
run_id="$(
printf '%s\n' "$dispatch_output" |
@@ -434,10 +438,6 @@ jobs:
printf '%s\n' "${run_id}"
}
dispatch_workflow() {
dispatch_workflow_at_ref "$CHILD_WORKFLOW_REF" "$@"
}
print_pending_deployments() {
local workflow="$1"
local run_id="$2"
@@ -659,128 +659,6 @@ jobs:
done
}
guard_existing_public_release() {
local release_version asset_name release_json is_draft has_sha has_proof has_asset release_url
if [[ "${PUBLISH_OPENCLAW_NPM}" != "true" ]]; then
return 0
fi
if ! release_json="$(gh release view "${RELEASE_TAG}" --repo "$GITHUB_REPOSITORY" --json isDraft,assets,body,url 2>/dev/null)"; then
return 0
fi
is_draft="$(printf '%s' "${release_json}" | jq -r '.isDraft')"
if [[ "${is_draft}" == "true" ]]; then
return 0
fi
release_version="${RELEASE_TAG#v}"
asset_name="openclaw-${release_version}-dependency-evidence.zip"
has_sha="$(printf '%s' "${release_json}" | jq --arg sha "${TARGET_SHA}" -r '.body | contains($sha)')"
has_proof="$(printf '%s' "${release_json}" | jq -r '.body | contains("### Release verification")')"
has_asset="$(printf '%s' "${release_json}" | jq --arg name "${asset_name}" -r 'any(.assets[]?; .name == $name)')"
release_url="$(printf '%s' "${release_json}" | jq -r '.url')"
if [[ "${has_sha}" == "true" && "${has_proof}" == "true" && "${has_asset}" == "true" ]]; then
return 0
fi
{
echo "Release ${RELEASE_TAG} already has a public GitHub release page without complete postpublish evidence for ${TARGET_SHA}."
echo "Refusing to reuse a public prerelease tag after publication started: ${release_url}"
echo "Create a new beta tag or delete/draft the incomplete public release before retrying."
} >&2
exit 1
}
guard_openclaw_npm_not_already_published() {
local release_version release_url
if [[ "${PUBLISH_OPENCLAW_NPM}" != "true" ]]; then
return 0
fi
release_version="${RELEASE_TAG#v}"
if ! npm view "openclaw@${release_version}" version >/dev/null 2>&1; then
return 0
fi
release_url="https://github.com/${GITHUB_REPOSITORY}/releases/tag/${RELEASE_TAG}"
{
echo "openclaw@${release_version} is already published on npm."
echo "Refusing to dispatch publish child workflows for an already-published version."
echo "If this is recovery from a failed postpublish evidence or draft-release step, repair/finalize the existing draft or create a correction tag; do not rerun the publish workflow for the same npm version."
echo "Release page, if present: ${release_url}"
} >&2
exit 1
}
resolve_clawhub_release_plan() {
local -a plan_args
clawhub_plan_path="${RUNNER_TEMP}/openclaw-release-clawhub-plan.json"
plan_args=(
--release-tag "${RELEASE_TAG}"
--release-publish-branch "${CHILD_WORKFLOW_REF}"
--release-publish-run-id "${GITHUB_RUN_ID}"
--plugin-publish-scope "${PLUGIN_PUBLISH_SCOPE}"
)
if [[ -n "${PLUGINS// }" ]]; then
plan_args+=(--plugins "${PLUGINS}")
fi
CLAWHUB_REGISTRY="${CLAWHUB_REGISTRY:-https://clawhub.ai}" \
node --import tsx scripts/openclaw-release-clawhub-plan.ts "${plan_args[@]}" > "${clawhub_plan_path}"
echo "Resolved OpenClaw release ClawHub dispatch plan:"
cat "${clawhub_plan_path}"
clawhub_workflow_ref="$(jq -r '.clawHubWorkflowRef' "${clawhub_plan_path}")"
normal_plugins="$(jq -r '.summary.normalPlugins' "${clawhub_plan_path}")"
bootstrap_plugins="$(jq -r '.summary.bootstrapPlugins' "${clawhub_plan_path}")"
missing_trusted_plugins="$(jq -r '.summary.missingTrustedPlugins' "${clawhub_plan_path}")"
normal_plugin_count="$(jq -r '.summary.normalCount' "${clawhub_plan_path}")"
bootstrap_plugin_count="$(jq -r '.summary.bootstrapCount' "${clawhub_plan_path}")"
missing_trusted_plugin_count="$(jq -r '.summary.missingTrustedPublisherCount' "${clawhub_plan_path}")"
{
echo "### ClawHub release plan"
echo
echo "- Normal OIDC candidates: \`${normal_plugin_count}\`"
echo "- Bootstrap/repair candidates: \`${bootstrap_plugin_count}\`"
echo "- Existing-package trusted-publisher repairs: \`${missing_trusted_plugin_count}\`"
if [[ -n "${normal_plugins}" ]]; then
echo "- Normal plugins: \`${normal_plugins}\`"
fi
if [[ -n "${bootstrap_plugins}" ]]; then
echo "- Bootstrap/repair plugins: \`${bootstrap_plugins}\`"
fi
if [[ -n "${missing_trusted_plugins}" ]]; then
echo "- Trusted-publisher repair plugins: \`${missing_trusted_plugins}\`"
fi
} >> "$GITHUB_STEP_SUMMARY"
}
append_clawhub_dispatch_args() {
local target="$1"
while IFS=$'\t' read -r key value; do
clawhub_dispatch_args+=(-f "${key}=${value}")
done < <(jq -r --arg target "${target}" '.[$target].inputs | to_entries[] | [.key, .value] | @tsv' "${clawhub_plan_path}")
}
write_clawhub_runtime_state() {
local force_skip_clawhub="$1"
local output_path="$2"
node --import tsx scripts/openclaw-release-clawhub-runtime-state.ts \
--repository "${GITHUB_REPOSITORY}" \
--wait-for-clawhub "${WAIT_FOR_CLAWHUB}" \
--force-skip-clawhub "${force_skip_clawhub}" \
--normal-run-id "${plugin_clawhub_run_id:-}" \
--bootstrap-run-id "${plugin_clawhub_bootstrap_run_id:-}" \
--bootstrap-completed "${plugin_clawhub_bootstrap_completed:-false}" > "${output_path}"
}
create_or_update_github_release() {
local release_version notes_version title notes_file changelog_file latest_arg prerelease_args
release_version="${RELEASE_TAG#v}"
@@ -826,17 +704,11 @@ jobs:
else
gh release create "${RELEASE_TAG}" --repo "$GITHUB_REPOSITORY" \
--verify-tag \
--draft \
--title "${title}" \
--notes-file "${notes_file}" \
"${prerelease_args[@]}" \
"${latest_arg}"
fi
echo "- GitHub release draft: https://github.com/${GITHUB_REPOSITORY}/releases/tag/${RELEASE_TAG}" >> "$GITHUB_STEP_SUMMARY"
}
publish_github_release() {
gh release edit "${RELEASE_TAG}" --repo "$GITHUB_REPOSITORY" --draft=false
echo "- GitHub release: https://github.com/${GITHUB_REPOSITORY}/releases/tag/${RELEASE_TAG}" >> "$GITHUB_STEP_SUMMARY"
}
@@ -869,11 +741,9 @@ jobs:
}
verify_published_release() {
local release_version evidence_path skip_clawhub clawhub_runtime_state_path
local release_version evidence_path
local -a verify_args
skip_clawhub="${1:-false}"
release_version="${RELEASE_TAG#v}"
evidence_path="${POSTPUBLISH_EVIDENCE_DIR}/release-postpublish-evidence.json"
mkdir -p "${POSTPUBLISH_EVIDENCE_DIR}"
@@ -886,18 +756,16 @@ jobs:
--dist-tag "${RELEASE_NPM_DIST_TAG}"
--repo "${GITHUB_REPOSITORY}"
--workflow-ref "${CHILD_WORKFLOW_REF}"
--clawhub-workflow-ref "${clawhub_workflow_ref}"
--full-release-validation-run "${FULL_RELEASE_VALIDATION_RUN_ID}"
--plugin-npm-run "${plugin_npm_run_id}"
--openclaw-npm-run "${openclaw_npm_run_id}"
--evidence-out "${evidence_path}"
--skip-github-release
)
clawhub_runtime_state_path="${RUNNER_TEMP}/openclaw-release-clawhub-runtime-state-verify.json"
write_clawhub_runtime_state "${skip_clawhub}" "${clawhub_runtime_state_path}"
while IFS= read -r arg; do
verify_args+=("${arg}")
done < <(jq -r '.verifierArgs[]' "${clawhub_runtime_state_path}")
if [[ "${WAIT_FOR_CLAWHUB}" == "true" ]]; then
verify_args+=(--plugin-clawhub-run "${plugin_clawhub_run_id}")
else
verify_args+=(--skip-clawhub)
fi
if [[ -n "${PLUGINS// }" ]]; then
verify_args+=(--plugins "${PLUGINS}")
fi
@@ -913,7 +781,7 @@ jobs:
}
append_release_proof_to_github_release() {
local release_version body_file notes_file tarball integrity telegram_line clawhub_line clawhub_bootstrap_line clawhub_runtime_state_path
local release_version body_file notes_file tarball integrity telegram_line clawhub_line
release_version="${RELEASE_TAG#v}"
body_file="${RUNNER_TEMP}/release-body.md"
@@ -927,16 +795,16 @@ jobs:
else
telegram_line="- npm Telegram beta E2E: not supplied"
fi
clawhub_runtime_state_path="${RUNNER_TEMP}/openclaw-release-clawhub-runtime-state-proof.json"
write_clawhub_runtime_state false "${clawhub_runtime_state_path}"
clawhub_line="$(jq -r '.proofLines.normal' "${clawhub_runtime_state_path}")"
clawhub_bootstrap_line="$(jq -r '.proofLines.bootstrap' "${clawhub_runtime_state_path}")"
if [[ "${WAIT_FOR_CLAWHUB}" == "true" ]]; then
clawhub_line="- plugin ClawHub publish: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${plugin_clawhub_run_id}"
else
clawhub_line="- plugin ClawHub publish: dispatched separately, not awaited by this proof: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${plugin_clawhub_run_id}"
fi
RELEASE_BODY_FILE="${body_file}" \
RELEASE_NOTES_FILE="${notes_file}" \
RELEASE_VERSION="${release_version}" \
RELEASE_TAG="${RELEASE_TAG}" \
RELEASE_SHA="${TARGET_SHA}" \
RELEASE_REPO="${GITHUB_REPOSITORY}" \
RELEASE_TARBALL="${tarball}" \
RELEASE_INTEGRITY="${integrity}" \
@@ -946,7 +814,6 @@ jobs:
PLUGIN_NPM_RUN_ID="${plugin_npm_run_id}" \
OPENCLAW_NPM_RUN_ID="${openclaw_npm_run_id}" \
CLAWHUB_LINE="${clawhub_line}" \
CLAWHUB_BOOTSTRAP_LINE="${clawhub_bootstrap_line}" \
TELEGRAM_LINE="${telegram_line}" \
node --input-type=module <<'NODE'
import { readFileSync, writeFileSync } from "node:fs";
@@ -964,14 +831,12 @@ jobs:
`- npm package: https://www.npmjs.com/package/openclaw/v/${process.env.RELEASE_VERSION}`,
`- registry tarball: ${process.env.RELEASE_TARBALL}`,
`- integrity: \`${process.env.RELEASE_INTEGRITY}\``,
`- release SHA: \`${process.env.RELEASE_SHA}\``,
`- full release CI report: https://github.com/openclaw/releases/blob/main/evidence/${process.env.RELEASE_VERSION}/release-evidence.md`,
`- release publish: https://github.com/${process.env.RELEASE_REPO}/actions/runs/${process.env.RELEASE_PUBLISH_RUN_ID}`,
`- npm preflight: https://github.com/${process.env.RELEASE_REPO}/actions/runs/${process.env.PREFLIGHT_RUN_ID}`,
`- full release validation: https://github.com/${process.env.RELEASE_REPO}/actions/runs/${process.env.FULL_RELEASE_VALIDATION_RUN_ID}`,
`- plugin npm publish: https://github.com/${process.env.RELEASE_REPO}/actions/runs/${process.env.PLUGIN_NPM_RUN_ID}`,
process.env.CLAWHUB_LINE,
process.env.CLAWHUB_BOOTSTRAP_LINE,
`- OpenClaw npm publish: https://github.com/${process.env.RELEASE_REPO}/actions/runs/${process.env.OPENCLAW_NPM_RUN_ID}`,
process.env.TELEGRAM_LINE,
].join("\n");
@@ -988,7 +853,6 @@ jobs:
echo "### Publish sequence"
echo
echo "- Workflow ref: \`${CHILD_WORKFLOW_REF}\`"
echo "- ClawHub workflow ref: release tag \`${RELEASE_TAG}\`"
echo "- Release tag: \`${RELEASE_TAG}\`"
echo "- Release SHA: \`${TARGET_SHA}\`"
echo "- Release approval: this workflow job"
@@ -1005,68 +869,26 @@ jobs:
fi
} >> "$GITHUB_STEP_SUMMARY"
guard_existing_public_release
guard_openclaw_npm_not_already_published
resolve_clawhub_release_plan
npm_args=(-f publish_scope="${PLUGIN_PUBLISH_SCOPE}" -f ref="${TARGET_SHA}" -f release_publish_run_id="${GITHUB_RUN_ID}")
clawhub_args=(-f publish_scope="${PLUGIN_PUBLISH_SCOPE}" -f ref="${TARGET_SHA}" -f release_publish_run_id="${GITHUB_RUN_ID}")
if [[ -n "${PLUGINS}" ]]; then
npm_args+=(-f plugins="${PLUGINS}")
clawhub_args+=(-f plugins="${PLUGINS}")
fi
plugin_npm_run_id="$(dispatch_workflow plugin-npm-release.yml "${npm_args[@]}")"
plugin_clawhub_run_id=""
if [[ "$(jq -r '.normal.shouldDispatch' "${clawhub_plan_path}")" == "true" ]]; then
clawhub_dispatch_args=()
append_clawhub_dispatch_args normal
plugin_clawhub_run_id="$(dispatch_workflow_at_ref \
"$(jq -r '.normal.ref' "${clawhub_plan_path}")" \
"$(jq -r '.normal.workflow' "${clawhub_plan_path}")" \
"${clawhub_dispatch_args[@]}")"
else
echo "- plugin-clawhub-release.yml: no normal OIDC candidates" >> "$GITHUB_STEP_SUMMARY"
fi
plugin_clawhub_bootstrap_run_id=""
plugin_clawhub_bootstrap_completed="false"
if [[ "$(jq -r '.bootstrap.shouldDispatch' "${clawhub_plan_path}")" == "true" ]]; then
clawhub_dispatch_args=()
append_clawhub_dispatch_args bootstrap
plugin_clawhub_bootstrap_run_id="$(dispatch_workflow_at_ref \
"$(jq -r '.bootstrap.ref' "${clawhub_plan_path}")" \
"$(jq -r '.bootstrap.workflow' "${clawhub_plan_path}")" \
"${clawhub_dispatch_args[@]}")"
else
echo "- plugin-clawhub-new.yml: no bootstrap candidates" >> "$GITHUB_STEP_SUMMARY"
fi
plugin_clawhub_run_id="$(dispatch_workflow plugin-clawhub-release.yml "${clawhub_args[@]}")"
{
echo "- Plugin npm run ID: \`${plugin_npm_run_id}\`"
echo "- Plugin ClawHub run ID: \`${plugin_clawhub_run_id:-none}\`"
echo "- Plugin ClawHub bootstrap run ID: \`${plugin_clawhub_bootstrap_run_id:-none}\`"
echo "- Plugin ClawHub run ID: \`${plugin_clawhub_run_id}\`"
} >> "$GITHUB_STEP_SUMMARY"
if ! wait_for_run plugin-npm-release.yml "${plugin_npm_run_id}"; then
echo "Plugin npm publish failed; cancelling dispatched ClawHub child workflows." >&2
if [[ -n "${plugin_clawhub_run_id}" ]]; then
gh run cancel --repo "$GITHUB_REPOSITORY" "${plugin_clawhub_run_id}" >/dev/null 2>&1 || true
fi
if [[ -n "${plugin_clawhub_bootstrap_run_id}" ]]; then
gh run cancel --repo "$GITHUB_REPOSITORY" "${plugin_clawhub_bootstrap_run_id}" >/dev/null 2>&1 || true
fi
echo "Plugin npm publish failed; cancelling ClawHub publish child ${plugin_clawhub_run_id}." >&2
gh run cancel --repo "$GITHUB_REPOSITORY" "${plugin_clawhub_run_id}" >/dev/null 2>&1 || true
exit 1
fi
if [[ -n "${plugin_clawhub_bootstrap_run_id}" && "${WAIT_FOR_CLAWHUB}" == "true" ]]; then
echo "Waiting for plugin-clawhub-new.yml bootstrap to finish before continuing release publish."
if wait_for_run plugin-clawhub-new.yml "${plugin_clawhub_bootstrap_run_id}"; then
plugin_clawhub_bootstrap_completed="true"
else
if [[ -n "${plugin_clawhub_run_id}" ]]; then
gh run cancel --repo "$GITHUB_REPOSITORY" "${plugin_clawhub_run_id}" >/dev/null 2>&1 || true
fi
exit 1
fi
fi
openclaw_npm_run_id=""
if [[ "${PUBLISH_OPENCLAW_NPM}" == "true" ]]; then
openclaw_npm_run_id="$(dispatch_workflow openclaw-npm-release.yml \
@@ -1083,52 +905,19 @@ jobs:
clawhub_result=""
clawhub_pid=""
clawhub_bootstrap_result=""
clawhub_bootstrap_pid=""
if [[ "${WAIT_FOR_CLAWHUB}" == "true" ]]; then
if [[ -n "${plugin_clawhub_run_id}" ]]; then
clawhub_result="$RUNNER_TEMP/clawhub-result.txt"
wait_run_pid=""
wait_for_run_background plugin-clawhub-release.yml "${plugin_clawhub_run_id}" "${clawhub_result}"
clawhub_pid="${wait_run_pid}"
fi
if [[ -n "${plugin_clawhub_bootstrap_run_id}" ]]; then
if [[ "${plugin_clawhub_bootstrap_completed}" == "true" ]]; then
echo "- plugin-clawhub-new.yml: bootstrap already completed before continuing" >> "$GITHUB_STEP_SUMMARY"
else
clawhub_bootstrap_result="$RUNNER_TEMP/clawhub-bootstrap-result.txt"
wait_run_pid=""
wait_for_run_background plugin-clawhub-new.yml "${plugin_clawhub_bootstrap_run_id}" "${clawhub_bootstrap_result}"
clawhub_bootstrap_pid="${wait_run_pid}"
fi
fi
clawhub_result="$RUNNER_TEMP/clawhub-result.txt"
wait_run_pid=""
wait_for_run_background plugin-clawhub-release.yml "${plugin_clawhub_run_id}" "${clawhub_result}"
clawhub_pid="${wait_run_pid}"
else
if [[ -n "${plugin_clawhub_run_id}" ]]; then
wait_for_job_success plugin-clawhub-release.yml "${plugin_clawhub_run_id}" "Validate release publish approval"
if approve_child_publish_environment plugin-clawhub-release.yml "${plugin_clawhub_run_id}"; then
:
else
echo "- plugin-clawhub-release.yml: child environment gate not ready; publish was left dispatched (${plugin_clawhub_run_id})" >> "$GITHUB_STEP_SUMMARY"
fi
echo "- plugin-clawhub-release.yml: publish not awaited (${plugin_clawhub_run_id})" >> "$GITHUB_STEP_SUMMARY"
wait_for_job_success plugin-clawhub-release.yml "${plugin_clawhub_run_id}" "Validate release publish approval"
if approve_child_publish_environment plugin-clawhub-release.yml "${plugin_clawhub_run_id}"; then
:
else
echo "- plugin-clawhub-release.yml: no normal OIDC publish to await" >> "$GITHUB_STEP_SUMMARY"
fi
if [[ -n "${plugin_clawhub_bootstrap_run_id}" ]]; then
if [[ "${plugin_clawhub_bootstrap_completed}" == "true" ]]; then
echo "- plugin-clawhub-new.yml: bootstrap already completed before continuing" >> "$GITHUB_STEP_SUMMARY"
else
wait_for_job_success plugin-clawhub-new.yml "${plugin_clawhub_bootstrap_run_id}" "Validate release publish approval"
if approve_child_publish_environment plugin-clawhub-new.yml "${plugin_clawhub_bootstrap_run_id}"; then
:
else
echo "- plugin-clawhub-new.yml: child environment gate not ready; bootstrap was left dispatched (${plugin_clawhub_bootstrap_run_id})" >> "$GITHUB_STEP_SUMMARY"
fi
echo "- plugin-clawhub-new.yml: bootstrap not awaited (${plugin_clawhub_bootstrap_run_id})" >> "$GITHUB_STEP_SUMMARY"
fi
else
echo "- plugin-clawhub-new.yml: no bootstrap publish to await" >> "$GITHUB_STEP_SUMMARY"
echo "- plugin-clawhub-release.yml: child environment gate not ready; publish was left dispatched (${plugin_clawhub_run_id})" >> "$GITHUB_STEP_SUMMARY"
fi
echo "- plugin-clawhub-release.yml: publish not awaited (${plugin_clawhub_run_id})" >> "$GITHUB_STEP_SUMMARY"
fi
openclaw_result=""
@@ -1151,33 +940,21 @@ jobs:
openclaw_failed=1
fi
if [[ -n "${openclaw_npm_run_id}" && "${openclaw_failed}" == "0" ]]; then
create_or_update_github_release
upload_dependency_evidence_release_asset
fi
if [[ -n "${clawhub_pid}" ]] && ! wait "${clawhub_pid}"; then
failed=1
fi
if [[ -f "${clawhub_result}" && "$(cat "${clawhub_result}")" != "success" ]]; then
failed=1
fi
if [[ -n "${clawhub_bootstrap_pid}" ]] && ! wait "${clawhub_bootstrap_pid}"; then
failed=1
fi
if [[ -f "${clawhub_bootstrap_result}" && "$(cat "${clawhub_bootstrap_result}")" != "success" ]]; then
failed=1
fi
if [[ -n "${openclaw_npm_run_id}" && "${openclaw_failed}" == "0" ]]; then
if [[ "${failed}" == "0" ]]; then
verify_published_release
else
verify_published_release true
fi
create_or_update_github_release
upload_dependency_evidence_release_asset
if [[ "${failed}" == "0" && -n "${openclaw_npm_run_id}" ]]; then
verify_published_release
append_release_proof_to_github_release
if [[ "${failed}" == "0" ]]; then
publish_github_release
else
echo "- GitHub release: left as draft because a required publish child failed" >> "$GITHUB_STEP_SUMMARY"
fi
fi
if [[ "${failed}" != "0" ]]; then
exit 1

View File

@@ -1,504 +0,0 @@
name: Plugin ClawHub New
on:
workflow_dispatch:
inputs:
plugins:
description: Comma-separated plugin package names to bootstrap on ClawHub
required: true
type: string
ref:
description: Commit SHA on main, a release branch, or the matching Tideclaw alpha branch to publish from; defaults to the workflow ref
required: false
default: ""
type: string
release_publish_run_id:
description: Approved OpenClaw Release Publish workflow run id
required: false
type: string
release_publish_branch:
description: Branch name of the approving OpenClaw Release Publish workflow run
required: false
type: string
dry_run:
description: Validate the token-gated ClawHub bootstrap handoff without publishing.
required: false
default: false
type: boolean
concurrency:
group: plugin-clawhub-new-${{ github.event_name == 'workflow_dispatch' && inputs.ref || github.sha }}
cancel-in-progress: false
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
NODE_VERSION: "24.15.0"
CLAWHUB_REGISTRY: "https://clawhub.ai"
CLAWHUB_CLI_PACKAGE: "clawhub@0.21.0"
jobs:
resolve_bootstrap_plan:
runs-on: ubuntu-latest
permissions:
contents: read
outputs:
ref_revision: ${{ steps.ref.outputs.sha }}
has_bootstrap_candidates: ${{ steps.plan.outputs.has_bootstrap_candidates }}
bootstrap_candidate_count: ${{ steps.plan.outputs.bootstrap_candidate_count }}
matrix: ${{ steps.plan.outputs.matrix }}
steps:
- name: Checkout
uses: actions/checkout@v6
with:
persist-credentials: false
ref: ${{ github.ref }}
fetch-depth: 0
- name: Resolve checked-out ref
id: ref
env:
TARGET_REF: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || '' }}
run: |
set -euo pipefail
git fetch --no-tags origin \
+refs/heads/main:refs/remotes/origin/main \
'+refs/heads/release/*:refs/remotes/origin/release/*'
if [[ -n "${TARGET_REF}" ]]; then
if git rev-parse --verify --quiet "${TARGET_REF}^{commit}" >/dev/null; then
target_sha="$(git rev-parse "${TARGET_REF}^{commit}")"
elif git rev-parse --verify --quiet "origin/${TARGET_REF}^{commit}" >/dev/null; then
target_sha="$(git rev-parse "origin/${TARGET_REF}^{commit}")"
else
echo "Unable to resolve requested publish ref: ${TARGET_REF}" >&2
exit 1
fi
git checkout --detach "${target_sha}"
fi
echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
- name: Validate ref is on a trusted publish branch
env:
TRUSTED_PUBLISH_BRANCH: ${{ inputs.release_publish_branch || github.ref_name }}
run: |
set -euo pipefail
if git merge-base --is-ancestor HEAD origin/main; then
exit 0
fi
while IFS= read -r release_ref; do
if git merge-base --is-ancestor HEAD "${release_ref}"; then
exit 0
fi
done < <(git for-each-ref --format='%(refname)' refs/remotes/origin/release)
if [[ "${TRUSTED_PUBLISH_BRANCH}" =~ ^tideclaw/alpha/[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{4}Z$ ]]; then
alpha_branch="${TRUSTED_PUBLISH_BRANCH}"
git fetch --no-tags origin "+refs/heads/${alpha_branch}:refs/remotes/origin/${alpha_branch}"
if git merge-base --is-ancestor HEAD "refs/remotes/origin/${alpha_branch}"; then
exit 0
fi
fi
echo "Plugin ClawHub bootstraps must target a commit reachable from main, release/*, or the matching Tideclaw alpha branch." >&2
exit 1
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
install-bun: "false"
- name: Validate publishable plugin metadata
env:
RELEASE_PLUGINS: ${{ inputs.plugins }}
run: |
set -euo pipefail
if [[ -z "${RELEASE_PLUGINS// }" ]]; then
echo "Plugin ClawHub bootstrap requires at least one package name in plugins." >&2
exit 1
fi
pnpm release:plugins:clawhub:check -- --selection-mode selected --plugins "${RELEASE_PLUGINS}"
- name: Resolve plugin bootstrap plan
id: plan
env:
RELEASE_PLUGINS: ${{ inputs.plugins }}
CLAWHUB_REGISTRY: ${{ env.CLAWHUB_REGISTRY }}
run: |
set -euo pipefail
mkdir -p .local
node --import tsx scripts/plugin-clawhub-release-plan.ts \
--selection-mode selected \
--plugins "${RELEASE_PLUGINS}" > .local/plugin-clawhub-release-plan.json
cat .local/plugin-clawhub-release-plan.json
bootstrap_candidate_count="$(jq -r '(.bootstrapCandidates | length) + (.missingTrustedPublisher | length)' .local/plugin-clawhub-release-plan.json)"
selected_count="$(jq -r '.all | length' .local/plugin-clawhub-release-plan.json)"
matrix_json="$(
jq -c '
[
.bootstrapCandidates[]? + {
bootstrapMode: "publish",
requiresManualOverride: false
},
.missingTrustedPublisher[]? + {
bootstrapMode: (if .alreadyPublished then "configure-only" else "publish" end),
requiresManualOverride: true
}
]
' .local/plugin-clawhub-release-plan.json
)"
has_bootstrap_candidates="false"
if [[ "${bootstrap_candidate_count}" != "0" ]]; then
has_bootstrap_candidates="true"
fi
invalid_scope="$(
jq -r '
(.bootstrapCandidates[]?, .missingTrustedPublisher[]?)
| select(.packageName | startswith("@openclaw/") | not)
| "- \(.packageName)@\(.version)"
' .local/plugin-clawhub-release-plan.json
)"
if [[ -n "${invalid_scope}" ]]; then
echo "Plugin ClawHub bootstrap only supports @openclaw/* packages." >&2
printf '%s\n' "${invalid_scope}" >&2
exit 1
fi
not_bootstrap="$(
jq -r '
(.bootstrapCandidates | map(.packageName)) as $bootstrapNames
| (.missingTrustedPublisher | map(.packageName)) as $repairNames
| .all[]?
| select(.packageName as $name | ($bootstrapNames + $repairNames | index($name) | not))
| "- \(.packageName)@\(.version)"
' .local/plugin-clawhub-release-plan.json
)"
if [[ -n "${not_bootstrap}" ]]; then
echo "Selected packages must all be first-publish bootstrap candidates or trusted-publisher repair candidates." >&2
printf '%s\n' "${not_bootstrap}" >&2
exit 1
fi
if [[ "${selected_count}" == "0" || "${bootstrap_candidate_count}" == "0" ]]; then
echo "No selected packages require ClawHub bootstrap." >&2
exit 1
fi
{
echo "bootstrap_candidate_count=${bootstrap_candidate_count}"
echo "has_bootstrap_candidates=${has_bootstrap_candidates}"
echo "matrix=${matrix_json}"
} >> "$GITHUB_OUTPUT"
echo "ClawHub bootstrap candidates:"
jq -r '
.bootstrapCandidates[]? | "- \(.packageName)@\(.version) [\(.publishTag)] from \(.packageDir)"
' .local/plugin-clawhub-release-plan.json
echo "ClawHub trusted-publisher repair candidates:"
jq -r '
.missingTrustedPublisher[]? | "- \(.packageName)@\(.version) [\(.publishTag)] from \(.packageDir), alreadyPublished=\(.alreadyPublished)"
' .local/plugin-clawhub-release-plan.json
- name: Validate Tideclaw alpha plugin channels
env:
TRUSTED_PUBLISH_BRANCH: ${{ inputs.release_publish_branch || github.ref_name }}
run: |
set -euo pipefail
if [[ ! "${TRUSTED_PUBLISH_BRANCH}" =~ ^tideclaw/alpha/[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{4}Z$ ]]; then
exit 0
fi
invalid="$(
jq -r '
(.bootstrapCandidates[]?, .missingTrustedPublisher[]?)
| select(.publishTag != "alpha" or .channel != "alpha")
| "- \(.packageName)@\(.version) [\(.publishTag)]"
' .local/plugin-clawhub-release-plan.json
)"
if [[ -n "${invalid}" ]]; then
echo "Tideclaw alpha ClawHub bootstraps may only publish alpha plugin versions." >&2
printf '%s\n' "${invalid}" >&2
exit 1
fi
validate_release_publish_approval:
name: Validate release publish approval
needs: resolve_bootstrap_plan
if: github.event_name == 'workflow_dispatch' && needs.resolve_bootstrap_plan.outputs.has_bootstrap_candidates == 'true'
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
steps:
- name: Checkout
uses: actions/checkout@v6
with:
persist-credentials: false
- name: Validate release publish approval run
env:
GH_TOKEN: ${{ github.token }}
RELEASE_PUBLISH_RUN_ID: ${{ inputs.release_publish_run_id }}
EXPECTED_WORKFLOW_BRANCH: ${{ inputs.release_publish_branch || github.ref_name }}
run: |
set -euo pipefail
if [[ -z "${RELEASE_PUBLISH_RUN_ID// }" ]]; then
if [[ "${GITHUB_ACTOR}" == "github-actions[bot]" ]]; then
echo "Plugin ClawHub bootstrap dispatched by another workflow must include release_publish_run_id." >&2
exit 1
fi
echo "Direct Plugin ClawHub New dispatch; relying on this workflow's clawhub-plugin-bootstrap environment approval."
exit 0
fi
direct_recovery=false
if [[ "${GITHUB_ACTOR}" != "github-actions[bot]" ]]; then
direct_recovery=true
echo "Direct Plugin ClawHub New recovery with release_publish_run_id; relying on this workflow's clawhub-plugin-bootstrap environment approval."
fi
RUN_JSON="$(gh run view "$RELEASE_PUBLISH_RUN_ID" --repo "$GITHUB_REPOSITORY" --json workflowName,headBranch,event,status,conclusion,url)"
printf '%s' "$RUN_JSON" | DIRECT_RELEASE_RECOVERY="${direct_recovery}" node scripts/validate-release-publish-approval.mjs
validate_bootstrap_trusted_publisher_cli:
needs: [resolve_bootstrap_plan, validate_release_publish_approval]
if: always() && github.event_name == 'workflow_dispatch' && inputs.dry_run != true && needs.resolve_bootstrap_plan.outputs.has_bootstrap_candidates == 'true' && needs.validate_release_publish_approval.result == 'success'
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Validate pinned ClawHub trusted publisher CLI support
env:
CLAWHUB_CLI_PACKAGE: ${{ env.CLAWHUB_CLI_PACKAGE }}
run: |
set -euo pipefail
help_output="$(
npm exec --yes --package "${CLAWHUB_CLI_PACKAGE}" -- \
clawhub package trusted-publisher set --help 2>&1 || true
)"
printf '%s\n' "${help_output}"
if ! grep -Fq "Usage: clawhub package trusted-publisher set" <<<"${help_output}"; then
echo "::error::CLAW-277 03 - Split OpenClaw plugin ClawHub publishing into OIDC release and token bootstrap workflows requires ${CLAWHUB_CLI_PACKAGE} to expose 'package trusted-publisher set' before token bootstrap publish can run. The pinned CLI returned parent help or no set command, so this workflow is stopping before creating a ClawHub package row."
exit 1
fi
for required_flag in --repository --workflow-filename; do
if ! grep -Fq -- "${required_flag}" <<<"${help_output}"; then
echo "::error::CLAW-277 03 - Split OpenClaw plugin ClawHub publishing into OIDC release and token bootstrap workflows requires ${CLAWHUB_CLI_PACKAGE} trusted-publisher set help to include ${required_flag}."
exit 1
fi
done
publish_bootstrap_plugins:
needs:
[
resolve_bootstrap_plan,
validate_release_publish_approval,
validate_bootstrap_trusted_publisher_cli,
]
if: always() && github.event_name == 'workflow_dispatch' && needs.resolve_bootstrap_plan.outputs.has_bootstrap_candidates == 'true' && needs.validate_release_publish_approval.result == 'success' && (inputs.dry_run == true || needs.validate_bootstrap_trusted_publisher_cli.result == 'success')
runs-on: ubuntu-latest
environment: clawhub-plugin-bootstrap
permissions:
contents: read
strategy:
fail-fast: false
max-parallel: 8
matrix:
plugin: ${{ fromJson(needs.resolve_bootstrap_plan.outputs.matrix) }}
steps:
- name: Checkout
uses: actions/checkout@v6
with:
persist-credentials: false
ref: ${{ github.ref }}
fetch-depth: 0
- name: Checkout target revision
env:
TARGET_SHA: ${{ needs.resolve_bootstrap_plan.outputs.ref_revision }}
run: |
set -euo pipefail
git fetch --no-tags origin \
+refs/heads/main:refs/remotes/origin/main \
'+refs/heads/release/*:refs/remotes/origin/release/*'
git checkout --detach "${TARGET_SHA}"
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
install-bun: "true"
install-deps: "true"
- name: Verify package-local runtime build
run: node scripts/check-plugin-npm-runtime-builds.mjs --package "${{ matrix.plugin.packageDir }}"
- name: Install pinned ClawHub CLI wrapper
run: |
set -euo pipefail
cat > "${RUNNER_TEMP}/clawhub" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
exec npm exec --yes --package "${CLAWHUB_CLI_PACKAGE}" -- clawhub "$@"
EOF
chmod +x "${RUNNER_TEMP}/clawhub"
echo "${RUNNER_TEMP}" >> "${GITHUB_PATH}"
- name: Write ClawHub token config
if: inputs.dry_run != true
env:
CLAWHUB_REGISTRY: ${{ env.CLAWHUB_REGISTRY }}
CLAWHUB_TOKEN: ${{ secrets.CLAWHUB_TOKEN }}
run: |
set -euo pipefail
config_path="${RUNNER_TEMP}/clawhub-config.json"
CONFIG_PATH="${config_path}" node --input-type=module <<'NODE'
import { writeFileSync } from "node:fs";
const registry = process.env.CLAWHUB_REGISTRY?.trim();
const token = process.env.CLAWHUB_TOKEN?.trim();
const configPath = process.env.CONFIG_PATH;
if (!registry) {
throw new Error("CLAWHUB_REGISTRY is required for token-gated ClawHub bootstrap.");
}
if (!token) {
throw new Error("CLAWHUB_TOKEN is required for token-gated ClawHub bootstrap.");
}
if (!configPath) {
throw new Error("CONFIG_PATH is required.");
}
writeFileSync(configPath, `${JSON.stringify({ registry, token }, null, 2)}\n`, {
encoding: "utf8",
mode: 0o600,
});
NODE
echo "CLAWHUB_CONFIG_PATH=${config_path}" >> "${GITHUB_ENV}"
- name: Publish ClawHub bootstrap package
env:
CLAWHUB_REGISTRY: ${{ env.CLAWHUB_REGISTRY }}
SOURCE_REPO: ${{ github.repository }}
SOURCE_COMMIT: ${{ needs.resolve_bootstrap_plan.outputs.ref_revision }}
SOURCE_REF: ${{ github.ref }}
PACKAGE_TAG: ${{ matrix.plugin.publishTag }}
PACKAGE_DIR: ${{ matrix.plugin.packageDir }}
BOOTSTRAP_MODE: ${{ matrix.plugin.bootstrapMode }}
REQUIRES_MANUAL_OVERRIDE: ${{ matrix.plugin.requiresManualOverride && 'true' || 'false' }}
DRY_RUN: ${{ inputs.dry_run && 'true' || 'false' }}
OPENCLAW_PLUGIN_NPM_RUNTIME_BUILD: "0"
run: |
set -euo pipefail
if [[ "${BOOTSTRAP_MODE}" == "configure-only" ]]; then
echo "Skipping bootstrap publish because ${PACKAGE_DIR} version is already present on ClawHub; configuring trusted publisher only."
elif [[ "${DRY_RUN}" == "true" ]]; then
bash scripts/plugin-clawhub-publish.sh --dry-run "${PACKAGE_DIR}"
else
if [[ "${REQUIRES_MANUAL_OVERRIDE}" == "true" ]]; then
export OPENCLAW_CLAWHUB_MANUAL_OVERRIDE_REASON="GitHub Actions trusted publisher repair before OIDC migration"
fi
bash scripts/plugin-clawhub-publish.sh --publish "${PACKAGE_DIR}"
fi
- name: Configure trusted publisher for normal OIDC releases
if: inputs.dry_run != true
env:
CLAWHUB_CLI_PACKAGE: ${{ env.CLAWHUB_CLI_PACKAGE }}
PACKAGE_NAME: ${{ matrix.plugin.packageName }}
run: |
set -euo pipefail
npm exec --yes --package "${CLAWHUB_CLI_PACKAGE}" -- \
clawhub package trusted-publisher set "${PACKAGE_NAME}" \
--repository openclaw/openclaw \
--workflow-filename plugin-clawhub-release.yml
verify_bootstrap_clawhub_package:
needs: [resolve_bootstrap_plan, publish_bootstrap_plugins]
if: github.event_name == 'workflow_dispatch' && inputs.dry_run != true && needs.resolve_bootstrap_plan.outputs.has_bootstrap_candidates == 'true'
runs-on: ubuntu-latest
permissions:
contents: read
strategy:
fail-fast: false
max-parallel: 8
matrix:
plugin: ${{ fromJson(needs.resolve_bootstrap_plan.outputs.matrix) }}
steps:
- name: Verify bootstrap ClawHub package and trusted publisher
env:
CLAWHUB_REGISTRY: ${{ env.CLAWHUB_REGISTRY }}
PACKAGE_NAME: ${{ matrix.plugin.packageName }}
PACKAGE_VERSION: ${{ matrix.plugin.version }}
PACKAGE_TAG: ${{ matrix.plugin.publishTag }}
run: |
set -euo pipefail
node --input-type=module <<'EOF'
const registry = (process.env.CLAWHUB_REGISTRY ?? "https://clawhub.ai").replace(/\/+$/, "");
const packageName = process.env.PACKAGE_NAME;
const packageVersion = process.env.PACKAGE_VERSION;
const packageTag = process.env.PACKAGE_TAG;
if (!packageName || !packageVersion || !packageTag) {
throw new Error("Missing ClawHub bootstrap verification env.");
}
const encodedName = encodeURIComponent(packageName);
const encodedVersion = encodeURIComponent(packageVersion);
const detailUrl = `${registry}/api/v1/packages/${encodedName}`;
const trustedPublisherUrl = `${detailUrl}/trusted-publisher`;
const versionUrl = `${detailUrl}/versions/${encodedVersion}`;
const artifactUrl = `${versionUrl}/artifact/download`;
async function fetchWithRetry(url, options = {}) {
let lastStatus = "unknown";
for (let attempt = 1; attempt <= 12; attempt += 1) {
try {
const response = await fetch(url, { redirect: "manual", ...options });
lastStatus = response.status;
if (response.status !== 429 && response.status < 500) {
return response;
}
} catch (error) {
lastStatus = error instanceof Error ? error.message : String(error);
}
await new Promise((resolve) => setTimeout(resolve, attempt * 5000));
}
throw new Error(`${url} did not stabilize; last status ${lastStatus}.`);
}
const detailResponse = await fetchWithRetry(detailUrl, {
headers: { accept: "application/json" },
});
if (!detailResponse.ok) {
throw new Error(`${detailUrl} returned HTTP ${detailResponse.status}.`);
}
const detail = await detailResponse.json();
const tags = detail?.package?.tags ?? {};
if (tags[packageTag] !== packageVersion) {
throw new Error(
`${packageName}: ClawHub tag ${packageTag} points to ${tags[packageTag] ?? "<missing>"}, expected ${packageVersion}.`,
);
}
const trustedPublisherResponse = await fetchWithRetry(trustedPublisherUrl, {
headers: { accept: "application/json" },
});
if (!trustedPublisherResponse.ok) {
throw new Error(`${trustedPublisherUrl} returned HTTP ${trustedPublisherResponse.status}.`);
}
const trustedPublisherDetail = await trustedPublisherResponse.json();
const trustedPublisher = trustedPublisherDetail?.trustedPublisher;
if (
trustedPublisher?.repository !== "openclaw/openclaw" ||
trustedPublisher?.workflowFilename !== "plugin-clawhub-release.yml" ||
trustedPublisher?.environment != null
) {
throw new Error(
`${packageName}: trusted publisher config did not match openclaw/openclaw plugin-clawhub-release.yml without an environment pin.`,
);
}
const versionResponse = await fetchWithRetry(versionUrl);
if (!versionResponse.ok) {
throw new Error(`${versionUrl} returned HTTP ${versionResponse.status}.`);
}
const artifactResponse = await fetchWithRetry(artifactUrl, { method: "HEAD" });
if (artifactResponse.status < 200 || artifactResponse.status >= 400) {
throw new Error(`${artifactUrl} returned HTTP ${artifactResponse.status}.`);
}
console.log(`${packageName}@${packageVersion} bootstrap verified on ClawHub.`);
EOF

View File

@@ -16,7 +16,7 @@ on:
required: false
type: string
ref:
description: Dry-run target ref to validate; real OIDC publishes must dispatch the workflow with --ref set to the target release tag/ref
description: Commit SHA on main, a release branch, or the matching Tideclaw alpha branch to publish from; defaults to the workflow ref
required: false
default: ""
type: string
@@ -24,10 +24,6 @@ on:
description: Approved OpenClaw Release Publish workflow run id
required: false
type: string
release_publish_branch:
description: Branch name of the approving OpenClaw Release Publish workflow run
required: false
type: string
dry_run:
description: Validate the full ClawHub artifact handoff without publishing.
required: false
@@ -42,7 +38,9 @@ env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
NODE_VERSION: "24.15.0"
CLAWHUB_REGISTRY: "https://clawhub.ai"
CLAWHUB_CLI_PACKAGE: "clawhub@0.21.0"
CLAWHUB_REPOSITORY: "openclaw/clawhub"
# Pinned to a reviewed ClawHub commit so release behavior stays reproducible.
CLAWHUB_REF: "c9bb13023598dcc547fdf4a93b9d42512b8c8854"
jobs:
preview_plugins_clawhub:
@@ -52,15 +50,9 @@ jobs:
outputs:
ref_revision: ${{ steps.ref.outputs.sha }}
has_candidates: ${{ steps.plan.outputs.has_candidates }}
has_bootstrap_candidates: ${{ steps.plan.outputs.has_bootstrap_candidates }}
has_missing_trusted_publisher: ${{ steps.plan.outputs.has_missing_trusted_publisher }}
candidate_count: ${{ steps.plan.outputs.candidate_count }}
bootstrap_candidate_count: ${{ steps.plan.outputs.bootstrap_candidate_count }}
missing_trusted_publisher_count: ${{ steps.plan.outputs.missing_trusted_publisher_count }}
skipped_published_count: ${{ steps.plan.outputs.skipped_published_count }}
matrix: ${{ steps.plan.outputs.matrix }}
bootstrap_matrix: ${{ steps.plan.outputs.bootstrap_matrix }}
missing_trusted_publisher_matrix: ${{ steps.plan.outputs.missing_trusted_publisher_matrix }}
steps:
- name: Checkout
uses: actions/checkout@v6
@@ -91,27 +83,9 @@ jobs:
fi
echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
- name: Validate OIDC source matches workflow ref
env:
TARGET_SHA: ${{ steps.ref.outputs.sha }}
WORKFLOW_SHA: ${{ github.sha }}
DRY_RUN: ${{ inputs.dry_run && 'true' || 'false' }}
run: |
set -euo pipefail
if [[ "${TARGET_SHA}" != "${WORKFLOW_SHA}" ]]; then
if [[ "${DRY_RUN}" == "true" ]]; then
echo "Dry-run publish target differs from workflow ref; allowing validation-only dispatch."
exit 0
fi
echo "Plugin ClawHub OIDC publishes must run from the same ref that is being published." >&2
echo "The ref input is only supported for dry_run=true." >&2
echo "For real publishes, dispatch this workflow with --ref pointing at the target release tag/ref and omit the ref input." >&2
exit 1
fi
- name: Validate ref is on a trusted publish branch
env:
TRUSTED_PUBLISH_BRANCH: ${{ inputs.release_publish_branch || github.ref_name }}
WORKFLOW_REF: ${{ github.ref }}
run: |
set -euo pipefail
if git merge-base --is-ancestor HEAD origin/main; then
@@ -122,8 +96,8 @@ jobs:
exit 0
fi
done < <(git for-each-ref --format='%(refname)' refs/remotes/origin/release)
if [[ "${TRUSTED_PUBLISH_BRANCH}" =~ ^tideclaw/alpha/[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{4}Z$ ]]; then
alpha_branch="${TRUSTED_PUBLISH_BRANCH}"
if [[ "${WORKFLOW_REF}" =~ ^refs/heads/tideclaw/alpha/[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{4}Z$ ]]; then
alpha_branch="${WORKFLOW_REF#refs/heads/}"
git fetch --no-tags origin "+refs/heads/${alpha_branch}:refs/remotes/origin/${alpha_branch}"
if git merge-base --is-ancestor HEAD "refs/remotes/origin/${alpha_branch}"; then
exit 0
@@ -184,78 +158,36 @@ jobs:
cat .local/plugin-clawhub-release-plan.json
candidate_count="$(jq -r '.candidates | length' .local/plugin-clawhub-release-plan.json)"
bootstrap_candidate_count="$(jq -r '.bootstrapCandidates | length' .local/plugin-clawhub-release-plan.json)"
missing_trusted_publisher_count="$(jq -r '.missingTrustedPublisher | length' .local/plugin-clawhub-release-plan.json)"
skipped_published_count="$(jq -r '.skippedPublished | length' .local/plugin-clawhub-release-plan.json)"
has_candidates="false"
if [[ "${candidate_count}" != "0" ]]; then
has_candidates="true"
fi
has_bootstrap_candidates="false"
if [[ "${bootstrap_candidate_count}" != "0" ]]; then
has_bootstrap_candidates="true"
fi
has_missing_trusted_publisher="false"
if [[ "${missing_trusted_publisher_count}" != "0" ]]; then
has_missing_trusted_publisher="true"
fi
matrix_json="$(jq -c '.candidates' .local/plugin-clawhub-release-plan.json)"
bootstrap_matrix_json="$(jq -c '.bootstrapCandidates' .local/plugin-clawhub-release-plan.json)"
missing_trusted_publisher_matrix_json="$(jq -c '.missingTrustedPublisher' .local/plugin-clawhub-release-plan.json)"
{
echo "candidate_count=${candidate_count}"
echo "bootstrap_candidate_count=${bootstrap_candidate_count}"
echo "missing_trusted_publisher_count=${missing_trusted_publisher_count}"
echo "skipped_published_count=${skipped_published_count}"
echo "has_candidates=${has_candidates}"
echo "has_bootstrap_candidates=${has_bootstrap_candidates}"
echo "has_missing_trusted_publisher=${has_missing_trusted_publisher}"
echo "matrix=${matrix_json}"
echo "bootstrap_matrix=${bootstrap_matrix_json}"
echo "missing_trusted_publisher_matrix=${missing_trusted_publisher_matrix_json}"
} >> "$GITHUB_OUTPUT"
echo "Plugin release candidates:"
jq -r '.candidates[]? | "- \(.packageName)@\(.version) [\(.publishTag)] from \(.packageDir)"' .local/plugin-clawhub-release-plan.json
echo "Bootstrap candidates requiring token bootstrap:"
jq -r '.bootstrapCandidates[]? | "- \(.packageName)@\(.version) [\(.publishTag)] from \(.packageDir)"' .local/plugin-clawhub-release-plan.json
echo "Missing trusted publisher candidates:"
jq -r '.missingTrustedPublisher[]? | "- \(.packageName)@\(.version) [\(.publishTag)] from \(.packageDir)"' .local/plugin-clawhub-release-plan.json
echo "Already published / skipped:"
jq -r '.skippedPublished[]? | "- \(.packageName)@\(.version)"' .local/plugin-clawhub-release-plan.json
- name: Fail when trusted publisher is missing
if: steps.plan.outputs.missing_trusted_publisher_count != '0'
run: |
echo "::error::One or more ClawHub packages exist but do not have trusted publishing configured. Configure trusted publishing before running the normal OIDC publish workflow."
jq -r '.missingTrustedPublisher[]? | "::error::Missing trusted publisher: \(.packageName)@\(.version). Configure trusted publishing for openclaw/openclaw, workflow plugin-clawhub-release.yml."' .local/plugin-clawhub-release-plan.json
exit 1
- name: Fail normal publish when bootstrap is required
if: steps.plan.outputs.bootstrap_candidate_count != '0'
run: |
echo "::error::One or more ClawHub packages do not exist yet and require the token-gated Plugin ClawHub New bootstrap workflow before normal OIDC publish can run."
jq -r '.bootstrapCandidates[]? | "::error::Bootstrap required: \(.packageName)@\(.version). Dispatch plugin-clawhub-new.yml for this package, then rerun the normal release."' .local/plugin-clawhub-release-plan.json
exit 1
- name: Fail manual publish when target versions already exist
if: github.event_name == 'workflow_dispatch' && inputs.dry_run != true && inputs.publish_scope == 'selected' && steps.plan.outputs.skipped_published_count != '0'
if: github.event_name == 'workflow_dispatch' && inputs.publish_scope == 'selected' && steps.plan.outputs.skipped_published_count != '0'
run: |
echo "::error::One or more selected plugin versions already exist on ClawHub. Bump the version before running a real publish."
exit 1
- name: Validate Tideclaw alpha plugin channels
env:
TRUSTED_PUBLISH_BRANCH: ${{ inputs.release_publish_branch || github.ref_name }}
if: startsWith(github.ref, 'refs/heads/tideclaw/alpha/')
run: |
set -euo pipefail
if [[ ! "${TRUSTED_PUBLISH_BRANCH}" =~ ^tideclaw/alpha/[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{4}Z$ ]]; then
exit 0
fi
invalid="$(
jq -r '.candidates[]? | select(.publishTag != "alpha" or .channel != "alpha") | "- \(.packageName)@\(.version) [\(.publishTag)]"' .local/plugin-clawhub-release-plan.json
)"
@@ -265,6 +197,12 @@ jobs:
exit 1
fi
- name: Verify OpenClaw ClawHub package ownership
if: steps.plan.outputs.has_candidates == 'true'
env:
CLAWHUB_REGISTRY: ${{ env.CLAWHUB_REGISTRY }}
run: node --import tsx scripts/plugin-clawhub-owner-preflight.ts .local/plugin-clawhub-release-plan.json
validate_release_publish_approval:
name: Validate release publish approval
needs: preview_plugins_clawhub
@@ -283,7 +221,7 @@ jobs:
env:
GH_TOKEN: ${{ github.token }}
RELEASE_PUBLISH_RUN_ID: ${{ inputs.release_publish_run_id }}
EXPECTED_WORKFLOW_BRANCH: ${{ inputs.release_publish_branch || github.ref_name }}
EXPECTED_WORKFLOW_BRANCH: ${{ github.ref_name }}
run: |
set -euo pipefail
if [[ -z "${RELEASE_PUBLISH_RUN_ID// }" ]]; then
@@ -302,8 +240,99 @@ jobs:
RUN_JSON="$(gh run view "$RELEASE_PUBLISH_RUN_ID" --repo "$GITHUB_REPOSITORY" --json workflowName,headBranch,event,status,conclusion,url)"
printf '%s' "$RUN_JSON" | DIRECT_RELEASE_RECOVERY="${direct_recovery}" node scripts/validate-release-publish-approval.mjs
preview_plugin_pack:
needs: preview_plugins_clawhub
if: needs.preview_plugins_clawhub.outputs.has_candidates == 'true'
runs-on: ubuntu-latest
permissions:
contents: read
strategy:
fail-fast: false
max-parallel: 12
matrix:
plugin: ${{ fromJson(needs.preview_plugins_clawhub.outputs.matrix) }}
steps:
- name: Checkout
uses: actions/checkout@v6
with:
persist-credentials: false
ref: ${{ github.ref }}
fetch-depth: 0
- name: Checkout target revision
env:
TARGET_SHA: ${{ needs.preview_plugins_clawhub.outputs.ref_revision }}
run: |
set -euo pipefail
git fetch --no-tags origin \
+refs/heads/main:refs/remotes/origin/main \
'+refs/heads/release/*:refs/remotes/origin/release/*'
git checkout --detach "${TARGET_SHA}"
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
install-bun: "true"
install-deps: "true"
- name: Checkout ClawHub CLI source
uses: actions/checkout@v6
with:
persist-credentials: false
repository: ${{ env.CLAWHUB_REPOSITORY }}
ref: main
path: clawhub-source
fetch-depth: 0
- name: Checkout pinned ClawHub CLI revision
working-directory: clawhub-source
env:
CLAWHUB_REF: ${{ env.CLAWHUB_REF }}
run: git checkout --detach "${CLAWHUB_REF}"
- name: Install ClawHub CLI dependencies
working-directory: clawhub-source
run: |
set -euo pipefail
for attempt in 1 2 3; do
if bun install --frozen-lockfile; then
exit 0
fi
status="$?"
if [[ "${attempt}" == "3" ]]; then
exit "${status}"
fi
echo "bun install failed while preparing ClawHub CLI; retrying (${attempt}/3)."
rm -rf node_modules "${RUNNER_TEMP}/bun-install-cache" || true
sleep $((attempt * 15))
done
- name: Bootstrap ClawHub CLI
run: |
cat > "$RUNNER_TEMP/clawhub" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
exec bun "$GITHUB_WORKSPACE/clawhub-source/packages/clawhub/src/cli.ts" "$@"
EOF
chmod +x "$RUNNER_TEMP/clawhub"
echo "$RUNNER_TEMP" >> "$GITHUB_PATH"
- name: Verify package-local runtime build
run: node scripts/check-plugin-npm-runtime-builds.mjs --package "${{ matrix.plugin.packageDir }}"
- name: Preview publish command
env:
CLAWHUB_REGISTRY: ${{ env.CLAWHUB_REGISTRY }}
SOURCE_REPO: ${{ github.repository }}
SOURCE_COMMIT: ${{ needs.preview_plugins_clawhub.outputs.ref_revision }}
SOURCE_REF: ${{ github.ref }}
PACKAGE_TAG: ${{ matrix.plugin.publishTag }}
PACKAGE_DIR: ${{ matrix.plugin.packageDir }}
run: bash scripts/plugin-clawhub-publish.sh --dry-run "${PACKAGE_DIR}"
pack_plugins_clawhub_artifacts:
needs: [preview_plugins_clawhub, validate_release_publish_approval]
needs: [preview_plugins_clawhub, preview_plugin_pack, validate_release_publish_approval]
if: github.event_name == 'workflow_dispatch' && needs.preview_plugins_clawhub.outputs.has_candidates == 'true'
runs-on: ubuntu-latest
permissions:
@@ -338,19 +367,47 @@ jobs:
install-bun: "true"
install-deps: "true"
- name: Verify package-local runtime build
run: node scripts/check-plugin-npm-runtime-builds.mjs --package "${{ matrix.plugin.packageDir }}"
- name: Checkout ClawHub CLI source
uses: actions/checkout@v6
with:
persist-credentials: false
repository: ${{ env.CLAWHUB_REPOSITORY }}
ref: main
path: clawhub-source
fetch-depth: 0
- name: Install pinned ClawHub CLI wrapper
- name: Checkout pinned ClawHub CLI revision
working-directory: clawhub-source
env:
CLAWHUB_REF: ${{ env.CLAWHUB_REF }}
run: git checkout --detach "${CLAWHUB_REF}"
- name: Install ClawHub CLI dependencies
working-directory: clawhub-source
run: |
set -euo pipefail
cat > "${RUNNER_TEMP}/clawhub" <<'EOF'
for attempt in 1 2 3; do
if bun install --frozen-lockfile; then
exit 0
fi
status="$?"
if [[ "${attempt}" == "3" ]]; then
exit "${status}"
fi
echo "bun install failed while preparing ClawHub CLI; retrying (${attempt}/3)."
rm -rf node_modules "${RUNNER_TEMP}/bun-install-cache" || true
sleep $((attempt * 15))
done
- name: Bootstrap ClawHub CLI
run: |
cat > "$RUNNER_TEMP/clawhub" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
exec npm exec --yes --package "${CLAWHUB_CLI_PACKAGE}" -- clawhub "$@"
exec bun "$GITHUB_WORKSPACE/clawhub-source/packages/clawhub/src/cli.ts" "$@"
EOF
chmod +x "${RUNNER_TEMP}/clawhub"
echo "${RUNNER_TEMP}" >> "${GITHUB_PATH}"
chmod +x "$RUNNER_TEMP/clawhub"
echo "$RUNNER_TEMP" >> "$GITHUB_PATH"
- name: Pack ClawHub package artifact
env:
@@ -371,23 +428,19 @@ jobs:
if-no-files-found: error
retention-days: 7
approve_plugins_clawhub_release:
approve_plugin_clawhub_release:
needs: [preview_plugins_clawhub, pack_plugins_clawhub_artifacts]
if: always() && github.event_name == 'workflow_dispatch' && inputs.dry_run != true && needs.preview_plugins_clawhub.outputs.has_candidates == 'true' && needs.pack_plugins_clawhub_artifacts.result == 'success'
if: github.event_name == 'workflow_dispatch' && inputs.dry_run != true && needs.preview_plugins_clawhub.outputs.has_candidates == 'true'
runs-on: ubuntu-latest
environment: clawhub-plugin-release
permissions:
contents: read
permissions: {}
steps:
- name: Approve Plugin ClawHub release publish
run: |
echo "Approved CLAW-277 03 - Split OpenClaw plugin ClawHub publishing into OIDC release and token bootstrap workflows release publish gate."
- name: Approve ClawHub package publish
run: echo "ClawHub package publish approved."
publish_plugins_clawhub:
needs:
[preview_plugins_clawhub, pack_plugins_clawhub_artifacts, approve_plugins_clawhub_release]
if: always() && github.event_name == 'workflow_dispatch' && needs.preview_plugins_clawhub.outputs.has_candidates == 'true' && needs.pack_plugins_clawhub_artifacts.result == 'success' && (inputs.dry_run == true || needs.approve_plugins_clawhub_release.result == 'success')
uses: openclaw/clawhub/.github/workflows/package-publish.yml@9d49df109d4ad3dc8a6ecf05d26b39f46d294721
needs: [preview_plugins_clawhub, pack_plugins_clawhub_artifacts, approve_plugin_clawhub_release]
if: always() && github.event_name == 'workflow_dispatch' && needs.preview_plugins_clawhub.outputs.has_candidates == 'true' && needs.pack_plugins_clawhub_artifacts.result == 'success' && (inputs.dry_run == true || needs.approve_plugin_clawhub_release.result == 'success')
permissions:
actions: read
contents: read
@@ -397,18 +450,19 @@ jobs:
max-parallel: 32
matrix:
plugin: ${{ fromJson(needs.preview_plugins_clawhub.outputs.matrix) }}
uses: openclaw/clawhub/.github/workflows/package-publish.yml@c9bb13023598dcc547fdf4a93b9d42512b8c8854
with:
package_artifact_name: ${{ matrix.plugin.artifactName }}
dry_run: ${{ inputs.dry_run }}
json: true
package_artifact_name: ${{ matrix.plugin.artifactName }}
registry: https://clawhub.ai
site: https://clawhub.ai
tags: ${{ matrix.plugin.publishTag }}
source_repo: ${{ github.repository }}
source_commit: ${{ needs.preview_plugins_clawhub.outputs.ref_revision }}
source_ref: ${{ github.ref }}
source_path: ${{ matrix.plugin.packageDir }}
inspector_artifact_name: ${{ matrix.plugin.artifactName }}-inspector
publish_json_artifact_name: ${{ matrix.plugin.artifactName }}-publish-json
tags: ${{ matrix.plugin.publishTag }}
secrets:
clawhub_token: ${{ secrets.CLAWHUB_TOKEN }}
verify_published_clawhub_package:
needs: [preview_plugins_clawhub, publish_plugins_clawhub]

View File

@@ -288,7 +288,6 @@ jobs:
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
OPENCLAW_NPM_PUBLISH_AUTH_MODE: trusted-publisher
run: bash scripts/plugin-npm-publish.sh --publish "${{ matrix.plugin.packageDir }}"
- name: Verify published runtime

View File

@@ -2,6 +2,12 @@
Docs: https://docs.openclaw.ai
## 2026.6.7
### Fixes
- Alpha/nightly release metadata refreshes generated plugin inventory, config, Plugin SDK API, package version, and shrinkwrap state for the June 11 candidate.
## 2026.6.6
### Highlights

View File

@@ -116,19 +116,11 @@ RUN pnpm_config_verify_deps_before_run=false pnpm canvas:a2ui:bundle || \
echo "/* A2UI bundle unavailable in this build */" > extensions/canvas/src/host/a2ui/a2ui.bundle.js && \
echo "stub" > extensions/canvas/src/host/a2ui/.bundle.hash && \
rm -rf vendor/a2ui apps/shared/OpenClawKit/Tools/CanvasA2UI)
RUN if printf '%s\n' "$OPENCLAW_EXTENSIONS" | tr ',' ' ' | tr ' ' '\n' | grep -qx 'qa-lab'; then \
export OPENCLAW_BUILD_PRIVATE_QA=1 OPENCLAW_ENABLE_PRIVATE_QA_CLI=1; \
fi && \
NODE_OPTIONS=--max-old-space-size=8192 pnpm_config_verify_deps_before_run=false pnpm build:docker
RUN NODE_OPTIONS=--max-old-space-size=8192 pnpm_config_verify_deps_before_run=false pnpm build:docker
# Force pnpm for UI build (Bun may fail on ARM/Synology architectures)
ENV OPENCLAW_PREFER_PNPM=1
RUN pnpm_config_verify_deps_before_run=false pnpm ui:build
RUN if printf '%s\n' "$OPENCLAW_EXTENSIONS" | tr ',' ' ' | tr ' ' '\n' | grep -qx 'qa-lab'; then \
pnpm_config_verify_deps_before_run=false pnpm qa:lab:build && \
mkdir -p dist/extensions/qa-lab/web && \
rm -rf dist/extensions/qa-lab/web/dist && \
cp -R extensions/qa-lab/web/dist dist/extensions/qa-lab/web/dist; \
fi
RUN pnpm_config_verify_deps_before_run=false pnpm qa:lab:build
# Prune dev dependencies, omitted plugin runtime packages, and build-only
# metadata before copying runtime assets into the final image.

View File

@@ -72,7 +72,7 @@ final class CronJobsStore {
do {
if let status = try? await GatewayConnection.shared.cronStatus() {
self.schedulerEnabled = status.enabled
self.schedulerStorePath = status.sqlitePath ?? status.storePath
self.schedulerStorePath = status.storePath
self.schedulerNextWakeAtMs = status.nextWakeAtMs
}
self.jobs = try await GatewayConnection.shared.cronList(includeDisabled: true)

View File

@@ -775,7 +775,6 @@ extension GatewayConnection {
struct CronSchedulerStatus: Decodable {
let enabled: Bool
let storePath: String
let sqlitePath: String?
let jobs: Int
let nextWakeAtMs: Int?
}

View File

@@ -1,4 +1,4 @@
37b56008790612b8293930b6a29d74490e98daa90f954fca9d133fcc28645c4c config-baseline.json
75b64c2ea081369ba4306493313a8a4cd48b784145f92fed995e6b77a5df350d config-baseline.core.json
17d64c9799dfa239a49493413f1100bdd9237e9b67aaeae331a4604dbc227023 config-baseline.channel.json
f9d1f50bfa8403891e76cd99dc1357cdece4a71e8ae18a39b190c2a14e6f97b0 config-baseline.plugin.json
761191a4d9d369cf2b81a9895c4a3a008e5b84b4009842667daa9fdc1c36371c config-baseline.json
cf4bb4479d9004385c016fc6dba1db9509852762979635de4c7e1b33e89fe87c config-baseline.core.json
7641a0edd1f7ddb904d394381017cb645d2c2c1a86d875df30e9bf11f8dd012e config-baseline.channel.json
b0dec5acfe60557e728e5ad03cc36d19d2432d51f755656c97846afa7fbe374a config-baseline.plugin.json

View File

@@ -1,2 +1,2 @@
2c783beea6b3cda3d79060739a923f9f39e7e8b5942123dd6b08a09143a587ca plugin-sdk-api-baseline.json
0b33af2cffb42abb46682fb71c8f214da220793f13d10a34d332e75ff99e8ce9 plugin-sdk-api-baseline.jsonl
dc8d56fe11e75a07fec4f5a2463e20a58b31c2c134b241420987dd98bc0cc7de plugin-sdk-api-baseline.json
3bfe162eb02450c4e12c1468076e9809c67c20b73d7e347554d8962ef4f1da2f plugin-sdk-api-baseline.jsonl

View File

@@ -59,14 +59,6 @@ export CLICKCLACK_BOT_TOKEN="ccb_..."
openclaw gateway
```
If `plugins.allow` is a non-empty restrictive list, explicitly selecting
ClickClack in channel setup or running `openclaw plugins enable clickclack`
appends `clickclack` to that list. Onboarding installation uses the same
explicit-selection behavior. These paths do not override `plugins.deny` or a
global `plugins.enabled: false` setting. Direct `openclaw plugins install
clickclack` follows the normal plugin-install policy and also records ClickClack
in an existing allowlist.
## Multiple bots
Each account opens its own ClickClack realtime connection and uses its own bot token.

View File

@@ -183,7 +183,7 @@ The workflow installs OCM from a pinned release and Kova from `openclaw/Kova` at
- `mock-deep-profile`: CPU/heap/trace profiling for startup, gateway, and agent-turn hotspots.
- `live-openai-candidate`: a real OpenAI `openai/gpt-5.5` agent turn, skipped when `OPENAI_API_KEY` is unavailable.
The mock-provider lane also runs OpenClaw-native source probes after the Kova pass: gateway boot timing and memory across default, hook, and 50-plugin startup cases; bundled plugin import RSS, repeated mock-OpenAI `channel-chat-baseline` hello loops, CLI startup commands against the booted gateway, and the SQLite state smoke performance probe. When the previous published mock-provider source report is available for the tested ref, the source summary compares current RSS and heap values against that baseline and marks large RSS increases as `watch`. The source probe Markdown summary lives at `source/index.md` in the report bundle, with raw JSON beside it.
The mock-provider lane also runs OpenClaw-native source probes after the Kova pass: gateway boot timing and memory across default, hook, and 50-plugin startup cases; bundled plugin import RSS, repeated mock-OpenAI `channel-chat-baseline` hello loops, and CLI startup commands against the booted gateway. When the previous published mock-provider source report is available for the tested ref, the source summary compares current RSS and heap values against that baseline and marks large RSS increases as `watch`. The source probe Markdown summary lives at `source/index.md` in the report bundle, with raw JSON beside it.
Every lane uploads GitHub artifacts. When `CLAWGRIT_REPORTS_TOKEN` is configured, the workflow also commits `report.json`, `report.md`, bundles, `index.md`, and source-probe artifacts into `openclaw/clawgrit-reports` under `openclaw-performance/<tested-ref>/<run-id>-<attempt>/<lane>/`. The current tested-ref pointer is written as `openclaw-performance/<tested-ref>/latest-<lane>.json`.
@@ -452,7 +452,7 @@ For normal PRs, follow scoped CI/check evidence instead of treating parity as a
The `CodeQL` workflow is intentionally a narrow first-pass security scanner, not the full repository sweep. Daily, manual, and non-draft pull request guard runs scan Actions workflow code plus the highest-risk JavaScript/TypeScript surfaces with high-confidence security queries filtered to high/critical `security-severity`.
The pull request guard stays light: it only starts for changes under `.github/actions`, `.github/codeql`, `.github/workflows`, `packages`, `scripts`, `src`, or process-owning bundled plugin runtime paths, and it runs the same high-confidence security matrix as the scheduled workflow. Android and macOS CodeQL stay out of PR defaults.
The pull request guard stays light: it only starts for changes under `.github/actions`, `.github/codeql`, `.github/workflows`, `packages`, or `src`, and it runs the same high-confidence security matrix as the scheduled workflow. Android and macOS CodeQL stay out of PR defaults.
### Security categories
@@ -462,7 +462,6 @@ The pull request guard stays light: it only starts for changes under `.github/ac
| `/codeql-security-high/channel-runtime-boundary` | Core channel implementation contracts plus the channel plugin runtime, gateway, Plugin SDK, secrets, audit touchpoints |
| `/codeql-security-high/network-ssrf-boundary` | Core SSRF, IP parsing, network guard, web-fetch, and Plugin SDK SSRF policy surfaces |
| `/codeql-security-high/mcp-process-tool-boundary` | MCP servers, process execution helpers, outbound delivery, and agent tool-execution gates |
| `/codeql-security-high/process-exec-boundary` | Local shell, process spawn helpers, subprocess-owning bundled plugin runtimes, and workflow script glue |
| `/codeql-security-high/plugin-trust-boundary` | Plugin install, loader, manifest, registry, package-manager install, source-loading, and Plugin SDK package contract trust surfaces |
### Platform-specific security shards

View File

@@ -35,7 +35,6 @@ openclaw wiki status
openclaw wiki doctor
openclaw wiki init
openclaw wiki ingest ./notes/alpha.md
openclaw wiki okf import ./knowledge-catalog/okf/bundles/ga4
openclaw wiki compile
openclaw wiki lint
openclaw wiki search "alpha"
@@ -105,31 +104,6 @@ Notes:
- imported source pages keep provenance in frontmatter
- auto-compile can run after ingest when enabled
### `wiki okf import <path>`
Import an unpacked Open Knowledge Format bundle into wiki concept pages.
The importer reads every non-reserved `.md` concept document in the OKF
directory tree, requires a non-empty `type` field, and treats unknown OKF
`type` values as generic concepts. Reserved OKF `index.md` and `log.md` files
are not imported as concepts.
Imported pages are flattened under `concepts/` so existing wiki compile,
search, get, digest, and dashboard flows see them immediately. The original OKF
concept ID, `type`, `resource`, `tags`, timestamp, source path, and full
frontmatter are preserved in the page frontmatter. Internal OKF markdown links
are rewritten to the generated wiki pages; broken or external links are left
unchanged.
Examples:
```bash
openclaw wiki okf import ./bundles/ga4
openclaw wiki okf import ./bundles/ga4 --json
openclaw wiki search "BigQuery Table" --mode source-evidence --json
openclaw wiki get <path-from-json-result>
```
### `wiki compile`
Rebuild indexes, related blocks, dashboards, and compiled digests.
@@ -259,8 +233,6 @@ These require the official `obsidian` CLI on `PATH` when
- Use `wiki lint` before trusting contradictory or low-confidence content.
- Use `wiki compile` after bulk imports or source changes when you want fresh
dashboards and compiled digests immediately.
- Use `wiki okf import` when a data catalog, documentation export, or agent
enrichment pipeline already emits OKF markdown bundles.
- Use `wiki bridge import` when bridge mode depends on newly exported memory
artifacts.

View File

@@ -368,7 +368,6 @@ Kimi K2 model IDs:
[//]: # "moonshot-kimi-k2-model-refs:start"
- `moonshot/kimi-k2.6`
- `moonshot/kimi-k2.7-code`
- `moonshot/kimi-k2.5`
- `moonshot/kimi-k2-thinking`
- `moonshot/kimi-k2-thinking-turbo`

View File

@@ -374,7 +374,7 @@ The implicit default set always covers canary, mention gating, native command re
Output artifacts:
- `telegram-qa-report.md`
- `qa-evidence.json` - evidence entries for the live transport checks, including profile, coverage, provider, channel, artifacts, result, and RTT fields.
- `telegram-qa-summary.json` - includes per-reply RTT (driver send → observed SUT reply) starting with the canary.
- `telegram-qa-observed-messages.json` - bodies redacted unless `OPENCLAW_QA_TELEGRAM_CAPTURE_CONTENT=1`.
Package RTT comparison uses the same Telegram credential contract while keeping
@@ -447,7 +447,7 @@ pnpm openclaw qa discord \
Output artifacts:
- `discord-qa-report.md`
- `qa-evidence.json` - evidence entries for the live transport checks.
- `discord-qa-summary.json`
- `discord-qa-observed-messages.json` - bodies redacted unless `OPENCLAW_QA_DISCORD_CAPTURE_CONTENT=1`.
- `discord-qa-reaction-timelines.json` and `discord-status-reactions-tool-only-timeline.png` when the status-reaction scenario runs.
@@ -495,7 +495,7 @@ Scenarios (`extensions/qa-lab/src/live-transports/slack/slack-live.runtime.ts`):
Output artifacts:
- `slack-qa-report.md`
- `qa-evidence.json` - evidence entries for the live transport checks.
- `slack-qa-summary.json`
- `slack-qa-observed-messages.json` - bodies redacted unless `OPENCLAW_QA_SLACK_CAPTURE_CONTENT=1`.
- `approval-checkpoints/` - only when Mantis sets
`OPENCLAW_QA_SLACK_APPROVAL_CHECKPOINT_DIR`; contains checkpoint JSON,
@@ -740,7 +740,7 @@ poll and upload-file coverage run through deterministic gateway `poll` and
Output artifacts:
- `whatsapp-qa-report.md`
- `qa-evidence.json` - evidence entries for the live transport checks.
- `whatsapp-qa-summary.json`
- `whatsapp-qa-observed-messages.json` - bodies redacted unless `OPENCLAW_QA_WHATSAPP_CAPTURE_CONTENT=1`.
### Convex credential pool
@@ -787,10 +787,9 @@ the source of truth for one test run and should define:
- docs and code refs
- optional plugin requirements
- optional gateway config patch
- an executable `qa-flow` block for flow scenarios, or `execution.kind`/`execution.path`
for Vitest and Playwright scenarios
- the executable `qa-flow`
The reusable runtime surface that backs `qa-flow` blocks is allowed to stay generic
The reusable runtime surface that backs `qa-flow` is allowed to stay generic
and cross-cutting. For example, markdown scenarios can combine transport-side
helpers with browser-side helpers that drive the embedded Control UI through the
Gateway `browser.request` seam without adding a special-case runner.
@@ -916,7 +915,6 @@ The report should answer:
For the inventory of available scenarios - useful when sizing follow-up work or wiring a new transport - run `pnpm openclaw qa coverage` (add `--json` for machine-readable output).
When choosing focused proof for a touched behavior or file path, run `pnpm openclaw qa coverage --match <query>`.
The match report searches scenario metadata, docs refs, code refs, coverage IDs, plugins, and provider requirements, then prints matching `qa suite --scenario ...` targets.
Every `qa suite` scenario execution writes a `qa-evidence.json` artifact. Flow scenarios also write `qa-suite-summary.json` for existing suite/report tooling; scenarios that declare `execution.kind: vitest` or `execution.kind: playwright` run the matching test path and write `qa-vitest-report.md` or `qa-playwright-report.md` plus per-scenario logs.
Treat it as a discovery aid, not a gate replacement; the selected scenario still needs the right provider mode, live transport, Multipass, Testbox, or release lane for the behavior under test.
For character and style checks, run the same scenario across multiple live model

View File

@@ -30,23 +30,6 @@ title: "Usage tracking"
- CLI: `openclaw channels list` prints the same usage snapshot alongside provider config (use `--no-usage` to skip).
- macOS menu bar: "Usage" section under Context (only if available).
## Custom `/usage full` footer
Set `messages.usageTemplate` to customize the per-response `/usage full`
footer. The value can be an inline template object or a JSON file path:
```json
{
"messages": {
"usageTemplate": "~/.openclaw/usage-footer.json"
}
}
```
Templates read the `openclaw.usageLine.v1` contract and can use `scales`,
`aliases`, and `output.surfaces` to render channel-specific footers. Missing,
unreadable, invalid, or empty templates fall back to the built-in usage line.
## Providers + credentials
- **Anthropic (Claude)**: OAuth tokens in auth profiles.

View File

@@ -1374,7 +1374,9 @@
"pages": [
"clawhub/cli",
"clawhub/publishing",
"clawhub/plugin-validation-fixes",
"clawhub/skill-format",
"clawhub/soul-format",
"clawhub/auth",
"clawhub/telemetry",
"clawhub/troubleshooting"

View File

@@ -493,8 +493,6 @@ example `~/.agents/skills/manager -> ~/Projects/manager/skills`.
- `extraDirs` scans the sibling repo as an explicit skill root.
- `allowSymlinkTargets` lets symlinked skill folders resolve into that trusted
real target root without allowing arbitrary symlink escapes.
- To let Skill Workshop apply write through the same trusted symlink target,
set `skills.workshop.allowSymlinkTargetWrites: true`.
## Common patterns

View File

@@ -200,9 +200,6 @@ See [MCP](/cli/mcp#openclaw-as-an-mcp-client-registry) and
nodeManager: "npm", // npm | pnpm | yarn | bun
allowUploadedArchives: false,
},
workshop: {
allowSymlinkTargetWrites: false,
},
entries: {
"image-lab": {
apiKey: { source: "env", provider: "default", id: "GEMINI_API_KEY" }, // or plaintext string
@@ -219,8 +216,6 @@ See [MCP](/cli/mcp#openclaw-as-an-mcp-client-registry) and
- `load.extraDirs`: extra shared skill roots (lowest precedence).
- `load.allowSymlinkTargets`: trusted real target roots that skill symlinks may
resolve into when the link lives outside its configured source root.
- `workshop.allowSymlinkTargetWrites`: allows Skill Workshop apply to write
through already-trusted symlink targets (default: false).
- `install.preferBrew`: when true, prefer Homebrew installers when `brew` is
available before falling back to other installer kinds.
- `install.nodeManager`: node installer preference for `metadata.openclaw.install`

View File

@@ -42,21 +42,6 @@ health commands above for live connectivity checks.
- `channels.<provider>.accounts.<accountId>.healthMonitor.enabled`: multi-account override that wins over the channel-level setting.
- These per-channel overrides apply to the built-in channel monitors that expose them today: Discord, Google Chat, iMessage, Microsoft Teams, Signal, Slack, Telegram, and WhatsApp.
## Uptime monitoring
External uptime monitoring services should use the dedicated `/health` endpoint, not `/v1/chat/completions`.
- **DO use:** `GET /health` — instant response, no session created, no LLM call, returns `{"ok":true,"status":"live"}`
- **DON'T use:** `/v1/chat/completions` for health checks — each request creates a full agent session with skill snapshot, context assembly, and LLM calls
When no `x-openclaw-session-key` header or `user` field is provided, `/v1/chat/completions` generates a new random session for each request. Monitoring services that ping every 15 minutes create ~96 sessions/day, each consuming 422KB. Over time this causes session store bloat and can lead to context window overflow.
### Monitoring service setup examples
- **BetterStack:** Set health check URL to `https://<your-gateway-host>:<port>/health`
- **UptimeRobot:** Add a new HTTP monitor with URL `https://<your-gateway-host>:<port>/health`
- **Generic:** Any HTTP GET to `/health` returns 200 with `{"ok":true}` when the gateway is healthy
## When something fails
- `logged out` or status 409515 → relink with `openclaw channels logout` then `openclaw channels login`.

View File

@@ -75,7 +75,6 @@ Auth matrix:
- honor `x-openclaw-scopes` when the header is present
- fall back to the normal operator default scope set when the header is absent
- only lose owner semantics when the caller explicitly narrows scopes and omits `operator.admin`
- require `operator.admin` for owner-level request controls such as `x-openclaw-model`
See [Security](/gateway/security) and [Remote access](/gateway/remote).
@@ -97,7 +96,7 @@ OpenClaw treats the OpenAI `model` field as an **agent target**, not a raw provi
Optional request headers:
- `x-openclaw-model: <provider/model-or-bare-id>` overrides the backend model for the selected agent. Shared-secret bearer callers can use this header. Identity-bearing callers, such as trusted-proxy or private no-auth ingress requests with `x-openclaw-scopes`, need `operator.admin`; write-only callers get `403 missing scope: operator.admin`.
- `x-openclaw-model: <provider/model-or-bare-id>` overrides the backend model for the selected agent.
- `x-openclaw-agent-id: <agentId>` remains supported as a compatibility override.
- `x-openclaw-session-key: <sessionKey>` fully controls session routing.
- `x-openclaw-message-channel: <channel>` sets the synthetic ingress channel context for channel-aware prompts and policies.
@@ -179,7 +178,7 @@ This is the highest-leverage compatibility set for self-hosted frontends and too
</Accordion>
<Accordion title="How do I override the backend model?">
Use `x-openclaw-model`. This is an owner-level override: it works with the Gateway shared-secret bearer token/password path, and it requires `operator.admin` on identity-bearing HTTP paths such as trusted proxy auth.
Use `x-openclaw-model`.
Examples:
`x-openclaw-model: openai/gpt-5.4`
@@ -192,7 +191,7 @@ This is the highest-leverage compatibility set for self-hosted frontends and too
`/v1/embeddings` uses the same agent-target `model` ids.
Use `model: "openclaw/default"` or `model: "openclaw/<agentId>"`.
When you need a specific embedding model, send it in `x-openclaw-model` from a shared-secret caller or an identity-bearing caller with `operator.admin`.
When you need a specific embedding model, send it in `x-openclaw-model`.
Without that header, the request passes through to the selected agent's normal embedding setup.
</Accordion>
@@ -286,7 +285,7 @@ Expected behavior:
- `GET /v1/models` should list `openclaw/default`
- Open WebUI should use `openclaw/default` as the chat model id
- If you want a specific backend provider/model for that agent, set the agent's normal default model or send `x-openclaw-model` from a shared-secret caller or an identity-bearing caller with `operator.admin`
- If you want a specific backend provider/model for that agent, set the agent's normal default model or send `x-openclaw-model`
Quick smoke:
@@ -371,7 +370,7 @@ Notes:
- `/v1/models` returns OpenClaw agent targets, not raw provider catalogs.
- `openclaw/default` is always present so one stable id works across environments.
- Backend provider/model overrides belong in `x-openclaw-model`, not the OpenAI `model` field. On identity-bearing HTTP auth paths, this header requires `operator.admin`.
- Backend provider/model overrides belong in `x-openclaw-model`, not the OpenAI `model` field.
- `/v1/embeddings` supports `input` as a string or array of strings.
## Related

View File

@@ -951,7 +951,7 @@ Important boundary note:
- Treat credentials that can call `/v1/chat/completions`, `/v1/responses`, plugin routes such as `/api/v1/admin/rpc`, or `/api/channels/*` as full-access operator secrets for that gateway.
- On the OpenAI-compatible HTTP surface, shared-secret bearer auth restores the full default operator scopes (`operator.admin`, `operator.approvals`, `operator.pairing`, `operator.read`, `operator.talk.secrets`, `operator.write`) and owner semantics for agent turns; narrower `x-openclaw-scopes` values do not reduce that shared-secret path.
- Per-request scope semantics on HTTP only apply when the request comes from an identity-bearing mode such as trusted proxy auth, or from an explicitly no-auth private ingress.
- In those identity-bearing modes, omitting `x-openclaw-scopes` falls back to the normal operator default scope set; send the header explicitly when you want a narrower scope set. Owner-level OpenAI-compatible headers such as `x-openclaw-model` require `operator.admin` when scopes are narrowed.
- In those identity-bearing modes, omitting `x-openclaw-scopes` falls back to the normal operator default scope set; send the header explicitly when you want a narrower scope set.
- `/tools/invoke` and HTTP session history endpoints follow the same shared-secret rule: token/password bearer auth is treated as full operator access there too, while identity-bearing modes still honor declared scopes.
- Do not share these credentials with untrusted callers; prefer separate gateways per trust boundary.

View File

@@ -154,10 +154,6 @@ Do not use broad targets such as `~`, `/`, or a whole synced project folder.
Keep `allowSymlinkTargets` scoped to the real skill root that contains trusted
`SKILL.md` directories.
If Skill Workshop apply should also write through those trusted symlinked
workspace skill paths, enable `skills.workshop.allowSymlinkTargetWrites`. Keep
it disabled for read-only shared skill roots.
Related:
- [Skills config](/tools/skills-config#symlinked-sibling-repos)

View File

@@ -18,13 +18,11 @@ most Linux-compatible Gateway runtime.
Windows Hub is the native WinUI companion app for Windows 10 20H2+ and Windows 11. It installs without administrator privileges and is published with signed
x64 and ARM64 installers on OpenClaw releases.
Download the latest stable installer from the [OpenClaw releases page](https://github.com/openclaw/openclaw/releases):
Download the latest stable installer:
- [OpenClawCompanion-Setup-x64.exe](https://github.com/openclaw/openclaw/releases/download/v2026.6.5/OpenClawCompanion-Setup-x64.exe)
- [OpenClawCompanion-Setup-arm64.exe](https://github.com/openclaw/openclaw/releases/download/v2026.6.5/OpenClawCompanion-Setup-arm64.exe)
- [Checksums](https://github.com/openclaw/openclaw/releases/download/v2026.6.5/OpenClawCompanion-SHA256SUMS.txt)
If a download link above returns a 404, visit the [releases page](https://github.com/openclaw/openclaw/releases) and look for the `OpenClawCompanion-Setup-*` assets on the latest release.
- [OpenClawCompanion-Setup-x64.exe](https://github.com/openclaw/openclaw/releases/latest/download/OpenClawCompanion-Setup-x64.exe)
- [OpenClawCompanion-Setup-arm64.exe](https://github.com/openclaw/openclaw/releases/latest/download/OpenClawCompanion-Setup-arm64.exe)
- [Checksums](https://github.com/openclaw/openclaw/releases/latest/download/OpenClawCompanion-SHA256SUMS.txt)
After install, launch **OpenClaw Companion** from the Start menu or the system
tray. The installer also adds shortcuts for Gateway Setup, Chat, Settings,

View File

@@ -128,10 +128,6 @@ Current compatibility records include:
- legacy runtime aliases such as `api.runtime.taskFlow`,
`api.runtime.subagent.getSession`, `api.runtime.stt`, and deprecated
`api.runtime.config.loadConfig()` / `api.runtime.config.writeConfigFile(...)`
- WhatsApp `WebInboundMessage` flat callback fields such as `body`, `chatId`,
`reply(...)`, and `mediaPath` while callback consumers migrate to the nested
`WebInboundCallbackMessage` `event`, `payload`, `quote`, `group`, and
`platform` contexts
- legacy memory-plugin split registration while memory plugins move to
`registerMemoryCapability`
- legacy memory-specific embedding provider registration while embedding
@@ -164,33 +160,6 @@ New plugin code should prefer the replacement listed in the registry and in the
specific migration guide. Existing plugins can keep using a compatibility path
until the docs, diagnostics, and release notes announce a removal window.
### WhatsApp Inbound Callback Flat Aliases
WhatsApp runtime callbacks deliver `WebInboundMessage`: the canonical nested
`event`, `payload`, `quote`, `group`, and `platform` contexts plus deprecated
flat aliases for the shipped callback fields. New callback code should read the
nested contexts. Code that constructs clean nested callback messages can use
`WebInboundCallbackMessage`; compatibility listeners that still inject old flat
test or plugin messages should use `LegacyFlatWebInboundMessage` or
`WebInboundMessageInput`.
The flat aliases remain available until **2026-08-30**. That removal window
applies only to flat alias access; the nested callback shape is the canonical
runtime contract. The TypeScript `@deprecated` annotations on each flat alias
name its exact nested replacement. Common examples:
- `id`, `timestamp`, and `isBatched` move under `event`.
- `body`, `mediaPath`, `mediaType`, `mediaFileName`, `mediaUrl`, `location`, and
`untrustedStructuredContext` move under `payload`.
- `to`, `chatId`, sender/self fields, `sendComposing`, `reply(...)`, and
`sendMedia(...)` move under `platform`.
- `replyTo*` fields move under `quote`, and group subject/participant/mention
fields move under `group`.
`payload.untrustedStructuredContext` is extracted from inbound provider payloads.
Plugins should inspect the `label`, `source`, and `type` before treating its
`payload` as authoritative.
## Release notes
Release notes should include upcoming plugin deprecations with target dates and

View File

@@ -425,10 +425,6 @@ even when the channel payload has no visible text/caption. Rewriting that
`content` updates the hook-visible transcript only; it is not rendered as a
media caption.
`reply_payload_sending` events may include `usageState`, a best-effort live
per-turn model/usage/context snapshot. Durable delivery, recovered replay, and
replies without exact run correlation omit it.
Message hook contexts expose stable correlation fields when available:
`ctx.sessionKey`, `ctx.runId`, `ctx.messageId`, `ctx.senderId`, `ctx.trace`,
`ctx.traceId`, `ctx.spanId`, `ctx.parentSpanId`, and `ctx.callDepth`. Inbound

View File

@@ -25,7 +25,6 @@ less like a pile of Markdown files.
- Page-level provenance, confidence, contradictions, and open questions
- Compiled digests for agent/runtime consumers
- Wiki-native search/get/apply/lint tools
- Open Knowledge Format imports into compiled wiki concepts
- Optional bridge mode that imports public artifacts from the active memory plugin
- Optional Obsidian-friendly render mode and CLI integration
@@ -136,34 +135,6 @@ The main page groups are:
- `syntheses/` for compiled summaries and maintained rollups
- `reports/` for generated dashboards
## Open Knowledge Format imports
`memory-wiki` can import unpacked Open Knowledge Format bundles with:
```bash
openclaw wiki okf import ./bundles/ga4
```
This is the cleanest fit when a data catalog, documentation crawler, or
enrichment agent already produces OKF: keep OKF as the portable exchange
artifact, then let `memory-wiki` turn it into OpenClaw-native concept pages and
compiled digests.
The importer follows the OKF v0.1 shape:
- non-reserved `.md` files are concept documents
- each imported concept needs a non-empty `type` frontmatter field
- unknown OKF `type` values are accepted
- reserved `index.md` and `log.md` files are not imported as concepts
- broken or external markdown links are preserved
Imported concept pages are flattened under `concepts/` so the existing compile,
search, get, dashboard, and prompt-digest paths see them without adding a second
wiki tree. Each page keeps the original OKF concept ID, source path, `type`,
`resource`, `tags`, timestamp, and full producer frontmatter. Internal OKF links
are rewritten to the generated wiki concept pages and also emitted as structured
`relationships` entries with `kind: okf-link`.
## Structured claims and evidence
Pages can carry structured `claims` frontmatter, not just freeform text.

View File

@@ -38,19 +38,6 @@ Choose your preferred auth method and follow the setup steps.
export AWS_REGION="us-west-2"
```
</Step>
<Step title="Opt in to provider data sharing for Claude Fable 5">
Claude Fable 5 and Claude Mythos-class Bedrock models require the Mantle Data Retention API mode `provider_data_share` before invocation. This opt-in allows Bedrock to share prompts and completions with Anthropic and retain them for up to 30 days for trust and safety review.
```bash
AWS_REGION="${AWS_REGION:-us-east-1}"
curl -X PUT "https://bedrock-mantle.${AWS_REGION}.api.aws/v1/data_retention" \
-H "Authorization: Bearer $AWS_BEARER_TOKEN_BEDROCK" \
-H "Content-Type: application/json" \
-d '{ "mode": "provider_data_share" }'
```
Use another Bedrock model in the config if you cannot accept that retention mode.
</Step>
<Step title="Verify models are discovered">
```bash
openclaw models list

View File

@@ -22,7 +22,6 @@ Moonshot and Kimi Coding are **separate providers**. Keys are not interchangeabl
| Model ref | Name | Reasoning | Input | Context | Max output |
| --------------------------------- | ---------------------- | --------- | ----------- | ------- | ---------- |
| `moonshot/kimi-k2.6` | Kimi K2.6 | No | text, image | 262,144 | 262,144 |
| `moonshot/kimi-k2.7-code` | Kimi K2.7 Code | Always on | text, image | 262,144 | 262,144 |
| `moonshot/kimi-k2.5` | Kimi K2.5 | No | text, image | 262,144 | 262,144 |
| `moonshot/kimi-k2-thinking` | Kimi K2 Thinking | Yes | text | 262,144 | 262,144 |
| `moonshot/kimi-k2-thinking-turbo` | Kimi K2 Thinking Turbo | Yes | text | 262,144 | 262,144 |
@@ -31,18 +30,11 @@ Moonshot and Kimi Coding are **separate providers**. Keys are not interchangeabl
[//]: # "moonshot-kimi-k2-ids:end"
Bundled cost estimates for current Moonshot-hosted K2 models use Moonshot's
published pay-as-you-go rates: Kimi K2.7 Code is $0.19/MTok cache hit,
$0.95/MTok input, and $4.00/MTok output; Kimi K2.6 is $0.16/MTok cache hit,
published pay-as-you-go rates: Kimi K2.6 is $0.16/MTok cache hit,
$0.95/MTok input, and $4.00/MTok output; Kimi K2.5 is $0.10/MTok cache hit,
$0.60/MTok input, and $3.00/MTok output. Other legacy catalog entries keep
zero-cost placeholders unless you override them in config.
Kimi K2.7 Code always uses native thinking. OpenClaw exposes only the `on`
thinking state for this model and omits outbound `thinking` and
`reasoning_effort` controls, as required by Moonshot. OpenClaw also omits
sampling overrides that K2.7 fixes to provider defaults. Kimi K2.6 remains the
onboarding default.
## Getting started
Choose your provider and follow the setup steps.
@@ -117,7 +109,6 @@ Choose your provider and follow the setup steps.
models: {
// moonshot-kimi-k2-aliases:start
"moonshot/kimi-k2.6": { alias: "Kimi K2.6" },
"moonshot/kimi-k2.7-code": { alias: "Kimi K2.7 Code" },
"moonshot/kimi-k2.5": { alias: "Kimi K2.5" },
"moonshot/kimi-k2-thinking": { alias: "Kimi K2 Thinking" },
"moonshot/kimi-k2-thinking-turbo": { alias: "Kimi K2 Thinking Turbo" },
@@ -144,15 +135,6 @@ Choose your provider and follow the setup steps.
contextWindow: 262144,
maxTokens: 262144,
},
{
id: "kimi-k2.7-code",
name: "Kimi K2.7 Code",
reasoning: true,
input: ["text", "image"],
cost: { input: 0.95, output: 4, cacheRead: 0.19, cacheWrite: 0 },
contextWindow: 262144,
maxTokens: 262144,
},
{
id: "kimi-k2.5",
name: "Kimi K2.5",
@@ -306,13 +288,7 @@ Config lives under `plugins.entries.moonshot.config.webSearch`:
<AccordionGroup>
<Accordion title="Native thinking mode">
Kimi K2.7 Code always uses native thinking. Moonshot requires clients to
omit the `thinking` field for this model, so OpenClaw exposes only `on` and
ignores stale `off` settings. K2.7 also fixes `temperature`, `top_p`, `n`,
`presence_penalty`, and `frequency_penalty`; OpenClaw omits configured
overrides for those fields.
Other Moonshot Kimi models support binary native thinking:
Moonshot Kimi supports binary native thinking:
- `thinking: { type: "enabled" }`
- `thinking: { type: "disabled" }`
@@ -335,7 +311,7 @@ Config lives under `plugins.entries.moonshot.config.webSearch`:
}
```
OpenClaw maps runtime `/think` levels for those models:
OpenClaw also maps runtime `/think` levels for Moonshot:
| `/think` level | Moonshot behavior |
| -------------------- | -------------------------- |
@@ -343,16 +319,14 @@ Config lives under `plugins.entries.moonshot.config.webSearch`:
| Any non-off level | `thinking.type=enabled` |
<Warning>
When Moonshot thinking is enabled, `tool_choice` must be `auto` or `none`. OpenClaw normalizes incompatible values to `auto`. This includes Kimi K2.7 Code, whose thinking mode cannot be disabled to preserve a pinned tool choice.
When Moonshot thinking is enabled, `tool_choice` must be `auto` or `none`. OpenClaw normalizes incompatible `tool_choice` values to `auto` for compatibility.
</Warning>
Kimi K2.6 also accepts an optional `thinking.keep` field that controls
multi-turn retention of `reasoning_content`. Set it to `"all"` to keep full
reasoning across turns; omit it (or leave it `null`) to use the server
default strategy. OpenClaw only forwards `thinking.keep` for
`moonshot/kimi-k2.6` and strips it from other models. Kimi K2.7 Code
preserves full reasoning history by default while OpenClaw omits the entire
`thinking` field.
`moonshot/kimi-k2.6` and strips it from other models.
```json5
{
@@ -373,7 +347,7 @@ Config lives under `plugins.entries.moonshot.config.webSearch`:
</Accordion>
<Accordion title="Tool call id sanitization">
Moonshot Kimi serves native tool_call ids shaped like `functions.<name>:<index>`. For the OpenAI-completions transport, OpenClaw preserves the first occurrence of each native Kimi id and rewrites later duplicates to deterministic OpenAI-style `call_*` ids. Matching tool results are remapped with the same id so replay remains unique without stripping Kimi's first native id.
Moonshot Kimi serves tool_call ids shaped like `functions.<name>:<index>`. OpenClaw preserves them unchanged so multi-turn tool calls keep working.
To force strict sanitization on a custom OpenAI-compatible provider, set `sanitizeToolCallIds: true`:

View File

@@ -190,7 +190,6 @@ agent session or the CLI.
autonomous: {
enabled: false,
},
allowSymlinkTargetWrites: false,
approvalPolicy: "pending",
maxPending: 50,
maxSkillBytes: 40000,
@@ -201,9 +200,6 @@ agent session or the CLI.
- `autonomous.enabled`: allows OpenClaw to create pending proposals from durable
conversation signals after successful turns. Default: `false`.
- `allowSymlinkTargetWrites`: allows apply to write through workspace skill
symlinks whose real target is listed in `skills.load.allowSymlinkTargets`.
Default: `false`.
- `approvalPolicy: "pending"`: requires an approval prompt before
agent-initiated `apply`, `reject`, or `quarantine`.
- `approvalPolicy: "auto"`: skips that approval prompt. The agent must still
@@ -269,7 +265,6 @@ Default state directory: `~/.openclaw`.
| `Skill proposal content is too large` | Shorten the proposal body or raise `skills.workshop.maxSkillBytes`. |
| `Target skill changed after proposal creation` | Revise the proposal against the current target, or create a new proposal. |
| `Proposal scan failed` | Inspect scanner findings, then revise or quarantine the proposal. |
| `untrusted symlink target` | Configure `skills.load.allowSymlinkTargets` and enable `skills.workshop.allowSymlinkTargetWrites` only for intentional shared skill roots. |
| `Support file paths must be under one of...` | Move support files under `assets/`, `examples/`, `references/`, `scripts/`, or `templates/`. |
| Proposal does not show in list | Check the selected `--agent` workspace and `OPENCLAW_STATE_DIR`. |
| Agent cannot call `skill_workshop` | Check the active tool policy and run mode. `coding` includes the tool; restrictive `tools.allow` policies must list it explicitly, and sandboxed runs must use a normal host-side agent session or the CLI. |

View File

@@ -29,7 +29,6 @@ Most skills configuration lives under `skills` in
},
workshop: {
autonomous: { enabled: false },
allowSymlinkTargetWrites: false,
approvalPolicy: "pending",
maxPending: 50,
maxSkillBytes: 40000,
@@ -334,13 +333,6 @@ different visible skill set per agent.
quarantine. `auto` allows those actions without approval.
</ParamField>
<ParamField path="skills.workshop.allowSymlinkTargetWrites" type="boolean" default="false">
Allow Skill Workshop apply to write through workspace skill symlinks whose
real target is already trusted by `skills.load.allowSymlinkTargets`. Keep this
disabled unless generated proposal applies should mutate that shared skill
root.
</ParamField>
<ParamField path="skills.workshop.maxPending" type="number" default="50">
Maximum pending and quarantined proposals retained per workspace.
</ParamField>
@@ -373,23 +365,6 @@ With this config, `<workspace>/skills/manager -> ~/Projects/manager/skills` is
accepted after realpath resolution. `extraDirs` scans the sibling repo directly;
`allowSymlinkTargets` preserves the symlinked path for existing layouts.
Skill Workshop apply does not write through those symlinks by default. To let
Workshop apply mutate skills under already-trusted symlink targets, opt in
separately:
```json5
{
skills: {
load: {
allowSymlinkTargets: ["~/Projects/manager/skills"],
},
workshop: {
allowSymlinkTargetWrites: true,
},
},
}
```
Managed `~/.openclaw/skills` and personal `~/.agents/skills` directories
already accept skill-directory symlinks (per-skill `SKILL.md` containment still
applies).

View File

@@ -204,8 +204,6 @@ publish and sync.
Workspace, project-agent, and extra-dir skill discovery only accepts skill
roots whose resolved realpath stays inside the configured root, unless
`skills.load.allowSymlinkTargets` explicitly trusts a target root.
Skill Workshop writes through those trusted targets only when
`skills.workshop.allowSymlinkTargetWrites` is enabled.
Managed `~/.openclaw/skills` and personal `~/.agents/skills` may contain
symlinked skill folders, but every `SKILL.md` realpath must still stay
inside its resolved skill directory.
@@ -535,8 +533,6 @@ aligned.
Use `allowSymlinkTargets` for intentional symlinked layouts where a skill
root symlink points outside the configured root, for example
`<workspace>/skills/manager -> ~/Projects/manager/skills`.
Enable `skills.workshop.allowSymlinkTargetWrites` only when Skill Workshop
should also apply proposals through those trusted symlinked paths.
</Accordion>
<Accordion title="Remote macOS nodes (Linux gateway)">

View File

@@ -35,7 +35,7 @@ title: "Thinking levels"
- Google Gemini maps `/think adaptive` to Gemini's provider-owned dynamic thinking. Gemini 3 requests omit a fixed `thinkingLevel`, while Gemini 2.5 requests send `thinkingBudget: -1`; fixed levels still map to the closest Gemini `thinkingLevel` or budget for that model family.
- MiniMax M2.x (`minimax/MiniMax-M2*`) on the Anthropic-compatible streaming path defaults to `thinking: { type: "disabled" }` unless you explicitly set thinking in model params or request params. This avoids leaked `reasoning_content` deltas from M2.x's non-native Anthropic stream format. MiniMax-M3 (and M3.x) is exempt: M3 emits proper Anthropic thinking blocks and returns empty content when thinking is disabled, so OpenClaw keeps M3 on the provider's omitted/adaptive thinking path.
- Z.AI (`zai/*`) only supports binary thinking (`on`/`off`). Any non-`off` level is treated as `on` (mapped to `low`).
- Moonshot Kimi K2.7 Code (`moonshot/kimi-k2.7-code`) always thinks. Its profile exposes only `on`, and OpenClaw omits the outbound `thinking` field as required by Moonshot. Other `moonshot/*` models map `/think off` to `thinking: { type: "disabled" }` and any non-`off` level to `thinking: { type: "enabled" }`. When thinking is enabled, Moonshot only accepts `tool_choice` `auto|none`; OpenClaw normalizes incompatible values to `auto`.
- Moonshot (`moonshot/*`) maps `/think off` to `thinking: { type: "disabled" }` and any non-`off` level to `thinking: { type: "enabled" }`. When thinking is enabled, Moonshot only accepts `tool_choice` `auto|none`; OpenClaw normalizes incompatible values to `auto`.
## Resolution order

View File

@@ -1,12 +1,12 @@
{
"name": "@openclaw/acpx",
"version": "2026.6.2",
"version": "2026.6.7-alpha.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@openclaw/acpx",
"version": "2026.6.2",
"version": "2026.6.7-alpha.3",
"dependencies": {
"@agentclientprotocol/claude-agent-acp": "0.39.0",
"@zed-industries/codex-acp": "0.15.0",
@@ -224,9 +224,9 @@
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz",
"integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz",
"integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==",
"cpu": [
"ppc64"
],
@@ -240,9 +240,9 @@
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz",
"integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz",
"integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==",
"cpu": [
"arm"
],
@@ -256,9 +256,9 @@
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz",
"integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz",
"integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==",
"cpu": [
"arm64"
],
@@ -272,9 +272,9 @@
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz",
"integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz",
"integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==",
"cpu": [
"x64"
],
@@ -288,9 +288,9 @@
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz",
"integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz",
"integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==",
"cpu": [
"arm64"
],
@@ -304,9 +304,9 @@
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz",
"integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz",
"integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==",
"cpu": [
"x64"
],
@@ -320,9 +320,9 @@
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz",
"integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz",
"integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==",
"cpu": [
"arm64"
],
@@ -336,9 +336,9 @@
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz",
"integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz",
"integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==",
"cpu": [
"x64"
],
@@ -352,9 +352,9 @@
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz",
"integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz",
"integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==",
"cpu": [
"arm"
],
@@ -368,9 +368,9 @@
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz",
"integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz",
"integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==",
"cpu": [
"arm64"
],
@@ -384,9 +384,9 @@
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz",
"integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz",
"integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==",
"cpu": [
"ia32"
],
@@ -400,9 +400,9 @@
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz",
"integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz",
"integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==",
"cpu": [
"loong64"
],
@@ -416,9 +416,9 @@
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz",
"integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz",
"integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==",
"cpu": [
"mips64el"
],
@@ -432,9 +432,9 @@
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz",
"integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz",
"integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==",
"cpu": [
"ppc64"
],
@@ -448,9 +448,9 @@
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz",
"integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz",
"integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==",
"cpu": [
"riscv64"
],
@@ -464,9 +464,9 @@
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz",
"integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz",
"integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==",
"cpu": [
"s390x"
],
@@ -480,9 +480,9 @@
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz",
"integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz",
"integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==",
"cpu": [
"x64"
],
@@ -496,9 +496,9 @@
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz",
"integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz",
"integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==",
"cpu": [
"arm64"
],
@@ -512,9 +512,9 @@
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz",
"integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz",
"integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==",
"cpu": [
"x64"
],
@@ -528,9 +528,9 @@
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz",
"integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz",
"integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==",
"cpu": [
"arm64"
],
@@ -544,9 +544,9 @@
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz",
"integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz",
"integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==",
"cpu": [
"x64"
],
@@ -560,9 +560,9 @@
}
},
"node_modules/@esbuild/openharmony-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz",
"integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz",
"integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==",
"cpu": [
"arm64"
],
@@ -576,9 +576,9 @@
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz",
"integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz",
"integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==",
"cpu": [
"x64"
],
@@ -592,9 +592,9 @@
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz",
"integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz",
"integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==",
"cpu": [
"arm64"
],
@@ -608,9 +608,9 @@
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz",
"integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz",
"integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==",
"cpu": [
"ia32"
],
@@ -624,9 +624,9 @@
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz",
"integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz",
"integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==",
"cpu": [
"x64"
],
@@ -1208,9 +1208,9 @@
}
},
"node_modules/esbuild": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz",
"integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz",
"integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==",
"hasInstallScript": true,
"license": "MIT",
"bin": {
@@ -1220,32 +1220,32 @@
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.28.1",
"@esbuild/android-arm": "0.28.1",
"@esbuild/android-arm64": "0.28.1",
"@esbuild/android-x64": "0.28.1",
"@esbuild/darwin-arm64": "0.28.1",
"@esbuild/darwin-x64": "0.28.1",
"@esbuild/freebsd-arm64": "0.28.1",
"@esbuild/freebsd-x64": "0.28.1",
"@esbuild/linux-arm": "0.28.1",
"@esbuild/linux-arm64": "0.28.1",
"@esbuild/linux-ia32": "0.28.1",
"@esbuild/linux-loong64": "0.28.1",
"@esbuild/linux-mips64el": "0.28.1",
"@esbuild/linux-ppc64": "0.28.1",
"@esbuild/linux-riscv64": "0.28.1",
"@esbuild/linux-s390x": "0.28.1",
"@esbuild/linux-x64": "0.28.1",
"@esbuild/netbsd-arm64": "0.28.1",
"@esbuild/netbsd-x64": "0.28.1",
"@esbuild/openbsd-arm64": "0.28.1",
"@esbuild/openbsd-x64": "0.28.1",
"@esbuild/openharmony-arm64": "0.28.1",
"@esbuild/sunos-x64": "0.28.1",
"@esbuild/win32-arm64": "0.28.1",
"@esbuild/win32-ia32": "0.28.1",
"@esbuild/win32-x64": "0.28.1"
"@esbuild/aix-ppc64": "0.28.0",
"@esbuild/android-arm": "0.28.0",
"@esbuild/android-arm64": "0.28.0",
"@esbuild/android-x64": "0.28.0",
"@esbuild/darwin-arm64": "0.28.0",
"@esbuild/darwin-x64": "0.28.0",
"@esbuild/freebsd-arm64": "0.28.0",
"@esbuild/freebsd-x64": "0.28.0",
"@esbuild/linux-arm": "0.28.0",
"@esbuild/linux-arm64": "0.28.0",
"@esbuild/linux-ia32": "0.28.0",
"@esbuild/linux-loong64": "0.28.0",
"@esbuild/linux-mips64el": "0.28.0",
"@esbuild/linux-ppc64": "0.28.0",
"@esbuild/linux-riscv64": "0.28.0",
"@esbuild/linux-s390x": "0.28.0",
"@esbuild/linux-x64": "0.28.0",
"@esbuild/netbsd-arm64": "0.28.0",
"@esbuild/netbsd-x64": "0.28.0",
"@esbuild/openbsd-arm64": "0.28.0",
"@esbuild/openbsd-x64": "0.28.0",
"@esbuild/openharmony-arm64": "0.28.0",
"@esbuild/sunos-x64": "0.28.0",
"@esbuild/win32-arm64": "0.28.0",
"@esbuild/win32-ia32": "0.28.0",
"@esbuild/win32-x64": "0.28.0"
}
},
"node_modules/escape-html": {

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/acpx",
"version": "2026.6.2",
"version": "2026.6.7-alpha.3",
"description": "OpenClaw ACP runtime backend with plugin-owned session and transport management.",
"repository": {
"type": "git",
@@ -26,10 +26,10 @@
"minHostVersion": ">=2026.4.25"
},
"compat": {
"pluginApi": ">=2026.6.2"
"pluginApi": ">=2026.6.7-alpha.3"
},
"build": {
"openclawVersion": "2026.6.2",
"openclawVersion": "2026.6.7-alpha.3",
"staticAssets": [
{
"source": "./src/runtime-internals/mcp-proxy.mjs",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/admin-http-rpc",
"version": "2026.6.2",
"version": "2026.6.7-alpha.3",
"private": true,
"description": "OpenClaw admin HTTP RPC endpoint",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/alibaba-provider",
"version": "2026.6.2",
"version": "2026.6.7-alpha.3",
"private": true,
"description": "OpenClaw Alibaba Model Studio video provider plugin",
"type": "module",

View File

@@ -477,21 +477,11 @@ describe("bedrock mantle discovery", () => {
expect(provider?.api).toBe("openai-completions");
expect(provider?.auth).toBe("api-key");
expect(provider?.apiKey).toBe("env:AWS_BEARER_TOKEN_BEDROCK");
expect(provider?.models).toHaveLength(3);
expect(provider?.models).toHaveLength(2);
const opus = provider?.models?.find((model) => model.id === "anthropic.claude-opus-4-7");
expect(opus?.api).toBe("anthropic-messages");
expect(opus?.reasoning).toBe(false);
expect(opus).not.toHaveProperty("baseUrl");
const mythos = provider?.models?.find(
(model) => model.id === "anthropic.claude-mythos-preview",
);
expect(mythos).toMatchObject({
api: "anthropic-messages",
reasoning: true,
params: { canonicalModelId: "claude-mythos-preview" },
contextWindow: 1_000_000,
maxTokens: 128_000,
});
});
it("returns null when no auth is available", async () => {

View File

@@ -404,17 +404,6 @@ export async function resolveImplicitMantleProvider(params: {
contextWindow: 1_000_000,
maxTokens: 128_000,
},
{
id: "anthropic.claude-mythos-preview",
name: "Claude Mythos Preview",
api: "anthropic-messages" as const,
reasoning: true,
params: { canonicalModelId: "claude-mythos-preview" },
input: ["text", "image"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 1_000_000,
maxTokens: 128_000,
},
];
const allModels = [...models, ...claudeModels];

View File

@@ -6,7 +6,7 @@ import {
resolveMantleAnthropicBaseUrl,
} from "./mantle-anthropic.runtime.js";
function createTestModel(overrides: Partial<Model> = {}): Model {
function createTestModel(): Model {
return {
id: "anthropic.claude-opus-4-7",
name: "Claude Opus 4.7",
@@ -21,7 +21,6 @@ function createTestModel(overrides: Partial<Model> = {}): Model {
cost: { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
contextWindow: 1_000_000,
maxTokens: 128_000,
...overrides,
} as Model;
}
@@ -113,69 +112,6 @@ describe("createMantleAnthropicStreamFn", () => {
expect(streamOptions.thinkingEnabled).toBe(false);
});
it("defaults Mythos Preview to adaptive high effort", () => {
const model = createTestModel({
id: "anthropic.claude-mythos-preview",
name: "Claude Mythos Preview",
reasoning: true,
params: { canonicalModelId: "claude-mythos-preview" },
});
const context = { messages: [] };
const deps = createTestDeps();
deps.stream.mockReturnValue({ kind: "anthropic-stream" } as never);
void createMantleAnthropicStreamFn(deps)(model, context, {
apiKey: "bedrock-bearer-token",
});
expectFirstStreamCall(deps, model, context);
const streamOptions = firstStreamOptions(deps);
expect(streamOptions.thinkingEnabled).toBe(true);
expect(streamOptions.effort).toBe("high");
});
it("clamps unsupported Mythos Preview max effort to high", () => {
const model = createTestModel({
id: "anthropic.claude-mythos-preview",
name: "Claude Mythos Preview",
reasoning: true,
params: { canonicalModelId: "claude-mythos-preview" },
});
const context = { messages: [] };
const deps = createTestDeps();
deps.stream.mockReturnValue({ kind: "anthropic-stream" } as never);
void createMantleAnthropicStreamFn(deps)(model, context, {
apiKey: "bedrock-bearer-token",
reasoning: "max",
});
expectFirstStreamCall(deps, model, context);
const streamOptions = firstStreamOptions(deps);
expect(streamOptions.thinkingEnabled).toBe(true);
expect(streamOptions.effort).toBe("high");
});
it("maps Mythos Preview minimal reasoning to low effort", () => {
const model = createTestModel({
id: "anthropic.claude-mythos-preview",
name: "Claude Mythos Preview",
reasoning: true,
params: { canonicalModelId: "claude-mythos-preview" },
});
const deps = createTestDeps();
deps.stream.mockReturnValue({ kind: "anthropic-stream" } as never);
void createMantleAnthropicStreamFn(deps)(model, { messages: [] }, {
apiKey: "bedrock-bearer-token",
reasoning: "minimal",
});
const streamOptions = firstStreamOptions(deps);
expect(streamOptions.thinkingEnabled).toBe(true);
expect(streamOptions.effort).toBe("low");
});
it("normalizes Mantle provider URLs to the Anthropic endpoint", () => {
expect(resolveMantleAnthropicBaseUrl("https://bedrock-mantle.us-east-1.api.aws/v1")).toBe(
"https://bedrock-mantle.us-east-1.api.aws/anthropic",

View File

@@ -27,36 +27,6 @@ function requiresDefaultSampling(modelId: string): boolean {
return modelId.includes("claude-opus-4-7");
}
function isClaudeMythosPreviewModel(model: Model): boolean {
return [model.id, model.name, model.params?.canonicalModelId]
.filter((value): value is string => typeof value === "string")
.some((value) =>
/(?:^|-)claude-mythos-preview(?=$|[^a-z0-9])/.test(
value
.trim()
.toLowerCase()
.replace(/[\s_.:]+/g, "-"),
),
);
}
function resolveMantleReasoning(
model: Model,
options: SimpleStreamOptions | undefined,
): NonNullable<SimpleStreamOptions["reasoning"]> | undefined {
if (requiresDefaultSampling(model.id)) {
return undefined;
}
const reasoning = options?.reasoning ?? (isClaudeMythosPreviewModel(model) ? "high" : undefined);
if (!isClaudeMythosPreviewModel(model)) {
return reasoning;
}
if (reasoning === "minimal") {
return "low";
}
return reasoning === "xhigh" || reasoning === "max" ? "high" : reasoning;
}
function mergeHeaders(
...headerSources: Array<Record<string, string> | undefined>
): Record<string, string> {
@@ -139,8 +109,7 @@ export function createMantleAnthropicStreamFn(deps?: {
// Plugin package deps can give this plugin a distinct physical SDK copy.
// The client API is the same, but the SDK class private field makes types nominal.
const streamClient = client as unknown as AnthropicStreamClient;
const reasoning = resolveMantleReasoning(model, options);
if (!reasoning) {
if (!options?.reasoning || requiresDefaultSampling(model.id)) {
return streamFn(model as Model<"anthropic-messages">, context, {
...base,
client: streamClient,
@@ -151,15 +120,14 @@ export function createMantleAnthropicStreamFn(deps?: {
const adjusted = adjustMaxTokensForThinking(
base.maxTokens || 0,
model.maxTokens,
reasoning,
options?.thinkingBudgets,
options.reasoning,
options.thinkingBudgets,
);
return streamFn(model as Model<"anthropic-messages">, context, {
...base,
client: streamClient,
maxTokens: adjusted.maxTokens,
thinkingEnabled: true,
...(isClaudeMythosPreviewModel(model) ? { effort: reasoning } : {}),
thinkingBudgetTokens: adjusted.thinkingBudget,
});
};

View File

@@ -1,12 +1,12 @@
{
"name": "@openclaw/amazon-bedrock-mantle-provider",
"version": "2026.6.2",
"version": "2026.6.7-alpha.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@openclaw/amazon-bedrock-mantle-provider",
"version": "2026.6.2",
"version": "2026.6.7-alpha.3",
"dependencies": {
"@anthropic-ai/sdk": "0.100.1",
"@aws/bedrock-token-generator": "1.1.0"

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/amazon-bedrock-mantle-provider",
"version": "2026.6.2",
"version": "2026.6.7-alpha.3",
"description": "OpenClaw Amazon Bedrock Mantle provider plugin for OpenAI-compatible model routing.",
"repository": {
"type": "git",
@@ -24,10 +24,10 @@
"minHostVersion": ">=2026.5.12-beta.1"
},
"compat": {
"pluginApi": ">=2026.6.2"
"pluginApi": ">=2026.6.7-alpha.3"
},
"build": {
"openclawVersion": "2026.6.2",
"openclawVersion": "2026.6.7-alpha.3",
"bundledDist": false
},
"release": {

View File

@@ -166,65 +166,6 @@ describe("bedrock discovery", () => {
});
});
it("marks known Fable inference profile fallbacks as reasoning capable", async () => {
sendMock
.mockResolvedValueOnce({
modelSummaries: [],
})
.mockResolvedValueOnce({
inferenceProfileSummaries: [
{
inferenceProfileId: "us.anthropic.claude-fable-5",
inferenceProfileName: "US Claude Fable 5",
status: "ACTIVE",
type: "SYSTEM_DEFINED",
models: [
{
modelArn: "arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-fable-5",
},
],
},
],
});
const models = await discoverBedrockModels({ region: "us-east-1", clientFactory });
expect(models).toHaveLength(1);
expectModelFields(models[0], {
id: "us.anthropic.claude-fable-5",
reasoning: true,
contextWindow: 1_000_000,
thinkingLevelMap: { off: "low", minimal: "low", xhigh: "xhigh", max: "max" },
});
});
it("skips Mythos Preview inference profiles because Mantle owns that route", async () => {
sendMock
.mockResolvedValueOnce({
modelSummaries: [],
})
.mockResolvedValueOnce({
inferenceProfileSummaries: [
{
inferenceProfileId: "us.anthropic.claude-mythos-preview",
inferenceProfileName: "US Claude Mythos Preview",
status: "ACTIVE",
type: "SYSTEM_DEFINED",
models: [
{
modelArn:
"arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-mythos-preview",
},
],
},
],
});
const models = await discoverBedrockModels({ region: "us-east-1", clientFactory });
expect(models).toEqual([]);
});
it("normalizes region-prefixed versioned model ids when resolving context windows", async () => {
sendMock
.mockResolvedValueOnce({

View File

@@ -157,13 +157,6 @@ function resolveKnownContextWindow(modelId: string): number | undefined {
return undefined;
}
function isKnownClaudeMythosPreviewModelId(modelId: string): boolean {
const stripped = modelId.replace(/^(?:us|eu|ap|apac|au|jp|global)\./, "");
return [modelId, stripped].some((candidate) =>
/(?:^|[/.:])anthropic\.claude-mythos-preview(?:$|[-.:/])/i.test(candidate),
);
}
function resolveKnownThinkingLevelMap(
modelId: string,
): ModelDefinitionConfig["thinkingLevelMap"] | undefined {
@@ -329,9 +322,6 @@ function shouldIncludeSummary(summary: BedrockModelSummary, filter: string[]): b
if (summary.responseStreamingSupported !== true) {
return false;
}
if (isKnownClaudeMythosPreviewModelId(summary.modelId)) {
return false;
}
if (!includesTextModalities(summary.outputModalities)) {
return false;
}
@@ -464,9 +454,6 @@ function resolveInferenceProfiles(
// Look up the underlying foundation model to inherit its capabilities.
const baseModelId = resolveBaseModelId(profile);
if (isKnownClaudeMythosPreviewModelId(baseModelId ?? profile.inferenceProfileId)) {
continue;
}
const baseModel = baseModelId
? foundationModels.get(normalizeLowercaseStringOrEmpty(baseModelId))
: undefined;

View File

@@ -835,44 +835,6 @@ describe("amazon-bedrock provider plugin", () => {
expect(payload.inferenceConfig).toEqual({});
});
it("does not re-upgrade Mythos Preview max thinking in the final payload", async () => {
const provider = await registerSingleProviderPlugin(amazonBedrockPlugin);
const wrapped = provider.wrapStreamFn?.({
provider: "amazon-bedrock",
modelId: "us.anthropic.claude-mythos-preview",
streamFn: spyStreamFn,
thinkingLevel: "max",
} as never);
const result = wrapped?.(
{
api: "bedrock-converse-stream",
provider: "amazon-bedrock",
id: "us.anthropic.claude-mythos-preview",
name: "Claude Mythos Preview",
reasoning: true,
} as never,
{ messages: [] } as never,
{ reasoning: "max" } as never,
) as Record<string, unknown> | undefined;
const payload = {
inferenceConfig: { temperature: 0.2 },
additionalModelRequestFields: {
thinking: { type: "adaptive" },
output_config: { effort: "high" },
},
};
await (result?.onPayload as ((p: Record<string, unknown>) => unknown) | undefined)?.(payload);
expect(payload.additionalModelRequestFields).toEqual({
thinking: { type: "adaptive" },
output_config: { effort: "high" },
});
expect(payload.inferenceConfig).toEqual({});
});
it("classifies nested Bedrock deprecated-temperature validation as format failover", async () => {
const provider = await registerSingleProviderPlugin(amazonBedrockPlugin);

View File

@@ -1,12 +1,12 @@
{
"name": "@openclaw/amazon-bedrock-provider",
"version": "2026.6.2",
"version": "2026.6.7-alpha.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@openclaw/amazon-bedrock-provider",
"version": "2026.6.2",
"version": "2026.6.7-alpha.3",
"dependencies": {
"@aws-sdk/client-bedrock": "3.1056.0",
"@aws-sdk/client-bedrock-runtime": "3.1056.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/amazon-bedrock-provider",
"version": "2026.6.2",
"version": "2026.6.7-alpha.3",
"description": "OpenClaw Amazon Bedrock provider plugin with model discovery, embeddings, and guardrail support.",
"repository": {
"type": "git",
@@ -28,10 +28,10 @@
"minHostVersion": ">=2026.5.12-beta.1"
},
"compat": {
"pluginApi": ">=2026.6.2"
"pluginApi": ">=2026.6.7-alpha.3"
},
"build": {
"openclawVersion": "2026.6.2",
"openclawVersion": "2026.6.7-alpha.3",
"bundledDist": false
},
"release": {

View File

@@ -23,7 +23,6 @@ import { mergeImplicitBedrockProvider, resolveBedrockConfigApiKey } from "./disc
import { bedrockMemoryEmbeddingProviderAdapter } from "./memory-embedding-adapter.js";
import { streamBedrock, streamSimpleBedrock } from "./stream.runtime.js";
import {
isLatestAdaptiveBedrockModelRef,
isOpus47OrNewerBedrockModelRef,
resolveBedrockNativeThinkingLevelMap,
resolveBedrockClaudeThinkingProfile,
@@ -597,10 +596,8 @@ export function registerAmazonBedrockPlugin(api: OpenClawPluginApi): void {
currentPluginConfig?.discovery?.region;
const mayNeedCacheInjection =
isBedrockAppInferenceProfile(modelId) && !sharedRuntimeWouldInjectCachePoints(modelId);
const shouldOmitTemperature =
opus47OrNewer || fable5 || isLatestAdaptiveBedrockModelRef(modelId, model?.params);
const shouldOmitTemperature = opus47OrNewer || fable5;
const shouldPatchMaxThinking = supportsNativeMax && thinkingLevel === "max";
const shouldPatchPayload = shouldOmitTemperature || shouldPatchMaxThinking;
// For known Anthropic models (heuristic match), enable injection immediately.
// For opaque profile IDs, we'll resolve via GetInferenceProfile on first call.
@@ -630,17 +627,13 @@ export function registerAmazonBedrockPlugin(api: OpenClawPluginApi): void {
context,
withAwsCredentialRefreshOnPayload({
...merged,
...(shouldPatchPayload
...(shouldPatchMaxThinking
? {
onPayload: (payload: unknown, payloadModel: unknown) => {
if (payload && typeof payload === "object") {
const payloadRecord = payload as Record<string, unknown>;
if (shouldPatchMaxThinking) {
patchMaxThinkingEffort(payloadRecord);
}
if (shouldOmitTemperature) {
omitUnsupportedClaudePayloadTemperature(payloadRecord);
}
patchMaxThinkingEffort(payloadRecord);
omitUnsupportedClaudePayloadTemperature(payloadRecord);
}
return originalOnPayload?.(payload, payloadModel);
},

View File

@@ -167,34 +167,7 @@ describe("Bedrock profile endpoint resolution", () => {
});
describe("Bedrock thinking effort mapping", () => {
it("does not force adaptive thinking for optional Claude models when callers omit reasoning", () => {
const model = bedrockModel({
id: "anthropic.claude-sonnet-4-6-v1:0",
name: "Claude Sonnet 4.6",
reasoning: true,
});
const options = testing.resolveSimpleBedrockOptions(model, {});
expect(options.reasoning).toBeUndefined();
expect(testing.buildAdditionalModelRequestFields(model, options)).toBeUndefined();
});
it("forces adaptive thinking for Bedrock Mythos Preview when callers omit reasoning", () => {
const model = bedrockModel({
id: "us.anthropic.claude-mythos-preview",
name: "US Claude Mythos Preview",
reasoning: true,
});
const options = testing.resolveSimpleBedrockOptions(model, {});
expect(options.reasoning).toBe("high");
expect(testing.buildAdditionalModelRequestFields(model, options)).toEqual({
thinking: { type: "adaptive", display: "summarized" },
output_config: { effort: "high" },
});
});
it("clamps max effort for Claude models without native max support", () => {
it("caps max effort at high for Claude Sonnet 4.6", () => {
expect(
testing.mapThinkingLevelToEffort(
bedrockModel({

View File

@@ -351,38 +351,29 @@ export const streamSimpleBedrock: StreamFunction<"bedrock-converse-stream", Simp
model: Model<"bedrock-converse-stream">,
context: Context,
options?: SimpleStreamOptions,
) => streamBedrock(model, context, resolveSimpleBedrockOptions(model, options));
function resolveSimpleBedrockOptions(
model: Model<"bedrock-converse-stream">,
options?: SimpleStreamOptions,
): BedrockOptions {
) => {
const base = buildBaseOptions(model, options, undefined);
if (usesClaudeFable5BedrockContract(model)) {
return {
return streamBedrock(model, context, {
...base,
reasoning: options?.reasoning ?? "high",
thinkingBudgets: options?.thinkingBudgets,
} satisfies BedrockOptions;
} satisfies BedrockOptions);
}
if (!options?.reasoning) {
const reasoning =
isAnthropicClaudeModel(model) && requiresMandatoryAdaptiveThinking(model)
? "high"
: undefined;
return {
return streamBedrock(model, context, {
...base,
reasoning,
} satisfies BedrockOptions;
reasoning: undefined,
} satisfies BedrockOptions);
}
if (isAnthropicClaudeModel(model)) {
if (supportsAdaptiveThinking(model)) {
return {
return streamBedrock(model, context, {
...base,
reasoning: options.reasoning,
thinkingBudgets: options.thinkingBudgets,
} satisfies BedrockOptions;
} satisfies BedrockOptions);
}
// Undefined means the caller did not request an output cap; let the helper use the model cap.
@@ -394,7 +385,7 @@ function resolveSimpleBedrockOptions(
options.thinkingBudgets,
);
return {
return streamBedrock(model, context, {
...base,
maxTokens: adjusted.maxTokens,
reasoning: options.reasoning,
@@ -402,15 +393,15 @@ function resolveSimpleBedrockOptions(
...options.thinkingBudgets,
[clampReasoning(options.reasoning)!]: adjusted.thinkingBudget,
},
} satisfies BedrockOptions;
} satisfies BedrockOptions);
}
return {
return streamBedrock(model, context, {
...base,
reasoning: options.reasoning,
thinkingBudgets: options.thinkingBudgets,
} satisfies BedrockOptions;
}
} satisfies BedrockOptions);
};
function handleContentBlockStart(
event: ContentBlockStartEvent,
@@ -562,37 +553,15 @@ function resolveClaudeProfileNameModelId(modelName?: string): string | undefined
if (!normalized.includes("claude")) {
return undefined;
}
const family = /(?:fable-5|mythos-preview|opus-4-(?:6|7|8)|sonnet-4-6)(?:$|-)/.exec(
normalized,
)?.[0];
const family = /(?:fable-5|opus-4-(?:6|7|8)|sonnet-4-6)(?:$|-)/.exec(normalized)?.[0];
return family ? `claude-${family.replace(/-$/, "")}` : undefined;
}
function isClaudeMythosPreviewModelId(modelId?: string): boolean {
return /(?:^|-)claude-mythos-preview(?=$|[^a-z0-9])/.test(
modelId
?.trim()
.toLowerCase()
.replace(/[\s_.:]+/g, "-") ?? "",
);
}
/** Check canonical metadata and profile names for adaptive Claude support. */
function supportsAdaptiveThinking(model: Model<"bedrock-converse-stream">): boolean {
const profileModelId = resolveClaudeProfileNameModelId(model.name);
return (
supportsClaudeAdaptiveThinking(model) ||
supportsClaudeAdaptiveThinking({ id: profileModelId }) ||
isClaudeMythosPreviewModelId(resolveClaudeModelIdentity(model)) ||
isClaudeMythosPreviewModelId(profileModelId)
);
}
function requiresMandatoryAdaptiveThinking(model: Model<"bedrock-converse-stream">): boolean {
const profileModelId = resolveClaudeProfileNameModelId(model.name);
return (
isClaudeMythosPreviewModelId(resolveClaudeModelIdentity(model)) ||
isClaudeMythosPreviewModelId(profileModelId)
supportsClaudeAdaptiveThinking(model) || supportsClaudeAdaptiveThinking({ id: profileModelId })
);
}
@@ -1102,11 +1071,9 @@ function createImageBlock(mimeType: string, data: string) {
/** Test-only hooks for Bedrock runtime conversion and endpoint policy. */
export const testing = {
buildAdditionalModelRequestFields,
convertMessages,
getConfiguredBedrockRegion,
hasConfiguredBedrockProfile,
mapThinkingLevelToEffort,
resolveSimpleBedrockOptions,
shouldUseExplicitBedrockEndpoint,
};

View File

@@ -43,28 +43,6 @@ export function isOpus47OrNewerBedrockModelRef(modelRef: string): boolean {
return isOpus47BedrockModelRef(modelRef) || isOpus48BedrockModelRef(modelRef);
}
function isMythosPreviewBedrockModelRef(modelRef: string): boolean {
return /(?:^|[/.:])(?:(?:us|eu|ap|apac|au|jp|global)\.)?(?:anthropic\.)?claude-mythos-preview(?:$|[-.:/])/i.test(
modelRef,
);
}
/** Return whether a Bedrock Claude ref needs latest adaptive-thinking request shaping. */
export function isLatestAdaptiveBedrockModelRef(
modelId: string,
params?: Record<string, unknown>,
): boolean {
const modelRef = { id: modelId, params };
const canonicalModelId = resolveClaudeModelIdentity(modelRef);
return (
resolveClaudeFable5ModelIdentity(modelRef) !== undefined ||
[modelId, canonicalModelId].some(
(candidate) =>
isOpus47OrNewerBedrockModelRef(candidate) || isMythosPreviewBedrockModelRef(candidate),
)
);
}
/** Return whether a Bedrock Claude ref supports max effort. */
export function supportsBedrockNativeMaxEffort(
modelId: string,
@@ -131,12 +109,6 @@ export function resolveBedrockClaudeThinkingProfile(
defaultLevel: "adaptive",
};
}
if (modelRefs.some(isMythosPreviewBedrockModelRef)) {
return {
levels: [...BASE_CLAUDE_THINKING_LEVELS, { id: "adaptive" }],
defaultLevel: "adaptive",
};
}
if (modelRefs.some((modelRef) => /claude-sonnet-4(?:\.|-)6(?:$|[-.])/i.test(modelRef))) {
return {
levels: [...BASE_CLAUDE_THINKING_LEVELS, { id: "adaptive" }],

View File

@@ -1,12 +1,12 @@
{
"name": "@openclaw/anthropic-vertex-provider",
"version": "2026.6.2",
"version": "2026.6.7-alpha.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@openclaw/anthropic-vertex-provider",
"version": "2026.6.2",
"version": "2026.6.7-alpha.3",
"dependencies": {
"@anthropic-ai/vertex-sdk": "0.16.1"
}

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/anthropic-vertex-provider",
"version": "2026.6.2",
"version": "2026.6.7-alpha.3",
"description": "OpenClaw Anthropic Vertex provider plugin for Claude models on Google Vertex AI.",
"repository": {
"type": "git",
@@ -23,10 +23,10 @@
"minHostVersion": ">=2026.5.12-beta.1"
},
"compat": {
"pluginApi": ">=2026.6.2"
"pluginApi": ">=2026.6.7-alpha.3"
},
"build": {
"openclawVersion": "2026.6.2",
"openclawVersion": "2026.6.7-alpha.3",
"bundledDist": false
},
"release": {

View File

@@ -3,6 +3,8 @@ import { createAssistantMessageEventStream, type Model } from "openclaw/plugin-s
import { beforeAll, describe, expect, it, vi } from "vitest";
import type { AnthropicVertexStreamDeps } from "./stream-runtime.js";
const SYSTEM_PROMPT_CACHE_BOUNDARY = "\n<!-- OPENCLAW_CACHE_BOUNDARY -->\n";
function createStreamDeps(): {
deps: AnthropicVertexStreamDeps;
streamAnthropicMock: ReturnType<typeof vi.fn>;
@@ -48,6 +50,8 @@ function makeModel(params: {
} as Model<"anthropic-messages">;
}
const CACHE_BOUNDARY_PROMPT = `Stable prefix${SYSTEM_PROMPT_CACHE_BOUNDARY}Dynamic suffix`;
type PayloadHook = (payload: unknown, payloadModel: unknown) => Promise<unknown>;
function streamAnthropicCall(streamAnthropicMock: ReturnType<typeof vi.fn>): unknown[] {
@@ -68,8 +72,8 @@ function streamTransportOptions(
return options as Record<string, unknown>;
}
function captureTransportPayloadHook(
onPayload: PayloadHook | undefined,
function captureCacheBoundaryPayloadHook(
onPayload: PayloadHook,
deps: AnthropicVertexStreamDeps,
streamAnthropicMock: ReturnType<typeof vi.fn>,
) {
@@ -78,8 +82,14 @@ function captureTransportPayloadHook(
void streamFn(
model,
{ messages: [{ role: "user", content: "Hello" }] } as never,
{ cacheRetention: "short", ...(onPayload ? { onPayload } : {}) } as never,
{
systemPrompt: CACHE_BOUNDARY_PROMPT,
messages: [{ role: "user", content: "Hello" }],
} as never,
{
cacheRetention: "short",
onPayload,
} as never,
);
const transportOptions = streamTransportOptions(streamAnthropicMock);
@@ -87,30 +97,26 @@ function captureTransportPayloadHook(
return { model, onPayload: transportOptions.onPayload as PayloadHook | undefined };
}
// Mirrors the shared anthropic-messages transport output: cache boundary already
// split (uncached dynamic suffix) and all four cache_control markers allocated.
function buildBudgetedTransportPayload() {
function buildExpectedCacheBoundaryPayload(messageText: string) {
return {
system: [
{ type: "text", text: "Stable prefix", cache_control: { type: "ephemeral" } },
{ type: "text", text: "Dynamic suffix" },
],
tools: [
{ name: "exec", input_schema: { type: "object" }, cache_control: { type: "ephemeral" } },
{
type: "text",
text: "Stable prefix",
cache_control: { type: "ephemeral" },
},
{
type: "text",
text: "Dynamic suffix",
},
],
messages: [
{
role: "user",
content: [{ type: "text", text: "Hello", cache_control: { type: "ephemeral" } }],
},
{ role: "assistant", content: [{ type: "tool_use", id: "t1", name: "exec", input: {} }] },
{
role: "user",
content: [
{
type: "tool_result",
tool_use_id: "t1",
content: [],
type: "text",
text: messageText,
cache_control: { type: "ephemeral" },
},
],
@@ -119,29 +125,6 @@ function buildBudgetedTransportPayload() {
};
}
function countCacheControlMarkers(payload: unknown): number {
let count = 0;
const visit = (value: unknown) => {
if (Array.isArray(value)) {
value.forEach(visit);
return;
}
if (!value || typeof value !== "object") {
return;
}
const record = value as Record<string, unknown>;
if (record.cache_control !== undefined) {
count += 1;
}
visit(record.content);
};
const record = payload as Record<string, unknown>;
visit(record.system);
visit(record.tools);
visit(record.messages);
return count;
}
describe("createAnthropicVertexStreamFn", () => {
beforeAll(async () => {
({ createAnthropicVertexStreamFn, createAnthropicVertexStreamFnForModel } =
@@ -197,7 +180,7 @@ describe("createAnthropicVertexStreamFn", () => {
expect(streamTransportOptions(streamAnthropicMock).maxTokens).toBe(128000);
});
it.each(["claude-opus-4-8", "claude-opus-4-7", "claude-fable-5", "claude-mythos-5"])(
it.each(["claude-opus-4-8", "claude-opus-4-7"])(
"omits unsupported temperature for %s",
(modelId) => {
const { deps, streamAnthropicMock } = createStreamDeps();
@@ -236,21 +219,6 @@ describe("createAnthropicVertexStreamFn", () => {
expect(streamTransportOptions(streamAnthropicMock)).not.toHaveProperty("temperature");
});
it("uses Mythos 5's mandatory adaptive Vertex contract by default", () => {
const { deps, streamAnthropicMock } = createStreamDeps();
const streamFn = createAnthropicVertexStreamFn("vertex-project", "us-east5", undefined, deps);
const model = makeModel({ id: "claude-mythos-5", maxTokens: 128000 });
void streamFn(model, { messages: [] }, { temperature: 0.7 });
expect(streamTransportOptions(streamAnthropicMock)).toMatchObject({
thinkingEnabled: true,
effort: "high",
maxTokens: 128000,
});
expect(streamTransportOptions(streamAnthropicMock)).not.toHaveProperty("temperature");
});
it("uses canonical Claude policy for Vertex deployment aliases", () => {
const { deps, streamAnthropicMock } = createStreamDeps();
const streamFn = createAnthropicVertexStreamFn("vertex-project", "us-east5", undefined, deps);
@@ -360,35 +328,63 @@ describe("createAnthropicVertexStreamFn", () => {
expect(transportOptions).not.toHaveProperty("temperature");
});
it("keeps already-budgeted cache_control markers intact when forwarding payload hooks", async () => {
it("applies Anthropic cache-boundary shaping before forwarding payload hooks", async () => {
const { deps, streamAnthropicMock } = createStreamDeps();
const onPayload = vi.fn(async (payload: unknown) => payload);
const { model, onPayload: transportPayloadHook } = captureTransportPayloadHook(
const { model, onPayload: transportPayloadHook } = captureCacheBoundaryPayloadHook(
onPayload,
deps,
streamAnthropicMock,
);
const payload = buildBudgetedTransportPayload();
const payload = {
system: [
{
type: "text",
text: CACHE_BOUNDARY_PROMPT,
cache_control: { type: "ephemeral" },
},
],
messages: [{ role: "user", content: "Hello" }],
};
const nextPayload = await transportPayloadHook?.(payload, model);
expect(onPayload).toHaveBeenCalledWith(payload, model);
expect(countCacheControlMarkers(nextPayload)).toBe(4);
expect((nextPayload as ReturnType<typeof buildBudgetedTransportPayload>).system[1]).toEqual({
type: "text",
text: "Dynamic suffix",
});
const expectedPayload = buildExpectedCacheBoundaryPayload("Hello");
expect(onPayload).toHaveBeenCalledWith(expectedPayload, model);
expect(nextPayload).toEqual(expectedPayload);
});
it("omits the transport payload hook when the caller provides none", () => {
it("reapplies Anthropic cache-boundary shaping when payload hooks return a fresh payload", async () => {
const { deps, streamAnthropicMock } = createStreamDeps();
const { onPayload: transportPayloadHook } = captureTransportPayloadHook(
undefined,
const onPayload = vi.fn(async () => ({
system: [
{
type: "text",
text: CACHE_BOUNDARY_PROMPT,
},
],
messages: [{ role: "user", content: "Hello again" }],
}));
const { model, onPayload: transportPayloadHook } = captureCacheBoundaryPayloadHook(
onPayload,
deps,
streamAnthropicMock,
);
expect(transportPayloadHook).toBeUndefined();
const nextPayload = await transportPayloadHook?.(
{
system: [
{
type: "text",
text: CACHE_BOUNDARY_PROMPT,
},
],
messages: [{ role: "user", content: "Hello" }],
},
model,
);
expect(nextPayload).toEqual(buildExpectedCacheBoundaryPayload("Hello again"));
});
it("omits maxTokens when neither the model nor request provide a finite limit", () => {

View File

@@ -1,6 +1,6 @@
/**
* Anthropic Vertex stream runtime. It constructs Vertex SDK clients and adapts
* OpenClaw stream options for the shared Anthropic Messages transport.
* OpenClaw stream options into Anthropic Messages payload policy.
*/
import { AnthropicVertex as AnthropicVertexSdk } from "@anthropic-ai/vertex-sdk";
import type { StreamFn } from "openclaw/plugin-sdk/agent-core";
@@ -18,6 +18,10 @@ import {
supportsClaudeNativeMaxEffort,
supportsClaudeNativeXhighEffort,
} from "openclaw/plugin-sdk/provider-model-shared";
import {
applyAnthropicPayloadPolicyToParams,
resolveAnthropicPayloadPolicy,
} from "openclaw/plugin-sdk/provider-stream-shared";
import { resolveAnthropicVertexClientRegion, resolveAnthropicVertexProjectId } from "./region.js";
type AnthropicVertexTransportOptions = ProviderStreamOptions & {
@@ -54,12 +58,8 @@ function isClaudeFable5Model(modelId: string): boolean {
return resolveClaudeFable5ModelIdentity({ id: modelId }) !== undefined;
}
function isClaudeMythos5Model(modelId: string): boolean {
return /(?:^|-)claude-mythos-5(?=$|[^a-z0-9])/.test(resolveClaudeModelIdentity({ id: modelId }));
}
function supportsAdaptiveThinking(modelId: string): boolean {
return supportsClaudeAdaptiveThinking({ id: modelId }) || isClaudeMythos5Model(modelId);
return supportsClaudeAdaptiveThinking({ id: modelId });
}
function mapAnthropicAdaptiveEffort(
@@ -82,13 +82,10 @@ function mapAnthropicAdaptiveEffort(
high: "high",
xhigh: isClaudeFable5Model(modelId)
? "xhigh"
: isClaudeOpus47OrNewerModel(modelId) || isClaudeMythos5Model(modelId)
: isClaudeOpus47OrNewerModel(modelId)
? "xhigh"
: "high",
max:
supportsClaudeNativeMaxEffort({ id: modelId }) || isClaudeMythos5Model(modelId)
? "max"
: "high",
max: supportsClaudeNativeMaxEffort({ id: modelId }) ? "max" : "high",
};
return effortMap[resolvedReasoning] ?? "high";
}
@@ -116,6 +113,36 @@ function resolveAnthropicVertexMaxTokens(params: {
return requested ?? modelMax;
}
function createAnthropicVertexOnPayload(params: {
model: { api: string; baseUrl?: string; provider: string };
cacheRetention: ProviderStreamOptions["cacheRetention"] | undefined;
onPayload: ProviderStreamOptions["onPayload"] | undefined;
}): NonNullable<ProviderStreamOptions["onPayload"]> {
const policy = resolveAnthropicPayloadPolicy({
provider: params.model.provider,
api: params.model.api,
baseUrl: params.model.baseUrl,
cacheRetention: params.cacheRetention,
enableCacheControl: true,
});
function applyPolicy(payload: unknown): unknown {
if (payload && typeof payload === "object" && !Array.isArray(payload)) {
applyAnthropicPayloadPolicyToParams(payload as Record<string, unknown>, policy);
}
return payload;
}
return async (payload, model) => {
const shapedPayload = applyPolicy(payload);
const nextPayload = await params.onPayload?.(shapedPayload, model);
if (nextPayload === undefined || nextPayload === shapedPayload) {
return shapedPayload;
}
return applyPolicy(nextPayload);
};
}
/**
* Create a StreamFn that routes through OpenClaw's generic model stream with an
* injected `AnthropicVertex` client. All streaming, message conversion, and
@@ -146,16 +173,11 @@ export function createAnthropicVertexStreamFn(
});
const contractModelId = resolveClaudeModelIdentity(model);
const fable5 = isClaudeFable5Model(contractModelId);
const mandatoryAdaptiveThinking = fable5 || isClaudeMythos5Model(contractModelId);
const reasoning =
(options?.reasoning as ModelThinkingLevel | undefined) ??
(mandatoryAdaptiveThinking ? "high" : undefined);
const reasoning = options?.reasoning as ModelThinkingLevel | undefined;
const adaptiveThinking =
mandatoryAdaptiveThinking || Boolean(reasoning && supportsAdaptiveThinking(contractModelId));
fable5 || Boolean(reasoning && supportsAdaptiveThinking(contractModelId));
const temperature =
adaptiveThinking ||
isClaudeOpus47OrNewerModel(contractModelId) ||
isClaudeMythos5Model(contractModelId)
adaptiveThinking || isClaudeOpus47OrNewerModel(contractModelId)
? undefined
: options?.temperature;
const opts: AnthropicVertexTransportOptions = {
@@ -166,10 +188,11 @@ export function createAnthropicVertexStreamFn(
cacheRetention: options?.cacheRetention,
sessionId: options?.sessionId,
headers: options?.headers,
// The shared anthropic-messages transport already splits the system prompt
// cache boundary and budgets all cache_control markers; re-applying the
// payload policy here marked the uncached suffix and breached the 4-marker cap.
onPayload: options?.onPayload,
onPayload: createAnthropicVertexOnPayload({
model: transportModel,
cacheRetention: options?.cacheRetention,
onPayload: options?.onPayload,
}),
maxRetryDelayMs: options?.maxRetryDelayMs,
metadata: options?.metadata,
};

View File

@@ -710,28 +710,6 @@ describe("anthropic provider replay hooks", () => {
expect(resolved).toBeUndefined();
});
it("normalizes Claude Mythos Preview with native max but no xhigh thinking map", async () => {
const provider = await registerSingleProviderPlugin(anthropicPlugin);
const normalized = provider.normalizeResolvedModel?.({
provider: "anthropic",
modelId: "claude-mythos-preview",
model: {
id: "claude-mythos-preview",
name: "Claude Mythos Preview",
provider: "anthropic",
api: "anthropic-messages",
reasoning: true,
input: ["text", "image"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 200_000,
maxTokens: 64_000,
},
} as never);
expect(normalized?.thinkingLevelMap).toEqual({ max: "max" });
});
it("normalizes stale text-only modern Claude vision rows to image-capable", async () => {
const provider = await registerSingleProviderPlugin(anthropicPlugin);

View File

@@ -101,28 +101,6 @@
"contextWindow": 200000,
"maxTokens": 64000
},
{
"id": "claude-haiku-4-5",
"name": "Claude Haiku 4.5",
"reasoning": true,
"input": ["text", "image"],
"mediaInput": {
"image": { "maxSidePx": 1568, "preferredSidePx": 1568, "tokenMode": "provider" }
},
"contextWindow": 200000,
"maxTokens": 64000
},
{
"id": "claude-haiku-4-5-20251001",
"name": "Claude Haiku 4.5",
"reasoning": true,
"input": ["text", "image"],
"mediaInput": {
"image": { "maxSidePx": 1568, "preferredSidePx": 1568, "tokenMode": "provider" }
},
"contextWindow": 200000,
"maxTokens": 64000
},
{
"id": "claude-sonnet-4-6",
"name": "Claude Sonnet 4.6",

View File

@@ -1,57 +0,0 @@
// Anthropic tests cover provider manifest model catalog behavior.
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
type AnthropicManifest = {
modelCatalog?: {
providers?: {
anthropic?: {
models?: Array<{
id?: string;
name?: string;
reasoning?: boolean;
input?: string[];
mediaInput?: {
image?: {
maxSidePx?: number;
preferredSidePx?: number;
tokenMode?: string;
};
};
contextWindow?: number;
maxTokens?: number;
}>;
};
};
discovery?: Record<string, string>;
};
};
const manifest = JSON.parse(
readFileSync(new URL("./openclaw.plugin.json", import.meta.url), "utf8"),
) as AnthropicManifest;
describe("Anthropic plugin manifest", () => {
it("resolves both official Claude Haiku 4.5 API identifiers from the static catalog", () => {
expect(manifest.modelCatalog?.discovery?.anthropic).toBe("static");
const models = manifest.modelCatalog?.providers?.anthropic?.models ?? [];
for (const id of ["claude-haiku-4-5", "claude-haiku-4-5-20251001"]) {
expect(models.find((model) => model.id === id)).toEqual({
id,
name: "Claude Haiku 4.5",
reasoning: true,
input: ["text", "image"],
mediaInput: {
image: {
maxSidePx: 1568,
preferredSidePx: 1568,
tokenMode: "provider",
},
},
contextWindow: 200000,
maxTokens: 64000,
});
}
});
});

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/anthropic-provider",
"version": "2026.6.2",
"version": "2026.6.7-alpha.3",
"private": true,
"description": "OpenClaw Anthropic provider plugin",
"type": "module",

View File

@@ -34,7 +34,6 @@ import {
resolveClaudeModelIdentity,
resolveClaudeThinkingProfile,
supportsClaudeAdaptiveThinking,
supportsClaudeNativeMaxEffort,
supportsClaudeNativeXhighEffort,
} from "openclaw/plugin-sdk/provider-model-shared";
import { fetchClaudeUsage } from "openclaw/plugin-sdk/provider-usage";
@@ -293,11 +292,6 @@ function buildAnthropicForwardCompatModel(
maxTokens: isAnthropic128kOutputModel(trimmedModelId)
? ANTHROPIC_MODERN_MAX_OUTPUT_TOKENS
: 64_000,
...(supportsClaudeNativeXhighEffort({ id: trimmedModelId })
? { thinkingLevelMap: { xhigh: "xhigh", max: "max" } }
: supportsAnthropicNativeMaxEffort(trimmedModelId)
? { thinkingLevelMap: { max: "max" } }
: {}),
};
}
@@ -367,16 +361,6 @@ function isAnthropicOpus47OrNewerModel(modelId: string): boolean {
return supportsClaudeNativeXhighEffort({ id: modelId }) && !isAnthropicFable5Model(modelId);
}
function isAnthropicMythosPreviewModel(modelId: string): boolean {
return /(?:^|-)claude-mythos-preview(?=$|[^a-z0-9])/.test(
resolveClaudeModelIdentity({ id: modelId }),
);
}
function supportsAnthropicNativeMaxEffort(modelId: string): boolean {
return supportsClaudeNativeMaxEffort({ id: modelId }) || isAnthropicMythosPreviewModel(modelId);
}
function hasConfiguredModelContextOverride(
config: ProviderNormalizeResolvedModelContext["config"],
provider: string,
@@ -471,17 +455,15 @@ function applyAnthropicThinkingLevelMap(params: {
}): ProviderRuntimeModel | undefined {
const fable5 = isAnthropicFable5Model(params.modelId);
const nativeXhigh = fable5 || isAnthropicOpus47OrNewerModel(params.modelId);
if (!supportsAnthropicNativeMaxEffort(params.modelId)) {
if (!matchesAnthropicModernModel(params.modelId)) {
return undefined;
}
const current = params.model.thinkingLevelMap;
const nativeDefaults = isAnthropicMythosPreviewModel(params.modelId)
? { max: "max" as const }
: {
...(fable5 ? { off: "low" as const, minimal: "low" as const } : {}),
xhigh: nativeXhigh ? ("xhigh" as const) : null,
max: "max" as const,
};
const nativeDefaults = {
...(fable5 ? { off: "low" as const, minimal: "low" as const } : {}),
xhigh: nativeXhigh ? ("xhigh" as const) : null,
max: "max" as const,
};
const currentEfforts = current as Record<string, string | null | undefined> | undefined;
if (Object.keys(nativeDefaults).every((level) => currentEfforts?.[level] !== undefined)) {
return undefined;
@@ -496,7 +478,7 @@ function applyAnthropicThinkingLevelMap(params: {
}
function matchesAnthropicModernModel(modelId: string): boolean {
return supportsClaudeAdaptiveThinking({ id: modelId }) || isAnthropicMythosPreviewModel(modelId);
return supportsClaudeAdaptiveThinking({ id: modelId });
}
function hasImageInput(input: unknown): boolean {

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/arcee-provider",
"version": "2026.6.2",
"version": "2026.6.7-alpha.3",
"private": true,
"description": "OpenClaw Arcee provider plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/azure-speech",
"version": "2026.6.2",
"version": "2026.6.7-alpha.3",
"private": true,
"description": "OpenClaw Azure Speech plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/bonjour",
"version": "2026.6.2",
"version": "2026.6.7-alpha.3",
"description": "OpenClaw Bonjour/mDNS gateway discovery",
"type": "module",
"dependencies": {

View File

@@ -1,12 +1,12 @@
{
"name": "@openclaw/brave-plugin",
"version": "2026.6.2",
"version": "2026.6.7-alpha.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@openclaw/brave-plugin",
"version": "2026.6.2"
"version": "2026.6.7-alpha.3"
}
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/brave-plugin",
"version": "2026.6.2",
"version": "2026.6.7-alpha.3",
"description": "OpenClaw Brave Search provider plugin for web search.",
"repository": {
"type": "git",
@@ -21,10 +21,10 @@
"allowInvalidConfigRecovery": true
},
"compat": {
"pluginApi": ">=2026.6.2"
"pluginApi": ">=2026.6.7-alpha.3"
},
"build": {
"openclawVersion": "2026.6.2"
"openclawVersion": "2026.6.7-alpha.3"
},
"release": {
"publishToClawHub": true,

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/browser-plugin",
"version": "2026.6.2",
"version": "2026.6.7-alpha.3",
"private": true,
"description": "OpenClaw browser tool plugin",
"type": "module",

View File

@@ -461,24 +461,4 @@ describe("browser manage output", () => {
expect(output).toContain("OK gateway: browser control endpoint reachable");
expect(output).toContain("OK tabs: 1 visible, use tab reference t1");
});
it("prints a readable browser doctor failure when gateway auth SecretRefs are unavailable", async () => {
const error = Object.assign(new Error("gateway.auth.password unavailable"), {
code: "GATEWAY_SECRET_REF_UNAVAILABLE",
name: "GatewaySecretRefUnavailableError",
});
getBrowserManageCallBrowserRequestMock().mockRejectedValueOnce(error);
const program = createBrowserManageProgram();
await expect(program.parseAsync(["browser", "doctor"], { from: "user" })).rejects.toThrow(
"__exit__:1",
);
const output = lastRuntimeLog();
expect(output).toContain(
"FAIL gateway: Gateway auth SecretRef is unavailable in this command path",
);
expect(output).toContain("OPENCLAW_GATEWAY_TOKEN");
expect(output).not.toContain("GatewaySecretRefUnavailableError");
});
});

View File

@@ -152,24 +152,6 @@ function formatDoctorLine(check: BrowserDoctorCheck): string {
return `${check.ok ? "OK" : "FAIL"} ${check.name}${check.detail ? `: ${check.detail}` : ""}`;
}
function isGatewaySecretRefUnavailableErrorShape(error: unknown): boolean {
if (!(error instanceof Error)) {
return false;
}
const errorRecord = error as Error & { code?: unknown };
return (
errorRecord.name === "GatewaySecretRefUnavailableError" ||
errorRecord.code === "GATEWAY_SECRET_REF_UNAVAILABLE"
);
}
function formatBrowserDoctorGatewayError(error: unknown): string {
if (!isGatewaySecretRefUnavailableErrorShape(error)) {
return String(error);
}
return "Gateway auth SecretRef is unavailable in this command path; browser doctor cannot reach the admin-scoped browser.request endpoint. Set OPENCLAW_GATEWAY_TOKEN or OPENCLAW_GATEWAY_PASSWORD, then retry.";
}
async function runBrowserDoctor(parent: BrowserParentOpts, profile?: string, deep?: boolean) {
const checks: BrowserDoctorCheck[] = [];
let status: BrowserStatus | null;
@@ -185,7 +167,7 @@ async function runBrowserDoctor(parent: BrowserParentOpts, profile?: string, dee
checks.push({
name: "gateway",
ok: false,
detail: formatBrowserDoctorGatewayError(err),
detail: String(err),
});
return { ok: false, checks };
}

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/byteplus-provider",
"version": "2026.6.2",
"version": "2026.6.7-alpha.3",
"private": true,
"description": "OpenClaw BytePlus provider plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/canvas-plugin",
"version": "2026.6.2",
"version": "2026.6.7-alpha.3",
"private": true,
"description": "OpenClaw Canvas plugin",
"type": "module",

View File

@@ -120,7 +120,6 @@ describe("canvas host", () => {
};
let createCanvasHostHandler: typeof import("./server.js").createCanvasHostHandler;
let startCanvasHost: typeof import("./server.js").startCanvasHost;
let canvasLiveReloadMaxInboundMessageBytes = 0;
let WebSocketServerClass: typeof import("ws").WebSocketServer;
let watcherState: ReturnType<typeof createMockWatcherState>;
let fixtureRoot = "";
@@ -163,10 +162,7 @@ describe("canvas host", () => {
};
});
vi.resetModules();
const serverModule = await import("./server.js");
({ createCanvasHostHandler, startCanvasHost } = serverModule);
canvasLiveReloadMaxInboundMessageBytes =
serverModule.CANVAS_LIVE_RELOAD_MAX_INBOUND_MESSAGE_BYTES;
({ createCanvasHostHandler, startCanvasHost } = await import("./server.js"));
const wsModule = await vi.importActual<typeof import("ws")>("ws");
WebSocketServerClass = wsModule.WebSocketServer;
fixtureRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-canvas-fixtures-"));
@@ -225,54 +221,6 @@ describe("canvas host", () => {
}
});
it("caps live reload WebSocket inbound payloads", async () => {
const dir = await createCaseDir();
const constructorOptions: unknown[] = [];
let connectionHandler: ((socket: TrackingWebSocket) => void) | undefined;
class CapturingWebSocketServer {
on(event: string, cb: (socket: TrackingWebSocket) => void) {
if (event === "connection") {
connectionHandler = cb;
}
return this;
}
close(cb?: () => void) {
cb?.();
}
constructor(options: unknown) {
constructorOptions.push(options);
}
}
const handler = await createTestCanvasHostHandler(dir, {
webSocketServerClass:
CapturingWebSocketServer as unknown as typeof import("ws").WebSocketServer,
});
try {
expect(constructorOptions[0]).toMatchObject({
noServer: true,
maxPayload: canvasLiveReloadMaxInboundMessageBytes,
});
const socketHandlers: string[] = [];
const socket: TrackingWebSocket = {
sent: [],
on: (event) => {
socketHandlers.push(event);
return socket;
},
send: vi.fn(),
};
expect(connectionHandler).toBeDefined();
connectionHandler?.(socket);
expect(socketHandlers).toEqual(expect.arrayContaining(["error", "close"]));
} finally {
await handler.close();
}
});
it("falls back to the default mount when the configured base path is malformed", async () => {
const dir = await createCaseDir();
await fs.writeFile(path.join(dir, "index.html"), "<html><body>fallback</body></html>", "utf8");

View File

@@ -30,8 +30,6 @@ import {
} from "./a2ui-shared.js";
import { normalizeUrlPath, resolveFileWithinRoot } from "./file-resolver.js";
export const CANVAS_LIVE_RELOAD_MAX_INBOUND_MESSAGE_BYTES = 64 * 1024;
type ChokidarWatch = typeof import("chokidar").watch;
/** Options for Canvas host creation. */
@@ -278,22 +276,11 @@ export async function createCanvasHostHandler(
const writeStabilityThresholdMs = testMode ? 12 : 75;
const writePollIntervalMs = testMode ? 5 : 10;
const WebSocketServerClass = opts.webSocketServerClass ?? WebSocketServer;
const wss = liveReload
? new WebSocketServerClass({
noServer: true,
// Live reload clients never need to send application payloads; cap frames
// before ws buffers oversized input on this long-lived upgrade route.
maxPayload: CANVAS_LIVE_RELOAD_MAX_INBOUND_MESSAGE_BYTES,
})
: null;
const wss = liveReload ? new WebSocketServerClass({ noServer: true }) : null;
const sockets = new Set<WebSocket>();
if (wss) {
wss.on("connection", (ws) => {
sockets.add(ws);
// ws emits error for maxPayload rejections; close handles final cleanup.
ws.on("error", () => {
sockets.delete(ws);
});
ws.on("close", () => sockets.delete(ws));
});
}

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/cerebras-provider",
"version": "2026.6.2",
"version": "2026.6.7-alpha.3",
"private": true,
"description": "OpenClaw Cerebras provider plugin",
"type": "module",

View File

@@ -15,6 +15,17 @@ function restoreEnvVar(name: string, value: string | undefined): void {
}
}
function readAuthorizationHeader(init?: { headers?: HeadersInit }): string {
const headers = init?.headers;
if (headers instanceof Headers) {
return headers.get("Authorization") ?? "";
}
if (Array.isArray(headers)) {
return headers.find(([key]) => key.toLowerCase() === "authorization")?.[1] ?? "";
}
return headers?.Authorization ?? headers?.authorization ?? "";
}
async function runChutesCatalog(params: { apiKey?: string; discoveryApiKey?: string }) {
const provider = await registerSingleProviderPlugin(plugin);
const result = await provider.catalog?.run({
@@ -102,9 +113,7 @@ describe("chutes implicit provider auth mode", () => {
const chutesCalls = fetchMock.mock.calls.filter(([url]) => String(url).includes("chutes.ai"));
expect(chutesCalls.length).toBeGreaterThan(0);
const request = chutesCalls[0]?.[1] as { headers?: HeadersInit } | undefined;
expect(new Headers(request?.headers).get("authorization")).toBe(
"Bearer my-chutes-access-token",
);
expect(readAuthorizationHeader(request)).toBe("Bearer my-chutes-access-token");
});
});
});

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/chutes-provider",
"version": "2026.6.2",
"version": "2026.6.7-alpha.3",
"private": true,
"description": "OpenClaw Chutes.ai provider plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/clickclack",
"version": "2026.6.2",
"version": "2026.6.7-alpha.3",
"private": true,
"description": "OpenClaw ClickClack channel plugin",
"type": "module",
@@ -18,7 +18,7 @@
"openclaw": "2026.5.28"
},
"peerDependencies": {
"openclaw": ">=2026.6.2"
"openclaw": ">=2026.6.7-alpha.3"
},
"peerDependenciesMeta": {
"openclaw": {

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/codex-supervisor",
"version": "2026.6.2",
"version": "2026.6.7-alpha.3",
"private": true,
"description": "OpenClaw Codex app-server fleet supervision plugin.",
"type": "module",

View File

@@ -1,12 +1,12 @@
{
"name": "@openclaw/codex",
"version": "2026.6.2",
"version": "2026.6.7-alpha.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@openclaw/codex",
"version": "2026.6.2",
"version": "2026.6.7-alpha.3",
"dependencies": {
"@openai/codex": "0.139.0",
"typebox": "1.1.39",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/codex",
"version": "2026.6.2",
"version": "2026.6.7-alpha.3",
"description": "OpenClaw Codex app-server harness and model provider plugin with a Codex-managed GPT catalog.",
"repository": {
"type": "git",
@@ -26,10 +26,10 @@
"minHostVersion": ">=2026.5.1-beta.1"
},
"compat": {
"pluginApi": ">=2026.6.2"
"pluginApi": ">=2026.6.7-alpha.3"
},
"build": {
"openclawVersion": "2026.6.2"
"openclawVersion": "2026.6.7-alpha.3"
},
"release": {
"publishToClawHub": true,

View File

@@ -15,7 +15,6 @@ import {
type EmbeddedRunAttemptResult,
} from "openclaw/plugin-sdk/agent-harness-runtime";
import { resolveAgentWorkspaceDir } from "openclaw/plugin-sdk/agent-runtime";
import { buildMemorySystemPromptAddition } from "openclaw/plugin-sdk/core";
import type { CodexDynamicToolSpec, JsonValue } from "./protocol.js";
import { isJsonObject } from "./protocol.js";
import type { CodexAppServerThreadBinding } from "./session-binding.js";
@@ -250,11 +249,9 @@ export async function buildCodexWorkspaceBootstrapContext(params: {
turnScopedDeveloperInstructionFiles,
),
memoryCollaborationInstructions: shouldInjectCodexOpenClawPromptContext(params.params)
? renderCodexWorkspaceMemoryCollaborationInstructions({
? renderCodexWorkspaceMemoryReference({
files: memoryReferenceFiles,
toolNames: params.memoryToolNames,
memoryToolRouted: memoryToolsAvailable,
citationsMode: params.params.config?.memory?.citations,
})
: undefined,
heartbeatCollaborationInstructions:
@@ -808,55 +805,6 @@ export function renderCodexWorkspaceMemoryReference(params: {
return lines.join("\n").trim();
}
function renderCodexWorkspaceMemoryCollaborationInstructions(params: {
files: EmbeddedContextFile[];
toolNames: readonly string[];
memoryToolRouted: boolean;
citationsMode?: Parameters<typeof buildMemorySystemPromptAddition>[0]["citationsMode"];
}): string | undefined {
const memoryRecallInstructions = params.memoryToolRouted
? renderCodexMemoryRecallInstructions({
toolNames: params.toolNames,
citationsMode: params.citationsMode,
})
: undefined;
const memoryReferenceInstructions = renderCodexWorkspaceMemoryReference({
files: params.files,
toolNames: params.toolNames,
});
const sections = [memoryRecallInstructions, memoryReferenceInstructions].filter(isNonEmptyString);
return sections.length > 0 ? sections.join("\n\n") : undefined;
}
function renderCodexMemoryRecallInstructions(params: {
toolNames: readonly string[];
citationsMode?: Parameters<typeof buildMemorySystemPromptAddition>[0]["citationsMode"];
}): string | undefined {
const availableTools = new Set(params.toolNames);
const memoryPrompt = buildMemorySystemPromptAddition({
availableTools,
citationsMode: params.citationsMode,
});
if (!memoryPrompt) {
// Memory recall policy belongs to the active memory plugin.
// Codex-side fallback text can mask plugin lifecycle bugs or misdescribe third-party memory tools.
return undefined;
}
const toolSearchBridge = renderCodexMemoryToolSearchBridge(params.toolNames);
return [memoryPrompt, toolSearchBridge].filter(isNonEmptyString).join("\n").trim();
}
function renderCodexMemoryToolSearchBridge(toolNames: readonly string[]): string | undefined {
const memoryToolNames = toolNames
.map((name) => normalizeCodexDynamicToolName(name))
.filter((name) => CODEX_MEMORY_TOOL_NAMES.has(name))
.toSorted();
if (memoryToolNames.length === 0) {
return undefined;
}
return `Codex may expose ${memoryToolNames.join(" and ")} as deferred tools. When the memory guidance above calls for memory recall, use an already-loaded memory tool directly. If the needed memory tool is deferred and not currently callable, use \`tool_search\` to load it, then call that memory tool.`;
}
/** Returns whether the current dynamic tool list can serve workspace memory. */
export function hasCodexWorkspaceMemoryTools(tools: readonly { name: string }[]): boolean {
return getCodexWorkspaceMemoryToolNames(tools).length > 0;

View File

@@ -30,6 +30,14 @@ type AttemptPaths = {
const tempRoots = new Set<string>();
function isolateCodexCliAuthHome(): void {
const root = path.join(os.tmpdir(), `openclaw-codex-attempt-auth-${randomUUID()}`);
tempRoots.add(root);
// Keep fallback auth lookup from reading the operator's real Codex CLI auth file.
vi.stubEnv("CODEX_HOME", path.join(root, "codex-home"));
vi.stubEnv("HOME", path.join(root, "home"));
}
function createAttemptPaths(): AttemptPaths {
const root = path.join(os.tmpdir(), `openclaw-codex-attempt-startup-${randomUUID()}`);
tempRoots.add(root);
@@ -168,6 +176,7 @@ describe("startCodexAttemptThread", () => {
vi.useRealTimers();
vi.stubEnv("CODEX_API_KEY", "");
vi.stubEnv("OPENAI_API_KEY", "");
isolateCodexCliAuthHome();
clearSharedCodexAppServerClient();
});
@@ -177,7 +186,7 @@ describe("startCodexAttemptThread", () => {
vi.restoreAllMocks();
vi.unstubAllEnvs();
for (const root of tempRoots) {
await fs.rm(root, { recursive: true, force: true });
await fs.rm(root, { recursive: true, force: true, maxRetries: 3, retryDelay: 50 });
}
tempRoots.clear();
});

View File

@@ -10,7 +10,6 @@ import {
} from "openclaw/plugin-sdk/agent-harness-runtime";
import { resetDiagnosticEventsForTest } from "openclaw/plugin-sdk/diagnostic-runtime";
import { clearInternalHooks, resetGlobalHookRunner } from "openclaw/plugin-sdk/hook-runtime";
import { clearMemoryPluginState } from "openclaw/plugin-sdk/memory-core-host-runtime-core";
import { clearPluginCommands } from "openclaw/plugin-sdk/plugin-runtime";
import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/temp-path";
import { afterEach, beforeEach, expect, vi } from "vitest";
@@ -496,7 +495,6 @@ export function setupRunAttemptTestHooks(): void {
beforeEach(async () => {
vi.useRealTimers();
clearInternalHooks();
clearMemoryPluginState();
resetAgentEventsForTest();
resetDiagnosticEventsForTest();
vi.stubEnv("OPENCLAW_TRAJECTORY", "0");
@@ -514,7 +512,6 @@ export function setupRunAttemptTestHooks(): void {
testing.clearPendingCodexNativeHookRelayUnregistersForTests();
resetCodexRateLimitCacheForTests();
nativeHookRelayTesting.clearNativeHookRelaysForTests();
clearMemoryPluginState();
clearPluginCommands();
resetAgentEventsForTest();
resetDiagnosticEventsForTest();

View File

@@ -12,7 +12,6 @@ import {
type DiagnosticEventPayload,
} from "openclaw/plugin-sdk/diagnostic-runtime";
import { initializeGlobalHookRunner, registerInternalHook } from "openclaw/plugin-sdk/hook-runtime";
import { registerMemoryCapability } from "openclaw/plugin-sdk/memory-core-host-runtime-core";
import { registerPluginCommand } from "openclaw/plugin-sdk/plugin-runtime";
import { createMockPluginRegistry } from "openclaw/plugin-sdk/plugin-test-runtime";
import { describe, expect, it, vi } from "vitest";
@@ -398,37 +397,6 @@ function createRuntimeDynamicTool(name: string): RuntimeDynamicToolForTest {
};
}
function registerMemoryPromptForTest() {
registerMemoryCapability("memory-core", {
promptBuilder({ availableTools }) {
const hasMemorySearch = availableTools.has("memory_search");
const hasMemoryGet = availableTools.has("memory_get");
if (hasMemorySearch && hasMemoryGet) {
return [
"## Memory Recall",
"Test recall: run memory_search on MEMORY.md + memory/*.md + indexed session transcripts; then use memory_get.",
"",
];
}
if (hasMemorySearch) {
return [
"## Memory Recall",
"Test recall: run memory_search on MEMORY.md + memory/*.md + indexed session transcripts.",
"",
];
}
if (hasMemoryGet) {
return [
"## Memory Recall",
"Test recall: run memory_get for a specific memory file or note.",
"",
];
}
return [];
},
});
}
function buildEmptyCodexToolTelemetry(): CodexAppServerToolTelemetry {
return {
didSendViaMessagingTool: false,
@@ -2235,7 +2203,6 @@ describe("runCodexAppServerAttempt", () => {
await fs.writeFile(path.join(workspaceDir, "TOOLS.md"), toolGuidance);
await fs.writeFile(path.join(workspaceDir, "USER.md"), userProfile);
await fs.writeFile(path.join(workspaceDir, "MEMORY.md"), memorySummary);
registerMemoryPromptForTest();
testing.setOpenClawCodingToolsFactoryForTests(() => [
createRuntimeDynamicTool("memory_search"),
createRuntimeDynamicTool("memory_get"),
@@ -2269,20 +2236,12 @@ describe("runCodexAppServerAttempt", () => {
expect(collaborationInstructions).toContain(identityGuidance);
expect(collaborationInstructions).not.toContain(toolGuidance);
expect(collaborationInstructions).toContain(userProfile);
expect(collaborationInstructions).toContain("## Memory Recall");
expect(collaborationInstructions).toContain("MEMORY.md + memory/*.md");
expect(collaborationInstructions).toContain("OpenClaw Workspace Memory");
expect(collaborationInstructions).toContain(
"MEMORY.md exists in the active agent workspace as a memory file, not an instruction file",
);
expect(collaborationInstructions).toContain("memory_search");
expect(collaborationInstructions).toContain("memory_get");
expect(collaborationInstructions).toContain(
"When the memory guidance above calls for memory recall, use an already-loaded memory tool directly.",
);
expect(collaborationInstructions).toContain(
"If the needed memory tool is deferred and not currently callable, use `tool_search` to load it, then call that memory tool.",
);
expect(collaborationInstructions).not.toContain(memorySummary);
expect(inputText).not.toContain("OpenClaw runtime context for this turn:");
expect(inputText).not.toContain("does not override Codex system/developer instructions");
@@ -2338,65 +2297,6 @@ describe("runCodexAppServerAttempt", () => {
});
});
it("adds memory recall guidance when dated memory notes exist without root MEMORY.md", async () => {
const sessionFile = path.join(tempDir, "session.jsonl");
const workspaceDir = path.join(tempDir, "workspace");
const datedMemory = "User avoids Chase cards while over 5/24.";
await fs.mkdir(path.join(workspaceDir, "memory"), { recursive: true });
await fs.writeFile(path.join(workspaceDir, "memory/2026-06-09.md"), datedMemory);
registerMemoryPromptForTest();
testing.setOpenClawCodingToolsFactoryForTests(() => [
createRuntimeDynamicTool("memory_search"),
createRuntimeDynamicTool("memory_get"),
]);
const params = createParams(sessionFile, workspaceDir);
params.disableTools = false;
params.runtimePlan = createCodexRuntimePlanFixture();
setAgentWorkspaceForTest(params, workspaceDir);
const { collaborationInstructions, inputText } = await buildCodexTurnContextForTest(
params,
workspaceDir,
);
expect(collaborationInstructions).toContain("## Memory Recall");
expect(collaborationInstructions).toContain("MEMORY.md + memory/*.md");
expect(collaborationInstructions).toContain("memory_search");
expect(collaborationInstructions).toContain("memory_get");
expect(collaborationInstructions).not.toContain("OpenClaw Workspace Memory");
expect(collaborationInstructions).not.toContain(datedMemory);
expect(inputText).toBe("hello");
expect(inputText).not.toContain(datedMemory);
});
it("does not synthesize memory recall guidance without a registered memory prompt builder", async () => {
const sessionFile = path.join(tempDir, "session.jsonl");
const workspaceDir = path.join(tempDir, "workspace");
const memorySummary = "User avoids Chase cards while over 5/24.";
await fs.mkdir(workspaceDir, { recursive: true });
await fs.writeFile(path.join(workspaceDir, "MEMORY.md"), memorySummary);
testing.setOpenClawCodingToolsFactoryForTests(() => [
createRuntimeDynamicTool("memory_search"),
createRuntimeDynamicTool("memory_get"),
]);
const params = createParams(sessionFile, workspaceDir);
params.disableTools = false;
params.runtimePlan = createCodexRuntimePlanFixture();
setAgentWorkspaceForTest(params, workspaceDir);
const { collaborationInstructions, inputText } = await buildCodexTurnContextForTest(
params,
workspaceDir,
);
expect(collaborationInstructions).not.toContain("## Memory Recall");
expect(collaborationInstructions).toContain("OpenClaw Workspace Memory");
expect(collaborationInstructions).not.toContain("Use `tool_search` first");
expect(collaborationInstructions).not.toContain(memorySummary);
expect(inputText).toBe("hello");
expect(inputText).not.toContain(memorySummary);
});
it("sends workspace bootstrap instructions through Codex app-server payloads", async () => {
const sessionFile = path.join(tempDir, "session.jsonl");
const workspaceDir = path.join(tempDir, "workspace");
@@ -2505,7 +2405,6 @@ describe("runCodexAppServerAttempt", () => {
const memorySummary = "Memory summary goes here.";
await fs.mkdir(workspaceDir, { recursive: true });
await fs.writeFile(path.join(workspaceDir, "MEMORY.md"), memorySummary);
registerMemoryPromptForTest();
testing.setOpenClawCodingToolsFactoryForTests(() => [createRuntimeDynamicTool("memory_get")]);
const params = createParams(sessionFile, workspaceDir);
params.disableTools = false;
@@ -2518,7 +2417,6 @@ describe("runCodexAppServerAttempt", () => {
expect(inputText).not.toContain("memory_get");
expect(inputText).not.toContain("memory_search");
expect(inputText).not.toContain(memorySummary);
expect(collaborationInstructions).toContain("## Memory Recall");
expect(collaborationInstructions).toContain("OpenClaw Workspace Memory");
expect(collaborationInstructions).toContain("memory_get");
expect(collaborationInstructions).not.toContain("memory_search");
@@ -2697,7 +2595,6 @@ describe("runCodexAppServerAttempt", () => {
const memorySummary = "Memory summary goes here.";
await fs.mkdir(workspaceDir, { recursive: true });
await fs.writeFile(path.join(workspaceDir, "MEMORY.md"), memorySummary);
registerMemoryPromptForTest();
testing.setOpenClawCodingToolsFactoryForTests(() => [
createRuntimeDynamicTool("memory_search"),
createRuntimeDynamicTool("memory_get"),
@@ -2707,10 +2604,10 @@ describe("runCodexAppServerAttempt", () => {
params.runtimePlan = createCodexRuntimePlanFixture();
setAgentWorkspaceForTest(params, path.join(tempDir, "memory-workspace"));
const { collaborationInstructions, inputText, systemPromptReport } =
await buildCodexTurnContextForTest(params, workspaceDir);
expect(collaborationInstructions).not.toContain("## Memory Recall");
expect(collaborationInstructions).not.toContain("OpenClaw Workspace Memory");
const { inputText, systemPromptReport } = await buildCodexTurnContextForTest(
params,
workspaceDir,
);
expect(inputText).not.toContain("OpenClaw Workspace Memory");
expect(inputText).toContain(memorySummary);

View File

@@ -1,7 +1,6 @@
// Codex tests cover sandbox exec server plugin behavior.
import { afterEach, describe, expect, it, vi } from "vitest";
import {
CODEX_SANDBOX_EXEC_SERVER_MAX_INBOUND_MESSAGE_BYTES,
closeCodexSandboxExecServersForTests,
ensureCodexSandboxExecServerEnvironment,
releaseCodexSandboxExecServerEnvironment,
@@ -192,22 +191,6 @@ describe("OpenClaw Codex sandbox exec-server", () => {
socket.close();
});
it("closes oversized sandbox exec-server frames before JSON-RPC parsing", async () => {
const sandbox = createSandboxContext({});
const client = createClient();
await ensureCodexSandboxExecServerEnvironment({
client: client as never,
sandbox,
});
const socket = await openSocket(execServerUrlFromClient(client));
const closed = waitForSocketClose(socket);
socket.send(Buffer.alloc(CODEX_SANDBOX_EXEC_SERVER_MAX_INBOUND_MESSAGE_BYTES + 1));
await expect(closed).resolves.toEqual({ code: 1009 });
});
it("rejects unsupported arg0 overrides instead of dropping them", async () => {
const buildExecSpec = vi.fn(async () => ({
argv: [process.execPath, "-e", ""],
@@ -458,26 +441,6 @@ describe("OpenClaw Codex sandbox exec-server", () => {
await expect(waitForSocketClose(socket)).resolves.toEqual({ code: 1008 });
});
it("handles oversized frames from unauthorized WebSocket clients", async () => {
const sandbox = createSandboxContext({});
const client = createClient();
await ensureCodexSandboxExecServerEnvironment({
client: client as never,
sandbox,
});
const unauthorizedUrl = execServerUrlFromClient(client).replace(
/\/openclaw-[^/?#]+/u,
"/wrong",
);
const socket = await openSocket(unauthorizedUrl);
const closed = waitForSocketClose(socket);
socket.send(Buffer.alloc(CODEX_SANDBOX_EXEC_SERVER_MAX_INBOUND_MESSAGE_BYTES + 1));
const closeResult = await closed;
expect([1008, 1009]).toContain(closeResult.code);
});
it("closes the exec-server when its sandbox environment is released", async () => {
const sandbox = createSandboxContext({});
const client = createClient();

View File

@@ -48,7 +48,6 @@ export type CodexSandboxExecEnvironment = {
};
const SANDBOX_EXEC_SERVERS = new Map<string, Promise<OpenClawExecServer>>();
export const CODEX_SANDBOX_EXEC_SERVER_MAX_INBOUND_MESSAGE_BYTES = 100 * 1024 * 1024;
/** Closes all cached sandbox exec-server instances for deterministic tests. */
export async function closeCodexSandboxExecServersForTests(): Promise<void> {
@@ -194,13 +193,7 @@ function startAndRememberOpenClawExecServer(sandbox: SandboxContext): Promise<Op
}
async function startOpenClawExecServer(sandbox: SandboxContext): Promise<OpenClawExecServer> {
const server = new WebSocketServer({
host: "127.0.0.1",
port: 0,
// Match ws' historical default: Codex fs/writeFile sends one base64 JSON-RPC
// frame, while the socket error handler below makes oversize frames nonfatal.
maxPayload: CODEX_SANDBOX_EXEC_SERVER_MAX_INBOUND_MESSAGE_BYTES,
});
const server = new WebSocketServer({ host: "127.0.0.1", port: 0 });
await once(server, "listening");
const address = server.address();
if (!address || typeof address === "string") {
@@ -219,8 +212,6 @@ async function startOpenClawExecServer(sandbox: SandboxContext): Promise<OpenCla
server,
};
server.on("connection", (socket, request) => {
// ws emits error for maxPayload rejections before auth or JSON-RPC sees the frame.
socket.on("error", handleExecServerSocketError);
if (!isAuthorizedExecServerRequest(execServer, request)) {
socket.close(1008, "unauthorized");
return;
@@ -295,10 +286,6 @@ function handleConnection(execServer: OpenClawExecServer, socket: WebSocket): vo
});
}
function handleExecServerSocketError(error: unknown): void {
embeddedAgentLog.debug("codex sandbox exec-server websocket failed", { error });
}
async function handleMessage(
execServer: OpenClawExecServer,
processes: Map<string, ManagedProcess>,

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/comfy-provider",
"version": "2026.6.2",
"version": "2026.6.7-alpha.3",
"private": true,
"description": "OpenClaw ComfyUI provider plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/copilot-proxy",
"version": "2026.6.2",
"version": "2026.6.7-alpha.3",
"private": true,
"description": "OpenClaw Copilot Proxy provider plugin",
"type": "module",

View File

@@ -1,12 +1,12 @@
{
"name": "@openclaw/copilot",
"version": "2026.6.2",
"version": "2026.6.7-alpha.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@openclaw/copilot",
"version": "2026.6.2",
"version": "2026.6.7-alpha.3",
"dependencies": {
"@github/copilot-sdk": "1.0.0-beta.9"
}

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/copilot",
"version": "2026.6.2",
"version": "2026.6.7-alpha.3",
"description": "OpenClaw GitHub Copilot agent runtime plugin (registers a `github-copilot` AgentHarness backed by @github/copilot-sdk over JSON-RPC to the GitHub Copilot CLI)",
"repository": {
"type": "git",
@@ -25,10 +25,10 @@
"minHostVersion": ">=2026.5.28"
},
"compat": {
"pluginApi": ">=2026.6.2"
"pluginApi": ">=2026.6.7-alpha.3"
},
"build": {
"openclawVersion": "2026.6.2",
"openclawVersion": "2026.6.7-alpha.3",
"bundledDist": false
},
"release": {

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