mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-21 06:22:28 +08:00
Compare commits
1 Commits
dallin/mat
...
codex/secu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
93fc591af9 |
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
61
.github/codeql/codeql-process-exec-boundary-critical-security.yml
vendored
Normal file
61
.github/codeql/codeql-process-exec-boundary-critical-security.yml
vendored
Normal 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
1
.github/labeler.yml
vendored
@@ -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:
|
||||
|
||||
47
.github/workflows/codeql.yml
vendored
47
.github/workflows/codeql.yml
vendored
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
216
.github/workflows/openclaw-release-publish.yml
vendored
216
.github/workflows/openclaw-release-publish.yml
vendored
@@ -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
|
||||
|
||||
11
.github/workflows/stale.yml
vendored
11
.github/workflows/stale.yml
vendored
@@ -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"]);
|
||||
|
||||
221
.github/workflows/windows-node-release.yml
vendored
221
.github/workflows/windows-node-release.yml
vendored
@@ -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
|
||||
|
||||
29
CHANGELOG.md
29
CHANGELOG.md
@@ -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
|
||||
|
||||
@@ -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 ──────────────────────────────────────────
|
||||
|
||||
@@ -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")!,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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"),
|
||||
],
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
|
||||
@@ -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"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -943,10 +943,6 @@
|
||||
"source": "Matrix QA",
|
||||
"target": "Matrix QA"
|
||||
},
|
||||
{
|
||||
"source": "Maturity tests",
|
||||
"target": "成熟度测试"
|
||||
},
|
||||
{
|
||||
"source": "Matrix presentation metadata",
|
||||
"target": "Matrix 呈现元数据"
|
||||
|
||||
@@ -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`).
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
11
docs/ci.md
11
docs/ci.md
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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 (5000–15000 ms) work too — the trade-off is a higher chance of
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
@@ -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 }`.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 },
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
866
docs/taxonomy.md
866
docs/taxonomy.md
@@ -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 |
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -34,7 +34,6 @@ export function buildAnthropicCliBackend(): CliBackendPlugin {
|
||||
bundleMcp: true,
|
||||
bundleMcpMode: "claude-config-file",
|
||||
nativeToolMode: "always-on",
|
||||
sideQuestionToolMode: "disabled",
|
||||
ownsNativeCompaction: true,
|
||||
config: {
|
||||
command: "claude",
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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[];
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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." }],
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -260,5 +260,4 @@ export function setFeishuClientRuntimeForTest(overrides?: {
|
||||
feishuClientSdk = overrides?.sdk
|
||||
? { ...defaultFeishuClientSdk, ...overrides.sdk }
|
||||
: defaultFeishuClientSdk;
|
||||
clearClientCache();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -54,8 +54,8 @@ async function waitForStartedAccount(started: string[], accountId: string) {
|
||||
);
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
await stopFeishuMonitor();
|
||||
afterEach(() => {
|
||||
stopFeishuMonitor();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user