Compare commits

..

1 Commits

Author SHA1 Message Date
Mason Huang
93fc591af9 ci: add process exec codeql security shard 2026-06-13 20:38:21 +08:00
684 changed files with 7326 additions and 50361 deletions

View File

@@ -54,13 +54,6 @@ pnpm crabbox:run -- --help | sed -n '1,120p'
- For broad OpenClaw maintainer `pnpm` gates, prefer the repo wrapper with
`--provider blacksmith-testbox` or the repo Testbox helpers when the standing
Testbox policy applies.
- Cold Testbox acquisition and hydration often take tens of seconds. When broad
remote proof is likely, immediately start
`node scripts/crabbox-wrapper.mjs warmup --provider blacksmith-testbox --keep --timing-json`
in a background command session while inspecting, editing, and running
focused local tests. Poll later, reuse the returned `tbx_...` with
`--provider blacksmith-testbox --id <tbx_id>`, and stop it before handoff.
Do not warm speculatively when remote proof is unlikely.
- Always report the actual provider and id. `cbx_...` means AWS Crabbox;
`tbx_...` means Blacksmith Testbox through Crabbox. If the output only says
`blacksmith testbox list`, use `blacksmith testbox list --all` before

View File

@@ -150,21 +150,9 @@ Use this skill for release and publish-time workflow. Load `$release-private` if
- Stable Windows Hub release closeout requires the signed
`OpenClawCompanion-Setup-x64.exe`, `OpenClawCompanion-Setup-arm64.exe`, and
`OpenClawCompanion-SHA256SUMS.txt` assets on the canonical
`openclaw/openclaw` GitHub Release. Pass the exact signed
`openclaw/openclaw-windows-node` release tag as `windows_node_tag` to
`OpenClaw Release Publish`, together with the candidate-approved
`windows_node_installer_digests` map; it prevalidates the published source
release and required installers against that map before any publish child,
dispatches the public `Windows Node Release` workflow while the OpenClaw
release is still a draft, carries those pinned source asset digests
unchanged, verifies the expected OpenClaw Foundation Authenticode signer on
Windows, re-downloads and checksum-verifies the promoted asset contract, and
blocks publication until the canonical asset contract is present. Use direct
`Windows Node Release` dispatch only for recovery, always with an exact tag,
never `latest`, and the explicit `expected_installer_digests` JSON map from
the approved source release. Recovery rejects unexpected
`OpenClawCompanion-*` target asset names, then replaces the expected contract
assets with the pinned source bytes.
`openclaw/openclaw` GitHub Release. Use the public `Windows Node Release`
workflow after the matching `openclaw/openclaw-windows-node` release exists;
it verifies Authenticode signatures on Windows before uploading assets.
- Website Windows Hub download links should target exact canonical
`openclaw/openclaw/releases/download/vYYYY.M.PATCH/...` assets for the current
stable release, or `releases/latest/download/...` only after verifying the
@@ -687,23 +675,19 @@ node --import tsx scripts/openclaw-npm-postpublish-verify.ts <published-version>
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.
22. Start `.github/workflows/openclaw-release-publish.yml` from the same branch with
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` plus the successful `full_release_validation_run_id`.
For stable publish, also pass the exact non-prerelease
`openclaw/openclaw-windows-node` tag as `windows_node_tag` and its
candidate-approved installer digest map as `windows_node_installer_digests`.
`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,
promote and verify the required Windows Hub assets for stable releases,
append release verification proof, and only then undraft/publish it. If a
waited plugin publish or Windows Hub promotion fails after OpenClaw npm
succeeds, the workflow keeps the release draft with OpenClaw npm evidence
and exits red; do not undraft until the gap is repaired. The standalone
verifier command remains the recovery probe:
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:
`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

@@ -0,0 +1,61 @@
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*"

1
.github/labeler.yml vendored
View File

@@ -122,7 +122,6 @@
- "docs/concepts/qa-e2e-automation.md"
- "docs/concepts/personal-agent-benchmark-pack.md"
- "docs/channels/qa-channel.md"
- "docs/reference/maturity-tests.md"
"channel: signal":
- changed-files:
- any-glob-to-any-file:

View File

@@ -17,7 +17,28 @@ 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:
@@ -26,7 +47,28 @@ 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 * * *"
@@ -73,6 +115,11 @@ 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

@@ -420,7 +420,6 @@ jobs:
add_suite live-cache
add_profile_suite native-live-src-agents "stable full"
add_profile_suite native-live-src-agents-zai-coding "stable full"
add_profile_suite native-live-src-gateway-core "beta minimum stable full"
add_profile_suite native-live-src-gateway-profiles-anthropic "stable full"
add_profile_suite native-live-src-gateway-profiles-anthropic-smoke "stable"
@@ -1957,12 +1956,6 @@ jobs:
timeout_minutes: 60
profile_env_only: false
profiles: stable full
- suite_id: native-live-src-agents-zai-coding
label: Native live Z.AI Coding Plan
command: ZAI_CODING_LIVE_TEST=1 node .release-harness/scripts/test-live-shard.mjs native-live-src-agents-zai-coding
timeout_minutes: 15
profile_env_only: false
profiles: stable full
- suite_id: native-live-src-gateway-core
label: Native live gateway core
command: OPENCLAW_LIVE_CODEX_HARNESS=1 OPENCLAW_LIVE_CODEX_HARNESS_AUTH=api-key node .release-harness/scripts/test-live-shard.mjs native-live-src-gateway-core

View File

@@ -15,14 +15,6 @@ on:
description: Successful Full Release Validation run id for this tag/SHA, required when publish_openclaw_npm=true
required: false
type: string
windows_node_tag:
description: Exact openclaw-windows-node release tag, required for stable OpenClaw publish
required: false
type: string
windows_node_installer_digests:
description: Candidate-approved compact JSON map of Windows installer names to pinned sha256 digests
required: false
type: string
npm_telegram_run_id:
description: Optional successful NPM Telegram Beta E2E run id to include in final release evidence
required: false
@@ -89,15 +81,12 @@ jobs:
outputs:
sha: ${{ steps.manifest.outputs.sha || steps.ref.outputs.sha }}
preflight_artifact_name: ${{ steps.preflight_artifact.outputs.name }}
windows_node_installer_digests: ${{ steps.windows_source.outputs.installer_digests }}
steps:
- name: Validate inputs
env:
RELEASE_TAG: ${{ inputs.tag }}
PREFLIGHT_RUN_ID: ${{ inputs.preflight_run_id }}
FULL_RELEASE_VALIDATION_RUN_ID: ${{ inputs.full_release_validation_run_id }}
WINDOWS_NODE_TAG: ${{ inputs.windows_node_tag }}
WINDOWS_NODE_INSTALLER_DIGESTS: ${{ inputs.windows_node_installer_digests }}
PUBLISH_OPENCLAW_NPM: ${{ inputs.publish_openclaw_npm && 'true' || 'false' }}
PLUGIN_PUBLISH_SCOPE: ${{ inputs.plugin_publish_scope }}
PLUGINS: ${{ inputs.plugins }}
@@ -126,22 +115,6 @@ jobs:
echo "publish_openclaw_npm=true requires full_release_validation_run_id." >&2
exit 1
fi
stable_release=true
if [[ "${RELEASE_TAG}" == *"-alpha."* || "${RELEASE_TAG}" == *"-beta."* ]]; then
stable_release=false
fi
if [[ -n "${WINDOWS_NODE_TAG}" && ! "${WINDOWS_NODE_TAG}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+([-.][0-9A-Za-z]+([.-][0-9A-Za-z]+)*)?$ ]]; then
echo "windows_node_tag must be an explicit openclaw-windows-node release tag, not latest: ${WINDOWS_NODE_TAG}" >&2
exit 1
fi
if [[ "${PUBLISH_OPENCLAW_NPM}" == "true" && "${stable_release}" == "true" && -z "${WINDOWS_NODE_TAG}" ]]; then
echo "Stable OpenClaw publish requires an explicit windows_node_tag." >&2
exit 1
fi
if [[ "${PUBLISH_OPENCLAW_NPM}" == "true" && "${stable_release}" == "true" && -z "${WINDOWS_NODE_INSTALLER_DIGESTS}" ]]; then
echo "Stable OpenClaw publish requires candidate-approved windows_node_installer_digests." >&2
exit 1
fi
tideclaw_alpha_publish=false
if [[ "${RELEASE_TAG}" == *"-alpha."* && "${RELEASE_NPM_DIST_TAG}" == "alpha" && "${WORKFLOW_REF}" =~ ^refs/heads/tideclaw/alpha/[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{4}Z$ ]]; then
tideclaw_alpha_publish=true
@@ -170,73 +143,6 @@ jobs:
;;
esac
- name: Validate stable Windows source release
id: windows_source
if: ${{ inputs.publish_openclaw_npm }}
env:
GH_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ inputs.tag }}
WINDOWS_NODE_TAG: ${{ inputs.windows_node_tag }}
APPROVED_INSTALLER_DIGESTS: ${{ inputs.windows_node_installer_digests }}
run: |
set -euo pipefail
if [[ "${RELEASE_TAG}" == *"-alpha."* || "${RELEASE_TAG}" == *"-beta."* ]]; then
exit 0
fi
source_json="$(gh release view "${WINDOWS_NODE_TAG}" \
--repo openclaw/openclaw-windows-node \
--json tagName,isDraft,isPrerelease,assets,url)"
if [[ "$(printf '%s' "${source_json}" | jq -r '.tagName')" != "${WINDOWS_NODE_TAG}" ]]; then
echo "Windows source release tag does not match ${WINDOWS_NODE_TAG}." >&2
exit 1
fi
if [[ "$(printf '%s' "${source_json}" | jq -r '.isDraft')" == "true" ]]; then
echo "Stable OpenClaw publish requires a published Windows source release." >&2
exit 1
fi
if [[ "$(printf '%s' "${source_json}" | jq -r '.isPrerelease')" == "true" ]]; then
echo "Stable OpenClaw publish requires a non-prerelease Windows source release." >&2
exit 1
fi
required_assets=(
"OpenClawCompanion-Setup-x64.exe"
"OpenClawCompanion-Setup-arm64.exe"
)
required_assets_json="$(printf '%s\n' "${required_assets[@]}" | jq -R . | jq -sc .)"
if ! approved_installer_digests="$(printf '%s' "${APPROVED_INSTALLER_DIGESTS}" | jq -ce --argjson names "${required_assets_json}" '
if type == "object" and
(keys | sort) == ($names | sort) and
all(.[]; type == "string" and test("^sha256:[a-f0-9]{64}$"))
then .
else error("invalid candidate-approved Windows installer digest map")
end
')"; then
echo "windows_node_installer_digests must contain exactly the candidate-approved current installer asset contract." >&2
exit 1
fi
for asset_name in "${required_assets[@]}"; do
asset_matches="$(printf '%s' "${source_json}" | jq -c --arg name "${asset_name}" '[.assets[]? | select(.name == $name)]')"
asset_match_count="$(printf '%s' "${asset_matches}" | jq 'length')"
if [[ "${asset_match_count}" != "1" ]]; then
echo "Windows source release ${WINDOWS_NODE_TAG} must contain exactly one required asset ${asset_name}; found ${asset_match_count}." >&2
exit 1
fi
asset_digest="$(printf '%s' "${asset_matches}" | jq -r '.[0].digest // empty')"
if [[ ! "${asset_digest}" =~ ^sha256:[a-f0-9]{64}$ ]]; then
echo "Windows source release ${WINDOWS_NODE_TAG} asset ${asset_name} is missing its immutable SHA-256 digest." >&2
exit 1
fi
approved_digest="$(printf '%s' "${approved_installer_digests}" | jq -r --arg name "${asset_name}" '.[$name]')"
if [[ "${asset_digest}" != "${approved_digest}" ]]; then
echo "Windows source release ${WINDOWS_NODE_TAG} asset ${asset_name} no longer matches its candidate-approved digest." >&2
exit 1
fi
done
echo "installer_digests=${approved_installer_digests}" >> "$GITHUB_OUTPUT"
echo "- Windows Node source release: prevalidated \`${WINDOWS_NODE_TAG}\`" >> "$GITHUB_STEP_SUMMARY"
- name: Download OpenClaw npm preflight manifest
id: preflight_artifact
if: ${{ inputs.publish_openclaw_npm }}
@@ -431,7 +337,6 @@ jobs:
TARGET_SHA: ${{ steps.manifest.outputs.sha || steps.ref.outputs.sha }}
RELEASE_PROFILE: ${{ steps.full_manifest.outputs.release_profile || inputs.release_profile }}
FULL_RELEASE_VALIDATION_RUN_ID: ${{ inputs.full_release_validation_run_id }}
WINDOWS_NODE_TAG: ${{ inputs.windows_node_tag }}
run: |
{
echo "### Release target"
@@ -442,16 +347,13 @@ jobs:
if [[ -n "${FULL_RELEASE_VALIDATION_RUN_ID// }" ]]; then
echo "- Full release validation: \`${FULL_RELEASE_VALIDATION_RUN_ID}\`"
fi
if [[ -n "${WINDOWS_NODE_TAG// }" ]]; then
echo "- Windows Node source release: \`${WINDOWS_NODE_TAG}\`"
fi
} >> "$GITHUB_STEP_SUMMARY"
publish:
name: Publish plugins, then OpenClaw
needs: [resolve_release_target]
runs-on: ubuntu-latest
timeout-minutes: 120
timeout-minutes: 60
environment: npm-release
steps:
- name: Checkout release SHA
@@ -481,16 +383,10 @@ jobs:
WAIT_FOR_CLAWHUB: ${{ inputs.wait_for_clawhub && 'true' || 'false' }}
PREFLIGHT_ARTIFACT_NAME: ${{ needs.resolve_release_target.outputs.preflight_artifact_name }}
NPM_TELEGRAM_RUN_ID: ${{ inputs.npm_telegram_run_id }}
WINDOWS_NODE_TAG: ${{ inputs.windows_node_tag }}
WINDOWS_NODE_INSTALLER_DIGESTS: ${{ needs.resolve_release_target.outputs.windows_node_installer_digests }}
POSTPUBLISH_EVIDENCE_DIR: ${{ runner.temp }}/openclaw-release-postpublish-evidence
run: |
set -euo pipefail
is_stable_release() {
[[ "${RELEASE_TAG}" != *"-alpha."* && "${RELEASE_TAG}" != *"-beta."* ]]
}
dispatch_workflow_at_ref() {
local workflow_ref="$1"
shift
@@ -940,105 +836,10 @@ jobs:
}
publish_github_release() {
if is_stable_release; then
verify_windows_release_asset_contract
fi
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"
}
verify_windows_release_asset_contract() {
local actual_companion_assets actual_digest asset_name expected_companion_assets expected_digest expected_hash expected_installer_names manifest_dir manifest_json manifest_path release_json
# Add future promoted installer names, such as MSIX x64/ARM64, here.
local -a installer_assets=(
"OpenClawCompanion-Setup-x64.exe"
"OpenClawCompanion-Setup-arm64.exe"
)
local -a required_assets=(
"${installer_assets[@]}"
"OpenClawCompanion-SHA256SUMS.txt"
)
release_json="$(gh release view "${RELEASE_TAG}" --repo "$GITHUB_REPOSITORY" --json assets,url)"
expected_companion_assets="$(printf '%s\n' "${required_assets[@]}" | jq -R . | jq -sc 'sort')"
actual_companion_assets="$(printf '%s' "${release_json}" | jq -c '
[.assets[]? | select(.name | startswith("OpenClawCompanion-")) | .name] | sort
')"
if [[ "${actual_companion_assets}" != "${expected_companion_assets}" ]]; then
echo "Stable release OpenClawCompanion asset names do not exactly match the current contract." >&2
return 1
fi
for asset_name in "${required_assets[@]}"; do
if ! printf '%s' "${release_json}" | jq -e --arg name "${asset_name}" 'any(.assets[]?; .name == $name)' >/dev/null; then
echo "Stable release is missing required Windows asset ${asset_name}." >&2
return 1
fi
done
manifest_dir="${RUNNER_TEMP}/openclaw-windows-release-contract"
manifest_path="${manifest_dir}/OpenClawCompanion-SHA256SUMS.txt"
rm -rf "${manifest_dir}"
mkdir -p "${manifest_dir}"
gh release download "${RELEASE_TAG}" \
--repo "$GITHUB_REPOSITORY" \
--pattern "OpenClawCompanion-SHA256SUMS.txt" \
--dir "${manifest_dir}"
if ! manifest_json="$(jq -Rsc '
split("\n") as $lines |
(if $lines[-1] == "" then $lines[0:-1] else $lines end) |
map(sub("\r$"; "")) |
if all(.[]; test("^(?<hash>[a-f0-9]{64}) (?<name>[^/\\\\]+)$"))
then map(capture("^(?<hash>[a-f0-9]{64}) (?<name>[^/\\\\]+)$"))
else error("malformed Windows checksum manifest entry")
end
' "${manifest_path}")"; then
echo "Stable release Windows checksum manifest contains malformed entries." >&2
return 1
fi
expected_installer_names="$(printf '%s\n' "${installer_assets[@]}" | jq -R . | jq -sc 'sort')"
if ! printf '%s' "${manifest_json}" | jq -e --argjson expected "${expected_installer_names}" '
length == ($expected | length) and
([.[].name] | sort) == $expected and
([.[].name] | unique | length) == length
' >/dev/null; then
echo "Stable release Windows checksum manifest does not exactly match the installer asset contract." >&2
return 1
fi
for asset_name in "${installer_assets[@]}"; do
expected_digest="$(printf '%s' "${WINDOWS_NODE_INSTALLER_DIGESTS}" | jq -r --arg name "${asset_name}" '.[$name] // empty')"
actual_digest="$(printf '%s' "${release_json}" | jq -r --arg name "${asset_name}" '.assets[]? | select(.name == $name) | .digest // empty')"
if [[ -z "${expected_digest}" || "${actual_digest}" != "${expected_digest}" ]]; then
echo "Stable release Windows asset ${asset_name} does not match its pinned digest." >&2
return 1
fi
expected_hash="${expected_digest#sha256:}"
if ! printf '%s' "${manifest_json}" | jq -e --arg name "${asset_name}" --arg hash "${expected_hash}" '
any(.[]; .name == $name and .hash == $hash)
' >/dev/null; then
echo "Stable release Windows checksum manifest does not match pinned digest for ${asset_name}." >&2
return 1
fi
done
echo "- Windows Hub asset contract: verified" >> "$GITHUB_STEP_SUMMARY"
}
promote_windows_release_assets() {
if ! is_stable_release; then
return 0
fi
if [[ -z "${WINDOWS_NODE_INSTALLER_DIGESTS// }" ]]; then
echo "Stable release is missing prevalidated Windows installer digests." >&2
return 1
fi
windows_node_run_id="$(dispatch_workflow windows-node-release.yml \
-f tag="${RELEASE_TAG}" \
-f windows_node_tag="${WINDOWS_NODE_TAG}" \
-f expected_installer_digests="${WINDOWS_NODE_INSTALLER_DIGESTS}")"
echo "- Windows Node release run ID: \`${windows_node_run_id}\`" >> "$GITHUB_STEP_SUMMARY"
wait_for_run windows-node-release.yml "${windows_node_run_id}"
}
upload_dependency_evidence_release_asset() {
local release_version download_dir asset_path asset_name artifact_name
release_version="${RELEASE_TAG#v}"
@@ -1112,7 +913,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 windows_line
local release_version body_file notes_file tarball integrity telegram_line clawhub_line clawhub_bootstrap_line clawhub_runtime_state_path
release_version="${RELEASE_TAG#v}"
body_file="${RUNNER_TEMP}/release-body.md"
@@ -1130,10 +931,6 @@ jobs:
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}")"
windows_line=""
if [[ -n "${windows_node_run_id// }" ]]; then
windows_line="- Windows Hub promotion: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${windows_node_run_id} from openclaw/openclaw-windows-node@${WINDOWS_NODE_TAG}"
fi
RELEASE_BODY_FILE="${body_file}" \
RELEASE_NOTES_FILE="${notes_file}" \
@@ -1151,7 +948,6 @@ jobs:
CLAWHUB_LINE="${clawhub_line}" \
CLAWHUB_BOOTSTRAP_LINE="${clawhub_bootstrap_line}" \
TELEGRAM_LINE="${telegram_line}" \
WINDOWS_LINE="${windows_line}" \
node --input-type=module <<'NODE'
import { readFileSync, writeFileSync } from "node:fs";
@@ -1178,7 +974,6 @@ jobs:
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,
...(process.env.WINDOWS_LINE ? [process.env.WINDOWS_LINE] : []),
].join("\n");
const withoutOldProof = body.replace(/\n?### Release verification\n[\s\S]*?(?=\n### |\n## |$)/, "");
@@ -1203,9 +998,6 @@ jobs:
else
echo "- OpenClaw npm publish: skipped by input"
fi
if is_stable_release && [[ "${PUBLISH_OPENCLAW_NPM}" == "true" ]]; then
echo "- Windows Hub promotion: required before the GitHub release can be published"
fi
if [[ "${WAIT_FOR_CLAWHUB}" == "true" ]]; then
echo "- Workflow completion waits for ClawHub"
else
@@ -1350,7 +1142,6 @@ jobs:
failed=0
openclaw_failed=0
windows_node_run_id=""
if [[ -n "${openclaw_pid}" ]] && ! wait "${openclaw_pid}"; then
failed=1
openclaw_failed=1
@@ -1381,9 +1172,6 @@ jobs:
fi
create_or_update_github_release
upload_dependency_evidence_release_asset
if ! promote_windows_release_assets; then
failed=1
fi
append_release_proof_to_github_release
if [[ "${failed}" == "0" ]]; then
publish_github_release

View File

@@ -68,7 +68,7 @@ jobs:
days-before-pr-close: 7
stale-issue-label: stale
stale-pr-label: stale
exempt-issue-labels: enhancement,maintainer,pinned,security,no-stale,bad-barnacle,clawsweeper:queueable-fix,clawsweeper:source-repro,clawsweeper:fix-shape-clear
exempt-issue-labels: enhancement,maintainer,pinned,security,no-stale,bad-barnacle
exempt-pr-labels: maintainer,no-stale,bad-barnacle
operations-per-run: 2000
ascending: true
@@ -100,7 +100,7 @@ jobs:
days-before-pr-stale: -1
days-before-pr-close: -1
stale-issue-label: stale
exempt-issue-labels: enhancement,maintainer,pinned,security,no-stale,bad-barnacle,clawsweeper:queueable-fix,clawsweeper:source-repro,clawsweeper:fix-shape-clear
exempt-issue-labels: enhancement,maintainer,pinned,security,no-stale,bad-barnacle
operations-per-run: 2000
ascending: true
include-only-assigned: true
@@ -172,7 +172,7 @@ jobs:
days-before-pr-close: 7
stale-issue-label: stale
stale-pr-label: stale
exempt-issue-labels: enhancement,maintainer,pinned,security,no-stale,bad-barnacle,clawsweeper:queueable-fix,clawsweeper:source-repro,clawsweeper:fix-shape-clear
exempt-issue-labels: enhancement,maintainer,pinned,security,no-stale,bad-barnacle
exempt-pr-labels: maintainer,no-stale,bad-barnacle
operations-per-run: 2000
ascending: true
@@ -203,7 +203,7 @@ jobs:
days-before-pr-stale: -1
days-before-pr-close: -1
stale-issue-label: stale
exempt-issue-labels: enhancement,maintainer,pinned,security,no-stale,bad-barnacle,clawsweeper:queueable-fix,clawsweeper:source-repro,clawsweeper:fix-shape-clear
exempt-issue-labels: enhancement,maintainer,pinned,security,no-stale,bad-barnacle
operations-per-run: 2000
ascending: true
include-only-assigned: true
@@ -277,9 +277,6 @@ jobs:
"security",
"no-stale",
"bad-barnacle",
"clawsweeper:queueable-fix",
"clawsweeper:source-repro",
"clawsweeper:fix-shape-clear",
]);
const prExemptLabels = new Set(["maintainer", "no-stale", "bad-barnacle"]);
const maintainerAssociations = new Set(["OWNER", "MEMBER", "COLLABORATOR"]);

View File

@@ -8,12 +8,9 @@ on:
required: true
type: string
windows_node_tag:
description: Exact openclaw-windows-node release tag to promote, for example v0.6.3
required: true
type: string
expected_installer_digests:
description: Compact JSON map of installer asset names to pinned source sha256 digests
description: openclaw-windows-node release tag to promote, or latest
required: true
default: latest
type: string
permissions:
@@ -34,129 +31,46 @@ jobs:
env:
RELEASE_TAG: ${{ inputs.tag }}
WINDOWS_NODE_TAG: ${{ inputs.windows_node_tag }}
EXPECTED_INSTALLER_DIGESTS: ${{ inputs.expected_installer_digests }}
GH_TOKEN: ${{ github.token }}
run: |
if ($env:RELEASE_TAG -notmatch '^v[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*((-(alpha|beta)\.[1-9][0-9]*)|(-[1-9][0-9]*))?$') {
throw "Invalid OpenClaw release tag: $env:RELEASE_TAG"
}
$stableRelease = -not (
$env:RELEASE_TAG.Contains("-alpha.") -or
$env:RELEASE_TAG.Contains("-beta.")
)
if ($env:WINDOWS_NODE_TAG -notmatch '^v[0-9]+\.[0-9]+\.[0-9]+([-.][0-9A-Za-z]+([.-][0-9A-Za-z]+)*)?$') {
throw "windows_node_tag must be an explicit openclaw-windows-node release tag, not latest: $env:WINDOWS_NODE_TAG"
}
try {
$expectedDigests = $env:EXPECTED_INSTALLER_DIGESTS | ConvertFrom-Json -AsHashtable
} catch {
throw "expected_installer_digests must be a JSON object: $_"
}
# Add future signed installer names, such as MSIX x64/ARM64, here.
$requiredInstallerNames = @(
"OpenClawCompanion-Setup-x64.exe",
"OpenClawCompanion-Setup-arm64.exe"
)
$allowedTargetCompanionAssetNames = @(
$requiredInstallerNames
"OpenClawCompanion-SHA256SUMS.txt"
)
if ($expectedDigests.Count -ne $requiredInstallerNames.Count) {
throw "expected_installer_digests must contain exactly the current installer asset contract."
}
foreach ($name in $requiredInstallerNames) {
$digest = [string]$expectedDigests[$name]
if ($digest -notmatch '^sha256:[A-Fa-f0-9]{64}$') {
throw "expected_installer_digests is missing a valid pinned digest for $name."
}
}
$targetRelease = gh release view $env:RELEASE_TAG --repo $env:GITHUB_REPOSITORY --json tagName,isDraft,isPrerelease,assets,url | ConvertFrom-Json
if ($targetRelease.tagName -ne $env:RELEASE_TAG) {
throw "OpenClaw release tag mismatch: expected $env:RELEASE_TAG, got $($targetRelease.tagName)"
}
$unexpectedTargetCompanionAssets = @(
$targetRelease.assets |
Where-Object {
$_.name.StartsWith("OpenClawCompanion-") -and
$_.name -notin $allowedTargetCompanionAssetNames
} |
ForEach-Object name |
Sort-Object
)
if ($unexpectedTargetCompanionAssets.Count -ne 0) {
throw "Target OpenClaw release contains unexpected OpenClawCompanion assets before upload: $($unexpectedTargetCompanionAssets -join ', ')"
}
$sourceRelease = gh release view $env:WINDOWS_NODE_TAG --repo openclaw/openclaw-windows-node --json tagName,isDraft,isPrerelease,assets,url | ConvertFrom-Json
if ($sourceRelease.tagName -ne $env:WINDOWS_NODE_TAG) {
throw "Windows source release tag mismatch: expected $env:WINDOWS_NODE_TAG, got $($sourceRelease.tagName)"
}
if ($sourceRelease.isDraft) {
throw "Windows source release must be published: $($sourceRelease.url)"
}
if ($stableRelease -and $sourceRelease.isPrerelease) {
throw "Stable OpenClaw releases require a non-prerelease Windows source release: $($sourceRelease.url)"
}
foreach ($name in $requiredInstallerNames) {
$sourceAssets = @($sourceRelease.assets | Where-Object name -eq $name)
if ($sourceAssets.Count -ne 1) {
throw "Windows source release must contain exactly one required asset $name; found $($sourceAssets.Count)."
}
if ([string]$sourceAssets[0].digest -ne [string]$expectedDigests[$name]) {
throw "Windows source release asset digest does not match the pinned digest: $name"
}
if ($env:WINDOWS_NODE_TAG -ne "latest" -and $env:WINDOWS_NODE_TAG -notmatch '^v[0-9]+\.[0-9]+\.[0-9]+([-.][0-9A-Za-z.-]+)?$') {
throw "Invalid openclaw-windows-node release tag: $env:WINDOWS_NODE_TAG"
}
gh release view $env:RELEASE_TAG --repo $env:GITHUB_REPOSITORY | Out-Null
- name: Download Windows Hub release installers
shell: pwsh
env:
WINDOWS_NODE_TAG: ${{ inputs.windows_node_tag }}
EXPECTED_INSTALLER_DIGESTS: ${{ inputs.expected_installer_digests }}
GH_TOKEN: ${{ github.token }}
run: |
New-Item -ItemType Directory -Force -Path dist | Out-Null
# Add future signed installer patterns, such as MSIX x64/ARM64, here.
# Every matched installer is signature-checked, checksummed, and promoted.
$installerPatterns = @(
"OpenClawCompanion-Setup-x64.exe",
"OpenClawCompanion-Setup-arm64.exe"
)
$downloadArgs = @(
$env:WINDOWS_NODE_TAG,
"--repo", "openclaw/openclaw-windows-node",
"--dir", "dist"
)
foreach ($pattern in $installerPatterns) {
$downloadArgs += @("--pattern", $pattern)
}
gh release download @downloadArgs
if ($LASTEXITCODE -ne 0) {
throw "Failed to download Windows release assets from $env:WINDOWS_NODE_TAG."
$tagArgs = @()
if ($env:WINDOWS_NODE_TAG -ne "latest") {
$tagArgs += $env:WINDOWS_NODE_TAG
}
gh release download @tagArgs `
--repo openclaw/openclaw-windows-node `
--pattern "OpenClawCompanion-Setup-*.exe" `
--dir dist
foreach ($pattern in $installerPatterns) {
$patternMatches = @(Get-ChildItem -LiteralPath dist -File | Where-Object Name -Like $pattern)
if ($patternMatches.Count -ne 1) {
throw "Expected exactly one Windows installer matching '$pattern', found $($patternMatches.Count)."
}
}
$expectedDigests = $env:EXPECTED_INSTALLER_DIGESTS | ConvertFrom-Json -AsHashtable
foreach ($file in Get-ChildItem -LiteralPath dist -File) {
$expectedHash = ([string]$expectedDigests[$file.Name]) -replace '^sha256:', ''
$actualHash = (Get-FileHash -Algorithm SHA256 -LiteralPath $file.FullName).Hash
if ($actualHash -ne $expectedHash) {
throw "Downloaded Windows source asset does not match pinned digest: $($file.Name)"
$expected = @(
"dist/OpenClawCompanion-Setup-x64.exe",
"dist/OpenClawCompanion-Setup-arm64.exe"
)
foreach ($file in $expected) {
if (-not (Test-Path -LiteralPath $file)) {
throw "Missing expected Windows installer: $file"
}
}
- name: Verify Authenticode signatures
shell: pwsh
run: |
$expectedSignerSubject = "CN=OpenClaw Foundation, O=OpenClaw Foundation, L=Mill Valley, S=California, C=US"
Get-ChildItem -LiteralPath dist -File | ForEach-Object {
Get-ChildItem -LiteralPath dist -Filter "OpenClawCompanion-Setup-*.exe" | ForEach-Object {
$signature = Get-AuthenticodeSignature -LiteralPath $_.FullName
if ($signature.Status -ne "Valid") {
throw "$($_.Name) Authenticode signature was $($signature.Status)."
@@ -164,9 +78,6 @@ jobs:
if (-not $signature.SignerCertificate) {
throw "$($_.Name) has no signer certificate."
}
if ($signature.SignerCertificate.Subject -ne $expectedSignerSubject) {
throw "$($_.Name) has unexpected signer subject $($signature.SignerCertificate.Subject)."
}
[pscustomobject]@{
File = $_.Name
Signer = $signature.SignerCertificate.Subject
@@ -177,7 +88,7 @@ jobs:
- name: Write SHA-256 manifest
shell: pwsh
run: |
Get-ChildItem -LiteralPath dist -File |
Get-ChildItem -LiteralPath dist -Filter "OpenClawCompanion-Setup-*.exe" |
Sort-Object Name |
ForEach-Object {
$hash = Get-FileHash -Algorithm SHA256 -LiteralPath $_.FullName
@@ -190,81 +101,12 @@ jobs:
RELEASE_TAG: ${{ inputs.tag }}
GH_TOKEN: ${{ github.token }}
run: |
$releaseAssets = @(Get-ChildItem -LiteralPath dist -File | Sort-Object Name | ForEach-Object FullName)
gh release upload $env:RELEASE_TAG @releaseAssets --repo $env:GITHUB_REPOSITORY --clobber
if ($LASTEXITCODE -ne 0) {
throw "Failed to upload Windows release assets to $env:RELEASE_TAG."
}
- name: Verify promoted release asset contract
shell: pwsh
env:
RELEASE_TAG: ${{ inputs.tag }}
GH_TOKEN: ${{ github.token }}
run: |
New-Item -ItemType Directory -Force -Path verified | Out-Null
$expectedAssets = @(Get-ChildItem -LiteralPath dist -File | Sort-Object Name)
$expectedCompanionAssetNames = @($expectedAssets | ForEach-Object Name | Sort-Object)
$targetRelease = gh release view $env:RELEASE_TAG --repo $env:GITHUB_REPOSITORY --json assets | ConvertFrom-Json
$actualCompanionAssetNames = @(
$targetRelease.assets |
Where-Object { $_.name.StartsWith("OpenClawCompanion-") } |
ForEach-Object name |
Sort-Object
)
$assetContractDiff = @(
Compare-Object `
-ReferenceObject $expectedCompanionAssetNames `
-DifferenceObject $actualCompanionAssetNames
)
if (
$actualCompanionAssetNames.Count -ne $expectedCompanionAssetNames.Count -or
$assetContractDiff.Count -ne 0
) {
throw "Promoted OpenClawCompanion asset names do not exactly match the current contract."
}
foreach ($asset in $expectedAssets) {
gh release download $env:RELEASE_TAG `
--repo $env:GITHUB_REPOSITORY `
--pattern $asset.Name `
--dir verified
if ($LASTEXITCODE -ne 0) {
throw "Failed to download promoted Windows release asset $($asset.Name)."
}
}
$manifestPath = "verified/OpenClawCompanion-SHA256SUMS.txt"
$manifestEntries = @(Get-Content -LiteralPath $manifestPath | ForEach-Object {
if ($_ -notmatch '^([A-Fa-f0-9]{64}) ([^\\/]+)$') {
throw "Invalid Windows SHA-256 manifest entry: $_"
}
[PSCustomObject]@{
Hash = $Matches[1]
Name = $Matches[2]
}
})
$expectedInstallerNames = @(
$expectedAssets |
Where-Object Name -ne "OpenClawCompanion-SHA256SUMS.txt" |
ForEach-Object Name
)
$manifestInstallerNames = @($manifestEntries | ForEach-Object Name | Sort-Object)
$contractDiff = @(
Compare-Object `
-ReferenceObject $expectedInstallerNames `
-DifferenceObject $manifestInstallerNames
)
if ($contractDiff.Count -ne 0) {
throw "Promoted Windows SHA-256 manifest does not match the installer asset contract."
}
foreach ($entry in $manifestEntries) {
$hash = (Get-FileHash -Algorithm SHA256 -LiteralPath "verified/$($entry.Name)").Hash
if ($hash -ne $entry.Hash) {
throw "Promoted Windows release asset checksum mismatch: $($entry.Name)"
}
}
gh release upload $env:RELEASE_TAG `
dist/OpenClawCompanion-Setup-x64.exe `
dist/OpenClawCompanion-Setup-arm64.exe `
dist/OpenClawCompanion-SHA256SUMS.txt `
--repo $env:GITHUB_REPOSITORY `
--clobber
- name: Summary
shell: pwsh
@@ -277,9 +119,8 @@ jobs:
OpenClaw release: $env:RELEASE_TAG
Source release: openclaw/openclaw-windows-node@$env:WINDOWS_NODE_TAG
- https://github.com/openclaw/openclaw/releases/download/$env:RELEASE_TAG/OpenClawCompanion-Setup-x64.exe
- https://github.com/openclaw/openclaw/releases/download/$env:RELEASE_TAG/OpenClawCompanion-Setup-arm64.exe
- https://github.com/openclaw/openclaw/releases/download/$env:RELEASE_TAG/OpenClawCompanion-SHA256SUMS.txt
"@ >> $env:GITHUB_STEP_SUMMARY
Get-ChildItem -LiteralPath dist -File |
Sort-Object Name |
ForEach-Object {
"- https://github.com/openclaw/openclaw/releases/download/$env:RELEASE_TAG/$($_.Name)"
} >> $env:GITHUB_STEP_SUMMARY

View File

@@ -2,35 +2,6 @@
Docs: https://docs.openclaw.ai
## 2026.6.8
### Highlights
- Telegram and WhatsApp channel delivery are richer and less brittle: Telegram can send structured rich text with tables, lists, expandable blockquotes, prompt-preserving CLI backend delivery, retired native draft migration, and safer rich-media boundaries, while WhatsApp now honors configured ACP bindings. (#92679, #84082, #89421, #92513) Thanks @obviyus, @jzakirov, @spacegeologist, and @TurboTheTurtle.
- Agent and Gateway recovery is sharper across account-scoped DM sends, generated media completions, restart shutdown aborts, yielded subagent pauses, yielded cron media, heartbeat dedupe, session identity prompts, and unknown OpenAI agent selector rejection. (#92788, #91246, #91357, #92631, #92146, #91287, #92468, #92510) Thanks @yetval, @TurboTheTurtle, @ooiuuii, @openperf, @IWhatsskill, @ZengWen-DT, and @zhangguiping-xydt.
- Provider/model handling expands and tightens with GLM-5.2, Claude Haiku 4.5 catalog rows, OpenRouter and Google Vertex provider-prefix normalization, managed SecretRef auth, bounded model browse discovery, storeless OpenAI Responses replay gating, and Claude 4.5 Copilot tool-streaming safety. (#92796, #90116, #92627, #91218, #90686, #92247, #90706, #75393) Thanks @arkyu2077, @liuhao1024, @bymle, @rohitjavvadi, @samson910022, @snowzlm, and @Kailigithub.
- `/usage` and reply payload hooks now have a native full footer renderer, default template, fixed-decimal formatting, credential-aware limits, better partial-count handling, and warnings for broken templates instead of silent bad output. (#92657, #89835, #89629) Thanks @Marvinthebored.
- UI and mobile flows are steadier: workspace files can collapse and start collapsed, WebChat backscroll survives streaming, the sidebar session picker remains interactive above the desktop workbench, reset soft args survive UI dispatch, stale dashboard session parent lineage is preserved, and iOS reconnects stale foreground gateways. (#92779, #92622, #92705, #91353, #90658, #92552) Thanks @shakkernerd, @TurboTheTurtle, @NianJiuZst, @zhouhe-xydt, @luoyanglang, and @Solvely-Colin.
- Memory, state, and diagnostics recover cleaner: oversized OpenAI embedding batches split before 431s, QMD memory search stays available in transient mode, SQLite avoids WAL on NFS state volumes, stuck-session recovery scheduling no longer resets warning backoff, and Infinity chunk limits stay genuinely unbounded. (#92650, #92618, #92639, #91247, #92752, #92735) Thanks @mushuiyu886, @TurboTheTurtle, @849261680, @gnanam1990, and @yhterrance.
### Changes
- Providers/models: add GLM-5.2 support and Claude Haiku 4.5 catalog entries while keeping provider-qualified model IDs normalized across OpenRouter and Google Vertex paths. (#92796, #90116, #92627, #91218) Thanks @arkyu2077, @liuhao1024, and @bymle.
- Channel plugins: ship Telegram rich-message delivery and WhatsApp ACP binding support, including rich prompt handoff to CLI backends and transport fixtures for richer drafts. (#92679, #92513) Thanks @obviyus and @TurboTheTurtle.
- Agent commands: support `/btw` in CLI-backed sessions and keep CLI usage-error exits classified as usage failures instead of successful runs. (#92669, #92162) Thanks @joshavant and @Pandah97.
- Usage hooks: add built-in full footer rendering, default footer templates, per-turn usage state, credential-aware limits, and fixed-decimal formatting for usage-bar templates. (#92657, #89835, #89629) Thanks @Marvinthebored.
- Docs and operator guidance: document node config examples, clarify before-install hook scope, correct agent default concurrency comments, refresh ZAI provider docs, and update channel/group docs for current Telegram and WhatsApp behavior. (#92677, #92766, #92695) Thanks @liuhao1024, @sallyom, and @ArielSmoliar.
### Fixes
- Channels and delivery: preserve account-scoped DM channel send policy, rich Telegram final replies, rich Telegram tables and lists, Telegram thread-create CLI remapping, Slack outbound `message_sent` hooks, contributed message-tool schema optionality, same-channel generated media completions, and channel chunking around surrogate pairs and Infinity limits. (#92788, #92679, #89421, #89943, #91137, #91246, #92735) Thanks @yetval, @obviyus, @spacegeologist, @rishitamrakar, @lundog, @TurboTheTurtle, and @yhterrance.
- Discord: give generated auto-thread titles a 60-second timeout and 4,096-token reasoning-model output budget, clamped to the selected model output cap. (#64734) Thanks @hanamizuki.
- Agent, cron, and Gateway runtime: mark active main sessions before restart shutdown aborts, pause yielded subagent runs whose terminal also signals abort, preserve yielded media completions, de-duplicate main-session heartbeat events, expose session identity in runtime prompts, reject unknown OpenAI agent selectors, keep generated media completions and slash-command block replies in WebChat, and require admin privileges for HTTP session/model override surfaces. (#91357, #92631, #92146, #91287, #92468, #92510, #91246, #92651, #92646) Thanks @ooiuuii, @openperf, @IWhatsskill, @ZengWen-DT, @zhangguiping-xydt, and @TurboTheTurtle.
- Providers and model replay: preserve storeless OpenAI Responses replay compatibility, avoid eager tool streaming for Claude 4.5 in Copilot, honor profile auth for SecretRef model entries, bound model browsing, strip provider prefixes where runtimes need bare IDs, and surface nested embedding fetch failures. (#90706, #75393, #90686, #92247, #92627, #91218, #92628) Thanks @snowzlm, @Kailigithub, @rohitjavvadi, @samson910022, @liuhao1024, @bymle, and @mushuiyu886.
- Memory, state, diagnostics, and config: split header-too-large embedding batches, keep QMD memory search enabled in transient mode, avoid SQLite WAL on NFS volumes, preserve recovery scheduling outside stuck-session warning backoff, and keep shell environment fallbacks contained in config write tests. (#92650, #92618, #92639, #91247, #92752) Thanks @mushuiyu886, @TurboTheTurtle, @849261680, and @gnanam1990.
- UI/mobile/TUI: preserve dashboard session parent lineage, WebChat backscroll, reset soft command args, sidebar session picker interactivity, collapsed workspace files, resolved `/model` confirmation refs, and stale foreground iOS Gateway reconnects. (#90658, #92622, #91353, #92705, #92779, #92773, #92552) Thanks @luoyanglang, @TurboTheTurtle, @zhouhe-xydt, @NianJiuZst, @shakkernerd, @NarahariRaghava, and @Solvely-Colin.
- Release and test reliability: extend slow Gateway/full-suite watchdogs, split local full-suite shards when throttled, stabilize plugin auth marker fixtures, avoid brittle provider-ref error text, and keep QA Lab bootstrap selection assertions aligned with flow-only scenarios. (#92652)
## 2026.6.6
### Highlights

View File

@@ -147,10 +147,6 @@ RUN --mount=type=cache,id=openclaw-pnpm-store,target=/root/.local/share/pnpm/sto
OPENCLAW_EXTENSIONS="$OPENCLAW_EXTENSIONS" OPENCLAW_BUNDLED_PLUGIN_DIR="$OPENCLAW_BUNDLED_PLUGIN_DIR" node scripts/prune-docker-plugin-dist.mjs && \
node scripts/postinstall-bundled-plugins.mjs && \
find dist -type f \( -name '*.d.ts' -o -name '*.d.mts' -o -name '*.d.cts' -o -name '*.map' \) -delete && \
rm -rf \
/app/node_modules/openclaw \
/app/node_modules/.bin/openclaw \
/app/node_modules/.pnpm/openclaw@*/node_modules/openclaw && \
node scripts/check-package-dist-imports.mjs /app
# ── Runtime base image ──────────────────────────────────────────

View File

@@ -188,7 +188,6 @@ final class NodeAppModel {
@ObservationIgnored private var backgroundGraceTaskTimer: Task<Void, Never>?
private var backgroundReconnectSuppressed = false
private var backgroundReconnectLeaseUntil: Date?
@ObservationIgnored private var foregroundGatewayResumeCheckInFlight = false
private var lastSignificantLocationWakeAt: Date?
@ObservationIgnored private let watchReplyCoordinator = WatchReplyCoordinator()
private var watchExecApprovalPromptsByID: [String: ExecApprovalPrompt] = [:]
@@ -215,7 +214,6 @@ final class NodeAppModel {
private static let watchExecApprovalBridgeStateKey = "watch.execApproval.bridge.state.v1"
private static let backgroundAliveLastSuccessAtMsKey = "gateway.backgroundAlive.lastSuccessAtMs"
private static let backgroundAliveLastTriggerKey = "gateway.backgroundAlive.lastTrigger"
private static let foregroundResumeHealthTimeoutSeconds = 1
var cameraHUDText: String?
var cameraHUDKind: CameraHUDKind?
@@ -419,7 +417,9 @@ final class NodeAppModel {
self.isBackgrounded = false
self.endBackgroundConnectionGracePeriod(reason: "scene_foreground")
self.clearBackgroundReconnectSuppression(reason: "scene_foreground")
var shouldStartGatewayHealthMonitor = self.operatorConnected
if self.operatorConnected {
self.startGatewayHealthMonitor()
}
if phase == .active {
self.voiceWake.resumeAfterExternalAudioCapture(wasSuspended: self.backgroundVoiceWakeSuspended)
self.backgroundVoiceWakeSuspended = false
@@ -444,8 +444,6 @@ final class NodeAppModel {
// iOS may suspend network sockets in background without a clean close.
// On foreground, force a fresh handshake to avoid "connected but dead" states.
if backgroundedFor >= 3.0 {
shouldStartGatewayHealthMonitor = false
self.foregroundGatewayResumeCheckInFlight = true
Task { [weak self] in
guard let self else { return }
let operatorWasConnected = await MainActor.run { self.operatorConnected }
@@ -454,26 +452,31 @@ final class NodeAppModel {
let healthy = await (try? self.operatorGateway.request(
method: "health",
paramsJSON: nil,
timeoutSeconds: Self.foregroundResumeHealthTimeoutSeconds)) != nil
timeoutSeconds: 2)) != nil
if healthy {
await MainActor.run {
self.foregroundGatewayResumeCheckInFlight = false
self.startGatewayHealthMonitor()
}
await MainActor.run { self.startGatewayHealthMonitor() }
return
}
}
await self.operatorGateway.disconnect()
await self.nodeGateway.disconnect()
await MainActor.run {
self.foregroundGatewayResumeCheckInFlight = false
guard !self.isAppleReviewDemoModeEnabled else { return }
self.setOperatorConnected(false)
self.gatewayConnected = false
// Foreground recovery must actively restart the saved gateway config.
// Disconnecting stale sockets alone can leave us idle if the old
// reconnect tasks were suppressed or otherwise got stuck in background.
self.gatewayStatusText = "Reconnecting…"
self.talkMode.updateGatewayConnected(false)
if let cfg = self.activeGatewayConnectConfig {
self.applyGatewayConnectConfig(cfg)
}
}
await self.restartGatewaySessionsAfterForegroundStaleConnection()
}
}
}
if shouldStartGatewayHealthMonitor {
self.startGatewayHealthMonitor()
}
@unknown default:
self.isBackgrounded = false
self.endBackgroundConnectionGracePeriod(reason: "scene_unknown")
@@ -783,12 +786,6 @@ final class NodeAppModel {
func refreshGatewayOverviewIfConnected() async {
guard await self.isOperatorConnected() else { return }
if self.foregroundGatewayResumeCheckInFlight {
GatewayDiagnostics.log("gateway overview refresh deferred reason=foreground_resume_check")
try? await Task.sleep(
nanoseconds: UInt64(Self.foregroundResumeHealthTimeoutSeconds) * 1_000_000_000)
guard await self.isOperatorConnected(), !self.foregroundGatewayResumeCheckInFlight else { return }
}
await self.refreshBrandingFromGateway()
await self.refreshAgentsFromGateway()
}
@@ -1989,33 +1986,12 @@ extension NodeAppModel {
}
func resetGatewaySessionsForForcedReconnect() async {
let nodeGatewayTask = self.nodeGatewayTask
let operatorGatewayTask = self.operatorGatewayTask
nodeGatewayTask?.cancel()
self.nodeGatewayTask?.cancel()
self.nodeGatewayTask = nil
operatorGatewayTask?.cancel()
self.operatorGatewayTask?.cancel()
self.operatorGatewayTask = nil
await self.operatorGateway.disconnect()
await self.nodeGateway.disconnect()
// Foreground recovery reuses the same config immediately after reset.
// Wait for canceled loops so their shutdown cleanup cannot clobber the new reconnect state.
if let operatorGatewayTask {
await operatorGatewayTask.value
}
if let nodeGatewayTask {
await nodeGatewayTask.value
}
}
private func restartGatewaySessionsAfterForegroundStaleConnection() async {
await self.resetGatewaySessionsForForcedReconnect()
guard !self.isAppleReviewDemoModeEnabled else { return }
self.setOperatorConnected(false)
self.gatewayConnected = false
self.gatewayStatusText = "Reconnecting…"
self.talkMode.updateGatewayConnected(false)
guard let cfg = self.activeGatewayConnectConfig else { return }
self.applyGatewayConnectConfig(cfg, forceReconnect: true)
}
func disconnectGateway() {
@@ -4850,10 +4826,6 @@ extension NodeAppModel {
(self.nodeGatewayTask != nil, self.operatorGatewayTask != nil)
}
func _test_restartGatewaySessionsAfterForegroundStaleConnection() async {
await self.restartGatewaySessionsAfterForegroundStaleConnection()
}
func _test_handleSuccessfulBootstrapGatewayOnboarding() async {
await self.handleSuccessfulBootstrapGatewayOnboarding(
url: URL(string: "wss://gateway.example")!,

View File

@@ -356,20 +356,6 @@ import UIKit
#expect(!appModel._test_hasGatewayLoopTasks().operator)
}
@Test @MainActor func foregroundStaleConnectionRestartReappliesActiveGatewayConfig() async {
let appModel = NodeAppModel()
defer { appModel.disconnectGateway() }
let config = Self.makeGatewayConnectConfig()
appModel.applyGatewayConnectConfig(config)
await appModel._test_restartGatewaySessionsAfterForegroundStaleConnection()
#expect(appModel.gatewayStatusText == "Reconnecting…")
#expect(appModel.activeGatewayConnectConfig?.hasSameConnectionInputs(as: config) == true)
#expect(appModel._test_hasGatewayLoopTasks().node)
#expect(appModel._test_hasGatewayLoopTasks().operator)
}
@Test @MainActor func loadLastConnectionReadsSavedValues() {
let prior = KeychainStore.loadString(service: "ai.openclaw.gateway", account: "lastConnection")
defer {

View File

@@ -1,5 +1,5 @@
{
"originHash" : "ae9f37f50cff0d32d189e60948f61e2fa1704e997a6ef4ad5e37f6a11c165ea4",
"originHash" : "035a4fe955164c62c1628de75f6437a14443a947eea2a1b0176ba484d6fde6f8",
"pins" : [
{
"identity" : "axorcist",
@@ -42,8 +42,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/steipete/Peekaboo.git",
"state" : {
"revision" : "ee0e3185431788dad533ffca77cd75315aa3d26f",
"version" : "3.4.1"
"revision" : "3a56ed2aa769bfefb5a78722dfce3c34088cfba1",
"version" : "3.4.0"
}
},
{
@@ -51,8 +51,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/sparkle-project/Sparkle",
"state" : {
"revision" : "d46d456107feacc80711b21847b82b07bd9fb46e",
"version" : "2.9.3"
"revision" : "6276ba2b404829d139c45ff98427cf90e2efc59b",
"version" : "2.9.2"
}
},
{
@@ -78,8 +78,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-log.git",
"state" : {
"revision" : "92448c359f00ebe36ae97d3bd9086f13c7692b5a",
"version" : "1.13.2"
"revision" : "2aed77ae5ec9a86d8fe42c12275e4c2653a286ee",
"version" : "1.13.1"
}
},
{

View File

@@ -19,7 +19,7 @@ let package = Package(
.package(url: "https://github.com/swiftlang/swift-subprocess.git", from: "0.4.0"),
.package(url: "https://github.com/apple/swift-log.git", from: "1.10.1"),
.package(url: "https://github.com/sparkle-project/Sparkle", from: "2.9.0"),
.package(url: "https://github.com/steipete/Peekaboo.git", exact: "3.4.1"),
.package(url: "https://github.com/steipete/Peekaboo.git", exact: "3.4.0"),
.package(path: "../shared/OpenClawKit"),
.package(path: "../swabble"),
],

View File

@@ -92,13 +92,7 @@ extension VoiceWakeOverlayController {
let contentHeight = ceil(used.height + (textInset.height * 2))
let total = contentHeight + self.verticalPadding * 2
// Defer the overflow state mutation to break the SwiftUI onChange measuredHeight
// isOverflowing re-render onChange synchronous render loop (fixes #43480).
let overflowing = total > self.maxHeight
DispatchQueue.main.async { [weak self] in
guard let self, self.model.isOverflowing != overflowing else { return }
self.model.isOverflowing = overflowing
}
self.model.isOverflowing = total > self.maxHeight
return max(self.minHeight, min(total, self.maxHeight))
}

View File

@@ -4,64 +4,14 @@ import Testing
@Suite(.serialized)
struct ExecApprovalsStoreRefactorTests {
private var realTemporaryDirectory: URL {
let path = FileManager().temporaryDirectory.path
if path.hasPrefix("/var/") {
return URL(fileURLWithPath: "/private\(path)", isDirectory: true)
}
return FileManager().temporaryDirectory.resolvingSymlinksInPath()
}
private func withLockedEnv(
_ values: [String: String?],
_ body: () async throws -> Void) async throws
{
func restoreEnv(_ values: [String: String?]) {
for (key, value) in values {
if let value {
setenv(key, value, 1)
} else {
unsetenv(key)
}
}
}
await TestIsolationLock.shared.acquire()
var previousEnv: [String: String?] = [:]
for (key, value) in values {
previousEnv[key] = getenv(key).map { String(cString: $0) }
if let value {
setenv(key, value, 1)
} else {
unsetenv(key)
}
}
do {
try await body()
restoreEnv(previousEnv)
await TestIsolationLock.shared.release()
} catch {
restoreEnv(previousEnv)
await TestIsolationLock.shared.release()
throw error
}
}
private func withTempStateDir(
_ body: @escaping @Sendable (URL) async throws -> Void) async throws
{
let root = self.realTemporaryDirectory
let stateDir = FileManager().temporaryDirectory
.appendingPathComponent("openclaw-state-\(UUID().uuidString)", isDirectory: true)
let home = root.appendingPathComponent("home", isDirectory: true)
let stateDir = root.appendingPathComponent("state", isDirectory: true)
defer { try? FileManager().removeItem(at: root) }
try Self.seedCurrentApprovalsFile(in: stateDir)
defer { try? FileManager().removeItem(at: stateDir) }
try await self.withLockedEnv([
"OPENCLAW_HOME": home.path,
"OPENCLAW_STATE_DIR": stateDir.path,
]) {
try await TestIsolation.withEnvValues(["OPENCLAW_STATE_DIR": stateDir.path]) {
try await body(stateDir)
}
}
@@ -69,13 +19,13 @@ struct ExecApprovalsStoreRefactorTests {
private func withTempHomeAndStateDir(
_ body: @escaping @Sendable (URL, URL) async throws -> Void) async throws
{
let root = self.realTemporaryDirectory
let root = FileManager().temporaryDirectory
.appendingPathComponent("openclaw-home-state-\(UUID().uuidString)", isDirectory: true)
let home = root.appendingPathComponent("home", isDirectory: true)
let stateDir = root.appendingPathComponent("state", isDirectory: true)
defer { try? FileManager().removeItem(at: root) }
try await self.withLockedEnv([
try await TestIsolation.withEnvValues([
"OPENCLAW_HOME": home.path,
"OPENCLAW_STATE_DIR": stateDir.path,
]) {
@@ -197,19 +147,4 @@ struct ExecApprovalsStoreRefactorTests {
}
return identifier
}
private static func seedCurrentApprovalsFile(in stateDir: URL) throws {
try FileManager().createDirectory(at: stateDir, withIntermediateDirectories: true)
let file = ExecApprovalsFile(
version: 1,
socket: ExecApprovalsSocketConfig(
path: stateDir.appendingPathComponent("exec-approvals.sock").path,
token: "test-token"),
defaults: nil,
agents: [:])
let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
try encoder.encode(file)
.write(to: stateDir.appendingPathComponent("exec-approvals.json"))
}
}

View File

@@ -1,4 +1,4 @@
0485ba902d2afd89d2c41cde7180d0cec2900b2db6804b9f97d42b7d85cd3af5 config-baseline.json
72bb80be618406f3337eaa2560d2559a35e49bd29576de8dd4a3aec1a6a94d92 config-baseline.core.json
1218f5555541b61bd5ddcac6441f15061b44789e2471d4ffecbe3059777c55c1 config-baseline.channel.json
a14ac4261e98403d1a7e047070e6f151938444e27382b860315bd0c74fda4861 config-baseline.plugin.json
37b56008790612b8293930b6a29d74490e98daa90f954fca9d133fcc28645c4c config-baseline.json
75b64c2ea081369ba4306493313a8a4cd48b784145f92fed995e6b77a5df350d config-baseline.core.json
17d64c9799dfa239a49493413f1100bdd9237e9b67aaeae331a4604dbc227023 config-baseline.channel.json
f9d1f50bfa8403891e76cd99dc1357cdece4a71e8ae18a39b190c2a14e6f97b0 config-baseline.plugin.json

View File

@@ -1,2 +1,2 @@
85c3572e6ed2bfe3df92c7d53cef465b30d2e861ad9529009faa287cdc5aec71 plugin-sdk-api-baseline.json
0d7c7e42d04b97d40519c5a23ba96599b05868c71a997eb913b9fccbc5fb2515 plugin-sdk-api-baseline.jsonl
2c783beea6b3cda3d79060739a923f9f39e7e8b5942123dd6b08a09143a587ca plugin-sdk-api-baseline.json
0b33af2cffb42abb46682fb71c8f214da220793f13d10a34d332e75ff99e8ce9 plugin-sdk-api-baseline.jsonl

View File

@@ -943,10 +943,6 @@
"source": "Matrix QA",
"target": "Matrix QA"
},
{
"source": "Maturity tests",
"target": "成熟度测试"
},
{
"source": "Matrix presentation metadata",
"target": "Matrix 呈现元数据"

View File

@@ -24,16 +24,6 @@ This directory owns docs authoring, Mintlify link rules, and docs i18n policy.
- `scripts/docs-sync-publish.mjs` excludes and prunes `docs/internal/**` from the public `openclaw/docs` publish repo if a page is force-added later.
- Internal docs may mention repo paths, private app names, 1Password item names, and runbooks, but never include secret values.
## Maturity Scorecard Editing
- `taxonomy.yaml` owns surfaces, categories, feature coverage IDs, maturity levels, QA profile membership, and `human_lts_override` source values.
- `docs/maturity-scores.yaml` owns the current subjective maturity score snapshot generated or refreshed by `claw-score`. Treat Coverage, Quality, Completeness, and manual LTS support in this file as reviewed score state, not deterministic QA evidence.
- `qa-evidence.json.scorecard` owns deterministic per-run QA evidence: category and feature fulfillment, covered or missing coverage IDs, and run evidence counts. Keep this evidence in GitHub Actions artifacts unless a maintainer explicitly asks to commit a sanitized projection.
- Generated scorecard pages such as `docs/maturity-scorecard.md`, `docs/taxonomy.md`, and `docs/taxonomy-outline.md` are projections. Do not hand-edit generated score, LTS, taxonomy, QA profile, or evidence tables; change the YAML/artifact inputs and rerender.
- Human score overrides are optional review actions, not the happy path. A human override must be introduced through a PR, must modify source score state rather than rendered Markdown, and must explain the reason plus public or redacted evidence in the PR body or review thread.
- If an override changes LTS support, update the source field that owns the decision: `taxonomy.yaml` for `human_lts_override` and `docs/maturity-scores.yaml` only for the rendered current score snapshot after regeneration.
- Do not put maintainer proposal docs, private audit notes, discrawl summaries, or release-history registry drafts in published docs. Use private/RFC storage for proposal work until a maintainer explicitly creates a public docs surface.
## Docs i18n
- Foreign-language docs are not maintained in this repo. The generated publish output lives in the separate `openclaw/docs` repo (often cloned locally as `../openclaw-docs`).

View File

@@ -161,20 +161,17 @@ Control how agents process messages:
<Step title="Incoming message arrives">
A WhatsApp group or DM message arrives.
</Step>
<Step title="Route and admission">
OpenClaw applies channel allowlists, group activation rules, and configured ACP binding ownership.
</Step>
<Step title="Broadcast check">
If no configured ACP binding owns the route, OpenClaw checks whether the peer ID is in `broadcast`.
System checks if peer ID is in `broadcast`.
</Step>
<Step title="If broadcast applies">
<Step title="If in broadcast list">
- All listed agents process the message.
- Each agent has its own session key and isolated context.
- Agents process in parallel (default) or sequentially.
</Step>
<Step title="If broadcast does not apply">
OpenClaw dispatches the ordinary route or the configured ACP session route selected during routing.
<Step title="If not in broadcast list">
Normal routing applies (first matching binding).
</Step>
</Steps>
@@ -325,7 +322,7 @@ Broadcast groups work alongside existing routing:
- `GROUP_B`: agent1 AND agent2 respond (broadcast).
<Note>
**Precedence:** `broadcast` takes priority over ordinary route bindings. Configured ACP bindings (`bindings[].type="acp"`) are exclusive: when one matches, OpenClaw dispatches to the configured ACP session instead of fan-out broadcast.
**Precedence:** `broadcast` takes priority over `bindings`.
</Note>
## Troubleshooting
@@ -346,9 +343,9 @@ Broadcast groups work alongside existing routing:
</Accordion>
<Accordion title="Only one agent responding">
**Cause:** Peer ID might be in ordinary route bindings but not `broadcast`, or it might match an exclusive configured ACP binding.
**Cause:** Peer ID might be in `bindings` but not `broadcast`.
**Fix:** Add ordinary route-bound peers to broadcast config, or remove/change the configured ACP binding if fan-out broadcast is desired.
**Fix:** Add to broadcast config or remove from bindings.
</Accordion>
<Accordion title="Performance issues">

View File

@@ -586,7 +586,7 @@ Group inbound payloads set:
- `WasMentioned` (mention gating result)
- Telegram forum topics also include `MessageThreadId` and `IsForum`.
The agent system prompt includes a group intro on the first turn of a new group session. It reminds the model to respond like a human, minimize empty lines and follow normal chat spacing, and avoid typing literal `\n` sequences. Non-Telegram groups also discourage Markdown tables; Telegram rich-text guidance comes from the Telegram channel prompt. Channel-sourced group names and participant labels are rendered as fenced untrusted metadata, not inline system instructions.
The agent system prompt includes a group intro on the first turn of a new group session. It reminds the model to respond like a human, avoid Markdown tables, minimize empty lines and follow normal chat spacing, and avoid typing literal `\n` sequences. Channel-sourced group names and participant labels are rendered as fenced untrusted metadata, not inline system instructions.
## iMessage specifics

View File

@@ -311,6 +311,7 @@ curl "https://api.telegram.org/bot<bot_token>/getUpdates"
- direct chats: preview message + `editMessageText`
- groups/topics: preview message + `editMessageText`
- direct-chat tool progress: optional native `sendMessageDraft` status preview when enabled and supported
Requirement:
@@ -319,10 +320,29 @@ curl "https://api.telegram.org/bot<bot_token>/getUpdates"
- `streaming.preview.toolProgress` controls whether tool/progress updates reuse the same edited preview message (default: `true` when preview streaming is active)
- `streaming.preview.commandText` controls command/exec detail inside those tool-progress lines: `raw` (default, preserves released behavior) or `status` (tool label only)
- `streaming.progress.commentary` (default: `false`) opts into assistant commentary/preamble text in the temporary progress draft
- legacy `channels.telegram.streamMode`, boolean `streaming` values, and retired native draft preview keys are detected; run `openclaw doctor --fix` to migrate them to current streaming config
- legacy `channels.telegram.streamMode` and boolean `streaming` values are detected; run `openclaw doctor --fix` to migrate them to `channels.telegram.streaming.mode`
Tool-progress preview updates are the short status lines shown while tools run, for example command execution, file reads, planning updates, patch summaries, or Codex preamble/commentary text in Codex app-server mode. Telegram keeps these enabled by default to match released OpenClaw behavior from `v2026.4.22` and later.
Direct chats can use native Telegram drafts for these tool-progress lines without persisting tool chatter into chat history. Native drafts stop before answer text starts; final answers stay on the normal persistent delivery path. This lane is off by default and should be gated to trusted DM IDs first:
```json
{
"channels": {
"telegram": {
"streaming": {
"mode": "partial",
"preview": {
"toolProgress": true,
"nativeToolProgress": true,
"nativeToolProgressAllowFrom": ["123456789"]
}
}
}
}
}
```
To keep the edited preview for answer text but hide tool-progress lines, set:
```json
@@ -400,16 +420,14 @@ curl "https://api.telegram.org/bot<bot_token>/getUpdates"
</Accordion>
<Accordion title="Rich message formatting">
Outbound text uses Telegram rich messages.
<Accordion title="Formatting and HTML fallback">
Outbound text uses Telegram `parse_mode: "HTML"`.
- Markdown text is sent as rich Markdown without converting it to HTML.
- Explicit HTML payloads are sent as rich HTML.
- Media captions still use Telegram HTML captions because rich messages do not replace captions.
- Markdown-ish text is rendered to Telegram-safe HTML.
- Supported Telegram HTML tags are preserved; unsupported HTML is escaped.
- If Telegram rejects parsed HTML, OpenClaw retries as plain text.
Long rich text is split automatically across Telegram's rich text and rich block limits. Tables over Telegram's column limit are sent as code blocks.
Link previews are enabled by default. `channels.telegram.linkPreview: false` skips automatic entity detection for rich text.
Link previews are enabled by default and can be disabled with `channels.telegram.linkPreview: false`.
</Accordion>

View File

@@ -319,40 +319,6 @@ content and identifiers.
</Tab>
</Tabs>
## Configured ACP bindings
WhatsApp supports persistent ACP bindings with top-level `bindings[]` entries:
```json5
{
bindings: [
{
type: "acp",
agentId: "codex",
match: {
channel: "whatsapp",
accountId: "work",
peer: { kind: "direct", id: "+15555550123" },
},
},
{
type: "acp",
agentId: "codex",
match: {
channel: "whatsapp",
accountId: "work",
peer: { kind: "group", id: "120363424282127706@g.us" },
},
},
],
}
```
- Direct chats match E.164 numbers such as `+15555550123`.
- Groups match WhatsApp group JIDs such as `120363424282127706@g.us`.
- Group allowlists, sender policy, and mention or activation gating run before OpenClaw ensures the configured ACP session exists.
- A matched configured ACP binding owns the route. WhatsApp broadcast groups do not fan out that turn to ordinary WhatsApp sessions.
## Personal-number and self-chat behavior
When the linked self number is also present in `allowFrom`, WhatsApp self-chat safeguards activate:

View File

@@ -200,19 +200,13 @@ from `release/YYYY.M.PATCH` or `main` after the release tag exists and after the
OpenClaw npm preflight has succeeded. It verifies `pnpm plugins:sync:check`,
dispatches `Plugin NPM Release` for all publishable plugin packages, dispatches
`Plugin ClawHub Release` for the same release SHA, and only then dispatches
`OpenClaw NPM Release` with the saved `preflight_run_id`. Stable publish also
requires an exact `windows_node_tag`; the workflow verifies the Windows source
release and compares its x64/ARM64 installers with the candidate-approved
`windows_node_installer_digests` input before any publish child, then promotes
and verifies those same pinned installer digests plus the exact companion asset
and checksum contract before publishing the GitHub release draft.
`OpenClaw NPM Release` with the saved `preflight_run_id`.
```bash
gh workflow run openclaw-release-publish.yml \
--ref release/YYYY.M.PATCH \
-f tag=vYYYY.M.PATCH-beta.N \
-f preflight_run_id=<successful-openclaw-npm-preflight-run-id> \
-f full_release_validation_run_id=<successful-full-release-validation-run-id> \
-f npm_dist_tag=beta
```
@@ -458,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`, or `src`, 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`, `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.
### Security categories
@@ -468,6 +462,7 @@ 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

@@ -174,22 +174,7 @@ Notes:
or `--element`.
- `existing-session` / `user` profiles support page screenshots and `--ref`
screenshots from snapshot output, but not CSS `--element` screenshots.
- `--labels` overlays current snapshot refs on the screenshot. On
Playwright-backed profiles, it works with `--full-page` (full-page label
overlay), `--ref` (element-clip label overlay by ARIA ref), and `--element`
(element-clip label overlay by CSS selector); in element-clip modes, labels
are projected relative to the element. The response also includes an
`annotations` array with each ref's bounding box. Each item has `ref`,
`number`, `role`, optional `name`, and `box: {x, y, width, height}`;
coordinates are in the captured image's space (viewport / fullpage /
element-relative). The field is omitted when empty.
`existing-session` profiles render a chrome-mcp overlay on page screenshots
but do not use the Playwright projection helper and do not include
`annotations`; CSS `--element` screenshots are unsupported there. Without
Playwright or chrome-mcp, labeled screenshots are not available. Prior
releases ignored `--full-page`, `--ref`, and `--element` on labeled
Playwright screenshots and always returned a viewport capture; labeled
screenshots now honor those scopes.
- `--labels` overlays current snapshot refs on the screenshot.
- `snapshot --urls` appends discovered link destinations to AI snapshots so
agents can choose direct navigation targets instead of guessing from link
text alone.

View File

@@ -182,10 +182,7 @@ Interactive onboarding behavior with reference mode:
### Non-interactive Z.AI endpoint choices
<Note>
`--auth-choice zai-api-key` auto-detects the best Z.AI endpoint and model for
your key. Coding Plan endpoints prefer `zai/glm-5.2`; general API endpoints use
`zai/glm-5.1`. To force a Coding Plan endpoint, pick `zai-coding-global` or
`zai-coding-cn`.
`--auth-choice zai-api-key` auto-detects the best Z.AI endpoint for your key (prefers the general API with `zai/glm-5.1`). If you specifically want the GLM Coding Plan endpoints, pick `zai-coding-global` or `zai-coding-cn`.
</Note>
```bash

View File

@@ -159,7 +159,7 @@ is available, then fall back to `latest`.
<Accordion title="--dangerously-force-unsafe-install">
`--dangerously-force-unsafe-install` is deprecated and is now a no-op. OpenClaw no longer runs built-in install-time dangerous-code blocking for plugin installs.
Use the shared operator-owned `security.installPolicy` surface when host-specific install policy is required. Plugin `before_install` hooks are plugin-runtime lifecycle hooks and are not the primary policy boundary for CLI installs.
Use the shared operator-owned `security.installPolicy` surface when host-specific install policy is required. Plugin `before_install` hooks and `security.installPolicy` can still block installs.
If a plugin you published on ClawHub is hidden or blocked by a registry scan, use the publisher steps in [ClawHub publishing](/clawhub/publishing). `--dangerously-force-unsafe-install` does not ask ClawHub to rescan the plugin or make a blocked release public.
@@ -405,7 +405,7 @@ Updates apply to tracked plugin installs in the managed plugin index and tracked
</Accordion>
<Accordion title="--dangerously-force-unsafe-install on update">
`--dangerously-force-unsafe-install` is also accepted on `plugins update` for compatibility, but it is deprecated and no longer changes plugin update behavior. Operator `security.installPolicy` can still block updates; plugin `before_install` hooks only apply in processes where plugin hooks are loaded.
`--dangerously-force-unsafe-install` is also accepted on `plugins update` for compatibility, but it is deprecated and no longer changes plugin update behavior. Operator `security.installPolicy` and plugin `before_install` hooks can still block updates.
</Accordion>
</AccordionGroup>

View File

@@ -479,9 +479,6 @@ names that plugin registers. Active Memory lists those tools in the recall
prompt and passes the same list to the embedded sub-agent. If none of the
configured tools are available, or the memory sub-agent fails, Active Memory
skips recall for that turn and the main reply continues without memory context.
For custom recall tools, non-empty model-visible tool output counts as recall
evidence unless structured result fields explicitly report an empty result or
failure.
`toolsAllow` only accepts concrete memory tool names. Wildcards, `group:*`
entries, and core agent tools such as `read`, `exec`, `message`, and
`web_search` are ignored before the hidden memory sub-agent starts.
@@ -746,11 +743,7 @@ Before v2026.5.2 the plugin silently extended your configured `timeoutMs` by an
extra 30000 ms during cold-start so model warm-up, embedding-index load, and
the first recall could share one larger budget. v2026.5.2 moved that grace
behind an explicit `setupGraceTimeoutMs` config — your configured `timeoutMs`
is now the recall-work budget by default, unless you opt in. The blocking hook
uses two bounded phases around that budget: up to 1500 ms for session/config
preflight before recall starts, then a separate fixed 1500 ms for abort
settlement and transcript recovery after recall work stops. Neither allowance
extends model or tool execution.
is now the budget by default, unless you opt in.
If you upgraded from v2026.4.x and you set `timeoutMs` to a value tuned for the
old implicit-grace world (the recommended starter `timeoutMs: 15000` is one
@@ -772,16 +765,14 @@ outer watchdog budgets back to the pre-v5.2 effective values:
}
```
The v2026.5.2 change removed the old implicit 30000 ms cold-start extension.
Beyond the configured recall-work budget, the hook can use up to 1500 ms for
preflight and another 1500 ms for post-recall completion. Its worst-case
blocking time is therefore `timeoutMs + setupGraceTimeoutMs + 3000` ms.
Per the v2026.5.2 changelog: _"use the configured recall timeout as the
blocking prompt-build hook budget by default and move cold-start setup grace
behind explicit `setupGraceTimeoutMs` config, so the plugin no longer silently
extends 15000 ms configs to 45000 ms on the main lane."_
The embedded recall runner uses the same effective timeout budget, so
`setupGraceTimeoutMs` covers both the outer prompt-build watchdog and the inner
blocking recall run. The preflight cap covers session/config checks before that
budget begins. The post-recall allowance lets the outer hook settle abort
cleanup and read any final transcript state.
blocking recall run.
For resource-tight gateways where cold-start latency is a known trade-off,
lower values (500015000 ms) work too — the trade-off is a higher chance of

View File

@@ -97,7 +97,7 @@ These run inside the agent loop or gateway pipeline:
- **`agent_end`**: inspect the final message list and run metadata after completion.
- **`before_compaction` / `after_compaction`**: observe or annotate compaction cycles.
- **`before_tool_call` / `after_tool_call`**: intercept tool params/results.
- **`before_install`**: inspect staged skill or plugin install material after operator install policy runs, when plugin hooks are loaded in the current OpenClaw process.
- **`before_install`**: inspect install context and optionally block skill or plugin installs after operator install policy runs.
- **`tool_result_persist`**: synchronously transform tool results before they are written to an OpenClaw-owned session transcript.
- **`message_received` / `message_sending` / `message_sent`**: inbound + outbound message hooks.
- **`session_start` / `session_end`**: session lifecycle boundaries.
@@ -109,7 +109,6 @@ Hook decision rules for outbound/tool guards:
- `before_tool_call`: `{ block: false }` is a no-op and does not clear a prior block.
- `before_install`: `{ block: true }` is terminal and stops lower-priority handlers.
- `before_install`: `{ block: false }` is a no-op and does not clear a prior block.
- Use `security.installPolicy`, not `before_install`, for operator-owned install allow/block decisions that must cover CLI install and update paths.
- `message_sending`: `{ cancel: true }` is terminal and stops lower-priority handlers.
- `message_sending`: `{ cancel: false }` is a no-op and does not clear a prior cancel.

View File

@@ -264,7 +264,7 @@ Gemini CLI JSON replies are parsed from `response`; usage falls back to `stats`,
- Provider: `zai`
- Auth: `ZAI_API_KEY`
- Example model: `zai/glm-5.2`
- Example model: `zai/glm-5.1`
- CLI: `openclaw onboard --auth-choice zai-api-key`
- Model refs use the canonical `zai/*` provider ID.
- `zai-api-key` auto-detects the matching Z.AI endpoint; `zai-coding-global`, `zai-coding-cn`, `zai-global`, and `zai-cn` force a specific surface

View File

@@ -918,7 +918,6 @@ When choosing focused proof for a touched behavior or file path, run `pnpm openc
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 scorecard context, see [Maturity tests](/reference/maturity-tests).
For character and style checks, run the same scenario across multiple live model
refs and write a judged Markdown report:
@@ -976,7 +975,6 @@ When no `--judge-model` is passed, the judges default to
## Related docs
- [Matrix QA](/concepts/qa-matrix)
- [Maturity tests](/reference/maturity-tests)
- [Personal agent benchmark pack](/concepts/personal-agent-benchmark-pack)
- [QA Channel](/channels/qa-channel)
- [Testing](/help/testing)

View File

@@ -32,13 +32,8 @@ title: "Usage tracking"
## Custom `/usage full` footer
`/usage full` shows a built-in compact footer with model, reasoning, fast/slow,
context window, turn tokens, cache, and cost when those fields are available. No
template file is required.
`messages.usageTemplate` is only for advanced custom layouts. The value is a
JSON file path (supports `~`) or an inline object, and it replaces the built-in
footer when valid:
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
{
@@ -48,182 +43,9 @@ footer when valid:
}
```
Missing or empty templates fall back to the built-in footer quietly. Unreadable
or invalid configured templates also fall back to the built-in footer and emit an
operator warning.
Start custom templates from the built-in shape, then edit the parts you want to
change:
```jsonc
{
"schema": "openclaw.usageBar.v1",
"scales": {
"braille": "⠐⡀⡄⡆⡇⣇⣧⣷⣿",
"block": "░▏▎▍▌▋▊▉█",
"shade": "░▒▓█",
"moon": "🌑🌘🌗🌖🌕",
"level": "▁▂▃▄▅▆▇█",
"weather": ["🥶", "☁️", "🌥", "⛅️", "🌤", "☀️"],
"plants": ["🪾", "🍂", "🌱", "☘️", "🍀", "🌿"],
"moons6": ["🌑", "🌚", "🌘", "🌗", "🌖", "🌝"],
},
"aliases": {
"models": {
"claude-opus-4-6": "opus46",
"claude-opus-4-8": "opus48",
"claude-sonnet-4-6": "sonnet46",
"claude-haiku-4-5": "haiku45",
"gpt-5.5": "gpt5.5",
},
"reasoning": {
"off": "🌑",
"minimal": "🌚",
"low": "🌘",
"medium": "🌗",
"high": "🌕",
"xhigh": "🌝",
},
},
"output": {
"sep": "",
"default": [
{ "text": "{model.provider}{identity.emoji|🤖} {model.display_name|alias:models}" },
{ "map": "model.is_fallback", "cases": { "true": " 🔄" } },
{ "map": "model.is_override", "cases": { "true": " 📌" } },
{ "when": "model.reasoning", "text": " {model.reasoning|alias:reasoning}" },
{ "map": "state.fast_mode", "cases": { "true": " ⚡", "false": " 🐌" } },
{
"when": "context.max_tokens",
"text": " | 📚 [{context.pct_used|meter:5:braille}]{context.max_tokens|num}",
},
{
"when": "usage.has_split_tokens",
"text": " ↕️ {usage.input_tokens|num|?}/{usage.output_tokens|num|?}",
},
{ "when": "usage.has_total_only_tokens", "text": " ↕️ {usage.total_tokens|num}" },
{ "when": "usage.cache_hit_pct", "text": " 🗄 {usage.cache_hit_pct|pct}" },
{ "when": "cost.turn_usd", "text": " 💰{cost.turn_usd|fixed:4}" },
],
"surfaces": {
"discord": [
{ "text": "-# -\n" },
{ "text": "-# {model.provider}{identity.emoji|🤖} {model.display_name|alias:models}" },
{ "map": "model.is_fallback", "cases": { "true": "🔄" } },
{ "map": "model.is_override", "cases": { "true": "📌" } },
{ "when": "model.reasoning", "text": " {model.reasoning|alias:reasoning}" },
{ "map": "state.fast_mode", "cases": { "true": " ⚡️", "false": " 🐌" } },
{
"when": "context.max_tokens",
"text": " | 📚 [{context.pct_used|meter:5:braille}]{context.max_tokens|num}",
},
{
"when": "usage.has_split_tokens",
"text": " ↕️ {usage.input_tokens|num|?}/{usage.output_tokens|num|?}",
},
{ "when": "usage.has_total_only_tokens", "text": " ↕️ {usage.total_tokens|num}" },
{ "when": "usage.cache_hit_pct", "text": " 🗄 {usage.cache_hit_pct|pct}" },
{ "when": "cost.turn_usd", "text": " 💰{cost.turn_usd|fixed:4}" },
],
},
},
}
```
### Shape
```jsonc
{
"schema": "openclaw.usageBar.v1",
"scales": { "<name>": "low-to-high glyphs" }, // string (1 glyph/char) or array
"aliases": { "<table>": { "<value>": "<label>" } },
"output": {
"sep": "", // joins surviving pieces
"default": [
/* pieces */
], // fallback for any surface
"surfaces": {
"discord": [
/* pieces */
],
"telegram": [
/* pieces */
],
},
},
}
```
Each surface is an ordered list of **pieces**; the engine renders each, drops
empties, and joins survivors with `sep`. A surface with no entry uses
`output.default`.
### Contract Paths
A piece reads values from the per-turn contract by dot-path. Absent values are
empty (so a `when` guard or a `|fallback` keeps the piece clean).
| Path | Meaning |
| ----------------------------------------------------------------------------------- | -------------------------------------- |
| `surface` | channel id (`discord`/`telegram`/etc.) |
| `model.provider` / `model.display_name` | provider id / model id |
| `model.reasoning` | effort (`off` through `xhigh`) |
| `model.is_fallback` / `model.is_override` | bool: fallback used / model pinned |
| `state.fast_mode` | bool: fast vs slow |
| `context.max_tokens` / `context.pct_used` | window budget / 0-100 used |
| `usage.input_tokens` / `usage.output_tokens` / `usage.total_tokens` | turn aggregate |
| `usage.has_split_tokens` / `usage.has_total_only_tokens` / `usage.cache_hit_pct` | token display guards and cache percent |
| `usage.last.input_tokens` / `usage.last.output_tokens` / `usage.last.cache_hit_pct` | final model call only |
| `cost.turn_usd` | estimated turn cost |
| `identity.name` / `identity.emoji` | agent name / chosen emoji |
(Provider rate-limit windows are **not** in this contract.)
### Verbs
Pipe a value through verbs left to right; a non-verb segment is the fallback.
| Verb | Effect | Example |
| --------------- | ------------------------------------- | --------------------------------- |
| `num` | compact count | `272000 -> 272k` |
| `fixed:N` | N decimals (default 2) | `0.0377` |
| `dur` | seconds to duration | `14820 -> 4h07m` |
| `pct` | append `%` | `96 -> 96%` |
| `inv` | `100 - x` | for used to remaining |
| `alias:TABLE` | lookup in `aliases`, echo if unlisted | `medium -> 🌗` |
| `meter:W:SCALE` | W-cell glyph bar over a 0-100 value | `[⣿⣿⠐⠐⠐]` (`meter:1` = one glyph) |
### Piece forms
- `{ "text": "📚 {context.max_tokens|num}" }`: literal + interpolation.
- `{ "when": "<path>", "text": "..." }`: render only if the path is truthy.
- `{ "map": "<path>", "cases": { "true": "⚡", "false": "🐌" } }`: value to glyph.
- `{ "each": "limits.windows", "item": "{label}" }`: iterate an array.
### Example
```jsonc
{
"schema": "openclaw.usageBar.v1",
"scales": { "braille": "⠐⡀⡄⡆⡇⣇⣧⣷⣿" },
"aliases": { "reasoning": { "medium": "🌗", "high": "🌕" } },
"output": {
"surfaces": {
"discord": [
{ "text": "{model.display_name}" },
{ "when": "model.reasoning", "text": " {model.reasoning|alias:reasoning}" },
{ "map": "state.fast_mode", "cases": { "true": " ⚡", "false": " 🐌" } },
{
"when": "context.max_tokens",
"text": " | 📚 [{context.pct_used|meter:5:braille}]{context.max_tokens|num}",
},
],
},
},
}
```
renders e.g. `claude-sonnet-4-6 🌗 🐌 | 📚 [⣿⣿⣿⣿⣧]272k`.
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

View File

@@ -1845,11 +1845,7 @@
"pages": [
"reference/RELEASING",
"reference/full-release-validation",
"reference/maturity-tests",
"reference/release-performance-sweep",
"maturity-scorecard",
"taxonomy",
"taxonomy-outline",
"reference/test",
"ci",
"help/scripts"

View File

@@ -130,8 +130,6 @@ WhatsApp runs through the gateway's web channel (Baileys Web). It starts automat
}
```
- Top-level `bindings[]` entries with `type: "acp"` configure persistent ACP bindings for WhatsApp DMs and groups. Use an E.164 direct number or WhatsApp group JID in `match.peer.id`. Field semantics are shared in [ACP Agents](/tools/acp-agents#persistent-channel-bindings).
<Accordion title="Multi-account WhatsApp">
```json5

View File

@@ -339,7 +339,7 @@ Configures inbound media understanding (image/audio/video):
- `capabilities`: optional list (`image`, `audio`, `video`). Defaults: `openai`/`anthropic`/`minimax` → image, `google` → image+audio+video, `groq` → audio.
- `prompt`, `maxChars`, `maxBytes`, `timeoutSeconds`, `language`: per-entry overrides.
- `tools.media.image.timeoutSeconds` and matching image model `timeoutSeconds` entries also apply when the agent calls the explicit `image` tool. For image understanding, this timeout applies to the request itself and is not reduced by earlier preparation work.
- `tools.media.image.timeoutSeconds` and matching image model `timeoutSeconds` entries also apply when the agent calls the explicit `image` tool.
- Failures fall back to the next entry.
Provider auth follows standard order: `auth-profiles.json` → env vars → `models.providers.*.apiKey`.

View File

@@ -73,7 +73,7 @@ Live tests are split into two layers so we can isolate failures:
- `pnpm test:live` (or `OPENCLAW_LIVE_TEST=1` if invoking Vitest directly)
- Set `OPENCLAW_LIVE_MODELS=modern`, `small`, or `all` (alias for modern) to actually run this suite; otherwise it skips to keep `pnpm test:live` focused on gateway smoke
- How to select models:
- `OPENCLAW_LIVE_MODELS=modern` to run the modern allowlist (Opus/Sonnet 4.6+, GPT-5.2 + Codex, Gemini 3, DeepSeek V4, GLM 5.1, MiniMax M3, Grok 4.3)
- `OPENCLAW_LIVE_MODELS=modern` to run the modern allowlist (Opus/Sonnet 4.6+, GPT-5.2 + Codex, Gemini 3, DeepSeek V4, GLM 4.7, MiniMax M3, Grok 4.3)
- `OPENCLAW_LIVE_MODELS=small` to run the constrained small-model allowlist (Qwen 8B/9B local-compatible routes, Ollama Gemma, OpenRouter Qwen/GLM, and Z.AI GLM)
- `OPENCLAW_LIVE_MODELS=all` is an alias for the modern allowlist
- or `OPENCLAW_LIVE_MODELS="openai/gpt-5.5,anthropic/claude-opus-4-6,..."` (comma allowlist)
@@ -357,9 +357,6 @@ Narrow, explicit allowlists are fastest and least flaky:
- Tool calling across several providers:
- `OPENCLAW_LIVE_GATEWAY_MODELS="openai/gpt-5.5,anthropic/claude-opus-4-6,google/gemini-3-flash-preview,deepseek/deepseek-v4-flash,zai/glm-5.1,minimax/MiniMax-M3" pnpm test:live src/gateway/gateway-models.profiles.live.test.ts`
- Z.AI Coding Plan GLM-5.2 direct smoke:
- `ZAI_CODING_LIVE_TEST=1 pnpm test:live src/agents/zai.live.test.ts`
- Google focus (Gemini API key + Antigravity):
- Gemini (API key): `OPENCLAW_LIVE_GATEWAY_MODELS="google/gemini-3-flash-preview" pnpm test:live src/gateway/gateway-models.profiles.live.test.ts`
- Antigravity (OAuth): `OPENCLAW_LIVE_GATEWAY_MODELS="google-antigravity/claude-opus-4-6-thinking,google-antigravity/gemini-3-pro-high" pnpm test:live src/gateway/gateway-models.profiles.live.test.ts`
@@ -391,7 +388,7 @@ This is the "common models" run we expect to keep working:
- Google (Gemini API): `google/gemini-3.1-pro-preview` and `google/gemini-3-flash-preview` (avoid older Gemini 2.x models)
- Google (Antigravity): `google-antigravity/claude-opus-4-6-thinking` and `google-antigravity/gemini-3-flash`
- DeepSeek: `deepseek/deepseek-v4-flash` and `deepseek/deepseek-v4-pro`
- Z.AI (GLM): `zai/glm-5.1` (general API) or `zai/glm-5.2` (Coding Plan)
- Z.AI (GLM): `zai/glm-5.1`
- MiniMax: `minimax/MiniMax-M3`
Run gateway smoke with tools + image:
@@ -405,7 +402,7 @@ Pick at least one per provider family:
- Anthropic: `anthropic/claude-opus-4-6` (or `anthropic/claude-sonnet-4-6`)
- Google: `google/gemini-3-flash-preview` (or `google/gemini-3.1-pro-preview`)
- DeepSeek: `deepseek/deepseek-v4-flash`
- Z.AI (GLM): `zai/glm-5.1` (general API) or `zai/glm-5.2` (Coding Plan)
- Z.AI (GLM): `zai/glm-5.1`
- MiniMax: `minimax/MiniMax-M3`
Optional additional coverage (nice to have):

View File

@@ -21,7 +21,6 @@ of Docker runners. This doc is a "how we test" guide:
- [QA overview](/concepts/qa-e2e-automation) - architecture, command surface, scenario authoring.
- [Matrix QA](/concepts/qa-matrix) - reference for `pnpm openclaw qa matrix`.
- [QA channel](/channels/qa-channel) - the synthetic transport plugin used by repo-backed scenarios.
- [Maturity tests](/reference/maturity-tests) - how QA evidence maps to scorecard coverage.
This page covers running the regular test suites and Docker/Parallels runners. The QA-specific runners section below ([QA-specific runners](#qa-specific-runners)) lists the concrete `qa` invocations and points back at the references above.
</Note>

View File

@@ -1,89 +0,0 @@
---
title: "Maturity scorecard"
summary: "Generated OpenClaw maturity scorecard for product, platform, provider, channel, and QA surfaces."
---
# Maturity scorecard
> This file is generated from `taxonomy.yaml` and `docs/maturity-scores.yaml`. Run `pnpm maturity:render` after editing scorecard sources.
> Committed docs intentionally exclude the old maintainer inventory tree; per-run QA evidence stays in GitHub Actions artifacts.
## Overview
- Active surfaces: 50
- Category scores: 281
- Process version: 3
## Rollups
| Basis | Coverage | Quality | Completeness |
| ---------------- | ------------- | ------------- | ------------- |
| Surface average | `Alpha (68%)` | `Alpha (66%)` | `Alpha (68%)` |
| Category average | `Alpha (69%)` | `Alpha (66%)` | `Alpha (69%)` |
## QA profiles
| Profile | Evidence mode | Scope | Description |
| ---------- | ------------- | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `smoke-ci` | slim | 16 categories | Deterministic PR and merge proof with mock model providers, synthetic qa-channel, and local SDK-backed channel upstreams; no live external service required. |
| `release` | full | All categories | Stable/LTS proof selector for live providers, live channels, package artifacts, upgrade paths, and platform proof where the claim depends on real upstreams or release artifacts. |
## Surface scorecard
| Surface | Family | Level | Coverage | Quality | Completeness | LTS | Categories | Last score run |
| --------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------- | --------------- | -------------------- | -------------------- | -------------------- | --------------- | ---------- | ---------------------- |
| [Gateway runtime](/taxonomy#gateway-runtime) | Core | M4 Stable | `Stable (81%)` | `Alpha (69%)` | `Stable (80%)` | partial (12/13) | 13 | complete on 2026-05-31 |
| [CLI](/taxonomy#cli-install-update-onboard-doctor) | Core | M4 Stable | `Stable (83%)` | `Beta (72%)` | `Stable (80%)` | partial (6/7) | 7 | complete on 2026-05-30 |
| [Plugins](/taxonomy#plugin-sdk-and-bundled-plugin-architecture) | Core | M3 Beta | `Stable (82%)` | `Stable (80%)` | `Stable (81%)` | partial (7/9) | 9 | complete on 2026-05-30 |
| [Agent Runtime](/taxonomy#agent-runtime-and-provider-execution) | Core | M3 Beta | `Stable (80%)` | `Alpha (69%)` | `Stable (80%)` | partial (6/9) | 9 | complete on 2026-05-30 |
| [Session, memory, and context engine](/taxonomy#session-memory-and-context-engine) | Core | M3 Beta | `Beta (74%)` | `Alpha (66%)` | `Beta (74%)` | partial (6/9) | 9 | complete on 2026-05-30 |
| [Channel framework](/taxonomy#channel-framework) | Core | M3 Beta | `Beta (77%)` | `Beta (74%)` | `Beta (77%)` | partial (5/8) | 8 | complete on 2026-05-30 |
| [Security, auth, pairing, and secrets](/taxonomy#security-auth-pairing-and-secrets) | Core | M3 Beta | `Stable (80%)` | `Alpha (67%)` | `Stable (80%)` | partial (5/6) | 6 | complete on 2026-05-30 |
| [Observability](/taxonomy#telemetry-diagnostics-and-observability) | Core | M3 Beta | `Stable (80%)` | `Beta (78%)` | `Stable (80%)` | partial (3/5) | 5 | complete on 2026-05-30 |
| [Automation: cron, hooks, tasks, polling](/taxonomy#automation-cron-hooks-tasks-polling) | Core | M3 Beta | `Beta (76%)` | `Alpha (69%)` | `Beta (76%)` | none (0/6) | 6 | complete on 2026-05-30 |
| [Media understanding and media generation](/taxonomy#media-understanding-and-media-generation) | Core | M2 Alpha | `Beta (78%)` | `Beta (70%)` | `Beta (78%)` | none (0/6) | 6 | complete on 2026-05-30 |
| [Voice and realtime talk](/taxonomy#voice-and-realtime-talk) | Core | M2 Alpha | `Beta (73%)` | `Alpha (67%)` | `Beta (73%)` | none (0/6) | 6 | complete on 2026-05-30 |
| [Gateway Web App](/taxonomy#browser-control-ui-and-webchat) | Core | M3 Beta | `Beta (79%)` | `Beta (71%)` | `Beta (79%)` | none (0/6) | 6 | complete on 2026-05-30 |
| [TUI](/taxonomy#tui-and-terminal-ux) | Core | M2 Alpha | `Beta (76%)` | `Beta (71%)` | `Beta (76%)` | none (0/5) | 5 | complete on 2026-05-30 |
| [ClawHub](/taxonomy#clawhub-and-external-plugin-distribution) | Core | M2 Alpha | `Beta (72%)` | `Beta (73%)` | `Beta (72%)` | none (0/4) | 4 | complete on 2026-05-30 |
| [OpenClaw App SDK](/taxonomy#openclaw-app-sdk) | Core | M2 Alpha | `Beta (75%)` | `Beta (75%)` | `Alpha (69%)` | none (0/6) | 6 | complete on 2026-06-01 |
| [macOS Gateway host](/taxonomy#macos-gateway-host) | Platform | M4 Stable | `Beta (75%)` | `Beta (79%)` | `Beta (75%)` | none (0/7) | 7 | complete on 2026-05-30 |
| [macOS companion app](/taxonomy#macos-companion-app) | Platform | M3 Beta | `Beta (71%)` | `Alpha (66%)` | `Beta (71%)` | none (0/8) | 8 | complete on 2026-05-30 |
| [Linux Gateway host](/taxonomy#linux-gateway-host) | Platform | M4 Stable | `Stable (80%)` | `Beta (76%)` | `Stable (80%)` | partial (4/5) | 5 | complete on 2026-05-30 |
| [Linux companion app](/taxonomy#linux-companion-app) | Platform | M0 Planned | `Experimental (5%)` | `Experimental (27%)` | `Experimental (5%)` | none (0/5) | 5 | complete on 2026-05-30 |
| [Windows via WSL2](/taxonomy#windows-via-wsl2) | Platform | M3 Beta | `Beta (72%)` | `Alpha (69%)` | `Beta (72%)` | partial (5/6) | 6 | complete on 2026-05-30 |
| [Native Windows](/taxonomy#native-windows-cli-and-gateway) | Platform | M2 Alpha | `Alpha (68%)` | `Alpha (63%)` | `Alpha (68%)` | partial (1/4) | 4 | complete on 2026-05-30 |
| [Native Windows companion app](/taxonomy#native-windows-companion-app) | Platform | M0 Planned | `Experimental (5%)` | `Experimental (30%)` | `Experimental (5%)` | none (0/5) | 5 | complete on 2026-05-30 |
| [Android app](/taxonomy#android-app) | Platform | M2 Alpha | `Alpha (65%)` | `Alpha (62%)` | `Alpha (65%)` | none (0/7) | 7 | complete on 2026-05-30 |
| [iOS app](/taxonomy#ios-app) | Platform | M1 Experimental | `Experimental (41%)` | `Experimental (45%)` | `Experimental (41%)` | none (0/8) | 8 | complete on 2026-05-30 |
| [watchOS companion surfaces](/taxonomy#watchos-companion-surfaces) | Platform | M1 Experimental | `Experimental (45%)` | `Alpha (57%)` | `Experimental (45%)` | none (0/5) | 5 | complete on 2026-05-30 |
| [Raspberry Pi / small Linux devices](/taxonomy#raspberry-pi-small-linux-devices) | Platform | M3 Beta | `Beta (70%)` | `Alpha (67%)` | `Beta (70%)` | none (0/4) | 4 | complete on 2026-05-30 |
| [Docker / Podman hosting](/taxonomy#docker-podman-hosting) | Platform | M3 Beta | `Beta (77%)` | `Beta (73%)` | `Beta (77%)` | none (0/4) | 4 | complete on 2026-05-30 |
| [Kubernetes hosting](/taxonomy#kubernetes-hosting) | Platform | M2 Alpha | `Alpha (50%)` | `Beta (75%)` | `Beta (74%)` | none (0/4) | 4 | complete on 2026-06-01 |
| [Nix install path](/taxonomy#nix-install-path) | Platform | M1 Experimental | `Experimental (38%)` | `Experimental (45%)` | `Experimental (38%)` | none (0/5) | 5 | complete on 2026-05-30 |
| [Discord](/taxonomy#discord) | Channel | M4 Stable | `Beta (71%)` | `Beta (71%)` | `Beta (71%)` | partial (4/6) | 6 | complete on 2026-05-30 |
| [Telegram](/taxonomy#telegram) | Channel | M3 Beta | `Beta (75%)` | `Beta (70%)` | `Beta (75%)` | full (5/5) | 5 | complete on 2026-05-30 |
| [WhatsApp](/taxonomy#whatsapp) | Channel | M3 Beta | `Beta (76%)` | `Beta (76%)` | `Beta (76%)` | none (0/5) | 5 | complete on 2026-05-30 |
| [Slack](/taxonomy#slack) | Channel | M3 Beta | `Beta (70%)` | `Alpha (68%)` | `Beta (70%)` | full (5/5) | 5 | complete on 2026-05-30 |
| [iMessage / BlueBubbles](/taxonomy#imessage-bluebubbles) | Channel | M3 Beta | `Beta (71%)` | `Beta (72%)` | `Beta (71%)` | none (0/5) | 5 | complete on 2026-05-30 |
| [Signal](/taxonomy#signal) | Channel | M2 Alpha | `Alpha (66%)` | `Alpha (65%)` | `Alpha (66%)` | none (0/5) | 5 | complete on 2026-05-30 |
| [Google Chat](/taxonomy#google-chat) | Channel | M2 Alpha | `Alpha (57%)` | `Alpha (53%)` | `Alpha (57%)` | none (0/5) | 5 | complete on 2026-05-30 |
| [Matrix](/taxonomy#matrix) | Channel | M2 Alpha | `Beta (72%)` | `Alpha (68%)` | `Beta (72%)` | none (0/6) | 6 | complete on 2026-05-30 |
| [Microsoft Teams](/taxonomy#microsoft-teams) | Channel | M2 Alpha | `Alpha (62%)` | `Alpha (63%)` | `Alpha (62%)` | none (0/5) | 5 | complete on 2026-05-30 |
| [Mattermost, LINE, IRC, Nextcloud Talk, Nostr, Twitch, Tlon, Synology Chat](/taxonomy#mattermost-line-irc-nextcloud-talk-nostr-twitch-tlon-synology-chat) | Channel | M2 Alpha | `Alpha (62%)` | `Alpha (58%)` | `Alpha (62%)` | none (0/4) | 4 | complete on 2026-05-30 |
| [Feishu, QQ Bot, WeChat, Yuanbao, Zalo, Zalo Personal, regional channels](/taxonomy#feishu-qq-bot-wechat-yuanbao-zalo-zalo-personal-regional-channels) | Channel | M2 Alpha | `Experimental (43%)` | `Experimental (47%)` | `Experimental (43%)` | none (0/4) | 4 | complete on 2026-05-30 |
| [Voice Call channel](/taxonomy#voice-call-channel) | Channel | M1 Experimental | `Experimental (49%)` | `Alpha (58%)` | `Experimental (49%)` | none (0/5) | 5 | complete on 2026-05-30 |
| [OpenAI / Codex provider path](/taxonomy#openai-codex-provider-path) | Provider and tool | M3 Beta | `Beta (78%)` | `Beta (70%)` | `Beta (78%)` | partial (3/5) | 5 | complete on 2026-05-30 |
| [Anthropic provider path](/taxonomy#anthropic-provider-path) | Provider and tool | M3 Beta | `Stable (80%)` | `Beta (74%)` | `Stable (80%)` | none (0/5) | 5 | complete on 2026-05-30 |
| [Google provider path](/taxonomy#google-provider-path) | Provider and tool | M3 Beta | `Beta (73%)` | `Alpha (68%)` | `Beta (73%)` | none (0/5) | 5 | complete on 2026-05-30 |
| [OpenRouter provider path](/taxonomy#openrouter-provider-path) | Provider and tool | M3 Beta | `Beta (75%)` | `Alpha (66%)` | `Beta (75%)` | none (0/4) | 4 | complete on 2026-05-30 |
| [Local model providers: Ollama, vLLM, SGLang, LM Studio](/taxonomy#local-model-providers-ollama-vllm-sglang-lm-studio) | Provider and tool | M2 Alpha | `Beta (77%)` | `Beta (74%)` | `Beta (77%)` | none (0/5) | 5 | complete on 2026-05-30 |
| [Long-tail hosted providers](/taxonomy#long-tail-hosted-providers) | Provider and tool | M2 Alpha | `Alpha (64%)` | `Alpha (60%)` | `Alpha (64%)` | none (0/3) | 3 | complete on 2026-05-30 |
| [Web search tools](/taxonomy#web-search-tools) | Provider and tool | M3 Beta | `Beta (79%)` | `Beta (76%)` | `Beta (79%)` | none (0/4) | 4 | complete on 2026-05-30 |
| [Browser automation and exec/sandbox tools](/taxonomy#browser-automation-and-exec-sandbox-tools) | Provider and tool | M3 Beta | `Beta (79%)` | `Beta (75%)` | `Beta (79%)` | partial (2/3) | 3 | complete on 2026-05-30 |
| [Image/video/music generation tools](/taxonomy#image-video-music-generation-tools) | Provider and tool | M2 Alpha | `Beta (77%)` | `Alpha (66%)` | `Beta (77%)` | none (0/5) | 5 | complete on 2026-05-30 |
## QA evidence artifacts
No `qa-evidence.json` artifact directory was provided for this render.
Use the `Maturity scorecard` workflow with a source run id, or run `pnpm maturity:render -- --evidence-dir <downloaded-artifacts> --output-dir <output-dir>` locally to produce an evidence-enriched docs artifact.

View File

@@ -1803,8 +1803,8 @@ surfaces:
supported: false
reason: none
human_override: false
- id: openclaw-app-sdk
name: OpenClaw App SDK
- id: gateway-external-apps
name: Gateway integrations for external apps
family: core
level:
id: alpha
@@ -1831,7 +1831,7 @@ surfaces:
source_ref: openclaw@29dd7847fd
process_version: 3
categories:
- name: Client API
- name: Gateway API
coverage:
score: 86
label: Stable

View File

@@ -214,59 +214,6 @@ permission boundary. Dangerous plugin node commands still require explicit
After a node changes its declared command list, reject the old device pairing
and approve the new request so the gateway stores the updated command snapshot.
## Config (`openclaw.json`)
Node-related settings live under `gateway.nodes` and `tools.exec`:
```json5
{
gateway: {
nodes: {
// Auto-approve first-time node pairing from trusted networks (CIDR list).
// Disabled when unset. Only applies to first-time role:node requests
// with no requested scopes; does not auto-approve upgrades.
pairing: {
autoApproveCidrs: ["192.168.1.0/24"],
},
// Opt into dangerous/privacy-heavy node commands (camera.snap, etc.).
allowCommands: ["camera.snap", "screen.record"],
// Block exact command names even if defaults or allowCommands include them.
denyCommands: ["camera.clip"],
},
},
tools: {
exec: {
// Default exec host: "node" routes all exec calls to a paired node.
host: "node",
// Security mode for node exec: allow only approved/allowlisted commands.
security: "allowlist",
// Pin exec to a specific node (id or name). Omit to allow any node.
node: "build-node",
},
},
}
```
Use exact node command names. `denyCommands` removes a command even when a
platform default or `allowCommands` entry would otherwise allow it. See
[Gateway configuration reference](/gateway/configuration-reference#gateway-field-details)
for gateway node pairing and command-policy field details.
Per-agent exec node override:
```json5
{
agents: {
list: [
{
id: "main",
tools: { exec: { node: "build-node" } },
},
],
},
}
```
## Screenshots (canvas snapshots)
If the node is showing the Canvas (WebView), `canvas.snapshot` returns `{ format, base64 }`.

View File

@@ -197,30 +197,22 @@ only for behavior that really belongs to the backend.
`CliBackendPlugin` can also define:
| Hook | Use |
| ---------------------------------- | --------------------------------------------------------------------------- |
| `normalizeConfig(config, context)` | Rewrite legacy user config after merge |
| `resolveExecutionArgs(ctx)` | Add request-scoped flags such as thinking effort or side-question isolation |
| `prepareExecution(ctx)` | Create temporary auth or config bridges before launch |
| `transformSystemPrompt(ctx)` | Apply a final CLI-specific system prompt transform |
| `textTransforms` | Bidirectional prompt/output replacements |
| `defaultAuthProfileId` | Prefer a specific OpenClaw auth profile |
| `authEpochMode` | Decide how auth changes invalidate stored CLI sessions |
| `nativeToolMode` | Declare whether the CLI has always-on native tools |
| `sideQuestionToolMode` | Declare disabled native tools for `/btw` side questions |
| `bundleMcp` / `bundleMcpMode` | Opt into OpenClaw's loopback MCP tool bridge |
| `ownsNativeCompaction` | Backend owns its own compaction - OpenClaw defers |
| Hook | Use |
| ---------------------------------- | ------------------------------------------------------ |
| `normalizeConfig(config, context)` | Rewrite legacy user config after merge |
| `resolveExecutionArgs(ctx)` | Add request-scoped flags such as thinking effort |
| `prepareExecution(ctx)` | Create temporary auth or config bridges before launch |
| `transformSystemPrompt(ctx)` | Apply a final CLI-specific system prompt transform |
| `textTransforms` | Bidirectional prompt/output replacements |
| `defaultAuthProfileId` | Prefer a specific OpenClaw auth profile |
| `authEpochMode` | Decide how auth changes invalidate stored CLI sessions |
| `nativeToolMode` | Declare whether the CLI has always-on native tools |
| `bundleMcp` / `bundleMcpMode` | Opt into OpenClaw's loopback MCP tool bridge |
| `ownsNativeCompaction` | Backend owns its own compaction - OpenClaw defers |
Keep these hooks provider-owned. Do not add CLI-specific branches to core when a
backend hook can express the behavior.
`ctx.executionMode` is `"agent"` for normal turns and `"side-question"` for
ephemeral `/btw` calls. Use it when the CLI needs different one-shot flags, such
as disabling native tools, session persistence, or resume behavior for BTW. If a
backend normally has `nativeToolMode: "always-on"` but its side-question argv
reliably disables those tools, also set `sideQuestionToolMode: "disabled"`;
otherwise OpenClaw fails closed when BTW requires a no-tools CLI run.
### `ownsNativeCompaction`: opting out of OpenClaw compaction
If your backend runs an agent that compacts its **own** transcript, set

View File

@@ -313,13 +313,9 @@ available timeout in this order:
- For `image_generate` without a configured timeout, the 120 second
image-generation default.
- For the media-understanding `image` tool, `tools.media.image.timeoutSeconds`
converted to milliseconds, or the 60 second media default. For image
understanding, this applies to the request itself and is not reduced by
earlier preparation work.
converted to milliseconds, or the 60 second media default.
- The 90 second dynamic-tool default.
This watchdog is the outer dynamic `item/tool/call` budget. Provider-specific
request timeouts run inside that call and keep their own timeout semantics.
Dynamic tool budgets are capped at 600000 ms. On timeout, OpenClaw aborts the
tool signal where supported and returns a failed dynamic-tool response to Codex
so the turn can continue instead of leaving the session in `processing`.

View File

@@ -557,14 +557,10 @@ or shortens that specific tool budget. The `image_generate` tool uses
`agents.defaults.imageGenerationModel.timeoutMs` when the tool call does not
provide its own timeout, or a 120 second image-generation default otherwise.
The media-understanding `image` tool uses
`tools.media.image.timeoutSeconds` or its 60 second media default. For image
understanding, that timeout applies to the request itself and is not
reduced by earlier preparation work. Dynamic tool budgets are
capped at 600000 ms. On timeout, OpenClaw aborts the tool signal
`tools.media.image.timeoutSeconds` or its 60 second media default. Dynamic tool
budgets are capped at 600000 ms. On timeout, OpenClaw aborts the tool signal
where supported and returns a failed dynamic-tool response to Codex so the turn
can continue instead of leaving the session in `processing`.
This watchdog is the outer dynamic `item/tool/call` budget; provider-specific
request timeouts run inside that call and keep their own timeout semantics.
After Codex accepts a turn, and after OpenClaw responds to a turn-scoped
app-server request, the harness expects Codex to make current-turn progress and

View File

@@ -152,8 +152,7 @@ observation-only.
- `gateway_start` / `gateway_stop` - start or stop plugin-owned services with the Gateway
- `deactivate` - deprecated compatibility alias for `gateway_stop`; use `gateway_stop` in new plugins
- `cron_changed` - observe gateway-owned cron lifecycle changes (added, updated, removed, started, finished, scheduled)
- **`before_install`** - inspect staged skill or plugin install material from a loaded
plugin runtime
- **`before_install`** - inspect skill or plugin install context and optionally block
## Debug runtime hooks
@@ -463,19 +462,11 @@ Decision rules:
## Install hooks
Use `security.installPolicy` for operator-owned allow/block decisions. That
policy runs from OpenClaw config, covers CLI install and update paths, and fails
closed when enabled but unavailable.
`before_install` is a plugin-runtime lifecycle hook. It runs after
`security.installPolicy` only in the OpenClaw process where plugin hooks have
already been loaded, such as Gateway-backed install flows. It is useful for
plugin-owned observations, warnings, and compatibility checks, but it is not the
primary enterprise or host security boundary for installs. The `builtinScan`
field remains in the event payload for compatibility, but OpenClaw no longer
runs built-in install-time dangerous-code blocking, so it is an empty `ok`
result. Return additional findings or `{ block: true, blockReason }` to stop the
install in that process.
`before_install` runs after the operator-owned `security.installPolicy` check
when one is configured. The `builtinScan` field remains in the event payload for
compatibility, but OpenClaw no longer runs built-in install-time dangerous-code
blocking, so it is an empty `ok` result. Return additional findings or
`{ block: true, blockReason }` to stop the install.
`block: true` is terminal. `block: false` is treated as no decision.
Handler failures block the install fail-closed.

View File

@@ -378,10 +378,7 @@ AI CLI backend such as `claude-cli` or `my-cli`.
(for example normalizing old flag shapes).
- Use `resolveExecutionArgs` for request-scoped argv rewrites that belong to
the CLI dialect, such as mapping OpenClaw thinking levels to a native effort
flag. The hook receives `ctx.executionMode`; use `"side-question"` to add
backend-native isolation flags for ephemeral `/btw` calls. If those flags
reliably disable native tools for an otherwise always-on CLI, declare
`sideQuestionToolMode: "disabled"` too.
flag.
For an end-to-end authoring guide, see
[CLI backend plugins](/plugins/cli-backend-plugins).
@@ -431,10 +428,6 @@ semantics.
### Hook decision semantics
`before_install` is a plugin-runtime lifecycle hook, not the operator install
policy surface. Use `security.installPolicy` when an allow/block decision must
cover CLI and Gateway-backed install or update paths.
- `before_tool_call`: returning `{ block: true }` is terminal. Once any handler sets it, lower-priority handlers are skipped.
- `before_tool_call`: returning `{ block: false }` is treated as no decision (same as omitting `block`), not as an override.
- `before_install`: returning `{ block: true }` is terminal. Once any handler sets it, lower-priority handlers are skipped.

View File

@@ -19,7 +19,7 @@ OpenClaw uses the `zai` provider with a Z.AI API key.
## GLM models
GLM is a model family, not a separate provider. In OpenClaw, GLM models use
refs such as `zai/glm-5.2`: provider `zai`, model id `glm-5.2`.
refs such as `zai/glm-5.1`: provider `zai`, model id `glm-5.1`.
## Getting started
@@ -85,12 +85,12 @@ you want to force a specific Coding Plan or general API surface.
models: {
providers: {
zai: {
// GLM-5.2 uses the Coding Plan endpoint.
baseUrl: "https://api.z.ai/api/coding/paas/v4",
// Example value. Onboarding writes the matching baseUrl for your endpoint.
baseUrl: "https://api.z.ai/api/paas/v4",
},
},
},
agents: { defaults: { model: { primary: "zai/glm-5.2" } } },
agents: { defaults: { model: { primary: "zai/glm-5.1" } } },
}
```
@@ -105,31 +105,28 @@ openclaw models list --all --provider zai
The manifest-backed catalog currently includes:
| Model ref | Notes |
| -------------------- | ------------------------------- |
| `zai/glm-5.2` | Coding Plan default; 1M context |
| `zai/glm-5.1` | General API default |
| `zai/glm-5` | |
| `zai/glm-5-turbo` | |
| `zai/glm-5v-turbo` | |
| `zai/glm-4.7` | |
| `zai/glm-4.7-flash` | |
| `zai/glm-4.7-flashx` | |
| `zai/glm-4.6` | |
| `zai/glm-4.6v` | |
| `zai/glm-4.5` | |
| `zai/glm-4.5-air` | |
| `zai/glm-4.5-flash` | |
| `zai/glm-4.5v` | |
| Model ref | Notes |
| -------------------- | ------------- |
| `zai/glm-5.1` | Default model |
| `zai/glm-5` | |
| `zai/glm-5-turbo` | |
| `zai/glm-5v-turbo` | |
| `zai/glm-4.7` | |
| `zai/glm-4.7-flash` | |
| `zai/glm-4.7-flashx` | |
| `zai/glm-4.6` | |
| `zai/glm-4.6v` | |
| `zai/glm-4.5` | |
| `zai/glm-4.5-air` | |
| `zai/glm-4.5-flash` | |
| `zai/glm-4.5v` | |
<Tip>
GLM models are available as `zai/<model>` (example: `zai/glm-5`).
</Tip>
<Note>
Coding Plan setup defaults to `zai/glm-5.2`; general API setup keeps
`zai/glm-5.1`. Endpoint auto-detection falls back to `glm-5.1` or `glm-4.7`
when the selected plan does not expose GLM-5.2. GLM versions and availability
The default bundled model ref is `zai/glm-5.1`. GLM versions and availability
can change; run `openclaw models list --all --provider zai` to see the catalog
known to your installed version.
</Note>
@@ -176,7 +173,7 @@ known to your installed version.
agents: {
defaults: {
models: {
"zai/glm-5.2": {
"zai/glm-5.1": {
params: { preserveThinking: true },
},
},

View File

@@ -99,14 +99,10 @@ the maintainer-only release runbook.
file, lane, workflow job, package profile, provider, or model allowlist that
proves the fix. Rerun the full umbrella only when the changed surface makes
prior evidence stale.
9. For a tagged beta candidate, run
`pnpm release:candidate -- --tag vYYYY.M.PATCH-beta.N` from the matching
`release/YYYY.M.PATCH` branch. For stable, pass the required Windows source
release too:
`pnpm release:candidate -- --tag vYYYY.M.PATCH --windows-node-tag vX.Y.Z`.
The helper runs the local generated-release checks, dispatches or verifies
the full release validation and npm preflight evidence, runs Parallels
fresh/update proof against the exact prepared tarball plus Telegram package
9. For beta, tag `vYYYY.M.PATCH-beta.N`, then run `pnpm release:candidate -- --tag
vYYYY.M.PATCH-beta.N` from the matching `release/YYYY.M.PATCH` branch. The helper runs
the local generated-release checks, dispatches or verifies the full release
validation and npm preflight evidence, runs Parallels and Telegram package
proof, records plugin npm and ClawHub plans, and prints the exact
`OpenClaw Release Publish` command only after the evidence bundle is green.
`OpenClaw Release Publish` dispatches the selected or all-publishable plugin
@@ -146,12 +142,9 @@ the maintainer-only release runbook.
direct push, it opens or updates an appcast PR. Stable Windows Hub
readiness requires the signed `OpenClawCompanion-Setup-x64.exe`,
`OpenClawCompanion-Setup-arm64.exe`, and
`OpenClawCompanion-SHA256SUMS.txt` assets on the OpenClaw GitHub release.
Pass the exact signed `openclaw/openclaw-windows-node` release tag as
`windows_node_tag` and its candidate-approved installer digest map as
`windows_node_installer_digests`; `OpenClaw Release Publish` keeps the
release draft, dispatches `Windows Node Release`, and verifies all three
assets before publication.
`OpenClawCompanion-SHA256SUMS.txt` assets on the OpenClaw GitHub release;
promote them with the `Windows Node Release` workflow after the matching
`openclaw/openclaw-windows-node` release has passed its signing workflow.
11. After publish, run the npm post-publish verifier, optional standalone
published-npm Telegram E2E when you need post-publish channel proof,
dist-tag promotion when needed, verify the generated GitHub release page,
@@ -260,36 +253,21 @@ the maintainer-only release runbook.
to the GitHub release as `openclaw-<version>-dependency-evidence.zip`.
- Run `OpenClaw Release Publish` for the mutating publish sequence after the
tag exists. Dispatch it from `release/YYYY.M.PATCH` (or `main` when publishing a
main-reachable tag), pass the release tag, successful OpenClaw npm
`preflight_run_id`, and successful `full_release_validation_run_id`, and keep
the default plugin publish scope `all-publishable` unless you are deliberately
running a focused repair. The workflow serializes plugin npm publish, plugin
ClawHub publish, and OpenClaw npm publish so the core package is not published
before its externalized plugins.
- Stable `OpenClaw Release Publish` requires an exact `windows_node_tag` after
the matching non-prerelease `openclaw/openclaw-windows-node` release exists.
It also requires the candidate-approved `windows_node_installer_digests` map.
Before dispatching any publish child, it verifies that source release is
published, non-prerelease, contains the required x64/ARM64 installers, and
still matches that approved map. It then dispatches `Windows Node Release`
while the OpenClaw release is still a draft, carrying the pinned installer
digest map unchanged. The child
workflow downloads the signed Windows Hub installers from that exact tag,
matches them against the pinned digests, verifies their Authenticode
signatures use the expected OpenClaw Foundation signer on a Windows runner,
writes a SHA-256 manifest, and uploads the installers plus manifest onto the
canonical OpenClaw GitHub release, then re-downloads the promoted assets and
verifies the manifest membership and hashes. The parent verifies the current
x64, ARM64, and checksum asset contract before publication. Direct recovery
rejects unexpected `OpenClawCompanion-*` asset names before replacing the
expected contract assets with the pinned source bytes. Manually dispatch
`Windows Node Release` only for recovery, and always pass an exact tag, never
`latest`, plus the explicit `expected_installer_digests` JSON map from the
approved source release. Website download links should target exact OpenClaw
release asset URLs for the current stable release, or
`releases/latest/download/...` only after verifying GitHub's latest redirect
points at that same release; do not link only to the companion repo release
page.
main-reachable tag), pass the release tag and successful OpenClaw npm
`preflight_run_id`, and keep the default plugin publish scope
`all-publishable` unless you are deliberately running a focused repair. The
workflow serializes plugin npm publish, plugin ClawHub publish, and OpenClaw
npm publish so the core package is not published before its externalized
plugins.
- Run the manual `Windows Node Release` workflow for stable releases after the
matching `openclaw/openclaw-windows-node` release exists. It downloads the
signed Windows Hub installers from the companion repo, verifies their
Authenticode signatures on a Windows runner, writes a SHA-256 manifest, and
uploads the installers plus manifest onto the canonical OpenClaw GitHub
release. Website download links should target exact OpenClaw release asset
URLs for the current stable release, or `releases/latest/download/...` only
after verifying GitHub's latest redirect points at that same release; do not
link only to the companion repo release page.
- Release checks now run in a separate manual workflow:
`OpenClaw Release Checks`
- `OpenClaw Release Checks` also runs the QA Lab mock parity lane plus the fast
@@ -719,12 +697,7 @@ orchestrates the trusted-publisher workflows in the order the release needs:
`ref=<release-sha>`.
5. Dispatch `Plugin ClawHub Release` with the same scope and SHA.
6. Dispatch `OpenClaw NPM Release` with the release tag, npm dist-tag, and
saved `preflight_run_id` after verifying the saved
`full_release_validation_run_id`.
7. For stable releases, create or update the GitHub release as a draft, dispatch
`Windows Node Release` with the explicit `windows_node_tag` and
candidate-approved `windows_node_installer_digests`, and verify the canonical
installer/checksum assets before publishing the draft.
saved `preflight_run_id`.
Beta publish example:
@@ -733,7 +706,6 @@ gh workflow run openclaw-release-publish.yml \
--ref release/YYYY.M.PATCH \
-f tag=vYYYY.M.PATCH-beta.N \
-f preflight_run_id=<successful-openclaw-npm-preflight-run-id> \
-f full_release_validation_run_id=<successful-full-release-validation-run-id> \
-f npm_dist_tag=beta
```
@@ -743,10 +715,7 @@ Stable publish to the default beta dist-tag:
gh workflow run openclaw-release-publish.yml \
--ref release/YYYY.M.PATCH \
-f tag=vYYYY.M.PATCH \
-f windows_node_tag=vX.Y.Z \
-f windows_node_installer_digests='{"OpenClawCompanion-Setup-x64.exe":"sha256:<approved-x64-sha256>","OpenClawCompanion-Setup-arm64.exe":"sha256:<approved-arm64-sha256>"}' \
-f preflight_run_id=<successful-openclaw-npm-preflight-run-id> \
-f full_release_validation_run_id=<successful-full-release-validation-run-id> \
-f npm_dist_tag=beta
```
@@ -756,10 +725,7 @@ Stable promotion directly to `latest` is explicit:
gh workflow run openclaw-release-publish.yml \
--ref release/YYYY.M.PATCH \
-f tag=vYYYY.M.PATCH \
-f windows_node_tag=vX.Y.Z \
-f windows_node_installer_digests='{"OpenClawCompanion-Setup-x64.exe":"sha256:<approved-x64-sha256>","OpenClawCompanion-Setup-arm64.exe":"sha256:<approved-arm64-sha256>"}' \
-f preflight_run_id=<successful-openclaw-npm-preflight-run-id> \
-f full_release_validation_run_id=<successful-full-release-validation-run-id> \
-f npm_dist_tag=latest
```
@@ -789,13 +755,6 @@ package cannot ship without every publishable official plugin, including
- `tag`: required release tag; must already exist
- `preflight_run_id`: successful `OpenClaw NPM Release` preflight run id;
required when `publish_openclaw_npm=true`
- `full_release_validation_run_id`: successful `Full Release Validation` run
id; required when `publish_openclaw_npm=true`
- `windows_node_tag`: exact non-prerelease `openclaw/openclaw-windows-node`
release tag; required for stable OpenClaw publish
- `windows_node_installer_digests`: candidate-approved compact JSON map of the
current Windows installer names to their pinned `sha256:` digests; required
for stable OpenClaw publish
- `npm_dist_tag`: npm target tag for the OpenClaw package
- `plugin_publish_scope`: defaults to `all-publishable`; use `selected` only
for focused plugin-only repair work with `publish_openclaw_npm=false`
@@ -841,21 +800,14 @@ When cutting a stable npm release:
Matrix, and Telegram coverage from one manual workflow
4. If you intentionally only need the deterministic normal test graph, run the
manual `CI` workflow on the release ref instead
5. Select the exact non-prerelease `openclaw/openclaw-windows-node` release tag
whose signed x64 and ARM64 installers should ship. Save it as
`windows_node_tag`, and save their validated digest map as
`windows_node_installer_digests`. The release-candidate helper records both
and includes them in its generated publish command.
6. Save the successful `preflight_run_id` and `full_release_validation_run_id`
7. Run `OpenClaw Release Publish` with the same `tag`, the same `npm_dist_tag`,
the selected `windows_node_tag`, its saved `windows_node_installer_digests`,
the saved `preflight_run_id`, and the saved `full_release_validation_run_id`;
it publishes externalized plugins to npm and ClawHub before promoting the
OpenClaw npm package
8. If the release landed on `beta`, use the
5. Save the successful `preflight_run_id`
6. Run `OpenClaw Release Publish` with the same `tag`, the same `npm_dist_tag`,
and the saved `preflight_run_id`; it publishes externalized plugins to npm
and ClawHub before promoting the OpenClaw npm package
7. If the release landed on `beta`, use the
`openclaw/releases/.github/workflows/openclaw-npm-dist-tags.yml`
workflow to promote that stable version from `beta` to `latest`
9. If the release intentionally published directly to `latest` and `beta`
8. If the release intentionally published directly to `latest` and `beta`
should follow the same stable build immediately, use that same release
workflow to point both dist-tags at the stable version, or let its scheduled
self-healing sync move `beta` later

View File

@@ -1,49 +0,0 @@
---
summary: "How OpenClaw maps the maturity scorecard to QA coverage and evidence."
read_when:
- Reading QA scorecard coverage
- Adding coverage IDs to QA scenarios
- Finding evidence for a maturity category
title: "Maturity tests"
---
Maturity tests are QA evidence linked to the OpenClaw maturity scorecard. They help maintainers see which scorecard categories already have runnable proof and which ones still need coverage.
The scorecard has two source files:
- `taxonomy.yaml` defines surfaces, categories, maturity levels, profile membership, and feature coverage IDs.
- `docs/maturity-scores.yaml` records the current score snapshot and LTS status.
QA scenarios connect to the scorecard by using the same coverage IDs:
- `qa/scenarios/**/*.md` stores `coverage.primary` and `coverage.secondary` IDs.
- `extensions/qa-lab` joins scenario coverage to the taxonomy report.
- `qa suite` writes `qa-evidence.json` for the scenarios it runs.
## Find Coverage
Start with the coverage inventory when a requirement needs a runnable mapping:
```bash
pnpm openclaw qa coverage --match <surface-or-coverage-id>
pnpm openclaw qa coverage --json --match <surface-or-coverage-id>
```
The report includes a **Scorecard Taxonomy** section, profile membership, mapped coverage IDs, evidence refs, and matching `qa suite --scenario ...` commands.
## Add Coverage
When a category needs new evidence:
1. Start from the matching `taxonomy.yaml` surface and category.
2. Reuse an existing feature `coverageIds` value, or add a broad behavior-shaped ID.
3. Add that ID to `coverage.primary` in the scenario that proves it.
4. Use `coverage.secondary` only for supporting evidence.
5. Add useful `docsRefs` and `codeRefs` to the scenario.
6. Run `pnpm openclaw qa coverage --match <coverage-id>` and then run the smallest relevant scenario or test lane.
## Related docs
- [QA overview](/concepts/qa-e2e-automation)
- [Testing](/help/testing)
- [Matrix QA](/concepts/qa-matrix)

File diff suppressed because it is too large Load Diff

View File

@@ -1,866 +0,0 @@
---
title: "Maturity taxonomy"
summary: "Generated taxonomy reference for OpenClaw maturity scorecard surfaces, categories, features, docs, and QA coverage IDs."
---
# Maturity taxonomy
> This file is generated from `taxonomy.yaml` and `docs/maturity-scores.yaml`. Run `pnpm maturity:render` after editing scorecard sources.
> Committed docs intentionally exclude the old maintainer inventory tree; per-run QA evidence stays in GitHub Actions artifacts.
## Maturity levels
| Level | Label | Meaning | Promotion bar |
| ----- | ------------ | ------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `M0` | Planned | Direction is known, but no supported user path exists. | Design issue, owner, and target surface exist. |
| `M1` | Experimental | Implemented behind caveats, flags, source builds, or maintainer-only flows. | Maintainer can run the scenario from current main. |
| `M2` | Alpha | Real users can try it, but breaking changes and incomplete UX are expected. | Documented setup, basic tests, known caveats, and at least one real-environment proof. |
| `M3` | Beta | Public path exists and the main workflow is usable with bounded caveats. | Install/update docs, regression tests, support runbook, and successful scenario proof across the expected environment. |
| `M4` | Stable | Recommended path for normal users. Failures are treated as regressions. | Release gate, doctor/troubleshooting path, broad docs, and repeated real-world proof. |
| `M5` | Lovable | Polished, delightful, well-instrumented, and competitive with the best comparable workflow. | Stable plus user scorecard pass across representative users. |
## Surface index
| Surface | Family | Level | Categories | Coverage | Quality | Completeness |
| ------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------- | --------------- | ---------- | -------------------- | -------------------- | -------------------- |
| [Gateway runtime](#gateway-runtime) | Core | M4 Stable | 13 | `Stable (81%)` | `Alpha (69%)` | `Stable (80%)` |
| [CLI](#cli-install-update-onboard-doctor) | Core | M4 Stable | 7 | `Stable (83%)` | `Beta (72%)` | `Stable (80%)` |
| [Plugins](#plugin-sdk-and-bundled-plugin-architecture) | Core | M3 Beta | 9 | `Stable (82%)` | `Stable (80%)` | `Stable (81%)` |
| [Agent Runtime](#agent-runtime-and-provider-execution) | Core | M3 Beta | 9 | `Stable (80%)` | `Alpha (69%)` | `Stable (80%)` |
| [Session, memory, and context engine](#session-memory-and-context-engine) | Core | M3 Beta | 9 | `Beta (74%)` | `Alpha (66%)` | `Beta (74%)` |
| [Channel framework](#channel-framework) | Core | M3 Beta | 8 | `Beta (77%)` | `Beta (74%)` | `Beta (77%)` |
| [Security, auth, pairing, and secrets](#security-auth-pairing-and-secrets) | Core | M3 Beta | 6 | `Stable (80%)` | `Alpha (67%)` | `Stable (80%)` |
| [Observability](#telemetry-diagnostics-and-observability) | Core | M3 Beta | 5 | `Stable (80%)` | `Beta (78%)` | `Stable (80%)` |
| [Automation: cron, hooks, tasks, polling](#automation-cron-hooks-tasks-polling) | Core | M3 Beta | 6 | `Beta (76%)` | `Alpha (69%)` | `Beta (76%)` |
| [Media understanding and media generation](#media-understanding-and-media-generation) | Core | M2 Alpha | 6 | `Beta (78%)` | `Beta (70%)` | `Beta (78%)` |
| [Voice and realtime talk](#voice-and-realtime-talk) | Core | M2 Alpha | 6 | `Beta (73%)` | `Alpha (67%)` | `Beta (73%)` |
| [Gateway Web App](#browser-control-ui-and-webchat) | Core | M3 Beta | 6 | `Beta (79%)` | `Beta (71%)` | `Beta (79%)` |
| [TUI](#tui-and-terminal-ux) | Core | M2 Alpha | 5 | `Beta (76%)` | `Beta (71%)` | `Beta (76%)` |
| [ClawHub](#clawhub-and-external-plugin-distribution) | Core | M2 Alpha | 4 | `Beta (72%)` | `Beta (73%)` | `Beta (72%)` |
| [OpenClaw App SDK](#openclaw-app-sdk) | Core | M2 Alpha | 6 | `Beta (75%)` | `Beta (75%)` | `Alpha (69%)` |
| [macOS Gateway host](#macos-gateway-host) | Platform | M4 Stable | 7 | `Beta (75%)` | `Beta (79%)` | `Beta (75%)` |
| [macOS companion app](#macos-companion-app) | Platform | M3 Beta | 8 | `Beta (71%)` | `Alpha (66%)` | `Beta (71%)` |
| [Linux Gateway host](#linux-gateway-host) | Platform | M4 Stable | 5 | `Stable (80%)` | `Beta (76%)` | `Stable (80%)` |
| [Linux companion app](#linux-companion-app) | Platform | M0 Planned | 5 | `Experimental (5%)` | `Experimental (27%)` | `Experimental (5%)` |
| [Windows via WSL2](#windows-via-wsl2) | Platform | M3 Beta | 6 | `Beta (72%)` | `Alpha (69%)` | `Beta (72%)` |
| [Native Windows](#native-windows-cli-and-gateway) | Platform | M2 Alpha | 4 | `Alpha (68%)` | `Alpha (63%)` | `Alpha (68%)` |
| [Native Windows companion app](#native-windows-companion-app) | Platform | M0 Planned | 5 | `Experimental (5%)` | `Experimental (30%)` | `Experimental (5%)` |
| [Android app](#android-app) | Platform | M2 Alpha | 7 | `Alpha (65%)` | `Alpha (62%)` | `Alpha (65%)` |
| [iOS app](#ios-app) | Platform | M1 Experimental | 8 | `Experimental (41%)` | `Experimental (45%)` | `Experimental (41%)` |
| [watchOS companion surfaces](#watchos-companion-surfaces) | Platform | M1 Experimental | 5 | `Experimental (45%)` | `Alpha (57%)` | `Experimental (45%)` |
| [Raspberry Pi / small Linux devices](#raspberry-pi-small-linux-devices) | Platform | M3 Beta | 4 | `Beta (70%)` | `Alpha (67%)` | `Beta (70%)` |
| [Docker / Podman hosting](#docker-podman-hosting) | Platform | M3 Beta | 4 | `Beta (77%)` | `Beta (73%)` | `Beta (77%)` |
| [Kubernetes hosting](#kubernetes-hosting) | Platform | M2 Alpha | 4 | `Alpha (50%)` | `Beta (75%)` | `Beta (74%)` |
| [Nix install path](#nix-install-path) | Platform | M1 Experimental | 5 | `Experimental (38%)` | `Experimental (45%)` | `Experimental (38%)` |
| [Discord](#discord) | Channel | M4 Stable | 6 | `Beta (71%)` | `Beta (71%)` | `Beta (71%)` |
| [Telegram](#telegram) | Channel | M3 Beta | 5 | `Beta (75%)` | `Beta (70%)` | `Beta (75%)` |
| [WhatsApp](#whatsapp) | Channel | M3 Beta | 5 | `Beta (76%)` | `Beta (76%)` | `Beta (76%)` |
| [Slack](#slack) | Channel | M3 Beta | 5 | `Beta (70%)` | `Alpha (68%)` | `Beta (70%)` |
| [iMessage / BlueBubbles](#imessage-bluebubbles) | Channel | M3 Beta | 5 | `Beta (71%)` | `Beta (72%)` | `Beta (71%)` |
| [Signal](#signal) | Channel | M2 Alpha | 5 | `Alpha (66%)` | `Alpha (65%)` | `Alpha (66%)` |
| [Google Chat](#google-chat) | Channel | M2 Alpha | 5 | `Alpha (57%)` | `Alpha (53%)` | `Alpha (57%)` |
| [Matrix](#matrix) | Channel | M2 Alpha | 6 | `Beta (72%)` | `Alpha (68%)` | `Beta (72%)` |
| [Microsoft Teams](#microsoft-teams) | Channel | M2 Alpha | 5 | `Alpha (62%)` | `Alpha (63%)` | `Alpha (62%)` |
| [Mattermost, LINE, IRC, Nextcloud Talk, Nostr, Twitch, Tlon, Synology Chat](#mattermost-line-irc-nextcloud-talk-nostr-twitch-tlon-synology-chat) | Channel | M2 Alpha | 4 | `Alpha (62%)` | `Alpha (58%)` | `Alpha (62%)` |
| [Feishu, QQ Bot, WeChat, Yuanbao, Zalo, Zalo Personal, regional channels](#feishu-qq-bot-wechat-yuanbao-zalo-zalo-personal-regional-channels) | Channel | M2 Alpha | 4 | `Experimental (43%)` | `Experimental (47%)` | `Experimental (43%)` |
| [Voice Call channel](#voice-call-channel) | Channel | M1 Experimental | 5 | `Experimental (49%)` | `Alpha (58%)` | `Experimental (49%)` |
| [OpenAI / Codex provider path](#openai-codex-provider-path) | Provider and tool | M3 Beta | 5 | `Beta (78%)` | `Beta (70%)` | `Beta (78%)` |
| [Anthropic provider path](#anthropic-provider-path) | Provider and tool | M3 Beta | 5 | `Stable (80%)` | `Beta (74%)` | `Stable (80%)` |
| [Google provider path](#google-provider-path) | Provider and tool | M3 Beta | 5 | `Beta (73%)` | `Alpha (68%)` | `Beta (73%)` |
| [OpenRouter provider path](#openrouter-provider-path) | Provider and tool | M3 Beta | 4 | `Beta (75%)` | `Alpha (66%)` | `Beta (75%)` |
| [Local model providers: Ollama, vLLM, SGLang, LM Studio](#local-model-providers-ollama-vllm-sglang-lm-studio) | Provider and tool | M2 Alpha | 5 | `Beta (77%)` | `Beta (74%)` | `Beta (77%)` |
| [Long-tail hosted providers](#long-tail-hosted-providers) | Provider and tool | M2 Alpha | 3 | `Alpha (64%)` | `Alpha (60%)` | `Alpha (64%)` |
| [Web search tools](#web-search-tools) | Provider and tool | M3 Beta | 4 | `Beta (79%)` | `Beta (76%)` | `Beta (79%)` |
| [Browser automation and exec/sandbox tools](#browser-automation-and-exec-sandbox-tools) | Provider and tool | M3 Beta | 3 | `Beta (79%)` | `Beta (75%)` | `Beta (79%)` |
| [Image/video/music generation tools](#image-video-music-generation-tools) | Provider and tool | M2 Alpha | 5 | `Beta (77%)` | `Alpha (66%)` | `Beta (77%)` |
## Surface taxonomy
### Core
#### Gateway runtime
- Surface id: `gateway-runtime`
- Level: M4 Stable
- Rationale: Core architecture, auth, pairing, protocol docs, daemon docs, and CLI runbooks are broad and current.
- Completeness instructions: `references/completeness/gateway-runtime.md`
| Category | Category ID | Features | Coverage IDs | Docs | Profiles | Coverage | Quality | Completeness | LTS |
| ------------------------------- | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | -------------- | -------------- | -------------- | --- |
| Approvals and Remote Execution | `gateway-runtime.approvals-and-remote-execution` | Exec approvals<br>Plugin approvals<br>Node exec approvals<br>Approved node execution<br>Approval mutation safety<br>Delivery fallback behavior | exec-approvals<br>plugin-approvals<br>node-exec-approvals<br>approved-node-execution<br>approval-mutation-safety<br>delivery-fallback-behavior | `docs/gateway/protocol.md`<br>`docs/gateway/security/index.md` | `release` | `Stable (88%)` | `Beta (72%)` | `Beta (78%)` | Yes |
| HTTP APIs | `gateway-runtime.http-apis` | OpenAI-compatible APIs<br>Tool invocation API<br>Admin API access<br>Hook ingress | openai-compatible-apis<br>tool-invocation-api<br>admin-api-access<br>hook-ingress | `docs/gateway/index.md`<br>`docs/gateway/openai-http-api.md`<br>`docs/gateway/openresponses-http-api.md`<br>`docs/gateway/tools-invoke-http-api.md`<br>`docs/automation/hooks.md`<br>`docs/web/index.md` | `release` | `Stable (88%)` | `Beta (74%)` | `Beta (72%)` | Yes |
| Hosted Web Surface | `gateway-runtime.hosted-web-surface` | Control UI<br>WebChat hosting<br>Plugin web routes<br>Canvas and A2UI routes | control-ui<br>webchat-hosting<br>plugin-web-routes<br>canvas-and-a2ui-routes | `docs/gateway/index.md`<br>`docs/concepts/architecture.md`<br>`docs/web/control-ui.md`<br>`docs/web/webchat.md`<br>`docs/refactor/canvas.md` | `release` | `Stable (88%)` | `Beta (74%)` | `Beta (72%)` | Yes |
| Gateway RPC APIs and Events | `gateway-runtime.gateway-rpc-apis-and-events` | Health APIs<br>Identity and presence APIs<br>Model APIs<br>Usage and memory APIs<br>Session APIs<br>Chat APIs<br>Channel APIs<br>Web login and wake APIs<br>Config and secrets APIs<br>Update and setup APIs<br>Agent and artifact APIs<br>Task and automation APIs<br>Tool and skill APIs<br>Request and event envelopes<br>Idempotent side effects<br>Method discovery<br>Event discovery<br>Accepted-then-final results<br>Event ordering<br>State refresh after gaps | health-apis<br>identity-and-presence-apis<br>model-apis<br>usage-and-memory-apis<br>agents.subagents<br>gateway.sessions-list<br>tools.session-status<br>chat-apis<br>channel-apis<br>web-login-and-wake-apis<br>config-and-secrets-apis<br>update-and-setup-apis<br>agent-and-artifact-apis<br>task-and-automation-apis<br>tool-and-skill-apis<br>request-and-event-envelopes<br>idempotent-side-effects<br>method-discovery<br>event-discovery<br>accepted-then-final-results<br>event-ordering<br>state-refresh-after-gaps | `docs/gateway/protocol.md`<br>`docs/gateway/index.md`<br>`docs/concepts/architecture.md` | `release` | `Alpha (68%)` | `Alpha (57%)` | `Stable (88%)` | Yes |
| Device Auth and Pairing | `gateway-runtime.device-auth-and-pairing` | Shared-secret login<br>Trusted proxy auth<br>Private ingress mode<br>Device challenge signing<br>Device tokens<br>Setup-code bootstrap<br>Auth mismatch recovery<br>Device auth migration<br>Client pairing<br>Node pairing | shared-secret-login<br>trusted-proxy-auth<br>private-ingress-mode<br>device-challenge-signing<br>device-tokens<br>setup-code-bootstrap<br>auth-mismatch-recovery<br>device-auth-migration<br>client-pairing<br>node-pairing | `docs/gateway/protocol.md`<br>`docs/gateway/pairing.md`<br>`docs/gateway/security/index.md` | `release` | `Stable (88%)` | `Beta (72%)` | `Stable (82%)` | Yes |
| Network Access and Discovery | `gateway-runtime.network-access-and-discovery` | Loopback and LAN access<br>Tailnet access<br>SSH tunnels<br>Endpoint discovery<br>Saved endpoints<br>TLS pinning | loopback-and-lan-access<br>tailnet-access<br>ssh-tunnels<br>endpoint-discovery<br>saved-endpoints<br>tls-pinning | `docs/gateway/index.md`<br>`docs/gateway/discovery.md`<br>`docs/gateway/protocol.md` | `release` | `Alpha (68%)` | `Alpha (62%)` | `Beta (74%)` | Yes |
| Nodes and Remote Capabilities | `gateway-runtime.nodes-and-remote-capabilities` | Node presence<br>Node capabilities<br>Node inventory<br>Node actions<br>Node events<br>Pending work delivery<br>Remote device capabilities<br>Remote host commands | node-presence<br>node-capabilities<br>node-inventory<br>node-actions<br>node-events<br>pending-work-delivery<br>remote-device-capabilities<br>remote-host-commands | `docs/gateway/protocol.md`<br>`docs/concepts/architecture.md`<br>`docs/nodes/index.md` | `release` | `Stable (84%)` | `Alpha (63%)` | `Beta (76%)` | No |
| Health, Diagnostics, and Repair | `gateway-runtime.health-diagnostics-and-repair` | Health snapshots<br>Channel readiness<br>Stability diagnostics<br>Payload diagnostics<br>Diagnostics exports<br>Doctor checks<br>Log tailing | health-snapshots<br>channel-readiness<br>stability-diagnostics<br>payload-diagnostics<br>diagnostics-exports<br>doctor-checks<br>log-tailing | `docs/gateway/index.md`<br>`docs/gateway/diagnostics.md`<br>`docs/gateway/doctor.md` | `release` | `Alpha (68%)` | `Alpha (62%)` | `Beta (78%)` | Yes |
| Protocol Compatibility | `gateway-runtime.protocol-compatibility` | Published protocol schema<br>Runtime request validation<br>JSON Schema export<br>Swift client models<br>Version negotiation<br>Client transport defaults<br>Backward-compatible evolution | published-protocol-schema<br>runtime-request-validation<br>json-schema-export<br>swift-client-models<br>version-negotiation<br>client-transport-defaults<br>backward-compatible-evolution | `docs/gateway/protocol.md`<br>`docs/concepts/architecture.md`<br>`docs/concepts/typebox.md`<br>`docs/gateway/bridge-protocol.md` | `release` | `Beta (72%)` | `Beta (70%)` | `Stable (84%)` | Yes |
| Roles and Permissions | `gateway-runtime.roles-and-permissions` | Role negotiation<br>Operator permissions<br>Approval-gated actions<br>Untrusted node declarations<br>Event scoping | role-negotiation<br>operator-permissions<br>approval-gated-actions<br>untrusted-node-declarations<br>event-scoping | `docs/gateway/protocol.md`<br>`docs/gateway/security/index.md` | `release` | `Stable (85%)` | `Alpha (62%)` | `Stable (80%)` | Yes |
| Gateway Lifecycle | `gateway-runtime.gateway-lifecycle` | Foreground startup<br>Service installation<br>Restart and stop<br>Service status<br>Bind and port settings<br>Config reload<br>Multi-gateway isolation | foreground-startup<br>service-installation<br>config.restart-apply<br>plugins.capabilities<br>runtime.gateway-restart<br>service-status<br>bind-and-port-settings<br>config.hot-apply<br>plugins.hot-reload<br>plugins.lifecycle<br>plugins.skills<br>multi-gateway-isolation | `docs/gateway/index.md`<br>`docs/concepts/architecture.md` | `release` | `Stable (86%)` | `Stable (82%)` | `Stable (88%)` | Yes |
| Security Controls | `gateway-runtime.security-controls` | Non-loopback auth<br>Trusted proxy exceptions<br>Gateway and node trust boundaries<br>Trusted CIDR auto-approval<br>Fail-closed protocol handling<br>Remote execution safeguards | non-loopback-auth<br>trusted-proxy-exceptions<br>gateway-and-node-trust-boundaries<br>trusted-cidr-auto-approval<br>fail-closed-protocol-handling<br>remote-execution-safeguards | `docs/gateway/security/index.md`<br>`docs/gateway/protocol.md`<br>`docs/gateway/discovery.md` | `release` | `Stable (84%)` | `Beta (74%)` | `Stable (80%)` | Yes |
| WebSocket Connection | `gateway-runtime.websocket-connection` | WebSocket transport<br>Connect challenge<br>Connect request<br>Protocol version negotiation<br>hello-ok snapshot<br>Startup retry<br>Session limits<br>Plugin surface URLs | websocket-transport<br>connect-challenge<br>connect-request<br>protocol-version-negotiation<br>hello-ok-snapshot<br>startup-retry<br>session-limits<br>plugin-surface-urls | `docs/gateway/protocol.md`<br>`docs/concepts/architecture.md` | `release` | `Stable (84%)` | `Beta (76%)` | `Stable (82%)` | Yes |
#### CLI
- Surface id: `cli-install-update-onboard-doctor`
- Level: M4 Stable
- Rationale: Normal setup and repair paths are documented across install, CLI, and gateway docs. Platform-specific Windows paths are tracked in the Windows via WSL2 and Native Windows rows.
- Completeness instructions: `references/completeness/cli-install-update-onboard-doctor.md`
| Category | Category ID | Features | Coverage IDs | Docs | Profiles | Coverage | Quality | Completeness | LTS |
| -------------------------- | -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------ | --------- | -------------- | ------------- | -------------- | --- |
| CLI Setup | `cli-install-update-onboard-doctor.cli-setup` | Installer scripts<br>Local prefix install<br>Package-manager installs<br>Supported Node runtime<br>Source checkout install<br>CLI entrypoint | installer-scripts<br>local-prefix-install<br>package-manager-installs<br>supported-node-runtime<br>source-checkout-install<br>cli-entrypoint | `docs/install/index.md`<br>`docs/install/installer.md`<br>`docs/install/node.md`<br>`docs/install/updating.md` | `release` | `Beta (78%)` | `Beta (75%)` | `Stable (84%)` | Yes |
| Onboarding and Auth Setup | `cli-install-update-onboard-doctor.onboarding-and-auth-setup` | Guided onboarding<br>Targeted reconfiguration<br>Auth choices<br>Gateway auth storage<br>Remote onboarding | guided-onboarding<br>targeted-reconfiguration<br>auth-choices<br>gateway-auth-storage<br>remote-onboarding | `docs/cli/onboard.md`<br>`docs/cli/configure.md`<br>`docs/start/onboarding-overview.md` | `release` | `Stable (86%)` | `Beta (78%)` | `Stable (80%)` | Yes |
| Plugin and Channel Setup | `cli-install-update-onboard-doctor.plugin-and-channel-setup` | Channel picker<br>Plugin install sources<br>Channel account setup<br>Post-setup probes<br>Remote gateway caveat | channel-picker<br>plugin-install-sources<br>channel-account-setup<br>post-setup-probes<br>remote-gateway-caveat | `docs/cli/onboard.md`<br>`docs/cli/plugins.md`<br>`docs/cli/channels.md` | `release` | `Stable (82%)` | `Beta (72%)` | `Beta (76%)` | No |
| Gateway Service Management | `cli-install-update-onboard-doctor.gateway-service-management` | Foreground gateway runs<br>Service install and control<br>Service auth wiring<br>Drift and reinstall recovery<br>Service health checks | foreground-gateway-runs<br>service-install-and-control<br>agents.create<br>channels.discord-config<br>config.crestodian-setup<br>drift-and-reinstall-recovery<br>service-health-checks | `docs/cli/gateway.md`<br>`docs/install/updating.md`<br>`docs/gateway/troubleshooting.md` | `release` | `Stable (88%)` | `Alpha (66%)` | `Stable (84%)` | Yes |
| CLI Observability | `cli-install-update-onboard-doctor.cli-observability` | Status snapshots<br>Health snapshots<br>Remote log tailing<br>Diagnostics export<br>Support-safe redaction | status-snapshots<br>health-snapshots<br>remote-log-tailing<br>diagnostics-export<br>support-safe-redaction | `docs/cli/status.md`<br>`docs/cli/health.md`<br>`docs/cli/logs.md`<br>`docs/gateway/diagnostics.md` | `release` | `Stable (84%)` | `Beta (74%)` | `Stable (84%)` | Yes |
| Doctor | `cli-install-update-onboard-doctor.doctor` | Interactive repair<br>Config migration<br>Auth and SecretRef checks<br>Plugin validation and repair<br>Lint and JSON findings<br>Extra gateway discovery<br>Supervisor drift repair<br>Port and startup diagnosis<br>Runtime path checks<br>Restart guidance | interactive-repair<br>config-migration<br>auth-and-secretref-checks<br>plugin-validation-and-repair<br>lint-and-json-findings<br>extra-gateway-discovery<br>supervisor-drift-repair<br>port-and-startup-diagnosis<br>runtime-path-checks<br>restart-guidance | `docs/cli/doctor.md`<br>`docs/gateway/doctor.md`<br>`docs/gateway/secrets.md`<br>`docs/gateway/troubleshooting.md` | `release` | `Stable (80%)` | `Alpha (68%)` | `Beta (77%)` | Yes |
| Updates and Upgrades | `cli-install-update-onboard-doctor.updates-and-upgrades` | Update channels<br>Install-kind switching<br>Managed gateway restart<br>Update status and RPC<br>Plugin convergence | update-channels<br>install-kind-switching<br>managed-gateway-restart<br>update-status-and-rpc<br>plugin-convergence | `docs/install/updating.md`<br>`docs/cli/update.md`<br>`docs/gateway/troubleshooting.md` | `release` | `Stable (82%)` | `Alpha (68%)` | `Beta (78%)` | Yes |
#### Plugins
- Surface id: `plugin-sdk-and-bundled-plugin-architecture`
- Level: M3 Beta
- Rationale: Broad docs and strong internal runtime evidence exist across manifests, discovery, loading, provider/tool architecture, and approval boundaries. Keep the row at beta until public SDK API/subpaths and external distribution proof are stronger.
- Completeness instructions: `references/completeness/plugin-sdk-and-bundled-plugin-architecture.md`
| Category | Category ID | Features | Coverage IDs | Docs | Profiles | Coverage | Quality | Completeness | LTS |
| ------------------------------- | ---------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- | -------------- | -------------- | -------------- | --- |
| Authoring and Packaging plugins | `plugin-sdk-and-bundled-plugin-architecture.authoring-and-packaging-plugins` | Root SDK entrypoint<br>Focused SDK imports<br>Entrypoint discovery<br>Migration shims<br>Plugin manifest<br>Package metadata<br>Runtime compatibility<br>Validation feedback | root-sdk-entrypoint<br>focused-sdk-imports<br>entrypoint-discovery<br>migration-shims<br>plugin-manifest<br>package-metadata<br>runtime-compatibility<br>validation-feedback | `docs/plugins/building-plugins.md`<br>`docs/plugins/sdk-overview.md`<br>`docs/plugins/sdk-entrypoints.md`<br>`docs/plugins/sdk-subpaths.md`<br>`docs/plugins/manifest.md`<br>`docs/plugins/reference.md` | `release` | `Beta (77%)` | `Beta (74%)` | `Beta (72%)` | Yes |
| Bundled plugins | `plugin-sdk-and-bundled-plugin-architecture.bundled-plugins` | Bundled plugin listing<br>Bundled source overlays<br>Packaged bundled plugins<br>Generated plugin inventory<br>Bundled channel IDs | bundled-plugin-listing<br>bundled-source-overlays<br>packaged-bundled-plugins<br>generated-plugin-inventory<br>bundled-channel-ids | `docs/plugins/plugin-inventory.md`<br>`docs/cli/plugins.md`<br>`docs/plugins/architecture-internals.md` | `release` | `Stable (86%)` | `Stable (84%)` | `Stable (88%)` | Yes |
| Canvas plugin | `plugin-sdk-and-bundled-plugin-architecture.canvas-plugin` | Hosted Canvas and A2UI surfaces<br>Agent canvas tool<br>Node Canvas commands<br>Control UI embeds<br>Canvas documents<br>A2UI transport and snapshots | hosted-canvas-and-a2ui-surfaces<br>agent-canvas-tool<br>node-canvas-commands<br>control-ui-embeds<br>canvas-documents<br>a2ui-transport-and-snapshots | `docs/plugins/reference/canvas.md`<br>`docs/refactor/canvas.md`<br>`docs/gateway/configuration-reference.md` | `release` | `Beta (76%)` | `Alpha (66%)` | `Beta (74%)` | No |
| Installing and running plugins | `plugin-sdk-and-bundled-plugin-architecture.installing-and-running-plugins` | Plugin setup<br>Runtime activation<br>Enable and disable<br>Safe load failures<br>Dependency repair<br>Install update and uninstall | plugin-setup<br>config.hot-apply<br>gateway.performance<br>models.live-openai<br>plugins.before-prompt-build<br>plugins.before-tool-call<br>plugins.hot-reload<br>plugins.kitchen-sink<br>plugins.lifecycle<br>plugins.plugin-tools<br>plugins.runtime<br>plugins.skills<br>runtime.gateway-log-sentinel.plugin-hooks<br>config.hot-apply<br>plugins.hot-reload<br>plugins.lifecycle<br>plugins.contracts.tools<br>runtime.gateway-log-sentinel.plugin-contracts<br>dependency-repair<br>plugins.hot-install<br>plugins.skills<br>runtime.gateway-restart<br>runtime.package-update<br>runtime.update-run | `docs/plugins/architecture.md`<br>`docs/plugins/architecture-internals.md`<br>`docs/cli/plugins.md` | `smoke-ci`<br>`release` | `Stable (86%)` | `Stable (84%)` | `Stable (88%)` | Yes |
| Channel plugins | `plugin-sdk-and-bundled-plugin-architecture.channel-plugins` | Inbound event handling<br>Outbound delivery<br>Ingress authorization<br>Destination resolution<br>Native approval prompts | inbound-event-handling<br>outbound-delivery<br>ingress-authorization<br>destination-resolution<br>native-approval-prompts | `docs/plugins/sdk-channel-plugins.md`<br>`docs/plugins/sdk-channel-inbound.md`<br>`docs/plugins/sdk-channel-outbound.md` | `release` | `Stable (82%)` | `Beta (78%)` | `Stable (80%)` | Yes |
| Provider and tool plugins | `plugin-sdk-and-bundled-plugin-architecture.provider-and-tool-plugins` | Provider plugins<br>Tool plugins<br>Model catalogs<br>Provider auth<br>Web search and fetch<br>Mixed plugins | provider-plugins<br>gateway.performance<br>models.live-openai<br>plugins.before-prompt-build<br>plugins.before-tool-call<br>plugins.kitchen-sink<br>plugins.lifecycle<br>plugins.mcp-tools<br>plugins.plugin-tools<br>runtime.gateway-log-sentinel.plugin-hooks<br>tools.invocation<br>model-catalogs<br>provider-auth<br>web-search-and-fetch<br>config.hot-apply<br>config.restart-apply<br>plugins.capabilities<br>plugins.hot-install<br>plugins.runtime<br>plugins.skills<br>tools.invocation<br>tools.skill-invocation | `docs/plugins/sdk-provider-plugins.md`<br>`docs/plugins/tool-plugins.md`<br>`docs/plugins/adding-capabilities.md` | `release` | `Stable (84%)` | `Stable (82%)` | `Stable (84%)` | Yes |
| Plugin approvals | `plugin-sdk-and-bundled-plugin-architecture.plugin-approvals` | Approval requests<br>Native approval delivery<br>Same-chat fallbacks<br>Exec and plugin separation<br>Approval replay protection<br>Security helpers | approval-requests<br>native-approval-delivery<br>same-chat-fallbacks<br>exec-and-plugin-separation<br>approval-replay-protection<br>security-helpers | `docs/plugins/plugin-permission-requests.md`<br>`docs/tools/exec-approvals.md`<br>`docs/plugins/sdk-channel-plugins.md` | `release` | `Stable (84%)` | `Stable (86%)` | `Stable (86%)` | Yes |
| Publishing plugins | `plugin-sdk-and-bundled-plugin-architecture.publishing-plugins` | Install sources<br>ClawHub publishing<br>npm publishing<br>Compatibility signaling<br>Update and rollback expectations<br>Third-party publication rules | install-sources<br>clawhub-publishing<br>npm-publishing<br>compatibility-signaling<br>update-and-rollback-expectations<br>third-party-publication-rules | `docs/cli/plugins.md`<br>`docs/plugins/compatibility.md`<br>`docs/clawhub/publishing.md` | `release` | `Beta (79%)` | `Stable (82%)` | `Beta (74%)` | Yes |
| Testing plugins | `plugin-sdk-and-bundled-plugin-architecture.testing-plugins` | Test fixtures<br>Local test environment<br>Plugin runtime harness<br>Unit and integration scaffolds<br>Docker lifecycle suites<br>Smoke tests | test-fixtures<br>local-test-environment<br>plugins.contracts.tools<br>runtime.gateway-log-sentinel.plugin-contracts<br>unit-and-integration-scaffolds<br>docker-lifecycle-suites<br>gateway.performance<br>models.live-openai<br>plugins.kitchen-sink<br>plugins.lifecycle<br>plugins.plugin-tools | `docs/plugins/sdk-testing.md`<br>`docs/plugins/sdk-setup.md`<br>`docs/plugins/codex-harness.md` | `release` | `Stable (84%)` | `Stable (81%)` | `Stable (82%)` | No |
#### Agent Runtime
- Surface id: `agent-runtime-and-provider-execution`
- Level: M3 Beta
- Rationale: Main loop, models, provider routing, and tool streaming are first-class, but provider behavior shifts weekly and needs scenario proof per release.
- Completeness instructions: `references/completeness/agent-runtime-and-provider-execution.md`
| Category | Category ID | Features | Coverage IDs | Docs | Profiles | Coverage | Quality | Completeness | LTS |
| -------------------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- | -------------- | ------------- | -------------- | --- |
| Agent Turn Execution | `agent-runtime-and-provider-execution.agent-turn-execution` | Turn startup and runtime choice<br>Session and run coordination<br>Abort and terminal outcomes | agents.create<br>agents.instructions<br>channels.discord-config<br>config.crestodian-setup<br>runtime.first-action<br>runtime.first-hour-20<br>runtime.long-context<br>agents.subagents<br>channels.dedup<br>channels.dm<br>channels.qa-channel<br>channels.reconnect<br>channels.streaming<br>channels.threads<br>commitments.heartbeat-target-none<br>commitments.scope<br>personal.channel-replies<br>runtime.codex-plugin.lifecycle<br>runtime.delivery<br>runtime.fallback-delivery<br>runtime.gateway-restart<br>runtime.restart-recovery<br>runtime.turn-ordering<br>channels.streaming<br>runtime.delivery<br>runtime.fallback-delivery<br>runtime.long-context<br>runtime.soak-100 | `docs/concepts/agent-loop.md`<br>`docs/cli/agent.md`<br>`docs/concepts/agent-runtimes.md` | `smoke-ci`<br>`release` | `Stable (82%)` | `Beta (74%)` | `Stable (82%)` | Yes |
| External Runtimes and Subagents | `agent-runtime-and-provider-execution.external-runtimes-and-subagents` | External harness selection<br>CLI runtime aliases<br>Subagent turns<br>Runtime recovery | agents.openclaw-harness<br>workspace.planning<br>cli-runtime-aliases<br>agents.subagents<br>agents.synthesis<br>channels.qa-channel<br>gateway.sessions-list<br>runtime.delivery<br>tools.sessions-spawn<br>runtime-recovery | `docs/concepts/agent-runtimes.md`<br>`docs/providers/anthropic.md`<br>`docs/providers/google.md`<br>`docs/tools/subagents.md` | `release` | `Beta (78%)` | `Alpha (66%)` | `Beta (78%)` | No |
| Hosted Provider Execution | `agent-runtime-and-provider-execution.hosted-provider-execution` | Hosted provider turns<br>Provider-specific model options<br>Hosted tool use<br>Reasoning and cache controls<br>Hosted streaming and replies | hosted-provider-turns<br>provider-specific-model-options<br>hosted-tool-use<br>reasoning-and-cache-controls<br>hosted-streaming-and-replies | `docs/providers/openai.md`<br>`docs/providers/anthropic.md`<br>`docs/providers/google.md`<br>`docs/concepts/models.md` | `release` | `Beta (76%)` | `Beta (70%)` | `Beta (76%)` | Yes |
| Local and Self-hosted Providers | `agent-runtime-and-provider-execution.local-and-self-hosted-providers` | Local provider profiles<br>Tool-capability flags<br>Timeouts and context windows<br>Local smoke checks<br>Local failure handling | local-provider-profiles<br>tool-capability-flags<br>timeouts-and-context-windows<br>local-smoke-checks<br>local-failure-handling | `docs/providers/ollama.md`<br>`docs/concepts/models.md`<br>`docs/cli/agent.md` | `release` | `Beta (70%)` | `Alpha (60%)` | `Beta (70%)` | No |
| Model and Runtime Selection | `agent-runtime-and-provider-execution.model-and-runtime-selection` | Model reference selection<br>Provider and runtime overrides<br>Thinking and context settings<br>Invalid route recovery | models.claude-cli<br>models.provider-capabilities<br>models.switching<br>models.thinking<br>runtime.session-continuity<br>runtime.tool-continuity<br>models.switching<br>models.thinking<br>runtime.reasoning-visibility<br>runtime.session-continuity<br>invalid-route-recovery | `docs/concepts/models.md`<br>`docs/cli/models.md`<br>`docs/providers/openai.md`<br>`docs/concepts/agent-runtimes.md` | `release` | `Stable (84%)` | `Beta (72%)` | `Stable (84%)` | Yes |
| Provider Auth | `agent-runtime-and-provider-execution.provider-auth` | Login and API-key setup<br>Auth profile selection<br>Credential health checks<br>Auth failover<br>Provider fallback recovery<br>Rate-limit and capacity recovery<br>Missing-key and OAuth guidance<br>Restart and stale-route recovery<br>Structured provider diagnostics<br>Subagent credential propagation | models.anthropic<br>models.provider-auth<br>auth-profiles.provider-selection<br>runtime.codex-plugin.auth<br>gateway.performance<br>models.live-openai<br>plugins.kitchen-sink<br>plugins.lifecycle<br>plugins.plugin-tools<br>auth-failover<br>memory.failure-handling<br>runtime.fallbacks<br>rate-limit-and-capacity-recovery<br>missing-key-and-oauth-guidance<br>restart-and-stale-route-recovery<br>structured-provider-diagnostics<br>subagent-credential-propagation | `docs/concepts/models.md`<br>`docs/cli/agent.md`<br>`docs/cli/models.md`<br>`docs/providers/openai.md`<br>`docs/providers/anthropic.md`<br>`docs/providers/google.md`<br>`docs/tools/subagents.md` | `release` | `Stable (80%)` | `Alpha (66%)` | `Stable (80%)` | Yes |
| Streaming and Progress | `agent-runtime-and-provider-execution.streaming-and-progress` | Streaming replies<br>Progress visibility | channels.streaming<br>runtime.delivery<br>runtime.fallback-delivery<br>models.thinking<br>personal.failure-recovery<br>personal.no-fake-progress<br>personal.task-followthrough<br>runtime.reasoning-visibility<br>tools.evidence | `docs/concepts/streaming.md`<br>`docs/concepts/agent-loop.md` | `release` | `Stable (84%)` | `Beta (70%)` | `Stable (84%)` | No |
| Tool Calls and Response Handling | `agent-runtime-and-provider-execution.tool-calls-and-response-handling` | Tool-call handling<br>Usage and response reporting<br>Failure recovery | models.switching<br>personal.no-fake-progress<br>personal.task-followthrough<br>personal.tool-safety<br>runtime.approvals<br>runtime.codex-native-workspace.read<br>runtime.prompt-compatibility<br>runtime.tool-continuity<br>tools.apply-patch<br>tools.edit<br>tools.evidence<br>tools.followthrough<br>tools.fs.list<br>tools.fs.read<br>tools.fs.write<br>tools.grep<br>workspace.artifacts<br>agents.subagents<br>agents.synthesis<br>personal.failure-recovery<br>personal.no-fake-progress<br>runtime.empty-response-recovery<br>runtime.reasoning-only-recovery<br>runtime.retry-policy<br>tools.evidence | `docs/concepts/agent-loop.md`<br>`docs/providers/ollama.md` | `release` | `Stable (80%)` | `Alpha (66%)` | `Stable (80%)` | Yes |
| Tool Execution Controls | `agent-runtime-and-provider-execution.tool-execution-controls` | Tool availability rules<br>Sandboxed exec behavior<br>Approval flow<br>Elevated execution<br>Tool safety controls<br>Delegated tool access | qa.artifact-safety<br>runtime.inventory<br>runtime.tool-policy<br>security.redaction<br>sandboxed-exec-behavior<br>personal.approval-denial<br>personal.tool-safety<br>runtime.approvals<br>tools.followthrough<br>tools.safety<br>elevated-execution<br>personal.approval-denial<br>personal.tool-safety<br>runtime.approvals<br>tools.followthrough<br>tools.safety<br>delegated-tool-access | `docs/gateway/sandbox-vs-tool-policy-vs-elevated.md`<br>`docs/concepts/agent-loop.md`<br>`docs/tools/subagents.md` | `release` | `Stable (86%)` | `Beta (74%)` | `Stable (86%)` | Yes |
#### Session, memory, and context engine
- Surface id: `session-memory-and-context-engine`
- Level: M3 Beta
- Rationale: Strong docs and active implementation. Maturity depends on transcript durability, compaction quality, and cross-client parity.
- Completeness instructions: `references/completeness/session-memory-and-context-engine.md`
| Category | Category ID | Features | Coverage IDs | Docs | Profiles | Coverage | Quality | Completeness | LTS |
| --------------------------------------- | --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | ----------------------- | -------------- | -------------- | -------------- | --- |
| CLI Session and Transcript Management | `session-memory-and-context-engine.cli-session-and-transcript-management` | CLI Session<br>Transcript Management | cli-session<br>transcript-management | `docs/concepts/session.md`<br>`docs/reference/session-management-compaction.md`<br>`docs/cli/sessions.md` | `release` | `Beta (74%)` | `Alpha (68%)` | `Beta (74%)` | Yes |
| Token Management | `session-memory-and-context-engine.token-management` | Compaction<br>Pruning<br>Token Pressure | runtime.compaction<br>runtime.empty-response-recovery<br>runtime.reasoning-only-recovery<br>runtime.retry-policy<br>pruning<br>runtime.codex-app-server<br>runtime.first-hour-20<br>runtime.gateway-log-sentinel.codex-progress<br>runtime.long-context<br>runtime.soak-100 | `docs/concepts/compaction.md`<br>`docs/concepts/context.md`<br>`docs/reference/session-management-compaction.md` | `smoke-ci`<br>`release` | `Beta (78%)` | `Alpha (60%)` | `Beta (78%)` | Yes |
| Context Engine | `session-memory-and-context-engine.context-engine` | Context Engine<br>Runtime Assembly | docs.discovery<br>workspace.artifacts<br>workspace.long-running-task<br>workspace.repo-discovery<br>agents.openclaw-harness<br>models.codex-cli<br>workspace.planning | `docs/concepts/context.md`<br>`docs/concepts/context-engine.md`<br>`docs/plan/codex-context-engine-harness.md` | `release` | `Beta (72%)` | `Stable (80%)` | `Beta (72%)` | Yes |
| Cross-client History and Session Parity | `session-memory-and-context-engine.cross-client-history-and-session-parity` | Cross-client History<br>Session Parity | channels.threads<br>memory.thread-isolation<br>models.switching<br>models.thinking<br>runtime.session-continuity | `docs/web/webchat.md`<br>`docs/platforms/android.md`<br>`docs/channels/channel-routing.md` | `release` | `Beta (76%)` | `Alpha (62%)` | `Beta (76%)` | No |
| Diagnostics, Maintenance, and Recovery | `session-memory-and-context-engine.diagnostics-maintenance-and-recovery` | Session diagnostic reports<br>Session maintenance warnings<br>Session and transcript recovery | session-diagnostic-reports<br>session-maintenance-warnings<br>config.restart-apply<br>memory.failure-handling<br>runtime.delivery<br>runtime.fallbacks<br>runtime.gateway-restart<br>runtime.package-update<br>runtime.restart-recovery<br>runtime.update-run | `docs/gateway/diagnostics.md`<br>`docs/reference/session-management-compaction.md`<br>`docs/diagnostics/flags.md` | `smoke-ci`<br>`release` | `Beta (72%)` | `Alpha (68%)` | `Beta (72%)` | No |
| Core Prompts and Context | `session-memory-and-context-engine.core-prompts-and-context` | Instruction Profile<br>Context Visibility | agents.instructions<br>character.persona<br>runtime.first-action<br>workspace.artifacts<br>docs.discovery<br>models.codex-cli<br>runtime.no-meta-leak<br>workspace.repo-discovery | `docs/concepts/context.md`<br>`docs/reference/transcript-hygiene.md`<br>`docs/channels/discord.md` | `release` | `Alpha (68%)` | `Beta (70%)` | `Alpha (68%)` | Yes |
| Memory | `session-memory-and-context-engine.memory` | Memory Backend Storage<br>Embedding Search<br>Memory Files<br>Memory search and store tools<br>Active Memory | memory-backend-storage<br>channels.qa-channel<br>memory.active-recall<br>memory.ranking<br>memory.recall<br>personal.memory-recall<br>memory.dreaming<br>memory.promotion<br>qa.artifact-safety<br>channels.group-messages<br>channels.qa-channel<br>memory.active-recall<br>memory.ranking<br>memory.recall<br>memory.tools<br>personal.memory-recall<br>tools.memory.add<br>tools.memory.recall<br>channels.qa-channel<br>memory.active-recall<br>memory.recall<br>personal.memory-recall | `docs/reference/memory-config.md`<br>`docs/concepts/memory-qmd.md`<br>`docs/concepts/memory.md`<br>`docs/channels/discord.md` | `smoke-ci`<br>`release` | `Alpha (66%)` | `Alpha (58%)` | `Alpha (66%)` | No |
| Session Routing | `session-memory-and-context-engine.session-routing` | Session Routing<br>Conversation routing | session-routing<br>channels.webchat<br>runtime.direct-reply-routing<br>tools.message | `docs/concepts/session.md`<br>`docs/channels/channel-routing.md`<br>`docs/channels/discord.md` | `release` | `Stable (82%)` | `Beta (74%)` | `Stable (82%)` | Yes |
| Transcript Persistence | `session-memory-and-context-engine.transcript-persistence` | Transcript Persistence<br>Durability | transcript-persistence<br>durability | `docs/reference/session-management-compaction.md`<br>`docs/reference/transcript-hygiene.md` | `release` | `Beta (78%)` | `Alpha (58%)` | `Beta (78%)` | Yes |
#### Channel framework
- Surface id: `channel-framework`
- Level: M3 Beta
- Rationale: Many channels share Gateway delivery and routing contracts, but channel behavior varies by upstream API and account-policy constraints.
- Completeness instructions: `references/completeness/channel-framework.md`
| Category | Category ID | Features | Coverage IDs | Docs | Profiles | Coverage | Quality | Completeness | LTS |
| --------------------------------------- | ----------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- | -------------- | ------------- | -------------- | --- |
| Channel Actions Commands and Approvals | `channel-framework.channel-actions-commands-and-approvals` | Channel-native commands<br>Native command session target<br>Message actions<br>Message tool API discovery<br>Channel-native approval prompts | channel-native-commands<br>native-command-session-target<br>message-actions<br>message-tool-api-discovery<br>channel-native-approval-prompts | `docs/channels/groups.md`<br>`docs/channels/discord.md`<br>`docs/channels/googlechat.md`<br>`docs/channels/signal.md`<br>`docs/channels/matrix.md` | `release` | `Alpha (68%)` | `Beta (72%)` | `Alpha (68%)` | No |
| Channel Setup | `channel-framework.channel-setup` | Supported channel catalog<br>Channel status taxonomy in channels list<br>Setup/onboarding flows<br>Install-on-demand<br>Setup wizard metadata | supported-channel-catalog<br>channel-status-taxonomy-in-channels-list<br>agents.create<br>channels.discord-config<br>config.crestodian-setup<br>install-on-demand<br>setup-wizard-metadata | `docs/channels/index.md`<br>`docs/channels/pairing.md`<br>`docs/channels/troubleshooting.md`<br>`docs/plugins/sdk-channel-plugins.md` | `release` | `Stable (84%)` | `Beta (78%)` | `Stable (84%)` | Yes |
| Group Thread and Ambient Room Behavior | `channel-framework.group-thread-and-ambient-room-behavior` | Group/channel session isolation<br>Mention-required<br>Native threads<br>Broadcast groups<br>Bot-loop protection | channels.group-messages<br>channels.qa-channel<br>memory.tools<br>channels.group-visible-replies<br>channels.qa-channel<br>tools.message<br>channels.dm<br>channels.qa-channel<br>channels.threads<br>memory.thread-isolation<br>personal.channel-replies<br>broadcast-groups<br>bot-loop-protection | `docs/channels/groups.md`<br>`docs/channels/group-messages.md`<br>`docs/channels/ambient-room-events.md`<br>`docs/channels/broadcast-groups.md`<br>`docs/channels/discord.md` | `smoke-ci`<br>`release` | `Beta (72%)` | `Alpha (68%)` | `Beta (72%)` | No |
| Inbound Access and Identity Gates | `channel-framework.inbound-access-and-identity-gates` | DM pairing<br>Group/channel allowlists<br>Access group expansion<br>Mention gating<br>Sanitized inbound identity/route projections | dm-pairing<br>group-channel-allowlists<br>access-group-expansion<br>mention-gating<br>sanitized-inbound-identity-route-projections | `docs/channels/access-groups.md`<br>`docs/channels/groups.md`<br>`docs/channels/discord.md`<br>`docs/channels/line.md` | `release` | `Stable (80%)` | `Beta (76%)` | `Stable (80%)` | Yes |
| Media Attachments and Rich Channel Data | `channel-framework.media-attachments-and-rich-channel-data` | Inbound media normalization<br>Outbound direct text/media sends<br>Provider-specific channelData<br>Media roots | inbound-media-normalization<br>outbound-direct-text-media-sends<br>provider-specific-channeldata<br>media-roots | `docs/channels/line.md`<br>`docs/channels/signal.md`<br>`docs/channels/googlechat.md`<br>`docs/channels/matrix.md`<br>`docs/channels/discord.md` | `release` | `Alpha (68%)` | `Beta (70%)` | `Alpha (68%)` | No |
| Outbound Delivery and Reply Pipeline | `channel-framework.outbound-delivery-and-reply-pipeline` | Automatic final reply delivery<br>Durable outbound send orchestration<br>Reply pipeline transforms<br>Provider outbound adapter bridge | agents.subagents<br>channels.dedup<br>channels.direct-visible-replies<br>channels.dm<br>channels.group-visible-replies<br>channels.qa-channel<br>channels.reconnect<br>channels.streaming<br>channels.threads<br>commitments.heartbeat-target-none<br>commitments.scope<br>personal.channel-replies<br>runtime.delivery<br>runtime.fallback-delivery<br>runtime.gateway-restart<br>runtime.restart-recovery<br>tools.message<br>channels.dedup<br>channels.reconnect<br>runtime.delivery<br>channels.message-actions<br>channels.qa-channel<br>channels.direct-visible-replies<br>channels.group-visible-replies<br>channels.qa-channel<br>channels.webchat<br>runtime.direct-reply-routing<br>tools.message<br>tools.message-tool | `docs/channels/groups.md`<br>`docs/channels/ambient-room-events.md`<br>`docs/channels/discord.md`<br>`docs/channels/matrix.md`<br>`docs/gateway/config-channels.md` | `smoke-ci`<br>`release` | `Stable (82%)` | `Beta (75%)` | `Stable (82%)` | Yes |
| Conversation Routing and Delivery | `channel-framework.conversation-routing-and-delivery` | Inbound conversation routing<br>Session key construction<br>Agent selection precedence<br>Runtime conversation routing<br>Thread/parent-child placement<br>Plugin registry resolution<br>Channel account startup<br>Whole-channel lifecycle controls<br>Config/secrets reload interactions<br>Auto-restart | channels.dm<br>channels.qa-channel<br>channels.threads<br>personal.channel-replies<br>session-key-construction<br>agent-selection-precedence<br>runtime-conversation-routing<br>thread-parent-child-placement<br>agents.subagents<br>channels.direct-visible-replies<br>channels.dm<br>channels.group-messages<br>channels.group-visible-replies<br>channels.message-actions<br>channels.qa-channel<br>channels.threads<br>media.image-generation<br>media.image-understanding<br>memory.recall<br>personal.channel-replies<br>personal.memory-recall<br>personal.reminders<br>runtime.delivery<br>scheduling.cron<br>scheduling.dedup<br>tools.message<br>ui.control<br>channel-account-startup<br>whole-channel-lifecycle-controls<br>config-secrets-reload-interactions<br>auto-restart | `docs/channels/channel-routing.md`<br>`docs/channels/groups.md`<br>`docs/channels/discord.md`<br>`docs/channels/matrix.md`<br>`docs/channels/troubleshooting.md`<br>`docs/gateway/configuration-reference.md` | `smoke-ci`<br>`release` | `Beta (77%)` | `Beta (71%)` | `Beta (77%)` | Yes |
| Status Health and Operator Controls | `channel-framework.status-health-and-operator-controls` | channels.status<br>Channel health policy<br>Operator CLI controls<br>Status read-model | channels-status<br>channels.dedup<br>channels.reconnect<br>runtime.delivery<br>operator-cli-controls<br>status-read-model | `docs/gateway/health.md`<br>`docs/gateway/configuration-reference.md`<br>`docs/channels/troubleshooting.md`<br>`docs/channels/discord.md` | `release` | `Stable (82%)` | `Beta (78%)` | `Stable (82%)` | Yes |
#### Security, auth, pairing, and secrets
- Surface id: `security-auth-pairing-and-secrets`
- Level: M3 Beta
- Rationale: Good docs and hardening surfaces exist. Promote after regular upgrade/security scenario runs prove no setup regressions.
- Completeness instructions: `references/completeness/security-auth-pairing-and-secrets.md`
| Category | Category ID | Features | Coverage IDs | Docs | Profiles | Coverage | Quality | Completeness | LTS |
| ----------------------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------- | -------------- | ------------- | -------------- | --- |
| Approval Policy and Tool Safeguards | `security-auth-pairing-and-secrets.approval-policy-and-tool-safeguards` | Approval Policy<br>Dangerous Tool Safeguards | personal.approval-denial<br>personal.tool-safety<br>runtime.approvals<br>tools.followthrough<br>tools.safety<br>dangerous-tool-safeguards | `docs/tools/exec-approvals.md`<br>`docs/cli/approvals.md`<br>`docs/plugins/plugin-permission-requests.md`<br>`docs/gateway/security/audit-checks.md` | `smoke-ci`<br>`release` | `Stable (86%)` | `Beta (72%)` | `Stable (86%)` | Yes |
| Gateway Auth and Remote Access | `security-auth-pairing-and-secrets.gateway-auth-and-remote-access` | Shared Gateway token/password auth<br>Gateway auth mode<br>Trusted-proxy identity<br>Tailscale Serve/Funnel<br>Bind and origin restrictions<br>WebSocket handshake auth<br>Operator-facing docs<br>Browser Control UI<br>Remote Client Trust | shared-gateway-token-password-auth<br>gateway-auth-mode<br>trusted-proxy-identity<br>tailscale-serve-funnel<br>bind-and-origin-restrictions<br>websocket-handshake-auth<br>operator-facing-docs<br>browser-control-ui<br>remote-client-trust | `docs/gateway/security/index.md`<br>`docs/gateway/security/exposure-runbook.md`<br>`docs/gateway/trusted-proxy-auth.md`<br>`docs/gateway/tailscale.md`<br>`docs/gateway/remote.md`<br>`docs/gateway/configuration-reference.md`<br>`docs/cli/gateway.md`<br>`docs/cli/doctor.md`<br>`docs/web/control-ui.md`<br>`docs/tools/browser-control.md`<br>`docs/gateway/security/audit-checks.md` | `release` | `Stable (82%)` | `Alpha (68%)` | `Stable (82%)` | Yes |
| Channel Access Control | `security-auth-pairing-and-secrets.channel-access-control` | Channel Identity<br>Allowlists<br>Sender Pairing | channel-identity<br>allowlists<br>sender-pairing | `docs/channels/pairing.md`<br>`docs/channels/telegram.md`<br>`docs/channels/access-groups.md`<br>`docs/gateway/security/audit-checks.md` | `release` | `Beta (78%)` | `Alpha (66%)` | `Beta (78%)` | Yes |
| Device and Node Pairing | `security-auth-pairing-and-secrets.device-and-node-pairing` | Setup codes<br>Device identity creation<br>Device-token issuance<br>Device pairing approvals for operator<br>Operator scopes that gate pairing<br>Local Control UI<br>Auth migration<br>Operator-facing docs<br>Node Pairing<br>Capability Trust<br>Remote Exec Approvals | setup-codes<br>device-identity-creation<br>device-token-issuance<br>device-pairing-approvals-for-operator<br>operator-scopes-that-gate-pairing<br>local-control-ui<br>auth-migration<br>operator-facing-docs<br>node-pairing<br>capability-trust<br>remote-exec-approvals | `docs/gateway/protocol.md`<br>`docs/cli/devices.md`<br>`docs/channels/pairing.md`<br>`docs/gateway/pairing.md`<br>`docs/gateway/operator-scopes.md`<br>`docs/web/control-ui.md`<br>`docs/web/webchat.md`<br>`docs/cli/approvals.md` | `release` | `Stable (83%)` | `Alpha (66%)` | `Stable (83%)` | Yes |
| Plugin Trust | `security-auth-pairing-and-secrets.plugin-trust` | Plugin Installation Trust<br>Security Boundaries | plugin-installation-trust<br>security-boundaries | `docs/plugins/manifest.md`<br>`docs/plugins/plugin-permission-requests.md`<br>`docs/plugins/manage-plugins.md`<br>`docs/gateway/security/audit-checks.md` | `release` | `Beta (76%)` | `Beta (70%)` | `Beta (76%)` | No |
| Credential and Secret Hygiene | `security-auth-pairing-and-secrets.credential-and-secret-hygiene` | Provider Auth Profiles<br>API Key Health<br>Secrets Storage<br>Redaction<br>Configuration Hygiene | provider-auth-profiles<br>api-key-health<br>secrets-storage<br>memory.dreaming<br>memory.promotion<br>personal.diagnostics<br>personal.redaction<br>qa.artifact-safety<br>runtime.tool-policy<br>security.redaction<br>configuration-hygiene | `docs/gateway/authentication.md`<br>`docs/cli/models.md`<br>`docs/providers/openai.md`<br>`docs/concepts/oauth.md`<br>`docs/gateway/secrets.md`<br>`docs/cli/secrets.md`<br>`docs/reference/secretref-credential-surface.md`<br>`docs/gateway/security/audit-checks.md` | `smoke-ci`<br>`release` | `Beta (78%)` | `Alpha (62%)` | `Beta (78%)` | Yes |
#### Observability
- Surface id: `telemetry-diagnostics-and-observability`
- Level: M3 Beta
- Rationale: OTel, Prometheus, logging, and diagnostics docs exist. Needs a public "what operators should look at first" maturity pass.
- Completeness instructions: `references/completeness/telemetry-diagnostics-and-observability.md`
| Category | Category ID | Features | Coverage IDs | Docs | Profiles | Coverage | Quality | Completeness | LTS |
| --------------------- | --------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- | -------------- | -------------- | -------------- | --- |
| Health and Repair | `telemetry-diagnostics-and-observability.health-and-repair` | Background health-monitor loop<br>Per-account enable/disable settings<br>Startup grace<br>Restart logging<br>openclaw doctor<br>Structured health checks<br>Core doctor checks<br>Plugin SDK doctor/health contracts<br>openclaw status<br>openclaw health<br>Gateway RPC health<br>Cached health snapshots | background-health-monitor-loop<br>per-account-enable-disable-settings<br>startup-grace<br>restart-logging<br>runtime.codex-plugin.auth<br>runtime.codex-plugin.lifecycle<br>runtime.doctor-repair<br>structured-health-checks<br>core-doctor-checks<br>plugin-sdk-doctor-health-contracts<br>openclaw-status<br>openclaw-health<br>gateway-rpc-health<br>gateway.performance<br>models.live-openai<br>plugins.kitchen-sink<br>plugins.lifecycle<br>plugins.plugin-tools | `docs/gateway/health.md`<br>`docs/channels/telegram.md`<br>`docs/cli/doctor.md`<br>`docs/gateway/doctor.md`<br>`docs/plugins/sdk-subpaths.md`<br>`docs/cli/health.md`<br>`docs/gateway/protocol.md` | `release` | `Stable (80%)` | `Beta (76%)` | `Stable (80%)` | Yes |
| Logging | `telemetry-diagnostics-and-observability.logging` | Rolling Gateway JSONL file logs<br>openclaw logs<br>Gateway RPC logs.tail<br>Redaction patterns and sinks<br>Trace correlation fields | rolling-gateway-jsonl-file-logs<br>openclaw-logs<br>gateway-rpc-logs-tail<br>redaction-patterns-and-sinks<br>trace-correlation-fields | `docs/logging.md`<br>`docs/gateway/logging.md`<br>`docs/cli/logs.md` | `release` | `Stable (82%)` | `Stable (84%)` | `Stable (82%)` | Yes |
| Diagnostic Collection | `telemetry-diagnostics-and-observability.diagnostic-collection` | openclaw gateway diagnostics export<br>openclaw gateway stability --bundle<br>Chat /diagnostics<br>Support zip composition<br>Bounded in-process stability recorder<br>openclaw gateway stability<br>Memory pressure events<br>Critical memory pressure snapshot option | openclaw-gateway-diagnostics-export<br>openclaw-gateway-stability-bundle<br>chat-diagnostics<br>personal.diagnostics<br>personal.redaction<br>qa.artifact-safety<br>bounded-in-process-stability-recorder<br>openclaw-gateway-stability<br>memory-pressure-events<br>critical-memory-pressure-snapshot-option | `docs/gateway/diagnostics.md`<br>`docs/gateway/health.md`<br>`docs/plugins/codex-harness.md`<br>`docs/gateway/protocol.md` | `release` | `Beta (76%)` | `Beta (74%)` | `Beta (76%)` | No |
| Telemetry Export | `telemetry-diagnostics-and-observability.telemetry-export` | Diagnostic event types<br>Async dispatch<br>W3C trace context creation<br>Plugin SDK diagnostic runtime exports<br>Model-call diagnostic events<br>diagnostics-otel plugin install<br>OTLP/HTTP traces<br>Trusted trace context<br>Model and runtime telemetry<br>diagnostics-prometheus plugin install<br>Gateway-authenticated GET /api/diagnostics/prometheus<br>Prometheus text exposition<br>Trusted diagnostic event subscription | diagnostic-event-types<br>async-dispatch<br>w3c-trace-context-creation<br>plugin-sdk-diagnostic-runtime-exports<br>model-call-diagnostic-events<br>diagnostics-otel-plugin-install<br>harness.qa-lab<br>telemetry.otel<br>trusted-trace-context<br>docker.e2e<br>harness.qa-lab<br>harness.tool-trace-visibility<br>personal.failure-recovery<br>personal.no-fake-progress<br>personal.task-followthrough<br>runtime.qa-bus<br>telemetry.otel<br>telemetry.prometheus<br>tools.evidence<br>tools.trace<br>diagnostics-prometheus-plugin-install<br>gateway-authenticated-get-api-diagnostics-prometheus<br>docker.e2e<br>harness.qa-lab<br>telemetry.prometheus<br>trusted-diagnostic-event-subscription | `docs/plugins/hooks.md`<br>`docs/gateway/opentelemetry.md`<br>`docs/logging.md`<br>`docs/plugins/sdk-subpaths.md`<br>`docs/plugins/reference/diagnostics-otel.md`<br>`docs/gateway/prometheus.md`<br>`docs/plugins/reference/diagnostics-prometheus.md` | `smoke-ci`<br>`release` | `Beta (78%)` | `Beta (78%)` | `Beta (78%)` | No |
| Session Diagnostics | `telemetry-diagnostics-and-observability.session-diagnostics` | session.state<br>Diagnostic session activity snapshots<br>Model usage<br>Export of session signals to stability | session-state<br>diagnostic-session-activity-snapshots<br>model-usage<br>export-of-session-signals-to-stability | `docs/gateway/opentelemetry.md`<br>`docs/gateway/prometheus.md`<br>`docs/gateway/diagnostics.md`<br>`docs/gateway/protocol.md` | `release` | `Stable (82%)` | `Beta (78%)` | `Stable (82%)` | Yes |
#### Automation: cron, hooks, tasks, polling
- Surface id: `automation-cron-hooks-tasks-polling`
- Level: M3 Beta
- Rationale: Documented and usable, but scenario proof should cover unattended delivery, retries, and failure visibility.
- Completeness instructions: `references/completeness/automation-cron-hooks-tasks-polling.md`
| Category | Category ID | Features | Coverage IDs | Docs | Profiles | Coverage | Quality | Completeness | LTS |
| -------------------------- | ---------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- | -------------- | ------------- | -------------- | --- |
| Cron Jobs | `automation-cron-hooks-tasks-polling.cron-jobs` | Create/edit/remove jobs<br>Schedule types<br>Timezone and stagger<br>Cron RPCs<br>Agent cron tool<br>Manual cron runs<br>Isolated cron execution<br>Model/provider preflight<br>Run history<br>Timeout and denial diagnostics<br>Chat announce delivery<br>Webhook delivery<br>Failure destinations<br>Skipped-run alerts<br>Delivery previews | create-edit-remove-jobs<br>schedule-types<br>timezone-and-stagger<br>cron-rpcs<br>channels.qa-channel<br>personal.reminders<br>scheduling.cron<br>channels.qa-channel<br>personal.reminders<br>scheduling.cron<br>scheduling.dedup<br>channels.qa-channel<br>personal.reminders<br>scheduling.cron<br>scheduling.dedup<br>model-provider-preflight<br>channels.qa-channel<br>scheduling.cron<br>scheduling.dedup<br>timeout-and-denial-diagnostics<br>chat-announce-delivery<br>webhook-delivery<br>failure-destinations<br>skipped-run-alerts<br>delivery-previews | `docs/automation/cron-jobs.md`<br>`docs/cli/cron.md`<br>`docs/gateway/protocol.md`<br>`docs/automation/tasks.md`<br>`docs/channels/discord.md` | `smoke-ci`<br>`release` | `Stable (82%)` | `Beta (73%)` | `Stable (82%)` | No |
| Event Ingress | `automation-cron-hooks-tasks-polling.event-ingress` | Telegram long polling<br>Telegram webhook mode<br>Zalo polling/webhook mode<br>Polling stall diagnostics<br>iMessage watch fallback<br>Gmail setup wizard<br>Watcher start/serve<br>Tailscale/public routing<br>Push token validation<br>Gmail event routing<br>POST /hooks/wake<br>POST /hooks/agent<br>Mapped hooks<br>Hook auth policy<br>Async dispatch | telegram-long-polling<br>telegram-webhook-mode<br>zalo-polling-webhook-mode<br>polling-stall-diagnostics<br>imessage-watch-fallback<br>gmail-setup-wizard<br>watcher-start-serve<br>tailscale-public-routing<br>push-token-validation<br>gmail-event-routing<br>post-hooks-wake<br>post-hooks-agent<br>mapped-hooks<br>hook-auth-policy<br>async-dispatch | `docs/channels/telegram.md`<br>`docs/channels/zalo.md`<br>`docs/channels/troubleshooting.md`<br>`docs/channels/imessage-from-bluebubbles.md`<br>`docs/automation/cron-jobs.md#gmail-pubsub-integration`<br>`docs/automation/gmail-pubsub.md`<br>`docs/cli/webhooks.md`<br>`docs/automation/cron-jobs.md#webhooks`<br>`docs/automation/webhook.md` | `release` | `Alpha (65%)` | `Alpha (58%)` | `Alpha (65%)` | No |
| Automation Hooks | `automation-cron-hooks-tasks-polling.automation-hooks` | HOOK.md authoring<br>Hook discovery<br>Hook CLI management<br>Hook packs<br>Lifecycle event dispatch<br>api.on registration<br>Tool-call policy hooks<br>Message hooks<br>Session/lifecycle hooks<br>Plugin approval requests<br>cron_changed | hook-md-authoring<br>hook-discovery<br>hook-cli-management<br>hook-packs<br>lifecycle-event-dispatch<br>api-on-registration<br>tool-call-policy-hooks<br>message-hooks<br>session-lifecycle-hooks<br>plugin-approval-requests<br>cron-changed | `docs/automation/hooks.md`<br>`docs/cli/hooks.md`<br>`docs/plugins/hooks.md`<br>`docs/plugins/plugin-permission-requests.md`<br>`docs/plugins/sdk-subpaths.md` | `release` | `Beta (78%)` | `Beta (72%)` | `Beta (78%)` | No |
| Background Tasks and Flows | `automation-cron-hooks-tasks-polling.background-tasks-and-flows` | Task list/show/cancel<br>Task notifications<br>Task audit and maintenance<br>Chat task board<br>Task pressure status<br>Managed flows<br>Mirrored flows<br>openclaw tasks flow<br>Flow audit and maintenance<br>Plugin managedFlows | task-list-show-cancel<br>task-notifications<br>task-audit-and-maintenance<br>chat-task-board<br>task-pressure-status<br>managed-flows<br>mirrored-flows<br>openclaw-tasks-flow<br>flow-audit-and-maintenance<br>plugin-managedflows | `docs/automation/tasks.md`<br>`docs/automation/index.md`<br>`docs/cli/tasks.md`<br>`docs/automation/taskflow.md`<br>`docs/plugins/sdk-runtime.md` | `release` | `Beta (73%)` | `Alpha (68%)` | `Beta (73%)` | No |
| Heartbeat | `automation-cron-hooks-tasks-polling.heartbeat` | Heartbeat scheduling<br>Active hours<br>Wake and cooldown handling<br>Due-only heartbeat tasks<br>Commitment check-ins | heartbeat-scheduling<br>active-hours<br>wake-and-cooldown-handling<br>due-only-heartbeat-tasks<br>commitments.heartbeat-target-none<br>commitments.scope<br>runtime.delivery | `docs/automation/index.md`<br>`docs/gateway/heartbeat.md`<br>`docs/concepts/commitments.md` | `release` | `Stable (82%)` | `Beta (72%)` | `Stable (82%)` | No |
| Polling Controls | `automation-cron-hooks-tasks-polling.polling-controls` | openclaw message poll<br>Telegram polls<br>Teams polls<br>Poll flags<br>Channel capability gates<br>process poll<br>process log<br>Background process status<br>No-progress loop detection<br>Process input controls | openclaw-message-poll<br>telegram-polls<br>teams-polls<br>poll-flags<br>channel-capability-gates<br>process-poll<br>process-log<br>background-process-status<br>no-progress-loop-detection<br>process-input-controls | `docs/automation/poll.md`<br>`docs/cli/message.md`<br>`docs/channels/telegram.md`<br>`docs/channels/msteams.md`<br>`docs/gateway/background-process.md` | `release` | `Beta (74%)` | `Beta (70%)` | `Beta (74%)` | No |
#### Media understanding and media generation
- Surface id: `media-understanding-and-media-generation`
- Level: M2 Alpha
- Rationale: Broad capability surface exists, but provider variance, file limits, and node/app parity make this not stable yet.
- Completeness instructions: `references/completeness/media-understanding-and-media-generation.md`
| Category | Category ID | Features | Coverage IDs | Docs | Profiles | Coverage | Quality | Completeness | LTS |
| ----------------------- | ------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- | -------------- | ------------- | -------------- | --- |
| Media Intake and Access | `media-understanding-and-media-generation.media-intake-and-access` | Local and remote media references<br>MIME and type detection<br>Size caps and bounded reads<br>Safe remote fetch<br>Local root policy<br>Inbound media store<br>PDF/document extraction dispatch<br>QR and media helper classification | local-and-remote-media-references<br>mime-and-type-detection<br>size-caps-and-bounded-reads<br>safe-remote-fetch<br>local-root-policy<br>inbound-media-store<br>pdf-document-extraction-dispatch<br>qr-and-media-helper-classification | `docs/tools/media-overview.md`<br>`docs/nodes/media-understanding.md`<br>`docs/gateway/security/secure-file-operations.md`<br>`docs/tools/pdf.md`<br>`docs/tools/image-generation.md`<br>`docs/cli/qr.md`<br>`docs/channels/line.md`<br>`docs/channels/whatsapp.md` | `release` | `Beta (74%)` | `Beta (76%)` | `Beta (74%)` | No |
| Channel Media Handling | `media-understanding-and-media-generation.channel-media-handling` | Inbound attachment staging<br>Sandbox media rewrites<br>Reply media templating<br>Message-tool attachment delivery<br>Duplicate delivery suppression | inbound-attachment-staging<br>sandbox-media-rewrites<br>reply-media-templating<br>message-tool-attachment-delivery<br>duplicate-delivery-suppression | `docs/nodes/images.md`<br>`docs/tools/media-overview.md`<br>`docs/channels/discord.md` | `release` | `Stable (84%)` | `Alpha (68%)` | `Stable (84%)` | No |
| Media Configuration | `media-understanding-and-media-generation.media-configuration` | Media capability configuration | media-capability-configuration | `docs/tools/media-overview.md`<br>`docs/tools/image-generation.md`<br>`docs/plugins/manifest.md`<br>`docs/plugins/codex-harness.md` | `release` | `Stable (82%)` | `Beta (77%)` | `Stable (82%)` | No |
| Text-to-Speech Delivery | `media-understanding-and-media-generation.text-to-speech-delivery` | TTS<br>Outbound Voice Audio Delivery | tts<br>outbound-voice-audio-delivery | `docs/tools/tts.md`<br>`docs/tools/media-overview.md`<br>`docs/channels/discord.md` | `release` | `Stable (84%)` | `Beta (70%)` | `Stable (84%)` | No |
| Media Understanding | `media-understanding-and-media-generation.media-understanding` | Audio attachment selection<br>Batch STT provider and CLI fallback<br>Voice-note mention preflight<br>Transcript insertion and echo<br>Audio proxy and limit handling<br>Inbound image summarization<br>Active vision model bypass<br>Text-only model media offload<br>Vision provider fallback<br>Image and PDF input routing<br>Video Understanding<br>Direct Video Analysis | audio-attachment-selection<br>batch-stt-provider-and-cli-fallback<br>voice-note-mention-preflight<br>transcript-insertion-and-echo<br>audio-proxy-and-limit-handling<br>channels.qa-channel<br>media.image-understanding<br>ui.control<br>active-vision-model-bypass<br>text-only-model-media-offload<br>vision-provider-fallback<br>image-and-pdf-input-routing<br>video-understanding<br>direct-video-analysis | `docs/nodes/audio.md`<br>`docs/nodes/media-understanding.md`<br>`docs/tools/media-overview.md`<br>`docs/channels/whatsapp.md`<br>`docs/nodes/images.md`<br>`docs/cli/infer.md`<br>`docs/tools/pdf.md` | `smoke-ci`<br>`release` | `Beta (72%)` | `Alpha (62%)` | `Beta (72%)` | No |
| Media Generation | `media-understanding-and-media-generation.media-generation` | Image generation tool invocation<br>Provider and model selection<br>Reference image editing<br>Generated image task lifecycle<br>Generated image persistence and delivery<br>Music generation tool invocation<br>Provider and model selection<br>Lyrics, instrumental, duration, and format controls<br>Reference inputs where supported<br>Music task lifecycle and duplicate status<br>Generated audio persistence and delivery<br>Video generation tool invocation<br>Mode and provider capability selection<br>Reference image, video, and audio inputs<br>Provider option validation<br>Video task lifecycle and status<br>Generated video persistence and delivery | channels.qa-channel<br>media.image-generation<br>tools.image-generate<br>tools.native-image-generation<br>media.image-generation<br>tools.native-image-generation<br>reference-image-editing<br>generated-image-task-lifecycle<br>generated-image-persistence-and-delivery<br>music-generation-tool-invocation<br>provider-and-model-selection-2<br>lyrics-instrumental-duration-and-format-controls<br>reference-inputs-where-supported<br>music-task-lifecycle-and-duplicate-status<br>tools.tts<br>video-generation-tool-invocation<br>mode-and-provider-capability-selection<br>reference-image-video-and-audio-inputs<br>provider-option-validation<br>video-task-lifecycle-and-status<br>generated-video-persistence-and-delivery | `docs/tools/image-generation.md`<br>`docs/tools/media-overview.md`<br>`docs/tools/skills.md`<br>`docs/tools/music-generation.md`<br>`docs/tools/video-generation.md` | `smoke-ci`<br>`release` | `Beta (74%)` | `Alpha (64%)` | `Beta (74%)` | No |
#### Voice and realtime talk
- Surface id: `voice-and-realtime-talk`
- Level: M2 Alpha
- Rationale: Multiple implementations exist across Control UI, apps, and providers. Needs latency, failure-mode, and setup scorecards before beta.
- Completeness instructions: `references/completeness/voice-and-realtime-talk.md`
| Category | Category ID | Features | Coverage IDs | Docs | Profiles | Coverage | Quality | Completeness | LTS |
| ------------------------ | -------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | --------- | ------------- | ------------- | ------------- | --- |
| Talk Providers | `voice-and-realtime-talk.talk-providers` | OpenAI Realtime voice backend bridge<br>Google Gemini Live backend bridge<br>Realtime voice provider SDK contracts<br>Provider diagnostics<br>Talk catalog<br>Talk provider config<br>Shared native config parsing | openai-realtime-voice-backend-bridge<br>google-gemini-live-backend-bridge<br>realtime-voice-provider-sdk-contracts<br>provider-diagnostics<br>talk-catalog<br>talk-provider-config<br>shared-native-config-parsing | `docs/providers/openai.md`<br>`docs/providers/google.md`<br>`docs/plugins/sdk-provider-plugins.md`<br>`docs/nodes/talk.md`<br>`docs/web/control-ui.md` | `release` | `Beta (74%)` | `Alpha (68%)` | `Beta (74%)` | No |
| Realtime Talk Sessions | `voice-and-realtime-talk.realtime-talk-sessions` | Agent consult handoff<br>Active Talk agent-run status<br>Talkback runtime behavior<br>Forced consult scheduling<br>Browser Talk start/stop UI<br>Browser WebRTC sessions<br>Browser relay mode<br>Browser tool-call forwarding<br>Realtime session controls<br>Gateway relay sessions<br>Audio-frame limits | agent-consult-handoff<br>active-talk-agent-run-status<br>talkback-runtime-behavior<br>forced-consult-scheduling<br>browser-talk-start-stop-ui<br>browser-webrtc-sessions<br>browser-relay-mode<br>browser-tool-call-forwarding<br>realtime-session-controls<br>gateway-relay-sessions<br>audio-frame-limits | `docs/nodes/talk.md`<br>`docs/web/control-ui.md` | `release` | `Beta (72%)` | `Alpha (68%)` | `Beta (72%)` | No |
| Speech and Transcription | `voice-and-realtime-talk.speech-and-transcription` | Voice directives<br>Talk speech playback<br>Transcription relay sessions<br>Realtime transcription providers<br>Native directive parsing | voice-directives<br>talk-speech-playback<br>transcription-relay-sessions<br>realtime-transcription-providers<br>native-directive-parsing | `docs/nodes/talk.md`<br>`docs/providers/openai.md`<br>`docs/providers/google.md` | `release` | `Beta (72%)` | `Alpha (68%)` | `Beta (72%)` | No |
| Native App Talk | `voice-and-realtime-talk.native-app-talk` | macOS native Talk mode<br>iOS Talk mode<br>Android Talk mode<br>Shared Talk config | macos-native-talk-mode<br>ios-talk-mode<br>android-talk-mode<br>shared-talk-config | `docs/nodes/talk.md`<br>`docs/platforms/mac/voicewake.md` | `release` | `Alpha (68%)` | `Alpha (64%)` | `Alpha (68%)` | No |
| Voice Wake and Routing | `voice-and-realtime-talk.voice-wake-and-routing` | Wake-word settings<br>Wake routing<br>macOS Voice Wake runtime<br>Mobile wake preferences | wake-word-settings<br>wake-routing<br>macos-voice-wake-runtime<br>mobile-wake-preferences | `docs/nodes/voicewake.md`<br>`docs/platforms/mac/voicewake.md`<br>`docs/platforms/mac/voice-overlay.md` | `release` | `Beta (74%)` | `Alpha (66%)` | `Beta (74%)` | No |
| Talk Observability | `voice-and-realtime-talk.talk-observability` | Talk event logging<br>Session-log health<br>Live smoke output<br>Prometheus diagnostic counters<br>Operator visibility into setup | talk-event-logging<br>session-log-health<br>live-smoke-output<br>prometheus-diagnostic-counters<br>operator-visibility-into-setup | `docs/web/control-ui.md`<br>`docs/platforms/mac/voice-overlay.md`<br>`docs/nodes/talk.md` | `release` | `Beta (76%)` | `Beta (70%)` | `Beta (76%)` | No |
#### Gateway Web App
- Surface id: `browser-control-ui-and-webchat`
- Level: M3 Beta
- Rationale: Web UI is documented with pairing, chat, PWA, Talk, push, and remote Gateway flows. Promote after cross-browser and mobile-PWA scorecards.
- Completeness instructions: `references/completeness/browser-control-ui-and-webchat.md`
| Category | Category ID | Features | Coverage IDs | Docs | Profiles | Coverage | Quality | Completeness | LTS |
| ------------------------ | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- | -------------- | ------------- | -------------- | --- |
| Browser Realtime Talk | `browser-control-ui-and-webchat.browser-realtime-talk` | Browser Talk start/stop<br>Provider session selection<br>Gateway relay audio<br>Tool-call consults<br>Steer and cancel | browser-talk-start-stop<br>provider-session-selection<br>gateway-relay-audio<br>tool-call-consults<br>steer-and-cancel | `docs/web/control-ui.md`<br>`docs/gateway/protocol.md`<br>`docs/nodes/talk.md` | `release` | `Beta (78%)` | `Beta (70%)` | `Beta (78%)` | No |
| Browser Access and Trust | `browser-control-ui-and-webchat.browser-access-and-trust` | Device pairing<br>Token/password auth<br>Tailscale Serve auth<br>Trusted proxy auth<br>Allowed origins/gatewayUrl | device-pairing<br>token-password-auth<br>tailscale-serve-auth<br>trusted-proxy-auth<br>allowed-origins-gatewayurl | `docs/web/control-ui.md`<br>`docs/web/dashboard.md`<br>`docs/gateway/tailscale.md`<br>`docs/gateway/remote.md` | `release` | `Stable (84%)` | `Alpha (68%)` | `Stable (84%)` | No |
| Configuration | `browser-control-ui-and-webchat.configuration` | Config snapshots<br>Schema form editing<br>Raw JSON editing<br>Base-hash guarded writes<br>Apply and restart | config-snapshots<br>schema-form-editing<br>raw-json-editing<br>base-hash-guarded-writes<br>apply-and-restart | `docs/web/control-ui.md`<br>`docs/gateway/configuration.md` | `release` | `Stable (82%)` | `Beta (78%)` | `Stable (82%)` | No |
| Browser UI | `browser-control-ui-and-webchat.browser-ui` | Gateway-hosted UI<br>Dashboard open/auth bootstrap<br>Base-path routing<br>Static asset recovery<br>Dev gatewayUrl target<br>PWA install metadata<br>Service worker updates<br>VAPID keys<br>Subscribe/unsubscribe<br>Test notifications | channels.qa-channel<br>media.image-understanding<br>ui.control<br>dashboard-open-auth-bootstrap<br>base-path-routing<br>static-asset-recovery<br>dev-gatewayurl-target<br>pwa-install-metadata<br>service-worker-updates<br>vapid-keys<br>subscribe-unsubscribe<br>test-notifications | `docs/web/control-ui.md`<br>`docs/web/index.md`<br>`docs/web/dashboard.md`<br>`docs/gateway/protocol.md` | `smoke-ci`<br>`release` | `Beta (74%)` | `Beta (72%)` | `Beta (74%)` | No |
| WebChat Conversations | `browser-control-ui-and-webchat.webchat-conversations` | Send and abort<br>Session and agent picker<br>Model/thinking controls<br>Attachments<br>Markdown/tool/media rendering<br>chat.history projection<br>chat.send lifecycle<br>Abort/partial retention<br>Injected assistant notes<br>Reconnect continuity<br>Hosted embeds<br>External embed gating<br>Assistant media tickets<br>Authenticated avatars<br>CSP image policy | send-and-abort<br>session-and-agent-picker<br>model-thinking-controls<br>attachments<br>markdown-tool-media-rendering<br>chat-history-projection<br>channels.qa-channel<br>channels.webchat<br>media.image-understanding<br>runtime.direct-reply-routing<br>tools.message<br>ui.control<br>abort-partial-retention<br>injected-assistant-notes<br>reconnect-continuity<br>hosted-embeds<br>external-embed-gating<br>assistant-media-tickets<br>authenticated-avatars<br>csp-image-policy | `docs/web/control-ui.md`<br>`docs/web/webchat.md`<br>`docs/start/getting-started.md`<br>`docs/channels/channel-routing.md`<br>`docs/gateway/security/secure-file-operations.md` | `release` | `Beta (78%)` | `Alpha (66%)` | `Beta (78%)` | No |
| Operator Console | `browser-control-ui-and-webchat.operator-console` | Health/status/models<br>Live log tail<br>Update run/status<br>Activity summaries<br>RPC timing telemetry<br>Channels/login<br>Session manager and history<br>Cron<br>Skills/nodes<br>Exec approvals/agents | health-status-models<br>live-log-tail<br>runtime.gateway-restart<br>runtime.package-update<br>runtime.update-run<br>activity-summaries<br>rpc-timing-telemetry<br>channels-login<br>session-manager-and-history<br>cron<br>skills-nodes<br>exec-approvals-agents | `docs/web/control-ui.md`<br>`docs/gateway/health.md`<br>`docs/gateway/protocol.md`<br>`docs/web/dashboard.md` | `release` | `Beta (78%)` | `Beta (74%)` | `Beta (78%)` | No |
#### TUI
- Surface id: `tui-and-terminal-ux`
- Level: M2 Alpha
- Rationale: Present in docs and source, but less visible as a primary user workflow. Needs explicit scenario definition.
- Completeness instructions: `references/completeness/tui-and-terminal-ux.md`
| Category | Category ID | Features | Coverage IDs | Docs | Profiles | Coverage | Quality | Completeness | LTS |
| --------------------------- | ------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | --------- | -------------- | ------------- | -------------- | --- |
| Runtime Modes | `tui-and-terminal-ux.runtime-modes` | Gateway TUI launch<br>Local chat launch<br>Terminal alias launch<br>Initial message launch<br>Launch option validation<br>Gateway connection<br>Gateway authentication<br>History load on attach<br>Reconnect visibility<br>Gateway command RPCs<br>Embedded local chat<br>Local auth flow<br>Config repair loop<br>Gateway-free recovery | gateway-tui-launch<br>local-chat-launch<br>terminal-alias-launch<br>initial-message-launch<br>launch-option-validation<br>gateway-connection<br>gateway-authentication<br>history-load-on-attach<br>reconnect-visibility<br>gateway-command-rpcs<br>embedded-local-chat<br>local-auth-flow<br>config-repair-loop<br>gateway-free-recovery | `docs/cli/tui.md`<br>`docs/web/tui.md`<br>`docs/cli/index.md` | `release` | `Beta (78%)` | `Beta (72%)` | `Beta (78%)` | No |
| Input and Commands | `tui-and-terminal-ux.input-and-commands` | Message composition<br>Input history<br>Keyboard shortcuts<br>Paste and busy-submit handling<br>IME and AltGr handling<br>Slash Commands<br>Pickers<br>Settings | message-composition<br>input-history<br>keyboard-shortcuts<br>paste-and-busy-submit-handling<br>ime-and-altgr-handling<br>slash-commands<br>pickers<br>settings | `docs/web/tui.md` | `release` | `Beta (76%)` | `Beta (70%)` | `Beta (76%)` | No |
| Session Management | `tui-and-terminal-ux.session-management` | Session Lifecycle<br>History<br>Resume | session-lifecycle<br>history<br>resume | `docs/web/tui.md`<br>`docs/cli/sessions.md` | `release` | `Stable (80%)` | `Alpha (68%)` | `Stable (80%)` | No |
| Local Shell Execution | `tui-and-terminal-ux.local-shell-execution` | Bang-command routing<br>Approval prompt<br>Command output display<br>Execution environment marker | bang-command-routing<br>approval-prompt<br>command-output-display<br>execution-environment-marker | `docs/web/tui.md`<br>`docs/cli/tui.md` | `release` | `Beta (70%)` | `Beta (76%)` | `Beta (70%)` | No |
| Rendering and Output Safety | `tui-and-terminal-ux.rendering-and-output-safety` | Streaming Message Rendering<br>Tool Cards<br>Terminal Rendering Primitives<br>Output Safety | streaming-message-rendering<br>tool-cards<br>terminal-rendering-primitives<br>output-safety | `docs/web/tui.md`<br>`docs/cli/qr.md`<br>`docs/cli/logs.md`<br>`docs/cli/completion.md` | `release` | `Beta (76%)` | `Beta (70%)` | `Beta (76%)` | No |
#### ClawHub
- Surface id: `clawhub-and-external-plugin-distribution`
- Level: M2 Alpha
- Rationale: Public docs and ecosystem concept exist. Needs install, trust, update, rollback, and compatibility scorecards.
- Completeness instructions: `references/completeness/clawhub-and-external-plugin-distribution.md`
| Category | Category ID | Features | Coverage IDs | Docs | Profiles | Coverage | Quality | Completeness | LTS |
| --------------------------- | ---------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | ------------- | ------------ | ------------- | --- |
| Publishing | `clawhub-and-external-plugin-distribution.publishing` | ClawHub package publishing owner<br>OpenClaw-owned package release validation for ClawHub<br>Version bump gates<br>npm trusted publishing provenance<br>External code plugin package contract required<br>Skill package metadata<br>Skill publishing flow | clawhub-package-publishing-owner<br>openclaw-owned-package-release-validation-for-clawhub<br>version-bump-gates<br>npm-trusted-publishing-provenance<br>external-code-plugin-package-contract-required<br>skill-package-metadata<br>skill-publishing-flow | `docs/clawhub/publishing.md`<br>`docs/clawhub/skill-format.md`<br>`docs/tools/creating-skills.md`<br>`docs/plugins/community.md` | `release` | `Beta (72%)` | `Beta (76%)` | `Beta (72%)` | No |
| Catalog Discovery | `clawhub-and-external-plugin-distribution.catalog-discovery` | openclaw plugins search as the ClawHub<br>Search result metadata<br>Distinction between plugin search<br>Catalog lookup failure<br>Skill catalog search | openclaw-plugins-search-as-the-clawhub<br>search-result-metadata<br>distinction-between-plugin-search<br>catalog-lookup-failure<br>skill-catalog-search | `docs/tools/plugin.md`<br>`docs/cli/plugins.md`<br>`docs/cli/skills.md`<br>`docs/tools/skills.md`<br>`docs/plugins/community.md` | `release` | `Alpha (66%)` | `Beta (72%)` | `Alpha (66%)` | No |
| Compatibility and Trust | `clawhub-and-external-plugin-distribution.compatibility-and-trust` | openclaw.compat.pluginApi<br>ClawHub package compatibility validation<br>npm compatibility fallback to the newest<br>Official external plugin catalog behavior<br>Compatibility docs<br>Operator trust model for installing<br>ClawHub archive<br>npm integrity drift<br>Built-in dangerous-code scanner<br>ClawHub publishing review/hidden-release behavior as upstream<br>Skill archive safety<br>Skill audit signals | openclaw-compat-pluginapi<br>clawhub-package-compatibility-validation<br>npm-compatibility-fallback-to-the-newest<br>official-external-plugin-catalog-behavior<br>compatibility-docs<br>operator-trust-model-for-installing<br>clawhub-archive<br>npm-integrity-drift<br>built-in-dangerous-code-scanner<br>clawhub-publishing-review-hidden-release-behavior-as-upstream<br>skill-archive-safety<br>skill-audit-signals | `docs/tools/plugin.md`<br>`docs/cli/plugins.md`<br>`docs/plugins/compatibility.md`<br>`docs/plugins/plugin-inventory.md`<br>`docs/clawhub/publishing.md`<br>`docs/clawhub/security-audits.md`<br>`docs/tools/skills.md`<br>`docs/tools/skills-config.md` | `release` | `Beta (76%)` | `Beta (74%)` | `Beta (76%)` | No |
| Plugin Lifecycle and Health | `clawhub-and-external-plugin-distribution.plugin-lifecycle-and-health` | Source prefixes<br>Bare package behavior during the launch<br>Explicit pinned versions<br>Managed install records that preserve source<br>Codex<br>Local<br>Marketplace list<br>Supported mapped features<br>Remote marketplace path safety<br>Update by plugin id<br>Reinstall vs update semantics<br>Downgrade<br>Uninstall config/index/policy/file cleanup<br>Gateway restart/reload requirements after<br>Per-plugin managed npm project<br>npm-pack local release-candidate installs<br>Dependency ownership between plugin packages<br>Peer dependency relinking<br>Legacy dependency root cleanup<br>plugins list<br>Local plugin index<br>Troubleshooting stale config<br>Runtime verification after Gateway<br>ClawHub skill installs<br>Skill upload install path<br>Skill dependency installers | source-prefixes<br>bare-package-behavior-during-the-launch<br>explicit-pinned-versions<br>managed-install-records-that-preserve-source<br>codex<br>local<br>marketplace-list<br>supported-mapped-features<br>remote-marketplace-path-safety<br>update-by-plugin-id<br>reinstall-vs-update-semantics<br>downgrade<br>uninstall-config-index-policy-file-cleanup<br>gateway-restart-reload-requirements-after<br>per-plugin-managed-npm-project<br>npm-pack-local-release-candidate-installs<br>dependency-ownership-between-plugin-packages<br>peer-dependency-relinking<br>legacy-dependency-root-cleanup<br>plugins-list<br>local-plugin-index<br>troubleshooting-stale-config<br>runtime-verification-after-gateway<br>clawhub-skill-installs<br>skill-upload-install-path<br>skill-dependency-installers | `docs/tools/plugin.md`<br>`docs/cli/plugins.md`<br>`docs/cli/skills.md`<br>`docs/tools/skills.md`<br>`docs/gateway/protocol.md`<br>`docs/plugins/bundles.md`<br>`docs/plugins/dependency-resolution.md` | `release` | `Beta (76%)` | `Beta (71%)` | `Beta (76%)` | No |
#### OpenClaw App SDK
- Surface id: `openclaw-app-sdk`
- Level: M2 Alpha
- Rationale: OpenClaw App SDK is a distinct external app contract separate from Gateway runtime and Plugin SDK. Current scoring shows a real `@openclaw/sdk` path with gaps around public packaging, auto-discovery, approvals, helpers, and compatibility.
- Completeness instructions: `references/completeness/openclaw-app-sdk.md`
| Category | Category ID | Features | Coverage IDs | Docs | Profiles | Coverage | Quality | Completeness | LTS |
| -------------------- | --------------------------------------- | ---------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | --------- | -------------- | -------------- | -------------- | --- |
| Client API | `openclaw-app-sdk.client-api` | SDK entrypoints<br>Namespace layout<br>Package split<br>App/plugin boundary | sdk-entrypoints<br>namespace-layout<br>package-split<br>app-plugin-boundary | `docs/concepts/openclaw-sdk.md`<br>`docs/reference/openclaw-sdk-api-design.md` | `release` | `Stable (86%)` | `Stable (82%)` | `Beta (78%)` | No |
| Gateway Access | `openclaw-app-sdk.gateway-access` | Gateway connect<br>URL and token config<br>Auto gateway<br>Custom transport<br>Scopes and redaction | gateway-connect<br>url-and-token-config<br>auto-gateway<br>custom-transport<br>scopes-and-redaction | `docs/concepts/openclaw-sdk.md`<br>`docs/reference/openclaw-sdk-api-design.md`<br>`docs/gateway/protocol.md`<br>`docs/gateway/security/index.md` | `release` | `Beta (78%)` | `Beta (74%)` | `Alpha (64%)` | No |
| Agent Conversations | `openclaw-app-sdk.agent-conversations` | Agent handles<br>Agent runs<br>Run results<br>Session creation<br>Session send<br>Session controls | agent-handles<br>agent-runs<br>run-results<br>session-creation<br>session-send<br>session-controls | `docs/concepts/openclaw-sdk.md`<br>`docs/reference/openclaw-sdk-api-design.md`<br>`docs/gateway/protocol.md` | `release` | `Beta (78%)` | `Stable (80%)` | `Stable (84%)` | No |
| Events and Approvals | `openclaw-app-sdk.events-and-approvals` | Event stream<br>Event envelope<br>Replay cursors<br>Approval callbacks<br>Questions | event-stream<br>event-envelope<br>replay-cursors<br>approval-callbacks<br>questions | `docs/concepts/openclaw-sdk.md`<br>`docs/reference/openclaw-sdk-api-design.md`<br>`docs/gateway/protocol.md` | `release` | `Beta (74%)` | `Beta (73%)` | `Alpha (58%)` | No |
| Resource Helpers | `openclaw-app-sdk.resource-helpers` | Models<br>ToolSpace<br>Artifacts<br>Tasks<br>Environments | models<br>toolspace<br>character.persona<br>personal.task-followthrough<br>tools.followthrough<br>workspace.artifacts<br>workspace.builds<br>workspace.long-running-task<br>workspace.repo-discovery<br>tasks<br>environments | `docs/concepts/openclaw-sdk.md`<br>`docs/reference/openclaw-sdk-api-design.md` | `release` | `Alpha (58%)` | `Beta (72%)` | `Beta (70%)` | No |
| Compatibility | `openclaw-app-sdk.compatibility` | Generated client<br>Ergonomic wrappers<br>Unsupported calls<br>Schema alignment<br>Public package contract | generated-client<br>ergonomic-wrappers<br>unsupported-calls<br>schema-alignment<br>public-package-contract | `docs/reference/openclaw-sdk-api-design.md`<br>`docs/concepts/typebox.md`<br>`docs/gateway/protocol.md` | `release` | `Beta (76%)` | `Beta (70%)` | `Alpha (62%)` | No |
### Platform
#### macOS Gateway host
- Surface id: `macos-gateway-host`
- Level: M4 Stable
- Rationale: LaunchAgent service path, local/remote Gateway modes, CLI install, and app integration are documented.
- Completeness instructions: `references/completeness/macos-gateway-host.md`
| Category | Category ID | Features | Coverage IDs | Docs | Profiles | Coverage | Quality | Completeness | LTS |
| ----------------------------------- | -------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | -------------- | -------------- | -------------- | --- |
| CLI Setup | `macos-gateway-host.cli-setup` | Hosted installer<br>Node 24 recommendation<br>App-triggered CLI install<br>Shell PATH and version-manager drift | hosted-installer<br>node-24-recommendation<br>app-triggered-cli-install<br>shell-path-and-version-manager-drift | `docs/platforms/macos.md`<br>`docs/platforms/mac/bundled-gateway.md`<br>`docs/install/installer.md`<br>`docs/install/node.md` | `release` | `Stable (82%)` | `Beta (76%)` | `Stable (82%)` | No |
| Local Gateway Integration | `macos-gateway-host.local-gateway-integration` | App local/remote connection mode<br>App-managed Gateway LaunchAgent install/restart/uninstall<br>CLI install detection<br>Attach-to-existing local Gateway compatibility<br>Gateway endpoint<br>gateway.mode=local configuration<br>Loopback bind<br>Local app endpoint resolution<br>Bonjour discovery | app-local-remote-connection-mode<br>app-managed-gateway-launchagent-install-restart-uninstall<br>cli-install-detection<br>attach-to-existing-local-gateway-compatibility<br>gateway-endpoint<br>gateway-mode-local-configuration<br>loopback-bind<br>local-app-endpoint-resolution<br>bonjour-discovery | `docs/platforms/macos.md`<br>`docs/platforms/mac/bundled-gateway.md`<br>`docs/platforms/mac/remote.md`<br>`docs/gateway/index.md`<br>`docs/cli/gateway.md`<br>`docs/gateway/bonjour.md` | `release` | `Beta (76%)` | `Stable (82%)` | `Beta (76%)` | No |
| Remote Gateway Mode | `macos-gateway-host.remote-gateway-mode` | macOS app "Remote over SSH"<br>SSH tunnel setup<br>Tailscale MagicDNS<br>Remote endpoint token/password/TLS fingerprint<br>Local node host startup | macos-app-remote-over-ssh<br>ssh-tunnel-setup<br>tailscale-magicdns<br>remote-endpoint-token-password-tls-fingerprint<br>local-node-host-startup | `docs/platforms/mac/remote.md`<br>`docs/gateway/remote.md`<br>`docs/gateway/tailscale.md` | `release` | `Beta (72%)` | `Stable (82%)` | `Beta (72%)` | No |
| Gateway Service Lifecycle | `macos-gateway-host.gateway-service-lifecycle` | Per-user Gateway LaunchAgent install<br>launchctl bootstrap<br>LaunchAgent labels<br>Gateway token/env handling<br>App-managed LaunchAgent handoff<br>openclaw update package/git handoff<br>Managed service refresh<br>Stale updater launchd job detection<br>openclaw uninstall<br>Stranded service recovery | per-user-gateway-launchagent-install<br>launchctl-bootstrap<br>launchagent-labels<br>gateway-token-env-handling<br>app-managed-launchagent-handoff<br>openclaw-update-package-git-handoff<br>managed-service-refresh<br>stale-updater-launchd-job-detection<br>openclaw-uninstall<br>stranded-service-recovery | `docs/platforms/macos.md`<br>`docs/platforms/mac/bundled-gateway.md`<br>`docs/cli/gateway.md`<br>`docs/gateway/index.md`<br>`docs/cli/update.md`<br>`docs/install/updating.md`<br>`docs/install/uninstall.md`<br>`docs/gateway/troubleshooting.md` | `release` | `Stable (82%)` | `Beta (76%)` | `Stable (82%)` | No |
| Diagnostics and Observability | `macos-gateway-host.diagnostics-and-observability` | LaunchAgent log paths<br>openclaw gateway status --deep<br>Gateway silently stops responding<br>Stale updater jobs | launchagent-log-paths<br>openclaw-gateway-status-deep<br>gateway-silently-stops-responding<br>stale-updater-jobs | `docs/platforms/mac/bundled-gateway.md`<br>`docs/platforms/macos.md`<br>`docs/cli/gateway.md`<br>`docs/gateway/doctor.md`<br>`docs/gateway/troubleshooting.md` | `release` | `Stable (80%)` | `Stable (83%)` | `Stable (80%)` | No |
| Permissions and Native Capabilities | `macos-gateway-host.permissions-and-native-capabilities` | macOS TCC permission prompts/status<br>Native node capability exposure<br>system.run policy<br>Permission-driven support | macos-tcc-permission-prompts-status<br>native-node-capability-exposure<br>system-run-policy<br>permission-driven-support | `docs/platforms/macos.md`<br>`docs/platforms/mac/remote.md` | `release` | `Alpha (62%)` | `Beta (73%)` | `Alpha (62%)` | No |
| Profiles and Isolation | `macos-gateway-host.profiles-and-isolation` | Profile-specific LaunchAgent labels<br>Profile-specific state/config/workspace roots<br>Derived ports<br>Rescue bot setup<br>Extra Gateway process detection | profile-specific-launchagent-labels<br>profile-specific-state-config-workspace-roots<br>derived-ports<br>rescue-bot-setup<br>extra-gateway-process-detection | `docs/gateway/multiple-gateways.md`<br>`docs/gateway/index.md`<br>`docs/cli/gateway.md` | `release` | `Beta (74%)` | `Stable (82%)` | `Beta (74%)` | No |
#### macOS companion app
- Surface id: `macos-companion-app`
- Level: M3 Beta
- Rationale: Rich menu bar app, permissions, node mode, Canvas, voice wake, WebChat, and remote mode exist. Still fast-moving enough to avoid Stable.
- Completeness instructions: `references/completeness/macos-companion-app.md`
| Category | Category ID | Features | Coverage IDs | Docs | Profiles | Coverage | Quality | Completeness | LTS |
| ------------------- | ----------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | ------------- | ------------- | ------------- | --- |
| Canvas | `macos-companion-app.canvas` | Canvas panel open/hide/navigate/eval/snapshot<br>Local custom URL scheme<br>A2UI host auto-navigation<br>Canvas enable/disable setting | canvas-panel-open-hide-navigate-eval-snapshot<br>local-custom-url-scheme<br>a2ui-host-auto-navigation<br>canvas-enable-disable-setting | `docs/platforms/mac/canvas.md`<br>`docs/platforms/macos.md`<br>`docs/web/webchat.md` | `release` | `Beta (74%)` | `Alpha (66%)` | `Beta (74%)` | No |
| Local Setup | `macos-companion-app.local-setup` | Local mode Gateway attach/start/stop<br>LaunchAgent install/update/restart/uninstall<br>Existing-listener detection<br>Native first-run onboarding flow<br>CLI discovery<br>Local workspace selection<br>Onboarding WebChat session separation | local-mode-gateway-attach-start-stop<br>launchagent-install-update-restart-uninstall<br>existing-listener-detection<br>native-first-run-onboarding-flow<br>cli-discovery<br>local-workspace-selection<br>onboarding-webchat-session-separation | `docs/platforms/mac/bundled-gateway.md`<br>`docs/platforms/macos.md`<br>`docs/platforms/mac/child-process.md`<br>`docs/platforms/mac/dev-setup.md` | `release` | `Beta (72%)` | `Alpha (65%)` | `Beta (72%)` | No |
| Status and Settings | `macos-companion-app.status-and-settings` | Menu-bar status<br>Activity state ingestion<br>Settings navigation<br>Health polling<br>Channels settings | menu-bar-status<br>activity-state-ingestion<br>settings-navigation<br>health-polling<br>channels-settings | `docs/platforms/mac/menu-bar.md`<br>`docs/platforms/mac/icon.md`<br>`docs/platforms/macos.md`<br>`docs/platforms/mac/health.md`<br>`docs/platforms/mac/logging.md`<br>`docs/platforms/mac/remote.md` | `release` | `Beta (70%)` | `Beta (72%)` | `Beta (70%)` | No |
| Native Capabilities | `macos-companion-app.native-capabilities` | Mac node session connection<br>system.run<br>Exec approval policy<br>Permission requests<br>TCC persistence | mac-node-session-connection<br>system-run<br>exec-approval-policy<br>permission-requests<br>tcc-persistence | `docs/platforms/macos.md`<br>`docs/platforms/mac/xpc.md`<br>`docs/platforms/mac/permissions.md`<br>`docs/platforms/mac/signing.md`<br>`docs/platforms/mac/peekaboo.md` | `release` | `Alpha (64%)` | `Alpha (60%)` | `Alpha (64%)` | No |
| Remote Connections | `macos-companion-app.remote-connections` | Remote connection mode selection<br>SSH tunnel<br>Gateway discovery | remote-connection-mode-selection<br>ssh-tunnel<br>gateway-discovery | `docs/platforms/mac/remote.md`<br>`docs/platforms/macos.md`<br>`docs/gateway/remote.md` | `release` | `Beta (72%)` | `Alpha (68%)` | `Beta (72%)` | No |
| Voice and Talk | `macos-companion-app.voice-and-talk` | Voice Wake runtime<br>Push-to-talk<br>Talk provider playback plan | voice-wake-runtime<br>push-to-talk<br>talk-provider-playback-plan | `docs/platforms/mac/voicewake.md`<br>`docs/platforms/mac/voice-overlay.md`<br>`docs/nodes/talk.md`<br>`docs/platforms/macos.md` | `release` | `Beta (70%)` | `Alpha (63%)` | `Beta (70%)` | No |
| WebChat | `macos-companion-app.webchat` | Native SwiftUI WebChat window<br>Gateway chat transport<br>Local and remote data-plane reuse | native-swiftui-webchat-window<br>gateway-chat-transport<br>local-and-remote-data-plane-reuse | `docs/platforms/mac/webchat.md`<br>`docs/platforms/macos.md`<br>`docs/web/webchat.md` | `release` | `Beta (72%)` | `Alpha (62%)` | `Beta (72%)` | No |
| Remote WebChat | `macos-companion-app.remote-webchat` | macOS WebChat transport<br>SSH tunnel data plane<br>Direct ws/wss remote mode<br>Session continuity<br>Remote troubleshooting | macos-webchat-transport<br>ssh-tunnel-data-plane<br>direct-ws-wss-remote-mode<br>session-continuity<br>remote-troubleshooting | `docs/platforms/mac/webchat.md`<br>`docs/gateway/remote.md`<br>`docs/platforms/mac/remote.md` | `release` | `Beta (74%)` | `Beta (76%)` | `Beta (74%)` | No |
#### Linux Gateway host
- Surface id: `linux-gateway-host`
- Level: M4 Stable
- Rationale: Node runtime is recommended, systemd user service is documented, and VPS/container guidance is broad.
- Completeness instructions: `references/completeness/linux-gateway-host.md`
| Category | Category ID | Features | Coverage IDs | Docs | Profiles | Coverage | Quality | Completeness | LTS |
| ----------------------------------- | -------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | -------------- | ------------ | -------------- | --- |
| Host Setup and Updates | `linux-gateway-host.host-setup-and-updates` | Linux CLI install<br>Node runtime prerequisites<br>Package-manager policy<br>Update path | linux-cli-install<br>node-runtime-prerequisites<br>package-manager-policy<br>update-path | `docs/install/index.md`<br>`docs/install/updating.md`<br>`docs/platforms/linux.md`<br>`docs/platforms/index.md` | `release` | `Stable (82%)` | `Beta (78%)` | `Stable (82%)` | Yes |
| Gateway Runtime and Service Control | `linux-gateway-host.gateway-runtime-and-service-control` | Foreground Gateway Runtime<br>Process Control<br>Systemd User Service Lifecycle setup<br>Systemd User Service Lifecycle operation<br>Systemd User Service Lifecycle status<br>Systemd User Service Lifecycle recovery | foreground-gateway-runtime<br>process-control<br>systemd-user-service-lifecycle-setup<br>systemd-user-service-lifecycle-operation<br>systemd-user-service-lifecycle-status<br>systemd-user-service-lifecycle-recovery | `docs/gateway/index.md`<br>`docs/cli/gateway.md`<br>`docs/platforms/linux.md`<br>`docs/vps.md` | `release` | `Stable (83%)` | `Beta (78%)` | `Stable (83%)` | Yes |
| Remote Access and Security | `linux-gateway-host.remote-access-and-security` | Remote Network Exposure<br>TLS<br>Tailscale<br>Gateway exposure safeguards<br>Gateway authentication modes<br>Secret Handling | remote-network-exposure<br>tls<br>tailscale<br>gateway-exposure-safeguards<br>gateway-authentication-modes<br>secret-handling | `docs/gateway/remote.md`<br>`docs/gateway/tailscale.md`<br>`docs/gateway/security/exposure-runbook.md`<br>`docs/gateway/authentication.md`<br>`docs/gateway/secrets.md` | `release` | `Beta (78%)` | `Beta (74%)` | `Beta (78%)` | Yes |
| Diagnostics and Repair | `linux-gateway-host.diagnostics-and-repair` | Gateway diagnostic reports<br>Gateway log tailing<br>Doctor checks<br>Operator repair guidance | gateway-diagnostic-reports<br>gateway-log-tailing<br>doctor-checks<br>operator-repair-guidance | `docs/cli/status.md`<br>`docs/cli/logs.md`<br>`docs/cli/doctor.md`<br>`docs/gateway/diagnostics.md`<br>`docs/gateway/index.md` | `release` | `Stable (82%)` | `Beta (78%)` | `Stable (82%)` | Yes |
| Deployment Targets | `linux-gateway-host.deployment-targets` | VPS<br>Container<br>Cloud Deployment Guidance | vps<br>container<br>cloud-deployment-guidance | `docs/vps.md`<br>`docs/install/docker.md`<br>`docs/install/hetzner.md`<br>`docs/install/digitalocean.md`<br>`docs/install/kubernetes.md`<br>`docs/install/podman.md` | `release` | `Beta (76%)` | `Beta (72%)` | `Beta (76%)` | No |
#### Linux companion app
- Surface id: `linux-companion-app`
- Level: M0 Planned
- Rationale: Docs say native Linux companion apps are planned; Gateway is the supported Linux path today.
- Completeness instructions: `references/completeness/linux-companion-app.md`
| Category | Category ID | Features | Coverage IDs | Docs | Profiles | Coverage | Quality | Completeness | LTS |
| ---------------------- | -------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | -------------------- | -------------------- | -------------------- | --- |
| App Distribution | `linux-companion-app.app-distribution` | Native app package<br>Distro package targets<br>Official release metadata | native-app-package<br>distro-package-targets<br>official-release-metadata | `docs/platforms/linux.md`<br>`docs/platforms/index.md`<br>`docs/install/index.md` | `release` | `Experimental (0%)` | `Experimental (18%)` | `Experimental (0%)` | No |
| Gateway Connectivity | `linux-companion-app.gateway-connectivity` | Local Gateway attach and status<br>Gateway pairing and auth<br>Remote mode<br>Local and remote resource boundaries | local-gateway-attach-and-status<br>gateway-pairing-and-auth<br>remote-mode<br>local-and-remote-resource-boundaries | `docs/platforms/linux.md`<br>`docs/gateway/index.md`<br>`docs/gateway/pairing.md`<br>`docs/gateway/remote.md` | `release` | `Experimental (8%)` | `Experimental (35%)` | `Experimental (8%)` | No |
| Chat and Sessions | `linux-companion-app.chat-and-sessions` | Native Linux chat window<br>Transcript<br>Gateway chat transport | native-linux-chat-window<br>transcript<br>gateway-chat-transport | `docs/platforms/linux.md`<br>`docs/gateway/protocol.md`<br>`docs/web/webchat.md` | `release` | `Experimental (10%)` | `Experimental (36%)` | `Experimental (10%)` | No |
| Desktop Capabilities | `linux-companion-app.desktop-capabilities` | Linux desktop permissions<br>Secret storage<br>Sandbox/package posture<br>Linux native node identity<br>Host command execution<br>Desktop tools<br>Linux native Talk<br>Microphone capture<br>Native media permissions | linux-desktop-permissions<br>secret-storage<br>sandbox-package-posture<br>linux-native-node-identity<br>host-command-execution<br>desktop-tools<br>linux-native-talk<br>microphone-capture<br>native-media-permissions | `docs/platforms/linux.md`<br>`docs/tools/exec-approvals.md`<br>`docs/gateway/secrets.md`<br>`docs/nodes/index.md`<br>`docs/tools/exec.md`<br>`docs/nodes/talk.md`<br>`docs/nodes/camera.md` | `release` | `Experimental (0%)` | `Experimental (20%)` | `Experimental (0%)` | No |
| Status and Diagnostics | `linux-companion-app.status-and-diagnostics` | Native Linux app readiness<br>Gateway health/status display<br>Log/transcript opening<br>Doctor/repair affordances<br>Linux tray/status item<br>Runtime status row<br>Desktop-environment integration | native-linux-app-readiness<br>gateway-health-status-display<br>log-transcript-opening<br>doctor-repair-affordances<br>linux-tray-status-item<br>runtime-status-row<br>desktop-environment-integration | `docs/platforms/linux.md`<br>`docs/start/openclaw.md`<br>`docs/gateway/doctor.md` | `release` | `Experimental (5%)` | `Experimental (25%)` | `Experimental (5%)` | No |
#### Windows via WSL2
- Surface id: `windows-via-wsl2`
- Level: M3 Beta
- Rationale: Recommended Windows path with systemd/user-service guidance and boot-chain docs. Promote after repeated install/update scorecards.
- Completeness instructions: `references/completeness/windows-via-wsl2.md`
| Category | Category ID | Features | Coverage IDs | Docs | Profiles | Coverage | Quality | Completeness | LTS |
| --------------------------- | ---------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | ------------- | ------------- | ------------- | --- |
| WSL Setup | `windows-via-wsl2.wsl-setup` | WSL2 + Ubuntu installation<br>Node runtime<br>Linux install flow inside WSL2<br>WSL2 runtime boundary<br>WSL2 network-family requirements<br>Source install and build inside WSL2 | wsl2-ubuntu-installation<br>node-runtime<br>linux-install-flow-inside-wsl2<br>wsl2-runtime-boundary<br>wsl2-network-family-requirements<br>source-install-and-build-inside-wsl2 | `docs/platforms/windows.md`<br>`docs/start/getting-started.md` | `release` | `Beta (76%)` | `Beta (70%)` | `Beta (76%)` | Yes |
| CLI | `windows-via-wsl2.cli` | WSL2 CLI entrypoints<br>openclaw onboard<br>openclaw doctor status and logs<br>openclaw update<br>npm/pnpm/git package-root<br>Managed systemd Gateway restart<br>Service metadata refresh<br>Package-manager caveats | wsl2-cli-entrypoints<br>openclaw-onboard<br>openclaw-doctor-status-and-logs<br>openclaw-update<br>npm-pnpm-git-package-root<br>managed-systemd-gateway-restart<br>service-metadata-refresh<br>package-manager-caveats | `docs/platforms/windows.md`<br>`docs/start/getting-started.md`<br>`docs/install/updating.md`<br>`docs/cli/onboard.md`<br>`docs/cli/doctor.md`<br>`docs/cli/status.md`<br>`docs/cli/logs.md` | `release` | `Beta (76%)` | `Beta (70%)` | `Beta (76%)` | Yes |
| Gateway Service Lifecycle | `windows-via-wsl2.gateway-service-lifecycle` | Onboarded systemd install<br>Gateway service install<br>systemd user unit rendering<br>WSL-aware systemd unavailable hints<br>Doctor service repair<br>WSL user-service linger<br>Systemd availability after Windows boot<br>Windows startup task for WSL<br>Verification before Windows sign-in<br>Clear expectations around PC power | onboarded-systemd-install<br>gateway-service-install<br>systemd-user-unit-rendering<br>wsl-aware-systemd-unavailable-hints<br>doctor-service-repair<br>wsl-user-service-linger<br>systemd-availability-after-windows-boot<br>windows-startup-task-for-wsl<br>verification-before-windows-sign-in<br>clear-expectations-around-pc-power | `docs/platforms/windows.md`<br>`docs/gateway/index.md`<br>`docs/gateway/doctor.md` | `release` | `Alpha (64%)` | `Alpha (66%)` | `Alpha (64%)` | Yes |
| Gateway Access and Exposure | `windows-via-wsl2.gateway-access-and-exposure` | Gateway token/password auth<br>Provider credentials<br>Gateway auth SecretRefs<br>Remote URL credential precedence<br>WSL virtual network<br>Windows portproxy setup<br>Windows Firewall rules<br>Reachable Gateway URLs<br>Loopback and LAN exposure<br>WSL2 IPv4 networking<br>Tailscale remote access | gateway-token-password-auth<br>provider-credentials<br>gateway-auth-secretrefs<br>remote-url-credential-precedence<br>wsl-virtual-network<br>windows-portproxy-setup<br>windows-firewall-rules<br>reachable-gateway-urls<br>loopback-and-lan-exposure<br>wsl2-ipv4-networking<br>tailscale-remote-access | `docs/gateway/authentication.md`<br>`docs/gateway/secrets.md`<br>`docs/gateway/remote.md`<br>`docs/gateway/security/exposure-runbook.md`<br>`docs/platforms/windows.md` | `release` | `Beta (70%)` | `Alpha (65%)` | `Beta (70%)` | Yes |
| Diagnostics and Repair | `windows-via-wsl2.diagnostics-and-repair` | openclaw doctor<br>openclaw status<br>openclaw logs<br>SecretRef<br>WSL/systemd unavailable hints<br>Operator repair guidance after WSL2 service | runtime.codex-plugin.auth<br>runtime.codex-plugin.lifecycle<br>runtime.doctor-repair<br>openclaw-status<br>openclaw-logs<br>secretref<br>wsl-systemd-unavailable-hints<br>operator-repair-guidance-after-wsl2-service | `docs/platforms/windows.md`<br>`docs/cli/status.md`<br>`docs/cli/logs.md`<br>`docs/cli/doctor.md`<br>`docs/gateway/doctor.md` | `release` | `Beta (74%)` | `Beta (72%)` | `Beta (74%)` | Yes |
| Browser and Control UI | `windows-via-wsl2.browser-and-control-ui` | WSL2 Gateway with Windows browser<br>Windows Control UI URL<br>Raw remote CDP to Windows Chrome<br>Host-local Chrome MCP<br>Browser profile cdpUrl<br>Layered diagnostics | wsl2-gateway-with-windows-browser<br>windows-control-ui-url<br>raw-remote-cdp-to-windows-chrome<br>host-local-chrome-mcp<br>browser-profile-cdpurl<br>layered-diagnostics | `docs/tools/browser-wsl2-windows-remote-cdp-troubleshooting.md`<br>`docs/tools/browser.md`<br>`docs/web/control-ui.md` | `release` | `Beta (72%)` | `Beta (70%)` | `Beta (72%)` | No |
#### Native Windows
- Surface id: `native-windows-cli-and-gateway`
- Level: M2 Alpha
- Rationale: Core CLI/Gateway flows work, but docs still recommend WSL2 for the full experience and list native caveats.
- Completeness instructions: `references/completeness/native-windows-cli-and-gateway.md`
| Category | Category ID | Features | Coverage IDs | Docs | Profiles | Coverage | Quality | Completeness | LTS |
| ------------------ | --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | ------------- | ------------- | ------------- | --- |
| CLI | `native-windows-cli-and-gateway.cli` | PowerShell installer<br>Node and package-manager bootstrap<br>npm global install<br>Packaged CLI launcher<br>Windows command shims<br>openclaw onboard<br>Local Gateway config<br>Daemon install flags<br>Native-vs-WSL setup boundary | powershell-installer<br>node-and-package-manager-bootstrap<br>npm-global-install<br>packaged-cli-launcher<br>windows-command-shims<br>openclaw-onboard<br>local-gateway-config<br>daemon-install-flags<br>native-vs-wsl-setup-boundary | `docs/install/index.md`<br>`docs/install/installer.md`<br>`docs/platforms/windows.md`<br>`docs/start/getting-started.md`<br>`docs/cli/onboard.md` | `release` | `Beta (72%)` | `Alpha (66%)` | `Beta (72%)` | Yes |
| Gateway Management | `native-windows-cli-and-gateway.gateway-management` | openclaw gateway<br>Foreground runtime health/readiness<br>Windows-specific restart/signal<br>Unmanaged foreground mode<br>openclaw gateway install<br>Gateway launcher files<br>Scheduled Task runtime status<br>Startup-folder fallback<br>openclaw status<br>Windows service inspection<br>Post-install diagnostics | openclaw-gateway<br>foreground-runtime-health-readiness<br>windows-specific-restart-signal<br>unmanaged-foreground-mode<br>openclaw-gateway-install<br>gateway-launcher-files<br>scheduled-task-runtime-status<br>startup-folder-fallback<br>openclaw-status<br>windows-service-inspection<br>post-install-diagnostics | `docs/platforms/windows.md`<br>`docs/gateway/index.md`<br>`docs/cli/gateway.md`<br>`docs/cli/doctor.md` | `release` | `Alpha (68%)` | `Alpha (62%)` | `Alpha (68%)` | No |
| Networking | `native-windows-cli-and-gateway.networking` | Native Windows host networking<br>netsh interface portproxy<br>Gateway status and probe output<br>Loopback, LAN, and WSL boundary | native-windows-host-networking<br>netsh-interface-portproxy<br>gateway-status-and-probe-output<br>loopback-lan-and-wsl-boundary | `docs/platforms/windows.md`<br>`docs/gateway/index.md`<br>`docs/cli/gateway.md` | `release` | `Alpha (58%)` | `Alpha (56%)` | `Alpha (58%)` | No |
| Updates | `native-windows-cli-and-gateway.updates` | openclaw update on native Windows package<br>Managed Gateway stop/restart<br>Detached update handoff<br>Windows package locks | openclaw-update-on-native-windows-package<br>managed-gateway-stop-restart<br>detached-update-handoff<br>windows-package-locks | `docs/install/updating.md`<br>`docs/ci.md` | `release` | `Beta (74%)` | `Alpha (68%)` | `Beta (74%)` | No |
#### Native Windows companion app
- Surface id: `native-windows-companion-app`
- Level: M0 Planned
- Rationale: Planned only.
- Completeness instructions: `references/completeness/native-windows-companion-app.md`
| Category | Category ID | Features | Coverage IDs | Docs | Profiles | Coverage | Quality | Completeness | LTS |
| ----------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | ------------------- | -------------------- | ------------------- | --- |
| Installation and Updates | `native-windows-companion-app.installation-and-updates` | Official app download<br>MSI/MSIX/App Installer/winget-style packaging<br>Windows architecture handling for x64<br>App release channel | official-app-download<br>msi-msix-app-installer-winget-style-packaging<br>windows-architecture-handling-for-x64<br>app-release-channel | `docs/platforms/windows.md`<br>`docs/install/index.md` | `release` | `Experimental (5%)` | `Experimental (25%)` | `Experimental (5%)` | No |
| Gateway Connection | `native-windows-companion-app.gateway-connection` | App-managed local Gateway attach/start<br>Remote Gateway connection modes<br>Device/node pairing | app-managed-local-gateway-attach-start<br>remote-gateway-connection-modes<br>device-node-pairing | `docs/platforms/windows.md`<br>`docs/gateway/index.md`<br>`docs/gateway/pairing.md`<br>`docs/gateway/remote.md` | `release` | `Experimental (8%)` | `Experimental (35%)` | `Experimental (8%)` | No |
| Chat Sessions | `native-windows-companion-app.chat-sessions` | Native Windows chat window<br>Gateway chat transport | native-windows-chat-window<br>gateway-chat-transport | `docs/platforms/windows.md`<br>`docs/gateway/protocol.md` | `release` | `Experimental (0%)` | `Experimental (25%)` | `Experimental (0%)` | No |
| Status and Repair | `native-windows-companion-app.status-and-repair` | App health states<br>App-specific repair<br>Windows system tray app<br>Status indicators<br>App-specific notification permission | app-health-states<br>app-specific-repair<br>windows-system-tray-app<br>status-indicators<br>app-specific-notification-permission | `docs/platforms/windows.md`<br>`docs/gateway/doctor.md`<br>`docs/gateway/index.md` | `release` | `Experimental (5%)` | `Experimental (35%)` | `Experimental (5%)` | No |
| Desktop Tools and Permissions | `native-windows-companion-app.desktop-tools-and-permissions` | Windows node identity<br>Host command execution<br>Desktop command policy<br>App approval prompts<br>Screen and media capture<br>Canvas host behavior<br>Windows shell integrations<br>App secrets<br>Windows ACL<br>Command approval | windows-node-identity<br>host-command-execution<br>desktop-command-policy<br>app-approval-prompts<br>screen-and-media-capture<br>canvas-host-behavior<br>windows-shell-integrations<br>app-secrets<br>windows-acl<br>command-approval | `docs/platforms/windows.md`<br>`docs/nodes/index.md`<br>`docs/tools/exec.md`<br>`docs/tools/exec-approvals.md`<br>`docs/gateway/security/index.md` | `release` | `Experimental (5%)` | `Experimental (28%)` | `Experimental (5%)` | No |
#### Android app
- Surface id: `android-app`
- Level: M2 Alpha
- Rationale: Public Google Play path exists, but app docs still describe the rebuild as extremely alpha and call out release hardening work.
- Completeness instructions: `references/completeness/android-app.md`
| Category | Category ID | Features | Coverage IDs | Docs | Profiles | Coverage | Quality | Completeness | LTS |
| ---------------- | ------------------------------ | ----------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | --------- | ------------- | ------------- | ------------- | --- |
| Media Capture | `android-app.media-capture` | Camera and media capture | camera-and-media-capture | `docs/platforms/android.md`<br>`docs/nodes/camera.md` | `release` | `Alpha (66%)` | `Alpha (62%)` | `Alpha (66%)` | No |
| Mobile Chat | `android-app.mobile-chat` | Chat tab | chat-tab | `docs/platforms/android.md` | `release` | `Beta (70%)` | `Alpha (66%)` | `Beta (70%)` | No |
| Connection Setup | `android-app.connection-setup` | Gateway discovery | gateway-discovery | `docs/platforms/android.md`<br>`docs/gateway/bonjour.md`<br>`docs/gateway/pairing.md` | `release` | `Alpha (68%)` | `Alpha (64%)` | `Alpha (68%)` | No |
| Distribution | `android-app.distribution` | Public Google Play install path<br>Manual install path<br>Release smoke and startup performance | public-google-play-install-path<br>manual-install-path<br>release-smoke-and-startup-performance | `docs/platforms/android.md` | `release` | `Alpha (60%)` | `Alpha (62%)` | `Alpha (60%)` | No |
| Settings | `android-app.settings` | Settings sheet | settings-sheet | `docs/platforms/android.md` | `release` | `Alpha (64%)` | `Alpha (66%)` | `Alpha (64%)` | No |
| Voice | `android-app.voice` | Voice tab | voice-tab | `docs/platforms/android.md`<br>`docs/nodes/talk.md` | `release` | `Alpha (66%)` | `Alpha (60%)` | `Alpha (66%)` | No |
| Device Runtime | `android-app.device-runtime` | Background reconnect and presence<br>Device command availability | background-reconnect-and-presence<br>device-command-availability | `docs/platforms/android.md`<br>`docs/nodes/troubleshooting.md`<br>`docs/gateway/protocol.md` | `release` | `Alpha (62%)` | `Alpha (55%)` | `Alpha (62%)` | No |
#### iOS app
- Surface id: `ios-app`
- Level: M1 Experimental
- Rationale: Internal preview / super-alpha. TestFlight and relay-backed push flows exist, but no public distribution yet.
- Completeness instructions: `references/completeness/ios-app.md`
| Category | Category ID | Features | Coverage IDs | Docs | Profiles | Coverage | Quality | Completeness | LTS |
| ----------------------------- | --------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ | --------- | -------------------- | -------------------- | -------------------- | --- |
| Media and Sharing | `ios-app.media-and-sharing` | Camera list/snap/clip | camera-list-snap-clip | `docs/platforms/ios.md`<br>`docs/nodes/camera.md` | `release` | `Experimental (42%)` | `Experimental (45%)` | `Experimental (42%)` | No |
| Canvas and Screen | `ios-app.canvas-and-screen` | Canvas present/hide/navigate/eval/snapshot | canvas-present-hide-navigate-eval-snapshot | `docs/platforms/ios.md`<br>`docs/plugins/reference/canvas.md` | `release` | `Experimental (44%)` | `Experimental (47%)` | `Experimental (44%)` | No |
| Chat and Sessions | `ios-app.chat-and-sessions` | Chat sessions and operator controls | chat-sessions-and-operator-controls | `docs/platforms/ios.md`<br>`docs/web/webchat.md`<br>`docs/gateway/protocol.md` | `release` | `Experimental (40%)` | `Experimental (44%)` | `Experimental (40%)` | No |
| Gateway Setup and Diagnostics | `ios-app.gateway-setup-and-diagnostics` | Bonjour/local<br>Manual host/port<br>Gateway connect configuration persistence<br>TLS fingerprint trust prompt<br>Pairing approval<br>Pairing/auth diagnostics for users<br>Settings tab | bonjour-local<br>manual-host-port<br>gateway-connect-configuration-persistence<br>tls-fingerprint-trust-prompt<br>pairing-approval<br>pairing-auth-diagnostics-for-users<br>settings-tab | `docs/platforms/ios.md`<br>`docs/channels/pairing.md` | `release` | `Experimental (41%)` | `Experimental (47%)` | `Experimental (41%)` | No |
| Distribution | `ios-app.distribution` | Internal preview status | internal-preview-status | `docs/platforms/ios.md` | `release` | `Experimental (42%)` | `Experimental (45%)` | `Experimental (42%)` | No |
| Device Commands | `ios-app.device-commands` | Location modes<br>Device command handling | location-modes<br>device-command-handling | `docs/platforms/ios.md`<br>`docs/gateway/protocol.md` | `release` | `Experimental (37%)` | `Experimental (45%)` | `Experimental (37%)` | No |
| Notifications and Background | `ios-app.notifications-and-background` | APNs registration and relay delivery | apns-registration-and-relay-delivery | `docs/platforms/ios.md`<br>`docs/gateway/configuration.md` | `release` | `Experimental (44%)` | `Experimental (46%)` | `Experimental (44%)` | No |
| Voice | `ios-app.voice` | Voice wake | voice-wake | `docs/platforms/ios.md`<br>`docs/nodes/talk.md` | `release` | `Experimental (38%)` | `Experimental (43%)` | `Experimental (38%)` | No |
#### watchOS companion surfaces
- Surface id: `watchos-companion-surfaces`
- Level: M1 Experimental
- Rationale: Source has Watch app/extension surfaces; public docs do not yet present this as a user feature.
- Completeness instructions: `references/completeness/watchos-companion-surfaces.md`
| Category | Category ID | Features | Coverage IDs | Docs | Profiles | Coverage | Quality | Completeness | LTS |
| ------------------------- | ------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------- | --------- | -------------------- | -------------------- | -------------------- | --- |
| Delivery and Recovery | `watchos-companion-surfaces.delivery-and-recovery` | APNs relay/direct registration as it affects<br>Silent push<br>Pending approval recovery IDs<br>Gateway-side iOS exec approval<br>iPhone-side WatchConnectivity transport<br>Watch-side receiver activation<br>Delivery fallback among reachable messages | apns-relay-direct-registration-as-it-affects<br>silent-push<br>pending-approval-recovery-ids<br>gateway-side-ios-exec-approval<br>iphone-side-watchconnectivity-transport<br>watch-side-receiver-activation<br>delivery-fallback-among-reachable-messages | `docs/platforms/ios.md` | `release` | `Experimental (46%)` | `Alpha (60%)` | `Experimental (46%)` | No |
| Exec Approvals | `watchos-companion-surfaces.exec-approvals` | Watch exec approval prompt<br>Watch approval list/detail UI<br>iPhone-side prompt caching | watch-exec-approval-prompt<br>watch-approval-list-detail-ui<br>iphone-side-prompt-caching | `docs/tools/exec-approvals.md`<br>`docs/platforms/ios.md` | `release` | `Alpha (54%)` | `Alpha (64%)` | `Alpha (54%)` | No |
| Distribution and Support | `watchos-companion-surfaces.distribution-and-support` | Watch app<br>Signing/profile variables<br>Public/support status<br>Changelog<br>Release metadata<br>Historical bug/regression themes relevant to scoring | watch-app<br>signing-profile-variables<br>public-support-status<br>changelog<br>release-metadata<br>historical-bug-regression-themes-relevant-to-scoring | `docs/platforms/ios.md` | `release` | `Experimental (38%)` | `Experimental (48%)` | `Experimental (38%)` | No |
| Notifications and Replies | `watchos-companion-surfaces.notifications-and-replies` | watch.status<br>Payload normalization<br>Mirrored iOS notification fallback when watch<br>Watch action buttons from generic prompt<br>Watch-to-iPhone reply payloads<br>iPhone-side dedupe<br>Mirrored iOS notification action | watch-status<br>payload-normalization<br>mirrored-ios-notification-fallback-when-watch<br>watch-action-buttons-from-generic-prompt<br>watch-to-iphone-reply-payloads<br>iphone-side-dedupe<br>mirrored-ios-notification-action | `docs/platforms/ios.md` | `release` | `Experimental (44%)` | `Alpha (57%)` | `Experimental (44%)` | No |
| Watch App UI | `watchos-companion-surfaces.watch-app-ui` | Watch app entry point<br>Generic inbox<br>Persistent watch inbox state | watch-app-entry-point<br>generic-inbox<br>persistent-watch-inbox-state | `docs/platforms/ios.md` | `release` | `Experimental (42%)` | `Alpha (58%)` | `Experimental (42%)` | No |
#### Raspberry Pi / small Linux devices
- Surface id: `raspberry-pi-small-linux-devices`
- Level: M3 Beta
- Rationale: Platform docs exist and Gateway path is Linux-based. Needs hardware-specific release smoke proof to move higher.
- Completeness instructions: `references/completeness/raspberry-pi-small-linux-devices.md`
| Category | Category ID | Features | Coverage IDs | Docs | Profiles | Coverage | Quality | Completeness | LTS |
| --------------------------- | -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | ------------- | ------------- | ------------- | --- |
| Setup and Compatibility | `raspberry-pi-small-linux-devices.setup-and-compatibility` | Hardware and 64-bit OS requirements<br>Node runtime setup<br>OpenClaw install and onboarding<br>First-run verification<br>Supported Pi model selection<br>64-bit ARM boundary<br>Unsupported device guidance<br>Slow-device caveats<br>npm/pnpm/Bun install modes<br>Installer architecture detection<br>Optional ARM binary checks<br>Fallback/build guidance | hardware-and-64-bit-os-requirements<br>node-runtime-setup<br>openclaw-install-and-onboarding<br>first-run-verification<br>supported-pi-model-selection<br>64-bit-arm-boundary<br>unsupported-device-guidance<br>slow-device-caveats<br>npm-pnpm-bun-install-modes<br>installer-architecture-detection<br>optional-arm-binary-checks<br>fallback-build-guidance | `docs/install/raspberry-pi.md`<br>`docs/install/index.md`<br>`docs/help/faq-first-run.md`<br>`docs/help/faq.md`<br>`docs/platforms/linux.md`<br>`docs/install/installer.md` | `release` | `Alpha (55%)` | `Alpha (58%)` | `Alpha (55%)` | No |
| Remote Access and Auth | `raspberry-pi-small-linux-devices.remote-access-and-auth` | Headless API-key auth<br>Gateway shared-secret auth<br>Device pairing approvals<br>SecretRef handling<br>Token drift recovery<br>SSH tunnel dashboard access<br>Tailscale Serve/Funnel<br>Loopback/non-loopback exposure controls<br>Authenticated Control UI access | headless-api-key-auth<br>gateway-shared-secret-auth<br>device-pairing-approvals<br>secretref-handling<br>token-drift-recovery<br>ssh-tunnel-dashboard-access<br>tailscale-serve-funnel<br>loopback-non-loopback-exposure-controls<br>authenticated-control-ui-access | `docs/install/raspberry-pi.md`<br>`docs/gateway/authentication.md`<br>`docs/gateway/secrets.md`<br>`docs/gateway/pairing.md`<br>`docs/cli/devices.md`<br>`docs/gateway/remote.md`<br>`docs/gateway/tailscale.md` | `release` | `Beta (74%)` | `Alpha (68%)` | `Beta (74%)` | No |
| Gateway Runtime | `raspberry-pi-small-linux-devices.gateway-runtime` | Always-on Gateway process<br>Cloud model configuration<br>Channel startup<br>Gateway health/status<br>User service install<br>linger/boot persistence<br>Service drop-ins<br>Restart tuning<br>Status/log inspection<br>Backup/restore | always-on-gateway-process<br>cloud-model-configuration<br>channel-startup<br>gateway-health-status<br>user-service-install<br>linger-boot-persistence<br>service-drop-ins<br>restart-tuning<br>status-log-inspection<br>backup-restore | `docs/gateway/index.md`<br>`docs/cli/gateway.md`<br>`docs/install/raspberry-pi.md`<br>`docs/platforms/linux.md`<br>`docs/vps.md` | `release` | `Beta (78%)` | `Beta (72%)` | `Beta (78%)` | No |
| Performance and Diagnostics | `raspberry-pi-small-linux-devices.performance-and-diagnostics` | Swap and low-RAM tuning<br>USB SSD guidance<br>Compile cache/no-respawn settings<br>OOM/performance troubleshooting<br>Diagnostics bundles | swap-and-low-ram-tuning<br>usb-ssd-guidance<br>compile-cache-no-respawn-settings<br>oom-performance-troubleshooting<br>diagnostics-bundles | `docs/install/raspberry-pi.md`<br>`docs/platforms/linux.md`<br>`docs/gateway/health.md`<br>`docs/gateway/diagnostics.md` | `release` | `Beta (75%)` | `Alpha (69%)` | `Beta (75%)` | No |
#### Docker / Podman hosting
- Surface id: `docker-podman-hosting`
- Level: M3 Beta
- Rationale: Install docs exist and are common deployment paths. Promote after recurring release smoke captures upgrade and volume behavior.
- Completeness instructions: `references/completeness/docker-podman-hosting.md`
| Category | Category ID | Features | Coverage IDs | Docs | Profiles | Coverage | Quality | Completeness | LTS |
| ---------------------------- | ---------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | -------------- | ------------- | -------------- | --- |
| Container Setup | `docker-podman-hosting.container-setup` | Local Image Setup Script<br>Docker Compose gateway<br>First-run onboarding<br>Docker-only first-run notes<br>Podman setup scripts and Quadlet template<br>Rootless Podman image setup | local-image-setup-script<br>docker-compose-gateway<br>first-run-onboarding<br>docker-only-first-run-notes<br>podman-setup-scripts-and-quadlet-template<br>rootless-podman-image-setup | `docs/install/docker.md`<br>`docs/install/podman.md` | `release` | `Beta (74%)` | `Beta (76%)` | `Beta (74%)` | No |
| Container Operations | `docker-podman-hosting.container-operations` | Host CLI routing into running Docker/Podman<br>Container Targeting<br>Container update/rebuild/restart guidance for Docker<br>Docker Compose<br>Gateway token generation<br>Ownership<br>Docker Compose<br>Container health endpoints<br>Provider/VPS Docker hosting docs<br>Docker VM persistence/update guidance<br>Operator-facing update | host-cli-routing-into-running-docker-podman<br>container-targeting<br>container-update-rebuild-restart-guidance-for-docker<br>docker-compose<br>gateway-token-generation<br>ownership<br>docker-compose-2<br>container-health-endpoints<br>provider-vps-docker-hosting-docs<br>docker-vm-persistence-update-guidance<br>operator-facing-update | `docs/install/podman.md`<br>`docs/install/docker-vm-runtime.md`<br>`docs/install/docker.md`<br>`docs/install/hetzner.md`<br>`docs/install/hostinger.md` | `release` | `Beta (76%)` | `Beta (70%)` | `Beta (76%)` | No |
| Image Release and Validation | `docker-podman-hosting.image-release-and-validation` | Root Dockerfile build stages<br>Docker release workflow<br>Docker E2E package artifact generation<br>Docker E2E plan/scheduler scripts<br>Release-path install | root-dockerfile-build-stages<br>docker-release-workflow<br>docker-e2e-package-artifact-generation<br>docker.e2e<br>harness.qa-lab<br>telemetry.prometheus<br>release-path-install | `docs/install/docker.md`<br>`docs/install/docker-vm-runtime.md`<br>`docs/reference/full-release-validation.md` | `release` | `Stable (84%)` | `Beta (78%)` | `Stable (84%)` | No |
| Agent Sandbox and Tooling | `docker-podman-hosting.agent-sandbox-and-tooling` | Docker gateway setup<br>Docker-backed agent sandbox support<br>Container image dependency baking | docker-gateway-setup<br>docker-backed-agent-sandbox-support<br>container-image-dependency-baking | `docs/install/docker.md`<br>`docs/install/docker-vm-runtime.md` | `release` | `Beta (75%)` | `Alpha (68%)` | `Beta (75%)` | No |
#### Kubernetes hosting
- Surface id: `kubernetes-hosting`
- Level: M2 Alpha
- Rationale: Kubernetes hosting is a distinct Kustomize-based cluster deployment path. Current scoring shows a real minimal deployment path with gaps around Kubernetes-specific CI, ingress/TLS/NetworkPolicy packaging, backup/restore, and production exposure hardening.
- Completeness instructions: `references/completeness/kubernetes-hosting.md`
| Category | Category ID | Features | Coverage IDs | Docs | Profiles | Coverage | Quality | Completeness | LTS |
| ------------------------- | ---------------------------------------------- | -------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | --------- | -------------------- | ------------ | -------------- | --- |
| Deployment Setup | `kubernetes-hosting.deployment-setup` | Kustomize packaging<br>Cluster prerequisites<br>Quick deploy<br>Manifest apply<br>Kind validation | kustomize-packaging<br>cluster-prerequisites<br>quick-deploy<br>manifest-apply<br>kind-validation | `docs/install/kubernetes.md`<br>`docs/install/index.md` | `release` | `Alpha (55%)` | `Beta (76%)` | `Stable (84%)` | No |
| Configuration and Secrets | `kubernetes-hosting.configuration-and-secrets` | Agent instructions<br>Gateway config<br>Provider secrets<br>Secret rotation<br>Image and namespace | agent-instructions<br>gateway-config<br>provider-secrets<br>secret-rotation<br>image-and-namespace | `docs/install/kubernetes.md`<br>`docs/gateway/secrets.md`<br>`docs/help/environment.md` | `release` | `Alpha (52%)` | `Beta (74%)` | `Beta (76%)` | No |
| Access and Exposure | `kubernetes-hosting.access-and-exposure` | Port-forward access<br>Service endpoint<br>Ingress exposure<br>Auth and TLS<br>Localhost posture | port-forward-access<br>service-endpoint<br>ingress-exposure<br>auth-and-tls<br>localhost-posture | `docs/install/kubernetes.md`<br>`docs/gateway/authentication.md`<br>`docs/gateway/remote.md`<br>`docs/gateway/security/exposure-runbook.md` | `release` | `Experimental (43%)` | `Beta (72%)` | `Alpha (58%)` | No |
| Cluster Lifecycle | `kubernetes-hosting.cluster-lifecycle` | Resource layout<br>State persistence<br>Redeploy<br>Teardown<br>Security context | resource-layout<br>state-persistence<br>redeploy<br>teardown<br>security-context | `docs/install/kubernetes.md`<br>`docs/gateway/index.md` | `release` | `Alpha (50%)` | `Beta (78%)` | `Beta (77%)` | No |
#### Nix install path
- Surface id: `nix-install-path`
- Level: M1 Experimental
- Rationale: Optional install flow. Needs clearer support promise before alpha/beta promotion.
- Completeness instructions: `references/completeness/nix-install-path.md`
| Category | Category ID | Features | Coverage IDs | Docs | Profiles | Coverage | Quality | Completeness | LTS |
| -------------------------- | --------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | --------- | -------------------- | -------------------- | -------------------- | --- |
| Install Handoff | `nix-install-path.install-handoff` | Nix install overview<br>nix-openclaw source-of-truth<br>Install discoverability<br>Verification handoff | nix-install-overview<br>nix-openclaw-source-of-truth<br>install-discoverability<br>verification-handoff | `docs/install/nix.md`<br>`docs/install/index.md`<br>`docs/start/docs-directory.md` | `release` | `Experimental (25%)` | `Experimental (45%)` | `Experimental (25%)` | No |
| Plugin Lifecycle | `nix-install-path.plugin-lifecycle` | Lifecycle command refusal<br>Declarative plugin selection<br>Nix-store plugin loading<br>Hardlink safety | lifecycle-command-refusal<br>declarative-plugin-selection<br>nix-store-plugin-loading<br>hardlink-safety | `docs/plugins/manage-plugins.md`<br>`docs/tools/plugin.md`<br>`docs/install/nix.md` | `release` | `Experimental (40%)` | `Experimental (35%)` | `Experimental (40%)` | No |
| Activation and App UX | `nix-install-path.activation-and-app-ux` | Environment activation<br>macOS defaults activation<br>Runtime Nix-mode detection<br>Stable Nix defaults<br>Managed-by-Nix banner<br>Read-only config controls<br>Onboarding skip | environment-activation<br>macos-defaults-activation<br>runtime-nix-mode-detection<br>stable-nix-defaults<br>managed-by-nix-banner<br>read-only-config-controls<br>onboarding-skip | `docs/install/nix.md` | `release` | `Experimental (42%)` | `Alpha (50%)` | `Experimental (42%)` | No |
| Config and State | `nix-install-path.config-and-state` | Immutable config guard<br>Config writer refusal<br>Agent-first Nix edits<br>Explicit config path<br>Writable state directory<br>Immutable-store config support<br>State integrity checks | immutable-config-guard<br>config-writer-refusal<br>agent-first-nix-edits<br>explicit-config-path<br>writable-state-directory<br>immutable-store-config-support<br>state-integrity-checks | `docs/install/nix.md`<br>`docs/cli/setup.md`<br>`docs/help/environment.md` | `release` | `Experimental (45%)` | `Alpha (50%)` | `Experimental (45%)` | No |
| Service Runtime and Guards | `nix-install-path.service-runtime-and-guards` | Nix profile PATH discovery<br>Profile precedence<br>Service PATH fallback<br>Trusted binary boundaries<br>Setup write refusal<br>Doctor repair refusal<br>Update handoff<br>Service lifecycle handoff | nix-profile-path-discovery<br>profile-precedence<br>service-path-fallback<br>trusted-binary-boundaries<br>setup-write-refusal<br>doctor-repair-refusal<br>update-handoff<br>service-lifecycle-handoff | `docs/install/nix.md`<br>`docs/cli/setup.md`<br>`docs/cli/doctor.md`<br>`docs/cli/update.md` | `release` | `Experimental (38%)` | `Experimental (45%)` | `Experimental (38%)` | No |
### Channel
#### Discord
- Surface id: `discord`
- Level: M4 Stable
- Rationale: Deep docs and broad feature coverage. Voice/delegation paths should stay separately scored as beta/alpha.
- Completeness instructions: `references/completeness/discord.md`
| Category | Category ID | Features | Coverage IDs | Docs | Profiles | Coverage | Quality | Completeness | LTS |
| --------------------------------- | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | ------------- | ------------- | ------------- | --- |
| Channel Setup and Operations | `discord.channel-setup-and-operations` | Application and bot setup<br>Token and application ID configuration<br>Setup wizard and account inspection<br>Status, doctor, and intent checks<br>Multi-account bot configuration<br>Account monitor startup<br>Gateway WebSocket lifecycle<br>Reconnect and heartbeat handling<br>Rate limits and gateway metadata<br>Status, probe, and health-monitor recovery | application-and-bot-setup<br>token-and-application-id-configuration<br>setup-wizard-and-account-inspection<br>status-doctor-and-intent-checks<br>multi-account-bot-configuration<br>account-monitor-startup<br>gateway-websocket-lifecycle<br>reconnect-and-heartbeat-handling<br>rate-limits-and-gateway-metadata<br>status-probe-and-health-monitor-recovery | `docs/channels/discord.md`<br>`docs/plugins/reference/discord.md`<br>`docs/install/fly.md`<br>`docs/tools/slash-commands.md`<br>`docs/gateway/health.md`<br>`docs/cli/channels.md`<br>`docs/gateway/config-channels.md` | `release` | `Beta (74%)` | `Beta (71%)` | `Beta (74%)` | Yes |
| Access and Identity | `discord.access-and-identity` | DM policy modes<br>Allowlist inheritance<br>Pairing-code approval<br>Sender authorization<br>Access-group authorization<br>Group DM authorization | dm-policy-modes<br>allowlist-inheritance<br>pairing-code-approval<br>sender-authorization<br>access-group-authorization<br>group-dm-authorization | `docs/channels/discord.md`<br>`docs/channels/pairing.md`<br>`docs/channels/access-groups.md`<br>`docs/channels/groups.md` | `release` | `Beta (74%)` | `Beta (72%)` | `Beta (74%)` | Yes |
| Conversation Routing and Delivery | `discord.conversation-routing-and-delivery` | Guild and channel admission<br>Mention gating<br>Session key isolation<br>Configured and runtime routing<br>Inbound context visibility<br>Forum and media-channel thread posts<br>Thread actions<br>Target parsing<br>Thread context resolution<br>Thread-bound session routing<br>ACP agent routing<br>Routing lifecycle | guild-and-channel-admission<br>mention-gating<br>session-key-isolation<br>configured-and-runtime-routing<br>inbound-context-visibility<br>forum-and-media-channel-thread-posts<br>thread-actions<br>target-parsing<br>thread-context-resolution<br>thread-bound-session-routing<br>acp-agent-routing<br>routing-lifecycle | `docs/channels/discord.md`<br>`docs/channels/channel-routing.md`<br>`docs/channels/groups.md`<br>`docs/channels/access-groups.md`<br>`docs/tools/acp-agents.md`<br>`docs/tools/subagents.md` | `release` | `Beta (74%)` | `Beta (72%)` | `Beta (74%)` | Yes |
| Media and Rich Content | `discord.media-and-rich-content` | Media and Rich Content | media-and-rich-content | `docs/channels/discord.md` | `release` | `Beta (74%)` | `Beta (72%)` | `Beta (74%)` | Yes |
| Native Controls and Approvals | `discord.native-controls-and-approvals` | Native slash command registration<br>Native slash command execution<br>Model Picker Commands<br>Components v2 messages<br>Callback TTL | native-slash-command-registration<br>native-slash-command-execution<br>model-picker-commands<br>components-v2-messages<br>callback-ttl | `docs/channels/discord.md`<br>`docs/tools/slash-commands.md` | `release` | `Alpha (58%)` | `Beta (72%)` | `Alpha (58%)` | No |
| Realtime Voice and Calls | `discord.realtime-voice-and-calls` | Voice Channel Lifecycle<br>Auto-join and follow-users<br>Realtime voice modes<br>Wake, barge-in, and echo handling<br>Voice codec and DAVE recovery | voice-channel-lifecycle<br>auto-join-and-follow-users<br>realtime-voice-modes<br>wake-barge-in-and-echo-handling<br>voice-codec-and-dave-recovery | `docs/channels/discord.md`<br>`docs/providers/openai.md`<br>`docs/providers/elevenlabs.md`<br>`docs/concepts/qa-e2e-automation.md`<br>`docs/gateway/config-channels.md` | `release` | `Beta (74%)` | `Alpha (66%)` | `Beta (74%)` | No |
#### Telegram
- Surface id: `telegram`
- Level: M3 Beta
- Rationale: Core channel is mature enough for regular use, but high-variance UX and media edge cases need recurring scenario proof.
- Completeness instructions: `references/completeness/telegram.md`
| Category | Category ID | Features | Coverage IDs | Docs | Profiles | Coverage | Quality | Completeness | LTS |
| --------------------------------- | -------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------- | ------------ | ------------- | ------------ | --- |
| Channel Setup and Operations | `telegram.channel-setup-and-operations` | BotFather token creation<br>TELEGRAM_BOT_TOKEN<br>Setup wizard credential capture<br>Startup getMe<br>Doctor/status surfacing<br>Named account configuration<br>CLI/message-tool targets<br>Directory adapters<br>Channel status<br>Account-scoped outbound | botfather-token-creation<br>telegram-bot-token<br>setup-wizard-credential-capture<br>startup-getme<br>doctor-status-surfacing<br>named-account-configuration<br>cli-message-tool-targets<br>directory-adapters<br>channel-status<br>account-scoped-outbound | `docs/channels/telegram.md`<br>`docs/gateway/config-channels.md`<br>`docs/cli/channels.md` | `release` | `Beta (76%)` | `Beta (70%)` | `Beta (76%)` | Yes |
| Access and Identity | `telegram.access-and-identity` | dmPolicy modes<br>Pairing-code approval<br>Numeric Telegram user ID normalization with telegram<br>allowFrom<br>Unauthorized DM<br>Group allowlists<br>Supergroup negative chat IDs<br>Forum topic session keys<br>ACP topic routing<br>Session key construction | dmpolicy-modes<br>pairing-code-approval<br>numeric-telegram-user-id-normalization-with-telegram<br>allowfrom<br>unauthorized-dm<br>group-allowlists<br>supergroup-negative-chat-ids<br>forum-topic-session-keys<br>acp-topic-routing<br>session-key-construction | `docs/channels/telegram.md`<br>`docs/channels/pairing.md`<br>`docs/channels/access-groups.md`<br>`docs/channels/groups.md`<br>`docs/concepts/multi-agent.md` | `release` | `Beta (76%)` | `Alpha (68%)` | `Beta (76%)` | Yes |
| Conversation Routing and Delivery | `telegram.conversation-routing-and-delivery` | Conversation Routing and Delivery | conversation-routing-and-delivery | `docs/channels/telegram.md`<br>`docs/channels/groups.md`<br>`docs/concepts/multi-agent.md` | `release` | `Beta (74%)` | `Alpha (68%)` | `Beta (74%)` | Yes |
| Media and Rich Content | `telegram.media-and-rich-content` | Media and Rich Content | media-and-rich-content | `docs/channels/telegram.md`<br>`docs/channels/location.md` | `release` | `Beta (74%)` | `Beta (72%)` | `Beta (74%)` | Yes |
| Native Controls and Approvals | `telegram.native-controls-and-approvals` | Inline keyboard rendering<br>Exec approvals in DMs<br>Message actions<br>Action capability discovery<br>Native setMyCommands startup sync<br>Command name/description normalization<br>Built-in commands<br>Command authorization in DMs<br>Model buttons | inline-keyboard-rendering<br>exec-approvals-in-dms<br>message-actions<br>action-capability-discovery<br>native-setmycommands-startup-sync<br>command-name-description-normalization<br>built-in-commands<br>command-authorization-in-dms<br>model-buttons | `docs/channels/telegram.md`<br>`docs/tools/exec-approvals.md`<br>`docs/tools/reactions.md` | `release` | `Beta (74%)` | `Beta (72%)` | `Beta (74%)` | Yes |
#### WhatsApp
- Surface id: `whatsapp`
- Level: M3 Beta
- Rationale: Core path is important and documented; upstream Baileys/session volatility keeps it below Stable.
- Completeness instructions: `references/completeness/whatsapp.md`
| Category | Category ID | Features | Coverage IDs | Docs | Profiles | Coverage | Quality | Completeness | LTS |
| --------------------------------- | -------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | ------------ | -------------- | ------------ | --- |
| Channel Setup and Operations | `whatsapp.channel-setup-and-operations` | Official @openclaw/whatsapp plugin metadata<br>openclaw plugin install whatsapp<br>Channel config schema<br>Baileys socket lifecycle<br>Operator troubleshooting | official-openclaw-whatsapp-plugin-metadata<br>openclaw-plugin-install-whatsapp<br>channel-config-schema<br>baileys-socket-lifecycle<br>operator-troubleshooting | `docs/channels/whatsapp.md`<br>`docs/gateway/config-channels.md`<br>`docs/plugins/reference/whatsapp.md`<br>`docs/concepts/qa-e2e-automation.md`<br>`docs/gateway/doctor.md` | `release` | `Beta (74%)` | `Beta (72%)` | `Beta (74%)` | No |
| Access and Identity | `whatsapp.access-and-identity` | QR login<br>Baileys multi-file auth persistence<br>DM pairing challenge<br>Multi-account/default-account resolution<br>Direct-message dmPolicy<br>Sender identity extraction<br>Privacy controls for plugin hooks | qr-login<br>baileys-multi-file-auth-persistence<br>dm-pairing-challenge<br>multi-account-default-account-resolution<br>direct-message-dmpolicy<br>sender-identity-extraction<br>privacy-controls-for-plugin-hooks | `docs/channels/whatsapp.md`<br>`docs/gateway/config-channels.md`<br>`docs/concepts/qa-e2e-automation.md`<br>`docs/channels/pairing.md` | `release` | `Beta (76%)` | `Beta (72%)` | `Beta (76%)` | No |
| Conversation Routing and Delivery | `whatsapp.conversation-routing-and-delivery` | Group allowlists<br>Group session keys<br>Outbound text sends<br>Provider-accepted receipts | group-allowlists<br>group-session-keys<br>outbound-text-sends<br>provider-accepted-receipts | `docs/channels/whatsapp.md`<br>`docs/channels/group-messages.md` | `release` | `Beta (76%)` | `Beta (72%)` | `Beta (76%)` | No |
| Media and Rich Content | `whatsapp.media-and-rich-content` | Inbound media download<br>Outbound image | inbound-media-download<br>outbound-image | `docs/channels/whatsapp.md` | `release` | `Beta (76%)` | `Stable (80%)` | `Beta (76%)` | No |
| Native Controls and Approvals | `whatsapp.native-controls-and-approvals` | Native exec<br>Approver target resolution | native-exec<br>approver-target-resolution | `docs/channels/whatsapp.md` | `release` | `Beta (78%)` | `Stable (84%)` | `Beta (78%)` | No |
#### Slack
- Surface id: `slack`
- Level: M3 Beta
- Rationale: First-class channel docs and routing surface. Needs workspace install/admin scenario scorecards.
- Completeness instructions: `references/completeness/slack.md`
| Category | Category ID | Features | Coverage IDs | Docs | Profiles | Coverage | Quality | Completeness | LTS |
| --------------------------------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------- | ------------- | ------------- | ------------- | --- |
| Channel Setup and Operations | `slack.channel-setup-and-operations` | App Install<br>Slack app credentials<br>Manifest<br>Scopes<br>Channel status diagnostics<br>Slack account status<br>Operator Repair<br>Socket<br>HTTP transport<br>Runtime Lifecycle | app-install<br>slack-app-credentials<br>manifest<br>scopes<br>channel-status-diagnostics<br>slack-account-status<br>operator-repair<br>socket<br>http-transport<br>runtime-lifecycle | `docs/channels/slack.md`<br>`docs/plugins/reference/slack.md`<br>`docs/gateway/secrets.md`<br>`docs/concepts/qa-e2e-automation.md`<br>`docs/channels/troubleshooting.md` | `release` | `Beta (74%)` | `Alpha (68%)` | `Beta (74%)` | Yes |
| Access and Identity | `slack.access-and-identity` | Access and Identity | access-and-identity | `docs/channels/slack.md`<br>`docs/channels/pairing.md` | `release` | `Beta (74%)` | `Beta (70%)` | `Beta (74%)` | Yes |
| Conversation Routing and Delivery | `slack.conversation-routing-and-delivery` | Channel allowlists<br>Thread routing<br>Session Isolation<br>DM Pairing<br>Sender Authorization | channel-allowlists<br>thread-routing<br>session-isolation<br>dm-pairing<br>sender-authorization | `docs/channels/slack.md`<br>`docs/channels/bot-loop-protection.md`<br>`docs/channels/pairing.md` | `release` | `Alpha (64%)` | `Alpha (66%)` | `Alpha (64%)` | Yes |
| Media and Rich Content | `slack.media-and-rich-content` | Media and Rich Content | media-and-rich-content | `docs/channels/slack.md`<br>`docs/concepts/qa-e2e-automation.md` | `release` | `Alpha (64%)` | `Alpha (66%)` | `Alpha (64%)` | Yes |
| Native Controls and Approvals | `slack.native-controls-and-approvals` | Slash Commands<br>Native Command Routing<br>Interactive Replies<br>App Home<br>Assistant Events<br>Native Approvals<br>Actions<br>Security-sensitive Ops | slash-commands<br>native-command-routing<br>interactive-replies<br>app-home<br>assistant-events<br>native-approvals<br>actions<br>security-sensitive-ops | `docs/channels/slack.md`<br>`docs/tools/slash-commands.md`<br>`docs/tools/exec-approvals.md` | `release` | `Beta (72%)` | `Beta (70%)` | `Beta (72%)` | Yes |
#### iMessage / BlueBubbles
- Surface id: `imessage-bluebubbles`
- Level: M3 Beta
- Rationale: Supported iMessage runs through imsg on a signed-in macOS Messages host; legacy BlueBubbles configs require migration. Keep macOS permissions, SSH wrapper, SIP/private API, and migration caveats visible.
- Completeness instructions: `references/completeness/imessage-bluebubbles.md`
| Category | Category ID | Features | Coverage IDs | Docs | Profiles | Coverage | Quality | Completeness | LTS |
| --------------------------------- | -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | ------------- | ------------ | ------------- | --- |
| Channel Setup and Operations | `imessage-bluebubbles.channel-setup-and-operations` | Translate legacy config<br>Cut over safely<br>Handle migration caveats<br>Run local imsg<br>Run through SSH wrapper<br>Grant macOS permissions<br>Probe runtime health<br>Account setup prompts<br>Account status checks<br>Doctor repair checks<br>Account Config | translate-legacy-config<br>cut-over-safely<br>handle-migration-caveats<br>run-local-imsg<br>run-through-ssh-wrapper<br>grant-macos-permissions<br>probe-runtime-health<br>account-setup-prompts<br>account-status-checks<br>doctor-repair-checks<br>account-config | `docs/announcements/bluebubbles-imessage.md`<br>`docs/channels/imessage-from-bluebubbles.md`<br>`docs/gateway/config-channels.md`<br>`docs/channels/imessage.md` | `release` | `Alpha (62%)` | `Beta (70%)` | `Alpha (62%)` | No |
| Access and Identity | `imessage-bluebubbles.access-and-identity` | Authorize direct senders<br>Route direct conversations<br>Bind ACP sessions<br>Group Policy<br>Mentions<br>System Prompts | authorize-direct-senders<br>route-direct-conversations<br>bind-acp-sessions<br>group-policy<br>mentions<br>system-prompts | `docs/channels/imessage.md`<br>`docs/channels/imessage-from-bluebubbles.md`<br>`docs/gateway/config-channels.md` | `release` | `Beta (75%)` | `Beta (74%)` | `Beta (75%)` | No |
| Conversation Routing and Delivery | `imessage-bluebubbles.conversation-routing-and-delivery` | Watch live messages<br>Coalesce split-send DMs<br>Replay missed messages<br>Seed conversation history | watch-live-messages<br>coalesce-split-send-dms<br>replay-missed-messages<br>seed-conversation-history | `docs/channels/imessage.md` | `release` | `Beta (74%)` | `Beta (73%)` | `Beta (74%)` | No |
| Media and Rich Content | `imessage-bluebubbles.media-and-rich-content` | Media<br>Attachments<br>Remote Fetch<br>Chunking<br>Native Actions<br>Private API<br>Message Tool | media<br>attachments<br>remote-fetch<br>chunking<br>native-actions<br>private-api<br>message-tool | `docs/channels/imessage.md`<br>`docs/channels/imessage-from-bluebubbles.md`<br>`docs/gateway/config-channels.md` | `release` | `Beta (73%)` | `Beta (71%)` | `Beta (73%)` | No |
| Native Controls and Approvals | `imessage-bluebubbles.native-controls-and-approvals` | Native Approvals<br>Reactions<br>Operator Control | native-approvals<br>reactions<br>operator-control | `docs/channels/imessage.md` | `release` | `Beta (73%)` | `Beta (71%)` | `Beta (73%)` | No |
#### Signal
- Surface id: `signal`
- Level: M2 Alpha
- Rationale: Supported channel docs exist; needs stronger install and reconnect proof.
- Completeness instructions: `references/completeness/signal.md`
| Category | Category ID | Features | Coverage IDs | Docs | Profiles | Coverage | Quality | Completeness | LTS |
| --------------------------------- | ------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- | --------- | ------------- | ------------- | ------------- | --- |
| Channel Setup and Operations | `signal.channel-setup-and-operations` | QR link setup<br>SMS registration<br>Installer and binary setup<br>Container account provisioning<br>Status probes<br>Setup diagnostics<br>Account safety guardrails | qr-link-setup<br>sms-registration<br>installer-and-binary-setup<br>container-account-provisioning<br>status-probes<br>setup-diagnostics<br>account-safety-guardrails | `docs/channels/signal.md`<br>`docs/plugins/reference/signal.md` | `release` | `Alpha (55%)` | `Alpha (58%)` | `Alpha (55%)` | No |
| Access and Identity | `signal.access-and-identity` | DM pairing<br>DM allowlists<br>Sender identity normalization<br>Group allowlists<br>Mention gates<br>Pending group history | dm-pairing<br>dm-allowlists<br>sender-identity-normalization<br>group-allowlists<br>mention-gates<br>pending-group-history | `docs/channels/signal.md` | `release` | `Beta (70%)` | `Alpha (66%)` | `Beta (70%)` | No |
| Conversation Routing and Delivery | `signal.conversation-routing-and-delivery` | Conversation Routing and Delivery | conversation-routing-and-delivery | `docs/channels/signal.md` | `release` | `Beta (70%)` | `Alpha (66%)` | `Beta (70%)` | No |
| Media and Rich Content | `signal.media-and-rich-content` | Text delivery targets<br>Media delivery and limits<br>Typing and read receipts<br>Styled/chunked output<br>Reaction action discovery<br>Add/remove reactions<br>Group reaction targeting | text-delivery-targets<br>media-delivery-and-limits<br>typing-and-read-receipts<br>styled-chunked-output<br>reaction-action-discovery<br>add-remove-reactions<br>group-reaction-targeting | `docs/channels/signal.md` | `release` | `Beta (70%)` | `Alpha (68%)` | `Beta (70%)` | No |
| Native Controls and Approvals | `signal.native-controls-and-approvals` | Native approval routing<br>Reaction approval responses<br>Approver targeting | native-approval-routing<br>reaction-approval-responses<br>approver-targeting | `docs/channels/signal.md` | `release` | `Alpha (65%)` | `Alpha (68%)` | `Alpha (65%)` | No |
#### Google Chat
- Surface id: `google-chat`
- Level: M2 Alpha
- Rationale: Documented channel, but enterprise/admin setup raises maturity risk.
- Completeness instructions: `references/completeness/google-chat.md`
| Category | Category ID | Features | Coverage IDs | Docs | Profiles | Coverage | Quality | Completeness | LTS |
| --------------------------------- | ----------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | ------------- | ------------- | ------------- | --- |
| Channel Setup and Operations | `google-chat.channel-setup-and-operations` | Google Cloud project setup<br>Chat app configuration<br>Service account setup<br>Webhook audience and path<br>Workspace visibility and app status<br>Guided channel setup<br>Account resolution<br>Service account SecretRefs<br>Env file and inline credentials<br>Channel status and probes<br>Directory and mutable-id diagnostics<br>NPM and ClawHub install<br>Plugin docs and catalog routing<br>Channel aliases and labels<br>Operator status UI<br>Install/update metadata | google-cloud-project-setup<br>chat-app-configuration<br>service-account-setup<br>webhook-audience-and-path<br>workspace-visibility-and-app-status<br>guided-channel-setup<br>account-resolution<br>service-account-secretrefs<br>env-file-and-inline-credentials<br>channel-status-and-probes<br>directory-and-mutable-id-diagnostics<br>npm-and-clawhub-install<br>plugin-docs-and-catalog-routing<br>channel-aliases-and-labels<br>operator-status-ui<br>install-update-metadata | `docs/channels/googlechat.md`<br>`docs/plugins/reference/googlechat.md`<br>`docs/gateway/config-channels.md`<br>`docs/start/wizard-cli-reference.md`<br>`docs/gateway/secrets.md`<br>`docs/reference/secretref-credential-surface.md`<br>`docs/gateway/health.md`<br>`docs/plugins/plugin-inventory.md`<br>`docs/channels/index.md`<br>`docs/docs.json` | `release` | `Alpha (64%)` | `Alpha (62%)` | `Alpha (64%)` | No |
| Access and Identity | `google-chat.access-and-identity` | DM pairing approval<br>Sender allowlists<br>Google Chat identity matching<br>Direct session routing<br>Pairing diagnostics<br>Space allowlists<br>Mention gating<br>Sender access groups<br>Group session isolation<br>Bot-loop protection<br>Space diagnostics | dm-pairing-approval<br>sender-allowlists<br>google-chat-identity-matching<br>direct-session-routing<br>pairing-diagnostics<br>space-allowlists<br>mention-gating<br>sender-access-groups<br>group-session-isolation<br>bot-loop-protection<br>space-diagnostics | `docs/channels/googlechat.md`<br>`docs/channels/pairing.md`<br>`docs/channels/access-groups.md`<br>`docs/gateway/config-channels.md`<br>`docs/channels/bot-loop-protection.md`<br>`docs/channels/channel-routing.md` | `release` | `Alpha (58%)` | `Alpha (55%)` | `Alpha (58%)` | No |
| Conversation Routing and Delivery | `google-chat.conversation-routing-and-delivery` | Conversation Routing and Delivery | conversation-routing-and-delivery | `docs/channels/googlechat.md`<br>`docs/channels/bot-loop-protection.md`<br>`docs/channels/access-groups.md`<br>`docs/channels/channel-routing.md` | `release` | `Alpha (55%)` | `Alpha (50%)` | `Alpha (55%)` | No |
| Media and Rich Content | `google-chat.media-and-rich-content` | Media and Rich Content | media-and-rich-content | `docs/channels/googlechat.md`<br>`docs/cli/message.md`<br>`docs/nodes/media-understanding.md`<br>`docs/reference/secretref-credential-surface.md` | `release` | `Alpha (55%)` | `Alpha (50%)` | `Alpha (55%)` | No |
| Native Controls and Approvals | `google-chat.native-controls-and-approvals` | Inbound attachments<br>Outbound media replies<br>Message upload action<br>Media source and size controls<br>Media receipts and thread placement<br>Text send action<br>Upload-file action<br>Reaction actions<br>Action capability gates<br>Approval sender matching<br>Thread-aware replies<br>Streaming and chunked replies<br>Typing placeholder lifecycle<br>Message-tool current-source replies<br>NO_REPLY cleanup<br>Markdown/text rendering | inbound-attachments<br>outbound-media-replies<br>message-upload-action<br>media-source-and-size-controls<br>media-receipts-and-thread-placement<br>text-send-action<br>upload-file-action<br>reaction-actions<br>action-capability-gates<br>approval-sender-matching<br>thread-aware-replies<br>streaming-and-chunked-replies<br>typing-placeholder-lifecycle<br>message-tool-current-source-replies<br>no-reply-cleanup<br>markdown-text-rendering | `docs/channels/googlechat.md`<br>`docs/cli/message.md`<br>`docs/nodes/media-understanding.md`<br>`docs/reference/secretref-credential-surface.md`<br>`docs/tools/reactions.md`<br>`docs/tools/slash-commands.md`<br>`docs/gateway/config-agents.md`<br>`docs/concepts/message-lifecycle-refactor.md` | `release` | `Alpha (55%)` | `Alpha (50%)` | `Alpha (55%)` | No |
#### Matrix
- Surface id: `matrix`
- Level: M2 Alpha
- Rationale: Supported via bundled plugin. Needs bridge, auth, and room lifecycle scorecards.
- Completeness instructions: `references/completeness/matrix.md`
| Category | Category ID | Features | Coverage IDs | Docs | Profiles | Coverage | Quality | Completeness | LTS |
| --------------------------------- | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------ | --------- | ------------- | ------------- | ------------- | --- |
| Channel Setup and Operations | `matrix.channel-setup-and-operations` | Matrix plugin identity<br>Setup wizard<br>Account discovery<br>Matrix doctor warnings<br>Matrix probe/status | matrix-plugin-identity<br>setup-wizard<br>account-discovery<br>matrix-doctor-warnings<br>matrix-probe-status | `docs/channels/matrix.md`<br>`docs/channels/matrix-migration.md` | `release` | `Beta (74%)` | `Beta (74%)` | `Beta (74%)` | No |
| Access and Identity | `matrix.access-and-identity` | DM policy<br>Direct-room classification<br>Inbound route selection across sender-bound DMs<br>Mention gates<br>Matrix thread reply routing<br>Persisted Matrix thread routing managers<br>ACP/subagent spawn hooks | dm-policy<br>direct-room-classification<br>inbound-route-selection-across-sender-bound-dms<br>mention-gates<br>matrix-thread-reply-routing<br>persisted-matrix-thread-routing-managers<br>acp-subagent-spawn-hooks | `docs/channels/matrix.md`<br>`docs/channels/groups.md`<br>`docs/channels/bot-loop-protection.md` | `release` | `Beta (72%)` | `Alpha (66%)` | `Beta (72%)` | No |
| Conversation Routing and Delivery | `matrix.conversation-routing-and-delivery` | Conversation Routing and Delivery | conversation-routing-and-delivery | `docs/channels/matrix.md` | `release` | `Beta (72%)` | `Alpha (66%)` | `Beta (72%)` | No |
| Media and Rich Content | `matrix.media-and-rich-content` | Media and Rich Content | media-and-rich-content | `docs/channels/matrix.md` | `release` | `Beta (74%)` | `Alpha (68%)` | `Beta (74%)` | No |
| Native Controls and Approvals | `matrix.native-controls-and-approvals` | Channel action discovery<br>Message send/read/edit/delete<br>Profile media loading<br>Outbound Matrix text<br>Message presentation metadata<br>Inbound media failure handling | channel-action-discovery<br>message-send-read-edit-delete<br>profile-media-loading<br>outbound-matrix-text<br>message-presentation-metadata<br>inbound-media-failure-handling | `docs/channels/matrix.md` | `release` | `Alpha (64%)` | `Alpha (68%)` | `Alpha (64%)` | No |
| Encryption and Verification | `matrix.encryption-and-verification` | Encryption setup<br>Encrypted media upload/download<br>Legacy state | encryption-setup<br>encrypted-media-upload-download<br>legacy-state | `docs/channels/matrix.md`<br>`docs/channels/matrix-migration.md` | `release` | `Beta (76%)` | `Alpha (68%)` | `Beta (76%)` | No |
#### Microsoft Teams
- Surface id: `microsoft-teams`
- Level: M2 Alpha
- Rationale: Enterprise auth/admin flows need explicit scenario proof.
- Completeness instructions: `references/completeness/microsoft-teams.md`
| Category | Category ID | Features | Coverage IDs | Docs | Profiles | Coverage | Quality | Completeness | LTS |
| --------------------------------- | --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | --------- | ------------- | ------------- | ------------- | --- |
| Channel Setup and Operations | `microsoft-teams.channel-setup-and-operations` | Teams CLI app creation<br>Bot registration and manifest upload<br>Credential configuration<br>Teams app install verification<br>Setup status<br>Probe and scope reporting<br>Teams app doctor<br>Webhook and health diagnostics<br>Operator repair paths | teams-cli-app-creation<br>bot-registration-and-manifest-upload<br>credential-configuration<br>teams-app-install-verification<br>setup-status<br>probe-and-scope-reporting<br>teams-app-doctor<br>webhook-and-health-diagnostics<br>operator-repair-paths | `docs/channels/msteams.md`<br>`docs/plugins/reference/msteams.md`<br>`docs/gateway/config-channels.md`<br>`docs/gateway/health.md` | `release` | `Alpha (58%)` | `Alpha (64%)` | `Alpha (58%)` | No |
| Access and Identity | `microsoft-teams.access-and-identity` | DM pairing<br>Stable sender identity<br>Allowlists and access groups<br>Invoke and command authorization<br>Teams-originated config writes<br>Bot Framework SSO invokes<br>Delegated token storage<br>Graph directory lookup<br>Member profile lookup | dm-pairing<br>stable-sender-identity<br>allowlists-and-access-groups<br>invoke-and-command-authorization<br>teams-originated-config-writes<br>bot-framework-sso-invokes<br>delegated-token-storage<br>graph-directory-lookup<br>member-profile-lookup | `docs/channels/msteams.md`<br>`docs/channels/pairing.md`<br>`docs/channels/access-groups.md` | `release` | `Alpha (60%)` | `Alpha (62%)` | `Alpha (60%)` | No |
| Conversation Routing and Delivery | `microsoft-teams.conversation-routing-and-delivery` | Team and channel allowlists<br>Deterministic channel replies<br>Mention-gated group access<br>Session routing<br>Reply and thread context | team-and-channel-allowlists<br>deterministic-channel-replies<br>mention-gated-group-access<br>session-routing<br>reply-and-thread-context | `docs/channels/msteams.md`<br>`docs/channels/groups.md`<br>`docs/channels/channel-routing.md` | `release` | `Alpha (68%)` | `Alpha (66%)` | `Alpha (68%)` | No |
| Media and Rich Content | `microsoft-teams.media-and-rich-content` | Inbound attachments<br>Graph-hosted media<br>File consent<br>SharePoint and OneDrive sharing<br>Media fetch safety | inbound-attachments<br>graph-hosted-media<br>file-consent<br>sharepoint-and-onedrive-sharing<br>media-fetch-safety | `docs/channels/msteams.md` | `release` | `Alpha (62%)` | `Alpha (58%)` | `Alpha (62%)` | No |
| Native Controls and Approvals | `microsoft-teams.native-controls-and-approvals` | Message action discovery<br>Polls and reactions<br>Read, edit, delete, and pin<br>Native approval cards<br>Feedback and group actions | message-action-discovery<br>polls-and-reactions<br>read-edit-delete-and-pin<br>native-approval-cards<br>feedback-and-group-actions | `docs/channels/msteams.md`<br>`docs/tools/exec-approvals-advanced.md` | `release` | `Alpha (64%)` | `Alpha (66%)` | `Alpha (64%)` | No |
#### Mattermost, LINE, IRC, Nextcloud Talk, Nostr, Twitch, Tlon, Synology Chat
- Surface id: `mattermost-line-irc-nextcloud-talk-nostr-twitch-tlon-synology-chat`
- Level: M2 Alpha
- Rationale: Supported surfaces exist, but maturity likely varies by upstream and maintainer coverage. Score individually later.
- Completeness instructions: `references/completeness/mattermost-line-irc-nextcloud-talk-nostr-twitch-tlon-synology-chat.md`
| Category | Category ID | Features | Coverage IDs | Docs | Profiles | Coverage | Quality | Completeness | LTS |
| --------------------------------- | ------------------------------------------------------------------------------------------------------ | --------------------------------- | --------------------------------- | ---- | --------- | ------------- | ------------- | ------------- | --- |
| Channel Setup and Operations | `mattermost-line-irc-nextcloud-talk-nostr-twitch-tlon-synology-chat.channel-setup-and-operations` | Channel Setup and Operations | channel-setup-and-operations | | `release` | `Alpha (62%)` | `Alpha (58%)` | `Alpha (62%)` | No |
| Access and Identity | `mattermost-line-irc-nextcloud-talk-nostr-twitch-tlon-synology-chat.access-and-identity` | Access and Identity | access-and-identity | | `release` | `Alpha (62%)` | `Alpha (58%)` | `Alpha (62%)` | No |
| Conversation Routing and Delivery | `mattermost-line-irc-nextcloud-talk-nostr-twitch-tlon-synology-chat.conversation-routing-and-delivery` | Conversation Routing and Delivery | conversation-routing-and-delivery | | `release` | `Alpha (62%)` | `Alpha (58%)` | `Alpha (62%)` | No |
| Media and Rich Content | `mattermost-line-irc-nextcloud-talk-nostr-twitch-tlon-synology-chat.media-and-rich-content` | Media and Rich Content | media-and-rich-content | | `release` | `Alpha (62%)` | `Alpha (58%)` | `Alpha (62%)` | No |
#### Feishu, QQ Bot, WeChat, Yuanbao, Zalo, Zalo Personal, regional channels
- Surface id: `feishu-qq-bot-wechat-yuanbao-zalo-zalo-personal-regional-channels`
- Level: M2 Alpha
- Rationale: Important regional coverage, but public support level should be calibrated per account type, upstream approval, and maintainer proof.
- Completeness instructions: `references/completeness/feishu-qq-bot-wechat-yuanbao-zalo-zalo-personal-regional-channels.md`
| Category | Category ID | Features | Coverage IDs | Docs | Profiles | Coverage | Quality | Completeness | LTS |
| --------------------------------- | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- | --------- | -------------------- | -------------------- | -------------------- | --- |
| Channel Setup and Operations | `feishu-qq-bot-wechat-yuanbao-zalo-zalo-personal-regional-channels.channel-setup-and-operations` | Docs channel index<br>Official external channel catalog entries<br>Core channel-plugin catalog<br>Channel setup wizard<br>Missing-plugin<br>Cross-channel ingress/access/refactor concerns | docs-channel-index<br>official-external-channel-catalog-entries<br>core-channel-plugin-catalog<br>channel-setup-wizard<br>missing-plugin<br>cross-channel-ingress-access-refactor-concerns | `docs/channels/index.md`<br>`docs/channels/pairing.md`<br>`docs/plugins/reference/feishu.md`<br>`docs/plugins/architecture-internals.md` | `release` | `Experimental (42%)` | `Experimental (44%)` | `Experimental (42%)` | No |
| Access and Identity | `feishu-qq-bot-wechat-yuanbao-zalo-zalo-personal-regional-channels.access-and-identity` | Access and Identity | access-and-identity | | `release` | `Experimental (42%)` | `Experimental (44%)` | `Experimental (42%)` | No |
| Conversation Routing and Delivery | `feishu-qq-bot-wechat-yuanbao-zalo-zalo-personal-regional-channels.conversation-routing-and-delivery` | Conversation Routing and Delivery | conversation-routing-and-delivery | | `release` | `Experimental (42%)` | `Experimental (44%)` | `Experimental (42%)` | No |
| Media and Rich Content | `feishu-qq-bot-wechat-yuanbao-zalo-zalo-personal-regional-channels.media-and-rich-content` | Media and Rich Content | media-and-rich-content | | `release` | `Experimental (47%)` | `Alpha (55%)` | `Experimental (47%)` | No |
#### Voice Call channel
- Surface id: `voice-call-channel`
- Level: M1 Experimental
- Rationale: Optional/plugin path with complex realtime behavior. Needs scenario scorecard before public beta.
- Completeness instructions: `references/completeness/voice-call-channel.md`
| Category | Category ID | Features | Coverage IDs | Docs | Profiles | Coverage | Quality | Completeness | LTS |
| --------------------------------- | ------------------------------------------------------ | ---------------------------------------- | ------------------------------------------ | ------------------------------------------------------------------------------------- | --------- | -------------------- | ------------- | -------------------- | --- |
| Channel Setup and Operations | `voice-call-channel.channel-setup-and-operations` | Voice Call Channel<br>Voice Call Channel | voice-call-channel<br>voice-call-channel-2 | `docs/cli/voicecall.md`<br>`docs/plugins/voice-call.md`<br>`docs/gateway/protocol.md` | `release` | `Experimental (42%)` | `Alpha (56%)` | `Experimental (42%)` | No |
| Access and Identity | `voice-call-channel.access-and-identity` | Voice Call Channel | voice-call-channel | `docs/plugins/voice-call.md`<br>`docs/cli/voicecall.md` | `release` | `Alpha (60%)` | `Alpha (62%)` | `Alpha (60%)` | No |
| Conversation Routing and Delivery | `voice-call-channel.conversation-routing-and-delivery` | Voice Call Channel | voice-call-channel | `docs/plugins/voice-call.md` | `release` | `Alpha (52%)` | `Alpha (58%)` | `Alpha (52%)` | No |
| Media and Rich Content | `voice-call-channel.media-and-rich-content` | Voice Call Channel<br>Voice Call Channel | voice-call-channel<br>voice-call-channel-2 | `docs/plugins/voice-call.md`<br>`docs/plugins/plugin-inventory.md` | `release` | `Experimental (48%)` | `Alpha (57%)` | `Experimental (48%)` | No |
| Realtime Voice and Calls | `voice-call-channel.realtime-voice-and-calls` | Voice Call Channel<br>Voice Call Channel | voice-call-channel<br>voice-call-channel-2 | `docs/plugins/voice-call.md` | `release` | `Experimental (44%)` | `Alpha (55%)` | `Experimental (44%)` | No |
### Provider and tool
#### OpenAI / Codex provider path
- Surface id: `openai-codex-provider-path`
- Level: M3 Beta
- Rationale: Deep docs, OAuth/subscription path, realtime voice, image, and compatibility behavior. Provider churn keeps this from Stable without release-scorecard proof.
- Completeness instructions: `references/completeness/openai-codex-provider-path.md`
| Category | Category ID | Features | Coverage IDs | Docs | Profiles | Coverage | Quality | Completeness | LTS |
| -------------------------------- | ------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | -------------- | ------------- | -------------- | --- |
| Model and Auth | `openai-codex-provider-path.model-and-auth` | Canonical OpenAI Model Routing<br>Catalog<br>Codex OAuth Profiles<br>Subscription Usage<br>Doctor Diagnostics<br>Operator Repair | models.openai<br>tools.web-search<br>catalog<br>auth-profiles.provider-selection<br>runtime.codex-plugin.auth<br>runtime.doctor-repair<br>subscription-usage<br>runtime.codex-plugin.version<br>operator-repair | `docs/providers/openai.md`<br>`docs/plugins/codex-harness.md`<br>`docs/concepts/models.md`<br>`docs/concepts/oauth.md`<br>`docs/plugins/codex-harness-reference.md`<br>`docs/automation/auth-monitoring.md` | `release` | `Beta (78%)` | `Alpha (66%)` | `Beta (78%)` | Yes |
| Responses and Tool Compatibility | `openai-codex-provider-path.responses-and-tool-compatibility` | Codex Responses Transport<br>Payload Compatibility<br>Tool Context<br>Capability Compatibility | codex-responses-transport<br>runtime.codex-native-workspace.read<br>runtime.prompt-compatibility<br>tools.fs.read<br>runtime.codex-native-workspace.read<br>runtime.prompt-compatibility<br>tools.fs.read<br>capability-compatibility | `docs/providers/openai.md`<br>`docs/gateway/openresponses-http-api.md`<br>`docs/gateway/openai-http-api.md`<br>`docs/plugins/codex-native-plugins.md` | `release` | `Beta (76%)` | `Beta (70%)` | `Beta (76%)` | Yes |
| Native Codex Harness | `openai-codex-provider-path.native-codex-harness` | Native Codex App-server Harness<br>Thread Lifecycle | models.codex-cli<br>runtime.codex-app-server<br>runtime.gateway-log-sentinel.codex-progress<br>runtime.long-context<br>runtime.no-meta-leak<br>workspace.planning<br>runtime.codex-app-server<br>runtime.codex-plugin.lifecycle<br>runtime.doctor-repair<br>runtime.gateway-log-sentinel.codex-progress<br>runtime.long-context<br>runtime.turn-ordering | `docs/plugins/codex-harness.md`<br>`docs/plugins/codex-harness-runtime.md`<br>`docs/plugins/codex-harness-reference.md`<br>`docs/plugins/codex-native-plugins.md` | `release` | `Stable (82%)` | `Beta (72%)` | `Stable (82%)` | Yes |
| Image and Multimodal Input | `openai-codex-provider-path.image-and-multimodal-input` | Image Generation Editing<br>Multimodal Input | image-generation-editing<br>multimodal-input | `docs/providers/openai.md`<br>`docs/tools/image-generation.md`<br>`docs/nodes/images.md` | `release` | `Stable (80%)` | `Beta (72%)` | `Stable (80%)` | No |
| Voice and Realtime Audio | `openai-codex-provider-path.voice-and-realtime-audio` | Realtime Voice Transcription<br>Speech | realtime-voice-transcription<br>speech | `docs/providers/openai.md`<br>`docs/channels/discord.md`<br>`docs/plugins/voice-call.md` | `release` | `Beta (72%)` | `Alpha (68%)` | `Beta (72%)` | No |
#### Anthropic provider path
- Surface id: `anthropic-provider-path`
- Level: M3 Beta
- Rationale: First-class model provider. Needs recurring auth/catalog/tool-call scenario proof.
- Completeness instructions: `references/completeness/anthropic-provider-path.md`
| Category | Category ID | Features | Coverage IDs | Docs | Profiles | Coverage | Quality | Completeness | LTS |
| ------------------------------------ | -------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | -------------- | -------------- | -------------- | --- |
| Provider Auth and Recovery | `anthropic-provider-path.provider-auth-and-recovery` | API-key onboarding<br>Claude CLI credential reuse<br>Setup-token auth<br>Auth profile health<br>Model status<br>Usage windows<br>Cooldown/profile reporting<br>Long-context recovery<br>Fallback guidance | api-key-onboarding<br>claude-cli-credential-reuse<br>setup-token-auth<br>auth-profile-health<br>model-status<br>usage-windows<br>cooldown-profile-reporting<br>long-context-recovery<br>fallback-guidance | `docs/providers/anthropic.md`<br>`docs/gateway/doctor.md`<br>`docs/gateway/configuration-examples.md`<br>`docs/gateway/troubleshooting.md`<br>`docs/reference/prompt-caching.md` | `release` | `Beta (78%)` | `Beta (70%)` | `Beta (78%)` | No |
| Model and Runtime Selection | `anthropic-provider-path.model-and-runtime-selection` | Bundled Claude catalog<br>Canonical anthropic refs<br>Claude CLI compatibility<br>Model picker availability<br>Capability metadata<br>Runtime selection<br>Session continuity<br>MCP/tool bridge<br>Permission-mode mapping<br>Fallback prelude | bundled-claude-catalog<br>models.anthropic<br>models.provider-auth<br>models.claude-cli<br>models.provider-capabilities<br>model-picker-availability<br>capability-metadata<br>runtime-selection<br>session-continuity<br>mcp-tool-bridge<br>permission-mode-mapping<br>fallback-prelude | `docs/providers/anthropic.md`<br>`docs/gateway/config-agents.md`<br>`docs/concepts/models.md`<br>`docs/gateway/cli-backends.md` | `release` | `Stable (82%)` | `Alpha (68%)` | `Stable (82%)` | No |
| Request Transport and Turn Semantics | `anthropic-provider-path.request-transport-and-turn-semantics` | API-key/OAuth transport<br>Messages payloads<br>Streaming decode<br>Usage and stop reasons<br>Abort/error handling<br>Tool-use blocks<br>Tool-result replay<br>Partial JSON recovery<br>Native thinking<br>Signed/redacted thinking replay | api-key-oauth-transport<br>messages-payloads<br>streaming-decode<br>usage-and-stop-reasons<br>abort-error-handling<br>tool-use-blocks<br>tool-result-replay<br>partial-json-recovery<br>native-thinking<br>signed-redacted-thinking-replay | `docs/providers/anthropic.md`<br>`docs/reference/prompt-caching.md`<br>`docs/gateway/troubleshooting.md`<br>`docs/gateway/cli-backends.md`<br>`docs/concepts/model-providers.md` | `release` | `Stable (82%)` | `Beta (72%)` | `Stable (82%)` | No |
| Prompt Cache and Context | `anthropic-provider-path.prompt-cache-and-context` | Cache retention<br>System-prompt cache boundary<br>1M context<br>Fast mode/service tier<br>Cache diagnostics | cache-retention<br>system-prompt-cache-boundary<br>1m-context<br>fast-mode-service-tier<br>cache-diagnostics | `docs/providers/anthropic.md`<br>`docs/reference/prompt-caching.md`<br>`docs/gateway/troubleshooting.md`<br>`docs/gateway/heartbeat.md` | `release` | `Stable (82%)` | `Beta (76%)` | `Stable (82%)` | No |
| Media Inputs | `anthropic-provider-path.media-inputs` | Image input<br>PDF document input<br>Media model fallback<br>Image tool results | image-input<br>pdf-document-input<br>media-model-fallback<br>image-tool-results | `docs/providers/anthropic.md`<br>`docs/gateway/config-agents.md` | `release` | `Beta (74%)` | `Stable (82%)` | `Beta (74%)` | No |
#### Google provider path
- Surface id: `google-provider-path`
- Level: M3 Beta
- Rationale: First-class provider with model and realtime surfaces. Needs separate Live/Talk scoring.
- Completeness instructions: `references/completeness/google-provider-path.md`
| Category | Category ID | Features | Coverage IDs | Docs | Profiles | Coverage | Quality | Completeness | LTS |
| ------------------------------ | ----------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | --------- | -------------- | -------------- | -------------- | --- |
| Provider Setup and Credentials | `google-provider-path.provider-setup-and-credentials` | API key onboarding<br>Auth choice metadata<br>Gemini CLI OAuth setup<br>Vertex ADC setup<br>Daemon and fallback credentials<br>CLI runtime selection<br>OAuth login and refresh<br>Canonical Google model refs<br>CLI usage normalization<br>OAuth diagnostics | api-key-onboarding<br>auth-choice-metadata<br>gemini-cli-oauth-setup<br>vertex-adc-setup<br>daemon-and-fallback-credentials<br>cli-runtime-selection<br>oauth-login-and-refresh<br>canonical-google-model-refs<br>cli-usage-normalization<br>oauth-diagnostics | `docs/providers/google.md`<br>`docs/concepts/model-providers.md` | `release` | `Beta (72%)` | `Alpha (60%)` | `Beta (72%)` | No |
| Model Routing and Endpoints | `google-provider-path.model-routing-and-endpoints` | Catalog rows and aliases<br>Dynamic model resolution<br>Provider routing<br>Google-native config normalization<br>Model picker availability<br>Vertex provider selection<br>ADC/service-account auth<br>Project/location endpoints<br>Custom base URL policy<br>Compatibility boundaries | catalog-rows-and-aliases<br>dynamic-model-resolution<br>provider-routing<br>google-native-config-normalization<br>model-picker-availability<br>vertex-provider-selection<br>adc-service-account-auth<br>project-location-endpoints<br>custom-base-url-policy<br>compatibility-boundaries | `docs/providers/google.md`<br>`docs/concepts/model-providers.md`<br>`docs/plugins/reference/google.md`<br>`docs/tools/gemini-search.md` | `release` | `Alpha (68%)` | `Alpha (62%)` | `Alpha (68%)` | No |
| Direct Gemini Runtime | `google-provider-path.direct-gemini-runtime` | Direct Gemini chat<br>Multimodal inputs<br>Tool-call streaming<br>Usage and stop reasons<br>Thought-signature replay<br>Thinking-level mapping<br>Thought-signature replay<br>Tool turn ordering<br>Incomplete-turn recovery<br>Planning-only turn recovery | direct-gemini-chat<br>multimodal-inputs<br>tool-call-streaming<br>usage-and-stop-reasons<br>thought-signature-replay<br>thinking-level-mapping<br>thought-signature-replay-2<br>tool-turn-ordering<br>incomplete-turn-recovery<br>planning-only-turn-recovery | `docs/providers/google.md`<br>`docs/concepts/model-providers.md`<br>`docs/help/faq-models.md`<br>`docs/help/testing-live.md` | `release` | `Stable (82%)` | `Stable (80%)` | `Stable (82%)` | No |
| Media, Search, and Realtime | `google-provider-path.media-search-and-realtime` | Bundled plugin distribution<br>Provider auto-enable metadata<br>Image and media adapters<br>Speech and realtime adapters<br>Search and generation tools<br>Realtime voice sessions<br>Constrained browser tokens<br>Audio and transcript events<br>Live tool calls<br>Session reconnects | bundled-plugin-distribution<br>provider-auto-enable-metadata<br>image-and-media-adapters<br>speech-and-realtime-adapters<br>search-and-generation-tools<br>realtime-voice-sessions<br>constrained-browser-tokens<br>audio-and-transcript-events<br>live-tool-calls<br>session-reconnects | `docs/plugins/reference/google.md`<br>`docs/providers/google.md` | `release` | `Beta (76%)` | `Alpha (65%)` | `Beta (76%)` | No |
| Prompt Caching | `google-provider-path.prompt-caching` | Cache retention config<br>Managed cachedContents<br>Manual cachedContent handles<br>Cache usage accounting<br>Cache diagnostics and live proof | cache-retention-config<br>managed-cachedcontents<br>manual-cachedcontent-handles<br>cache-usage-accounting<br>cache-diagnostics-and-live-proof | `docs/reference/prompt-caching.md`<br>`docs/providers/google.md`<br>`docs/concepts/model-providers.md`<br>`docs/reference/token-use.md` | `release` | `Alpha (68%)` | `Beta (74%)` | `Alpha (68%)` | No |
#### OpenRouter provider path
- Surface id: `openrouter-provider-path`
- Level: M3 Beta
- Rationale: Unified provider path is documented and valuable, but model-specific behavior varies.
- Completeness instructions: `references/completeness/openrouter-provider-path.md`
| Category | Category ID | Features | Coverage IDs | Docs | Profiles | Coverage | Quality | Completeness | LTS |
| --------------------------------- | ------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | ------------ | ------------- | ------------ | --- |
| Provider Setup and Auth | `openrouter-provider-path.provider-setup-and-auth` | First-run setup<br>Default model selection<br>Provider plugin registration<br>Model-ref examples<br>OPENROUTER_API_KEY<br>Auth profiles and auth order<br>Status/probe and removal<br>Provider-entry SecretRef/API-key resolution<br>Gateway env inheritance<br>Static catalog rows<br>Dynamic /models discovery<br>openrouter/auto and nested refs<br>Free-model scan/probe<br>Model list/picker cache | first-run-setup<br>default-model-selection<br>provider-plugin-registration<br>model-ref-examples<br>openrouter-api-key<br>auth-profiles-and-auth-order<br>status-probe-and-removal<br>provider-entry-secretref-api-key-resolution<br>gateway-env-inheritance<br>static-catalog-rows<br>dynamic-models-discovery<br>openrouter-auto-and-nested-refs<br>free-model-scan-probe<br>model-list-picker-cache | `docs/providers/openrouter.md`<br>`docs/concepts/model-providers.md`<br>`docs/cli/configure.md`<br>`docs/gateway/authentication.md`<br>`docs/help/environment.md`<br>`docs/cli/models.md`<br>`docs/concepts/models.md` | `release` | `Beta (78%)` | `Alpha (64%)` | `Beta (78%)` | No |
| Chat Runtime and Normalization | `openrouter-provider-path.chat-runtime-and-normalization` | Chat completions route<br>Provider routing params<br>Per-model route overrides<br>Reasoning payload policy<br>Anthropic/Gemini/DeepSeek variants<br>Streamed content parsing<br>reasoning_details visible output<br>Tool-call delta preservation<br>Family-specific replay policy<br>Response-model and usage normalization<br>Attribution headers<br>Response-cache headers/TTL/clear<br>Anthropic cache-control markers<br>Cache usage mapping<br>Custom proxy exclusions | chat-completions-route<br>provider-routing-params<br>per-model-route-overrides<br>reasoning-payload-policy<br>anthropic-gemini-deepseek-variants<br>streamed-content-parsing<br>reasoning-details-visible-output<br>tool-call-delta-preservation<br>family-specific-replay-policy<br>response-model-and-usage-normalization<br>attribution-headers<br>response-cache-headers-ttl-clear<br>anthropic-cache-control-markers<br>cache-usage-mapping<br>custom-proxy-exclusions | `docs/providers/openrouter.md`<br>`docs/concepts/model-providers.md`<br>`docs/reference/prompt-caching.md` | `release` | `Beta (76%)` | `Beta (70%)` | `Beta (76%)` | No |
| Provider Recovery and Diagnostics | `openrouter-provider-path.provider-recovery-and-diagnostics` | Timeout/retry classification<br>Auth/billing/key-limit classification<br>Context overflow<br>Model fallback notices<br>Guarded fetch/pricing warnings | timeout-retry-classification<br>auth-billing-key-limit-classification<br>context-overflow<br>model-fallback-notices<br>guarded-fetch-pricing-warnings | `docs/concepts/model-failover.md`<br>`docs/providers/openrouter.md`<br>`docs/cli/models.md` | `release` | `Beta (74%)` | `Alpha (65%)` | `Beta (74%)` | No |
| Media Generation and Speech | `openrouter-provider-path.media-generation-and-speech` | image_generate OpenRouter route<br>video_generate async jobs/polling/download<br>music_generate audio route<br>Text-to-speech<br>Speech-to-text transcription<br>Inbound media understanding<br>Generated artifact delivery | image-generate-openrouter-route<br>video-generate-async-jobs-polling-download<br>music-generate-audio-route<br>text-to-speech<br>speech-to-text-transcription<br>inbound-media-understanding<br>generated-artifact-delivery | `docs/providers/openrouter.md`<br>`docs/tools/image-generation.md`<br>`docs/tools/music-generation.md`<br>`docs/tools/media-overview.md`<br>`docs/tools/video-generation.md`<br>`docs/tools/tts.md` | `release` | `Beta (72%)` | `Alpha (66%)` | `Beta (72%)` | No |
#### Local model providers: Ollama, vLLM, SGLang, LM Studio
- Surface id: `local-model-providers-ollama-vllm-sglang-lm-studio`
- Level: M2 Alpha
- Rationale: Useful and documented, but environment variance is high.
- Completeness instructions: `references/completeness/local-model-providers-ollama-vllm-sglang-lm-studio.md`
| Category | Category ID | Features | Coverage IDs | Docs | Profiles | Coverage | Quality | Completeness | LTS |
| ------------------------------------------ | --------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | -------------- | -------------- | -------------- | --- |
| Provider Setup, Lifecycle, and Diagnostics | `local-model-providers-ollama-vllm-sglang-lm-studio.provider-setup-lifecycle-and-diagnostics` | Provider Selection<br>Onboarding<br>localService configuration<br>Process startup and readiness<br>Request leases and idle shutdown<br>Health checks and restart<br>Provider recipes<br>Local provider status<br>Backend reachability probes<br>Model availability errors<br>Memory readiness diagnostics<br>Provider troubleshooting docs | provider-selection<br>onboarding<br>localservice-configuration<br>process-startup-and-readiness<br>request-leases-and-idle-shutdown<br>health-checks-and-restart<br>provider-recipes<br>local-provider-status<br>backend-reachability-probes<br>model-availability-errors<br>memory-readiness-diagnostics<br>provider-troubleshooting-docs | `docs/gateway/local-models.md`<br>`docs/providers/lmstudio.md`<br>`docs/providers/ollama.md`<br>`docs/providers/vllm.md`<br>`docs/gateway/local-model-services.md`<br>`docs/gateway/config-agents.md`<br>`docs/gateway/troubleshooting.md`<br>`docs/gateway/doctor.md` | `release` | `Beta (74%)` | `Beta (72%)` | `Beta (74%)` | No |
| Native Provider Plugins | `local-model-providers-ollama-vllm-sglang-lm-studio.native-provider-plugins` | Ollama setup and model pulling<br>Model discovery<br>Streaming and vision<br>Ollama embeddings<br>Web-search support<br>LM Studio setup<br>Model discovery and auth<br>Model preload and JIT loading<br>Streaming compatibility<br>LM Studio embeddings | ollama-setup-and-model-pulling<br>model-discovery<br>streaming-and-vision<br>ollama-embeddings<br>web-search-support<br>lm-studio-setup<br>model-discovery-and-auth<br>model-preload-and-jit-loading<br>streaming-compatibility<br>lm-studio-embeddings | `docs/providers/ollama.md`<br>`docs/providers/lmstudio.md` | `release` | `Beta (78%)` | `Beta (78%)` | `Beta (78%)` | No |
| OpenAI-Compatible Runtime Compatibility | `local-model-providers-ollama-vllm-sglang-lm-studio.openai-compatible-runtime-compatibility` | Bundled provider setup<br>Model Discovery Endpoint<br>Non-interactive configuration<br>vLLM thinking controls<br>OpenAI-compatible chat and tool semantics<br>SGLang compatibility guidance<br>Request Stream Compatibility<br>Tool Calling | bundled-provider-setup<br>model-discovery-endpoint<br>non-interactive-configuration<br>vllm-thinking-controls<br>openai-compatible-chat-and-tool-semantics<br>sglang-compatibility-guidance<br>request-stream-compatibility<br>tool-calling | `docs/providers/vllm.md`<br>`docs/providers/sglang.md`<br>`docs/gateway/local-models.md`<br>`docs/providers/lmstudio.md` | `release` | `Beta (74%)` | `Alpha (68%)` | `Beta (74%)` | No |
| Local Memory and Embeddings | `local-model-providers-ollama-vllm-sglang-lm-studio.local-memory-and-embeddings` | Embedding provider selection<br>Memory search readiness<br>memoryFlush model override<br>Fallback lexical search<br>Provider mismatch guidance | embedding-provider-selection<br>memory-search-readiness<br>memoryflush-model-override<br>fallback-lexical-search<br>provider-mismatch-guidance | `docs/concepts/memory.md`<br>`docs/gateway/doctor.md` | `release` | `Beta (76%)` | `Alpha (68%)` | `Beta (76%)` | No |
| Network Safety and Prompt Controls | `local-model-providers-ollama-vllm-sglang-lm-studio.network-safety-and-prompt-controls` | Safety Network<br>Prompt Pressure Controls | safety-network<br>prompt-pressure-controls | `docs/gateway/security/index.md`<br>`docs/gateway/config-tools.md`<br>`docs/gateway/local-models.md` | `release` | `Stable (82%)` | `Stable (82%)` | `Stable (82%)` | No |
#### Long-tail hosted providers
- Surface id: `long-tail-hosted-providers`
- Level: M2 Alpha
- Rationale: Many docs/reference pages exist; score should be generated from provider metadata plus live smoke coverage.
- Completeness instructions: `references/completeness/long-tail-hosted-providers.md`
| Category | Category ID | Features | Coverage IDs | Docs | Profiles | Coverage | Quality | Completeness | LTS |
| ---------------------- | --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | ------------- | ------------- | ------------- | --- |
| Hosted LLM Providers | `long-tail-hosted-providers.hosted-llm-providers` | Bedrock setup<br>Gateway/proxy routing<br>Copilot/OpenCode hosted access<br>Proxy capability diagnostics<br>Hosted text completion<br>Tool-call and streaming compatibility<br>Model catalog resolution<br>Provider-specific request shaping<br>Regional provider setup<br>Region and plan routing<br>Regional live smoke<br>Account prerequisite diagnostics | bedrock-setup<br>gateway-proxy-routing<br>copilot-opencode-hosted-access<br>proxy-capability-diagnostics<br>hosted-text-completion<br>tool-call-and-streaming-compatibility<br>model-catalog-resolution<br>provider-specific-request-shaping<br>regional-provider-setup<br>region-and-plan-routing<br>regional-live-smoke<br>account-prerequisite-diagnostics | `docs/providers/index.md`<br>`docs/concepts/model-providers.md`<br>`docs/help/testing-live.md`<br>`docs/cli/onboard.md` | `release` | `Alpha (58%)` | `Alpha (56%)` | `Alpha (58%)` | No |
| Hosted Media Providers | `long-tail-hosted-providers.hosted-media-providers` | Image generation providers<br>Video generation providers<br>Music generation providers<br>Media mode coverage<br>Text-to-speech providers<br>Speech-to-text providers<br>Realtime transcription providers<br>Audio format diagnostics | image-generation-providers<br>video-generation-providers<br>music-generation-providers<br>media-mode-coverage<br>text-to-speech-providers<br>speech-to-text-providers<br>realtime-transcription-providers<br>audio-format-diagnostics | `docs/plugins/manifest.md`<br>`docs/help/testing-live.md`<br>`docs/providers/index.md` | `release` | `Beta (70%)` | `Alpha (64%)` | `Beta (70%)` | No |
| Provider Operations | `long-tail-hosted-providers.provider-operations` | Provider directory<br>Provider install catalog<br>Model catalog metadata<br>Catalog parity checks<br>Provider setup descriptors<br>Auth profiles and aliases<br>Credential health probes<br>Key rotation and recovery<br>Direct provider smoke<br>Gateway live smoke<br>Models status probes<br>Fallback trace and repair | provider-directory<br>provider-install-catalog<br>model-catalog-metadata<br>catalog-parity-checks<br>provider-setup-descriptors<br>auth-profiles-and-aliases<br>credential-health-probes<br>key-rotation-and-recovery<br>direct-provider-smoke<br>gateway-live-smoke<br>models-status-probes<br>fallback-trace-and-repair | `docs/providers/index.md`<br>`docs/concepts/model-providers.md`<br>`docs/plugins/manifest.md`<br>`docs/help/testing-live.md`<br>`docs/cli/models.md` | `release` | `Alpha (64%)` | `Alpha (60%)` | `Alpha (64%)` | No |
#### Web search tools
- Surface id: `web-search-tools`
- Level: M3 Beta
- Rationale: Multiple providers and docs exist. Needs quota/error/SSRF proof per provider family.
- Completeness instructions: `references/completeness/web-search-tools.md`
| Category | Category ID | Features | Coverage IDs | Docs | Profiles | Coverage | Quality | Completeness | LTS |
| --------------------------- | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------- | -------------- | -------------- | -------------- | --- |
| Search Providers | `web-search-tools.search-providers` | API-backed providers<br>Keyless and self-hosted providers<br>Provider comparison and auto-detection<br>Provider-specific filters and extraction<br>Result normalization<br>OpenAI native web_search<br>Codex native web_search<br>Gemini grounding<br>Grok web grounding<br>Kimi web search<br>Provider-native citations<br>Model and filter routing<br>webSearchProviders<br>registerWebSearchProvider<br>webFetchProviders<br>registerWebFetchProvider<br>public-artifact loading<br>runtime resolution<br>contract tests | tools.tavily-search<br>keyless-and-self-hosted-providers<br>provider-comparison-and-auto-detection<br>provider-specific-filters-and-extraction<br>result-normalization<br>openai-native-web-search<br>codex-native-web-search<br>gemini-grounding<br>grok-web-grounding<br>kimi-web-search<br>provider-native-citations<br>model-and-filter-routing<br>websearchproviders<br>registerwebsearchprovider<br>webfetchproviders<br>registerwebfetchprovider<br>public-artifact-loading<br>runtime-resolution<br>contract-tests | `docs/tools/web.md`<br>`docs/tools/brave-search.md`<br>`docs/tools/tavily.md`<br>`docs/tools/exa-search.md`<br>`docs/tools/firecrawl.md`<br>`docs/tools/perplexity-search.md`<br>`docs/tools/duckduckgo-search.md`<br>`docs/tools/searxng-search.md`<br>`docs/tools/gemini-search.md`<br>`docs/tools/grok-search.md`<br>`docs/tools/kimi-search.md`<br>`docs/tools/minimax-search.md`<br>`docs/tools/ollama-search.md`<br>`docs/plugins/sdk-subpaths.md`<br>`docs/plugins/sdk-overview.md`<br>`docs/plugins/manifest.md` | `release` | `Beta (76%)` | `Beta (72%)` | `Beta (76%)` | No |
| Setup and Diagnostics | `web-search-tools.setup-and-diagnostics` | Provider credentials<br>Default provider selection<br>Credential repair<br>Status checks<br>Quota errors<br>Cache controls<br>Provider diagnostics<br>Retry and fallback<br>Operator repair | provider-credentials<br>default-provider-selection<br>credential-repair<br>status-checks<br>quota-errors<br>cache-controls<br>provider-diagnostics<br>retry-and-fallback<br>operator-repair | `docs/tools/web.md`<br>`docs/tools/web-fetch.md`<br>`docs/help/faq.md`<br>`docs/reference/api-usage-costs.md`<br>`docs/tools/brave-search.md`<br>`docs/tools/perplexity-search.md`<br>`docs/tools/tavily.md`<br>`docs/tools/firecrawl.md` | `release` | `Beta (74%)` | `Beta (70%)` | `Beta (74%)` | No |
| Network Safety | `web-search-tools.network-safety` | Network Safety<br>SSRF<br>Redirects<br>Untrusted Content | network-safety<br>ssrf<br>redirects<br>untrusted-content | `docs/tools/web.md`<br>`docs/tools/web-fetch.md`<br>`docs/tools/firecrawl.md`<br>`docs/tools/searxng-search.md` | `release` | `Stable (84%)` | `Stable (84%)` | `Stable (84%)` | No |
| Tool Availability and Fetch | `web-search-tools.tool-availability-and-fetch` | web_search exposure<br>web_fetch exposure<br>x_search exposure<br>group:web policy<br>disabled-state diagnostics<br>provider/model gating<br>URL fetch<br>HTML extraction<br>PDF/text extraction<br>Safe truncation<br>Content citation handoff | models.openai<br>tools.web-search<br>tools.web-fetch<br>x-search-exposure<br>group-web-policy<br>disabled-state-diagnostics<br>provider-model-gating<br>url-fetch<br>tools.tavily-extract<br>pdf-text-extraction<br>safe-truncation<br>content-citation-handoff | `docs/gateway/config-tools.md`<br>`docs/tools/web-fetch.md`<br>`docs/tools/web.md`<br>`docs/help/faq.md` | `release` | `Stable (82%)` | `Stable (80%)` | `Stable (82%)` | No |
#### Browser automation and exec/sandbox tools
- Surface id: `browser-automation-and-exec-sandbox-tools`
- Level: M3 Beta
- Rationale: Core tools are documented, but host security and permission UX should stay under active scorecard review.
- Completeness instructions: `references/completeness/browser-automation-and-exec-sandbox-tools.md`
| Category | Category ID | Features | Coverage IDs | Docs | Profiles | Coverage | Quality | Completeness | LTS |
| ----------------------------- | ------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- | -------------- | ------------ | -------------- | --- |
| Browser Automation | `browser-automation-and-exec-sandbox-tools.browser-automation` | Browser Actions<br>Snapshots<br>Artifacts<br>Browser Plugin Service<br>Profiles<br>Browser Security<br>SSRF<br>Remote Control | browser-actions<br>snapshots<br>character.persona<br>personal.task-followthrough<br>tools.followthrough<br>workspace.artifacts<br>workspace.builds<br>workspace.long-running-task<br>workspace.repo-discovery<br>browser-plugin-service<br>profiles<br>browser-security<br>ssrf<br>remote-control | `docs/tools/browser-control.md`<br>`docs/help/testing.md`<br>`docs/tools/browser.md`<br>`docs/gateway/security/index.md`<br>`docs/gateway/security/audit-checks.md` | `release` | `Beta (78%)` | `Beta (74%)` | `Beta (78%)` | No |
| Tool Invocation and Execution | `browser-automation-and-exec-sandbox-tools.tool-invocation-and-execution` | Exec Routing<br>Process Lifecycle<br>Direct Tool Invoke API<br>Node System.run<br>Host Exec Approvals<br>Elevated Mode | tools.bash<br>tools.exec<br>workspace.artifacts<br>workspace.builds<br>workspace.long-running-task<br>workspace.repo-discovery<br>plugins.mcp-tools<br>plugins.skills<br>tools.invocation<br>node-system-run<br>host-exec-approvals<br>elevated-mode | `docs/tools/exec.md`<br>`docs/gateway/background-process.md`<br>`docs/gateway/tools-invoke-http-api.md`<br>`docs/gateway/operator-scopes.md`<br>`docs/gateway/protocol.md`<br>`docs/tools/exec-approvals.md`<br>`docs/tools/exec-approvals-advanced.md`<br>`docs/tools/elevated.md` | `smoke-ci`<br>`release` | `Stable (82%)` | `Beta (79%)` | `Stable (82%)` | Yes |
| Sandbox and Tool Policy | `browser-automation-and-exec-sandbox-tools.sandbox-and-tool-policy` | Sandbox Backends<br>Workspace Isolation<br>Sandboxed Browser<br>Codex Dynamic Tools<br>Tool Policy<br>Sandbox Tool Gates | sandbox-backends<br>workspace-isolation<br>sandboxed-browser<br>codex-dynamic-tools<br>tool-policy<br>sandbox-tool-gates | `docs/gateway/sandboxing.md`<br>`docs/gateway/sandbox-vs-tool-policy-vs-elevated.md`<br>`docs/tools/multi-agent-sandbox-tools.md`<br>`docs/plugins/codex-harness-reference.md`<br>`docs/gateway/config-tools.md` | `release` | `Beta (76%)` | `Beta (72%)` | `Beta (76%)` | Yes |
#### Image/video/music generation tools
- Surface id: `image-video-music-generation-tools`
- Level: M2 Alpha
- Rationale: Capability exists across providers, but quality, latency, and parameter compatibility vary too much for beta without per-provider proof.
- Completeness instructions: `references/completeness/image-video-music-generation-tools.md`
| Category | Category ID | Features | Coverage IDs | Docs | Profiles | Coverage | Quality | Completeness | LTS |
| --------------------------- | ---------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | -------------- | ------------- | -------------- | --- |
| Media Routing and Discovery | `image-video-music-generation-tools.media-routing-and-discovery` | default media model config<br>per-call model refs and fallbacks<br>auth-backed tool discovery<br>action=list provider inspection | default-media-model-config<br>per-call-model-refs-and-fallbacks<br>auth-backed-tool-discovery<br>action-list-provider-inspection | `docs/gateway/config-agents.md`<br>`docs/tools/image-generation.md`<br>`docs/tools/video-generation.md`<br>`docs/tools/music-generation.md` | `release` | `Stable (82%)` | `Beta (74%)` | `Stable (82%)` | No |
| Task Lifecycle and Delivery | `image-video-music-generation-tools.task-lifecycle-and-delivery` | background task creation<br>task status/list/show/cancel<br>duplicate guards<br>progress keepalive<br>completion/failure wake<br>no-session inline fallback<br>local media persistence<br>MIME/filename inference<br>Hosted URL fallback<br>message-tool handoff<br>idempotent missing-media fallback<br>channel attachment proof | background-task-creation<br>task-status-list-show-cancel<br>duplicate-guards<br>progress-keepalive<br>completion-failure-wake<br>no-session-inline-fallback<br>local-media-persistence<br>mime-filename-inference<br>hosted-url-fallback<br>message-tool-handoff<br>idempotent-missing-media-fallback<br>channel-attachment-proof | `docs/tools/media-overview.md`<br>`docs/tools/image-generation.md`<br>`docs/tools/video-generation.md`<br>`docs/tools/music-generation.md` | `release` | `Beta (78%)` | `Alpha (65%)` | `Beta (78%)` | No |
| Image Generation | `image-video-music-generation-tools.image-generation` | text-to-image<br>reference-image editing<br>output hints<br>action=status<br>provider attempt metadata<br>OpenAI/Codex OAuth<br>API-key OpenAI<br>OpenRouter/xAI/fal/LiteLLM/DeepInfra/Google/MiniMax/ComfyUI auth<br>provider error diagnostics | text-to-image<br>reference-image-editing<br>output-hints<br>action-status<br>provider-attempt-metadata<br>openai-codex-oauth<br>api-key-openai<br>openrouter-xai-fal-litellm-deepinfra-google-minimax-comfyui-auth<br>provider-error-diagnostics | `docs/tools/image-generation.md`<br>`docs/cli/infer.md`<br>`docs/tools/media-overview.md` | `release` | `Beta (78%)` | `Alpha (66%)` | `Beta (78%)` | No |
| Video Generation | `image-video-music-generation-tools.video-generation` | text-to-video<br>image-to-video<br>video-to-video<br>reference role validation<br>audio refs<br>typed providerOptions<br>queue-backed jobs<br>polling/timeout handling<br>Hosted URL download<br>provider skip explanations<br>returned asset metadata | text-to-video<br>image-to-video<br>video-to-video<br>reference-role-validation<br>audio-refs<br>typed-provideroptions<br>queue-backed-jobs<br>polling-timeout-handling<br>hosted-url-download<br>provider-skip-explanations<br>returned-asset-metadata | `docs/tools/video-generation.md`<br>`docs/providers/runway.md`<br>`docs/providers/pixverse.md`<br>`docs/providers/fal.md`<br>`docs/providers/openrouter.md` | `release` | `Beta (76%)` | `Alpha (62%)` | `Beta (76%)` | No |
| Music Generation | `image-video-music-generation-tools.music-generation` | prompt and lyrics input<br>instrumental mode<br>duration/format controls<br>image-reference edit lanes<br>generated audio outputs<br>provider fallback | prompt-and-lyrics-input<br>instrumental-mode<br>duration-format-controls<br>image-reference-edit-lanes<br>generated-audio-outputs<br>provider-fallback | `docs/tools/music-generation.md` | `release` | `Beta (72%)` | `Alpha (61%)` | `Beta (72%)` | No |

View File

@@ -336,7 +336,6 @@ top-level `bindings[]` entries.
- **Discord channel/thread:** `match.channel="discord"` + `match.peer.id="<channelOrThreadId>"`
- **Slack channel/DM:** `match.channel="slack"` + `match.peer.id="<channelId|channel:<channelId>|#<channelId>|userId|user:<userId>|slack:<userId>|<@userId>>"`. Prefer stable Slack ids; channel bindings also match replies inside that channel's threads.
- **Telegram forum topic:** `match.channel="telegram"` + `match.peer.id="<chatId>:topic:<topicId>"`
- **WhatsApp DM/group:** `match.channel="whatsapp"` + `match.peer.id="<E.164|group JID>"`. Use E.164 numbers such as `+15555550123` for direct chats and WhatsApp group JIDs such as `120363424282127706@g.us` for groups.
- **iMessage DM/group:** `match.channel="imessage"` + `match.peer.id="<handle|chat_id:*|chat_guid:*|chat_identifier:*>"`. Prefer `chat_id:*` for stable group bindings.
</ParamField>
@@ -454,9 +453,8 @@ Use `agents.list[].runtime` to define ACP defaults once per agent:
### Behavior
- OpenClaw ensures the configured ACP session exists after channel-specific admission and before use.
- Messages in that channel, topic, or chat route to the configured ACP session.
- Configured ACP bindings own their session route. Channel broadcast fan-out does not replace the configured ACP session for a matched binding.
- OpenClaw ensures the configured ACP session exists before use.
- Messages in that channel or topic route to the configured ACP session.
- In bound conversations, `/new` and `/reset` reset the same ACP session key in place.
- Temporary runtime bindings (for example created by thread-focus flows) still apply where present.
- For cross-agent ACP spawns without an explicit `cwd`, OpenClaw inherits the target agent workspace from agent config.

View File

@@ -258,14 +258,7 @@ Snapshot flags at a glance:
- `--format aria`: accessibility tree with `axN` refs. When Playwright is available, OpenClaw binds refs with backend DOM ids to the live page so follow-up actions can use them; otherwise treat the output as inspection-only.
- `--efficient` (or `--mode efficient`): compact role snapshot preset. Set `browser.snapshotDefaults.mode: "efficient"` to make this the default (see [Gateway configuration](/gateway/configuration-reference#browser)).
- `--interactive`, `--compact`, `--depth`, `--selector` force a role snapshot with `ref=e12` refs. `--frame "<iframe>"` scopes role snapshots to an iframe.
- With Playwright, `--labels` adds a screenshot with overlayed ref labels
(prints `MEDIA:<path>`) plus an `annotations` array with each ref's bounding
box. On `screenshot`, Playwright-backed labels work with `--full-page`,
`--ref`, and `--element`; on `snapshot`, the accompanying screenshot remains
viewport-only. Existing-session/chrome-mcp profiles render overlay labels on
page screenshots but do not return `annotations` or use the Playwright
full-page/ref/element projection helper. Without Playwright or chrome-mcp,
labeled screenshots are not available.
- `--labels` adds a viewport-only screenshot with overlayed ref labels and prints the saved path.
- `--urls` appends discovered link destinations to AI snapshots.
## Snapshots and refs
@@ -281,9 +274,7 @@ OpenClaw supports two "snapshot" styles:
- Output: a role-based list/tree with `[ref=e12]` (and optional `[nth=1]`).
- Actions: `openclaw browser click e12`, `openclaw browser highlight e12`.
- Internally, the ref is resolved via `getByRole(...)` (plus `nth()` for duplicates).
- Add `--labels` to include a screenshot with overlayed `e12` labels. On
Playwright-backed profiles this also returns per-ref bounding-box metadata
(`annotations[]`).
- Add `--labels` to include a viewport screenshot with overlayed `e12` labels.
- Add `--urls` when link text is ambiguous and the agent needs concrete
navigation targets.

View File

@@ -42,14 +42,8 @@ app-server thread as an ephemeral side thread. That keeps Codex OAuth and native
thread behavior intact while still isolating the side answer from the parent
transcript. Like Codex `/side`, the side thread keeps the current Codex
permissions and native tool surface, with guardrails that tell the model not to
treat inherited parent-thread work as active instructions.
For CLI runtime aliases, BTW uses the owning CLI backend in side-question mode
instead of falling back to a direct provider call. OpenClaw seeds sanitized
conversation context into a fresh one-shot CLI invocation, disables OpenClaw MCP
tool bundling and reusable CLI session state for that invocation, and lets the
backend add any CLI-native no-resume or no-tools flags it supports. Direct
non-CLI runtimes keep the direct one-shot path.
treat inherited parent-thread work as active instructions. Non-Codex runtimes
keep the older direct one-shot path.
## What it does not do

View File

@@ -147,12 +147,10 @@ such as `@beta` stay pinned to the selected package and fail when incompatible.
Configure `security.installPolicy` to run a trusted local policy command before
plugin install or update proceeds. The policy receives metadata plus the staged
source path and can allow or block the install. It covers CLI and Gateway-backed
plugin install/update paths. Plugin `before_install` hooks run later only in
OpenClaw processes where plugin hooks are loaded, so use `security.installPolicy`
for operator-owned install decisions. The deprecated
`--dangerously-force-unsafe-install` flag is accepted for compatibility but does
not bypass install policy or OpenClaw's built-in plugin dependency denylist.
source path and can allow or block the install. It runs before plugin
`before_install` hooks. The deprecated `--dangerously-force-unsafe-install`
flag is accepted for compatibility but does not bypass install policy, hooks, or
OpenClaw's built-in plugin dependency denylist.
See [Skills config](/tools/skills-config#operator-install-policy-securityinstallpolicy)
for the shared `security.installPolicy` exec schema used by both skills and

View File

@@ -16,9 +16,9 @@ search or dynamic-tools surface. Codex-native code mode, tool search, deferred
dynamic tools, and nested tool calls are stable Codex harness surfaces and do
not depend on `tools.toolSearch`.
When enabled for OpenClaw runs, the model receives one `tool_search_code` tool
by default. That tool runs a short JavaScript body in an isolated Node
subprocess with an `openclaw.tools` bridge:
When enabled for OpenClaw runs, the model receives one `tool_search_code` tool by default.
That tool runs a short JavaScript body in an isolated Node subprocess with an
`openclaw.tools` bridge:
```js
const hits = await openclaw.tools.search("create a GitHub issue");
@@ -49,8 +49,8 @@ run:
3. List eligible MCP tools through the session MCP runtime.
4. Add eligible client tools supplied for the current run.
5. Index compact descriptors for search.
6. Expose the OpenClaw code bridge, the structured fallback tools, or the
compact directory surface to the model.
6. Expose either the OpenClaw code bridge or the structured fallback tools to the
model.
At execution time every real tool call returns to OpenClaw. The isolated Node
runtime does not hold plugin implementations, MCP client objects, or secrets.
@@ -59,26 +59,18 @@ normal policy, approval, hook, logging, and result handling still apply.
## Modes
`tools.toolSearch` has three model-facing modes:
`tools.toolSearch` has two model-facing modes:
- `code`: exposes `tool_search_code`, the default compact JavaScript bridge.
- `tools`: exposes `tool_search`, `tool_describe`, and `tool_call` as plain
structured tools for providers that should not receive code.
- `directory`: exposes `tool_search`, `tool_describe`, and `tool_call` plus a
bounded prompt directory of available tool names and descriptions for
providers that should see tool names without every full schema. OpenClaw can
also expose a small bounded set of likely or required tool schemas directly
for the current turn.
All modes use the same policy-filtered catalog and normal OpenClaw execution
path. If the current runtime cannot launch the isolated Node code-mode child
process, the default `code` mode falls back to `tools` before catalog
compaction. In `directory` mode, client-provided tools stay directly visible
for the current run while OpenClaw tools, plugin tools, and MCP tools can be
compacted behind the directory catalog. A direct call to an exact hidden
directory name is hydrated from that same authorized catalog before execution.
Both modes use the same catalog and execution path. The only difference is the
shape the model sees. If the current runtime cannot launch the isolated Node
code-mode child process, the default `code` mode falls back to `tools` before
catalog compaction.
All modes are experimental. Prefer direct tool exposure for small OpenClaw tool
Both modes are experimental. Prefer direct tool exposure for small OpenClaw tool
catalogs, and prefer the Codex-native stable surfaces for Codex harness runs.
There is no separate source-selection config. When Tool Search is enabled, the
@@ -98,10 +90,7 @@ Tool Search changes the shape:
contract
- Tool Search tools mode: the model sees three compact structured fallback
tools
- Tool Search directory mode: the model sees a bounded directory plus
search/describe/call controls and a small bounded set of likely or required
schemas
- during the turn: the model can load remaining schemas as needed
- during the turn: the model loads only the tool schemas it actually needs
Direct tool exposure is still the right default for small catalogs. Tool Search
is best when one run can see many tools, especially from MCP servers or
@@ -143,20 +132,6 @@ The structured fallback mode exposes the same operations as tools:
- `tool_describe`
- `tool_call`
Directory mode exposes:
- `tool_search`
- `tool_describe`
- `tool_call`
It also keeps client-provided tools directly visible and may expose a small
bounded set of likely or required catalog tool schemas directly for the current
turn. If the bounded directory omits entries, use `tool_search` to find them. If
the model requests an exact hidden directory tool name directly, OpenClaw
hydrates it from the authorized catalog before normal execution.
Directory-mode client tool names must not collide with OpenClaw, plugin, or MCP
tool names because exact deferred dispatch uses those names.
## Runtime boundary
The code bridge runs in a short-lived Node subprocess. The subprocess starts
@@ -211,18 +186,6 @@ Use the structured fallback tools instead for OpenClaw runs:
}
```
Use the compact directory surface instead for OpenClaw runs:
```json5
{
tools: {
toolSearch: {
mode: "directory",
},
},
}
```
Tune code-mode timeout and search result limits:
```json5

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -123,12 +123,11 @@
"help": "Optional explicit denylist of chat/user IDs. Sessions whose resolved conversation id matches the list are skipped even when the chat type is allowed. Applied after allowedChatIds."
},
"timeoutMs": {
"label": "Timeout (ms)",
"help": "Recall work budget on the main lane. Before recall, the hook allows up to 1500 ms for session/config preflight. After recall starts, it reserves another fixed 1500 ms only for abort settlement and transcript recovery."
"label": "Timeout (ms)"
},
"setupGraceTimeoutMs": {
"label": "Setup Grace Timeout (ms)",
"help": "Advanced: extra recall-work budget for cold embedded-run setup. Defaults to 0. The separate 1500 ms preflight cap and 1500 ms post-recall completion allowance still apply."
"help": "Advanced: extra blocking budget for cold embedded-run setup before the recall timeout is considered exhausted. Defaults to 0 so timeoutMs remains the main-lane hook budget unless you opt in."
},
"queryMode": {
"label": "Query Mode",

View File

@@ -34,7 +34,6 @@ export function buildAnthropicCliBackend(): CliBackendPlugin {
bundleMcp: true,
bundleMcpMode: "claude-config-file",
nativeToolMode: "always-on",
sideQuestionToolMode: "disabled",
ownsNativeCompaction: true,
config: {
command: "claude",

View File

@@ -150,61 +150,6 @@ describe("resolveClaudeCliExecutionArgs", () => {
}),
).toEqual(["-p", "--effort", "max"]);
});
it("forces isolated no-tool one-shot args for side-question execution", () => {
expect(
resolveClaudeCliExecutionArgs({
workspaceDir: "/tmp",
provider: "claude-cli",
modelId: "claude-opus-4-7",
thinkingLevel: "max",
useResume: true,
executionMode: "side-question",
baseArgs: [
"-p",
"--output-format",
"stream-json",
"--allowedTools=mcp__openclaw__*",
"--allowedTools",
"Read",
"Grep",
"--permission-mode",
"bypassPermissions",
"--session-id=abc",
"--resume",
"old-session",
"--resume-session-at",
"old-message",
"--resume-session-at=old-message-equals",
"--mcp-config",
"/tmp/side-question-mcp.json",
"--bare",
"--safe-mode",
"--strict-mcp-config",
"--no-session-persistence",
"--max-turns",
"4",
"--effort",
"high",
],
}),
).toEqual([
"-p",
"--output-format",
"stream-json",
"--safe-mode",
"--tools",
"",
"--disallowedTools",
"mcp__*",
"--strict-mcp-config",
"--no-session-persistence",
"--max-turns",
"1",
"--permission-mode",
"default",
]);
});
});
describe("normalizeClaudeBackendConfig", () => {

View File

@@ -67,26 +67,8 @@ const CLAUDE_LEGACY_SKIP_PERMISSIONS_ARG = "--dangerously-skip-permissions";
const CLAUDE_PERMISSION_MODE_ARG = "--permission-mode";
const CLAUDE_SETTING_SOURCES_ARG = "--setting-sources";
const CLAUDE_EFFORT_ARG = "--effort";
const CLAUDE_BARE_ARG = "--bare";
const CLAUDE_SAFE_MODE_ARG = "--safe-mode";
const CLAUDE_TOOLS_ARG = "--tools";
const CLAUDE_DISALLOWED_TOOLS_ARG = "--disallowedTools";
const CLAUDE_MCP_CONFIG_ARG = "--mcp-config";
const CLAUDE_STRICT_MCP_CONFIG_ARG = "--strict-mcp-config";
const CLAUDE_NO_SESSION_PERSISTENCE_ARG = "--no-session-persistence";
const CLAUDE_MAX_TURNS_ARG = "--max-turns";
const CLAUDE_SESSION_ID_ARG = "--session-id";
const CLAUDE_RESUME_ARG = "--resume";
const CLAUDE_RESUME_SESSION_AT_ARG = "--resume-session-at";
const CLAUDE_RESUME_SHORT_ARG = "-r";
const CLAUDE_CONTINUE_ARG = "--continue";
const CLAUDE_CONTINUE_SHORT_ARG = "-c";
const CLAUDE_FORK_SESSION_ARG = "--fork-session";
const CLAUDE_SAFE_SETTING_SOURCES = "user";
const CLAUDE_BYPASS_PERMISSION_MODE = "bypassPermissions";
const CLAUDE_DEFAULT_PERMISSION_MODE = "default";
const CLAUDE_NO_TOOLS_VALUE = "";
const CLAUDE_DENY_MCP_TOOLS_VALUE = "mcp__*";
type ClaudeCliEffort = "low" | "medium" | "high" | "xhigh" | "max";
@@ -250,89 +232,10 @@ function stripClaudeEffortArgs(args: readonly string[]): string[] {
return normalized;
}
const CLAUDE_SIDE_QUESTION_VARIADIC_VALUE_ARGS = new Set([
"--allowedTools",
"--allowed-tools",
CLAUDE_DISALLOWED_TOOLS_ARG,
"--disallowed-tools",
CLAUDE_TOOLS_ARG,
CLAUDE_MCP_CONFIG_ARG,
]);
const CLAUDE_SIDE_QUESTION_VALUE_ARGS = new Set([
CLAUDE_PERMISSION_MODE_ARG,
CLAUDE_SESSION_ID_ARG,
CLAUDE_RESUME_ARG,
CLAUDE_RESUME_SESSION_AT_ARG,
CLAUDE_RESUME_SHORT_ARG,
CLAUDE_MAX_TURNS_ARG,
]);
const CLAUDE_SIDE_QUESTION_BARE_ARGS = new Set([
CLAUDE_CONTINUE_ARG,
CLAUDE_CONTINUE_SHORT_ARG,
CLAUDE_FORK_SESSION_ARG,
CLAUDE_BARE_ARG,
CLAUDE_SAFE_MODE_ARG,
CLAUDE_STRICT_MCP_CONFIG_ARG,
CLAUDE_NO_SESSION_PERSISTENCE_ARG,
]);
function stripClaudeSideQuestionConflictingArgs(args: readonly string[]): string[] {
const normalized: string[] = [];
for (let i = 0; i < args.length; i += 1) {
const arg = args[i] ?? "";
const equalsIndex = arg.indexOf("=");
const argName = equalsIndex > 0 ? arg.slice(0, equalsIndex) : arg;
if (CLAUDE_SIDE_QUESTION_BARE_ARGS.has(argName)) {
continue;
}
if (CLAUDE_SIDE_QUESTION_VARIADIC_VALUE_ARGS.has(argName)) {
if (equalsIndex < 0) {
while (typeof args[i + 1] === "string" && !args[i + 1]?.startsWith("-")) {
i += 1;
}
}
continue;
}
if (CLAUDE_SIDE_QUESTION_VALUE_ARGS.has(argName)) {
if (equalsIndex < 0) {
const maybeValue = args[i + 1];
if (typeof maybeValue === "string" && !maybeValue.startsWith("-")) {
i += 1;
}
}
continue;
}
normalized.push(arg);
}
return normalized;
}
function resolveClaudeCliSideQuestionExecutionArgs(baseArgs: readonly string[]): string[] {
return [
...stripClaudeSideQuestionConflictingArgs(stripClaudeEffortArgs(baseArgs)),
CLAUDE_SAFE_MODE_ARG,
CLAUDE_TOOLS_ARG,
CLAUDE_NO_TOOLS_VALUE,
CLAUDE_DISALLOWED_TOOLS_ARG,
CLAUDE_DENY_MCP_TOOLS_VALUE,
CLAUDE_STRICT_MCP_CONFIG_ARG,
CLAUDE_NO_SESSION_PERSISTENCE_ARG,
CLAUDE_MAX_TURNS_ARG,
"1",
CLAUDE_PERMISSION_MODE_ARG,
CLAUDE_DEFAULT_PERMISSION_MODE,
];
}
/** Resolve final Claude CLI execution args for one backend invocation. */
export function resolveClaudeCliExecutionArgs(
context: CliBackendResolveExecutionArgsContext,
): string[] {
if (context.executionMode === "side-question") {
return resolveClaudeCliSideQuestionExecutionArgs(context.baseArgs);
}
const effort = mapClaudeCliThinkingLevelToEffort(context.thinkingLevel);
if (!effort) {
return [...context.baseArgs];

View File

@@ -25,7 +25,7 @@ Use this skill when you need the `browser` tool for anything beyond a single pag
- Use the same `targetId` for follow-up actions so refs stay on the same tab.
- For durable Playwright refs, request `refs="aria"` when supported. If you receive `axN` refs from `snapshotFormat="aria"`, use them only after that same snapshot call; stale or unbound `axN` refs fail fast and need a fresh snapshot.
- Use `urls=true` when link text is ambiguous or a direct navigation target would avoid brittle clicks.
- Use `labels=true` on snapshot or screenshot when visual position matters. On Playwright-backed profiles, the response includes an `annotations` array (`{ref, number, role, name?, box}`) with each ref's bounding box in the captured image's coordinate space, so you can reason about position without re-snapshotting; screenshot labels can also combine with `fullPage=true` (CLI: `--full-page`) to label the whole document, or `ref` / `element` to clip to one element. `profile="user"` and other existing-session (chrome-mcp) profiles render an overlay into page screenshots but do not attach `annotations` or use the Playwright full-page/ref/element projection helper, so read positions from the labeled image itself on those profiles. The raw-CDP fallback (no Playwright) does not support labeled screenshots at all and returns a 501, so only request `labels` when Playwright is available.
- Use `labels=true` on snapshot or screenshot when visual position matters.
4. Act narrowly:
- Prefer `action="act"` with a ref from the latest snapshot.
- After navigation, modal changes, or form submission, snapshot again before the next action.

View File

@@ -486,7 +486,6 @@ export async function executeSnapshotAction(params: {
labels: snapshot.labels,
labelsCount: snapshot.labelsCount,
labelsSkipped: snapshot.labelsSkipped,
annotations: snapshot.annotations,
imagePath: snapshot.imagePath,
imageType: snapshot.imageType,
refsFallback,

View File

@@ -1,8 +1,6 @@
/**
* Shared result types for browser client action helpers.
*/
import type { AnnotationItem } from "./screenshot-annotate.js";
/** Generic success result for action endpoints. */
export type BrowserActionOk = { ok: true };
@@ -22,10 +20,4 @@ export type BrowserActionPathResult = {
labels?: boolean;
labelsCount?: number;
labelsSkipped?: number;
/**
* Per-ref bounding boxes when labels=true. Coordinates are in the
* captured image's space (viewport / fullpage / element-relative).
* Omitted when empty.
*/
annotations?: AnnotationItem[];
};

View File

@@ -18,7 +18,6 @@ import type {
} from "./client.types.js";
import { DEFAULT_BROWSER_SNAPSHOT_TIMEOUT_MS } from "./constants.js";
import type { BrowserDoctorReport } from "./doctor.js";
import type { AnnotationItem } from "./screenshot-annotate.js";
export type { BrowserStatus, BrowserTab, BrowserTransport } from "./client.types.js";
export type { BrowserDoctorCheck, BrowserDoctorReport } from "./doctor.js";
@@ -125,11 +124,6 @@ export type SnapshotResult =
labels?: boolean;
labelsCount?: number;
labelsSkipped?: number;
/**
* Per-ref bounding boxes when labels=true. Coordinates are in the
* captured image's space. Omitted when empty.
*/
annotations?: AnnotationItem[];
imagePath?: string;
imageType?: "png" | "jpeg";
blockedByDialog?: boolean;

View File

@@ -1,205 +0,0 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import {
installPwToolsCoreTestHooks,
setPwToolsCoreCurrentPage,
setPwToolsCoreCurrentRefLocator,
} from "./pw-tools-core.test-harness.js";
installPwToolsCoreTestHooks();
const mod = await import("./pw-tools-core.js");
type EvaluateArg = unknown;
function evaluateMockReturning(view: { x: number; y: number; width?: number; height?: number }) {
// Caller reads { x, y, width, height } in one evaluate; default to a normal
// desktop viewport so refs near the top stay in-viewport unless a test puts
// them out of range explicitly.
const result = { width: 1280, height: 720, ...view };
return vi.fn(async (arg: EvaluateArg) => {
if (typeof arg === "function") {
return result;
}
return true;
});
}
describe("screenshotWithLabelsViaPlaywright (viewport)", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("calls page.screenshot without fullPage and returns annotations", async () => {
const evaluate = evaluateMockReturning({ x: 0, y: 100 });
const screenshot = vi.fn(async () => Buffer.from("PNG"));
setPwToolsCoreCurrentPage({ evaluate, screenshot, url: () => "https://example.com" });
setPwToolsCoreCurrentRefLocator({
boundingBox: async () => ({ x: 10, y: 200, width: 50, height: 20 }),
});
const result = await mod.screenshotWithLabelsViaPlaywright({
cdpUrl: "http://127.0.0.1:18792",
targetId: "T1",
refs: { e1: { role: "button", name: "Submit" } },
type: "png",
});
expect(screenshot).toHaveBeenCalledWith(expect.objectContaining({ type: "png" }));
expect(screenshot).not.toHaveBeenCalledWith(expect.objectContaining({ fullPage: true }));
expect(result.annotations).toHaveLength(1);
expect(result.annotations[0]).toMatchObject({
ref: "e1",
number: 1,
role: "button",
name: "Submit",
});
// viewport-mode box = doc(box.x + scroll.x, box.y + scroll.y) - scroll = bbox
expect(result.annotations[0]?.box).toEqual({ x: 10, y: 200, width: 50, height: 20 });
expect(result.skipped).toBe(0);
});
it("runs the clear script even when screenshot throws", async () => {
const evaluate = evaluateMockReturning({ x: 0, y: 0 });
const screenshot = vi.fn(async () => {
throw new Error("boom");
});
setPwToolsCoreCurrentPage({ evaluate, screenshot });
setPwToolsCoreCurrentRefLocator({
boundingBox: async () => ({ x: 0, y: 0, width: 1, height: 1 }),
});
await expect(
mod.screenshotWithLabelsViaPlaywright({
cdpUrl: "http://127.0.0.1:18792",
targetId: "T1",
refs: { e1: { role: "button" } },
}),
).rejects.toThrow(/boom/);
// The clear script must have run (string evaluate calls include the overlay attr)
const clearCalls = evaluate.mock.calls.filter(
([arg]) => typeof arg === "string" && arg.includes("data-openclaw-labels"),
);
// inject + clear = at least 2 string evaluations
expect(clearCalls.length).toBeGreaterThanOrEqual(2);
});
it("counts off-viewport refs as skipped but still surfaces them in annotations", async () => {
const evaluate = evaluateMockReturning({ x: 0, y: 0, width: 1280, height: 720 });
const screenshot = vi.fn(async () => Buffer.from("PNG"));
setPwToolsCoreCurrentPage({ evaluate, screenshot });
// bbox is far below the viewport (y: 5000): not drawn, but still reported
// so callers keep the position and a non-zero skipped count.
setPwToolsCoreCurrentRefLocator({
boundingBox: async () => ({ x: 0, y: 5000, width: 50, height: 20 }),
});
const result = await mod.screenshotWithLabelsViaPlaywright({
cdpUrl: "http://127.0.0.1:18792",
targetId: "T1",
refs: { e1: { role: "button" } },
});
expect(result.skipped).toBe(1);
expect(result.labels).toBe(0);
expect(result.annotations).toHaveLength(1);
expect(result.annotations[0]?.ref).toBe("e1");
});
});
describe("screenshotWithLabelsViaPlaywright (fullpage)", () => {
beforeEach(() => vi.clearAllMocks());
it("forwards fullPage:true to page.screenshot and uses doc-space annotations", async () => {
const evaluate = evaluateMockReturning({ x: 0, y: 1000 });
const screenshot = vi.fn(async () => Buffer.from("FULL"));
setPwToolsCoreCurrentPage({ evaluate, screenshot });
setPwToolsCoreCurrentRefLocator({
boundingBox: async () => ({ x: 10, y: 200, width: 50, height: 20 }),
});
const result = await mod.screenshotWithLabelsViaPlaywright({
cdpUrl: "http://127.0.0.1:18792",
targetId: "T1",
refs: { e1: { role: "button" } },
fullPage: true,
});
expect(screenshot).toHaveBeenCalledWith(expect.objectContaining({ fullPage: true }));
// doc-space: scroll y=1000 + bbox y=200 = 1200
expect(result.annotations[0]?.box.y).toBe(1200);
expect(result.annotations[0]?.box.x).toBe(10);
});
});
describe("screenshotWithLabelsViaPlaywright (element/ref)", () => {
beforeEach(() => vi.clearAllMocks());
it("uses refLocator.screenshot for ref mode and projects relative to element", async () => {
const evaluate = evaluateMockReturning({ x: 0, y: 0 });
// First call resolves the element rect (container), second resolves e1 annotation bbox.
const boundingBox = vi
.fn<() => Promise<{ x: number; y: number; width: number; height: number } | null>>()
.mockResolvedValueOnce({ x: 50, y: 100, width: 200, height: 300 })
.mockResolvedValueOnce({ x: 60, y: 110, width: 30, height: 20 });
const elementScreenshot = vi.fn(async () => Buffer.from("ELEM"));
setPwToolsCoreCurrentPage({ evaluate, screenshot: vi.fn() });
setPwToolsCoreCurrentRefLocator({ boundingBox, screenshot: elementScreenshot });
const result = await mod.screenshotWithLabelsViaPlaywright({
cdpUrl: "http://127.0.0.1:18792",
targetId: "T1",
refs: { e1: { role: "button" } },
ref: "container",
});
expect(elementScreenshot).toHaveBeenCalledTimes(1);
// Element-relative: doc(60,110) - elementRect(50,100) = (10,10)
expect(result.annotations).toHaveLength(1);
expect(result.annotations[0]?.box).toEqual({ x: 10, y: 10, width: 30, height: 20 });
});
it("throws when ref/element cannot be resolved", async () => {
const evaluate = evaluateMockReturning({ x: 0, y: 0 });
setPwToolsCoreCurrentPage({ evaluate, screenshot: vi.fn() });
setPwToolsCoreCurrentRefLocator({
boundingBox: async () => null,
screenshot: vi.fn(),
});
await expect(
mod.screenshotWithLabelsViaPlaywright({
cdpUrl: "http://127.0.0.1:18792",
targetId: "T1",
refs: { e1: { role: "button" } },
ref: "missing",
}),
).rejects.toThrow(/element not found/i);
});
});
describe("screenshotWithLabelsViaPlaywright (skipped accounting)", () => {
beforeEach(() => vi.clearAllMocks());
it("counts refs whose boundingBox is null toward skipped", async () => {
const evaluate = evaluateMockReturning({ x: 0, y: 0 });
const screenshot = vi.fn(async () => Buffer.from("PNG"));
setPwToolsCoreCurrentPage({ evaluate, screenshot });
// Two refs: first returns a box, second returns null (e.g. element detached).
const boundingBox = vi
.fn<() => Promise<{ x: number; y: number; width: number; height: number } | null>>()
.mockResolvedValueOnce({ x: 10, y: 20, width: 30, height: 40 })
.mockResolvedValueOnce(null);
setPwToolsCoreCurrentRefLocator({ boundingBox });
const result = await mod.screenshotWithLabelsViaPlaywright({
cdpUrl: "http://127.0.0.1:18792",
targetId: "T1",
refs: { e1: { role: "button" }, e2: { role: "link" } },
});
expect(result.annotations).toHaveLength(1);
expect(result.annotations[0]?.ref).toBe("e1");
expect(result.skipped).toBe(1);
});
});

View File

@@ -41,15 +41,6 @@ import {
toAIFriendlyError,
} from "./pw-tools-core.shared.js";
import { closePageViaPlaywright, resizeViewportViaPlaywright } from "./pw-tools-core.snapshot.js";
import {
ANNOTATION_MAX_LABELS_DEFAULT,
type AnnotationItem,
buildOverlayClearScript,
buildOverlayInjectionScript,
type CoordinateSpace,
planAnnotations,
type RawAnnotationInput,
} from "./screenshot-annotate.js";
type TargetOpts = {
cdpUrl: string;
@@ -1296,15 +1287,7 @@ export async function screenshotWithLabelsViaPlaywright(opts: {
maxLabels?: number;
type?: "png" | "jpeg";
timeoutMs?: number;
fullPage?: boolean;
ref?: string;
element?: string;
}): Promise<{
buffer: Buffer;
labels: number;
skipped: number;
annotations: AnnotationItem[];
}> {
}): Promise<{ buffer: Buffer; labels: number; skipped: number }> {
const page = await getPageForTargetId(opts);
ensurePageState(page);
restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
@@ -1312,151 +1295,119 @@ export async function screenshotWithLabelsViaPlaywright(opts: {
const maxLabels =
typeof opts.maxLabels === "number" && Number.isFinite(opts.maxLabels)
? Math.max(1, Math.floor(opts.maxLabels))
: ANNOTATION_MAX_LABELS_DEFAULT;
: 150;
const refKey = normalizeOptionalString(opts.ref) ?? undefined;
const elementSelector = normalizeOptionalString(opts.element) ?? undefined;
const space: CoordinateSpace = opts.fullPage
? "fullpage"
: refKey || elementSelector
? "element"
: "viewport";
// Read scroll + viewport size. Scroll converts Playwright's viewport-space
// boundingBoxes into document-space inputs; the viewport size lets the helper
// restore the shipped `labelsSkipped` semantics by counting off-viewport refs
// as skipped (in viewport capture mode).
const view = await page.evaluate(() => ({
x: window.scrollX || 0,
y: window.scrollY || 0,
const viewport = await page.evaluate(() => ({
scrollX: window.scrollX || 0,
scrollY: window.scrollY || 0,
width: window.innerWidth || 0,
height: window.innerHeight || 0,
}));
const scroll = { x: view.x, y: view.y };
let elementRect: { x: number; y: number; width: number; height: number } | undefined;
if (space === "element") {
const box = await resolveElementBoundingBoxForLabels(page, refKey, elementSelector);
if (!box) {
throw new Error(
`screenshotWithLabelsViaPlaywright: element not found for ${
refKey ? `ref="${refKey}"` : `selector="${elementSelector ?? ""}"`
}`,
);
}
// Convert viewport-space bbox to document space.
elementRect = {
x: box.x + scroll.x,
y: box.y + scroll.y,
width: box.width,
height: box.height,
};
}
const refs = Object.keys(opts.refs ?? {});
const boxes: Array<{ ref: string; x: number; y: number; w: number; h: number }> = [];
let skipped = 0;
const refKeys = Object.keys(opts.refs ?? {});
const inputs: RawAnnotationInput[] = [];
let bboxFailures = 0;
for (const ref of refKeys) {
const box = await refLocator(page, ref)
.boundingBox()
.catch(() => null);
if (!box) {
bboxFailures += 1;
for (const ref of refs) {
if (boxes.length >= maxLabels) {
skipped += 1;
continue;
}
inputs.push({
ref,
role: opts.refs[ref].role,
name: opts.refs[ref].name,
doc: {
x: box.x + scroll.x,
y: box.y + scroll.y,
width: box.width,
height: box.height,
},
});
try {
const box = await refLocator(page, ref).boundingBox();
if (!box) {
skipped += 1;
continue;
}
const x0 = box.x;
const y0 = box.y;
const x1 = box.x + box.width;
const y1 = box.y + box.height;
const vx0 = viewport.scrollX;
const vy0 = viewport.scrollY;
const vx1 = viewport.scrollX + viewport.width;
const vy1 = viewport.scrollY + viewport.height;
if (x1 < vx0 || x0 > vx1 || y1 < vy0 || y0 > vy1) {
skipped += 1;
continue;
}
boxes.push({
ref,
x: x0 - viewport.scrollX,
y: y0 - viewport.scrollY,
w: Math.max(1, box.width),
h: Math.max(1, box.height),
});
} catch {
skipped += 1;
}
}
const plan = planAnnotations({
inputs,
space,
scroll,
viewport: { width: view.width, height: view.height },
elementRect,
maxLabels,
});
try {
if (plan.overlayItems.length > 0) {
const captureY = space === "element" ? elementRect?.y : space === "viewport" ? scroll.y : 0;
await page.evaluate(buildOverlayInjectionScript({ items: plan.overlayItems, captureY }));
if (boxes.length > 0) {
await page.evaluate((labels) => {
const existing = document.querySelectorAll("[data-openclaw-labels]");
existing.forEach((el) => el.remove());
const root = document.createElement("div");
root.setAttribute("data-openclaw-labels", "1");
root.style.position = "fixed";
root.style.left = "0";
root.style.top = "0";
root.style.zIndex = "2147483647";
root.style.pointerEvents = "none";
root.style.fontFamily =
'"SF Mono","SFMono-Regular",Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace';
const clamp = (value: number, min: number, max: number) =>
Math.min(max, Math.max(min, value));
for (const label of labels) {
const box = document.createElement("div");
box.setAttribute("data-openclaw-labels", "1");
box.style.position = "absolute";
box.style.left = `${label.x}px`;
box.style.top = `${label.y}px`;
box.style.width = `${label.w}px`;
box.style.height = `${label.h}px`;
box.style.border = "2px solid #ffb020";
box.style.boxSizing = "border-box";
const tag = document.createElement("div");
tag.setAttribute("data-openclaw-labels", "1");
tag.textContent = label.ref;
tag.style.position = "absolute";
tag.style.left = `${label.x}px`;
tag.style.top = `${clamp(label.y - 18, 0, 20000)}px`;
tag.style.background = "#ffb020";
tag.style.color = "#1a1a1a";
tag.style.fontSize = "12px";
tag.style.lineHeight = "14px";
tag.style.padding = "1px 4px";
tag.style.borderRadius = "3px";
tag.style.boxShadow = "0 1px 2px rgba(0,0,0,0.35)";
tag.style.whiteSpace = "nowrap";
root.appendChild(box);
root.appendChild(tag);
}
document.documentElement.appendChild(root);
}, boxes);
}
const buffer =
space === "element"
? await captureElementScreenshotForLabels(
page,
refKey,
elementSelector,
type,
opts.timeoutMs,
)
: await page.screenshot({
type,
fullPage: Boolean(opts.fullPage),
timeout: opts.timeoutMs,
});
return {
// `labels` reports overlay boxes actually drawn on the captured image
// (in-viewport, within budget); off-viewport refs are surfaced via
// `annotations` but not drawn, and are reflected in `skipped`.
buffer,
labels: plan.overlayItems.length,
skipped: plan.skipped + bboxFailures,
annotations: plan.annotations,
};
const buffer = await page.screenshot({ type, timeout: opts.timeoutMs });
return { buffer, labels: boxes.length, skipped };
} finally {
await page.evaluate(buildOverlayClearScript()).catch(() => {});
await page
.evaluate(() => {
const existing = document.querySelectorAll("[data-openclaw-labels]");
existing.forEach((el) => el.remove());
})
.catch(() => {});
}
}
async function resolveElementBoundingBoxForLabels(
page: Page,
refKey: string | undefined,
cssSelector: string | undefined,
): Promise<{ x: number; y: number; width: number; height: number } | null> {
if (refKey) {
try {
return await refLocator(page, refKey).boundingBox();
} catch {
return null;
}
}
if (cssSelector) {
try {
return await page.locator(cssSelector).first().boundingBox();
} catch {
return null;
}
}
return null;
}
async function captureElementScreenshotForLabels(
page: Page,
refKey: string | undefined,
cssSelector: string | undefined,
type: "png" | "jpeg",
timeoutMs: number | undefined,
): Promise<Buffer> {
if (refKey) {
return await refLocator(page, refKey).screenshot({ type, timeout: timeoutMs });
}
if (cssSelector) {
return await page.locator(cssSelector).first().screenshot({ type, timeout: timeoutMs });
}
throw new Error("captureElementScreenshotForLabels: requires refKey or cssSelector");
}
/** Sets file inputs for a role ref or selector with strict existing-path checks. */
export async function setInputFilesViaPlaywright(opts: {
cdpUrl: string;

View File

@@ -5,7 +5,6 @@
* navigation policy checks, media storage, and screenshot normalization.
*/
import path from "node:path";
import { getImageMetadata } from "../../media/media-services.js";
import { ensureMediaDir, saveMediaBuffer } from "../../media/store.js";
import { captureScreenshot, snapshotAria, snapshotRoleViaCdp } from "../cdp.js";
import {
@@ -25,8 +24,6 @@ import {
assertBrowserNavigationResultAllowed,
} from "../navigation-guard.js";
import { getBrowserProfileCapabilities } from "../profile-capabilities.js";
import type { AnnotationItem } from "../screenshot-annotate.js";
import { scaleAnnotations } from "../screenshot-annotate.js";
import {
DEFAULT_BROWSER_SCREENSHOT_MAX_BYTES,
DEFAULT_BROWSER_SCREENSHOT_MAX_SIDE,
@@ -195,24 +192,11 @@ async function saveNormalizedScreenshotResponse(params: {
labels?: boolean;
labelsCount?: number;
labelsSkipped?: number;
annotations?: AnnotationItem[];
}) {
// Measure original dimensions BEFORE normalization so we can rescale
// annotation coordinates if the response pipeline shrinks the image
// (longest-side or byte-budget cap). Annotation boxes are in the captured
// image's pixel space, so they would otherwise drift from the saved media.
const originalMeta = params.annotations?.length
? ((await getImageMetadata(params.buffer)) ?? undefined)
: undefined;
const normalized = await normalizeBrowserScreenshot(params.buffer, {
maxSide: DEFAULT_BROWSER_SCREENSHOT_MAX_SIDE,
maxBytes: DEFAULT_BROWSER_SCREENSHOT_MAX_BYTES,
});
const annotations = await rescaleAnnotationsForNormalization({
annotations: params.annotations,
originalMeta,
normalizedBuffer: normalized.buffer,
});
await saveBrowserMediaResponse({
res: params.res,
buffer: normalized.buffer,
@@ -223,39 +207,9 @@ async function saveNormalizedScreenshotResponse(params: {
labels: params.labels,
labelsCount: params.labelsCount,
labelsSkipped: params.labelsSkipped,
annotations,
});
}
/**
* Keep annotation coordinates aligned with the saved media after
* normalizeBrowserScreenshot. Returns the original annotations unchanged
* when normalization did not change the image dimensions, or when image
* metadata is unavailable (best-effort: better to ship pre-resize coords
* than to drop the field entirely).
*/
async function rescaleAnnotationsForNormalization(params: {
annotations?: AnnotationItem[];
originalMeta?: { width?: number; height?: number };
normalizedBuffer: Buffer;
}): Promise<AnnotationItem[] | undefined> {
if (!params.annotations || params.annotations.length === 0) {
return params.annotations;
}
const orig = params.originalMeta;
if (!orig?.width || !orig?.height) {
return params.annotations;
}
const next = await getImageMetadata(params.normalizedBuffer);
if (!next?.width || !next?.height) {
return params.annotations;
}
if (next.width === orig.width && next.height === orig.height) {
return params.annotations;
}
return scaleAnnotations(params.annotations, next.width / orig.width, next.height / orig.height);
}
async function saveBrowserMediaResponse(params: {
res: BrowserResponse;
buffer: Buffer;
@@ -266,7 +220,6 @@ async function saveBrowserMediaResponse(params: {
labels?: boolean;
labelsCount?: number;
labelsSkipped?: number;
annotations?: AnnotationItem[];
}) {
await ensureMediaDir();
const saved = await saveMediaBuffer(
@@ -283,9 +236,6 @@ async function saveBrowserMediaResponse(params: {
...(params.labels ? { labels: true } : {}),
...(typeof params.labelsCount === "number" ? { labelsCount: params.labelsCount } : {}),
...(typeof params.labelsSkipped === "number" ? { labelsSkipped: params.labelsSkipped } : {}),
...(params.annotations && params.annotations.length > 0
? { annotations: params.annotations }
: {}),
});
}
@@ -528,9 +478,6 @@ export function registerBrowserAgentSnapshotRoutes(
refs: snap.refs,
type,
timeoutMs,
fullPage,
ref,
element,
});
await saveNormalizedScreenshotResponse({
res,
@@ -541,7 +488,6 @@ export function registerBrowserAgentSnapshotRoutes(
labels: true,
labelsCount: labeled.labels,
labelsSkipped: labeled.skipped,
annotations: labeled.annotations,
});
return;
}
@@ -797,18 +743,10 @@ export function registerBrowserAgentSnapshotRoutes(
type: "png",
timeoutMs: plan.timeoutMs,
});
const originalMeta = labeled.annotations.length
? ((await getImageMetadata(labeled.buffer)) ?? undefined)
: undefined;
const normalized = await normalizeBrowserScreenshot(labeled.buffer, {
maxSide: DEFAULT_BROWSER_SCREENSHOT_MAX_SIDE,
maxBytes: DEFAULT_BROWSER_SCREENSHOT_MAX_BYTES,
});
const scaledAnnotations = await rescaleAnnotationsForNormalization({
annotations: labeled.annotations,
originalMeta,
normalizedBuffer: normalized.buffer,
});
await ensureMediaDir();
const saved = await saveMediaBuffer(
normalized.buffer,
@@ -826,9 +764,6 @@ export function registerBrowserAgentSnapshotRoutes(
labels: true,
labelsCount: labeled.labels,
labelsSkipped: labeled.skipped,
...(scaledAnnotations && scaledAnnotations.length > 0
? { annotations: scaledAnnotations }
: {}),
imagePath: path.resolve(saved.path),
imageType,
...snap,

View File

@@ -1,345 +0,0 @@
import { describe, expect, it } from "vitest";
import {
ANNOTATION_OVERLAY_ATTR,
type AnnotationItem,
buildOverlayClearScript,
buildOverlayInjectionScript,
planAnnotations,
type RawAnnotationInput,
refToNumber,
scaleAnnotations,
} from "./screenshot-annotate.js";
const sampleInputs: RawAnnotationInput[] = [
{
ref: "e1",
role: "button",
name: "Submit",
doc: { x: 100, y: 200, width: 50, height: 20 },
},
{
ref: "e2",
role: "link",
doc: { x: 300, y: 1500, width: 80, height: 18 },
},
];
describe("refToNumber", () => {
it("extracts number from `e<N>` form", () => {
expect(refToNumber("e12")).toBe(12);
expect(refToNumber("e0")).toBe(0);
});
it("extracts number from `ax<N>` form", () => {
expect(refToNumber("ax12")).toBe(12);
});
it("extracts number from bare numeric form", () => {
expect(refToNumber("12")).toBe(12);
});
it("returns 0 for non-numeric refs", () => {
expect(refToNumber("foo")).toBe(0);
expect(refToNumber("")).toBe(0);
});
});
describe("planAnnotations - viewport mode", () => {
it("subtracts scroll from doc coords", () => {
const plan = planAnnotations({
inputs: sampleInputs,
space: "viewport",
scroll: { x: 0, y: 1000 },
});
expect(plan.annotations).toHaveLength(2);
expect(plan.annotations[0]).toEqual({
ref: "e1",
number: 1,
role: "button",
name: "Submit",
box: { x: 100, y: -800, width: 50, height: 20 },
});
expect(plan.annotations[1]).toEqual({
ref: "e2",
number: 2,
role: "link",
box: { x: 300, y: 500, width: 80, height: 18 },
});
expect(plan.skipped).toBe(0);
});
it("keeps overlay items in document space regardless of mode", () => {
const plan = planAnnotations({
inputs: sampleInputs,
space: "viewport",
scroll: { x: 0, y: 1000 },
});
expect(plan.overlayItems).toEqual([
{ ref: "e1", x: 100, y: 200, w: 50, h: 20 },
{ ref: "e2", x: 300, y: 1500, w: 80, h: 18 },
]);
});
it("omits empty name field", () => {
const plan = planAnnotations({
inputs: [{ ref: "e1", role: "button", name: "", doc: { x: 0, y: 0, width: 1, height: 1 } }],
space: "viewport",
scroll: { x: 0, y: 0 },
});
expect(plan.annotations[0]).not.toHaveProperty("name");
});
it("throws when scroll missing in viewport mode", () => {
expect(() => planAnnotations({ inputs: sampleInputs, space: "viewport" })).toThrow(/scroll/);
});
});
describe("planAnnotations - viewport off-screen accounting", () => {
it("counts off-viewport refs as skipped but keeps them in annotations when viewport size is given", () => {
const plan = planAnnotations({
inputs: [
{ ref: "e1", role: "button", doc: { x: 10, y: 50, width: 40, height: 20 } }, // in viewport
{ ref: "e2", role: "link", doc: { x: 10, y: 5000, width: 40, height: 20 } }, // below viewport
],
space: "viewport",
scroll: { x: 0, y: 0 },
viewport: { width: 1280, height: 720 },
});
// Only the in-viewport ref is drawn.
expect(plan.overlayItems.map((o) => o.ref)).toEqual(["e1"]);
// Both refs are surfaced for callers (off-viewport box can be out of image).
expect(plan.annotations.map((a) => a.ref)).toEqual(["e1", "e2"]);
// The off-viewport ref raises skipped, preserving the shipped contract.
expect(plan.skipped).toBe(1);
});
it("does not count off-viewport refs when viewport size is omitted", () => {
const plan = planAnnotations({
inputs: [{ ref: "e2", role: "link", doc: { x: 10, y: 5000, width: 40, height: 20 } }],
space: "viewport",
scroll: { x: 0, y: 0 },
});
expect(plan.skipped).toBe(0);
expect(plan.overlayItems).toHaveLength(1);
expect(plan.annotations).toHaveLength(1);
});
});
describe("planAnnotations - fullpage mode", () => {
it("returns box equal to doc (document coordinates)", () => {
const plan = planAnnotations({ inputs: sampleInputs, space: "fullpage" });
expect(plan.annotations[0].box).toEqual({ x: 100, y: 200, width: 50, height: 20 });
expect(plan.annotations[1].box).toEqual({ x: 300, y: 1500, width: 80, height: 18 });
});
it("does not require scroll", () => {
expect(() => planAnnotations({ inputs: sampleInputs, space: "fullpage" })).not.toThrow();
});
});
describe("planAnnotations - element mode", () => {
const elementRect = { x: 50, y: 100, width: 200, height: 300 };
it("projects box relative to element top-left", () => {
const plan = planAnnotations({
inputs: [{ ref: "e1", role: "button", doc: { x: 60, y: 110, width: 40, height: 20 } }],
space: "element",
elementRect,
});
expect(plan.annotations[0].box).toEqual({ x: 10, y: 10, width: 40, height: 20 });
});
it("filters out inputs that do not overlap element rect", () => {
const plan = planAnnotations({
inputs: [
{ ref: "e1", role: "button", doc: { x: 60, y: 110, width: 40, height: 20 } }, // inside
{ ref: "e2", role: "link", doc: { x: 500, y: 500, width: 40, height: 20 } }, // outside
],
space: "element",
elementRect,
});
expect(plan.annotations).toHaveLength(1);
expect(plan.annotations[0].ref).toBe("e1");
expect(plan.overlayItems).toHaveLength(1);
});
it("throws when elementRect missing", () => {
expect(() => planAnnotations({ inputs: [], space: "element" })).toThrow(/elementRect/);
});
});
describe("planAnnotations - maxLabels", () => {
it("truncates to maxLabels and reports skipped", () => {
const inputs = Array.from({ length: 5 }, (_, i) => ({
ref: `e${i + 1}`,
role: "button",
doc: { x: 0, y: i * 10, width: 5, height: 5 },
}));
const plan = planAnnotations({ inputs, space: "fullpage", maxLabels: 2 });
expect(plan.annotations).toHaveLength(2);
expect(plan.overlayItems).toHaveLength(2);
expect(plan.skipped).toBe(3);
});
it("uses ANNOTATION_MAX_LABELS_DEFAULT when not specified", () => {
const inputs = Array.from({ length: 200 }, (_, i) => ({
ref: `e${i + 1}`,
role: "button",
doc: { x: 0, y: i, width: 5, height: 5 },
}));
const plan = planAnnotations({ inputs, space: "fullpage" });
expect(plan.annotations).toHaveLength(150);
expect(plan.skipped).toBe(50);
});
});
describe("buildOverlayInjectionScript", () => {
it("returns a self-contained IIFE", () => {
const script = buildOverlayInjectionScript({
items: [{ ref: "e1", x: 100, y: 200, w: 50, h: 20 }],
});
expect(script).toMatch(/^\(\s*\(\s*\)\s*=>\s*\{/);
expect(script).toMatch(/\}\s*\)\s*\(\s*\)\s*;?\s*$/);
});
it("embeds the overlay attr", () => {
const script = buildOverlayInjectionScript({ items: [] });
expect(script).toContain(ANNOTATION_OVERLAY_ATTR);
});
it("embeds each item's ref text and coordinates", () => {
const script = buildOverlayInjectionScript({
items: [
{ ref: "e1", x: 100, y: 200, w: 50, h: 20 },
{ ref: "ax42", x: 999, y: 1500, w: 80, h: 18 },
],
});
expect(script).toMatch(/"ref":\s*"e1"/);
expect(script).toMatch(/"ref":\s*"ax42"/);
expect(script).toMatch(/"x":\s*100/);
expect(script).toMatch(/"x":\s*999/);
});
it("handles empty items without throwing", () => {
expect(() => buildOverlayInjectionScript({ items: [] })).not.toThrow();
});
it("rounds coordinates to integers", () => {
const script = buildOverlayInjectionScript({
items: [{ ref: "e1", x: 100.7, y: 200.4, w: 50.6, h: 20.1 }],
});
expect(script).toMatch(/"x":\s*101/); // 100.7 -> 101
expect(script).toMatch(/"y":\s*200/); // 200.4 -> 200
});
it("clamps zero/negative-size boxes to 1px so they remain visible", () => {
const script = buildOverlayInjectionScript({
items: [{ ref: "e1", x: 10, y: 10, w: 0, h: 0 }],
});
expect(script).toMatch(/"w":\s*1/);
expect(script).toMatch(/"h":\s*1/);
});
it("escapes hostile ref characters via JSON.stringify (no breakout)", () => {
const hostile = 'e1");alert(1);//';
const script = buildOverlayInjectionScript({
items: [{ ref: hostile, x: 0, y: 0, w: 1, h: 1 }],
});
// The hostile `"` MUST be escaped as `\"` inside the JSON literal.
expect(script).toContain('"e1\\");alert(1);//"');
// The unescaped breakout MUST NOT appear anywhere in the script as a
// bare statement that would terminate the JSON literal early.
expect(script).not.toContain('e1");alert(1);');
});
it("flips label below the box when y < 14 (no headroom)", () => {
const script = buildOverlayInjectionScript({
items: [{ ref: "e1", x: 0, y: 5, w: 10, h: 10 }],
});
// labelTop = relativeY < 14 ? it.y + 2 : it.y - 14
// The expression literal `relativeY < 14 ? (it.y + 2) : (it.y - 14)` is in the script.
expect(script).toContain("relativeY < 14 ? (it.y + 2) : (it.y - 14)");
});
it("uses capture-relative y when deciding whether to flip labels below boxes", () => {
const script = buildOverlayInjectionScript({
items: [{ ref: "e1", x: 0, y: 1005, w: 10, h: 10 }],
captureY: 1000,
});
expect(script).toContain("var captureY = 1000;");
expect(script).toContain("var relativeY = it.y - captureY;");
expect(script).toContain("relativeY < 14 ? (it.y + 2) : (it.y - 14)");
});
});
describe("buildOverlayClearScript", () => {
it("returns an IIFE selecting overlay attr", () => {
const script = buildOverlayClearScript();
expect(script).toContain(`[${ANNOTATION_OVERLAY_ATTR}]`);
expect(script).toMatch(/^\(\s*\(\s*\)\s*=>\s*\{/);
});
});
describe("scaleAnnotations", () => {
const sample: AnnotationItem[] = [
{
ref: "e1",
number: 1,
role: "button",
name: "Submit",
box: { x: 100, y: 200, width: 50, height: 20 },
},
];
it("returns identity (structural copy) when both factors are 1", () => {
const out = scaleAnnotations(sample, 1, 1);
expect(out[0]).toEqual(sample[0]);
expect(out[0]).not.toBe(sample[0]);
expect(out[0]?.box).not.toBe(sample[0]?.box);
});
it("scales box dimensions by independent x/y factors", () => {
const out = scaleAnnotations(sample, 0.5, 0.485);
expect(out[0]?.box).toEqual({
x: 50,
y: 97,
width: 25,
height: 10,
});
});
it("clamps width/height to a minimum of 1 to avoid disappearing labels", () => {
const tiny: AnnotationItem[] = [
{
ref: "e1",
number: 1,
role: "button",
box: { x: 0, y: 0, width: 1, height: 1 },
},
];
const out = scaleAnnotations(tiny, 0.1, 0.1);
expect(out[0]?.box.width).toBeGreaterThanOrEqual(1);
expect(out[0]?.box.height).toBeGreaterThanOrEqual(1);
});
it("returns identity (structural copy) for invalid factors", () => {
const out = scaleAnnotations(sample, Number.NaN, 0.5);
expect(out[0]?.box).toEqual(sample[0]?.box);
const out2 = scaleAnnotations(sample, 0, 0.5);
expect(out2[0]?.box).toEqual(sample[0]?.box);
const out3 = scaleAnnotations(sample, -1, 1);
expect(out3[0]?.box).toEqual(sample[0]?.box);
});
it("preserves ref/number/role/name fields verbatim", () => {
const out = scaleAnnotations(sample, 0.5, 0.5);
expect(out[0]?.ref).toBe("e1");
expect(out[0]?.number).toBe(1);
expect(out[0]?.role).toBe("button");
expect(out[0]?.name).toBe("Submit");
});
});

View File

@@ -1,282 +0,0 @@
// extensions/browser/src/browser/screenshot-annotate.ts
//
// Pure helper module for screenshot label annotations.
// Has no Playwright / CDP / page dependency: takes document-space inputs,
// returns coordinate-projected annotations + IIFE strings the caller can
// hand to page.evaluate / Runtime.evaluate.
//
// Used by:
// - pw-tools-core.interactions.ts (Playwright path, M1.2-a)
// - planned: raw-CDP path in M1.2-b
//
// chrome-mcp path keeps its own inline overlay (renderChromeMcpLabels) for now.
export const ANNOTATION_OVERLAY_ATTR = "data-openclaw-labels";
export const ANNOTATION_OVERLAY_ROOT_ID = "__openclaw-annotations__";
export const ANNOTATION_MAX_LABELS_DEFAULT = 150;
export type CoordinateSpace = "viewport" | "fullpage" | "element";
export interface RawAnnotationInput {
ref: string;
role: string;
name?: string;
/** Bounding box in document coordinates (viewport top-left + scroll). */
doc: { x: number; y: number; width: number; height: number };
}
export interface AnnotationBox {
x: number;
y: number;
width: number;
height: number;
}
export interface AnnotationItem {
ref: string;
number: number;
role: string;
name?: string;
box: AnnotationBox;
}
export interface OverlayItem {
ref: string;
x: number;
y: number;
w: number;
h: number;
}
export interface AnnotationPlan {
/** Always document-space items, fed to buildOverlayInjectionScript. */
overlayItems: OverlayItem[];
/** Items projected into the capture mode's image-space coordinates. */
annotations: AnnotationItem[];
/** Refs dropped because of maxLabels truncation. */
skipped: number;
}
export interface PlanAnnotationsParams {
inputs: RawAnnotationInput[];
space: CoordinateSpace;
/** Required when space === "viewport". */
scroll?: { x: number; y: number };
/**
* Viewport size (CSS px). Only meaningful when space === "viewport". When
* provided, refs whose document box falls outside the current viewport rect
* (`scroll` + this size) are counted as skipped instead of drawn, preserving
* the shipped `labelsSkipped` contract. Omit it to disable that accounting.
*/
viewport?: { width: number; height: number };
/** Required when space === "element". */
elementRect?: { x: number; y: number; width: number; height: number };
maxLabels?: number;
}
export function refToNumber(ref: string): number {
const match = ref.match(/(\d+)/);
if (!match) {
return 0;
}
const n = Number(match[1]);
return Number.isFinite(n) ? n : 0;
}
export function planAnnotations(params: PlanAnnotationsParams): AnnotationPlan {
const maxLabels = params.maxLabels ?? ANNOTATION_MAX_LABELS_DEFAULT;
if (params.space === "viewport" && !params.scroll) {
throw new Error("planAnnotations: scroll is required when space is 'viewport'");
}
if (params.space === "element" && !params.elementRect) {
throw new Error("planAnnotations: elementRect is required when space is 'element'");
}
// Element-mode filter: discard inputs that do not overlap the element rect.
let kept = params.inputs;
if (params.space === "element" && params.elementRect) {
const er = params.elementRect;
kept = params.inputs.filter((input) => rectsOverlap(input.doc, er));
}
// Viewport capture only shows refs inside the current viewport rect. An
// off-viewport ref is still surfaced in `annotations` (with its real,
// possibly out-of-image box) so callers can locate it, but it is not drawn
// and is counted as skipped. This keeps the shipped `labelsSkipped` meaning
// ("refs not present in the captured viewport image") instead of silently
// narrowing it. Only applied when the caller supplies the viewport size;
// without it we cannot decide off-screen state and skip nothing.
const viewportRect =
params.space === "viewport" && params.scroll && params.viewport
? {
x: params.scroll.x,
y: params.scroll.y,
width: params.viewport.width,
height: params.viewport.height,
}
: undefined;
const overlayItems: OverlayItem[] = [];
const annotations: AnnotationItem[] = [];
let skipped = 0;
for (const input of kept) {
if (viewportRect && !rectsOverlap(input.doc, viewportRect)) {
// Outside the captured viewport: count as skipped (compat) but still
// report the annotation; do not draw it or consume the label budget.
skipped += 1;
annotations.push(toAnnotation(input, params));
continue;
}
if (overlayItems.length >= maxLabels) {
skipped += 1;
continue;
}
overlayItems.push({
ref: input.ref,
x: input.doc.x,
y: input.doc.y,
w: input.doc.width,
h: input.doc.height,
});
annotations.push(toAnnotation(input, params));
}
return { overlayItems, annotations, skipped };
}
function toAnnotation(input: RawAnnotationInput, params: PlanAnnotationsParams): AnnotationItem {
return {
ref: input.ref,
number: refToNumber(input.ref),
role: input.role,
...(input.name ? { name: input.name } : {}),
box: projectBox(input.doc, params),
};
}
function projectBox(
doc: { x: number; y: number; width: number; height: number },
params: PlanAnnotationsParams,
): AnnotationBox {
if (params.space === "viewport") {
const scroll = params.scroll!;
return {
x: doc.x - scroll.x,
y: doc.y - scroll.y,
width: doc.width,
height: doc.height,
};
}
if (params.space === "element") {
const er = params.elementRect!;
// NOTE: width/height pass through unchanged even when the input rect
// partially extends past the element. The capture backend (e.g.
// locator.screenshot) is responsible for clipping; the box may have
// negative x/y or extend past elementRect width/height for partial overlaps.
return {
x: doc.x - er.x,
y: doc.y - er.y,
width: doc.width,
height: doc.height,
};
}
// fullpage: document coordinates as-is
return { x: doc.x, y: doc.y, width: doc.width, height: doc.height };
}
function rectsOverlap(
a: { x: number; y: number; width: number; height: number },
b: { x: number; y: number; width: number; height: number },
): boolean {
return a.x < b.x + b.width && a.x + a.width > b.x && a.y < b.y + b.height && a.y + a.height > b.y;
}
export function buildOverlayInjectionScript(params: {
items: OverlayItem[];
captureY?: number;
}): string {
const itemsJson = JSON.stringify(
params.items.map((it) => ({
ref: it.ref,
x: round(it.x),
y: round(it.y),
w: Math.max(1, round(it.w)),
h: Math.max(1, round(it.h)),
})),
);
const attr = ANNOTATION_OVERLAY_ATTR;
const rootId = ANNOTATION_OVERLAY_ROOT_ID;
const captureY = Number.isFinite(params.captureY) ? round(params.captureY ?? 0) : 0;
return `(() => {
var items = ${itemsJson};
var captureY = ${captureY};
var existing = document.querySelectorAll("[${attr}]");
for (var k = 0; k < existing.length; k++) existing[k].remove();
var root = document.createElement("div");
root.id = ${JSON.stringify(rootId)};
root.setAttribute("${attr}", "1");
root.style.cssText = "position:absolute;top:0;left:0;width:0;height:0;pointer-events:none;z-index:2147483647;font-family:'SF Mono','SFMono-Regular',Menlo,Monaco,Consolas,'Liberation Mono','Courier New',monospace;";
for (var i = 0; i < items.length; i++) {
var it = items[i];
var box = document.createElement("div");
box.setAttribute("${attr}", "1");
box.style.cssText = "position:absolute;left:" + it.x + "px;top:" + it.y + "px;width:" + it.w + "px;height:" + it.h + "px;border:2px solid #ffb020;box-sizing:border-box;pointer-events:none;";
var tag = document.createElement("div");
tag.setAttribute("${attr}", "1");
tag.textContent = String(it.ref);
var relativeY = it.y - captureY;
var labelTop = relativeY < 14 ? (it.y + 2) : (it.y - 14);
tag.style.cssText = "position:absolute;left:" + it.x + "px;top:" + labelTop + "px;background:#ffb020;color:#1a1a1a;font:bold 11px/14px monospace;padding:0 4px;border-radius:2px;white-space:nowrap;pointer-events:none;";
root.appendChild(box);
root.appendChild(tag);
}
document.documentElement.appendChild(root);
return true;
})();`;
}
export function buildOverlayClearScript(): string {
const attr = ANNOTATION_OVERLAY_ATTR;
return `(() => {
var existing = document.querySelectorAll("[${attr}]");
for (var k = 0; k < existing.length; k++) existing[k].remove();
return true;
})();`;
}
/**
* Scale annotation boxes by independent x/y factors. Used to keep annotation
* coordinates aligned with the saved image after the response pipeline
* resizes the screenshot (e.g. via normalizeBrowserScreenshot capping the
* longest side or the byte budget). Returns a new array; inputs are not
* mutated. When both factors are 1 the boxes are returned unchanged (modulo
* structural copy) so callers can share the same code path for resized and
* non-resized captures.
*/
export function scaleAnnotations(
items: AnnotationItem[],
scaleX: number,
scaleY: number,
): AnnotationItem[] {
if (!Number.isFinite(scaleX) || !Number.isFinite(scaleY) || scaleX <= 0 || scaleY <= 0) {
return items.map((it) => ({ ...it, box: { ...it.box } }));
}
if (scaleX === 1 && scaleY === 1) {
return items.map((it) => ({ ...it, box: { ...it.box } }));
}
return items.map((it) => ({
...it,
box: {
x: round(it.box.x * scaleX),
y: round(it.box.y * scaleY),
width: Math.max(1, round(it.box.width * scaleX)),
height: Math.max(1, round(it.box.height * scaleY)),
},
}));
}
function round(v: number): number {
return Math.round(v);
}

View File

@@ -61,18 +61,4 @@ describe("browser navigation commands", () => {
expect(capture.runtimeErrors.join("\n")).toContain("Invalid width: maximum is 8192");
expect(mocks.runBrowserResizeWithOutput).not.toHaveBeenCalled();
});
it("navigate and resize commands are registered after removing dead import (#83878)", async () => {
const program = createNavigationProgram();
const browserCmd = program.commands.find((c) => c.name() === "browser");
expect(browserCmd).toBeDefined();
const cmds = browserCmd!.commands.map((c) => c.name());
expect(cmds).toContain("resize");
expect(cmds).toContain("navigate");
// Verify the shared module still exports requireRef (used by other modules)
const shared = await import("./shared.js");
expect(typeof shared.requireRef).toBe("function");
});
});

View File

@@ -12,7 +12,7 @@ import {
type BrowserParentOpts,
} from "../browser-cli-shared.js";
import { danger, defaultRuntime } from "../core-api.js";
import { resolveBrowserActionContext } from "./shared.js";
import { requireRef, resolveBrowserActionContext } from "./shared.js";
/** Registers Browser navigate and resize commands. */
export function registerBrowserNavigationCommands(
@@ -94,4 +94,7 @@ export function registerBrowserNavigationCommands(
defaultRuntime.exit(1);
}
});
// Keep `requireRef` reachable; shared utilities are intended for other modules too.
void requireRef;
}

View File

@@ -51,11 +51,7 @@ export function registerBrowserInspectCommands(
.option("--full-page", "Capture full scrollable page", false)
.option("--ref <ref>", "ARIA ref from ai snapshot")
.option("--element <selector>", "CSS selector for element screenshot")
.option(
"--labels",
"Overlay role refs on the screenshot (works with --full-page, --ref, and --element)",
false,
)
.option("--labels", "Overlay role refs on the screenshot", false)
.option("--type <png|jpeg>", "Output type (default: png)", "png")
.action(async (targetId: string | undefined, opts, cmd) => {
const parent = parentOpts(cmd);
@@ -102,7 +98,7 @@ export function registerBrowserInspectCommands(
.option("--depth <n>", "Role snapshot: max depth")
.option("--selector <sel>", "Role snapshot: scope to CSS selector")
.option("--frame <sel>", "Role snapshot: scope to an iframe selector")
.option("--labels", "Include label overlay screenshot with annotations", false)
.option("--labels", "Include viewport label overlay screenshot", false)
.option("--urls", "Append discovered link URLs to AI snapshots", false)
.option("--out <path>", "Write snapshot to a file")
.action(async (opts, cmd) => {

View File

@@ -1,11 +1,7 @@
// Canvas tests cover cli plugin behavior.
import { Command } from "commander";
import { describe, expect, it, vi } from "vitest";
import {
createDefaultCanvasCliDependencies,
registerNodesCanvasCommands,
type CanvasCliDependencies,
} from "./cli.js";
import { registerNodesCanvasCommands, type CanvasCliDependencies } from "./cli.js";
function createCanvasCliDeps() {
const writtenFiles: Array<{ filePath: string; base64: string }> = [];
@@ -51,26 +47,6 @@ function createCanvasCliDeps() {
return { deps, runtime, writtenFiles };
}
function createCanvasCliDepsWithDefaultParsers() {
const baseDeps = createDefaultCanvasCliDependencies();
const harness = createCanvasCliDeps();
return {
...harness,
deps: {
...baseDeps,
defaultRuntime: harness.runtime,
nodesCallOpts: harness.deps.nodesCallOpts,
runNodesCommand: harness.deps.runNodesCommand,
getNodesTheme: harness.deps.getNodesTheme,
resolveNodeId: harness.deps.resolveNodeId,
buildNodeInvokeParams: harness.deps.buildNodeInvokeParams,
callGatewayCli: harness.deps.callGatewayCli,
writeBase64ToFile: harness.deps.writeBase64ToFile,
shortenHomePath: harness.deps.shortenHomePath,
},
};
}
describe("canvas CLI", () => {
it("registers under nodes and captures a snapshot media path", async () => {
const program = new Command();
@@ -159,8 +135,6 @@ describe("canvas CLI", () => {
it.each([
["--max-width", "640px", "--max-width must be a positive integer."],
["--quality", "0.8x", "--quality must be a number."],
["--quality", "-0.1", "--quality must be between 0 and 1."],
["--quality", "5", "--quality must be between 0 and 1."],
])("rejects partial numeric snapshot %s values", async (flag, value, message) => {
const program = new Command();
program.exitOverride();
@@ -177,62 +151,6 @@ describe("canvas CLI", () => {
expect(deps.callGatewayCli).not.toHaveBeenCalled();
});
it.each(["0", "1"])("accepts snapshot --quality boundary value %s", async (quality) => {
const program = new Command();
program.exitOverride();
const nodes = program.command("nodes");
const { deps } = createCanvasCliDeps();
registerNodesCanvasCommands(nodes, deps);
await program.parseAsync(
["nodes", "canvas", "snapshot", "--node", "ios-node", "--quality", quality],
{
from: "user",
},
);
expect(deps.callGatewayCli).toHaveBeenCalledWith(
"node.invoke",
expect.any(Object),
expect.objectContaining({
params: expect.objectContaining({
quality: Number(quality),
}),
}),
);
});
it.each([
["snapshot"],
["present"],
["hide"],
["navigate", "https://example.com"],
["eval", "1 + 1"],
["a2ui", "push", "--text", "hello"],
["a2ui", "reset"],
])("rejects invalid %s invoke timeouts before invoking the node", async (...args) => {
const program = new Command();
program.exitOverride();
const nodes = program.command("nodes");
const { deps } = createCanvasCliDepsWithDefaultParsers();
deps.resolveNodeId = vi.fn(async () => {
throw new Error("resolveNodeId should not be called");
});
registerNodesCanvasCommands(nodes, deps);
await expect(
program.parseAsync(
["nodes", "canvas", ...args, "--node", "ios-node", "--invoke-timeout", "20ms"],
{
from: "user",
},
),
).rejects.toThrow("--invoke-timeout must be a positive integer.");
expect(deps.resolveNodeId).not.toHaveBeenCalled();
expect(deps.callGatewayCli).not.toHaveBeenCalled();
});
it.each([
["--x", "1x"],
["--y", "2px"],

View File

@@ -97,11 +97,7 @@ function parseTimeoutMs(raw: unknown): number | undefined {
if (raw === undefined || raw === null) {
return undefined;
}
const parsed = parseStrictPositiveInteger(raw);
if (parsed === undefined) {
throw new Error("--invoke-timeout must be a positive integer.");
}
return parsed;
return parseStrictPositiveInteger(raw);
}
function parseCanvasPositiveIntOption(raw: string | undefined, flag: string): number | undefined {
@@ -126,14 +122,6 @@ function parseCanvasFiniteNumberOption(raw: string | undefined, flag: string): n
return parsed;
}
function parseCanvasSnapshotQualityOption(raw: string | undefined): number | undefined {
const parsed = parseCanvasFiniteNumberOption(raw, "--quality");
if (parsed !== undefined && (parsed < 0 || parsed > 1)) {
throw new Error("--quality must be between 0 and 1.");
}
return parsed;
}
function parseNodeCandidates(raw: unknown): CanvasNodeCandidate[] {
const payload =
raw && typeof raw === "object" ? (raw as { nodes?: unknown; paired?: unknown }) : {};
@@ -257,8 +245,8 @@ async function invokeCanvas(
command: string,
params?: Record<string, unknown>,
) {
const timeoutMs = deps.parseTimeoutMs(opts.invokeTimeout);
const nodeId = await deps.resolveNodeId(opts, normalizeOptionalString(opts.node) ?? "");
const timeoutMs = deps.parseTimeoutMs(opts.invokeTimeout);
return await deps.callGatewayCli(
"node.invoke",
opts,
@@ -290,7 +278,7 @@ export function registerNodesCanvasCommands(nodes: Command, deps: CanvasCliDepen
await deps.runNodesCommand("canvas snapshot", async () => {
const format = parseCanvasSnapshotRequestFormat(opts.format);
const maxWidth = parseCanvasPositiveIntOption(opts.maxWidth, "--max-width");
const quality = parseCanvasSnapshotQualityOption(opts.quality);
const quality = parseCanvasFiniteNumberOption(opts.quality, "--quality");
const raw = await invokeCanvas(deps, opts, "canvas.snapshot", {
format,
maxWidth: Number.isFinite(maxWidth) ? maxWidth : undefined,

View File

@@ -183,7 +183,6 @@ describe("dynamic tool execution helpers", () => {
vi.useFakeTimers();
let capturedSignal: AbortSignal | undefined;
const onTimeout = vi.fn();
const onAgentToolResult = vi.fn();
const response = handleDynamicToolCallWithTimeout({
call: {
threadId: "thread-1",
@@ -201,7 +200,6 @@ describe("dynamic tool execution helpers", () => {
},
signal: new AbortController().signal,
timeoutMs: 1,
onAgentToolResult,
onTimeout,
});
@@ -218,64 +216,6 @@ describe("dynamic tool execution helpers", () => {
});
expect(capturedSignal?.aborted).toBe(true);
expect(onTimeout).toHaveBeenCalledTimes(1);
expect(onAgentToolResult).toHaveBeenCalledWith({
toolName: "message",
result: {
content: [
{
type: "text",
text: "OpenClaw dynamic tool call timed out after 1ms while running tool message.",
},
],
details: {
status: "failed",
error: "OpenClaw dynamic tool call timed out after 1ms while running tool message.",
},
},
isError: true,
});
});
it("reports pre-execution aborts to the private result observer", async () => {
const controller = new AbortController();
controller.abort(new Error("run cancelled"));
const onAgentToolResult = vi.fn();
const handleToolCall = vi.fn();
const result = await handleDynamicToolCallWithTimeout({
call: {
threadId: "thread-1",
turnId: "turn-1",
callId: "call-aborted",
namespace: null,
tool: "memory_search",
arguments: {},
},
toolBridge: { handleToolCall },
signal: controller.signal,
timeoutMs: 1_000,
onAgentToolResult,
});
expect(result).toEqual({
success: false,
contentItems: [
{ type: "inputText", text: "OpenClaw dynamic tool call aborted before execution." },
],
});
expect(handleToolCall).not.toHaveBeenCalled();
expect(onAgentToolResult).toHaveBeenCalledOnce();
expect(onAgentToolResult).toHaveBeenCalledWith({
toolName: "memory_search",
result: {
content: [{ type: "text", text: "OpenClaw dynamic tool call aborted before execution." }],
details: {
status: "failed",
error: "OpenClaw dynamic tool call aborted before execution.",
},
},
isError: true,
});
});
it("logs process poll timeout context separately from session idle", async () => {

View File

@@ -126,41 +126,10 @@ export async function handleDynamicToolCallWithTimeout(params: {
toolBridge: Pick<CodexDynamicToolBridge, "handleToolCall">;
signal: AbortSignal;
timeoutMs: number;
onAgentToolResult?: EmbeddedRunAttemptParams["onAgentToolResult"];
onTimeout?: () => void;
}): Promise<CodexDynamicToolCallResponse> {
// Timeout or run abort can win while a tool ignores cancellation. Keep the
// private observer terminal result exactly once across those competing paths.
let didNotifyAgentToolResult = false;
const notifyAgentToolResult = (
event: Parameters<NonNullable<EmbeddedRunAttemptParams["onAgentToolResult"]>>[0],
) => {
if (didNotifyAgentToolResult) {
return;
}
didNotifyAgentToolResult = true;
try {
params.onAgentToolResult?.(event);
} catch (error) {
embeddedAgentLog.warn(
`onAgentToolResult handler failed: tool=${params.call.tool} error=${String(error)}`,
);
}
};
const notifyFailedToolResult = (message: string) => {
notifyAgentToolResult({
toolName: params.call.tool,
result: {
content: [{ type: "text", text: message }],
details: { status: "failed", error: message },
},
isError: true,
});
};
if (params.signal.aborted) {
const message = "OpenClaw dynamic tool call aborted before execution.";
notifyFailedToolResult(message);
return failedDynamicToolResponse(message);
return failedDynamicToolResponse("OpenClaw dynamic tool call aborted before execution.");
}
const controller = new AbortController();
@@ -170,7 +139,6 @@ export async function handleDynamicToolCallWithTimeout(params: {
const abortFromRun = () => {
const message = "OpenClaw dynamic tool call aborted.";
controller.abort(params.signal.reason ?? new Error(message));
notifyFailedToolResult(message);
resolveAbort?.(failedDynamicToolResponse(message, { sideEffectEvidence: true }));
};
const abortPromise = new Promise<CodexDynamicToolCallResponse>((resolve) => {
@@ -187,7 +155,6 @@ export async function handleDynamicToolCallWithTimeout(params: {
...timeoutDetails.meta,
consoleMessage: timeoutDetails.consoleMessage,
});
notifyFailedToolResult(timeoutDetails.responseMessage);
resolve(
failedDynamicToolResponse(timeoutDetails.responseMessage, { sideEffectEvidence: true }),
);
@@ -200,22 +167,13 @@ export async function handleDynamicToolCallWithTimeout(params: {
if (params.signal.aborted) {
abortFromRun();
}
const response = await Promise.race([
params.toolBridge.handleToolCall(params.call, {
signal: controller.signal,
onAgentToolResult: notifyAgentToolResult,
}),
return await Promise.race([
params.toolBridge.handleToolCall(params.call, { signal: controller.signal }),
abortPromise,
timeoutPromise,
]);
if (!response.success && !didNotifyAgentToolResult) {
notifyFailedToolResult(readDynamicToolResponseText(response));
}
return response;
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
notifyFailedToolResult(message);
return failedDynamicToolResponse(message, {
return failedDynamicToolResponse(error instanceof Error ? error.message : String(error), {
sideEffectEvidence: true,
});
} finally {
@@ -230,16 +188,6 @@ export async function handleDynamicToolCallWithTimeout(params: {
}
}
function readDynamicToolResponseText(response: CodexDynamicToolCallResponse): string {
const text = response.contentItems
.flatMap((item) =>
item.type === "inputText" && typeof item.text === "string" ? [item.text] : [],
)
.join("\n")
.trim();
return text || "OpenClaw dynamic tool call failed.";
}
function failedDynamicToolResponse(
message: string,
options?: { sideEffectEvidence?: boolean },

View File

@@ -222,7 +222,6 @@ describe("createCodexDynamicToolBridge", () => {
it("can register a durable tool schema while denying execution for the current turn", async () => {
const heartbeatExecute = vi.fn(async () => textToolResult("heartbeat recorded"));
const onAgentToolResult = vi.fn();
const bridge = createCodexDynamicToolBridge({
tools: [createTool({ name: "message" })],
registeredTools: [
@@ -238,17 +237,14 @@ describe("createCodexDynamicToolBridge", () => {
HEARTBEAT_RESPONSE_TOOL_NAME,
]);
const result = await bridge.handleToolCall(
{
threadId: "thread-1",
turnId: "turn-1",
callId: "call-1",
namespace: null,
tool: HEARTBEAT_RESPONSE_TOOL_NAME,
arguments: {},
},
{ onAgentToolResult },
);
const result = await bridge.handleToolCall({
threadId: "thread-1",
turnId: "turn-1",
callId: "call-1",
namespace: null,
tool: HEARTBEAT_RESPONSE_TOOL_NAME,
arguments: {},
});
expect(result).toEqual({
success: false,
@@ -260,22 +256,6 @@ describe("createCodexDynamicToolBridge", () => {
],
});
expect(heartbeatExecute).not.toHaveBeenCalled();
expect(onAgentToolResult).toHaveBeenCalledWith({
toolName: HEARTBEAT_RESPONSE_TOOL_NAME,
result: {
content: [
{
type: "text",
text: `OpenClaw tool is not available for this turn: ${HEARTBEAT_RESPONSE_TOOL_NAME}`,
},
],
details: {
status: "failed",
error: `OpenClaw tool is not available for this turn: ${HEARTBEAT_RESPONSE_TOOL_NAME}`,
},
},
isError: true,
});
});
it("keeps available and registered schemas paired with their tools", () => {
@@ -1047,152 +1027,6 @@ describe("createCodexDynamicToolBridge", () => {
expectContextFields(callArg(handler, 0, 1, "middleware context"), { runtime: "codex" });
});
it("keeps unrecognized non-success statuses fail-closed", async () => {
const onAgentToolResult = vi.fn();
const bridge = createCodexDynamicToolBridge({
tools: [
createTool({
name: "exec",
execute: vi.fn(async () =>
textToolResult("Approval is unavailable.", { status: "approval-unavailable" }),
),
}),
],
signal: new AbortController().signal,
});
const result = await bridge.handleToolCall(
{
threadId: "thread-1",
turnId: "turn-1",
callId: "call-1",
namespace: null,
tool: "exec",
arguments: { command: "pwd" },
},
{ onAgentToolResult },
);
expect(result).toMatchObject({ success: false });
expect(onAgentToolResult).toHaveBeenCalledWith({
toolName: "exec",
result: textToolResult("Approval is unavailable.", { status: "approval-unavailable" }),
isError: true,
});
});
it("preserves explicitly successful cancellation outcomes", async () => {
const onAgentToolResult = vi.fn();
const cancelledResult = textToolResult("Approval rejected.", {
ok: true,
status: "cancelled",
});
const bridge = createCodexDynamicToolBridge({
tools: [
createTool({
name: "lobster",
execute: vi.fn(async () => cancelledResult),
}),
],
signal: new AbortController().signal,
});
const result = await bridge.handleToolCall(
{
threadId: "thread-1",
turnId: "turn-1",
callId: "call-1",
namespace: null,
tool: "lobster",
arguments: {},
},
{ onAgentToolResult },
);
expect(result).toMatchObject({ success: true });
expect(onAgentToolResult).toHaveBeenCalledWith({
toolName: "lobster",
result: cancelledResult,
isError: false,
});
});
it("reports sanitized dynamic tool results to the private result observer", async () => {
const onAgentToolResult = vi.fn();
const bridge = createCodexDynamicToolBridge({
tools: [
createTool({
name: "memory_lookup_custom",
execute: vi.fn(async () =>
textToolResult("OPENROUTER_API_KEY=sk-or-v1-abcdef0123456789", {
status: "failed",
error: "backend unavailable",
}),
),
}),
],
signal: new AbortController().signal,
});
await bridge.handleToolCall(
{
threadId: "thread-1",
turnId: "turn-1",
callId: "call-1",
namespace: null,
tool: "memory_lookup_custom",
arguments: {},
},
{ onAgentToolResult },
);
expect(onAgentToolResult).toHaveBeenCalledOnce();
expect(onAgentToolResult).toHaveBeenCalledWith({
toolName: "memory_lookup_custom",
result: {
content: [{ type: "text", text: "OPENROUTER_API_KEY=sk-or-…6789" }],
details: { status: "failed", error: "backend unavailable" },
},
isError: true,
});
});
it("reports thrown dynamic tool failures to the private result observer", async () => {
const onAgentToolResult = vi.fn();
const bridge = createCodexDynamicToolBridge({
tools: [
createTool({
name: "memory_lookup_custom",
execute: vi.fn(async () => {
throw new Error("backend unavailable");
}),
}),
],
signal: new AbortController().signal,
});
await bridge.handleToolCall(
{
threadId: "thread-1",
turnId: "turn-1",
callId: "call-1",
namespace: null,
tool: "memory_lookup_custom",
arguments: {},
},
{ onAgentToolResult },
);
expect(onAgentToolResult).toHaveBeenCalledWith({
toolName: "memory_lookup_custom",
result: {
content: [{ type: "text", text: "backend unavailable" }],
details: { status: "failed", error: "backend unavailable" },
},
isError: true,
});
});
it("preserves terminal async tool results without marking them as errors", async () => {
const bridge = createBridgeWithToolResult("image_generate", {
content: [{ type: "text", text: "Background task started." }],

View File

@@ -12,13 +12,11 @@ import {
embeddedAgentLog,
type EmbeddedRunAttemptParams,
isToolWrappedWithBeforeToolCallHook,
isToolResultError,
isMessagingTool,
isMessagingToolSendAction,
normalizeHeartbeatToolResponse,
projectRuntimeToolInputSchema,
runAgentHarnessAfterToolCallHook,
sanitizeToolResult,
setBeforeToolCallDiagnosticsEnabled,
type AnyAgentTool,
type HeartbeatToolResponse,
@@ -73,10 +71,7 @@ export type CodexDynamicToolBridge = {
specs: CodexDynamicToolSpec[];
handleToolCall: (
params: CodexDynamicToolCallParams,
options?: {
signal?: AbortSignal;
onAgentToolResult?: EmbeddedRunAttemptParams["onAgentToolResult"];
},
options?: { signal?: AbortSignal },
) => Promise<CodexDynamicToolCallResponse>;
telemetry: {
didSendViaMessagingTool: boolean;
@@ -160,6 +155,7 @@ export function createCodexDynamicToolBridge(params: {
...ALWAYS_DIRECT_DYNAMIC_TOOL_NAMES,
...(params.directToolNames ?? []),
]);
return {
availableSpecs: availableTools.map((entry) =>
createCodexDynamicToolSpec({
@@ -179,28 +175,19 @@ export function createCodexDynamicToolBridge(params: {
handleToolCall: async (call, options) => {
const toolEntry = toolMap.get(call.tool);
if (!toolEntry) {
const message = registeredToolNames.has(call.tool)
? `OpenClaw tool is not available for this turn: ${call.tool}`
: `Unknown OpenClaw tool: ${call.tool}`;
notifyAgentToolResult(
options?.onAgentToolResult,
call.tool,
failedToolResult(message),
true,
);
if (registeredToolNames.has(call.tool)) {
return {
contentItems: [
{
type: "inputText",
text: message,
text: `OpenClaw tool is not available for this turn: ${call.tool}`,
},
],
success: false,
};
}
return {
contentItems: [{ type: "inputText", text: message }],
contentItems: [{ type: "inputText", text: `Unknown OpenClaw tool: ${call.tool}` }],
success: false,
};
}
@@ -215,7 +202,7 @@ export function createCodexDynamicToolBridge(params: {
const preparedArgs = tool.prepareArguments ? tool.prepareArguments(args) : args;
didStartExecution = true;
const rawResult = await tool.execute(call.callId, preparedArgs, signal);
const rawIsError = isCodexToolResultError(rawResult);
const rawIsError = isToolResultError(rawResult);
const middlewareResult = await middlewareRunner.applyToolResultMiddleware({
threadId: call.threadId,
turnId: call.turnId,
@@ -233,8 +220,7 @@ export function createCodexDynamicToolBridge(params: {
args,
result: middlewareResult,
});
const resultIsError = rawIsError || isCodexToolResultError(result);
notifyAgentToolResult(options?.onAgentToolResult, toolName, result, resultIsError);
const resultIsError = rawIsError || isToolResultError(result);
collectToolTelemetry({
toolName,
args,
@@ -276,13 +262,6 @@ export function createCodexDynamicToolBridge(params: {
);
return withSideEffectEvidence(response, terminalType !== "blocked");
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
notifyAgentToolResult(
options?.onAgentToolResult,
toolName,
failedToolResult(errorMessage),
true,
);
collectToolTelemetry({
toolName,
args,
@@ -299,7 +278,7 @@ export function createCodexDynamicToolBridge(params: {
sessionKey: toolResultHookContext.sessionKey,
channelId: toolResultHookContext.channelId,
startArgs: args,
error: errorMessage,
error: error instanceof Error ? error.message : String(error),
startedAt,
});
return withSideEffectEvidence(
@@ -308,7 +287,7 @@ export function createCodexDynamicToolBridge(params: {
contentItems: [
{
type: "inputText",
text: errorMessage,
text: error instanceof Error ? error.message : String(error),
},
],
success: false,
@@ -322,32 +301,6 @@ export function createCodexDynamicToolBridge(params: {
};
}
function notifyAgentToolResult(
observer: EmbeddedRunAttemptParams["onAgentToolResult"] | undefined,
toolName: string,
result: unknown,
isError: boolean,
) {
try {
observer?.({
toolName,
result: sanitizeToolResult(result),
isError,
});
} catch (error) {
embeddedAgentLog.warn(
`onAgentToolResult handler failed: tool=${toolName} error=${String(error)}`,
);
}
}
function failedToolResult(message: string): AgentToolResult<unknown> {
return {
content: [{ type: "text", text: message }],
details: { status: "failed", error: message },
};
}
function wrapProjectedCodexDynamicTools(
tools: readonly ProjectedCodexDynamicTool[],
hookContext: CodexDynamicToolHookContext | undefined,
@@ -735,17 +688,11 @@ function readPositiveInteger(value: unknown): number | undefined {
return Math.floor(value);
}
function isCodexToolResultError(result: AgentToolResult<unknown>): boolean {
if (isToolResultError(result)) {
return true;
}
function isToolResultError(result: AgentToolResult<unknown>): boolean {
const details = result.details;
if (!isRecord(details)) {
return false;
}
if (details.ok === true || details.success === true) {
return false;
}
if (details.timedOut === true) {
return true;
}

View File

@@ -1846,7 +1846,6 @@ export async function runCodexAppServerAttempt(
toolBridge,
signal: runAbortController.signal,
timeoutMs: dynamicToolTimeoutMs,
onAgentToolResult: params.onAgentToolResult,
onTimeout: () => {
trajectoryRecorder?.recordEvent("tool.timeout", {
threadId: call.threadId,

View File

@@ -1213,70 +1213,14 @@ describe("convertOpenClawToolToSdkTool", () => {
});
it("converts single text content to an exact textResultForLlm", async () => {
const onAgentToolResult = vi.fn();
const sourceResult = {
content: [{ text: "hello", type: "text" }],
details: { results: [{ text: "hello" }] },
};
const sdkTool = convertOpenClawToolToSdkTool(makeTool({}, sourceResult), { onAgentToolResult });
const sdkTool = convertOpenClawToolToSdkTool(
makeTool({}, { content: [{ text: "hello", type: "text" }], details: null }),
{},
);
const result = await runSdkTool(sdkTool, {});
expect(result).toEqual({ resultType: "success", textResultForLlm: "hello" });
expect(onAgentToolResult).toHaveBeenCalledWith({
toolName: "tool-a",
result: sourceResult,
isError: false,
});
});
it("reports thrown tool failures to the private result observer", async () => {
const error = new Error("backend unavailable");
const onAgentToolResult = vi.fn();
const sdkTool = convertOpenClawToolToSdkTool(
makeTool({
execute: vi.fn(async () => {
throw error;
}),
}),
{ onAgentToolResult },
);
await runSdkTool(sdkTool, {});
expect(onAgentToolResult).toHaveBeenCalledWith({
toolName: "tool-a",
result: {
content: [
{
type: "text",
text: "[copilot-tool-bridge] tool 'tool-a' failed: backend unavailable",
},
],
details: { status: "failed", error: "backend unavailable" },
},
isError: true,
});
});
it("reports returned OpenClaw error results as observer failures", async () => {
const onAgentToolResult = vi.fn();
const sourceResult = {
content: [{ text: '{"status":"error","error":"backend unavailable"}', type: "text" }],
details: { status: "error", error: "backend unavailable" },
};
const sdkTool = convertOpenClawToolToSdkTool(makeTool({}, sourceResult), {
onAgentToolResult,
});
const result = await runSdkTool(sdkTool, {});
expect(result).toMatchObject({ resultType: "success" });
expect(onAgentToolResult).toHaveBeenCalledWith({
toolName: "tool-a",
result: sourceResult,
isError: true,
});
});
it("joins multiple text blocks with newlines", async () => {
@@ -1332,12 +1276,16 @@ describe("convertOpenClawToolToSdkTool", () => {
});
it("returns a failure result for unsupported content shapes", async () => {
const onAgentToolResult = vi.fn();
const sourceResult = {
content: [{ type: "resource" }],
details: null,
};
const sdkTool = convertOpenClawToolToSdkTool(makeTool({}, sourceResult), { onAgentToolResult });
const sdkTool = convertOpenClawToolToSdkTool(
makeTool(
{},
{
content: [{ type: "resource" }],
details: null,
},
),
{},
);
const result = await runSdkTool(sdkTool, {});
@@ -1348,11 +1296,6 @@ describe("convertOpenClawToolToSdkTool", () => {
expect(getError(result as ToolResultObject)).toBe(
"[copilot-tool-bridge] unsupported AgentToolResult content shape: resource",
);
expect(onAgentToolResult).toHaveBeenCalledWith({
toolName: "tool-a",
result: sourceResult,
isError: true,
});
});
it("returns a failure result when execute throws and preserves the error", async () => {

View File

@@ -10,11 +10,9 @@ import {
buildEmbeddedAttemptToolRunContext,
getPluginToolMeta,
isSubagentSessionKey,
isToolResultError,
resolveAttemptSpawnWorkspaceDir,
resolveEmbeddedAttemptToolConstructionPlan,
resolveModelAuthMode,
sanitizeToolResult,
} from "openclaw/plugin-sdk/agent-harness-runtime";
type CreateOpenClawCodingTools =
@@ -207,7 +205,6 @@ export async function createCopilotToolBridge(
convertOpenClawToolToSdkTool(sourceTool, {
abortSignal: input.abortSignal,
beforeExecute: input.beforeExecute,
onAgentToolResult: input.attemptParams?.onAgentToolResult,
}),
),
sourceTools: filteredTools,
@@ -387,7 +384,6 @@ export function convertOpenClawToolToSdkTool(
ctx: {
abortSignal?: AbortSignal;
beforeExecute?: CopilotToolBridgeInput["beforeExecute"];
onAgentToolResult?: CopilotToolAttemptParams["onAgentToolResult"];
},
): SdkTool {
if (typeof sourceTool.name !== "string" || sourceTool.name.trim().length === 0) {
@@ -401,30 +397,13 @@ export function convertOpenClawToolToSdkTool(
}
let sequentialLock = Promise.resolve();
const notifyToolResult = (result: unknown, isError: boolean) => {
try {
ctx.onAgentToolResult?.({ toolName: sourceTool.name, result, isError });
} catch (error) {
console.warn("[copilot-tool-bridge] onAgentToolResult handler threw; continuing", error);
}
};
const failureResult = (message: string, error: unknown): ToolResultObject => {
notifyToolResult(
sanitizeToolResult({
content: [{ type: "text", text: message }],
details: { status: "failed", error: toError(error).message },
}),
true,
);
return createFailureResult(message, error);
};
const executeOnce = async (
args: unknown,
invocation: ToolInvocation,
): Promise<ToolResultObject> => {
if (ctx.abortSignal?.aborted) {
const error = new Error("[copilot-tool-bridge] aborted before execution");
return failureResult(error.message, error);
return createFailureResult(error.message, error);
}
try {
@@ -436,7 +415,7 @@ export function convertOpenClawToolToSdkTool(
toolName: sourceTool.name,
});
} catch (error: unknown) {
return failureResult(
return createFailureResult(
`[copilot-tool-bridge] beforeExecute failed for tool '${sourceTool.name}': ${toError(error).message}`,
error,
);
@@ -446,7 +425,7 @@ export function convertOpenClawToolToSdkTool(
try {
preparedArgs = sourceTool.prepareArguments ? sourceTool.prepareArguments(args) : args;
} catch (error: unknown) {
return failureResult(
return createFailureResult(
`[copilot-tool-bridge] prepareArguments failed for tool '${sourceTool.name}': ${toError(error).message}`,
error,
);
@@ -461,19 +440,13 @@ export function convertOpenClawToolToSdkTool(
undefined,
);
} catch (error: unknown) {
return failureResult(
return createFailureResult(
`[copilot-tool-bridge] tool '${sourceTool.name}' failed: ${toError(error).message}`,
error,
);
}
const sdkResult = agentToolResultToSdk(result);
const sanitizedResult = sanitizeToolResult(result);
notifyToolResult(
sanitizedResult,
sdkResult.resultType === "failure" || isToolResultError(sanitizedResult),
);
return sdkResult;
return agentToolResultToSdk(result);
};
const handler =

View File

@@ -91,11 +91,11 @@ describe("discord config schema", () => {
expect(cfg.accounts?.noisy?.suppressEmbeds).toBe(false);
});
it("rejects unknown preview config keys", () => {
it("rejects Telegram-only native tool-progress draft config", () => {
const issues = expectInvalidDiscordConfig({
streaming: {
preview: {
unknownPreviewFlag: true,
nativeToolProgress: true,
},
},
});

View File

@@ -41,7 +41,6 @@ beforeEach(() => {
model: {
provider: "anthropic",
id: "claude-sonnet-4-6",
maxTokens: 64_000,
},
auth: {
apiKey: "sk-test",
@@ -76,7 +75,6 @@ describe("generateThreadTitle", () => {
model: {
provider: "openrouter",
id: "anthropic/claude-sonnet-4-5",
maxTokens: 64_000,
},
auth: {
apiKey: "sk-openrouter",
@@ -160,7 +158,6 @@ describe("generateThreadTitle", () => {
it("builds contextual prompt and forwards completion options", async () => {
const now = 1_700_000_000_000;
const dateNowSpy = vi.spyOn(Date, "now").mockReturnValue(now);
const setTimeoutSpy = vi.spyOn(globalThis, "setTimeout");
let result: string | null;
try {
result = await generateThreadTitle({
@@ -190,40 +187,11 @@ describe("generateThreadTitle", () => {
],
});
expect(completionArgs.options).toEqual({
maxTokens: 4_096,
maxTokens: 512,
signal: completionArgs.options?.signal,
});
expect(completionArgs.options?.signal).toBeInstanceOf(AbortSignal);
expect(completionArgs.options).not.toHaveProperty("temperature");
expect(setTimeoutSpy).toHaveBeenCalledWith(expect.any(Function), 60_000);
});
it("clamps completion budget to the selected model output cap", async () => {
prepareSimpleCompletionModelForAgentMock.mockResolvedValueOnce({
selection: {
provider: "anthropic",
modelId: "claude-haiku-4-5",
agentDir: "/tmp/openclaw-agent",
},
model: {
provider: "anthropic",
id: "claude-haiku-4-5",
maxTokens: 1_024,
},
auth: {
apiKey: "sk-test",
source: "env:TEST_API_KEY",
mode: "api-key",
},
} as Awaited<ReturnType<typeof agentRuntimeModule.prepareSimpleCompletionModelForAgent>>);
await generateThreadTitle({
cfg: EMPTY_DISCORD_TEST_CONFIG,
agentId: "main",
messageText: "Need a generated title.",
});
expect(firstCompletionArgs().options?.maxTokens).toBe(1_024);
});
it("returns null when completion throws", async () => {

View File

@@ -8,7 +8,7 @@ import {
} from "openclaw/plugin-sdk/simple-completion-runtime";
import { withAbortTimeout } from "./timeouts.js";
const DEFAULT_THREAD_TITLE_TIMEOUT_MS = 60_000;
const DEFAULT_THREAD_TITLE_TIMEOUT_MS = 10_000;
const MAX_THREAD_TITLE_SOURCE_CHARS = 600;
const MAX_THREAD_TITLE_CHANNEL_NAME_CHARS = 120;
const MAX_THREAD_TITLE_CHANNEL_DESCRIPTION_CHARS = 320;
@@ -17,7 +17,7 @@ const MAX_THREAD_TITLE_CHANNEL_DESCRIPTION_CHARS = 320;
// capacity: the entire budget is consumed by the thinking block before any
// text is emitted, so extractAssistantText returns empty and the rename is
// silently skipped.
const DISCORD_THREAD_TITLE_MAX_TOKENS = 4_096;
const DISCORD_THREAD_TITLE_MAX_TOKENS = 512;
const DISCORD_THREAD_TITLE_SYSTEM_PROMPT =
"Generate a concise Discord thread title (3-6 words). Return only the title. Use channel context when provided and avoid redundant channel-name words unless needed for clarity.";
@@ -77,7 +77,6 @@ async function completeThreadTitle(params: {
userMessage: string;
timeoutMs: number;
}) {
const maxTokens = Math.min(DISCORD_THREAD_TITLE_MAX_TOKENS, Math.floor(params.model.maxTokens));
return await withAbortTimeout({
timeoutMs: params.timeoutMs,
createTimeoutError: () => new Error(`thread-title timed out after ${params.timeoutMs}ms`),
@@ -96,7 +95,7 @@ async function completeThreadTitle(params: {
],
},
options: {
maxTokens,
maxTokens: DISCORD_THREAD_TITLE_MAX_TOKENS,
signal,
},
}),

View File

@@ -183,7 +183,6 @@ describe("Feishu Card Action Handler", () => {
tag: "button",
},
context: { open_id: "u123", user_id: "uid1", chat_id: "chat1" },
open_message_id: "om_card_message",
};
await handleFeishuCardAction({ cfg, event, runtime });
@@ -191,8 +190,6 @@ describe("Feishu Card Action Handler", () => {
const message = handleMessage();
expect(message.content).toBe('{"text":"/ping"}');
expect(message.chat_id).toBe("chat1");
expect(message.reply_target_message_id).toBe("om_card_message");
expect(message.typing_target_message_id).toBe("om_card_message");
});
it("handles card action with JSON object payload", async () => {

View File

@@ -3267,14 +3267,13 @@ describe("handleFeishuMessage command authorization", () => {
await dispatchMessage({ cfg, event });
const dispatcherOptions = mockCallArg<{
replyToMessageId?: string;
rootId?: string;
typingTargetMessageId?: string;
}>(mockCreateFeishuReplyDispatcher, 0, 0);
const dispatcherOptions = mockCallArg<{ replyToMessageId?: string; rootId?: string }>(
mockCreateFeishuReplyDispatcher,
0,
0,
);
expect(dispatcherOptions.replyToMessageId).toBe("om_root_topic");
expect(dispatcherOptions.rootId).toBe("om_root_topic");
expect(dispatcherOptions.typingTargetMessageId).toBe("om_child_message");
});
it("replies to triggering message in normal group even when root_id is present (#32980)", async () => {
@@ -3346,14 +3345,13 @@ describe("handleFeishuMessage command authorization", () => {
await dispatchMessage({ cfg, event });
const dispatcherOptions = mockCallArg<{
replyToMessageId?: string;
rootId?: string;
typingTargetMessageId?: string;
}>(mockCreateFeishuReplyDispatcher, 0, 0);
const dispatcherOptions = mockCallArg<{ replyToMessageId?: string; rootId?: string }>(
mockCreateFeishuReplyDispatcher,
0,
0,
);
expect(dispatcherOptions.replyToMessageId).toBe("om_topic_root");
expect(dispatcherOptions.rootId).toBe("om_topic_root");
expect(dispatcherOptions.typingTargetMessageId).toBe("om_topic_reply");
});
it("replies to topic root in topic-sender group with root_id", async () => {
@@ -3386,48 +3384,13 @@ describe("handleFeishuMessage command authorization", () => {
await dispatchMessage({ cfg, event });
const dispatcherOptions = mockCallArg<{
replyToMessageId?: string;
rootId?: string;
typingTargetMessageId?: string;
}>(mockCreateFeishuReplyDispatcher, 0, 0);
const dispatcherOptions = mockCallArg<{ replyToMessageId?: string; rootId?: string }>(
mockCreateFeishuReplyDispatcher,
0,
0,
);
expect(dispatcherOptions.replyToMessageId).toBe("om_topic_sender_root");
expect(dispatcherOptions.rootId).toBe("om_topic_sender_root");
expect(dispatcherOptions.typingTargetMessageId).toBe("om_topic_sender_reply");
});
it("uses explicit synthetic typing targets without changing reply routing", async () => {
mockShouldComputeCommandAuthorized.mockReturnValue(false);
const cfg: ClawdbotConfig = {
channels: {
feishu: {
dmPolicy: "open",
},
},
} as ClawdbotConfig;
const event: FeishuMessageEvent = {
sender: { sender_id: { open_id: "ou-synthetic" } },
message: {
message_id: "synthetic-reaction-turn",
typing_target_message_id: "om_reacted_message",
reply_target_message_id: "om_reply_anchor",
chat_id: "oc-synthetic-dm",
chat_type: "p2p",
message_type: "text",
content: JSON.stringify({ text: "[reacted with THUMBSUP to message om_reply_anchor]" }),
},
};
await dispatchMessage({ cfg, event });
expect(mockCreateFeishuReplyDispatcher).toHaveBeenCalledWith(
expect.objectContaining({
replyToMessageId: "om_reply_anchor",
typingTargetMessageId: "om_reacted_message",
}),
);
});
it("keeps P2P replies inside a direct-message thread when Feishu supplies thread_id", async () => {

View File

@@ -301,7 +301,6 @@ export function parseFeishuMessageEvent(
chatId: event.message.chat_id,
messageId: event.message.message_id,
replyTargetMessageId: event.message.reply_target_message_id?.trim() || undefined,
typingTargetMessageId: event.message.typing_target_message_id?.trim() || undefined,
suppressReplyTarget: event.message.suppress_reply_target === true,
senderId: senderUserId || senderOpenId || "",
// Keep the historical field name, but fall back to user_id when open_id is unavailable
@@ -1404,8 +1403,6 @@ export async function handleFeishuMessage(params: {
: isTopicSession || configReplyInThread
? topicReplyTargetMessageId
: defaultReplyTargetMessageId;
const typingTargetMessageId =
ctx.typingTargetMessageId ?? (ctx.suppressReplyTarget ? undefined : ctx.messageId);
const threadReply = isGroup ? (groupSession?.threadReply ?? false) : directThreadReply;
const lastRouteThreadId =
isGroup && (isTopicSession || configReplyInThread || threadReply)
@@ -1531,7 +1528,6 @@ export async function handleFeishuMessage(params: {
chatId: ctx.chatId,
allowReasoningPreview,
replyToMessageId: replyTargetMessageId,
typingTargetMessageId,
skipReplyToInMessages: !isGroup && !directThreadReply,
replyInThread,
rootId: ctx.rootId,
@@ -1708,7 +1704,6 @@ export async function handleFeishuMessage(params: {
chatId: ctx.chatId,
allowReasoningPreview,
replyToMessageId: replyTargetMessageId,
typingTargetMessageId,
skipReplyToInMessages: !isGroup && !directThreadReply,
replyInThread,
rootId: ctx.rootId,

View File

@@ -147,7 +147,6 @@ function buildSyntheticMessageEvent(
message: {
message_id: `card-action-${event.token}`,
...(replyTargetMessageId ? { reply_target_message_id: replyTargetMessageId } : {}),
...(replyTargetMessageId ? { typing_target_message_id: replyTargetMessageId } : {}),
...(!replyTargetMessageId ? { suppress_reply_target: true } : {}),
chat_id: event.context.chat_id || event.operator.open_id,
chat_type: chatType,

View File

@@ -386,31 +386,6 @@ describe("createFeishuClient HTTP timeout", () => {
timeout: 45_000,
});
});
it("evicts client cache when SDK is replaced via setFeishuClientRuntimeForTest (#83911)", () => {
const ctorCountA = clientCtorMock.mock.calls.length;
// First client gets cached
createFeishuClient({ appId: "app_7", appSecret: "secret_7", accountId: "cache-clear-test" }); // pragma: allowlist secret
expect(clientCtorMock.mock.calls.length).toBe(ctorCountA + 1);
// SDK swap via setFeishuClientRuntimeForTest should clear the cache
setFeishuClientRuntimeForTest({
sdk: {
AppType: { SelfBuild: "self" } as never,
Client: clientCtorMock as never,
Domain: { Feishu: "https://open.feishu.cn", Lark: "https://open.larksuite.com" } as never,
LoggerLevel: { info: "info" } as never,
WSClient: vi.fn() as never,
EventDispatcher: vi.fn() as never,
defaultHttpInstance: mockBaseHttpInstance as never,
},
});
// Same credentials — would hit cache before the fix; now evicted
createFeishuClient({ appId: "app_7", appSecret: "secret_7", accountId: "cache-clear-test" }); // pragma: allowlist secret
expect(clientCtorMock.mock.calls.length).toBe(ctorCountA + 2);
});
});
describe("createFeishuWSClient proxy handling", () => {

View File

@@ -260,5 +260,4 @@ export function setFeishuClientRuntimeForTest(overrides?: {
feishuClientSdk = overrides?.sdk
? { ...defaultFeishuClientSdk, ...overrides.sdk }
: defaultFeishuClientSdk;
clearClientCache();
}

View File

@@ -12,7 +12,6 @@ export type FeishuMessageEvent = {
message: {
message_id: string;
reply_target_message_id?: string;
typing_target_message_id?: string;
suppress_reply_target?: boolean;
root_id?: string;
parent_id?: string;

View File

@@ -143,7 +143,6 @@ export async function resolveReactionSyntheticEvent(
},
message: {
message_id: `${messageId}:reaction:${emoji}:${uuid()}`,
typing_target_message_id: messageId,
chat_id: syntheticChatId,
chat_type: syntheticChatType,
message_type: "text",

View File

@@ -3,7 +3,7 @@ import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runti
import type { RuntimeEnv } from "../runtime-api.js";
import { waitForAbortableDelay } from "./async.js";
import { fetchBotIdentityForMonitor, type FeishuMonitorBotIdentity } from "./monitor.startup.js";
import { setFeishuBotIdentityState } from "./monitor.state.js";
import { botNames, botOpenIds } from "./monitor.state.js";
import type { ResolvedFeishuAccount } from "./types.js";
// Delays must be >= PROBE_ERROR_TTL_MS (60s) so each retry makes a real network request
@@ -17,7 +17,12 @@ export function applyBotIdentityState(
const botOpenId = normalizeOptionalString(identity.botOpenId);
const botName = normalizeOptionalString(identity.botName);
setFeishuBotIdentityState(accountId, { botOpenId: botOpenId ?? "", botName });
botOpenIds.set(accountId, botOpenId ?? "");
if (botName) {
botNames.set(accountId, botName);
} else {
botNames.delete(accountId);
}
return { botOpenId, botName };
}

View File

@@ -1,15 +1,6 @@
// Feishu tests cover monitor.cleanup plugin behavior.
import type { Server } from "node:http";
import { afterAll, afterEach, describe, expect, it, vi } from "vitest";
import {
botNames,
botOpenIds,
FEISHU_HTTP_SERVER_CLOSE_TIMEOUT_MS,
httpServers,
setFeishuBotIdentityState,
stopFeishuMonitorState,
wsClients,
} from "./monitor.state.js";
import { botNames, botOpenIds, stopFeishuMonitorState, wsClients } from "./monitor.state.js";
import type { ResolvedFeishuAccount } from "./types.js";
const createFeishuWSClientMock = vi.hoisted(() => vi.fn());
@@ -47,34 +38,6 @@ function createWsClient(): MockWsClient {
};
}
function createHttpServerMock(): {
server: Server;
close: ReturnType<typeof vi.fn>;
closeAllConnections: ReturnType<typeof vi.fn>;
finishClose: (error?: Error) => void;
} {
let closeCallback: ((err?: Error) => void) | undefined;
const server = {} as Server;
const close = vi.fn((callback?: (err?: Error) => void) => {
closeCallback = callback;
return server;
});
const closeAllConnections = vi.fn();
server.close = close as unknown as Server["close"];
server.closeAllConnections = closeAllConnections;
return {
server,
close,
closeAllConnections,
finishClose: (error?: Error) => {
if (!closeCallback) {
throw new Error("expected HTTP server close callback");
}
closeCallback(error);
},
};
}
function firstRuntimeError(runtime: { error: ReturnType<typeof vi.fn> }): string {
return String(runtime.error.mock.calls[0]?.[0] ?? "");
}
@@ -87,9 +50,9 @@ function firstWsCallbacks(): { onError?: (err: Error) => void } {
return callbacks as { onError?: (err: Error) => void };
}
afterEach(async () => {
afterEach(() => {
vi.useRealTimers();
await stopFeishuMonitorState();
stopFeishuMonitorState();
vi.clearAllMocks();
});
@@ -376,7 +339,7 @@ describe("feishu websocket cleanup", () => {
expect(errorMessage).not.toContain("secret_token");
});
it("closes targeted websocket clients during stop cleanup", async () => {
it("closes targeted websocket clients during stop cleanup", () => {
const alphaClient = createWsClient();
const betaClient = createWsClient();
@@ -387,7 +350,7 @@ describe("feishu websocket cleanup", () => {
botNames.set("alpha", "Alpha");
botNames.set("beta", "Beta");
await stopFeishuMonitorState("alpha");
stopFeishuMonitorState("alpha");
expect(alphaClient.close).toHaveBeenCalledTimes(1);
expect(betaClient.close).not.toHaveBeenCalled();
@@ -399,7 +362,7 @@ describe("feishu websocket cleanup", () => {
expect(botNames.has("beta")).toBe(true);
});
it("closes all websocket clients during global stop cleanup", async () => {
it("closes all websocket clients during global stop cleanup", () => {
const alphaClient = createWsClient();
const betaClient = createWsClient();
@@ -410,7 +373,7 @@ describe("feishu websocket cleanup", () => {
botNames.set("alpha", "Alpha");
botNames.set("beta", "Beta");
await stopFeishuMonitorState();
stopFeishuMonitorState();
expect(alphaClient.close).toHaveBeenCalledTimes(1);
expect(betaClient.close).toHaveBeenCalledTimes(1);
@@ -418,128 +381,4 @@ describe("feishu websocket cleanup", () => {
expect(botOpenIds.size).toBe(0);
expect(botNames.size).toBe(0);
});
it("keeps targeted HTTP server state until close completes", async () => {
const { server, close, closeAllConnections, finishClose } = createHttpServerMock();
httpServers.set("alpha", server);
botOpenIds.set("alpha", "ou_alpha");
botNames.set("alpha", "Alpha");
const stopPromise = stopFeishuMonitorState("alpha");
await Promise.resolve();
expect(close).toHaveBeenCalledTimes(1);
expect(httpServers.get("alpha")).toBe(server);
expect(botOpenIds.get("alpha")).toBe("ou_alpha");
expect(botNames.get("alpha")).toBe("Alpha");
finishClose();
await stopPromise;
expect(closeAllConnections).not.toHaveBeenCalled();
expect(httpServers.has("alpha")).toBe(false);
expect(botOpenIds.has("alpha")).toBe(false);
expect(botNames.has("alpha")).toBe(false);
});
it("preserves replacement HTTP state after delayed targeted cleanup", async () => {
const oldServer = createHttpServerMock();
const replacementServer = createHttpServerMock();
httpServers.set("alpha", oldServer.server);
setFeishuBotIdentityState("alpha", { botOpenId: "ou_old", botName: "Old" });
const stopPromise = stopFeishuMonitorState("alpha");
await Promise.resolve();
setFeishuBotIdentityState("alpha", { botOpenId: "ou_new", botName: "New" });
httpServers.set("alpha", replacementServer.server);
oldServer.finishClose();
await stopPromise;
expect(httpServers.get("alpha")).toBe(replacementServer.server);
expect(botOpenIds.get("alpha")).toBe("ou_new");
expect(botNames.get("alpha")).toBe("New");
const cleanupPromise = stopFeishuMonitorState("alpha");
await Promise.resolve();
replacementServer.finishClose();
await cleanupPromise;
});
it("preserves replacement identity written before the replacement HTTP server is tracked", async () => {
const oldServer = createHttpServerMock();
httpServers.set("alpha", oldServer.server);
setFeishuBotIdentityState("alpha", { botOpenId: "ou_old", botName: "Old" });
const stopPromise = stopFeishuMonitorState("alpha");
await Promise.resolve();
setFeishuBotIdentityState("alpha", { botOpenId: "ou_new", botName: "New" });
oldServer.finishClose();
await stopPromise;
expect(httpServers.has("alpha")).toBe(false);
expect(botOpenIds.get("alpha")).toBe("ou_new");
expect(botNames.get("alpha")).toBe("New");
await stopFeishuMonitorState("alpha");
});
it("forces targeted HTTP server cleanup after the close timeout", async () => {
vi.useFakeTimers();
const { server, close, closeAllConnections } = createHttpServerMock();
httpServers.set("alpha", server);
botOpenIds.set("alpha", "ou_alpha");
botNames.set("alpha", "Alpha");
const stopPromise = stopFeishuMonitorState("alpha");
await Promise.resolve();
expect(close).toHaveBeenCalledTimes(1);
expect(httpServers.get("alpha")).toBe(server);
await vi.advanceTimersByTimeAsync(FEISHU_HTTP_SERVER_CLOSE_TIMEOUT_MS - 1);
expect(closeAllConnections).not.toHaveBeenCalled();
expect(httpServers.get("alpha")).toBe(server);
await vi.advanceTimersByTimeAsync(1);
await stopPromise;
expect(closeAllConnections).toHaveBeenCalledTimes(1);
expect(httpServers.has("alpha")).toBe(false);
expect(botOpenIds.has("alpha")).toBe(false);
expect(botNames.has("alpha")).toBe(false);
});
it("preserves replacement HTTP state after delayed global cleanup", async () => {
const oldServer = createHttpServerMock();
const replacementServer = createHttpServerMock();
httpServers.set("alpha", oldServer.server);
setFeishuBotIdentityState("alpha", { botOpenId: "ou_old", botName: "Old" });
const stopPromise = stopFeishuMonitorState();
await Promise.resolve();
setFeishuBotIdentityState("alpha", { botOpenId: "ou_new", botName: "New" });
httpServers.set("alpha", replacementServer.server);
oldServer.finishClose();
await stopPromise;
expect(httpServers.get("alpha")).toBe(replacementServer.server);
expect(botOpenIds.get("alpha")).toBe("ou_new");
expect(botNames.get("alpha")).toBe("New");
const cleanupPromise = stopFeishuMonitorState("alpha");
await Promise.resolve();
replacementServer.finishClose();
await cleanupPromise;
});
});

View File

@@ -365,7 +365,6 @@ describe("resolveReactionSyntheticEvent", () => {
uuid: () => "fixed-uuid",
});
expect(result?.message.message_id).toBe("om_msg1:reaction:THUMBSUP:fixed-uuid");
expect(result?.message.typing_target_message_id).toBe("om_msg1");
});
it("drops unverified reactions when sender verification times out", async () => {
@@ -400,7 +399,6 @@ describe("resolveReactionSyntheticEvent", () => {
},
message: {
message_id: "om_msg1:reaction:THUMBSUP:fixed-uuid",
typing_target_message_id: "om_msg1",
chat_id: "oc_group_from_event",
chat_type: "group",
message_type: "text",

View File

@@ -54,8 +54,8 @@ async function waitForStartedAccount(started: string[], accountId: string) {
);
}
afterEach(async () => {
await stopFeishuMonitor();
afterEach(() => {
stopFeishuMonitor();
});
afterAll(() => {

View File

@@ -13,14 +13,9 @@ export const wsClients = new Map<string, Lark.WSClient>();
export const httpServers = new Map<string, http.Server>();
export const botOpenIds = new Map<string, string>();
export const botNames = new Map<string, string>();
// HTTP close is awaited, so a replacement monitor can write identity before
// registering its replacement server. Revisions keep stale close cleanup from
// erasing that newer identity.
const botIdentityRevisions = new Map<string, number>();
export const FEISHU_WEBHOOK_MAX_BODY_BYTES = 64 * 1024;
export const FEISHU_WEBHOOK_BODY_TIMEOUT_MS = 5_000;
export const FEISHU_HTTP_SERVER_CLOSE_TIMEOUT_MS = 5_000;
type WebhookRateLimitDefaults = {
windowMs: number;
@@ -34,10 +29,6 @@ type WebhookAnomalyDefaults = {
logEvery: number;
};
type BotIdentitySnapshot = {
revision: number;
};
const FEISHU_WEBHOOK_RATE_LIMIT_FALLBACK_DEFAULTS: WebhookRateLimitDefaults = {
windowMs: 60_000,
maxRequests: 120,
@@ -125,124 +116,6 @@ function closeWsClient(client: Lark.WSClient | undefined): void {
}
}
function readBotIdentityRevision(accountId: string): number {
return botIdentityRevisions.get(accountId) ?? 0;
}
function bumpBotIdentityRevision(accountId: string): void {
botIdentityRevisions.set(accountId, readBotIdentityRevision(accountId) + 1);
}
function captureBotIdentitySnapshot(accountId: string): BotIdentitySnapshot {
return { revision: readBotIdentityRevision(accountId) };
}
function captureBotIdentitySnapshots(): Array<[accountId: string, snapshot: BotIdentitySnapshot]> {
const accountIds = new Set([...botOpenIds.keys(), ...botNames.keys()]);
return Array.from(accountIds, (accountId): [string, BotIdentitySnapshot] => [
accountId,
captureBotIdentitySnapshot(accountId),
]);
}
function clearFeishuBotIdentityStateIfUnchanged(
accountId: string,
snapshot: BotIdentitySnapshot,
): void {
if (readBotIdentityRevision(accountId) !== snapshot.revision) {
return;
}
botOpenIds.delete(accountId);
botNames.delete(accountId);
bumpBotIdentityRevision(accountId);
}
export function setFeishuBotIdentityState(
accountId: string,
identity: { botOpenId: string; botName: string | undefined },
): void {
botOpenIds.set(accountId, identity.botOpenId);
if (identity.botName) {
botNames.set(accountId, identity.botName);
} else {
botNames.delete(accountId);
}
bumpBotIdentityRevision(accountId);
}
export function clearFeishuBotIdentityState(accountId: string): void {
botOpenIds.delete(accountId);
botNames.delete(accountId);
bumpBotIdentityRevision(accountId);
}
function isServerNotRunningError(error: Error): boolean {
return (error as NodeJS.ErrnoException).code === "ERR_SERVER_NOT_RUNNING";
}
export async function closeFeishuHttpServer(server: http.Server): Promise<void> {
await new Promise<void>((resolve, reject) => {
let settled = false;
const settle = (err?: Error) => {
if (settled) {
return;
}
settled = true;
clearTimeout(fallbackTimer);
if (!err || isServerNotRunningError(err)) {
resolve();
return;
}
reject(err);
};
const fallbackTimer = setTimeout(() => {
try {
server.closeAllConnections();
settle();
} catch (err) {
settle(err instanceof Error ? err : new Error(String(err)));
}
}, FEISHU_HTTP_SERVER_CLOSE_TIMEOUT_MS);
try {
server.close((err) => {
settle(err);
});
} catch (err) {
settle(err instanceof Error ? err : new Error(String(err)));
}
});
}
export async function closeTrackedFeishuHttpServer(
accountId: string,
server: http.Server,
): Promise<void> {
const identitySnapshot = captureBotIdentitySnapshot(accountId);
try {
await closeFeishuHttpServer(server);
} finally {
if (httpServers.get(accountId) === server) {
httpServers.delete(accountId);
clearFeishuBotIdentityStateIfUnchanged(accountId, identitySnapshot);
}
}
}
async function closeTrackedHttpServers(
entries: Array<[accountId: string, server: http.Server]>,
): Promise<void> {
const results = await Promise.allSettled(
entries.map(([accountId, server]) => closeTrackedFeishuHttpServer(accountId, server)),
);
const rejected = results.find(
(result): result is PromiseRejectedResult => result.status === "rejected",
);
if (rejected) {
throw rejected.reason;
}
}
export function clearFeishuWebhookRateLimitStateForTest(): void {
feishuWebhookRateLimiter.clear();
feishuWebhookAnomalyTracker.clear();
@@ -271,16 +144,17 @@ export function recordWebhookStatus(
});
}
export async function stopFeishuMonitorState(accountId?: string): Promise<void> {
export function stopFeishuMonitorState(accountId?: string): void {
if (accountId) {
closeWsClient(wsClients.get(accountId));
wsClients.delete(accountId);
const server = httpServers.get(accountId);
if (server) {
await closeTrackedFeishuHttpServer(accountId, server);
return;
server.close();
httpServers.delete(accountId);
}
clearFeishuBotIdentityState(accountId);
botOpenIds.delete(accountId);
botNames.delete(accountId);
return;
}
@@ -288,14 +162,10 @@ export async function stopFeishuMonitorState(accountId?: string): Promise<void>
closeWsClient(client);
}
wsClients.clear();
const identitySnapshots = captureBotIdentitySnapshots();
try {
await closeTrackedHttpServers([...httpServers.entries()]);
} finally {
for (const [identityAccountId, snapshot] of identitySnapshots) {
if (!httpServers.has(identityAccountId)) {
clearFeishuBotIdentityStateIfUnchanged(identityAccountId, snapshot);
}
}
for (const server of httpServers.values()) {
server.close();
}
httpServers.clear();
botOpenIds.clear();
botNames.clear();
}

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