Compare commits

..

1 Commits

Author SHA1 Message Date
Vincent Koc
22f2d1a4d0 docs(codex): document native model lifecycle hooks 2026-04-23 20:16:25 -07:00
2215 changed files with 24466 additions and 127979 deletions

View File

@@ -49,66 +49,6 @@ pnpm openclaw qa suite \
5. If the user wants to watch the live UI, find the current `openclaw-qa` listen port and report `http://127.0.0.1:<port>`.
6. If a scenario fails, fix the product or harness root cause, then rerun the full lane.
## QA credentials and 1Password
- Use `op` only inside `tmux` for QA secret lookup in this repo.
- Quick auth check inside tmux:
```bash
op account list
```
- Direct Telegram npm live test secrets currently live in 1Password item:
- vault: `OpenClaw`
- item: `Telegram E2E`
- That item is the first place to look for:
- `OPENCLAW_QA_TELEGRAM_DRIVER_BOT_TOKEN`
- `OPENCLAW_QA_TELEGRAM_SUT_BOT_TOKEN`
- `OPENCLAW_QA_PROVIDER_MODE`
- `OPENCLAW_NPM_TELEGRAM_PACKAGE_SPEC`
- Convex QA secrets currently live in 1Password items:
- vault: `OpenClaw`
- item: `OPENCLAW_QA_CONVEX_SITE_URL`
- item: `OPENCLAW_QA_CONVEX_SECRET_MAINTAINER`
- item: `OPENCLAW_QA_CONVEX_SECRET_CI`
- Additional related notes/login items seen during QA credential work:
- vault: `Private`
- items: `OPENCLAW QA`, `Convex`, `Telegram`
- If a required value is missing from those notes:
- do not guess
- ask the maintainer/operator for the current value or the current 1Password item name
- for Telegram direct runs, `OPENCLAW_QA_TELEGRAM_GROUP_ID` may be stored separately from `Telegram E2E`
- for Convex runs, the leased Telegram credential should provide the Telegram group id and bot tokens together; do not require a separate `OPENCLAW_QA_TELEGRAM_GROUP_ID`
- for Convex runs, prefer `OpenClaw/OPENCLAW_QA_CONVEX_SITE_URL`; if that is stale or unclear, ask for the active pool URL before running
- Prefer direct Telegram envs for the npm Telegram Docker lane when available:
```bash
OPENCLAW_QA_TELEGRAM_GROUP_ID="..." \
OPENCLAW_QA_TELEGRAM_DRIVER_BOT_TOKEN="..." \
OPENCLAW_QA_TELEGRAM_SUT_BOT_TOKEN="..." \
OPENCLAW_QA_PROVIDER_MODE="mock-openai" \
OPENCLAW_NPM_TELEGRAM_PACKAGE_SPEC="openclaw@beta" \
pnpm test:docker:npm-telegram-live
```
- Prefer Convex mode when the goal is stable shared QA infra:
- round-robin credential leasing
- thinner wrapper for channel-specific setup
- CLI/admin flows around the pooled credentials
- Live npm Telegram Docker lane note:
- `scripts/e2e/npm-telegram-live-runner.ts` reads `OPENCLAW_NPM_TELEGRAM_PROVIDER_MODE`
- do not assume `OPENCLAW_QA_PROVIDER_MODE` is consumed by that wrapper
- if a 1Password note only gives `OPENCLAW_QA_PROVIDER_MODE`, map it explicitly to `OPENCLAW_NPM_TELEGRAM_PROVIDER_MODE` before running the Docker lane
- Verified live shape:
- Convex mode can pass the real Docker lane without direct Telegram env vars
- leased Telegram payload includes the group id coupled to the driver/SUT tokens
- a real run of `pnpm test:docker:npm-telegram-live` passed with:
- `OPENCLAW_QA_CREDENTIAL_SOURCE=convex`
- `OPENCLAW_QA_CREDENTIAL_ROLE=maintainer`
- `OPENCLAW_QA_CONVEX_SITE_URL`
- `OPENCLAW_QA_CONVEX_SECRET_MAINTAINER`
- `OPENCLAW_NPM_TELEGRAM_PROVIDER_MODE=mock-openai`
## Character evals
Use `qa character-eval` for style/persona/vibe checks across multiple live models.

View File

@@ -279,20 +279,8 @@ node --import tsx scripts/openclaw-npm-postpublish-verify.ts <published-version>
- `node --import tsx scripts/openclaw-npm-postpublish-verify.ts <beta-version>`
- install/update smoke against the published beta channel
- Docker install/update coverage that exercises the published beta package
- published npm Telegram proof: dispatch Actions > `NPM Telegram Beta E2E`
from `main` with `package_spec=openclaw@<beta-version>` and
`provider_mode=mock-openai`, approve `npm-release`, and require success.
This is the default button path for installed-package onboarding,
Telegram setup, and real Telegram E2E against the published npm package.
Use the local `pnpm test:docker:npm-telegram-live` lane with the matching
`OPENCLAW_NPM_TELEGRAM_PACKAGE_SPEC` and Convex CI env only as a fallback
or debugging path.
- Parallels published beta install/update coverage with both OpenAI and
Anthropic provider keys available
- Parallels install/update proof must keep plugin installs enabled unless the
operator explicitly scopes a harness-only isolation check; a lane that
disables bundled plugin installs is not valid plugin/dependency release
evidence.
- targeted QA reruns only for areas touched by fixes after the full pre-npm
roster, unless the operator requests the full QA roster again. If the fix
touches live channel QA, credential plumbing, Matrix, Telegram, or the QA
@@ -341,17 +329,10 @@ node --import tsx scripts/openclaw-npm-postpublish-verify.ts <published-version>
`openclaw/releases-private/.github/workflows/openclaw-npm-dist-tags.yml`
workflow because `npm dist-tag` management needs `NPM_TOKEN`, while the
public npm release workflow stays OIDC-only.
- Prefer fixing the private workflow token path over any local 1Password
fallback. The desired setup is a granular npm token stored as the private
repo's `NPM_TOKEN` secret, scoped to the `openclaw` package with read/write
and 2FA bypass for automation.
- If the private dist-tag workflow cannot promote because `NPM_TOKEN` is absent
or stale, use the local tmux + 1Password fallback:
- Start or reuse a tmux session so interactive `npm login` and OTP prompts
are observable and recoverable.
- Hard rule: never run `op` directly in the main agent shell during release
work. Any 1Password CLI use must happen inside that tmux session so prompts
and alerts are contained and observable.
- Use the 1Password item `op://Private/Npmjs` for npm credentials and OTP.
Do not print passwords, tokens, or OTPs to the transcript; send them through
tmux buffers, env vars scoped to the tmux command, or `expect` with
@@ -521,11 +502,9 @@ node --import tsx scripts/openclaw-npm-postpublish-verify.ts <published-version>
23. Run the post-published beta verification roster. If any lane fails after
the beta tag/package is pushed or published, fix, commit/push/pull,
increment to the next beta tag, and restart at the full pre-npm beta test
roster for the new beta. The roster includes the manual Actions >
`NPM Telegram Beta E2E` workflow against the exact published beta package.
If a pre-npm lane fails before any tag/package leaves the machine, fix and
rerun the same intended beta attempt. Repeat up to the operator's
authorized beta-attempt limit, normally 4.
roster for the new beta. If a pre-npm lane fails before any tag/package
leaves the machine, fix and rerun the same intended beta attempt. Repeat up
to the operator's authorized beta-attempt limit, normally 4.
24. Announce the beta/stable release on Discord best-effort using Peter's bot
token from `.profile`.
25. If the operator requested beta only, stop after beta verification and the

View File

@@ -8,14 +8,6 @@
.bun-cache
.bun
.artifacts
**/.artifacts
.local
**/.local
.pi
**/.pi
__openclaw_vitest__
**/__openclaw_vitest__
.tmp
**/.tmp
.DS_Store
@@ -46,9 +38,6 @@ docs/.generated
*.log
tmp
**/tmp
dist-runtime
**/dist-runtime
openclaw-path-alias-*
# build artifacts
dist

View File

@@ -12,9 +12,6 @@ paths-ignore:
- docs
- "**/node_modules"
- "**/coverage"
- "**/*.generated.ts"
- "**/*.bundle.js"
- "**/*-runtime.js"
- "**/*.test.ts"
- "**/*.test.tsx"
- "**/*.e2e.test.ts"

9
.github/labeler.yml vendored
View File

@@ -29,11 +29,6 @@
- any-glob-to-any-file:
- "extensions/google-meet/**"
- "docs/plugins/google-meet.md"
"plugin: bonjour":
- changed-files:
- any-glob-to-any-file:
- "extensions/bonjour/**"
- "docs/gateway/bonjour.md"
"channel: imessage":
- changed-files:
- any-glob-to-any-file:
@@ -387,7 +382,3 @@
- changed-files:
- any-glob-to-any-file:
- "extensions/fal/**"
"extensions: gradium":
- changed-files:
- any-glob-to-any-file:
- "extensions/gradium/**"

View File

@@ -3,9 +3,6 @@ name: CI
on:
push:
branches: [main]
paths-ignore:
- "**/*.md"
- "docs/**"
pull_request:
types: [opened, reopened, synchronize, ready_for_review, converted_to_draft]
@@ -40,7 +37,6 @@ jobs:
has_changed_extensions: ${{ steps.manifest.outputs.has_changed_extensions }}
changed_extensions_matrix: ${{ steps.manifest.outputs.changed_extensions_matrix }}
run_build_artifacts: ${{ steps.manifest.outputs.run_build_artifacts }}
run_checks_fast_core: ${{ steps.manifest.outputs.run_checks_fast_core }}
run_checks_fast: ${{ steps.manifest.outputs.run_checks_fast }}
checks_fast_core_matrix: ${{ steps.manifest.outputs.checks_fast_core_matrix }}
channel_contracts_matrix: ${{ steps.manifest.outputs.channel_contracts_matrix }}
@@ -131,9 +127,6 @@ jobs:
OPENCLAW_CI_RUN_MACOS: ${{ steps.changed_scope.outputs.run_macos || 'false' }}
OPENCLAW_CI_RUN_ANDROID: ${{ steps.changed_scope.outputs.run_android || 'false' }}
OPENCLAW_CI_RUN_WINDOWS: ${{ steps.changed_scope.outputs.run_windows || 'false' }}
OPENCLAW_CI_RUN_NODE_FAST_ONLY: ${{ steps.changed_scope.outputs.run_node_fast_only || 'false' }}
OPENCLAW_CI_RUN_NODE_FAST_PLUGIN_CONTRACTS: ${{ steps.changed_scope.outputs.run_node_fast_plugin_contracts || 'false' }}
OPENCLAW_CI_RUN_NODE_FAST_CI_ROUTING: ${{ steps.changed_scope.outputs.run_node_fast_ci_routing || 'false' }}
OPENCLAW_CI_RUN_SKILLS_PYTHON: ${{ steps.changed_scope.outputs.run_skills_python || 'false' }}
OPENCLAW_CI_RUN_CONTROL_UI_I18N: ${{ steps.changed_scope.outputs.run_control_ui_i18n || 'false' }}
OPENCLAW_CI_HAS_CHANGED_EXTENSIONS: ${{ steps.changed_extensions.outputs.has_changed_extensions || 'false' }}
@@ -177,23 +170,12 @@ jobs:
const docsOnly = parseBoolean(process.env.OPENCLAW_CI_DOCS_ONLY);
const docsChanged = parseBoolean(process.env.OPENCLAW_CI_DOCS_CHANGED);
const runNode = parseBoolean(process.env.OPENCLAW_CI_RUN_NODE) && !docsOnly;
const runNodeFastOnly =
runNode && parseBoolean(process.env.OPENCLAW_CI_RUN_NODE_FAST_ONLY);
const runNodeFull = runNode && !runNodeFastOnly;
const runNodeFastPluginContracts =
runNode && parseBoolean(process.env.OPENCLAW_CI_RUN_NODE_FAST_PLUGIN_CONTRACTS);
const runNodeFastCiRouting =
runNode && parseBoolean(process.env.OPENCLAW_CI_RUN_NODE_FAST_CI_ROUTING);
const runChecksFastCore = runNodeFull || runNodeFastPluginContracts || runNodeFastCiRouting;
const runMacos =
parseBoolean(process.env.OPENCLAW_CI_RUN_MACOS) && !docsOnly && isCanonicalRepository;
const runAndroid =
parseBoolean(process.env.OPENCLAW_CI_RUN_ANDROID) && !docsOnly && isCanonicalRepository;
const runWindows =
parseBoolean(process.env.OPENCLAW_CI_RUN_WINDOWS) &&
!docsOnly &&
!runNodeFastOnly &&
isCanonicalRepository;
parseBoolean(process.env.OPENCLAW_CI_RUN_WINDOWS) && !docsOnly && isCanonicalRepository;
const runSkillsPython = parseBoolean(process.env.OPENCLAW_CI_RUN_SKILLS_PYTHON) && !docsOnly;
const runControlUiI18n =
parseBoolean(process.env.OPENCLAW_CI_RUN_CONTROL_UI_I18N) && !docsOnly;
@@ -206,49 +188,18 @@ jobs:
? DEFAULT_EXTENSION_TEST_SHARD_COUNT
: Math.max(DEFAULT_EXTENSION_TEST_SHARD_COUNT, 36);
const extensionShardMatrix = createMatrix(
runNodeFull
runNode
? createExtensionTestShards({
shardCount: extensionTestShardCount,
}).map((shard) => ({
check_name: shard.checkName,
extensions_csv: shard.extensionIds.join(","),
runner: isCanonicalRepository && [0, 3, 4].includes(shard.index)
? "blacksmith-8vcpu-ubuntu-2404"
: isCanonicalRepository
? "blacksmith-4vcpu-ubuntu-2404"
: "ubuntu-24.04",
shard_index: shard.index + 1,
task: "extensions-batch",
}))
: [],
);
const checksFastCoreTasks = [];
if (runNodeFull) {
checksFastCoreTasks.push(
{ check_name: "checks-fast-bundled", runtime: "node", task: "bundled" },
{
check_name: "checks-fast-contracts-plugins",
runtime: "node",
task: "contracts-plugins",
},
);
} else {
if (runNodeFastPluginContracts) {
checksFastCoreTasks.push({
check_name: "checks-fast-contracts-plugins",
runtime: "node",
task: runNodeFastCiRouting ? "contracts-plugins-ci-routing" : "contracts-plugins",
});
} else if (runNodeFastCiRouting) {
checksFastCoreTasks.push({
check_name: "checks-fast-ci-routing",
runtime: "node",
task: "ci-routing",
});
}
}
const nodeTestShards = runNodeFull
const nodeTestShards = runNode
? createNodeTestShards().map((shard) => ({
check_name: shard.checkName,
runtime: "node",
@@ -257,7 +208,6 @@ jobs:
configs: shard.configs,
includePatterns: shard.includePatterns,
requires_dist: shard.requiresDist,
runner: shard.runner,
}))
: [];
const nodeTestNonDistShards = nodeTestShards.filter((shard) => !shard.requires_dist);
@@ -273,17 +223,25 @@ jobs:
run_windows: runWindows,
has_changed_extensions: hasChangedExtensions,
changed_extensions_matrix: changedExtensionsMatrix,
run_build_artifacts: runNodeFull,
run_checks_fast_core: runChecksFastCore,
run_checks_fast: runNodeFull,
checks_fast_core_matrix: createMatrix(checksFastCoreTasks),
channel_contracts_matrix: createMatrix(
runNodeFull ? createChannelContractTestShards() : [],
run_build_artifacts: runNode,
run_checks_fast: runNode,
checks_fast_core_matrix: createMatrix(
runNode
? [
{ check_name: "checks-fast-bundled", runtime: "node", task: "bundled" },
{
check_name: "checks-fast-contracts-plugins",
runtime: "node",
task: "contracts-plugins",
},
]
: [],
),
channel_contracts_matrix: createMatrix(runNode ? createChannelContractTestShards() : []),
checks_node_extensions_matrix: extensionShardMatrix,
run_checks: runNodeFull,
run_checks: runNode,
checks_matrix: createMatrix(
runNodeFull
runNode
? [
{ check_name: "checks-node-channels", runtime: "node", task: "channels" },
]
@@ -302,9 +260,9 @@ jobs:
}))
: [],
),
run_check: runNodeFull,
run_check_additional: runNodeFull,
run_build_smoke: runNodeFull,
run_check: runNode,
run_check_additional: runNode,
run_build_smoke: runNode,
run_check_docs: docsChanged,
run_control_ui_i18n: runControlUiI18n,
run_skills_python_job: runSkillsPython,
@@ -695,8 +653,8 @@ jobs:
contents: read
name: ${{ matrix.check_name }}
needs: [preflight]
if: needs.preflight.outputs.run_checks_fast_core == 'true'
runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-24.04' }}
if: needs.preflight.outputs.run_checks_fast == 'true'
runs-on: ubuntu-24.04
timeout-minutes: 60
strategy:
fail-fast: false
@@ -772,13 +730,6 @@ jobs:
contracts-plugins)
pnpm test:contracts:plugins
;;
contracts-plugins-ci-routing)
pnpm test:contracts:plugins
pnpm test src/commands/status.scan-result.test.ts src/scripts/ci-changed-scope.test.ts test/scripts/test-projects.test.ts
;;
ci-routing)
pnpm test src/commands/status.scan-result.test.ts src/scripts/ci-changed-scope.test.ts test/scripts/test-projects.test.ts
;;
*)
echo "Unsupported checks-fast task: $TASK" >&2
exit 1
@@ -962,7 +913,7 @@ jobs:
name: ${{ matrix.check_name }}
needs: [preflight]
if: needs.preflight.outputs.run_checks_fast == 'true'
runs-on: ${{ matrix.runner }}
runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-8vcpu-ubuntu-2404' || 'ubuntu-24.04' }}
timeout-minutes: 60
strategy:
fail-fast: false
@@ -1024,7 +975,7 @@ jobs:
- name: Run extension shard
env:
NODE_OPTIONS: --max-old-space-size=6144
OPENCLAW_EXTENSION_BATCH_PARALLEL: 2
OPENCLAW_EXTENSION_BATCH_PARALLEL: 1
OPENCLAW_VITEST_MAX_WORKERS: 1
OPENCLAW_EXTENSION_BATCH: ${{ matrix.extensions_csv }}
run: pnpm test:extensions:batch -- "$OPENCLAW_EXTENSION_BATCH"
@@ -1084,8 +1035,8 @@ jobs:
contents: read
name: checks-node-compat-node22
needs: [preflight]
if: needs.preflight.outputs.run_build_artifacts == 'true' && github.event_name == 'push'
runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-24.04' }}
if: needs.preflight.outputs.run_node == 'true' && github.event_name == 'push'
runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-8vcpu-ubuntu-2404' || 'ubuntu-24.04' }}
timeout-minutes: 60
steps:
- name: Checkout
@@ -1162,7 +1113,10 @@ jobs:
name: ${{ matrix.check_name }}
needs: [preflight]
if: needs.preflight.outputs.run_checks_node_core_nondist == 'true'
runs-on: ${{ github.repository == 'openclaw/openclaw' && (matrix.runner || 'ubuntu-24.04') || 'ubuntu-24.04' }}
# Keep core shards on GitHub-hosted runners. The Blacksmith pool is already
# occupied by build and extension shards; queueing these shards there hides
# actual test-speed improvements behind runner wait time.
runs-on: ubuntu-24.04
timeout-minutes: 60
strategy:
fail-fast: false
@@ -1418,16 +1372,16 @@ jobs:
runner: ubuntu-24.04
- check_name: check-prod-types
task: prod-types
runner: blacksmith-4vcpu-ubuntu-2404
runner: ubuntu-24.04
- check_name: check-lint
task: lint
runner: blacksmith-16vcpu-ubuntu-2404
runner: ubuntu-24.04
- check_name: check-policy-guards
task: policy-guards
runner: ubuntu-24.04
- check_name: check-test-types
task: test-types
runner: blacksmith-4vcpu-ubuntu-2404
runner: ubuntu-24.04
- check_name: check-strict-smoke
task: strict-smoke
runner: ubuntu-24.04
@@ -1502,7 +1456,7 @@ jobs:
pnpm tsgo:prod
;;
lint)
pnpm lint --threads=8
pnpm lint
;;
policy-guards)
pnpm lint:webhook:no-low-level-body-read
@@ -1627,7 +1581,7 @@ jobs:
packages/plugin-sdk/dist
extensions/*/dist/.boundary-tsc.tsbuildinfo
extensions/*/dist/.boundary-tsc.stamp
key: ${{ runner.os }}-extension-package-boundary-v1-${{ hashFiles('tsconfig.json', 'tsconfig.plugin-sdk.dts.json', 'packages/plugin-sdk/tsconfig.json', 'scripts/check-extension-package-tsc-boundary.mjs', 'scripts/prepare-extension-package-boundary-artifacts.mjs', 'scripts/write-plugin-sdk-entry-dts.ts', 'scripts/lib/plugin-sdk-entrypoints.json', 'scripts/lib/plugin-sdk-entries.mjs', 'src/plugin-sdk/**', 'src/auto-reply/**', 'src/video-generation/dashscope-compatible.ts', 'src/video-generation/types.ts', 'src/types/**', 'extensions/**', 'extensions/tsconfig.package-boundary*.json', 'package.json', 'pnpm-lock.yaml') }}
key: ${{ runner.os }}-extension-package-boundary-v1-${{ hashFiles('tsconfig.json', 'tsconfig.plugin-sdk.dts.json', 'packages/plugin-sdk/tsconfig.json', 'scripts/check-extension-package-tsc-boundary.mjs', 'scripts/prepare-extension-package-boundary-artifacts.mjs', 'scripts/write-plugin-sdk-entry-dts.ts', 'scripts/lib/plugin-sdk-entrypoints.json', 'scripts/lib/plugin-sdk-entries.mjs', 'src/plugin-sdk/**', 'src/video-generation/dashscope-compatible.ts', 'src/video-generation/types.ts', 'src/types/**', 'extensions/**', 'extensions/tsconfig.package-boundary*.json', 'package.json', 'pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-extension-package-boundary-v1-

View File

@@ -26,7 +26,7 @@ jobs:
matrix:
include:
- language: javascript-typescript
runs_on: blacksmith-32vcpu-ubuntu-2404
runs_on: blacksmith-16vcpu-ubuntu-2404
needs_node: true
needs_python: false
needs_java: false

View File

@@ -137,7 +137,7 @@ jobs:
env:
OPENAI_API_KEY: ${{ secrets.OPENCLAW_DOCS_I18N_OPENAI_API_KEY || secrets.OPENAI_API_KEY }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
OPENCLAW_CONTROL_UI_I18N_MODEL: ${{ vars.OPENCLAW_CI_OPENAI_MODEL_BARE }}
OPENCLAW_CONTROL_UI_I18N_MODEL: gpt-5.4
OPENCLAW_CONTROL_UI_I18N_THINKING: low
LOCALE: ${{ matrix.locale }}
run: node --import tsx scripts/control-ui-i18n.ts sync --locale "${LOCALE}" --write

View File

@@ -156,7 +156,7 @@ jobs:
with:
openai-api-key: ${{ secrets.OPENCLAW_DOCS_AGENT_OPENAI_API_KEY || secrets.OPENAI_API_KEY }}
prompt-file: .github/codex/prompts/docs-agent.md
model: ${{ vars.OPENCLAW_CI_OPENAI_MODEL_BARE }}
model: gpt-5.4
effort: medium
sandbox: workspace-write
safety-strategy: drop-sudo

View File

@@ -63,43 +63,18 @@ jobs:
working-directory: publish
run: |
set -euo pipefail
remote_source_sha() {
git show refs/remotes/origin/main:.openclaw-sync/source.json 2>/dev/null \
| node -e 'const fs = require("node:fs"); try { const data = JSON.parse(fs.readFileSync(0, "utf8")); if (data.sha) process.stdout.write(data.sha); } catch {}' \
|| true
}
skip_stale_source() {
current_source_sha="$(remote_source_sha)"
if [ -z "$current_source_sha" ] || [ "$current_source_sha" = "$GITHUB_SHA" ]; then
return
fi
if git -C "$GITHUB_WORKSPACE" merge-base --is-ancestor "$GITHUB_SHA" "$current_source_sha"; then
echo "Skipping stale publish sync for $GITHUB_SHA; origin/main already mirrors $current_source_sha."
exit 0
fi
}
if git diff --quiet -- docs .openclaw-sync; then
echo "No publish-repo changes."
exit 0
fi
if git fetch origin main:refs/remotes/origin/main; then
skip_stale_source
fi
git config user.name "openclaw-docs-sync[bot]"
git config user.email "openclaw-docs-sync[bot]@users.noreply.github.com"
git add docs .openclaw-sync
git commit -m "chore(sync): mirror docs from $GITHUB_REPOSITORY@$GITHUB_SHA"
for attempt in 1 2 3 4 5; do
if git fetch origin main:refs/remotes/origin/main; then
skip_stale_source
if git rebase -X theirs origin/main && git push origin HEAD:main; then
exit 0
fi
if git fetch origin main && git rebase origin/main && git push origin HEAD:main; then
exit 0
fi
git rebase --abort >/dev/null 2>&1 || true
echo "Publish sync attempt ${attempt} failed; retrying."

View File

@@ -1,39 +0,0 @@
name: Docs
on:
push:
branches: [main]
paths:
- "**/*.md"
- "docs/**"
permissions:
contents: read
concurrency:
group: ${{ format('{0}-{1}', github.workflow, github.ref) }}
cancel-in-progress: true
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
jobs:
docs:
runs-on: ubuntu-24.04
timeout-minutes: 20
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 1
fetch-tags: false
persist-credentials: false
submodules: false
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
install-bun: "false"
- name: Check docs
run: pnpm check:docs

View File

@@ -1,6 +1,10 @@
name: Install Smoke
on:
push:
branches: [main]
pull_request:
types: [opened, reopened, synchronize, ready_for_review, converted_to_draft]
schedule:
- cron: "17 3 * * *"
workflow_dispatch:
@@ -26,7 +30,7 @@ permissions:
contents: read
concurrency:
group: ${{ github.event_name == 'workflow_dispatch' && format('{0}-manual-{1}', github.workflow, github.run_id) || format('{0}-{1}', github.workflow, github.ref) }}
group: ${{ github.event_name == 'workflow_dispatch' && format('{0}-manual-{1}', github.workflow, github.run_id) || github.event_name == 'pull_request' && format('{0}-{1}', github.workflow, github.event.pull_request.number) || format('{0}-{1}', github.workflow, github.ref) }}
cancel-in-progress: true
env:
@@ -34,6 +38,7 @@ env:
jobs:
preflight:
if: github.event_name != 'pull_request' || !github.event.pull_request.draft
runs-on: ubuntu-24.04
outputs:
docs_only: ${{ steps.manifest.outputs.docs_only }}
@@ -51,19 +56,67 @@ jobs:
persist-credentials: false
submodules: false
- name: Ensure preflight base commit
if: github.event_name != 'workflow_dispatch' && github.event_name != 'schedule' && github.event_name != 'workflow_call'
uses: ./.github/actions/ensure-base-commit
with:
base-sha: ${{ github.event_name == 'push' && github.event.before || github.event.pull_request.base.sha }}
fetch-ref: ${{ github.event_name == 'push' && github.ref_name || github.event.pull_request.base.ref }}
- name: Detect docs-only changes
id: docs_scope
uses: ./.github/actions/detect-docs-changes
- name: Detect changed smoke scope
id: changed_scope
if: github.event_name != 'workflow_dispatch' && github.event_name != 'schedule' && github.event_name != 'workflow_call' && steps.docs_scope.outputs.docs_only != 'true'
shell: bash
run: |
set -euo pipefail
if [ "${{ github.event_name }}" = "push" ]; then
BASE="${{ github.event.before }}"
else
BASE="${{ github.event.pull_request.base.sha }}"
fi
node scripts/ci-changed-scope.mjs --base "$BASE" --head HEAD
- name: Build install-smoke CI manifest
id: manifest
env:
OPENCLAW_CI_DOCS_ONLY: ${{ steps.docs_scope.outputs.docs_only }}
OPENCLAW_CI_EVENT_NAME: ${{ github.event_name }}
OPENCLAW_CI_FORCE_FULL_INSTALL_SMOKE: ${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'schedule' || github.event_name == 'workflow_call') && 'true' || 'false' }}
OPENCLAW_CI_WORKFLOW_BUN_GLOBAL_INSTALL_SMOKE: ${{ inputs.run_bun_global_install_smoke || 'false' }}
OPENCLAW_CI_RUN_FAST_INSTALL_SMOKE: ${{ steps.changed_scope.outputs.run_fast_install_smoke || steps.changed_scope.outputs.run_changed_smoke || 'false' }}
OPENCLAW_CI_RUN_FULL_INSTALL_SMOKE: ${{ steps.changed_scope.outputs.run_full_install_smoke || 'false' }}
run: |
docs_only="${OPENCLAW_CI_DOCS_ONLY:-false}"
event_name="${OPENCLAW_CI_EVENT_NAME:-}"
force_full_install_smoke="${OPENCLAW_CI_FORCE_FULL_INSTALL_SMOKE:-false}"
workflow_bun_global_install_smoke="${OPENCLAW_CI_WORKFLOW_BUN_GLOBAL_INSTALL_SMOKE:-false}"
docs_only=false
run_fast_install_smoke=true
run_full_install_smoke=true
run_changed_fast_install_smoke="${OPENCLAW_CI_RUN_FAST_INSTALL_SMOKE:-false}"
run_changed_full_install_smoke="${OPENCLAW_CI_RUN_FULL_INSTALL_SMOKE:-false}"
run_fast_install_smoke=false
run_full_install_smoke=false
run_bun_global_install_smoke=false
run_install_smoke=true
run_install_smoke=false
if [ "$force_full_install_smoke" = "true" ]; then
run_fast_install_smoke=true
run_full_install_smoke=true
run_install_smoke=true
elif [ "$docs_only" != "true" ] && [ "$event_name" != "push" ] && [ "$run_changed_full_install_smoke" = "true" ]; then
run_fast_install_smoke=true
run_full_install_smoke=true
run_install_smoke=true
elif [ "$docs_only" != "true" ] && [ "$run_changed_full_install_smoke" = "true" ]; then
run_fast_install_smoke=true
run_install_smoke=true
elif [ "$docs_only" != "true" ] && [ "$run_changed_fast_install_smoke" = "true" ]; then
run_fast_install_smoke=true
run_install_smoke=true
fi
if [ "$event_name" = "schedule" ]; then
run_bun_global_install_smoke=true
elif [ "$event_name" = "workflow_dispatch" ] || [ "$event_name" = "workflow_call" ]; then
@@ -116,12 +169,6 @@ jobs:
run: |
docker run --rm --entrypoint sh openclaw-dockerfile-smoke:local -lc 'which openclaw && openclaw --version'
- name: Run agents delete shared workspace Docker CLI smoke
env:
OPENCLAW_AGENTS_DELETE_SHARED_WORKSPACE_E2E_IMAGE: openclaw-dockerfile-smoke:local
OPENCLAW_AGENTS_DELETE_SHARED_WORKSPACE_E2E_SKIP_BUILD: "1"
run: bash scripts/e2e/agents-delete-shared-workspace-docker.sh
- name: Run Docker gateway network e2e
env:
OPENCLAW_GATEWAY_NETWORK_E2E_IMAGE: openclaw-dockerfile-smoke:local
@@ -217,12 +264,6 @@ jobs:
run: |
docker run --rm --entrypoint sh openclaw-dockerfile-smoke:local -lc 'which openclaw && openclaw --version'
- name: Run agents delete shared workspace Docker CLI smoke
env:
OPENCLAW_AGENTS_DELETE_SHARED_WORKSPACE_E2E_IMAGE: openclaw-dockerfile-smoke:local
OPENCLAW_AGENTS_DELETE_SHARED_WORKSPACE_E2E_SKIP_BUILD: "1"
run: bash scripts/e2e/agents-delete-shared-workspace-docker.sh
- name: Run Docker gateway network e2e
env:
OPENCLAW_GATEWAY_NETWORK_E2E_IMAGE: openclaw-dockerfile-smoke:local
@@ -285,6 +326,7 @@ jobs:
provenance: false
- name: Build installer non-root image
if: github.event_name != 'pull_request'
uses: useblacksmith/build-push-action@cbd1f60d194a98cb3be5523b15134501eaf0fbf3 # v2
with:
context: ./scripts/docker
@@ -314,8 +356,8 @@ jobs:
OPENCLAW_NO_ONBOARD: "1"
OPENCLAW_INSTALL_SMOKE_SKIP_CLI: "1"
OPENCLAW_INSTALL_SMOKE_SKIP_IMAGE_BUILD: "1"
OPENCLAW_INSTALL_NONROOT_SKIP_IMAGE_BUILD: "1"
OPENCLAW_INSTALL_SMOKE_SKIP_NONROOT: "0"
OPENCLAW_INSTALL_NONROOT_SKIP_IMAGE_BUILD: ${{ github.event_name == 'pull_request' && '0' || '1' }}
OPENCLAW_INSTALL_SMOKE_SKIP_NONROOT: ${{ github.event_name == 'pull_request' && '1' || '0' }}
OPENCLAW_INSTALL_SMOKE_SKIP_NPM_GLOBAL: "1"
OPENCLAW_INSTALL_SMOKE_SKIP_PREVIOUS: "1"
OPENCLAW_INSTALL_SMOKE_UPDATE_BASELINE: latest
@@ -349,5 +391,4 @@ jobs:
- name: Run fast bundled plugin Docker E2E
env:
OPENCLAW_BUNDLED_CHANNEL_DEPS_E2E_IMAGE: openclaw-bundled-channel-fast:local
OPENCLAW_BUNDLED_CHANNEL_DOCKER_RUN_TIMEOUT: 90s
run: timeout 240s pnpm test:docker:bundled-channel-deps:fast
run: timeout 120s pnpm test:docker:bundled-channel-deps:fast

View File

@@ -1,210 +0,0 @@
name: NPM Telegram Beta E2E
on:
workflow_dispatch:
inputs:
package_spec:
description: Published OpenClaw package spec to test
required: true
default: openclaw@beta
type: string
provider_mode:
description: QA provider mode
required: true
default: mock-openai
type: choice
options:
- mock-openai
- live-frontier
scenario:
description: Optional comma-separated Telegram scenario ids
required: false
type: string
permissions:
contents: read
concurrency:
group: npm-telegram-beta-e2e-${{ github.run_id }}
cancel-in-progress: false
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
NODE_VERSION: "24.x"
PNPM_VERSION: "10.33.0"
jobs:
validate_dispatch_ref:
name: Validate dispatch ref
runs-on: blacksmith-8vcpu-ubuntu-2404
steps:
- name: Require main workflow ref
env:
WORKFLOW_REF: ${{ github.ref }}
run: |
set -euo pipefail
if [[ "${WORKFLOW_REF}" != "refs/heads/main" ]]; then
echo "NPM Telegram beta E2E must be dispatched from main so workflow logic stays controlled." >&2
exit 1
fi
approve_release_manager:
name: Approve npm Telegram beta E2E
needs: validate_dispatch_ref
runs-on: ubuntu-latest
environment: npm-release
steps:
- name: Record approval
env:
PACKAGE_SPEC: ${{ inputs.package_spec }}
run: echo "Approved npm Telegram beta E2E for ${PACKAGE_SPEC}"
prepare_docker_e2e_image:
name: Prepare Docker E2E image
needs: validate_dispatch_ref
runs-on: blacksmith-32vcpu-ubuntu-2404
timeout-minutes: 90
permissions:
contents: read
packages: write
outputs:
image: ${{ steps.image.outputs.image }}
env:
DOCKER_BUILD_SUMMARY: "false"
DOCKER_BUILD_RECORD_UPLOAD: "false"
steps:
- name: Checkout main
uses: actions/checkout@v6
with:
ref: ${{ github.sha }}
fetch-depth: 1
- name: Resolve Docker E2E image tag
id: image
shell: bash
env:
SELECTED_SHA: ${{ github.sha }}
run: |
set -euo pipefail
repository="${GITHUB_REPOSITORY,,}"
image="ghcr.io/${repository}-docker-e2e:${SELECTED_SHA}"
echo "image=$image" >> "$GITHUB_OUTPUT"
echo "Docker E2E image: \`$image\`" >> "$GITHUB_STEP_SUMMARY"
- name: Set up Blacksmith Docker Builder
uses: useblacksmith/setup-docker-builder@ac083cc84672d01c60d5e8561d0a939b697de542 # v1
- name: Log in to GHCR
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ github.token }}
- name: Build and push Docker E2E image
uses: useblacksmith/build-push-action@cbd1f60d194a98cb3be5523b15134501eaf0fbf3 # v2
with:
context: .
file: ./scripts/e2e/Dockerfile
target: build
platforms: linux/amd64
tags: ${{ steps.image.outputs.image }}
provenance: false
push: true
run_npm_telegram_beta_e2e:
name: Run published npm Telegram E2E
needs: [approve_release_manager, prepare_docker_e2e_image]
runs-on: blacksmith-32vcpu-ubuntu-2404
timeout-minutes: 60
environment: qa-live-shared
permissions:
contents: read
packages: read
steps:
- name: Checkout main
uses: actions/checkout@v6
with:
ref: ${{ github.sha }}
fetch-depth: 1
- name: Log in to GHCR
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ github.token }}
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: "true"
- name: Validate inputs and secrets
env:
PACKAGE_SPEC: ${{ inputs.package_spec }}
PROVIDER_MODE: ${{ inputs.provider_mode }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENCLAW_QA_CONVEX_SITE_URL: ${{ secrets.OPENCLAW_QA_CONVEX_SITE_URL }}
OPENCLAW_QA_CONVEX_SECRET_CI: ${{ secrets.OPENCLAW_QA_CONVEX_SECRET_CI }}
shell: bash
run: |
set -euo pipefail
if [[ ! "${PACKAGE_SPEC}" =~ ^openclaw@(beta|latest|[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*(-[1-9][0-9]*|-beta\.[1-9][0-9]*)?)$ ]]; then
echo "package_spec must be openclaw@beta, openclaw@latest, or an exact OpenClaw release version; got: ${PACKAGE_SPEC}" >&2
exit 1
fi
require_var() {
local key="$1"
if [[ -z "${!key:-}" ]]; then
echo "Missing required ${key}." >&2
exit 1
fi
}
require_var OPENCLAW_QA_CONVEX_SITE_URL
require_var OPENCLAW_QA_CONVEX_SECRET_CI
if [[ "${PROVIDER_MODE}" == "live-frontier" ]]; then
require_var OPENAI_API_KEY
fi
- name: Run npm Telegram beta E2E
id: run_lane
shell: bash
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENCLAW_SKIP_DOCKER_BUILD: "1"
OPENCLAW_DOCKER_E2E_IMAGE: ${{ needs.prepare_docker_e2e_image.outputs.image }}
OPENCLAW_NPM_TELEGRAM_PACKAGE_SPEC: ${{ inputs.package_spec }}
OPENCLAW_NPM_TELEGRAM_PROVIDER_MODE: ${{ inputs.provider_mode }}
OPENCLAW_NPM_TELEGRAM_CREDENTIAL_SOURCE: convex
OPENCLAW_NPM_TELEGRAM_CREDENTIAL_ROLE: ci
OPENCLAW_QA_CONVEX_SITE_URL: ${{ secrets.OPENCLAW_QA_CONVEX_SITE_URL }}
OPENCLAW_QA_CONVEX_SECRET_CI: ${{ secrets.OPENCLAW_QA_CONVEX_SECRET_CI }}
OPENCLAW_QA_REDACT_PUBLIC_METADATA: "1"
INPUT_SCENARIO: ${{ inputs.scenario }}
run: |
set -euo pipefail
output_dir=".artifacts/qa-e2e/npm-telegram-beta-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
echo "output_dir=${output_dir}" >> "$GITHUB_OUTPUT"
export OPENCLAW_NPM_TELEGRAM_OUTPUT_DIR="${output_dir}"
if [[ -n "${INPUT_SCENARIO// }" ]]; then
export OPENCLAW_NPM_TELEGRAM_SCENARIOS="${INPUT_SCENARIO}"
fi
pnpm test:docker:npm-telegram-live
- name: Upload npm Telegram E2E artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: npm-telegram-beta-e2e-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ steps.run_lane.outputs.output_dir }}
retention-days: 14
if-no-files-found: warn

View File

@@ -432,35 +432,24 @@ jobs:
OPENCLAW_DISCORD_SMOKE_CHANNEL_ID: ${{ secrets.OPENCLAW_DISCORD_SMOKE_CHANNEL_ID }}
OPENCLAW_RELEASE_CHECK_OS: ${{ matrix.os_id }}
OPENCLAW_RELEASE_CHECK_RUNNER: ${{ matrix.runner }}
CANDIDATE_TGZ: ${{ runner.temp }}/openclaw-cross-os-release-checks/candidate/${{ needs.prepare.outputs.candidate_file_name }}
CANDIDATE_VERSION: ${{ needs.prepare.outputs.candidate_version }}
SOURCE_SHA: ${{ needs.prepare.outputs.source_sha }}
BASELINE_SPEC: ${{ needs.prepare.outputs.baseline_spec }}
PREVIOUS_VERSION: ${{ inputs.previous_version }}
BASELINE_TGZ: ${{ runner.temp }}/openclaw-cross-os-release-checks/baseline/${{ needs.prepare.outputs.baseline_file_name }}
PROVIDER: ${{ inputs.provider }}
MODE: ${{ matrix.lane }}
SUITE: ${{ matrix.suite }}
REF: ${{ inputs.ref }}
OUTPUT_DIR: ${{ runner.temp }}/openclaw-cross-os-release-checks/${{ matrix.artifact_name }}-${{ matrix.suite }}
run: |
DISCORD_ARGS=()
if [[ -n "${OPENCLAW_DISCORD_SMOKE_BOT_TOKEN}" ]] && [[ -n "${OPENCLAW_DISCORD_SMOKE_GUILD_ID}" ]] && [[ -n "${OPENCLAW_DISCORD_SMOKE_CHANNEL_ID}" ]]; then
DISCORD_ARGS+=(--run-discord-roundtrip true)
fi
pnpm dlx "tsx@${TSX_VERSION}" workflow/scripts/openclaw-cross-os-release-checks.ts \
--candidate-tgz "${CANDIDATE_TGZ}" \
--candidate-version "${CANDIDATE_VERSION}" \
--source-sha "${SOURCE_SHA}" \
--baseline-spec "${BASELINE_SPEC}" \
--previous-version "${PREVIOUS_VERSION}" \
--baseline-tgz "${BASELINE_TGZ}" \
--provider "${PROVIDER}" \
--mode "${MODE}" \
--suite "${SUITE}" \
--ref "${REF}" \
--candidate-tgz "$RUNNER_TEMP/openclaw-cross-os-release-checks/candidate/${{ needs.prepare.outputs.candidate_file_name }}" \
--candidate-version "${{ needs.prepare.outputs.candidate_version }}" \
--source-sha "${{ needs.prepare.outputs.source_sha }}" \
--baseline-spec "${{ needs.prepare.outputs.baseline_spec }}" \
--previous-version "${{ inputs.previous_version }}" \
--baseline-tgz "$RUNNER_TEMP/openclaw-cross-os-release-checks/baseline/${{ needs.prepare.outputs.baseline_file_name }}" \
--provider "${{ inputs.provider }}" \
--mode "${{ matrix.lane }}" \
--suite "${{ matrix.suite }}" \
--ref "${{ inputs.ref }}" \
"${DISCORD_ARGS[@]}" \
--output-dir "${OUTPUT_DIR}"
--output-dir "$RUNNER_TEMP/openclaw-cross-os-release-checks/${{ matrix.artifact_name }}-${{ matrix.suite }}"
- name: Summarize release checks
if: always()

View File

@@ -623,9 +623,6 @@ jobs:
username: ${{ github.actor }}
password: ${{ github.token }}
- name: Setup Docker builder
uses: useblacksmith/setup-docker-builder@ac083cc84672d01c60d5e8561d0a939b697de542 # v1
- name: Build and push shared Docker E2E image
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with:

View File

@@ -34,7 +34,6 @@ env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
NODE_VERSION: "24.x"
PNPM_VERSION: "10.33.0"
OPENCLAW_CI_OPENAI_MODEL: ${{ vars.OPENCLAW_CI_OPENAI_MODEL }}
jobs:
resolve_target:
@@ -246,13 +245,13 @@ jobs:
- name: Build private QA runtime
run: pnpm build
- name: Run OpenAI candidate lane
- name: Run GPT-5.4 lane
run: |
pnpm openclaw qa suite \
--provider-mode mock-openai \
--parity-pack agentic \
--concurrency "${QA_PARITY_CONCURRENCY}" \
--model "${OPENCLAW_CI_OPENAI_MODEL}" \
--model openai/gpt-5.4 \
--alt-model openai/gpt-5.4-alt \
--output-dir .artifacts/qa-e2e/gpt54
@@ -272,7 +271,7 @@ jobs:
--repo-root . \
--candidate-summary .artifacts/qa-e2e/gpt54/qa-suite-summary.json \
--baseline-summary .artifacts/qa-e2e/opus46/qa-suite-summary.json \
--candidate-label "${OPENCLAW_CI_OPENAI_MODEL}" \
--candidate-label openai/gpt-5.4 \
--baseline-label anthropic/claude-opus-4-6 \
--output-dir .artifacts/qa-e2e/parity
@@ -342,8 +341,8 @@ jobs:
--repo-root . \
--output-dir "${output_dir}" \
--provider-mode live-frontier \
--model "${OPENCLAW_CI_OPENAI_MODEL}" \
--alt-model "${OPENCLAW_CI_OPENAI_MODEL}" \
--model openai/gpt-5.4 \
--alt-model openai/gpt-5.4 \
--fast
- name: Upload Matrix QA artifacts
@@ -424,8 +423,8 @@ jobs:
--repo-root . \
--output-dir "${output_dir}" \
--provider-mode live-frontier \
--model "${OPENCLAW_CI_OPENAI_MODEL}" \
--alt-model "${OPENCLAW_CI_OPENAI_MODEL}" \
--model openai/gpt-5.4 \
--alt-model openai/gpt-5.4 \
--fast \
--credential-source convex \
--credential-role ci

View File

@@ -24,7 +24,7 @@ concurrency:
jobs:
parity-gate:
name: Run the OpenAI / Opus 4.6 parity gate against the qa-lab mock
name: Run the GPT-5.4 / Opus 4.6 parity gate against the qa-lab mock
if: ${{ github.event.pull_request.draft != true }}
runs-on: blacksmith-32vcpu-ubuntu-2404
timeout-minutes: 30
@@ -42,7 +42,6 @@ jobs:
# followthrough gate that expects a fast post-approval read within a 30s
# agent.wait timeout.
QA_PARITY_CONCURRENCY: "1"
OPENCLAW_CI_OPENAI_MODEL: ${{ vars.OPENCLAW_CI_OPENAI_MODEL }}
OPENCLAW_QA_TRANSPORT_READY_TIMEOUT_MS: "180000"
OPENAI_API_KEY: ""
ANTHROPIC_API_KEY: ""
@@ -76,13 +75,13 @@ jobs:
# The approval-turn sentinel still runs inside the full parity pack below.
# Keep the exact mock read-plan contract in deterministic unit tests instead
# of paying for a separate full-runtime preflight that has been flaky in CI.
- name: Run OpenAI candidate lane
- name: Run GPT-5.4 lane
run: |
pnpm openclaw qa suite \
--provider-mode mock-openai \
--parity-pack agentic \
--concurrency "${QA_PARITY_CONCURRENCY}" \
--model "${OPENCLAW_CI_OPENAI_MODEL}" \
--model openai/gpt-5.4 \
--alt-model openai/gpt-5.4-alt \
--output-dir .artifacts/qa-e2e/gpt54
@@ -102,7 +101,7 @@ jobs:
--repo-root . \
--candidate-summary .artifacts/qa-e2e/gpt54/qa-suite-summary.json \
--baseline-summary .artifacts/qa-e2e/opus46/qa-suite-summary.json \
--candidate-label "${OPENCLAW_CI_OPENAI_MODEL}" \
--candidate-label openai/gpt-5.4 \
--baseline-label anthropic/claude-opus-4-6 \
--output-dir .artifacts/qa-e2e/parity

View File

@@ -31,7 +31,6 @@ env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
NODE_VERSION: "24.x"
PNPM_VERSION: "10.33.0"
OPENCLAW_CI_OPENAI_MODEL: ${{ vars.OPENCLAW_CI_OPENAI_MODEL }}
OPENCLAW_BUILD_PRIVATE_QA: "1"
OPENCLAW_ENABLE_PRIVATE_QA_CLI: "1"
@@ -157,13 +156,13 @@ jobs:
- name: Build private QA runtime
run: pnpm build
- name: Run OpenAI candidate lane
- name: Run GPT-5.4 lane
run: |
pnpm openclaw qa suite \
--provider-mode mock-openai \
--parity-pack agentic \
--concurrency "${QA_PARITY_CONCURRENCY}" \
--model "${OPENCLAW_CI_OPENAI_MODEL}" \
--model openai/gpt-5.4 \
--alt-model openai/gpt-5.4-alt \
--output-dir .artifacts/qa-e2e/gpt54
@@ -183,7 +182,7 @@ jobs:
--repo-root . \
--candidate-summary .artifacts/qa-e2e/gpt54/qa-suite-summary.json \
--baseline-summary .artifacts/qa-e2e/opus46/qa-suite-summary.json \
--candidate-label "${OPENCLAW_CI_OPENAI_MODEL}" \
--candidate-label openai/gpt-5.4 \
--baseline-label anthropic/claude-opus-4-6 \
--output-dir .artifacts/qa-e2e/parity
@@ -247,8 +246,8 @@ jobs:
--repo-root . \
--output-dir "${output_dir}" \
--provider-mode live-frontier \
--model "${OPENCLAW_CI_OPENAI_MODEL}" \
--alt-model "${OPENCLAW_CI_OPENAI_MODEL}" \
--model openai/gpt-5.4 \
--alt-model openai/gpt-5.4 \
--fast
- name: Upload Matrix QA artifacts
@@ -336,8 +335,8 @@ jobs:
--repo-root . \
--output-dir "${output_dir}" \
--provider-mode live-frontier \
--model "${OPENCLAW_CI_OPENAI_MODEL}" \
--alt-model "${OPENCLAW_CI_OPENAI_MODEL}" \
--model openai/gpt-5.4 \
--alt-model openai/gpt-5.4 \
--fast \
--credential-source convex \
--credential-role ci \

View File

@@ -133,7 +133,7 @@ jobs:
with:
openai-api-key: ${{ secrets.OPENCLAW_TEST_PERF_AGENT_OPENAI_API_KEY || secrets.OPENAI_API_KEY }}
prompt-file: .github/codex/prompts/test-performance-agent.md
model: ${{ vars.OPENCLAW_CI_OPENAI_MODEL_BARE }}
model: gpt-5.4
effort: high
sandbox: workspace-write
safety-strategy: drop-sudo

283
AGENTS.md
View File

@@ -1,165 +1,214 @@
# AGENTS.MD
Telegraph style. Root rules only. Read scoped `AGENTS.md` before subtree work.
Telegraph style. Root rules only. Read scoped `AGENTS.md` before touching a subtree.
## Start
- Repo: `https://github.com/openclaw/openclaw`
- Replies: repo-root refs only: `extensions/telegram/src/index.ts:80`. No absolute paths, no `~/`.
- Run docs list first: `pnpm docs:list` if available; read relevant docs only.
- High-confidence answers only when fixing/triaging: verify source, tests, shipped/current behavior, and dependency contracts before deciding.
- Dependency-backed behavior: read upstream dependency docs/source/types first. Do not assume APIs, defaults, errors, timing, or runtime behavior.
- Missing deps: `pnpm install`, retry once, then report first actionable error.
- CODEOWNERS: maint/refactor/tests ok. Larger behavior/product/security/ownership: owner ask/review.
- Wording: product/docs/UI/changelog say "plugin/plugins"; `extensions/` is internal.
- New channel/plugin/app/doc surface: update `.github/labeler.yml` + GH labels.
- New `AGENTS.md`: add sibling `CLAUDE.md` symlink.
- Replies: repo-root file refs only, e.g. `extensions/telegram/src/index.ts:80`. No absolute paths, no `~/`.
- CODEOWNERS: maintenance/refactors/tests are ok. For larger behavior, product, security, or ownership-sensitive changes, get a listed owner request/review first.
- First pass: run docs list (`pnpm docs:list`; ignore if unavailable), then read only relevant docs/guides.
- Missing deps: run `pnpm install`, rerun once, then report first actionable error.
- Use "plugin/plugins" in docs/UI/changelog. `extensions/` remains internal workspace layout.
- Add channel/plugin/app/doc surface: update `.github/labeler.yml` and matching GitHub labels.
- New `AGENTS.md`: add sibling `CLAUDE.md` symlink to it.
## Map
## Repo Map
- Core TS: `src/`, `ui/`, `packages/`; plugins: `extensions/`; SDK: `src/plugin-sdk/*`; channels: `src/channels/*`; loader: `src/plugins/*`; protocol: `src/gateway/protocol/*`; docs/apps: `docs/`, `apps/`, `Swabble/`.
- Installers: sibling `../openclaw.ai`.
- Scoped guides exist in: `extensions/`, `src/{plugin-sdk,channels,plugins,gateway,gateway/protocol,agents}/`, `test/helpers*/`, `docs/`, `ui/`, `scripts/`.
- Core TS: `src/`, `ui/`, `packages/`
- Bundled plugins: `extensions/`
- Plugin SDK/public contract: `src/plugin-sdk/*`
- Core channel internals: `src/channels/*`
- Plugin loader/registry/contracts: `src/plugins/*`
- Gateway protocol: `src/gateway/protocol/*`
- Docs: `docs/`
- Apps: `apps/`, `Swabble/`
- Installers served from `openclaw.ai`: sibling `../openclaw.ai`
Scoped guides:
- `extensions/AGENTS.md`: bundled plugin rules
- `src/plugin-sdk/AGENTS.md`: public SDK rules
- `src/channels/AGENTS.md`: channel core rules
- `src/plugins/AGENTS.md`: plugin loader/registry rules
- `src/gateway/AGENTS.md`, `src/gateway/protocol/AGENTS.md`: gateway/protocol rules
- `src/agents/AGENTS.md`: agent import/test perf rules
- `test/helpers/AGENTS.md`, `test/helpers/channels/AGENTS.md`: shared test helpers
- `docs/AGENTS.md`, `ui/AGENTS.md`, `scripts/AGENTS.md`: docs/UI/scripts
## Architecture
- Core stays extension-agnostic. No bundled ids in core when manifest/registry/capability contracts work.
- Extensions cross into core only via `openclaw/plugin-sdk/*`, manifest metadata, injected runtime helpers, documented barrels (`api.ts`, `runtime-api.ts`).
- Extension prod code: no core `src/**`, `src/plugin-sdk-internal/**`, other extension `src/**`, or relative outside package.
- Core/tests: no deep plugin internals (`extensions/*/src/**`, `onboard.js`). Use `api.ts`, SDK facade, generic contracts.
- Extension-owned behavior stays extension-owned: repair, detection, onboarding, auth/provider defaults, provider tools/settings.
- Legacy config repair: doctor/fix paths, not startup/load-time core migrations.
- Core test asserting extension-specific behavior: move to owner extension or generic contract test.
- Core must stay extension-agnostic. No core special cases for bundled plugin/provider/channel ids when manifest/registry/capability contracts can express it.
- Extensions cross into core only via `openclaw/plugin-sdk/*`, manifest metadata, injected runtime helpers, and documented local barrels (`api.ts`, `runtime-api.ts`).
- Extension production code must not import core `src/**`, `src/plugin-sdk-internal/**`, another extension's `src/**`, or relative paths outside its package.
- Core code/tests must not deep-import plugin internals (`extensions/*/src/**`, `onboard.js`). Use plugin `api.ts` / public SDK facade / generic contract.
- Extension-owned behavior stays in the extension: legacy repair, detection, onboarding, auth/provider defaults, provider tools/settings.
- Legacy config repair: prefer doctor/fix paths over startup/load-time core migrations.
- If a core test asserts extension-specific behavior, move it to the owning extension or a generic contract test.
- New seams: backwards-compatible, documented, versioned. Third-party plugins exist.
- Channels: `src/channels/**` is implementation; plugin authors get SDK seams.
- Providers: core owns generic loop; provider plugins own auth/catalog/runtime hooks.
- Gateway protocol changes: additive first; incompatible needs versioning/docs/client follow-through.
- Config contract: exported types, schema/help, metadata, baselines, docs aligned. Retired public keys stay retired; compat in raw migration/doctor.
- Direction: manifest-first control plane; targeted runtime loaders; no hidden contract bypasses; broad mutable registries transitional.
- Prompt cache: deterministic ordering for maps/sets/registries/plugin lists/files/network results before model/tool payloads. Preserve old transcript bytes when possible.
- Channels: `src/channels/**` is implementation. Plugin authors get SDK seams, not channel internals.
- Providers: core owns generic inference loop; provider plugins own provider-specific auth/catalog/runtime hooks.
- Gateway protocol changes are contract changes: additive first; incompatible needs versioning/docs/client follow-through.
- Config contract: keep exported types, schema/help, generated metadata, baselines, docs aligned. Retired public keys stay retired; compatibility belongs in raw migration/doctor paths.
- Plugin architecture direction: manifest-first control plane; targeted runtime loaders; no hidden paths around declared contracts; broad mutable registries are transitional.
- Prompt-cache rule: deterministic ordering for maps/sets/registries/plugin lists/files/network results before model/tool payloads. Preserve old transcript bytes when possible.
## Commands
- Runtime: Node 22+. Keep Node + Bun paths working.
- Install: `pnpm install` (keep Bun lock/patches aligned if touched).
- CLI: `pnpm openclaw ...` or `pnpm dev`; build: `pnpm build`.
- Smart gate: `pnpm check:changed`; explain `pnpm changed:lanes --json`; staged preview `pnpm check:changed --staged`.
- Prod sweep: `pnpm check`; tests: `pnpm test`, `pnpm test:changed`, `pnpm test:serial`, `pnpm test:coverage`.
- Extension tests: `pnpm test:extensions`, `pnpm test extensions`, `pnpm test extensions/<id>`.
- Targeted tests: `pnpm test <path-or-filter> [vitest args...]`; never raw `vitest`.
- Typecheck: `tsgo` lanes only (`pnpm tsgo*`, `pnpm check:test-types`); do not add `tsc --noEmit`, `typecheck`, `check:types`.
- Format/lint: `pnpm format:check`/`pnpm format`; `pnpm lint*` lanes.
- Heavy checks: `OPENCLAW_LOCAL_CHECK=1`, mode `OPENCLAW_LOCAL_CHECK_MODE=throttled|full`; CI/shared use `OPENCLAW_LOCAL_CHECK=0`.
- Local first. Use repo `pnpm` lanes before Blacksmith/Testbox. Remote only for parity-only failures, secrets/services, or explicit ask.
- Runtime: Node 22+. Keep Node and Bun paths working.
- Install: `pnpm install` (Bun supported; keep lockfiles/patches aligned if touched).
- Dev CLI: `pnpm openclaw ...` or `pnpm dev`.
- Build: `pnpm build`
- Smart local gate: `pnpm check:changed` (scoped typecheck/lint/guards + relevant tests)
- Explain smart gate: `pnpm changed:lanes --json`
- Staged gate preview: `pnpm check:changed --staged`
- Normal full prod sweep: `pnpm check` (prod typecheck/lint/guards, no tests)
- Full tests: `pnpm test`
- Changed tests only: `pnpm test:changed`
- Local serial loop: `pnpm test:serial`
- Extension tests: `pnpm test:extensions` or `pnpm test extensions` = all extension shards; `pnpm test extensions/<id>` = one extension lane. Heavy channels/OpenAI have dedicated shards.
- Shard timing artifact: `.artifacts/vitest-shard-timings.json`; auto-used for balanced shard ordering. Disable with `OPENCLAW_TEST_PROJECTS_TIMINGS=0`.
- Targeted tests: `pnpm test <path-or-filter> [vitest args...]`; do not call raw `vitest`.
- Coverage: `pnpm test:coverage`
- Format check/fix: `pnpm format:check` / `pnpm format`
- Typecheck:
- `pnpm tsgo`: fastest core prod graph
- `pnpm tsgo:prod`: core + extensions prod graphs; used by `pnpm check`
- `pnpm check:test-types` / `pnpm tsgo:test`: all test graphs
- `pnpm tsgo:all`: all prod + test project refs
- Debug slices exist; do not present as normal user flow.
- Profile: `pnpm tsgo:profile [core-test|extensions-test|--all]`
- Type policy: use `tsgo`; do not add `tsc --noEmit`, `typecheck`, or `check:types` lanes. `tsc` only for declaration/package-boundary emit gaps.
- Lint:
- `pnpm lint`: core/extensions/scripts shards
- `pnpm lint:core`, `pnpm lint:extensions`, `pnpm lint:scripts`
- `pnpm lint:apps`: Swift/app surface, separate from TS lint
- `pnpm lint:all`: legacy comparison lane
- Local heavy-check behavior: `OPENCLAW_LOCAL_CHECK=1` default; `OPENCLAW_LOCAL_CHECK_MODE=throttled|full`; `OPENCLAW_LOCAL_CHECK=0` for CI/shared runs.
- Local validation is local-first. Do not default to Blacksmith/Testbox for routine OpenClaw iteration; it burns warm caches and startup time. Use repo `pnpm` lanes first, then reach for remote CI/Testbox only for parity-only failures, secrets/services, or when explicitly requested.
## GitHub / CI
## GitHub API
- Triage: list first, hydrate few. Use bounded `gh --json --jq`; avoid repeated full comment scans.
- Search/dedupe: prefer `gh search issues 'repo:openclaw/openclaw is:open <terms>' --json number,title,state,updatedAt --limit 20`.
- PR shortlist: `gh pr list ...`; then `gh pr view <n> --json number,title,body,closingIssuesReferences,files,statusCheckRollup,reviewDecision`.
- After landing PR: search duplicate open issues/PRs. Before closing: comment why + canonical link.
- GH comments with markdown backticks, `$`, or shell snippets: avoid inline double-quoted `--body`; use single quotes or `--body-file`.
- PR review answer must explicitly cover: what bug/behavior we are trying to fix; PR/issue URL(s) and affected endpoint/surface; whether this is the best possible fix, with high-certainty evidence from code, tests, CI, and shipped/current behavior.
- CI polling: exact SHA, needed fields only. Example: `gh api repos/<owner>/<repo>/actions/runs/<id> --jq '{status,conclusion,head_sha,updated_at,name,path}'`.
- Post-land wait: minimal. Exact landed SHA only. If superseded on `main`, same-branch `cancel-in-progress` cancellations are expected; stop once local touched-surface proof exists. Never wait for newer unrelated `main` unless asked.
- Wait matrix:
- never: `Auto response`, `Labeler`, `Docs Sync Publish Repo`, `Docs Agent`, `Test Performance Agent`, `Stale`.
- conditional: `CI` exact SHA only; `Docs` only docs task/no local docs proof; `Workflow Sanity` only workflow/composite/CI-policy edits; `Plugin NPM Release` only plugin package/release metadata.
- release/manual only: `Docker Release`, `OpenClaw NPM Release`, `macOS Release`, `OpenClaw Release Checks`, `Cross-OS Release Checks`, `NPM Telegram Beta E2E`.
- explicit/surface only: `QA-Lab - All Lanes`, `Scheduled Live And E2E`, `Install Smoke`, `CodeQL`, `Sandbox Common Smoke`, `Parity gate`, `Blacksmith Testbox`, `Control UI Locale Refresh`.
- `/landpr`: do not idle on `auto-response` or `check-docs`. Treat docs as local proof unless `check-docs` already failed with actionable relevant error.
- Poll 30-60s. Fetch jobs/logs/artifacts only after failure/completion or concrete need.
- Issue/PR triage: list first, hydrate few. Use bounded fields + `--jq`, e.g. `gh issue list --state open --limit 80 --json number,title,labels,updatedAt,comments --jq '.[]|[.number,.updatedAt,.comments,.title]|@tsv'`; then `gh issue view <n> --json title,body,comments,labels,createdAt,updatedAt,url --jq '{title,labels,createdAt,updatedAt,url,body,comments:[.comments[]|{author:.author.login,createdAt,body}]}'` only for shortlisted items.
- Search/dedupe: prefer `gh search issues 'repo:openclaw/openclaw is:open <terms>' --json number,title,state,updatedAt --limit 20 --jq '.[]|[.number,.updatedAt,.title]|@tsv'`; avoid repeated full `--comments` scans.
- After landing a PR: search for duplicate open issues/PRs that can be closed.
- Before closing an issue/PR: add a comment explaining why, usually duplicate/invalid, with the canonical issue/PR when relevant.
- PR links: `gh pr list --state open --search '<issue-or-terms>' --json number,title,updatedAt,headRefName --limit 20`; use `gh pr view <n> --json number,title,body,closingIssuesReferences,files,statusCheckRollup,reviewDecision` only after shortlist.
- CI polling: keep full `gh` capability, but request only needed fields. Known run status: `gh api repos/<owner>/<repo>/actions/runs/<id> --jq '{status,conclusion,head_sha,updated_at,name,path}'`.
- Non-blocking background workflows: `Auto response`, `Docs Sync Publish Repo`, `Docs Agent`, and `Test Performance Agent` are service/agent work. Do not wait on, rerun, or fix them during normal push/PR verification unless the user explicitly asks or the task is about those workflows. Report them as background if mentioned.
- `/landpr` CI wait scope: do not idle on pending `auto-response`/`Auto response` or `check-docs`. Treat docs as local proof unless `check-docs` already failed with a relevant, actionable error. If required product/code gates and touched-surface local gates are green, proceed without waiting for docs-only or auto-response automation.
- Waiting: poll lightly, usually 30-60s backoff. Fetch jobs/logs/artifacts only after completion/failure or when job detail is needed; avoid repeated workflow + run + jobs loops.
## Gates
- Pre-commit hook: staged formatting only. Validation explicit.
- Pre-commit hook: staged formatting only. It does not run lint, typecheck, or tests.
- Changed lanes:
- core prod: core prod typecheck + core tests
- core tests: core test typecheck/tests
- extension prod: extension prod typecheck + extension tests
- extension tests: extension test typecheck/tests
- public SDK/plugin contract: extension prod/test too
- unknown root/config: all lanes
- Before handoff/push: `pnpm check:changed`. Tests-only: `pnpm test:changed`. Full prod sweep: `pnpm check`.
- Landing on `main`: verify touched surface near landing. Default feasible bar: `pnpm check` + `pnpm test`.
- Hard build gate: `pnpm build` before push if build output, packaging, lazy/module boundaries, or published surfaces can change.
- Do not land related failing format/lint/type/build/tests. If unrelated on latest `origin/main`, say so with scoped proof.
- Generated/API drift: `pnpm check:architecture`, `pnpm config:docs:gen/check`, `pnpm plugin-sdk:api:gen/check`. Track `docs/.generated/*.sha256`; full JSON ignored.
- core prod => core prod typecheck + core tests
- core tests => core test typecheck/tests only
- extension prod => extension prod typecheck + extension tests
- extension tests => extension test typecheck/tests only
- public SDK/plugin contract => extension prod/test validation too
- unknown root/config => all lanes
- Local loop: run `pnpm check:changed` explicitly before handoff/push; use `pnpm test:changed` for tests only; use `pnpm check` for full prod TS/lint sweep without tests.
- Landing on `main`: verify touched surface near landing; default bar is `pnpm check` + `pnpm test` when feasible.
- Hard build gate: run/pass `pnpm build` before push if build output, packaging, lazy/module boundaries, or published surfaces can change.
- Do not land related failing format/lint/type/build/tests. If failures are unrelated on latest `origin/main`, say so and give scoped proof.
- Commit helper is formatting-only; validation gates are explicit commands, not commit side effects.
- CI architecture gate: `check-additional`; local equivalent `pnpm check:architecture`.
- Config docs drift: `pnpm config:docs:gen/check`
- Plugin SDK API drift: `pnpm plugin-sdk:api:gen/check`
- Generated docs baselines: tracked `docs/.generated/*.sha256`; full JSON ignored.
## Code
## Code Style
- TS ESM, strict. Avoid `any`; prefer real types, `unknown`, narrow adapters.
- No `@ts-nocheck`. Lint suppressions only intentional + explained.
- TypeScript ESM. Strict types. Avoid `any`; prefer real types/`unknown`/narrow adapters.
- No `@ts-nocheck`. No lint suppressions unless intentional and explained.
- External boundaries: prefer `zod` or existing schema helpers.
- Runtime branching: discriminated unions/closed codes over freeform strings.
- Avoid semantic sentinels: `?? 0`, empty object/string, etc.
- Dynamic import: no static+dynamic import for same prod module. Use `*.runtime.ts` lazy boundary. After edits: `pnpm build`; check `[INEFFECTIVE_DYNAMIC_IMPORT]`.
- Cycles: keep `pnpm check:import-cycles` + architecture/madge green.
- Classes: no prototype mixins/mutations. Prefer inheritance/composition. Tests prefer per-instance stubs.
- Comments: brief, only non-obvious logic.
- Split files around ~700 LOC when clarity/testability improves.
- Naming: **OpenClaw** product/docs; `openclaw` CLI/package/path/config.
- English: American spelling.
- Runtime branching: prefer discriminated unions / closed codes over freeform strings.
- Avoid magic sentinels like `?? 0`, empty object/string when semantics change.
- Dynamic import: do not mix static and dynamic import for same module in prod path. Use dedicated `*.runtime.ts` lazy boundary. After lazy-boundary edits, run `pnpm build` and check `[INEFFECTIVE_DYNAMIC_IMPORT]`.
- Cycles: keep `pnpm check:import-cycles` and architecture/madge cycle checks green.
- Classes: no prototype mixins/mutations. Use explicit inheritance/composition. Tests prefer per-instance stubs.
- Comments: brief only for non-obvious logic.
- File size: split around ~700 LOC when it improves clarity/testability.
- Product naming: **OpenClaw** product/docs; `openclaw` CLI/package/path/config.
- Written English: American spelling.
## Tests
- Vitest. Colocated `*.test.ts`; e2e `*.e2e.test.ts`; example models `sonnet-4.6`, `gpt-5.4`.
- Clean timers/env/globals/mocks/sockets/temp dirs/module state; `--isolate=false` safe.
- Hot tests: avoid per-test `vi.resetModules()` + heavy imports. Measure with `pnpm test:perf:imports <file>` / `pnpm test:perf:hotspots --limit N`.
- Seam depth: pure helper/contract unit tests; one integration smoke per boundary.
- Mock expensive seams directly: scanners, manifests, registries, fs crawls, provider SDKs, network/process launch.
- Prefer injection; if module mocking, mock narrow local `*.runtime.ts`, not broad barrels or `openclaw/plugin-sdk/*`.
- Share fixtures/builders; delete duplicate assertions; assert behavior that can regress here.
- Do not edit baseline/inventory/ignore/snapshot/expected-failure files to silence checks without explicit approval.
- Test workers max 16. Memory pressure: `OPENCLAW_VITEST_MAX_WORKERS=1 pnpm test`.
- Live: `OPENCLAW_LIVE_TEST=1 pnpm test:live`; verbose `OPENCLAW_LIVE_TEST_QUIET=0`.
- Guide: `docs/help/testing.md`.
- Vitest. Tests colocated `*.test.ts`; e2e `*.e2e.test.ts`.
- Example models in tests: `sonnet-4.6`, `gpt-5.4`.
- Clean up timers/env/globals/mocks/sockets/temp dirs/module state; `--isolate=false` must stay safe.
- Hot tests: avoid per-test `vi.resetModules()` + fresh heavy imports; prefer static or `beforeAll` imports and reset state directly.
- Measure first: `pnpm test:perf:imports <file>` for import drag; `pnpm test:perf:hotspots --limit N` for suite targets.
- Keep tests at seam depth: unit-test pure helpers/contracts; one integration smoke per boundary, not per branch.
- Mock expensive runtime seams directly: scanners, manifests, package registries, filesystem crawls, provider SDKs, network/process launch.
- Prefer injected deps over module mocks; if mocking modules, mock narrow local `*.runtime.ts` seams, not broad barrels.
- Share fixtures/builders; do not recreate temp dirs, package manifests, or plugin workspaces in every case unless state isolation needs it.
- Delete duplicate assertions when another test owns the boundary; assert only the behavior that can regress here.
- Avoid broad `importOriginal()` / broad `openclaw/plugin-sdk/*` partial mocks in hot tests. Add narrow local `*.runtime.ts` seam and mock it.
- Use existing deps/callback/runtime injection seams before module mocks.
- Import-dominated test time is a boundary smell; shrink import surface before adding cases.
- Replacing slow integration coverage: extract production composition into a named helper and test that helper.
- Do not modify baseline/inventory/ignore/snapshot/expected-failure files to silence checks without explicit approval.
- Do not set test workers above 16. For memory pressure: `OPENCLAW_VITEST_MAX_WORKERS=1 pnpm test`.
- Live: `OPENCLAW_LIVE_TEST=1 pnpm test:live`; full logs `OPENCLAW_LIVE_TEST_QUIET=0`.
- Full testing guide: `docs/help/testing.md`.
## Docs / Changelog
- Docs change with behavior/API. Use docs list/read_when hints; docs links per `docs/AGENTS.md`.
- Changelog user-facing only; pure test/internal usually no entry.
- Changelog placement: active version `### Changes`/`### Fixes`; at most one contributor mention, prefer `Thanks @user`.
- Update docs when behavior/API changes. Use docs list/read_when hints.
- Docs links: see `docs/AGENTS.md`.
- Changelog: user-facing only. Pure test/internal changes usually no entry.
- Changelog placement: append to active version `### Changes`/`### Fixes`; at most one contributor mention, prefer `Thanks @user`.
## Git
- Commit via `scripts/committer "<msg>" <file...>`; stage intended files only. It formats staged files; still run gates.
- Commits: conventional-ish, concise, grouped.
- No manual stash/autostash unless explicit. No branch/worktree changes unless requested.
- `main`: no merge commits; rebase on latest `origin/main` before push.
- User says `commit`: your changes only. `commit all`: all changes in grouped chunks. `push`: may `git pull --rebase` first.
- Do not delete/rename unexpected files; ask if blocking, else ignore.
- Bulk PR close/reopen >5: ask with count/scope.
- PR/issue workflows: `$openclaw-pr-maintainer`. `/landpr`: `~/.codex/prompts/landpr.md`.
- Use `scripts/committer "<msg>" <file...>`; stage only intended files. It formats staged files only; run validation separately.
- Commits: conventional-ish, concise/action-oriented. Group related changes.
- No manual stash/autostash unless explicitly requested. No branch/worktree changes unless requested.
- No merge commits on `main`; rebase on latest `origin/main` before push.
- User says "commit": commit your changes only. "commit all": commit everything in grouped chunks. "push": may `git pull --rebase` first.
- Do not delete/rename unexpected files; ask if it blocks. Otherwise ignore unrelated WIP.
- If bulk PR close/reopen affects >5 PRs, ask with exact count/scope.
- PR/issue workflows: use `$openclaw-pr-maintainer`.
- `/landpr`: use `~/.codex/prompts/landpr.md`.
## Security / Release
- Never commit real phone numbers, videos, credentials, live config.
- Secrets: channel/provider creds in `~/.openclaw/credentials/`; model auth profiles in `~/.openclaw/agents/<agentId>/agent/auth-profiles.json`.
- Secrets: channel/provider credentials under `~/.openclaw/credentials/`; model auth profiles under `~/.openclaw/agents/<agentId>/agent/auth-profiles.json`.
- Env keys: check `~/.profile`.
- Dependency patches/overrides/vendor changes need explicit approval. `pnpm.patchedDependencies` exact versions only.
- Carbon pins owner-only: do not change `@buape/carbon` unless Shadow (`@thewilloftheshadow`, verified by `gh`) asks.
- Releases/publish/version bumps need explicit approval. Release docs: `docs/reference/RELEASING.md`; use `$openclaw-release-maintainer`.
- GHSA/advisories: `$openclaw-ghsa-maintainer`.
- Beta tag/version match: `vYYYY.M.D-beta.N` -> npm `YYYY.M.D-beta.N --tag beta`.
- Dependency patches/overrides/vendor changes require explicit approval. `pnpm.patchedDependencies` must use exact versions.
- Carbon pins owner-only: do not change `@buape/carbon` versions unless Shadow (`@thewilloftheshadow`, verified by `gh`) asks.
- Releases/publish/version bumps require explicit approval.
- Release docs: `docs/reference/RELEASING.md`; use `$openclaw-release-maintainer`.
- GHSA/advisories: use `$openclaw-ghsa-maintainer`.
- Beta tag/version must match, e.g. `vYYYY.M.D-beta.N` => npm `YYYY.M.D-beta.N --tag beta`.
## Apps / Platform
- Before simulator/emulator testing, check real iOS/Android devices.
- Before simulator/emulator testing, check connected real iOS/Android devices first.
- "restart iOS/Android apps" = rebuild/reinstall/relaunch, not kill/launch.
- SwiftUI: Observation (`@Observable`, `@Bindable`) over new `ObservableObject`.
- Mac gateway: use app or `openclaw gateway restart/status --deep`; no ad-hoc tmux gateway. Logs: `./scripts/clawlog.sh`.
- Version bump touches: `package.json`, `apps/android/app/build.gradle.kts`, `apps/ios/version.json` + `pnpm ios:version:sync`, macOS `Info.plist`, `docs/install/updating.md`. Appcast only for Sparkle release.
- Mobile LAN pairing: plaintext `ws://` loopback-only. Private-network `ws://` needs `OPENCLAW_ALLOW_INSECURE_PRIVATE_WS=1`; Tailscale/public use `wss://` or tunnel.
- SwiftUI: prefer Observation (`@Observable`, `@Bindable`) over new `ObservableObject`.
- mac gateway: use app or `openclaw gateway restart/status --deep`; avoid ad-hoc tmux gateway sessions. Rebuild mac app locally, not over SSH.
- mac logs: `./scripts/clawlog.sh`.
- Version bump touches: `package.json`, `apps/android/app/build.gradle.kts`, `apps/ios/version.json` then `pnpm ios:version:sync`, `apps/macos/.../Info.plist`, `docs/install/updating.md`. Appcast only for Sparkle release.
- iOS Team ID: `security find-identity -p codesigning -v`; fallback `defaults read com.apple.dt.Xcode IDEProvisioningTeamIdentifiers`.
- Mobile LAN pairing: plaintext `ws://` is loopback-only by default. Trusted private-network `ws://` needs `OPENCLAW_ALLOW_INSECURE_PRIVATE_WS=1`; Tailscale/public use `wss://` or a tunnel.
- A2UI hash `src/canvas-host/a2ui/.bundle.hash`: generated; ignore unless running `pnpm canvas:a2ui:bundle`; commit separately.
## Ops / Footguns
## External Ops
- Remote install docs: `docs/install/exe-dev.md`, `docs/install/fly.md`, `docs/install/hetzner.md`.
- Parallels smoke: `$openclaw-parallels-smoke`; Discord roundtrip: `parallels-discord-roundtrip`.
## Misc Footguns
- Remote install docs: `docs/install/{exe-dev,fly,hetzner}.md`. Parallels smoke: `$openclaw-parallels-smoke`; Discord roundtrip: `parallels-discord-roundtrip`.
- Rebrand/migration/config warnings: run `openclaw doctor`.
- Never edit `node_modules`.
- Local-only `.agents` ignores: `.git/info/exclude`, not repo `.gitignore`.
- CLI progress: `src/cli/progress.ts`; status tables: `src/terminal/table.ts`.
- Local-only `.agents` ignores: use `.git/info/exclude`, not repo `.gitignore`.
- CLI progress: use `src/cli/progress.ts`; status tables: `src/terminal/table.ts`.
- Connection/provider additions: update all UI surfaces + docs + status/config forms.
- Provider tool schemas: prefer flat string enum helpers over `Type.Union([Type.Literal(...)])`; some providers reject `anyOf`. Not a repo-wide protocol/schema ban.
- External messaging: no token-delta channel messages. Follow `docs/concepts/streaming.md`; preview/block streaming uses edits/chunks and preserves final/fallback delivery.
- Provider-facing tool schemas: prefer flat string enum helpers over `Type.Union([Type.Literal(...)])`; some providers reject generated `anyOf`. Do not treat this as a repo-wide protocol/schema ban.
- External messaging surfaces: no token-delta channel messages. Follow `docs/concepts/streaming.md`; preview/block streaming uses message edits/chunks and must preserve final/fallback delivery.

File diff suppressed because it is too large Load Diff

View File

@@ -29,9 +29,9 @@ ARG OPENCLAW_NODE_BOOKWORM_SLIM_DIGEST="sha256:e8e2e91b1378f83c5b2dd15f0247f3411
FROM ${OPENCLAW_NODE_BOOKWORM_IMAGE} AS ext-deps
ARG OPENCLAW_EXTENSIONS
ARG OPENCLAW_BUNDLED_PLUGIN_DIR
COPY ${OPENCLAW_BUNDLED_PLUGIN_DIR} /tmp/${OPENCLAW_BUNDLED_PLUGIN_DIR}
# Copy package.json for opted-in extensions so pnpm resolves their deps.
RUN --mount=type=bind,source=${OPENCLAW_BUNDLED_PLUGIN_DIR},target=/tmp/${OPENCLAW_BUNDLED_PLUGIN_DIR},readonly \
mkdir -p /out && \
RUN mkdir -p /out && \
for ext in $OPENCLAW_EXTENSIONS; do \
if [ -f "/tmp/${OPENCLAW_BUNDLED_PLUGIN_DIR}/$ext/package.json" ]; then \
mkdir -p "/out/$ext" && \

View File

@@ -53,24 +53,12 @@ We prioritize secure defaults, but also expose clear knobs for trusted high-powe
OpenClaw has an extensive plugin API.
Core stays lean; optional capability should usually ship as plugins.
We are generally slimming down core while expanding what plugins can do.
If a useful feature cannot be built as a plugin yet, we welcome PRs and design discussions that extend the plugin API instead of adding one-off core behavior.
There are two broad plugin styles:
- Code plugins run OpenClaw plugin code and are appropriate for deeper runtime extension.
- Bundle-style plugins package stable external surfaces such as skills, MCP servers, and related configuration.
Prefer bundle-style plugins when they can express the capability.
They have a smaller, more stable interface and better security boundaries.
Use code plugins when the capability needs runtime hooks, providers, channels, tools, or other in-process extension points.
Preferred plugin path is npm package distribution plus local extension loading for development.
If you build a plugin, host and maintain it in your own repository.
The bar for adding optional plugins to core is intentionally high.
Plugin docs: [`docs/tools/plugin.md`](docs/tools/plugin.md)
Plugin discovery, official publisher status, provenance, and security review live in [ClawHub](https://clawhub.ai/).
OpenClaw docs should document core extension points; plugin promotion belongs in ClawHub, preferably under vetted org publishers for official plugins.
Community plugin listing + PR bar: https://docs.openclaw.ai/plugins/community
Memory is a special plugin slot where only one memory plugin can be active at a time.
Today we ship multiple memory options; over time we plan to converge on one recommended default path.
@@ -78,16 +66,21 @@ Today we ship multiple memory options; over time we plan to converge on one reco
### Skills
We still ship some bundled skills for baseline UX.
New skills should be published through [ClawHub](https://clawhub.ai/) first, not added to core by default.
Official or bundled promotion should require a clear product, security, or maintainer-ownership reason.
New skills should be published to ClawHub first (`clawhub.ai`), not added to core by default.
Core skill additions should be rare and require a strong product or security reason.
### MCP Support
OpenClaw supports MCP as both a server and a runtime integration surface.
MCP details live in [`docs/cli/mcp.md`](docs/cli/mcp.md).
OpenClaw supports MCP through `mcporter`: https://github.com/steipete/mcporter
The project goal is pragmatic MCP support without duplicating existing agent,
tool, ACPX, plugin, or ClawHub paths.
This keeps MCP integration flexible and decoupled from core runtime:
- add or change MCP servers without restarting the gateway
- keep core tool/context surface lean
- reduce MCP churn impact on core stability and security
For now, we prefer this bridge model over building first-class MCP runtime into core.
If there is an MCP server or feature `mcporter` does not support yet, please open an issue there.
### Setup
@@ -105,11 +98,11 @@ It is widely known, fast to iterate in, and easy to read, modify, and extend.
## What We Will Not Merge (For Now)
- New core skills when they can live on [ClawHub](https://clawhub.ai/)
- New core skills when they can live on ClawHub
- Full-doc translation sets for all docs (deferred; we plan AI-generated translations later)
- Commercial service integrations that do not clearly fit the model-provider category
- Wrapper channels around already supported channels without a clear capability or security gap
- MCP work that duplicates existing MCP, ACPX, plugin, or ClawHub paths without a clear product or security gap
- First-class MCP runtime in core when `mcporter` already provides the integration path
- Agent-hierarchy frameworks (manager-of-managers / nested planner trees) as a default architecture
- Heavy orchestration layers that duplicate existing agent and tool infrastructure

View File

@@ -1,24 +1,6 @@
import Foundation
import OpenClawProtocol
func whatsappLoginWaitRequestTimeoutMs(
startedAt: Date,
timeoutMs: Int,
didRunFinalWait: inout Bool,
now: Date = Date()) -> Int?
{
let elapsedMs = Int(now.timeIntervalSince(startedAt) * 1000)
let remainingMs = max(timeoutMs - elapsedMs, 0)
if remainingMs > 0 {
return remainingMs
}
if didRunFinalWait {
return nil
}
didRunFinalWait = true
return 1
}
extension ChannelsStore {
func start() {
guard !self.isPreview else { return }
@@ -95,28 +77,18 @@ extension ChannelsStore {
guard !self.whatsappBusy else { return }
self.whatsappBusy = true
defer { self.whatsappBusy = false }
let startedAt = Date()
var didRunFinalWait = false
do {
while let remainingMs = whatsappLoginWaitRequestTimeoutMs(
startedAt: startedAt,
timeoutMs: timeoutMs,
didRunFinalWait: &didRunFinalWait)
{
var params: [String: AnyCodable] = [
"timeoutMs": AnyCodable(remainingMs),
]
if let currentQrDataUrl = self.whatsappLoginQrDataUrl {
params["currentQrDataUrl"] = AnyCodable(currentQrDataUrl)
}
let result: WhatsAppLoginWaitResult = try await GatewayConnection.shared.requestDecoded(
method: .webLoginWait,
params: params,
timeoutMs: Double(remainingMs) + 5000)
self.applyWhatsAppLoginWaitResult(result)
if result.connected || result.qrDataUrl == nil || didRunFinalWait {
break
}
let params: [String: AnyCodable] = [
"timeoutMs": AnyCodable(timeoutMs),
]
let result: WhatsAppLoginWaitResult = try await GatewayConnection.shared.requestDecoded(
method: .webLoginWait,
params: params,
timeoutMs: Double(timeoutMs) + 5000)
self.whatsappLoginMessage = result.message
self.whatsappLoginConnected = result.connected
if result.connected {
self.whatsappLoginQrDataUrl = nil
}
} catch {
self.whatsappLoginMessage = error.localizedDescription
@@ -179,10 +151,9 @@ private struct WhatsAppLoginStartResult: Codable {
let connected: Bool?
}
struct WhatsAppLoginWaitResult: Codable {
private struct WhatsAppLoginWaitResult: Codable {
let connected: Bool
let message: String
let qrDataUrl: String?
}
private struct ChannelLogoutResult: Codable {

View File

@@ -290,16 +290,6 @@ final class ChannelsStore {
return self.snapshot?.channelOrder ?? []
}
func applyWhatsAppLoginWaitResult(_ result: WhatsAppLoginWaitResult) {
self.whatsappLoginMessage = result.message
self.whatsappLoginConnected = result.connected
if let qrDataUrl = result.qrDataUrl {
self.whatsappLoginQrDataUrl = qrDataUrl
} else if result.connected {
self.whatsappLoginQrDataUrl = nil
}
}
init(isPreview: Bool = ProcessInfo.processInfo.isPreview) {
self.isPreview = isPreview
}

View File

@@ -9,14 +9,8 @@ enum ExecAllowlistMatcher {
for entry in entries {
switch ExecApprovalHelpers.validateAllowlistPattern(entry.pattern) {
case let .valid(pattern):
if ExecApprovalHelpers.patternHasPathSelector(pattern) {
let target = resolvedPath ?? rawExecutable
if self.matches(pattern: pattern, target: target) { return entry }
} else if pattern != "*",
!ExecApprovalHelpers.patternHasPathSelector(rawExecutable),
self.matchesExecutableBasename(pattern: pattern, resolution: resolution) {
return entry
}
let target = resolvedPath ?? rawExecutable
if self.matches(pattern: pattern, target: target) { return entry }
case .invalid:
continue
}
@@ -40,20 +34,6 @@ enum ExecAllowlistMatcher {
return matches
}
private static func matchesExecutableBasename(
pattern: String,
resolution: ExecCommandResolution) -> Bool
{
var candidates = Set<String>()
if !resolution.executableName.isEmpty {
candidates.insert(resolution.executableName)
}
if let resolvedPath = resolution.resolvedPath, !resolvedPath.isEmpty {
candidates.insert(URL(fileURLWithPath: resolvedPath).lastPathComponent)
}
return candidates.contains { self.matches(pattern: pattern, target: $0) }
}
private static func matches(pattern: String, target: String) -> Bool {
let trimmed = pattern.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return false }

View File

@@ -616,17 +616,6 @@ enum ExecApprovalsStore {
let trimmedResolved = entry.lastResolvedPath?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
let normalizedResolved = trimmedResolved.isEmpty ? nil : trimmedResolved
if !ExecApprovalHelpers.patternHasPathSelector(trimmedPattern),
!trimmedResolved.isEmpty,
case let .valid(migratedPattern) = ExecApprovalHelpers.validateAllowlistPattern(trimmedResolved) {
return ExecAllowlistEntry(
id: entry.id,
pattern: migratedPattern,
lastUsedAt: entry.lastUsedAt,
lastUsedCommand: entry.lastUsedCommand,
lastResolvedPath: normalizedResolved)
}
switch ExecApprovalHelpers.validateAllowlistPattern(trimmedPattern) {
case let .valid(pattern):
return ExecAllowlistEntry(
@@ -735,10 +724,11 @@ enum ExecApprovalHelpers {
static func validateAllowlistPattern(_ pattern: String?) -> ExecAllowlistPatternValidation {
let trimmed = pattern?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
guard !trimmed.isEmpty else { return .invalid(.empty) }
guard self.containsPathComponent(trimmed) else { return .invalid(.missingPathComponent) }
return .valid(trimmed)
}
static func isValidAllowlistPattern(_ pattern: String?) -> Bool {
static func isPathPattern(_ pattern: String?) -> Bool {
switch self.validateAllowlistPattern(pattern) {
case .valid:
true
@@ -747,11 +737,6 @@ enum ExecApprovalHelpers {
}
}
static func isPathPattern(_ pattern: String?) -> Bool {
let trimmed = pattern?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
return self.patternHasPathSelector(trimmed)
}
static func parseDecision(_ raw: String?) -> ExecApprovalDecision? {
let trimmed = raw?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
guard !trimmed.isEmpty else { return nil }
@@ -774,7 +759,7 @@ enum ExecApprovalHelpers {
return pattern.isEmpty ? nil : pattern
}
static func patternHasPathSelector(_ pattern: String) -> Bool {
private static func containsPathComponent(_ pattern: String) -> Bool {
pattern.contains("/") || pattern.contains("~") || pattern.contains("\\")
}
}

View File

@@ -70,7 +70,6 @@ actor GatewayConnection {
case wizardStatus = "wizard.status"
case talkConfig = "talk.config"
case talkMode = "talk.mode"
case talkSpeak = "talk.speak"
case webLoginStart = "web.login.start"
case webLoginWait = "web.login.wait"
case channelsLogout = "channels.logout"

View File

@@ -105,7 +105,7 @@ struct SystemRunSettingsView: View {
.foregroundStyle(.secondary)
} else {
HStack(spacing: 8) {
TextField("Add command name or path glob", text: self.$newPattern)
TextField("Add allowlist path pattern (case-insensitive globs)", text: self.$newPattern)
.textFieldStyle(.roundedBorder)
Button("Add") {
if self.model.addEntry(self.newPattern) == nil {
@@ -113,10 +113,10 @@ struct SystemRunSettingsView: View {
}
}
.buttonStyle(.bordered)
.disabled(!self.model.isValidPattern(self.newPattern))
.disabled(!self.model.isPathPattern(self.newPattern))
}
Text("Bare names match PATH-resolved commands. Use a path glob for a specific binary.")
Text("Path patterns only. Basename entries like \"echo\" are ignored.")
.font(.footnote)
.foregroundStyle(.secondary)
if let validationMessage = self.model.allowlistValidationMessage {
@@ -424,8 +424,8 @@ final class ExecApprovalsSettingsModel {
self.entries.first(where: { $0.id == id })
}
func isValidPattern(_ pattern: String) -> Bool {
ExecApprovalHelpers.isValidAllowlistPattern(pattern)
func isPathPattern(_ pattern: String) -> Bool {
ExecApprovalHelpers.isPathPattern(pattern)
}
func refreshSkillBins(force: Bool = false) async {

View File

@@ -2,7 +2,6 @@ import AVFoundation
import Foundation
import OpenClawChatUI
import OpenClawKit
import OpenClawProtocol
import OSLog
import Speech
@@ -476,16 +475,7 @@ actor TalkModeRuntime {
self.ttsLogger
.error(
"talk TTS failed: \(error.localizedDescription, privacy: .public); " +
"retrying gateway talk.speak")
do {
try await self.playGatewayTalkSpeak(input: input)
return
} catch {
self.ttsLogger
.error(
"talk gateway TTS failed: \(error.localizedDescription, privacy: .public); " +
"falling back to system voice")
}
"falling back to system voice")
do {
try await self.playSystemVoice(input: input)
} catch {
@@ -730,42 +720,6 @@ actor TalkModeRuntime {
return await self.playMP3(stream: stream)
}
private func playGatewayTalkSpeak(input: TalkPlaybackInput) async throws {
let params = Self.makeTalkSpeakParams(
text: input.cleanedText,
voiceId: input.voiceId,
modelId: self.currentModelId ?? self.defaultModelId,
outputFormat: self.defaultOutputFormat,
directive: input.directive)
let result: TalkSpeakResult = try await GatewayConnection.shared.requestDecoded(
method: .talkSpeak,
params: params,
timeoutMs: max(30000, input.synthTimeoutSeconds * 1000 + 5000))
guard let audioData = Data(base64Encoded: result.audiobase64), !audioData.isEmpty else {
throw NSError(domain: "TalkSpeak", code: 1, userInfo: [
NSLocalizedDescriptionKey: "gateway talk.speak returned empty audio",
])
}
_ = await self.stopPCM()
_ = await self.stopMP3()
if self.interruptOnSpeech {
guard await self.prepareForPlayback(generation: input.generation) else { return }
}
await MainActor.run { TalkModeController.shared.updatePhase(.speaking) }
self.phase = .speaking
let playback = await self.playTalkAudio(data: audioData)
self.ttsLogger
.info(
"talk gateway audio provider=\(result.provider, privacy: .public) " +
"format=\(result.outputformat ?? "unknown", privacy: .public) " +
"finished=\(playback.finished, privacy: .public)")
if !playback.finished, playback.interruptedAt == nil {
throw NSError(domain: "TalkSpeak", code: 2, userInfo: [
NSLocalizedDescriptionKey: "gateway talk.speak audio playback failed",
])
}
}
private func playSystemVoice(input: TalkPlaybackInput) async throws {
self.ttsLogger.info("talk system voice start chars=\(input.cleanedText.count, privacy: .public)")
if self.interruptOnSpeech {
@@ -893,54 +847,6 @@ actor TalkModeRuntime {
}
extension TalkModeRuntime {
static func makeTalkSpeakParams(
text: String,
voiceId: String?,
modelId: String?,
outputFormat: String?,
directive: TalkDirective?) -> [String: AnyCodable]
{
var params: [String: AnyCodable] = ["text": AnyCodable(text)]
func addString(_ key: String, _ value: String?) {
let trimmed = value?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
guard !trimmed.isEmpty else { return }
params[key] = AnyCodable(trimmed)
}
addString("voiceId", voiceId)
addString("modelId", directive?.modelId ?? modelId)
addString("outputFormat", directive?.outputFormat ?? outputFormat)
if let speed = directive?.speed {
params["speed"] = AnyCodable(speed)
}
if let rateWPM = directive?.rateWPM {
params["rateWpm"] = AnyCodable(rateWPM)
}
if let stability = directive?.stability {
params["stability"] = AnyCodable(stability)
}
if let similarity = directive?.similarity {
params["similarity"] = AnyCodable(similarity)
}
if let style = directive?.style {
params["style"] = AnyCodable(style)
}
if let speakerBoost = directive?.speakerBoost {
params["speakerBoost"] = AnyCodable(speakerBoost)
}
if let seed = directive?.seed {
params["seed"] = AnyCodable(seed)
}
addString("normalize", directive?.normalize)
addString("language", directive?.language)
if let latencyTier = directive?.latencyTier {
params["latencyTier"] = AnyCodable(latencyTier)
}
return params
}
// MARK: - Audio playback (MainActor helpers)
@MainActor

View File

@@ -2330,17 +2330,20 @@ public struct TalkRealtimeSessionParams: Codable, Sendable {
public let provider: String?
public let model: String?
public let voice: String?
public let instructions: String?
public init(
sessionkey: String?,
provider: String?,
model: String?,
voice: String?)
voice: String?,
instructions: String?)
{
self.sessionkey = sessionkey
self.provider = provider
self.model = model
self.voice = voice
self.instructions = instructions
}
private enum CodingKeys: String, CodingKey {
@@ -2348,6 +2351,7 @@ public struct TalkRealtimeSessionParams: Codable, Sendable {
case provider
case model
case voice
case instructions
}
}
@@ -2610,22 +2614,18 @@ public struct WebLoginStartParams: Codable, Sendable {
public struct WebLoginWaitParams: Codable, Sendable {
public let timeoutms: Int?
public let accountid: String?
public let currentqrdataurl: String?
public init(
timeoutms: Int?,
accountid: String?,
currentqrdataurl: String?)
accountid: String?)
{
self.timeoutms = timeoutms
self.accountid = accountid
self.currentqrdataurl = currentqrdataurl
}
private enum CodingKeys: String, CodingKey {
case timeoutms = "timeoutMs"
case accountid = "accountId"
case currentqrdataurl = "currentQrDataUrl"
}
}

View File

@@ -156,63 +156,4 @@ struct ChannelsSettingsSmokeTests {
let view = ChannelsSettings(store: store)
_ = view.body
}
@Test func `whatsapp login wait result keeps latest qr until connected`() {
let store = makeChannelsStore(channels: [:])
store.whatsappLoginQrDataUrl = "data:image/png;base64,initial"
store.applyWhatsAppLoginWaitResult(
WhatsAppLoginWaitResult(
connected: false,
message: "QR refreshed. Scan the latest code in WhatsApp → Linked Devices.",
qrDataUrl: "data:image/png;base64,rotated"))
#expect(store.whatsappLoginQrDataUrl == "data:image/png;base64,rotated")
#expect(store.whatsappLoginConnected == false)
store.applyWhatsAppLoginWaitResult(
WhatsAppLoginWaitResult(
connected: false,
message: "Still waiting for the QR scan. Let me know when youve scanned it.",
qrDataUrl: nil))
#expect(store.whatsappLoginQrDataUrl == "data:image/png;base64,rotated")
store.applyWhatsAppLoginWaitResult(
WhatsAppLoginWaitResult(
connected: true,
message: "✅ Linked! WhatsApp is ready.",
qrDataUrl: nil))
#expect(store.whatsappLoginQrDataUrl == nil)
#expect(store.whatsappLoginConnected == true)
}
@Test func `whatsapp login wait budget allows one final poll`() {
let startedAt = Date(timeIntervalSince1970: 1_700_000_000)
var didRunFinalWait = false
#expect(
whatsappLoginWaitRequestTimeoutMs(
startedAt: startedAt,
timeoutMs: 1_000,
didRunFinalWait: &didRunFinalWait,
now: Date(timeInterval: 0.25, since: startedAt)) == 750)
#expect(didRunFinalWait == false)
#expect(
whatsappLoginWaitRequestTimeoutMs(
startedAt: startedAt,
timeoutMs: 1_000,
didRunFinalWait: &didRunFinalWait,
now: Date(timeInterval: 1.25, since: startedAt)) == 1)
#expect(didRunFinalWait == true)
#expect(
whatsappLoginWaitRequestTimeoutMs(
startedAt: startedAt,
timeoutMs: 1_000,
didRunFinalWait: &didRunFinalWait,
now: Date(timeInterval: 1.5, since: startedAt)) == nil)
}
}

View File

@@ -66,34 +66,22 @@ struct ExecAllowlistTests {
#expect(match?.pattern == entry.pattern)
}
@Test func `match accepts basename pattern for PATH resolved executable`() {
@Test func `match ignores basename pattern`() {
let entry = ExecAllowlistEntry(pattern: "rg")
let resolution = Self.homebrewRGResolution()
let match = ExecAllowlistMatcher.match(entries: [entry], resolution: resolution)
#expect(match?.pattern == entry.pattern)
#expect(match == nil)
}
@Test func `match accepts basename glob for PATH resolved executable`() {
let entry = ExecAllowlistEntry(pattern: "r?")
let resolution = Self.homebrewRGResolution()
let match = ExecAllowlistMatcher.match(entries: [entry], resolution: resolution)
#expect(match?.pattern == entry.pattern)
}
@Test func `match ignores basename for path selected executable`() {
@Test func `match ignores basename for relative executable`() {
let entry = ExecAllowlistEntry(pattern: "echo")
let relativeResolution = ExecCommandResolution(
let resolution = ExecCommandResolution(
rawExecutable: "./echo",
resolvedPath: "/tmp/oc-basename/echo",
executableName: "echo",
cwd: "/tmp/oc-basename")
let absoluteResolution = ExecCommandResolution(
rawExecutable: "/tmp/oc-basename/echo",
resolvedPath: "/tmp/oc-basename/echo",
executableName: "echo",
cwd: "/tmp/oc-basename")
#expect(ExecAllowlistMatcher.match(entries: [entry], resolution: relativeResolution) == nil)
#expect(ExecAllowlistMatcher.match(entries: [entry], resolution: absoluteResolution) == nil)
let match = ExecAllowlistMatcher.match(entries: [entry], resolution: resolution)
#expect(match == nil)
}
@Test func `match is case insensitive`() {

View File

@@ -33,13 +33,18 @@ struct ExecApprovalHelpersTests {
#expect(ExecApprovalHelpers.isPathPattern("/usr/bin/rg"))
#expect(ExecApprovalHelpers.isPathPattern(" ~/bin/rg "))
#expect(!ExecApprovalHelpers.isPathPattern("rg"))
#expect(ExecApprovalHelpers.isValidAllowlistPattern("rg"))
if case let .invalid(reason) = ExecApprovalHelpers.validateAllowlistPattern(" ") {
#expect(reason == .empty)
} else {
Issue.record("Expected empty pattern rejection")
}
if case let .invalid(reason) = ExecApprovalHelpers.validateAllowlistPattern("echo") {
#expect(reason == .missingPathComponent)
} else {
Issue.record("Expected basename pattern rejection")
}
}
@Test func `requires ask matches policy`() {

View File

@@ -31,7 +31,7 @@ struct ExecApprovalsStoreRefactorTests {
}
@Test
func `update allowlist accepts basename pattern`() async throws {
func `update allowlist reports rejected basename pattern`() async throws {
try await self.withTempStateDir { _ in
let rejected = ExecApprovalsStore.updateAllowlist(
agentId: "main",
@@ -39,10 +39,12 @@ struct ExecApprovalsStoreRefactorTests {
ExecAllowlistEntry(pattern: "echo"),
ExecAllowlistEntry(pattern: "/bin/echo"),
])
#expect(rejected.isEmpty)
#expect(rejected.count == 1)
#expect(rejected.first?.reason == .missingPathComponent)
#expect(rejected.first?.pattern == "echo")
let resolved = ExecApprovalsStore.resolve(agentId: "main")
#expect(resolved.allowlist.map(\.pattern) == ["echo", "/bin/echo"])
#expect(resolved.allowlist.map(\.pattern) == ["/bin/echo"])
}
}

View File

@@ -1,4 +1,3 @@
import OpenClawKit
import Speech
import Testing
@testable import OpenClaw
@@ -17,19 +16,23 @@ struct TalkModeRuntimeSpeechTests {
let elevenLabsPlan = TalkModeRuntime.playbackPlan(
provider: "elevenlabs",
apiKey: "key",
voiceId: "voice")
voiceId: "voice"
)
let missingKeyPlan = TalkModeRuntime.playbackPlan(
provider: "elevenlabs",
apiKey: nil,
voiceId: "voice")
voiceId: "voice"
)
let missingVoicePlan = TalkModeRuntime.playbackPlan(
provider: "elevenlabs",
apiKey: "key",
voiceId: nil)
voiceId: nil
)
let blankKeyPlan = TalkModeRuntime.playbackPlan(
provider: "elevenlabs",
apiKey: "",
voiceId: "voice")
voiceId: "voice"
)
let mlxPlan = TalkModeRuntime.playbackPlan(provider: "mlx", apiKey: nil, voiceId: nil)
let systemPlan = TalkModeRuntime.playbackPlan(provider: "system", apiKey: nil, voiceId: nil)
@@ -40,40 +43,4 @@ struct TalkModeRuntimeSpeechTests {
#expect(mlxPlan == .mlxThenSystemVoice)
#expect(systemPlan == .systemVoiceOnly)
}
@Test func `talk speak params carry resolved voice and directive overrides`() {
let params = TalkModeRuntime.makeTalkSpeakParams(
text: "hello",
voiceId: "voice-123",
modelId: "eleven_v3",
outputFormat: "mp3_44100_128",
directive: TalkDirective(
modelId: "eleven_turbo_v2_5",
speed: 1.1,
rateWPM: 180,
stability: 0.4,
similarity: 0.7,
style: 0.2,
speakerBoost: true,
seed: 42,
normalize: "auto",
language: "en",
outputFormat: "mp3_44100_128",
latencyTier: 3))
#expect(params["text"]?.value as? String == "hello")
#expect(params["voiceId"]?.value as? String == "voice-123")
#expect(params["modelId"]?.value as? String == "eleven_turbo_v2_5")
#expect(params["outputFormat"]?.value as? String == "mp3_44100_128")
#expect(params["speed"]?.value as? Double == 1.1)
#expect(params["rateWpm"]?.value as? Int == 180)
#expect(params["stability"]?.value as? Double == 0.4)
#expect(params["similarity"]?.value as? Double == 0.7)
#expect(params["style"]?.value as? Double == 0.2)
#expect(params["speakerBoost"]?.value as? Bool == true)
#expect(params["seed"]?.value as? Int == 42)
#expect(params["normalize"]?.value as? String == "auto")
#expect(params["language"]?.value as? String == "en")
#expect(params["latencyTier"]?.value as? Int == 3)
}
}

View File

@@ -2330,17 +2330,20 @@ public struct TalkRealtimeSessionParams: Codable, Sendable {
public let provider: String?
public let model: String?
public let voice: String?
public let instructions: String?
public init(
sessionkey: String?,
provider: String?,
model: String?,
voice: String?)
voice: String?,
instructions: String?)
{
self.sessionkey = sessionkey
self.provider = provider
self.model = model
self.voice = voice
self.instructions = instructions
}
private enum CodingKeys: String, CodingKey {
@@ -2348,6 +2351,7 @@ public struct TalkRealtimeSessionParams: Codable, Sendable {
case provider
case model
case voice
case instructions
}
}
@@ -2610,22 +2614,18 @@ public struct WebLoginStartParams: Codable, Sendable {
public struct WebLoginWaitParams: Codable, Sendable {
public let timeoutms: Int?
public let accountid: String?
public let currentqrdataurl: String?
public init(
timeoutms: Int?,
accountid: String?,
currentqrdataurl: String?)
accountid: String?)
{
self.timeoutms = timeoutms
self.accountid = accountid
self.currentqrdataurl = currentqrdataurl
}
private enum CodingKeys: String, CodingKey {
case timeoutms = "timeoutMs"
case accountid = "accountId"
case currentqrdataurl = "currentQrDataUrl"
}
}

View File

@@ -1,4 +1,4 @@
13b68287fec00108ca66032120909a0eac797ed541e026357e175e3fce5bacdd config-baseline.json
77ee66fb3b2cde94b393712bc03a132b096cf601c193bde1fe42902eecb0b66b config-baseline.core.json
d72032762ab46b99480b57deb81130a0ab5b1401189cfbaf4f7fef4a063a7f6c config-baseline.channel.json
0d5ba81f0030bd39b7ae285096276cc18b150836c2252fd2217329fc6154e80e config-baseline.plugin.json
d3b5638e205a94e40d07aa1830c8d57135df18ff9388fb7d72ee84c791ac293f config-baseline.json
bf00f7910d8f0d8e12592e8a1c6bd0397f8e62fef2c11eb0cbd3b3a3e2a78ffe config-baseline.core.json
22d7cd6d8279146b2d79c9531a55b80b52a2c99c81338c508104729154fdd02d config-baseline.channel.json
a91304e3566ecc8906f199b88a2e38eaee86130aad799bf4d62921e2f0ddc1b5 config-baseline.plugin.json

View File

@@ -1,2 +1,2 @@
56ccee3ef8ff3b0ba7e2e765ae631b59254464585d5fef9db7e905f2c4c34ded plugin-sdk-api-baseline.json
39184cf8afaec691f0352d1a113e30a7099b87c0748237a3c7307e903ba24eee plugin-sdk-api-baseline.jsonl
96905c33f4498446f612ae17dee6affdf84ef0e2e5a0f25bf7191c315f5b826f plugin-sdk-api-baseline.json
d8eb6331562fde29531eaac18409bb7fabcc70623bf25395f8e5710a49765f0f plugin-sdk-api-baseline.jsonl

View File

@@ -3,18 +3,6 @@
"source": "OpenClaw",
"target": "OpenClaw"
},
{
"source": "OpenAI",
"target": "OpenAI"
},
{
"source": "OpenAI provider",
"target": "OpenAI provider"
},
{
"source": "Status",
"target": "Status"
},
{
"source": "Gateway",
"target": "Gateway 网关"
@@ -23,30 +11,6 @@
"source": "Pi",
"target": "Pi"
},
{
"source": "Agent runtimes",
"target": "Agent Runtimes"
},
{
"source": "Agent Runtimes",
"target": "Agent Runtimes"
},
{
"source": "Codex harness",
"target": "Codex harness"
},
{
"source": "Agent harness plugins",
"target": "Agent harness plugins"
},
{
"source": "Agent loop",
"target": "Agent loop"
},
{
"source": "Models",
"target": "Models"
},
{
"source": "Skills",
"target": "Skills"
@@ -367,18 +331,10 @@
"source": "Plugin SDK",
"target": "插件 SDK"
},
{
"source": "Building plugins",
"target": "构建插件"
},
{
"source": "Plugin SDK Overview",
"target": "插件 SDK 概览"
},
{
"source": "Plugin SDK overview",
"target": "插件 SDK 概览"
},
{
"source": "SDK Overview",
"target": "SDK 概览"
@@ -387,22 +343,6 @@
"source": "Plugin Entry Points",
"target": "插件入口点"
},
{
"source": "Plugin entry points",
"target": "插件入口点"
},
{
"source": "Plugin hooks",
"target": "插件钩子"
},
{
"source": "Internal hooks",
"target": "内部钩子"
},
{
"source": "Plugin architecture internals",
"target": "插件架构内部机制"
},
{
"source": "Entry Points",
"target": "入口点"
@@ -463,10 +403,6 @@
"source": "Tencent Cloud (TokenHub)",
"target": "腾讯云TokenHub"
},
{
"source": "Codex Harness Context Engine Port",
"target": "Codex Harness Context Engine Port"
},
{
"source": "/gateway/configuration#strict-validation",
"target": "/gateway/configuration#strict-validation"

View File

@@ -76,8 +76,3 @@ For script compatibility, probe errors keep this first line unchanged:
`Auth profile credentials are missing or expired.`
Human-friendly detail and stable reason codes may be added on subsequent lines.
## Related
- [Secrets management](/gateway/secrets)
- [Auth storage](/concepts/oauth)

View File

@@ -7,6 +7,8 @@ read_when:
title: "Scheduled tasks"
---
# Scheduled Tasks (Cron)
Cron is the Gateway's built-in scheduler. It persists jobs, wakes the agent at the right time, and can deliver output back to a chat channel or webhook endpoint.
## Quick start
@@ -86,8 +88,6 @@ This fires ~56 times per month instead of 01 times per month. OpenClaw use
**Main session** jobs enqueue a system event and optionally wake the heartbeat (`--wake now` or `--wake next-heartbeat`). **Isolated** jobs run a dedicated agent turn with a fresh session. **Custom sessions** (`session:xxx`) persist context across runs, enabling workflows like daily standups that build on previous summaries.
For isolated jobs, “fresh session” means a new transcript/session id for each run. OpenClaw may carry safe preferences such as thinking/fast/verbose settings, labels, and explicit user-selected model/auth overrides, but it does not inherit ambient conversation context from an older cron row: channel/group routing, send or queue policy, elevation, origin, or ACP runtime binding. Use `current` or `session:<id>` when a recurring job should deliberately build on the same conversation context.
For isolated jobs, runtime teardown now includes best-effort browser cleanup for that cron session. Cleanup failures are ignored so the actual cron result still wins.
Isolated cron runs also dispose any bundled MCP runtime instances created for the job through the shared runtime-cleanup path. This matches how main-session and custom-session MCP clients are torn down, so isolated cron jobs do not leak stdio child processes or long-lived MCP connections across runs.
@@ -96,11 +96,6 @@ When isolated cron runs orchestrate subagents, delivery also prefers the final
descendant output over stale parent interim text. If descendants are still
running, OpenClaw suppresses that partial parent update instead of announcing it.
For text-only Discord announce targets, OpenClaw sends the canonical final
assistant text once instead of replaying both streamed/intermediate text payloads
and the final answer. Media and structured Discord payloads are still delivered
as separate payloads so attachments and components are not dropped.
### Payload options for isolated jobs
- `--message`: prompt text (required for isolated)
@@ -118,7 +113,7 @@ Model-selection precedence for isolated jobs is:
1. Gmail hook model override (when the run came from Gmail and that override is allowed)
2. Per-job payload `model`
3. User-selected stored cron session model override
3. Stored cron session model override
4. Agent/default model selection
Fast mode follows the resolved live selection too. If the selected model config
@@ -126,11 +121,10 @@ has `params.fastMode`, isolated cron uses that by default. A stored session
`fastMode` override still wins over config in either direction.
If an isolated run hits a live model-switch handoff, cron retries with the
switched provider/model and persists that live selection for the active run
before retrying. When the switch also carries a new auth profile, cron persists
that auth profile override for the active run too. Retries are bounded: after
the initial attempt plus 2 switch retries, cron aborts instead of looping
forever.
switched provider/model and persists that live selection before retrying. When
the switch also carries a new auth profile, cron persists that auth profile
override too. Retries are bounded: after the initial attempt plus 2 switch
retries, cron aborts instead of looping forever.
## Delivery and output

View File

@@ -106,7 +106,7 @@ const handler = async (event) => {
export default handler;
```
Each event includes: `type`, `action`, `sessionKey`, `timestamp`, `messages` (push to send to user), and `context` (event-specific data). Agent and tool plugin hook contexts can also include `trace`, a read-only W3C-compatible diagnostic trace context that plugins may pass into structured logs for OTEL correlation.
Each event includes: `type`, `action`, `sessionKey`, `timestamp`, `messages` (push to send to user), and `context` (event-specific data).
### Event context highlights
@@ -205,12 +205,9 @@ Runs `BOOT.md` from the active workspace when the gateway starts.
## Plugin hooks
Plugins can register typed hooks through the Plugin SDK for deeper integration:
intercepting tool calls, modifying prompts, controlling message flow, and more.
Use plugin hooks when you need `before_tool_call`, `before_agent_reply`,
`before_install`, or other in-process lifecycle hooks.
Plugins can register hooks through the Plugin SDK for deeper integration: intercepting tool calls, modifying prompts, controlling message flow, and more. The Plugin SDK exposes 28 hooks covering model resolution, agent lifecycle, message flow, tool execution, subagent coordination, and gateway lifecycle.
For the complete plugin hook reference, see [Plugin hooks](/plugins/hooks).
For the complete plugin hook reference including `before_tool_call`, `before_agent_reply`, `before_install`, and all other plugin hooks, see [Plugin Architecture](/plugins/architecture-internals#provider-runtime-hooks).
## Configuration
@@ -318,5 +315,5 @@ Check for missing binaries (PATH), environment variables, config values, or OS c
- [CLI Reference: hooks](/cli/hooks)
- [Webhooks](/automation/cron-jobs#webhooks)
- [Plugin hooks](/plugins/hooks) — in-process plugin lifecycle hooks
- [Plugin Architecture](/plugins/architecture-internals#provider-runtime-hooks) — full plugin hook reference
- [Configuration](/gateway/configuration-reference#hooks)

View File

@@ -40,7 +40,7 @@ flowchart TD
| Audit what ran and when | Background Tasks | `openclaw tasks list` and `openclaw tasks audit` |
| Multi-step research then summarize | Task Flow | Durable orchestration with revision tracking |
| Run a script on session reset | Hooks | Event-driven, fires on lifecycle events |
| Execute code on every tool call | Plugin hooks | In-process hooks can intercept tool calls |
| Execute code on every tool call | Hooks | Hooks can filter by event type |
| Always check compliance before replying | Standing Orders | Injected into every session automatically |
### Scheduled Tasks (Cron) vs Heartbeat
@@ -83,11 +83,7 @@ See [Standing Orders](/automation/standing-orders).
### Hooks
Internal hooks are event-driven scripts triggered by agent lifecycle events
(`/new`, `/reset`, `/stop`), session compaction, gateway startup, and message
flow. They are automatically discovered from directories and can be managed
with `openclaw hooks`. For in-process tool-call interception, use
[Plugin hooks](/plugins/hooks).
Hooks are event-driven scripts triggered by agent lifecycle events (`/new`, `/reset`, `/stop`), session compaction, gateway startup, message flow, and tool calls. Hooks are automatically discovered from directories and can be managed with `openclaw hooks`.
See [Hooks](/automation/hooks).
@@ -101,7 +97,7 @@ See [Heartbeat](/gateway/heartbeat).
- **Cron** handles precise schedules (daily reports, weekly reviews) and one-shot reminders. All cron executions create task records.
- **Heartbeat** handles routine monitoring (inbox, calendar, notifications) in one batched turn every 30 minutes.
- **Hooks** react to specific events (session resets, compaction, message flow) with custom scripts. Plugin hooks cover tool calls.
- **Hooks** react to specific events (tool calls, session resets, compaction) with custom scripts.
- **Standing orders** give the agent persistent context and authority boundaries.
- **Task Flow** coordinates multi-step flows above individual tasks.
- **Tasks** automatically track all detached work so you can inspect and audit it.
@@ -112,7 +108,6 @@ See [Heartbeat](/gateway/heartbeat).
- [Background Tasks](/automation/tasks) — task ledger for all detached work
- [Task Flow](/automation/taskflow) — durable multi-step flow orchestration
- [Hooks](/automation/hooks) — event-driven lifecycle scripts
- [Plugin hooks](/plugins/hooks) — in-process tool, prompt, message, and lifecycle hooks
- [Standing Orders](/automation/standing-orders) — persistent agent instructions
- [Heartbeat](/gateway/heartbeat) — periodic main-session turns
- [Configuration Reference](/gateway/configuration-reference) — all config keys

View File

@@ -27,7 +27,7 @@ This is the difference between telling your assistant "send the weekly report" e
- You only get involved for exceptions and approvals
- The agent fills idle time productively
## How they work
## How They Work
Standing orders are defined in your [agent workspace](/concepts/agent-workspace) files. The recommended approach is to include them directly in `AGENTS.md` (which is auto-injected every session) so the agent always has them in context. For larger configurations, you can also place them in a dedicated file like `standing-orders.md` and reference it from `AGENTS.md`.
@@ -198,6 +198,8 @@ This pattern prevents the most common agent failure mode: acknowledging a task w
For agents managing multiple concerns, organize standing orders as separate programs with clear boundaries:
```markdown
# Standing Orders
## Program 1: [Domain A] (Weekly)
...

View File

@@ -20,78 +20,6 @@ Use Task Flow when work spans multiple sequential or branching steps and you nee
| Observe externally created tasks | Task Flow (mirrored) |
| One-shot reminder | Cron job |
## Reliable scheduled workflow pattern
For recurring workflows such as market intelligence briefings, treat the schedule, orchestration, and reliability checks as separate layers:
1. Use [Scheduled Tasks](/automation/cron-jobs) for timing.
2. Use a persistent cron session when the workflow should build on prior context.
3. Use [Lobster](/tools/lobster) for deterministic steps, approval gates, and resume tokens.
4. Use Task Flow to track the multi-step run across child tasks, waits, retries, and gateway restarts.
Example cron shape:
```bash
openclaw cron add \
--name "Market intelligence brief" \
--cron "0 7 * * 1-5" \
--tz "America/New_York" \
--session session:market-intel \
--message "Run the market-intel Lobster workflow. Verify source freshness before summarizing." \
--announce \
--channel slack \
--to "channel:C1234567890"
```
Use `session:<id>` instead of `isolated` when the recurring workflow needs deliberate history, previous run summaries, or standing context. Use `isolated` when each run should start fresh and all required state is explicit in the workflow.
Inside the workflow, put reliability checks before the LLM summary step:
```yaml
name: market-intel-brief
steps:
- id: preflight
command: market-intel check --json
- id: collect
command: market-intel collect --json
stdin: $preflight.json
- id: summarize
command: market-intel summarize --json
stdin: $collect.json
- id: approve
command: market-intel deliver --preview
stdin: $summarize.json
approval: required
- id: deliver
command: market-intel deliver --execute
stdin: $summarize.json
condition: $approve.approved
```
Recommended preflight checks:
- Browser availability and profile choice, for example `openclaw` for managed state or `user` when a signed-in Chrome session is required. See [Browser](/tools/browser).
- API credentials and quota for each source.
- Network reachability for required endpoints.
- Required tools enabled for the agent, such as `lobster`, `browser`, and `llm-task`.
- Failure destination configured for cron so preflight failures are visible. See [Scheduled Tasks](/automation/cron-jobs#delivery-and-output).
Recommended data provenance fields for every collected item:
```json
{
"sourceUrl": "https://example.com/report",
"retrievedAt": "2026-04-24T12:00:00Z",
"asOf": "2026-04-24",
"title": "Example report",
"content": "..."
}
```
Have the workflow reject or mark stale items before summarization. The LLM step should receive only structured JSON and should be asked to preserve `sourceUrl`, `retrievedAt`, and `asOf` in its output. Use [LLM Task](/tools/llm-task) when you need a schema-validated model step inside the workflow.
For reusable team or community workflows, package the CLI, `.lobster` files, and any setup notes as a skill or plugin and publish it through [ClawHub](/tools/clawhub). Keep workflow-specific guardrails in that package unless the plugin API is missing a needed generic capability.
## Sync modes
### Managed mode

View File

@@ -101,7 +101,3 @@ await web_search({
- Results are cached for 15 minutes by default (configurable via `cacheTtlMinutes`).
See [Web tools](/tools/web) for the full web_search configuration.
## Related
- [Brave search](/tools/brave-search)

View File

@@ -7,6 +7,8 @@ read_when:
title: "BlueBubbles"
---
# BlueBubbles (macOS REST)
Status: bundled plugin that talks to the BlueBubbles macOS server over HTTP. **Recommended for iMessage integration** due to its richer API and easier setup compared to the legacy imsg channel.
## Bundled plugin

View File

@@ -433,10 +433,14 @@ Planned features:
- [ ] Dynamic agent selection (choose agents based on message content)
- [ ] Agent priorities (some agents respond before others)
## See Also
- [Multi-Agent Configuration](/tools/multi-agent-sandbox-tools)
- [Routing Configuration](/channels/channel-routing)
- [Session Management](/concepts/session)
## Related
- [Groups](/channels/groups)
- [Channel routing](/channels/channel-routing)
- [Pairing](/channels/pairing)
- [Multi-agent sandbox tools](/tools/multi-agent-sandbox-tools)
- [Session management](/concepts/session)

View File

@@ -267,9 +267,6 @@ Now create some channels on your Discord server and start chatting. Your agent c
- Guild channels are isolated session keys (`agent:<agentId>:discord:channel:<channelId>`).
- Group DMs are ignored by default (`channels.discord.dm.groupEnabled=false`).
- Native slash commands run in isolated command sessions (`agent:<agentId>:discord:slash:<userId>`), while still carrying `CommandTargetSessionKey` to the routed conversation session.
- Text-only cron/heartbeat announce delivery to Discord uses the final
assistant-visible answer once. Media and structured component payloads remain
multi-message when the agent emits multiple deliverable payloads.
## Forum channels
@@ -308,7 +305,7 @@ By default, components are single use. Set `components.reusable=true` to allow b
To restrict who can click a button, set `allowedUsers` on that button (Discord user IDs, tags, or `*`). When configured, unmatched users receive an ephemeral denial.
The `/model` and `/models` slash commands open an interactive model picker with provider, model, and compatible runtime dropdowns plus a Submit step. `/models add` is deprecated and now returns a deprecation message instead of registering models from chat. The picker reply is ephemeral and only the invoking user can use it.
The `/model` and `/models` slash commands open an interactive model picker with provider and model dropdowns plus a Submit step. Unless `commands.modelsWrite=false`, `/models add` also supports adding a new provider/model entry from chat, and newly added models show up without restarting the gateway. The picker reply is ephemeral and only the invoking user can use it.
File attachments:

View File

@@ -430,12 +430,6 @@ Full configuration: [Gateway configuration](/gateway/configuration)
- ✅ Thread replies
- ✅ Media replies stay thread-aware when replying to a thread message
For `groupSessionScope: "group_topic"` and `"group_topic_sender"`, native
Feishu/Lark topic groups use the event `thread_id` (`omt_*`) as the canonical
topic session key. Normal group replies that OpenClaw turns into threads keep
using the reply root message ID (`om_*`) so the first turn and follow-up turn
stay in the same session.
---
## Related

View File

@@ -5,6 +5,8 @@ read_when:
title: "Group messages"
---
# Group messages (WhatsApp web channel)
Goal: let Clawd sit in WhatsApp groups, wake up only when pinged, and keep that thread separate from the personal DM session.
Note: `agents.list[].groupChat.mentionPatterns` is now used by Telegram/Discord/Slack/iMessage as well; this doc focuses on WhatsApp-specific behavior. For multi-agent setups, set `agents.list[].groupChat.mentionPatterns` per agent (or use `messages.groupChat.mentionPatterns` as a global fallback).

View File

@@ -6,6 +6,8 @@ read_when:
title: "iMessage"
---
# iMessage (legacy: imsg)
<Warning>
For new iMessage deployments, use <a href="/channels/bluebubbles">BlueBubbles</a>.

View File

@@ -9,16 +9,6 @@ title: "Chat channels"
OpenClaw can talk to you on any chat app you already use. Each channel connects via the Gateway.
Text is supported everywhere; media and reactions vary by channel.
## Delivery notes
- Telegram replies that contain markdown image syntax, such as `![alt](url)`,
are converted into media replies on the final outbound path when possible.
- Slack multi-person DMs route as group chats, so group policy, mention
behavior, and group-session rules apply to MPIM conversations.
- WhatsApp setup is install-on-demand: onboarding can show the setup flow before
Baileys runtime dependencies are staged, and the Gateway loads the WhatsApp
runtime only when the channel is actually active.
## Supported channels
- [BlueBubbles](/channels/bluebubbles) — **Recommended for iMessage**; uses the BlueBubbles macOS server REST API with full feature support (bundled plugin; edit, unsend, effects, reactions, group management — edit currently broken on macOS 26 Tahoe).

View File

@@ -310,127 +310,16 @@ Enable encryption:
Verification commands (all take `--verbose` for diagnostics and `--json` for machine-readable output):
```bash
openclaw matrix verify status
```
Verbose status (full diagnostics):
```bash
openclaw matrix verify status --verbose
```
Include the stored recovery key in machine-readable output:
```bash
openclaw matrix verify status --include-recovery-key --json
```
Bootstrap cross-signing and verification state:
```bash
openclaw matrix verify bootstrap
```
Verbose bootstrap diagnostics:
```bash
openclaw matrix verify bootstrap --verbose
```
Force a fresh cross-signing identity reset before bootstrapping:
```bash
openclaw matrix verify bootstrap --force-reset-cross-signing
```
Verify this device with a recovery key:
```bash
openclaw matrix verify device "<your-recovery-key>"
```
This command reports three separate states:
- `Recovery key accepted`: Matrix accepted the recovery key for secret storage or device trust.
- `Backup usable`: room-key backup can be loaded with trusted recovery material.
- `Device verified by owner`: the current OpenClaw device has full Matrix cross-signing identity trust.
`Signed by owner` in verbose or JSON output is diagnostic only. OpenClaw does not
treat that as sufficient unless `Cross-signing verified` is also `yes`.
The command still exits non-zero when full Matrix identity trust is incomplete,
even if the recovery key can unlock backup material. In that case, complete
self-verification from another Matrix client:
```bash
openclaw matrix verify self
```
Accept the request in another Matrix client, compare the SAS emoji or decimals,
and type `yes` only when they match. The command waits for Matrix to report
`Cross-signing verified: yes` before it exits successfully.
Use `verify bootstrap --force-reset-cross-signing` only when you intentionally
want to replace the current cross-signing identity.
Verbose device verification details:
```bash
openclaw matrix verify device "<your-recovery-key>" --verbose
```
Check room-key backup health:
```bash
openclaw matrix verify backup status
```
Verbose backup health diagnostics:
```bash
openclaw matrix verify backup status --verbose
```
Restore room keys from server backup:
```bash
openclaw matrix verify backup restore
```
Interactive self-verification flow:
```bash
openclaw matrix verify self
```
For lower-level or inbound verification requests, use:
```bash
openclaw matrix verify accept <id>
openclaw matrix verify start <id>
openclaw matrix verify sas <id>
openclaw matrix verify confirm-sas <id>
```
Use `openclaw matrix verify cancel <id>` to cancel a request.
Verbose restore diagnostics:
```bash
openclaw matrix verify backup restore --verbose
```
Delete the current server backup and create a fresh backup baseline. If the stored
backup key cannot be loaded cleanly, this reset can also recreate secret storage so
future cold starts can load the new backup key:
```bash
openclaw matrix verify backup reset --yes
```
All `verify` commands are concise by default (including quiet internal SDK logging) and show detailed diagnostics only with `--verbose`.
Use `--json` for full machine-readable output when scripting.
| Command | Purpose |
| -------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
| `openclaw matrix verify status` | Check cross-signing and device verification state |
| `openclaw matrix verify status --include-recovery-key --json` | Include the stored recovery key |
| `openclaw matrix verify bootstrap` | Bootstrap cross-signing and verification (see below) |
| `openclaw matrix verify bootstrap --force-reset-cross-signing` | Discard the current cross-signing identity and create a new one |
| `openclaw matrix verify device "<recovery-key>"` | Verify this device with a recovery key |
| `openclaw matrix verify backup status` | Check room-key backup health |
| `openclaw matrix verify backup restore` | Restore room keys from server backup |
| `openclaw matrix verify backup reset --yes` | Delete the current backup and create a fresh baseline (may recreate secret storage) |
In multi-account setups, Matrix CLI commands use the implicit Matrix default account unless you pass `--account <id>`.
If you configure multiple named accounts, set `channels.matrix.defaultAccount` first or those implicit CLI operations will stop and ask you to choose an account explicitly.
@@ -452,9 +341,7 @@ When encryption is disabled or unavailable for a named account, Matrix warnings
- `Cross-signing verified`: the SDK reports verification via cross-signing
- `Signed by owner`: signed by your own self-signing key
`Verified by owner` becomes `yes` only when cross-signing verification is present.
Local trust or an owner signature by itself is not enough for OpenClaw to treat
the device as fully verified.
`Verified by owner` becomes `yes` only when cross-signing or owner-signing is present. Local trust alone is not enough.
</Accordion>

View File

@@ -75,7 +75,7 @@ Disable with:
- Default: `channels.msteams.dmPolicy = "pairing"`. Unknown senders are ignored until approved.
- `channels.msteams.allowFrom` should use stable AAD object IDs.
- Do not rely on UPN/display-name matching for allowlists — they can change. OpenClaw disables direct name matching by default; opt in explicitly with `channels.msteams.dangerouslyAllowNameMatching: true`.
- UPNs/display names are mutable; direct matching is disabled by default and only enabled with `channels.msteams.dangerouslyAllowNameMatching: true`.
- The wizard can resolve names to IDs via Microsoft Graph when credentials allow.
**Group access**

View File

@@ -44,31 +44,6 @@ Details: [Plugins](/tools/plugin)
4. Configure OpenClaw:
- Config: `channels.nextcloud-talk.baseUrl` + `channels.nextcloud-talk.botSecret`
- Or env: `NEXTCLOUD_TALK_BOT_SECRET` (default account only)
CLI setup:
```bash
openclaw channels add --channel nextcloud-talk \
--url https://cloud.example.com \
--token "<shared-secret>"
```
Equivalent explicit fields:
```bash
openclaw channels add --channel nextcloud-talk \
--base-url https://cloud.example.com \
--secret "<shared-secret>"
```
File-backed secret:
```bash
openclaw channels add --channel nextcloud-talk \
--base-url https://cloud.example.com \
--secret-file /path/to/nextcloud-talk-secret
```
5. Restart the gateway (or finish setup).
Minimal config:

View File

@@ -104,28 +104,6 @@ existing approval as-is and creates a fresh pending upgrade request. Use
`openclaw devices list` to compare the currently approved access with the newly
requested access before you approve.
### Optional trusted-CIDR node auto-approve
Device pairing remains manual by default. For tightly controlled node networks,
you can opt in to first-time node auto-approval with explicit CIDRs or exact IPs:
```json5
{
gateway: {
nodes: {
pairing: {
autoApproveCidrs: ["192.168.1.0/24"],
},
},
},
}
```
This only applies to fresh `role: node` pairing requests with no requested
scopes. Operator, browser, Control UI, and WebChat clients still require manual
approval. Role, scope, metadata, and public-key changes still require manual
approval.
### Node pairing state storage
Stored under `~/.openclaw/devices/`:

View File

@@ -6,6 +6,8 @@ read_when:
title: "Signal"
---
# Signal (signal-cli)
Status: external CLI integration. Gateway talks to `signal-cli` over HTTP JSON-RPC + SSE.
## Prerequisites
@@ -208,7 +210,6 @@ Groups:
- Outbound text is chunked to `channels.signal.textChunkLimit` (default 4000).
- Optional newline chunking: set `channels.signal.chunkMode="newline"` to split on blank lines (paragraph boundaries) before length chunking.
- Attachments supported (base64 fetched from `signal-cli`).
- Voice-note attachments use the `signal-cli` filename as a MIME fallback when `contentType` is missing, so audio transcription can still classify AAC voice memos.
- Default media cap: `channels.signal.mediaMaxMb` (default 8).
- Use `channels.signal.ignoreAttachments` to skip downloading media.
- Group history context uses `channels.signal.historyLimit` (or `channels.signal.accounts.*.historyLimit`), falling back to `messages.groupChat.historyLimit`. Set `0` to disable (default 50).

View File

@@ -301,8 +301,8 @@ Surface different features that extend the above defaults.
},
{
"command": "/models",
"description": "List providers/models",
"usage_hint": "[provider] [page] [limit=<n>|size=<n>|all]"
"description": "List providers/models or add a model",
"usage_hint": "[provider] [page] [limit=<n>|size=<n>|all] | add <provider> <modelId>"
},
{
"command": "/help",
@@ -436,7 +436,7 @@ Available action groups in current Slack tooling:
| memberInfo | enabled |
| emojiList | enabled |
Current Slack message actions include `send`, `upload-file`, `download-file`, `read`, `edit`, `delete`, `pin`, `unpin`, `list-pins`, `member-info`, and `emoji-list`. `download-file` accepts Slack file IDs shown in inbound file placeholders and returns image previews for images or local file metadata for other file types.
Current Slack message actions include `send`, `upload-file`, `download-file`, `read`, `edit`, `delete`, `pin`, `unpin`, `list-pins`, `member-info`, and `emoji-list`.
## Access control and routing
@@ -606,7 +606,7 @@ Notes:
<AccordionGroup>
<Accordion title="Inbound attachments">
Slack file attachments are downloaded from Slack-hosted private URLs (token-authenticated request flow) and written to the media store when fetch succeeds and size limits permit. File placeholders include the Slack `fileId` so agents can fetch the original file with `download-file`.
Slack file attachments are downloaded from Slack-hosted private URLs (token-authenticated request flow) and written to the media store when fetch succeeds and size limits permit.
Runtime inbound size cap defaults to `20MB` unless overridden by `channels.slack.mediaMaxMb`.
@@ -773,8 +773,7 @@ Same-chat `/approve` also works in Slack channels and DMs that already support c
## Events and operational behavior
- Message edits/deletes are mapped into system events.
- Thread broadcasts ("Also send to channel" thread replies) are processed as normal user messages.
- Message edits/deletes/thread broadcasts are mapped into system events.
- Reaction add/remove events are mapped into system events.
- Member join/leave, channel created/renamed, and pin add/remove events are mapped into system events.
- `channel_id_changed` can migrate channel config keys when `configWrites` is enabled.
@@ -827,9 +826,6 @@ openclaw doctor
- `channels.slack.dm.enabled`
- `channels.slack.dmPolicy` (or legacy `channels.slack.dm.policy`)
- pairing approvals / allowlist entries
- Slack Assistant DM events: verbose logs mentioning `drop message_changed`
usually mean Slack sent an edited Assistant-thread event without a
recoverable human sender in message metadata
```bash
openclaw pairing list slack

View File

@@ -273,28 +273,9 @@ curl "https://api.telegram.org/bot<bot_token>/getUpdates"
- `channels.telegram.streaming` is `off | partial | block | progress` (default: `partial`)
- `progress` maps to `partial` on Telegram (compat with cross-channel naming)
- `streaming.preview.toolProgress` controls whether tool/progress updates reuse the same edited preview message (default: `true` when preview streaming is active)
- `streaming.preview.toolProgress` controls whether tool/progress updates reuse the same edited preview message (default: `true`). Set `false` to keep separate tool/progress messages.
- legacy `channels.telegram.streamMode` and boolean `streaming` values are auto-mapped
Tool-progress preview updates are the short "Working..." lines shown while tools run, for example command execution, file reads, planning updates, or patch summaries. Telegram keeps these enabled by default to match released OpenClaw behavior from `v2026.4.22` and later. To keep the edited preview for answer text but hide tool-progress lines, set:
```json
{
"channels": {
"telegram": {
"streaming": {
"mode": "partial",
"preview": {
"toolProgress": false
}
}
}
}
}
```
Use `streaming.mode: "off"` only when you want to disable Telegram preview edits entirely. Use `streaming.preview.toolProgress: false` when you only want to disable the tool-progress status lines.
For text-only replies:
- DM: OpenClaw keeps the same preview message and performs a final edit in place (no second message)

View File

@@ -64,13 +64,6 @@ openclaw channels login --channel whatsapp
```bash
openclaw channels login --channel whatsapp --account work
```
To attach an existing/custom WhatsApp Web auth directory before login:
```bash
openclaw channels add --channel whatsapp --account work --auth-dir /path/to/wa-auth
openclaw channels login --channel whatsapp --account work
```
</Step>
@@ -152,46 +145,6 @@ OpenClaw recommends running WhatsApp on a separate number when possible. (The ch
- Group sessions are isolated (`agent:<agentId>:whatsapp:group:<jid>`).
- WhatsApp Web transport honors standard proxy environment variables on the gateway host (`HTTPS_PROXY`, `HTTP_PROXY`, `NO_PROXY` / lowercase variants). Prefer host-level proxy config over channel-specific WhatsApp proxy settings.
## Plugin hooks and privacy
WhatsApp inbound messages can contain personal message content, phone numbers,
group identifiers, sender names, and session correlation fields. For that reason,
WhatsApp does not broadcast inbound `message_received` hook payloads to plugins
unless you explicitly opt in:
```json5
{
channels: {
whatsapp: {
pluginHooks: {
messageReceived: true,
},
},
},
}
```
You can scope the opt-in to one account:
```json5
{
channels: {
whatsapp: {
accounts: {
work: {
pluginHooks: {
messageReceived: true,
},
},
},
},
},
}
```
Only enable this for plugins you trust to receive inbound WhatsApp message
content and identifiers.
## Access control and activation
<Tabs>
@@ -211,7 +164,7 @@ content and identifiers.
- pairings are persisted in channel allow-store and merged with configured `allowFrom`
- if no allowlist is configured, the linked self number is allowed by default
- OpenClaw never auto-pairs outbound `fromMe` DMs (messages you send to yourself from the linked device)
- outbound `fromMe` DMs are never auto-paired
</Tab>
@@ -361,7 +314,6 @@ When the linked self number is also present in `allowFrom`, WhatsApp self-chat s
<Accordion title="Outbound media behavior">
- supports image, video, audio (PTT voice-note), and document payloads
- reply payloads preserve `audioAsVoice`; WhatsApp sends audio media as Baileys PTT voice notes
- `audio/ogg` is rewritten to `audio/ogg; codecs=opus` for voice-note compatibility
- animated GIF playback is supported via `gifPlayback: true` on video sends
- captions are applied to the first media item when sending multi-media reply payloads
@@ -381,20 +333,19 @@ When the linked self number is also present in `allowFrom`, WhatsApp self-chat s
WhatsApp supports native reply quoting, where outbound replies visibly quote the inbound message. Control it with `channels.whatsapp.replyToMode`.
| Value | Behavior |
| ----------- | --------------------------------------------------------------------- |
| `"off"` | Never quote; send as a plain message |
| `"first"` | Quote only the first outbound reply chunk |
| `"all"` | Quote every outbound reply chunk |
| `"batched"` | Quote queued batched replies while leaving immediate replies unquoted |
| Value | Behavior |
| -------- | ---------------------------------------------------------------------------------- |
| `"auto"` | Quote the inbound message when the provider supports it; skip quoting otherwise |
| `"on"` | Always quote the inbound message; fall back to a plain send if quoting is rejected |
| `"off"` | Never quote; send as a plain message |
Default is `"off"`. Per-account overrides use `channels.whatsapp.accounts.<id>.replyToMode`.
Default is `"auto"`. Per-account overrides use `channels.whatsapp.accounts.<id>.replyToMode`.
```json5
{
channels: {
whatsapp: {
replyToMode: "first",
replyToMode: "on",
},
},
}
@@ -542,15 +493,15 @@ Resolution hierarchy for group messages:
The effective `groups` map is determined first: if the account defines its own `groups`, it fully replaces the root `groups` map (no deep merge). Prompt lookup then runs on the resulting single map:
1. **Group-specific system prompt** (`groups["<groupId>"].systemPrompt`): used when the specific group entry exists in the map **and** its `systemPrompt` key is defined. If `systemPrompt` is an empty string (`""`), the wildcard is suppressed and no system prompt is applied.
2. **Group wildcard system prompt** (`groups["*"].systemPrompt`): used when the specific group entry is absent from the map entirely, or when it exists but defines no `systemPrompt` key.
1. **Group-specific system prompt** (`groups["<groupId>"].systemPrompt`): used if the specific group entry defines a `systemPrompt`.
2. **Group wildcard system prompt** (`groups["*"].systemPrompt`): used when the specific group entry is absent or defines no `systemPrompt`.
Resolution hierarchy for direct messages:
The effective `direct` map is determined first: if the account defines its own `direct`, it fully replaces the root `direct` map (no deep merge). Prompt lookup then runs on the resulting single map:
1. **Direct-specific system prompt** (`direct["<peerId>"].systemPrompt`): used when the specific peer entry exists in the map **and** its `systemPrompt` key is defined. If `systemPrompt` is an empty string (`""`), the wildcard is suppressed and no system prompt is applied.
2. **Direct wildcard system prompt** (`direct["*"].systemPrompt`): used when the specific peer entry is absent from the map entirely, or when it exists but defines no `systemPrompt` key.
1. **Direct-specific system prompt** (`direct["<peerId>"].systemPrompt`): used if the specific peer entry defines a `systemPrompt`.
2. **Direct wildcard system prompt** (`direct["*"].systemPrompt`): used when the specific peer entry is absent or defines no `systemPrompt`.
Note: `dms` remains the lightweight per-DM history override bucket (`dms.<id>.historyLimit`); prompt overrides live under `direct`.

View File

@@ -5,6 +5,8 @@ read_when:
title: "Zalo"
---
# Zalo (Bot API)
Status: experimental. DMs are supported. The [Capabilities](#capabilities) section below reflects current Marketplace-bot behavior.
## Bundled plugin

View File

@@ -6,6 +6,8 @@ read_when:
title: "Zalo personal"
---
# Zalo Personal (unofficial)
Status: experimental. This integration automates a **personal Zalo account** via native `zca-js` inside OpenClaw.
> **Warning:** This is an unofficial integration and may result in account suspension/ban. Use at your own risk.

View File

@@ -79,7 +79,7 @@ gh workflow run duplicate-after-merge.yml \
| `android` | Android unit tests for both flavors plus one debug APK build | Android-relevant changes |
| `test-performance-agent` | Daily Codex slow-test optimization after trusted activity | Main CI success or manual dispatch |
## Fail-fast order
## Fail-Fast Order
Jobs are ordered so cheap checks fail before expensive ones run:
@@ -90,15 +90,14 @@ Jobs are ordered so cheap checks fail before expensive ones run:
Scope logic lives in `scripts/ci-changed-scope.mjs` and is covered by unit tests in `src/scripts/ci-changed-scope.test.ts`.
CI workflow edits validate the Node CI graph plus workflow linting, but do not force Windows, Android, or macOS native builds by themselves; those platform lanes stay scoped to platform source changes.
CI routing-only edits, selected cheap core-test fixture edits, and narrow plugin contract helper/test-routing edits use a fast Node-only manifest path: preflight, security, and a single `checks-fast-core` task. That path avoids build artifacts, Node 22 compatibility, channel contracts, full core shards, bundled-plugin shards, and additional guard matrices when the changed files are limited to the routing or helper surfaces that the fast task exercises directly.
Windows Node checks are scoped to Windows-specific process/path wrappers, npm/pnpm/UI runner helpers, package manager config, and the CI workflow surfaces that execute that lane; unrelated source, plugin, install-smoke, and test-only changes stay on the Linux Node lanes so they do not reserve a 16-vCPU Windows worker for coverage that is already exercised by the normal test shards.
The separate `install-smoke` workflow reuses the same scope script through its own `preflight` job. It splits smoke coverage into `run_fast_install_smoke` and `run_full_install_smoke`. Pull requests run the fast path for Docker/package surfaces, bundled plugin package/manifest changes, and core plugin/channel/gateway/Plugin SDK surfaces that the Docker smoke jobs exercise. Source-only bundled plugin changes, test-only edits, and docs-only edits do not reserve Docker workers. The fast path builds the root Dockerfile image once, checks the CLI, runs the agents delete shared-workspace CLI smoke, runs the container gateway-network e2e, verifies a bundled extension build arg, and runs the bounded bundled-plugin Docker profile under a 240-second aggregate command timeout with each scenario's Docker run capped separately. The full path keeps QR package install and installer Docker/update coverage for nightly scheduled runs, manual dispatches, workflow-call release checks, and pull requests that truly touch installer/package/Docker surfaces. `main` pushes, including merge commits, do not force the full path; when changed-scope logic would request full coverage on a push, the workflow keeps the fast Docker smoke and leaves the full install smoke to nightly or release validation. The slow Bun global install image-provider smoke is separately gated by `run_bun_global_install_smoke`; it runs on the nightly schedule and from the release checks workflow, and manual `install-smoke` dispatches can opt into it, but pull requests and `main` pushes do not run it. QR and installer Docker tests keep their own install-focused Dockerfiles. Local `test:docker:all` prebuilds one shared live-test image and one shared `scripts/e2e/Dockerfile` built-app image, then runs the live/E2E smoke lanes with a weighted scheduler and `OPENCLAW_SKIP_DOCKER_BUILD=1`; tune the default main-pool slot count of 10 with `OPENCLAW_DOCKER_ALL_PARALLELISM` and the provider-sensitive tail-pool slot count of 10 with `OPENCLAW_DOCKER_ALL_TAIL_PARALLELISM`. Heavy lane caps default to `OPENCLAW_DOCKER_ALL_LIVE_LIMIT=6`, `OPENCLAW_DOCKER_ALL_NPM_LIMIT=8`, and `OPENCLAW_DOCKER_ALL_SERVICE_LIMIT=7` so npm install and multi-service lanes do not overcommit Docker while lighter lanes still fill available slots. Lane starts are staggered by 2 seconds by default to avoid local Docker daemon create storms; override with `OPENCLAW_DOCKER_ALL_START_STAGGER_MS=0` or another millisecond value. The local aggregate preflights Docker, removes stale OpenClaw E2E containers, emits active-lane status, persists lane timings for longest-first ordering, and supports `OPENCLAW_DOCKER_ALL_DRY_RUN=1` for scheduler inspection. It stops scheduling new pooled lanes after the first failure by default, and each lane has a 120-minute fallback timeout overrideable with `OPENCLAW_DOCKER_ALL_LANE_TIMEOUT_MS`; selected live/tail lanes use tighter per-lane caps. The reusable live/E2E workflow mirrors the shared-image pattern by building and pushing one SHA-tagged GHCR Docker E2E image before the Docker matrix, then running the matrix with `OPENCLAW_SKIP_DOCKER_BUILD=1`. The scheduled live/E2E workflow runs the full release-path Docker suite daily. The bundled update matrix is split by update target so repeated npm update and doctor repair passes can shard with other bundled checks.
The separate `install-smoke` workflow reuses the same scope script through its own `preflight` job. It splits smoke coverage into `run_fast_install_smoke` and `run_full_install_smoke`. Pull requests run the fast path for Docker/package surfaces, bundled plugin package/manifest changes, and core plugin/channel/gateway/Plugin SDK surfaces that the Docker smoke jobs exercise. Source-only bundled plugin changes, test-only edits, and docs-only edits do not reserve Docker workers. The fast path builds the root Dockerfile image once, checks the CLI, runs the container gateway-network e2e, verifies a bundled extension build arg, and runs the bounded bundled-plugin Docker profile under a 120-second command timeout. The full path keeps QR package install and installer Docker/update coverage for nightly scheduled runs, manual dispatches, workflow-call release checks, and pull requests that truly touch installer/package/Docker surfaces. `main` pushes, including merge commits, do not force the full path; when changed-scope logic would request full coverage on a push, the workflow keeps the fast Docker smoke and leaves the full install smoke to nightly or release validation. The slow Bun global install image-provider smoke is separately gated by `run_bun_global_install_smoke`; it runs on the nightly schedule and from the release checks workflow, and manual `install-smoke` dispatches can opt into it, but pull requests and `main` pushes do not run it. QR and installer Docker tests keep their own install-focused Dockerfiles. Local `test:docker:all` prebuilds one shared live-test image and one shared `scripts/e2e/Dockerfile` built-app image, then runs the live/E2E smoke lanes in parallel with `OPENCLAW_SKIP_DOCKER_BUILD=1`; tune the default concurrency of 4 with `OPENCLAW_DOCKER_ALL_PARALLELISM`. The local aggregate stops scheduling new pooled lanes after the first failure by default, and each lane has a 120-minute timeout overrideable with `OPENCLAW_DOCKER_ALL_LANE_TIMEOUT_MS`. Startup- or provider-sensitive lanes run exclusively after the parallel pool. The reusable live/E2E workflow mirrors the shared-image pattern by building and pushing one SHA-tagged GHCR Docker E2E image before the Docker matrix, then running the matrix with `OPENCLAW_SKIP_DOCKER_BUILD=1`. The scheduled live/E2E workflow runs the full release-path Docker suite daily. The full bundled update/channel matrix remains manual/full-suite because it performs repeated real npm update and doctor repair passes.
Local changed-lane logic lives in `scripts/changed-lanes.mjs` and is executed by `scripts/check-changed.mjs`. That local gate is stricter about architecture boundaries than the broad CI platform scope: core production changes run core prod typecheck plus core tests, core test-only changes run only core test typecheck/tests, extension production changes run extension prod typecheck plus extension tests, and extension test-only changes run only extension test typecheck/tests. Public Plugin SDK or plugin-contract changes expand to extension validation because extensions depend on those core contracts. Release metadata-only version bumps run targeted version/config/root-dependency checks. Unknown root/config changes fail safe to all lanes.
On pushes, the `checks` matrix adds the push-only `compat-node22` lane. On pull requests, that lane is skipped and the matrix stays focused on the normal test/channel lanes.
The slowest Node test families are split or balanced so each job stays small without over-reserving runners: channel contracts run as three weighted shards, bundled plugin tests balance across six extension workers, small core unit lanes are paired, auto-reply runs as three balanced workers instead of six tiny workers, and agentic gateway/plugin configs are spread across the existing source-only agentic Node jobs instead of waiting on built artifacts. Broad browser, QA, media, and miscellaneous plugin tests use their dedicated Vitest configs instead of the shared plugin catch-all. Extension shard jobs run up to two plugin config groups at a time with one Vitest worker per group and a larger Node heap so import-heavy plugin batches do not create extra CI jobs. The broad agents lane uses the shared Vitest file-parallel scheduler because it is import/scheduling dominated rather than owned by a single slow test file. `runtime-config` runs with the infra core-runtime shard to keep the shared runtime shard from owning the tail. `check-additional` keeps package-boundary compile/canary work together and separates runtime topology architecture from gateway watch coverage; the boundary guard shard runs its small independent guards concurrently inside one job. Gateway watch, channel tests, and the core support-boundary shard run concurrently inside `build-artifacts` after `dist/` and `dist-runtime/` are already built, keeping their old check names as lightweight verifier jobs while avoiding two extra Blacksmith workers and a second artifact-consumer queue.
The slowest Node test families are split or balanced so each job stays small without over-reserving runners: channel contracts run as three weighted shards, bundled plugin tests balance across six extension workers, small core unit lanes are paired, auto-reply runs as three balanced workers instead of six tiny workers, and agentic gateway/plugin configs are spread across the existing source-only agentic Node jobs instead of waiting on built artifacts. Broad browser, QA, media, and miscellaneous plugin tests use their dedicated Vitest configs instead of the shared plugin catch-all. Extension shard jobs run plugin config groups serially with one Vitest worker and a larger Node heap so import-heavy plugin batches do not overcommit small CI runners. The broad agents lane uses the shared Vitest file-parallel scheduler because it is import/scheduling dominated rather than owned by a single slow test file. `runtime-config` runs with the infra core-runtime shard to keep the shared runtime shard from owning the tail. `check-additional` keeps package-boundary compile/canary work together and separates runtime topology architecture from gateway watch coverage; the boundary guard shard runs its small independent guards concurrently inside one job. Gateway watch, channel tests, and the core support-boundary shard run concurrently inside `build-artifacts` after `dist/` and `dist-runtime/` are already built, keeping their old check names as lightweight verifier jobs while avoiding two extra Blacksmith workers and a second artifact-consumer queue.
Android CI runs both `testPlayDebugUnitTest` and `testThirdPartyDebugUnitTest`, then builds the Play debug APK. The third-party flavor has no separate source set or manifest; its unit-test lane still compiles that flavor with the SMS/call-log BuildConfig flags, while avoiding a duplicate debug APK packaging job on every Android-relevant push.
`extension-fast` is PR-only because push runs already execute the full bundled plugin shards. That keeps changed-plugin feedback for reviews without reserving an extra Blacksmith worker on `main` for coverage already present in `checks-node-extensions`.
@@ -137,8 +136,3 @@ node scripts/ci-run-timings.mjs --recent 10 # compare recent successful main C
pnpm test:perf:groups --full-suite --allow-failures --output .artifacts/test-perf/baseline-before.json
pnpm test:perf:groups:compare .artifacts/test-perf/baseline-before.json .artifacts/test-perf/after-agent.json
```
## Related
- [Install overview](/install)
- [Release channels](/install/development-channels)

View File

@@ -314,8 +314,3 @@ Security note:
- `--server-args <args...>`: extra arguments passed to the ACP server.
- `--server-verbose`: enable verbose logging on the ACP server.
- `--verbose, -v`: verbose client logging.
## Related
- [CLI reference](/cli)
- [ACP agents](/tools/acp-agents)

View File

@@ -53,7 +53,6 @@ openclaw agent --agent ops --message "Run locally" --local
- Gateway mode falls back to the embedded agent when the Gateway request fails. Use `--local` to force embedded execution up front.
- `--local` still preloads the plugin registry first, so plugin-provided providers, tools, and channels stay available during embedded runs.
- `--channel`, `--reply-channel`, and `--reply-account` affect reply delivery, not session routing.
- `--json` keeps stdout reserved for the JSON response. Gateway, plugin, and embedded-fallback diagnostics are routed to stderr so scripts can parse stdout directly.
- When this command triggers `models.json` regeneration, SecretRef-managed provider credentials are persisted as non-secret markers (for example env var names, `secretref-env:ENV_VAR_NAME`, or `secretref-managed`), not resolved secret plaintext.
- Marker writes are source-authoritative: OpenClaw persists markers from the active source config snapshot, not from resolved runtime secret values.

View File

@@ -150,9 +150,6 @@ Notes:
- `main` cannot be deleted.
- Without `--force`, interactive confirmation is required.
- Workspace, agent state, and session transcript directories are moved to Trash, not hard-deleted.
- If another agent's workspace is the same path, inside this workspace, or contains this workspace,
the workspace is retained and `--json` reports `workspaceRetained`,
`workspaceRetainedReason`, and `workspaceSharedWith`.
## Identity files

View File

@@ -82,7 +82,3 @@ Practical limits come from the local machine and destination filesystem:
Large workspaces are usually the main driver of archive size. If you want a smaller or faster backup, use `--no-include-workspace`.
For the smallest archive, use `--only-config`.
## Related
- [CLI reference](/cli)

View File

@@ -33,8 +33,6 @@ openclaw browser --browser-profile openclaw open https://example.com
openclaw browser --browser-profile openclaw snapshot
```
Agents can run the same readiness check with `browser({ action: "doctor" })`.
## Quick troubleshooting
If `start` fails with `not reachable after start`, troubleshoot CDP readiness first. If `start` and `tabs` succeed but `open` or `navigate` fails, the browser control plane is healthy and the failure is usually navigation SSRF policy.
@@ -42,7 +40,6 @@ If `start` fails with `not reachable after start`, troubleshoot CDP readiness fi
Minimal sequence:
```bash
openclaw browser --browser-profile openclaw doctor
openclaw browser --browser-profile openclaw start
openclaw browser --browser-profile openclaw tabs
openclaw browser --browser-profile openclaw open https://example.com
@@ -54,7 +51,6 @@ Detailed guidance: [Browser troubleshooting](/tools/browser#cdp-startup-failure-
```bash
openclaw browser status
openclaw browser doctor
openclaw browser start
openclaw browser stop
openclaw browser --browser-profile openclaw reset-profile
@@ -115,28 +111,20 @@ openclaw browser --browser-profile work tabs
```bash
openclaw browser tabs
openclaw browser tab new --label docs
openclaw browser tab label t1 docs
openclaw browser tab new
openclaw browser tab select 2
openclaw browser tab close 2
openclaw browser open https://docs.openclaw.ai --label docs
openclaw browser focus docs
openclaw browser close t1
openclaw browser open https://docs.openclaw.ai
openclaw browser focus <targetId>
openclaw browser close <targetId>
```
`tabs` returns `suggestedTargetId` first, then the stable `tabId` such as `t1`,
the optional label, and the raw `targetId`. Agents should pass
`suggestedTargetId` back into `focus`, `close`, snapshots, and actions. You can
assign a label with `open --label`, `tab new --label`, or `tab label`; labels,
tab ids, raw target ids, and unique target-id prefixes are all accepted.
## Snapshot / screenshot / actions
Snapshot:
```bash
openclaw browser snapshot
openclaw browser snapshot --urls
```
Screenshot:
@@ -145,7 +133,6 @@ Screenshot:
openclaw browser screenshot
openclaw browser screenshot --full-page
openclaw browser screenshot --ref e12
openclaw browser screenshot --labels
```
Notes:
@@ -154,17 +141,12 @@ 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.
- `snapshot --urls` appends discovered link destinations to AI snapshots so
agents can choose direct navigation targets instead of guessing from link
text alone.
Navigate/click/type (ref-based UI automation):
```bash
openclaw browser navigate https://example.com
openclaw browser click <ref>
openclaw browser click-coords 120 340
openclaw browser type <ref> "hello"
openclaw browser press Enter
openclaw browser hover <ref>
@@ -263,8 +245,3 @@ If the Gateway runs on a different machine than the browser, run a **node host**
Use `gateway.nodes.browser.mode` to control auto-routing and `gateway.nodes.browser.node` to pin a specific node if multiple are connected.
Security + remote setup: [Browser tool](/tools/browser), [Remote access](/gateway/remote), [Tailscale](/gateway/tailscale), [Security](/gateway/security)
## Related
- [CLI reference](/cli)
- [Browser](/tools/browser)

View File

@@ -19,7 +19,3 @@ Current supported alias:
Prefer modern top-level commands directly:
- `openclaw clawbot qr` -> `openclaw qr`
## Related
- [CLI reference](/cli)

View File

@@ -33,7 +33,3 @@ openclaw completion --shell bash --write-state
- `--install` writes a small "OpenClaw Completion" block into your shell profile and points it at the cached script.
- Without `--install` or `--write-state`, the command prints the script to stdout.
- Completion generation eagerly loads command trees so nested subcommands are included.
## Related
- [CLI reference](/cli)

View File

@@ -36,7 +36,6 @@ openclaw config --section gateway --section daemon
openclaw config schema
openclaw config get browser.executablePath
openclaw config set browser.executablePath "/usr/bin/google-chrome"
openclaw config set browser.profiles.work.executablePath "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
openclaw config set agents.defaults.heartbeat.every "2h"
openclaw config set agents.list[0].tools.exec.node "node-id-or-name"
openclaw config set agents.defaults.models '{"openai/gpt-5.4":{}}' --strict-json --merge
@@ -186,7 +185,7 @@ openclaw config set secrets.providers.vaultfile \
--strict-json
```
## Provider builder flags
## Provider Builder Flags
Provider builder targets must use `secrets.providers.<alias>` as the path.
@@ -279,7 +278,7 @@ Dry-run behavior:
- `skippedExecRefs`: number of exec refs skipped because `--allow-exec` was not set
- `errors`: structured schema/resolvability failures when `ok=false`
### JSON output shape
### JSON Output Shape
```json5
{
@@ -386,15 +385,6 @@ untrusted until they validate. Invalid direct edits can be restored from the
last-known-good backup during startup or hot reload. See
[Gateway troubleshooting](/gateway/troubleshooting#gateway-restored-last-known-good-config).
Whole-file recovery is reserved for globally broken config, such as parse
errors, root-level schema failures, legacy migration failures, or mixed plugin
and root failures. If validation fails only under `plugins.entries.<id>...`,
OpenClaw keeps the active `openclaw.json` in place and reports the plugin-local
issue instead of restoring `.last-good`. This prevents plugin schema changes or
`minHostVersion` skew from rolling back unrelated user settings such as models,
providers, auth profiles, channels, gateway exposure, tools, memory, browser, or
cron config.
## Subcommands
- `config file`: Print the active config file path (resolved from `OPENCLAW_CONFIG_PATH` or default location). The path should name a regular file, not a symlink.

View File

@@ -13,9 +13,6 @@ Note: The **Model** section now includes a multi-select for the
`agents.defaults.models` allowlist (what shows up in `/model` and the model picker).
Provider-scoped setup choices merge their selected models into the existing
allowlist instead of replacing unrelated providers already in the config.
Re-running provider auth from configure preserves an existing
`agents.defaults.model.primary`; use `openclaw models auth login --provider <id> --set-default`
or `openclaw models set <model>` when you intentionally want to change the default model.
When configure starts from a provider auth choice, the default-model and
allowlist pickers prefer that provider automatically. For paired providers such
@@ -73,8 +70,3 @@ openclaw configure --section web
openclaw configure --section model --section channels
openclaw configure --section gateway --section daemon
```
## Related
- [CLI reference](/cli)
- [Configuration](/gateway/configuration)

View File

@@ -33,11 +33,6 @@ Note: `--session` supports `main`, `isolated`, `current`, and `session:<id>`.
Use `current` to bind to the active session at creation time, or `session:<id>` for
an explicit persistent session key.
Note: `--session isolated` creates a fresh transcript/session id for each run.
Safe preferences and explicit user-selected model/auth overrides can carry, but
ambient conversation context does not: channel/group routing, send/queue policy,
elevation, origin, and ACP runtime binding are reset for the new isolated run.
Note: for one-shot CLI jobs, offset-less `--at` datetimes are treated as UTC unless you also pass
`--tz <iana>`, which interprets that local wall-clock time in the given timezone.
@@ -64,17 +59,17 @@ model override with no explicit per-job fallback list no longer appends the
agent primary as a hidden extra retry target.
Note: isolated cron model precedence is Gmail-hook override first, then per-job
`--model`, then any user-selected stored cron-session model override, then the
normal agent/default selection.
`--model`, then any stored cron-session model override, then the normal
agent/default selection.
Note: isolated cron fast mode follows the resolved live model selection. Model
config `params.fastMode` applies by default, but a stored session `fastMode`
override still wins over config.
Note: if an isolated run throws `LiveSessionModelSwitchError`, cron persists the
switched provider/model (and switched auth profile override when present) for
the active run before retrying. The outer retry loop is bounded to 2 switch
retries after the initial attempt, then aborts instead of looping forever.
switched provider/model (and switched auth profile override when present) before
retrying. The outer retry loop is bounded to 2 switch retries after the initial
attempt, then aborts instead of looping forever.
Note: failure notifications use `delivery.failureDestination` first, then
global `cron.failureDestination`, and finally fall back to the job's primary
@@ -179,8 +174,3 @@ Failure-delivery note:
delivery mode is `webhook`.
- If you do not set any failure destination and the job already announces to a
channel, failure notifications reuse that same announce target.
## Related
- [CLI reference](/cli)
- [Scheduled tasks](/automation/cron-jobs)

View File

@@ -55,8 +55,3 @@ Notes:
## Prefer
Use [`openclaw gateway`](/cli/gateway) for current docs and examples.
## Related
- [CLI reference](/cli)
- [Gateway runbook](/gateway)

View File

@@ -20,8 +20,3 @@ Notes:
- `dashboard` resolves configured `gateway.auth.token` SecretRefs when possible.
- For SecretRef-managed tokens (resolved or unresolved), `dashboard` prints/copies/opens a non-tokenized URL to avoid exposing external secrets in terminal output, clipboard history, or browser-launch arguments.
- If `gateway.auth.token` is SecretRef-managed but unresolved in this command path, the command prints a non-tokenized URL and explicit remediation guidance instead of embedding an invalid token placeholder.
## Related
- [CLI reference](/cli)
- [Dashboard](/web/dashboard)

View File

@@ -66,12 +66,6 @@ request. Review the `Requested` vs `Approved` columns in `openclaw devices list`
or use `openclaw devices approve --latest` to preview the exact upgrade before
approving it.
If the Gateway is explicitly configured with
`gateway.nodes.pairing.autoApproveCidrs`, first-time `role: node` requests from
matching client IPs can be approved before they appear in this list. That policy
is disabled by default and never applies to operator/browser clients or upgrade
requests.
```
openclaw devices approve
openclaw devices approve <requestId>
@@ -133,8 +127,6 @@ Pass `--token` or `--password` explicitly. Missing explicit credentials is an er
- Token rotation returns a new token (sensitive). Treat it like a secret.
- These commands require `operator.pairing` (or `operator.admin`) scope.
- `gateway.nodes.pairing.autoApproveCidrs` is an opt-in Gateway policy for
fresh node device pairing only; it does not change CLI approval authority.
- Token rotation stays inside the approved pairing role set and approved scope
baseline for that device. A stray cached token entry does not grant a new
rotate target.
@@ -186,8 +178,3 @@ Related:
- [Dashboard auth troubleshooting](/web/dashboard#if-you-see-unauthorized-1008)
- [Gateway troubleshooting](/gateway/troubleshooting#dashboard-control-ui-connectivity)
## Related
- [CLI reference](/cli)
- [Nodes](/nodes)

View File

@@ -61,7 +61,3 @@ openclaw directory groups list --channel zalouser
openclaw directory groups list --channel zalouser --query "work"
openclaw directory groups members --channel zalouser --group-id <id>
```
## Related
- [CLI reference](/cli)

View File

@@ -46,8 +46,3 @@ Notes:
- If `--domain` is omitted, OpenClaw uses `discovery.wideArea.domain` from config.
- `--apply` currently supports macOS only and expects Homebrew CoreDNS.
- `--apply` bootstraps the zone file if needed, ensures the CoreDNS import stanza exists, and restarts the `coredns` brew service.
## Related
- [CLI reference](/cli)
- [Discovery](/gateway/discovery)

View File

@@ -26,7 +26,3 @@ Notes:
- With no query, `openclaw docs` opens the live docs search entrypoint.
- Multi-word queries are passed through as one search request.
## Related
- [CLI reference](/cli)

View File

@@ -43,7 +43,7 @@ Notes:
- `--fix` (alias for `--repair`) writes a backup to `~/.openclaw/openclaw.json.bak` and drops unknown config keys, listing each removal.
- State integrity checks now detect orphan transcript files in the sessions directory and can archive them as `.deleted.<timestamp>` to reclaim space safely.
- Doctor also scans `~/.openclaw/cron/jobs.json` (or `cron.store`) for legacy cron job shapes and can rewrite them in place before the scheduler has to auto-normalize them at runtime.
- Doctor repairs missing bundled plugin runtime dependencies without writing into packaged global installs. For root-owned npm installs or hardened systemd units, set `OPENCLAW_PLUGIN_STAGE_DIR` to a writable directory such as `/var/lib/openclaw/plugin-runtime-deps`.
- Doctor repairs missing bundled plugin runtime dependencies without requiring write access to the installed OpenClaw package. For root-owned npm installs or hardened systemd units, set `OPENCLAW_PLUGIN_STAGE_DIR` to a writable directory such as `/var/lib/openclaw/plugin-runtime-deps`.
- Doctor auto-migrates legacy flat Talk config (`talk.voiceId`, `talk.modelId`, and friends) into `talk.provider` + `talk.providers.<provider>`.
- Repeat `doctor --fix` runs no longer report/apply Talk normalization when the only difference is object key order.
- Doctor includes a memory-search readiness check and can recommend `openclaw configure --section model` when embedding credentials are missing.
@@ -63,8 +63,3 @@ launchctl getenv OPENCLAW_GATEWAY_PASSWORD
launchctl unsetenv OPENCLAW_GATEWAY_TOKEN
launchctl unsetenv OPENCLAW_GATEWAY_PASSWORD
```
## Related
- [CLI reference](/cli)
- [Gateway doctor](/gateway/doctor)

View File

@@ -16,8 +16,3 @@ openclaw tasks flow cancel <lookup>
```
For full documentation see [Task Flow](/automation/taskflow) and the [tasks CLI reference](/cli/tasks).
## Related
- [CLI reference](/cli)
- [Automation](/automation)

View File

@@ -371,8 +371,3 @@ Notes:
- On `local.` mDNS, `sshPort` and `cliPath` are only broadcast when
`discovery.mdns.mode` is `full`. Wide-area DNS-SD still writes `cliPath`; `sshPort`
stays optional there too.
## Related
- [CLI reference](/cli)
- [Gateway runbook](/gateway)

View File

@@ -34,8 +34,3 @@ Notes:
- `--verbose` forces a live probe, prints gateway connection details, and expands the
human-readable output across all configured accounts and agents.
- Output includes per-agent session stores when multiple agents are configured.
## Related
- [CLI reference](/cli)
- [Gateway health](/gateway/health)

View File

@@ -15,7 +15,7 @@ Running `openclaw hooks` with no subcommand is equivalent to `openclaw hooks lis
Related:
- Hooks: [Hooks](/automation/hooks)
- Plugin hooks: [Plugin hooks](/plugins/hooks)
- Plugin hooks: [Plugin hooks](/plugins/architecture-internals#provider-runtime-hooks)
## List All Hooks

View File

@@ -47,11 +47,6 @@ Benefits:
- Prefer a first-party OpenClaw surface when the task is fundamentally "run inference."
- Use the normal local path without requiring the gateway for most infer commands.
For end-to-end provider checks, prefer `openclaw infer ...` once lower-level
provider tests are green. It exercises the shipped CLI, config loading,
default-agent resolution, bundled plugin activation, runtime-dependency repair,
and the shared capability runtime before the provider request is made.
## Command tree
```text
@@ -162,25 +157,6 @@ openclaw infer image describe --file ./photo.jpg --model ollama/qwen2.5vl:7b --j
Notes:
- Use `image edit` when starting from existing input files.
- Use `image providers --json` to verify which bundled image providers are
discoverable, configured, selected, and which generation/edit capabilities
each provider exposes.
- Use `image generate --model <provider/model> --json` as the narrowest live
CLI smoke for image generation changes. Example:
```bash
openclaw infer image providers --json
openclaw infer image generate \
--model google/gemini-3.1-flash-image-preview \
--prompt "Minimal flat test image: one blue square on a white background, no text." \
--output ./openclaw-infer-image-smoke.png \
--json
```
The JSON response reports `ok`, `provider`, `model`, `attempts`, and written
output paths. When `--output` is set, the final extension may follow the
provider's returned MIME type.
- For `image describe`, `--model` must be an image-capable `<provider/model>`.
- For local Ollama vision models, pull the model first and set `OLLAMA_API_KEY` to any placeholder value, for example `ollama-local`. See [Ollama](/providers/ollama#vision-and-image-description).
@@ -282,10 +258,6 @@ Top-level fields are stable:
- `outputs`
- `error`
For generated media commands, `outputs` contains files written by OpenClaw. Use
the `path`, `mimeType`, `size`, and any media-specific dimensions in that array
for automation instead of parsing human-readable stdout.
## Common pitfalls
```bash

View File

@@ -57,8 +57,3 @@ openclaw logs --url ws://127.0.0.1:18789 --token "$OPENCLAW_GATEWAY_TOKEN"
- Use `--local-time` to render timestamps in your local timezone.
- If the local loopback Gateway asks for pairing, `openclaw logs` falls back to the configured local log file automatically. Explicit `--url` targets do not use this fallback.
## Related
- [CLI reference](/cli)
- [Gateway logging](/gateway/logging)

View File

@@ -376,9 +376,6 @@ Important behavior:
- embedded Pi exposes configured MCP tools in normal `coding` and `messaging`
tool profiles; `minimal` still hides them, and `tools.deny: ["bundle-mcp"]`
disables them explicitly
- session-scoped bundled MCP runtimes are reaped after `mcp.sessionIdleTtlMs`
milliseconds of idle time (default 10 minutes; set `0` to disable) and
one-shot embedded runs clean them up at run end
## Saved MCP server definitions

View File

@@ -174,8 +174,3 @@ Notes:
- `memory rem-backfill --path <file-or-dir> --stage-short-term` also seeds grounded durable candidates into the live short-term promotion store so the normal deep phase can rank them.
- `memory rem-backfill --rollback` removes previously written grounded diary entries, and `memory rem-backfill --rollback-short-term` removes previously staged grounded short-term candidates.
- See [Dreaming](/concepts/dreaming) for full phase descriptions and configuration reference.
## Related
- [CLI reference](/cli)
- [Memory overview](/concepts/memory)

View File

@@ -296,8 +296,3 @@ Send a Telegram image as a document to avoid compression:
openclaw message send --channel telegram --target @mychat \
--media ./diagram.png --force-document
```
## Related
- [CLI reference](/cli)
- [Agent send](/tools/agent-send)

View File

@@ -50,10 +50,6 @@ Notes:
- `models list --all` includes bundled provider-owned static catalog rows even
when you have not authenticated with that provider yet. Those rows still show
as unavailable until matching auth is configured.
- `models list` keeps native model metadata and runtime caps distinct. In table
output, `Ctx` shows `contextTokens/contextWindow` when an effective runtime
cap differs from the native context window; JSON rows include `contextTokens`
when a provider exposes that cap.
- `models list --provider <id>` filters by provider id, such as `moonshot` or
`openai-codex`. It does not accept display labels from interactive provider
pickers, such as `Moonshot AI`.

View File

@@ -74,13 +74,6 @@ Options:
- In `gateway.mode=remote`, remote client fields (`gateway.remote.token` / `gateway.remote.password`) are also eligible per remote precedence rules.
- Node host auth resolution only honors `OPENCLAW_GATEWAY_*` env vars.
For a node connecting to a non-loopback `ws://` Gateway on a trusted private
network, set `OPENCLAW_ALLOW_INSECURE_PRIVATE_WS=1`. Without it, node startup
fails closed and asks you to use `wss://`, an SSH tunnel, or Tailscale.
This is a process-environment opt-in, not an `openclaw.json` config key.
`openclaw node install` persists it into the supervised node service when it is
present in the install command environment.
## Service (background)
Install a headless node host as a user service.
@@ -123,25 +116,6 @@ openclaw devices list
openclaw devices approve <requestId>
```
On tightly controlled node networks, the Gateway operator can explicitly opt in
to auto-approving first-time node pairing from trusted CIDRs:
```json5
{
gateway: {
nodes: {
pairing: {
autoApproveCidrs: ["192.168.1.0/24"],
},
},
},
}
```
This is disabled by default. It only applies to fresh `role: node` pairing with
no requested scopes. Operator/browser clients, Control UI, WebChat, and role,
scope, metadata, or public-key upgrades still require manual approval.
If the node retries pairing with changed auth details (role/scopes/public key),
the previous pending request is superseded and a new `requestId` is created.
Run `openclaw devices list` again before approval.
@@ -161,8 +135,3 @@ For approved async node exec, OpenClaw prepares a canonical `systemRunPlan`
before prompting. The later approved `system.run` forward reuses that stored
plan, so edits to command/cwd/session fields after the approval request was
created are rejected instead of changing what the node executes.
## Related
- [CLI reference](/cli)
- [Nodes](/nodes)

View File

@@ -42,9 +42,6 @@ filter to nodes that connected within a duration (e.g. `24h`, `7d`).
Approval note:
- `openclaw nodes pending` only needs pairing scope.
- `gateway.nodes.pairing.autoApproveCidrs` can skip the pending step only for
explicitly trusted, first-time `role: node` device pairing. It is off by
default and does not approve upgrades.
- `openclaw nodes approve <requestId>` inherits extra scope requirements from the
pending request:
- commandless request: pairing only

View File

@@ -23,14 +23,11 @@ Interactive onboarding for local or remote Gateway setup.
openclaw onboard
openclaw onboard --flow quickstart
openclaw onboard --flow manual
openclaw onboard --skip-bootstrap
openclaw onboard --mode remote --remote-url wss://gateway-host:18789
```
For plaintext private-network `ws://` targets (trusted networks only), set
`OPENCLAW_ALLOW_INSECURE_PRIVATE_WS=1` in the onboarding process environment.
There is no `openclaw.json` equivalent for this client-side transport
break-glass.
Non-interactive custom provider:
@@ -116,7 +113,6 @@ Non-interactive local gateway health:
- Unless you pass `--skip-health`, onboarding waits for a reachable local gateway before it exits successfully.
- `--install-daemon` starts the managed gateway install path first. Without it, you must already have a local gateway running, for example `openclaw gateway run`.
- If you only want config/workspace/bootstrap writes in automation, use `--skip-health`.
- If you manage workspace files yourself, pass `--skip-bootstrap` to set `agents.defaults.skipBootstrap: true` and skip creating `AGENTS.md`, `SOUL.md`, `TOOLS.md`, `IDENTITY.md`, `USER.md`, `HEARTBEAT.md`, and `BOOTSTRAP.md`.
- On native Windows, `--install-daemon` tries Scheduled Tasks first and falls back to a per-user Startup-folder login item if task creation is denied.
Interactive onboarding behavior with reference mode:

View File

@@ -200,23 +200,6 @@ table view to per-plugin detail lines with source/origin/version/activation
metadata. Use `--json` for machine-readable inventory plus registry
diagnostics.
`plugins list` runs discovery from the current CLI environment and config. It is
useful for checking whether a plugin is enabled/loadable, but it is not a live
runtime probe of an already-running Gateway process. After changing plugin code,
enablement, hook policy, or `plugins.load.paths`, restart the Gateway that
serves the channel before expecting new `register(api)` code or hooks to run.
For remote/container deployments, verify you are restarting the actual
`openclaw gateway run` child, not only a wrapper process.
For runtime hook debugging:
- `openclaw plugins inspect <id> --json` shows registered hooks and diagnostics
from a module-loaded inspection pass.
- `openclaw gateway status --deep --require-rpc` confirms the reachable Gateway,
service/process hints, config path, and RPC health.
- Non-bundled conversation hooks (`llm_input`, `llm_output`, `agent_end`) require
`plugins.entries.<id>.hooks.allowConversationAccess=true`.
Use `--link` to avoid copying a local directory (adds to `plugins.load.paths`):
```bash

View File

@@ -43,8 +43,3 @@ openclaw proxy purge
- `start` defaults to `127.0.0.1` unless `--host` is set.
- `run` starts a local debug proxy and then runs the command after `--`.
- Captures are local debugging data; use `openclaw proxy purge` when finished.
## Related
- [CLI reference](/cli)
- [Trusted proxy auth](/gateway/trusted-proxy-auth)

View File

@@ -50,8 +50,3 @@ openclaw qr --url wss://gateway.example/ws
- After scanning, approve device pairing with:
- `openclaw devices list`
- `openclaw devices approve <requestId>`
## Related
- [CLI reference](/cli)
- [Pairing](/cli/pairing)

View File

@@ -33,7 +33,3 @@ Notes:
- Run `openclaw backup create` first if you want a restorable snapshot before removing local state.
- If you omit `--scope`, `openclaw reset` uses an interactive prompt to choose what to remove.
- `--non-interactive` is only valid when both `--scope` and `--yes` are set.
## Related
- [CLI reference](/cli)

View File

@@ -188,9 +188,13 @@ Sandbox settings live in `~/.openclaw/openclaw.json` under `agents.defaults.sand
}
```
## See Also
- [Sandbox Documentation](/gateway/sandboxing)
- [Agent Configuration](/concepts/agent-workspace)
- [Doctor Command](/gateway/doctor) - Check sandbox setup
## Related
- [CLI reference](/cli)
- [Sandboxing](/gateway/sandboxing)
- [Agent workspace](/concepts/agent-workspace)
- [Doctor](/gateway/doctor) — checks sandbox setup

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