mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-17 03:28:57 +08:00
Compare commits
103 Commits
codex/llm-
...
codex/exte
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
499b8ad972 | ||
|
|
ef7ae4c6db | ||
|
|
1ea8fc0db6 | ||
|
|
67ca7cc1d8 | ||
|
|
955be3c50f | ||
|
|
7c172ad97c | ||
|
|
7fa8e9748d | ||
|
|
3aaa809d63 | ||
|
|
2e80ccce38 | ||
|
|
d608a6abaf | ||
|
|
653c84bd35 | ||
|
|
daad214a3a | ||
|
|
542d17de04 | ||
|
|
a1c5cbabff | ||
|
|
4cb3600549 | ||
|
|
d9fb2cbaf8 | ||
|
|
89e02b6a89 | ||
|
|
7ed6880d0e | ||
|
|
6eb1d803dc | ||
|
|
e2635aaf00 | ||
|
|
e7ad81179d | ||
|
|
1548728a28 | ||
|
|
88cf5ef972 | ||
|
|
7802655c9b | ||
|
|
bb8349fde1 | ||
|
|
bb4681fca6 | ||
|
|
e7e59a862d | ||
|
|
354ac4185e | ||
|
|
856c885057 | ||
|
|
c8d30dc144 | ||
|
|
aa47414c95 | ||
|
|
7b779f7b3f | ||
|
|
c03d3b33e3 | ||
|
|
62b0245655 | ||
|
|
3fd9f20f5b | ||
|
|
b33e0c566b | ||
|
|
161c167e14 | ||
|
|
975c9ec32b | ||
|
|
f996804369 | ||
|
|
2ad45cf8d2 | ||
|
|
9ded1afa13 | ||
|
|
492293addc | ||
|
|
04996c60aa | ||
|
|
0064a89415 | ||
|
|
8d0c487a45 | ||
|
|
adbf2a3ddc | ||
|
|
4c22681ddb | ||
|
|
d7f201fcd7 | ||
|
|
50f2293018 | ||
|
|
639178bd16 | ||
|
|
cacd8898b2 | ||
|
|
b195ce5275 | ||
|
|
8bfe6bb03f | ||
|
|
8f22067992 | ||
|
|
1c247ebaa7 | ||
|
|
abb6dd493e | ||
|
|
02b4cb2787 | ||
|
|
ce4c0fa44e | ||
|
|
3545e89fcc | ||
|
|
af5809283a | ||
|
|
e36d2136ea | ||
|
|
a8b3b1c008 | ||
|
|
d6d28037db | ||
|
|
652a95ad41 | ||
|
|
1af9ca694f | ||
|
|
977610fbde | ||
|
|
6fb5324f73 | ||
|
|
938bbe0343 | ||
|
|
7be38f29a4 | ||
|
|
49ae3b65a5 | ||
|
|
1817c6fcf6 | ||
|
|
376acd54ed | ||
|
|
b57c45ba9c | ||
|
|
7c1d0785c4 | ||
|
|
9cbdb075f3 | ||
|
|
ae6210d789 | ||
|
|
67b11c18c4 | ||
|
|
d9981847e1 | ||
|
|
5fc9f4d33c | ||
|
|
2777895047 | ||
|
|
90ef0afe14 | ||
|
|
5b26ce7252 | ||
|
|
5de733fa80 | ||
|
|
4ca027bf2b | ||
|
|
a54eda5051 | ||
|
|
d4c20c0841 | ||
|
|
b319d651f1 | ||
|
|
03656c8f26 | ||
|
|
25fc09885e | ||
|
|
f67ed5329a | ||
|
|
b9e4fb47a4 | ||
|
|
b721840568 | ||
|
|
1c88ecda3f | ||
|
|
ed46940ad6 | ||
|
|
8775207d81 | ||
|
|
b3e2c6d516 | ||
|
|
d17c4cca02 | ||
|
|
2358a2ac9c | ||
|
|
dc4fc0f8f8 | ||
|
|
bce8b67777 | ||
|
|
032b5dee20 | ||
|
|
bcb74de2ff | ||
|
|
fb9a0383d1 |
@@ -1,62 +0,0 @@
|
||||
---
|
||||
name: parallels-discord-roundtrip
|
||||
description: Run the macOS Parallels smoke harness with Discord end-to-end roundtrip verification, including guest send, host verification, host reply, and guest readback.
|
||||
---
|
||||
|
||||
# Parallels Discord Roundtrip
|
||||
|
||||
Use when macOS Parallels smoke must prove Discord two-way delivery end to end.
|
||||
|
||||
## Goal
|
||||
|
||||
Cover:
|
||||
|
||||
- install on fresh macOS snapshot
|
||||
- onboard + gateway health
|
||||
- guest `message send` to Discord
|
||||
- host sees that message on Discord
|
||||
- host posts a new Discord message
|
||||
- guest `message read` sees that new message
|
||||
|
||||
## Inputs
|
||||
|
||||
- host env var with Discord bot token
|
||||
- Discord guild ID
|
||||
- Discord channel ID
|
||||
- `OPENAI_API_KEY`
|
||||
|
||||
## Preferred run
|
||||
|
||||
```bash
|
||||
export OPENCLAW_PARALLELS_DISCORD_TOKEN="$(
|
||||
ssh peters-mac-studio-1 'jq -r ".channels.discord.token" ~/.openclaw/openclaw.json' | tr -d '\n'
|
||||
)"
|
||||
|
||||
pnpm test:parallels:macos \
|
||||
--discord-token-env OPENCLAW_PARALLELS_DISCORD_TOKEN \
|
||||
--discord-guild-id 1456350064065904867 \
|
||||
--discord-channel-id 1456744319972282449 \
|
||||
--json
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- Snapshot target: closest to `macOS 26.3.1 fresh`.
|
||||
- Snapshot resolver now prefers matching `*-poweroff*` clones when the base hint also matches. That lets the harness reuse disk-only recovery snapshots without passing a longer hint.
|
||||
- If Windows/Linux snapshot restore logs show `PET_QUESTION_SNAPSHOT_STATE_INCOMPATIBLE_CPU`, drop the suspended state once, create a `*-poweroff*` replacement snapshot, and rerun. The smoke scripts now auto-start restored power-off snapshots.
|
||||
- Harness configures Discord inside the guest; no checked-in token/config.
|
||||
- Use the `openclaw` wrapper for guest `message send/read`; `node openclaw.mjs message ...` does not expose the lazy message subcommands the same way.
|
||||
- Write `channels.discord.guilds` in one JSON object (`--strict-json`), not dotted `config set channels.discord.guilds.<snowflake>...` paths; numeric snowflakes get treated like array indexes.
|
||||
- Avoid `prlctl enter` / expect for long Discord setup scripts; it line-wraps/corrupts long commands. Use `prlctl exec --current-user /bin/sh -lc ...` for the Discord config phase.
|
||||
- Full 3-OS sweeps: the shared build lock is safe in parallel, but snapshot restore is still a Parallels bottleneck. Prefer serialized Windows/Linux restore-heavy reruns if the host is already under load.
|
||||
- Harness cleanup deletes the temporary Discord smoke messages at exit.
|
||||
- Per-phase logs: `/tmp/openclaw-parallels-smoke.*`
|
||||
- Machine summary: pass `--json`
|
||||
- If roundtrip flakes, inspect `fresh.discord-roundtrip.log` and `discord-last-readback.json` in the run dir first.
|
||||
|
||||
## Pass criteria
|
||||
|
||||
- fresh lane or upgrade lane requested passes
|
||||
- summary reports `discord=pass` for that lane
|
||||
- guest outbound nonce appears in channel history
|
||||
- host inbound nonce appears in `openclaw message read` output
|
||||
20
.github/labeler.yml
vendored
20
.github/labeler.yml
vendored
@@ -198,6 +198,14 @@
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "extensions/diagnostics-otel/**"
|
||||
"extensions: google-antigravity-auth":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "extensions/google-antigravity-auth/**"
|
||||
"extensions: google-gemini-cli-auth":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "extensions/google-gemini-cli-auth/**"
|
||||
"extensions: llm-task":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
@@ -234,10 +242,6 @@
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "extensions/byteplus/**"
|
||||
"extensions: anthropic":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "extensions/anthropic/**"
|
||||
"extensions: cloudflare-ai-gateway":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
@@ -254,10 +258,6 @@
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "extensions/kilocode/**"
|
||||
"extensions: openai":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "extensions/openai/**"
|
||||
"extensions: kimi-coding":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
@@ -314,7 +314,3 @@
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "extensions/xiaomi/**"
|
||||
"extensions: fal":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "extensions/fal/**"
|
||||
|
||||
231
.github/workflows/ci.yml
vendored
231
.github/workflows/ci.yml
vendored
@@ -78,50 +78,6 @@ jobs:
|
||||
|
||||
node scripts/ci-changed-scope.mjs --base "$BASE" --head HEAD
|
||||
|
||||
changed-extensions:
|
||||
needs: [docs-scope, changed-scope]
|
||||
if: needs.docs-scope.outputs.docs_only != 'true' && needs.changed-scope.outputs.run_node == 'true'
|
||||
runs-on: blacksmith-16vcpu-ubuntu-2404
|
||||
outputs:
|
||||
has_changed_extensions: ${{ steps.changed.outputs.has_changed_extensions }}
|
||||
changed_extensions_matrix: ${{ steps.changed.outputs.changed_extensions_matrix }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 1
|
||||
fetch-tags: false
|
||||
submodules: false
|
||||
|
||||
- name: Ensure changed-extensions base commit
|
||||
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: Setup Node environment
|
||||
uses: ./.github/actions/setup-node-env
|
||||
with:
|
||||
install-bun: "false"
|
||||
install-deps: "false"
|
||||
use-sticky-disk: "false"
|
||||
|
||||
- name: Detect changed extensions
|
||||
id: changed
|
||||
env:
|
||||
BASE_SHA: ${{ github.event_name == 'push' && github.event.before || github.event.pull_request.base.sha }}
|
||||
run: |
|
||||
node --input-type=module <<'EOF'
|
||||
import { appendFileSync } from "node:fs";
|
||||
import { listChangedExtensionIds } from "./scripts/test-extension.mjs";
|
||||
|
||||
const extensionIds = listChangedExtensionIds({ base: process.env.BASE_SHA, head: "HEAD" });
|
||||
const matrix = JSON.stringify({ include: extensionIds.map((extension) => ({ extension })) });
|
||||
|
||||
appendFileSync(process.env.GITHUB_OUTPUT, `has_changed_extensions=${extensionIds.length > 0}\n`, "utf8");
|
||||
appendFileSync(process.env.GITHUB_OUTPUT, `changed_extensions_matrix=${matrix}\n`, "utf8");
|
||||
EOF
|
||||
|
||||
# Build dist once for Node-relevant changes and share it with downstream jobs.
|
||||
build-artifacts:
|
||||
needs: [docs-scope, changed-scope]
|
||||
@@ -206,9 +162,6 @@ jobs:
|
||||
- runtime: node
|
||||
task: channels
|
||||
command: pnpm test:channels
|
||||
- runtime: node
|
||||
task: contracts
|
||||
command: pnpm test:contracts
|
||||
- runtime: node
|
||||
task: protocol
|
||||
command: pnpm protocol:check
|
||||
@@ -252,31 +205,6 @@ jobs:
|
||||
if: matrix.runtime != 'bun' || github.event_name != 'pull_request'
|
||||
run: ${{ matrix.command }}
|
||||
|
||||
extension-fast:
|
||||
name: "extension-fast (${{ matrix.extension }})"
|
||||
needs: [docs-scope, changed-scope, changed-extensions]
|
||||
if: needs.docs-scope.outputs.docs_only != 'true' && needs.changed-scope.outputs.run_node == 'true' && needs.changed-extensions.outputs.has_changed_extensions == 'true'
|
||||
runs-on: blacksmith-16vcpu-ubuntu-2404
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix: ${{ fromJson(needs.changed-extensions.outputs.changed_extensions_matrix) }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: false
|
||||
|
||||
- name: Setup Node environment
|
||||
uses: ./.github/actions/setup-node-env
|
||||
with:
|
||||
install-bun: "false"
|
||||
use-sticky-disk: "false"
|
||||
|
||||
- name: Run changed extension tests
|
||||
env:
|
||||
OPENCLAW_CHANGED_EXTENSION: ${{ matrix.extension }}
|
||||
run: pnpm test:extension "$OPENCLAW_CHANGED_EXTENSION"
|
||||
|
||||
# Types, lint, and format check.
|
||||
check:
|
||||
name: "check"
|
||||
@@ -304,146 +232,6 @@ jobs:
|
||||
- name: Enforce safe external URL opening policy
|
||||
run: pnpm lint:ui:no-raw-window-open
|
||||
|
||||
plugin-extension-boundary:
|
||||
name: "plugin-extension-boundary"
|
||||
needs: [docs-scope, changed-scope]
|
||||
if: needs.docs-scope.outputs.docs_only != 'true' && needs.changed-scope.outputs.run_node == 'true'
|
||||
runs-on: blacksmith-16vcpu-ubuntu-2404
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: false
|
||||
|
||||
- name: Setup Node environment
|
||||
uses: ./.github/actions/setup-node-env
|
||||
with:
|
||||
install-bun: "false"
|
||||
use-sticky-disk: "false"
|
||||
|
||||
- name: Run plugin extension boundary guard
|
||||
run: pnpm run lint:plugins:no-extension-imports
|
||||
|
||||
web-search-provider-boundary:
|
||||
name: "web-search-provider-boundary"
|
||||
needs: [docs-scope, changed-scope]
|
||||
if: needs.docs-scope.outputs.docs_only != 'true' && needs.changed-scope.outputs.run_node == 'true'
|
||||
runs-on: blacksmith-16vcpu-ubuntu-2404
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: false
|
||||
|
||||
- name: Setup Node environment
|
||||
uses: ./.github/actions/setup-node-env
|
||||
with:
|
||||
install-bun: "false"
|
||||
use-sticky-disk: "false"
|
||||
|
||||
- name: Run web search provider boundary guard
|
||||
run: pnpm run lint:web-search-provider-boundaries
|
||||
|
||||
extension-src-outside-plugin-sdk-boundary:
|
||||
name: "extension-src-outside-plugin-sdk-boundary"
|
||||
needs: [docs-scope, changed-scope]
|
||||
if: needs.docs-scope.outputs.docs_only != 'true' && needs.changed-scope.outputs.run_node == 'true'
|
||||
runs-on: blacksmith-16vcpu-ubuntu-2404
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: false
|
||||
|
||||
- name: Setup Node environment
|
||||
uses: ./.github/actions/setup-node-env
|
||||
with:
|
||||
install-bun: "false"
|
||||
use-sticky-disk: "false"
|
||||
|
||||
- name: Run extension src boundary guard
|
||||
run: pnpm run lint:extensions:no-src-outside-plugin-sdk
|
||||
|
||||
extension-plugin-sdk-internal-boundary:
|
||||
name: "extension-plugin-sdk-internal-boundary"
|
||||
needs: [docs-scope, changed-scope]
|
||||
if: needs.docs-scope.outputs.docs_only != 'true' && needs.changed-scope.outputs.run_node == 'true'
|
||||
runs-on: blacksmith-16vcpu-ubuntu-2404
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: false
|
||||
|
||||
- name: Setup Node environment
|
||||
uses: ./.github/actions/setup-node-env
|
||||
with:
|
||||
install-bun: "false"
|
||||
use-sticky-disk: "false"
|
||||
|
||||
- name: Run extension plugin-sdk-internal guard
|
||||
run: pnpm run lint:extensions:no-plugin-sdk-internal
|
||||
|
||||
build-smoke:
|
||||
name: "build-smoke"
|
||||
needs: [docs-scope, changed-scope]
|
||||
if: needs.docs-scope.outputs.docs_only != 'true' && needs.changed-scope.outputs.run_node == 'true'
|
||||
runs-on: blacksmith-16vcpu-ubuntu-2404
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: false
|
||||
|
||||
- name: Setup Node environment
|
||||
uses: ./.github/actions/setup-node-env
|
||||
with:
|
||||
install-bun: "false"
|
||||
use-sticky-disk: "false"
|
||||
|
||||
- name: Build dist
|
||||
run: pnpm build
|
||||
|
||||
- name: Smoke test CLI launcher help
|
||||
run: node openclaw.mjs --help
|
||||
|
||||
- name: Smoke test CLI launcher status json
|
||||
run: node openclaw.mjs status --json --timeout 1
|
||||
|
||||
- name: Smoke test built bundled plugin singleton
|
||||
run: pnpm test:build:singleton
|
||||
|
||||
- name: Check CLI startup memory
|
||||
run: pnpm test:startup:memory
|
||||
|
||||
gateway-watch-regression:
|
||||
name: "gateway-watch-regression"
|
||||
needs: [docs-scope, changed-scope]
|
||||
if: needs.docs-scope.outputs.docs_only != 'true' && needs.changed-scope.outputs.run_node == 'true'
|
||||
runs-on: blacksmith-16vcpu-ubuntu-2404
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: false
|
||||
|
||||
- name: Setup Node environment
|
||||
uses: ./.github/actions/setup-node-env
|
||||
with:
|
||||
install-bun: "false"
|
||||
use-sticky-disk: "false"
|
||||
|
||||
- name: Run gateway watch regression harness
|
||||
run: pnpm test:gateway:watch-regression
|
||||
|
||||
- name: Upload gateway watch regression artifacts
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: gateway-watch-regression
|
||||
path: .local/gateway-watch-regression/
|
||||
retention-days: 7
|
||||
|
||||
# Validate docs (format, lint, broken links) only when docs files changed.
|
||||
check-docs:
|
||||
needs: [docs-scope]
|
||||
@@ -571,30 +359,21 @@ jobs:
|
||||
run: pre-commit run --all-files detect-private-key
|
||||
|
||||
- name: Audit changed GitHub workflows with zizmor
|
||||
env:
|
||||
BASE_SHA: ${{ github.event_name == 'push' && github.event.before || github.event.pull_request.base.sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
if [ -z "${BASE_SHA:-}" ] || [ "${BASE_SHA}" = "0000000000000000000000000000000000000000" ]; then
|
||||
echo "No usable base SHA detected; skipping zizmor."
|
||||
exit 0
|
||||
if [ "${{ github.event_name }}" = "push" ]; then
|
||||
BASE="${{ github.event.before }}"
|
||||
else
|
||||
BASE="${{ github.event.pull_request.base.sha }}"
|
||||
fi
|
||||
|
||||
if ! git cat-file -e "${BASE_SHA}^{commit}" 2>/dev/null; then
|
||||
echo "Base SHA ${BASE_SHA} is unavailable; skipping zizmor."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
mapfile -t workflow_files < <(
|
||||
git diff --name-only "${BASE_SHA}" HEAD -- '.github/workflows/*.yml' '.github/workflows/*.yaml'
|
||||
)
|
||||
mapfile -t workflow_files < <(git diff --name-only "$BASE" HEAD -- '.github/workflows/*.yml' '.github/workflows/*.yaml')
|
||||
if [ "${#workflow_files[@]}" -eq 0 ]; then
|
||||
echo "No workflow changes detected; skipping zizmor."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
printf 'Auditing workflow files:\n%s\n' "${workflow_files[@]}"
|
||||
pre-commit run zizmor --files "${workflow_files[@]}"
|
||||
|
||||
- name: Audit production dependencies
|
||||
|
||||
214
.github/workflows/plugin-npm-release.yml
vendored
214
.github/workflows/plugin-npm-release.yml
vendored
@@ -1,214 +0,0 @@
|
||||
name: Plugin NPM Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- ".github/workflows/plugin-npm-release.yml"
|
||||
- "extensions/**"
|
||||
- "package.json"
|
||||
- "scripts/lib/plugin-npm-release.ts"
|
||||
- "scripts/plugin-npm-publish.sh"
|
||||
- "scripts/plugin-npm-release-check.ts"
|
||||
- "scripts/plugin-npm-release-plan.ts"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
publish_scope:
|
||||
description: Publish the selected plugins or all publishable plugins from the ref
|
||||
required: true
|
||||
default: selected
|
||||
type: choice
|
||||
options:
|
||||
- selected
|
||||
- all-publishable
|
||||
ref:
|
||||
description: Commit SHA on main to publish from (copy from the preview run)
|
||||
required: true
|
||||
type: string
|
||||
plugins:
|
||||
description: Comma-separated plugin package names to publish when publish_scope=selected
|
||||
required: false
|
||||
type: string
|
||||
|
||||
concurrency:
|
||||
group: plugin-npm-release-${{ github.event_name == 'workflow_dispatch' && inputs.ref || github.sha }}
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||
NODE_VERSION: "24.x"
|
||||
PNPM_VERSION: "10.23.0"
|
||||
|
||||
jobs:
|
||||
preview_plugins_npm:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
outputs:
|
||||
ref_sha: ${{ steps.ref.outputs.sha }}
|
||||
has_candidates: ${{ steps.plan.outputs.has_candidates }}
|
||||
candidate_count: ${{ steps.plan.outputs.candidate_count }}
|
||||
matrix: ${{ steps.plan.outputs.matrix }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || github.sha }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Node environment
|
||||
uses: ./.github/actions/setup-node-env
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
pnpm-version: ${{ env.PNPM_VERSION }}
|
||||
install-bun: "false"
|
||||
use-sticky-disk: "false"
|
||||
|
||||
- name: Resolve checked-out ref
|
||||
id: ref
|
||||
run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Validate ref is on main
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main
|
||||
git merge-base --is-ancestor HEAD origin/main
|
||||
|
||||
- name: Validate publishable plugin metadata
|
||||
env:
|
||||
PUBLISH_SCOPE: ${{ github.event_name == 'workflow_dispatch' && inputs.publish_scope || '' }}
|
||||
RELEASE_PLUGINS: ${{ github.event_name == 'workflow_dispatch' && inputs.plugins || '' }}
|
||||
BASE_REF: ${{ github.event_name != 'workflow_dispatch' && github.event.before || '' }}
|
||||
HEAD_REF: ${{ steps.ref.outputs.sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [[ -n "${PUBLISH_SCOPE}" ]]; then
|
||||
release_args=(--selection-mode "${PUBLISH_SCOPE}")
|
||||
if [[ -n "${RELEASE_PLUGINS}" ]]; then
|
||||
release_args+=(--plugins "${RELEASE_PLUGINS}")
|
||||
fi
|
||||
pnpm release:plugins:npm:check -- "${release_args[@]}"
|
||||
elif [[ -n "${BASE_REF}" ]]; then
|
||||
pnpm release:plugins:npm:check -- --base-ref "${BASE_REF}" --head-ref "${HEAD_REF}"
|
||||
else
|
||||
pnpm release:plugins:npm:check
|
||||
fi
|
||||
|
||||
- name: Resolve plugin release plan
|
||||
id: plan
|
||||
env:
|
||||
PUBLISH_SCOPE: ${{ github.event_name == 'workflow_dispatch' && inputs.publish_scope || '' }}
|
||||
RELEASE_PLUGINS: ${{ github.event_name == 'workflow_dispatch' && inputs.plugins || '' }}
|
||||
BASE_REF: ${{ github.event_name != 'workflow_dispatch' && github.event.before || '' }}
|
||||
HEAD_REF: ${{ steps.ref.outputs.sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p .local
|
||||
if [[ -n "${PUBLISH_SCOPE}" ]]; then
|
||||
plan_args=(--selection-mode "${PUBLISH_SCOPE}")
|
||||
if [[ -n "${RELEASE_PLUGINS}" ]]; then
|
||||
plan_args+=(--plugins "${RELEASE_PLUGINS}")
|
||||
fi
|
||||
node --import tsx scripts/plugin-npm-release-plan.ts "${plan_args[@]}" > .local/plugin-npm-release-plan.json
|
||||
elif [[ -n "${BASE_REF}" ]]; then
|
||||
node --import tsx scripts/plugin-npm-release-plan.ts --base-ref "${BASE_REF}" --head-ref "${HEAD_REF}" > .local/plugin-npm-release-plan.json
|
||||
else
|
||||
node --import tsx scripts/plugin-npm-release-plan.ts > .local/plugin-npm-release-plan.json
|
||||
fi
|
||||
|
||||
cat .local/plugin-npm-release-plan.json
|
||||
|
||||
candidate_count="$(jq -r '.candidates | length' .local/plugin-npm-release-plan.json)"
|
||||
has_candidates="false"
|
||||
if [[ "${candidate_count}" != "0" ]]; then
|
||||
has_candidates="true"
|
||||
fi
|
||||
matrix_json="$(jq -c '.candidates' .local/plugin-npm-release-plan.json)"
|
||||
|
||||
{
|
||||
echo "candidate_count=${candidate_count}"
|
||||
echo "has_candidates=${has_candidates}"
|
||||
echo "matrix=${matrix_json}"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
echo "Plugin release candidates:"
|
||||
jq -r '.candidates[]? | "- \(.packageName)@\(.version) [\(.publishTag)] from \(.packageDir)"' .local/plugin-npm-release-plan.json
|
||||
|
||||
echo "Already published / skipped:"
|
||||
jq -r '.skippedPublished[]? | "- \(.packageName)@\(.version)"' .local/plugin-npm-release-plan.json
|
||||
|
||||
preview_plugin_pack:
|
||||
needs: preview_plugins_npm
|
||||
if: needs.preview_plugins_npm.outputs.has_candidates == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
plugin: ${{ fromJson(needs.preview_plugins_npm.outputs.matrix) }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ needs.preview_plugins_npm.outputs.ref_sha }}
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Setup Node environment
|
||||
uses: ./.github/actions/setup-node-env
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
pnpm-version: ${{ env.PNPM_VERSION }}
|
||||
install-bun: "false"
|
||||
use-sticky-disk: "false"
|
||||
install-deps: "false"
|
||||
|
||||
- name: Preview publish command
|
||||
run: bash scripts/plugin-npm-publish.sh --dry-run "${{ matrix.plugin.packageDir }}"
|
||||
|
||||
- name: Preview npm pack contents
|
||||
working-directory: ${{ matrix.plugin.packageDir }}
|
||||
run: npm pack --dry-run --json --ignore-scripts
|
||||
|
||||
publish_plugins_npm:
|
||||
needs: [preview_plugins_npm, preview_plugin_pack]
|
||||
if: github.event_name == 'workflow_dispatch' && needs.preview_plugins_npm.outputs.has_candidates == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
environment: npm-release
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
plugin: ${{ fromJson(needs.preview_plugins_npm.outputs.matrix) }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ needs.preview_plugins_npm.outputs.ref_sha }}
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Setup Node environment
|
||||
uses: ./.github/actions/setup-node-env
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
pnpm-version: ${{ env.PNPM_VERSION }}
|
||||
install-bun: "false"
|
||||
use-sticky-disk: "false"
|
||||
install-deps: "false"
|
||||
|
||||
- name: Ensure version is not already published
|
||||
env:
|
||||
PACKAGE_NAME: ${{ matrix.plugin.packageName }}
|
||||
PACKAGE_VERSION: ${{ matrix.plugin.version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if npm view "${PACKAGE_NAME}@${PACKAGE_VERSION}" version >/dev/null 2>&1; then
|
||||
echo "${PACKAGE_NAME}@${PACKAGE_VERSION} is already published on npm."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Publish
|
||||
run: bash scripts/plugin-npm-publish.sh --publish "${{ matrix.plugin.packageDir }}"
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -4,12 +4,10 @@ node_modules
|
||||
docker-compose.override.yml
|
||||
docker-compose.extra.yml
|
||||
dist
|
||||
dist-runtime
|
||||
pnpm-lock.yaml
|
||||
bun.lock
|
||||
bun.lockb
|
||||
coverage
|
||||
__openclaw_vitest__/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.tsbuildinfo
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
**/node_modules/
|
||||
docs/.generated/
|
||||
|
||||
10
AGENTS.md
10
AGENTS.md
@@ -114,7 +114,6 @@
|
||||
- Never add `@ts-nocheck` and do not disable `no-explicit-any`; fix root causes and update Oxlint/Oxfmt config only when required.
|
||||
- Dynamic import guardrail: do not mix `await import("x")` and static `import ... from "x"` for the same module in production code paths. If you need lazy loading, create a dedicated `*.runtime.ts` boundary (that re-exports from `x`) and dynamically import that boundary from lazy callers only.
|
||||
- Dynamic import verification: after refactors that touch lazy-loading/module boundaries, run `pnpm build` and check for `[INEFFECTIVE_DYNAMIC_IMPORT]` warnings before submitting.
|
||||
- Extension SDK self-import guardrail: inside an extension package, do not import that same extension via `openclaw/plugin-sdk/<extension>` from production files. Route internal imports through a local barrel such as `./api.ts` or `./runtime-api.ts`, and keep the `plugin-sdk/<extension>` path as the external contract only.
|
||||
- Never share class behavior via prototype mutation (`applyPrototypeMixins`, `Object.defineProperty` on `.prototype`, or exporting `Class.prototype` for merges). Use explicit inheritance/composition (`A extends B extends C`) or helper composition so TypeScript can typecheck.
|
||||
- If this pattern is needed, stop and get explicit approval before shipping; default behavior is to split/refactor into an explicit class hierarchy and keep members strongly typed.
|
||||
- In tests, prefer per-instance stubs over prototype mutation (`SomeClass.prototype.method = ...`) unless a test explicitly documents why prototype-level patching is required.
|
||||
@@ -140,7 +139,7 @@
|
||||
- Do not set test workers above 16; tried already.
|
||||
- If local Vitest runs cause memory pressure (common on non-Mac-Studio hosts), use `OPENCLAW_TEST_PROFILE=low OPENCLAW_TEST_SERIAL_GATEWAY=1 pnpm test` for land/gate runs.
|
||||
- Live tests (real keys): `CLAWDBOT_LIVE_TEST=1 pnpm test:live` (OpenClaw-only) or `LIVE=1 pnpm test:live` (includes provider live tests). Docker: `pnpm test:docker:live-models`, `pnpm test:docker:live-gateway`. Onboarding Docker E2E: `pnpm test:docker:onboard`.
|
||||
- Full kit + what’s covered: `docs/help/testing.md`.
|
||||
- Full kit + what’s covered: `docs/testing.md`.
|
||||
- Changelog: user-facing changes only; no internal/meta notes (version alignment, appcast reminders, release process).
|
||||
- Changelog placement: in the active version block, append new entries to the end of the target section (`### Changes` or `### Fixes`); do not insert new entries at the top of a section.
|
||||
- Changelog attribution: use at most one contributor mention per line; prefer `Thanks @author` and do not also add `by @author` on the same entry.
|
||||
@@ -213,11 +212,6 @@
|
||||
- `prlctl exec` is fine for deterministic repo commands, but it can misrepresent interactive shell behavior (`PATH`, `HOME`, `curl | bash`, shebang resolution). For installer parity or shell-sensitive repros, prefer the guest Terminal or `prlctl enter`.
|
||||
- Fresh Tahoe snapshot current reality: `brew` exists, `node` may not be on `PATH` in noninteractive guest exec. Use absolute `/opt/homebrew/bin/node` for repo/CLI runs when needed.
|
||||
- Preferred automation entrypoint: `pnpm test:parallels:macos`. It restores the snapshot most closely matching `macOS 26.3.1 fresh`, serves the current `main` tarball from the host, then runs fresh-install and latest-release-to-main smoke lanes.
|
||||
- Discord roundtrip smoke is opt-in. Pass `--discord-token-env <VAR> --discord-guild-id <guild> --discord-channel-id <channel>`; the harness will configure Discord in-guest, post a guest message, verify host-side visibility via the Discord REST API, post a fresh host-side message back into the channel, then verify `openclaw message read` sees it in-guest.
|
||||
- Keep the Discord token in a host env var only. For Peter’s Mac Studio bot, fetch it into a temp env var from `~/.openclaw/openclaw.json` over SSH instead of hardcoding it in repo files/shell history.
|
||||
- For Discord smoke on this snapshot: use `openclaw message send/read` via the installed wrapper, not `node openclaw.mjs message ...`; lazy `message` subcommands do not resolve the same way through the direct module entrypoint.
|
||||
- For Discord guild allowlists: set `channels.discord.guilds` as one JSON object. Do not use dotted `config set channels.discord.guilds.<snowflake>...` paths; numeric snowflakes get treated as array indexes.
|
||||
- Avoid `prlctl enter` / expect for the Discord config phase; long lines get mangled. Use `prlctl exec --current-user /bin/sh -lc ...` with short commands or temp files.
|
||||
- Gateway verification in smoke runs should use `openclaw gateway status --deep --require-rpc`, not plain `--deep`, so probe failures go non-zero.
|
||||
- Latest-release pre-upgrade diagnostics still need compatibility fallback: stable `2026.3.12` does not know `--require-rpc`, so precheck status dumps should fall back to plain `gateway status --deep` until the guest is upgraded.
|
||||
- Harness output: pass `--json` for machine-readable summary; per-phase logs land under `/tmp/openclaw-parallels-smoke.*`.
|
||||
@@ -281,7 +275,7 @@
|
||||
- If staged+unstaged diffs are formatting-only, auto-resolve without asking.
|
||||
- If commit/push already requested, auto-stage and include formatting-only follow-ups in the same commit (or a tiny follow-up commit if needed), no extra confirmation.
|
||||
- Only ask when changes are semantic (logic/data/behavior).
|
||||
- Lobster palette: use the shared CLI palette in `src/terminal/palette.ts` (no hardcoded colors); apply palette to onboarding/config prompts and other TTY UI output as needed.
|
||||
- Lobster seam: use the shared CLI palette in `src/terminal/palette.ts` (no hardcoded colors); apply palette to onboarding/config prompts and other TTY UI output as needed.
|
||||
- **Multi-agent safety:** focus reports on your edits; avoid guard-rail disclaimers unless truly blocked; when multiple agents touch the same file, continue if safe; end with a brief “other files present” note only if relevant.
|
||||
- Bug investigations: read source code of relevant npm dependencies and all related local code before concluding; aim for high-confidence root cause.
|
||||
- Code style: add brief comments for tricky logic; keep files under ~500 LOC when feasible (split/refactor as needed).
|
||||
|
||||
450
CHANGELOG.md
450
CHANGELOG.md
@@ -6,160 +6,73 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Changes
|
||||
|
||||
- Commands/btw: add `/btw` side questions for quick tool-less answers about the current session without changing future session context, with dismissible in-session TUI answers and explicit BTW replies on external channels. (#45444) Thanks @ngutman.
|
||||
- Gateway/docs: clarify that empty URL input allowlists are treated as unset, document `allowUrl: false` as the deny-all switch, and add regression coverage for the normalization path.
|
||||
- Sandbox/runtime: add pluggable sandbox backends, ship an OpenShell backend with `mirror` and `remote` workspace modes, and make sandbox list/recreate/prune backend-aware instead of Docker-only.
|
||||
- Sandbox/SSH: add a core SSH sandbox backend with secret-backed key, certificate, and known_hosts inputs, move shared remote exec/filesystem tooling into core, and keep OpenShell focused on sandbox lifecycle plus optional `mirror` mode.
|
||||
- Web tools/Firecrawl: add Firecrawl as an `onboard`/configure search provider via a bundled plugin, expose explicit `firecrawl_search` and `firecrawl_scrape` tools, and align core `web_fetch` fallback behavior with Firecrawl base-URL/env fallback plus guarded endpoint fetches.
|
||||
- Plugins/bundles: add compatible Codex, Claude, and Cursor bundle discovery/install support, map bundle skills into OpenClaw skills, and apply Claude bundle `settings.json` defaults to embedded Pi with shell overrides sanitized.
|
||||
- Plugins/providers: move OpenRouter, GitHub Copilot, and OpenAI Codex provider/runtime logic into bundled plugins, including dynamic model fallback, runtime auth exchange, stream wrappers, capability hints, and cache-TTL policy.
|
||||
- Plugins/agent integrations: broaden the plugin surface for app-server integrations with channel-aware commands, interactive callbacks, inbound claims, and Discord/Telegram conversation binding support. (#45318) Thanks @huntharo and @vincentkoc.
|
||||
- Install/update: allow package-manager installs from GitHub `main` via `openclaw update --tag main`, installer `--version main`, or direct npm/pnpm git specs. (#47630) Thanks @vincentkoc.
|
||||
- Gateway/health monitor: add configurable stale-event thresholds and restart limits, plus per-channel and per-account `healthMonitor.enabled` overrides, while keeping the existing global disable path on `gateway.channelHealthCheckMinutes=0`. (#42107) Thanks @rstar327.
|
||||
- Android/mobile: add a system-aware dark theme across onboarding and post-onboarding screens so the app follows the device theme through setup, chat, and voice flows. (#46249) Thanks @sibbl.
|
||||
- Feishu/ACP: add current-conversation ACP and subagent session binding for supported DMs and topic conversations, including completion delivery back to the originating Feishu conversation. (#46819) Thanks @Takhoffman.
|
||||
- Plugins/marketplaces: add Claude marketplace registry resolution, `plugin@marketplace` installs, marketplace listing, and update support, plus Docker E2E coverage for local and official marketplace flows. (#48058) Thanks @vincentkoc.
|
||||
- Commands/plugins: add owner-gated `/plugins` and `/plugin` chat commands for plugin list/show and enable/disable flows, alongside explicit `commands.plugins` config gating. Thanks @vincentkoc.
|
||||
- Feishu/cards: add structured interactive approval and quick-action launcher cards, preserve callback user and conversation context through routing, and keep legacy card-action fallback behavior so common actions can run without typing raw commands. (#47873) Thanks @Takhoffman.
|
||||
- Feishu/streaming: add `onReasoningStream` and `onReasoningEnd` support to streaming cards, so `/reasoning stream` renders thinking tokens as markdown blockquotes in the same card — matching the Telegram channel's reasoning lane behavior. (#46029) Thanks @day253.
|
||||
- Commands/btw: add `/btw` side questions for quick tool-less answers about the current session without changing future session context, with dismissible in-session TUI answers and explicit BTW replies on external channels. (#45444) Thanks @ngutman.
|
||||
- Gateway/health monitor: add configurable stale-event thresholds and restart limits, plus per-channel and per-account `healthMonitor.enabled` overrides, while keeping the existing global disable path on `gateway.channelHealthCheckMinutes=0`. (#42107) Thanks @rstar327.
|
||||
- Feishu/cards: add identity-aware structured card headers and note footers for Feishu replies and direct sends, while keeping that presentation wired through the shared outbound identity path. (#29938) Thanks @nszhsl.
|
||||
- Feishu/streaming: add `onReasoningStream` and `onReasoningEnd` support to streaming cards, so `/reasoning stream` renders thinking tokens as markdown blockquotes in the same card — matching the Telegram channel's reasoning lane behavior. (#46029)
|
||||
- Refactor/channels: remove the legacy channel shim directories and point channel-specific imports directly at the extension-owned implementations. (#45967) thanks @scoootscooob.
|
||||
- Android/nodes: add `callLog.search` plus shared Call Log permission wiring so Android nodes can search recent call history through the gateway. (#44073) Thanks @lxk7280.
|
||||
- Plugins/MiniMax: merge the bundled MiniMax API and MiniMax OAuth plugin surfaces into a single default-on `minimax` plugin, while keeping legacy `minimax-portal-auth` config ids aliased for compatibility.
|
||||
- Telegram/actions: add `topic-edit` for forum-topic renames and icon updates while sharing the same Telegram topic-edit transport used by the plugin runtime. (#47798) Thanks @obviyus.
|
||||
- Telegram/error replies: add a default-off `channels.telegram.silentErrorReplies` setting so bot error replies can be delivered silently across regular replies, native commands, and fallback sends. (#19776) Thanks @ImLukeF.
|
||||
- Refactor/channels: remove the legacy channel shim directories and point channel-specific imports directly at the extension-owned implementations. (#45967) Thanks @scoootscooob.
|
||||
- Docs/Zalo: clarify the Marketplace-bot support matrix and config guidance so the Zalo channel docs match current Bot Creator behavior more closely. (#47552) Thanks @No898.
|
||||
- secrets: harden read-only SecretRef command paths and diagnostics. (#47794) Thanks @joshavant.
|
||||
- Browser/existing-session: support `browser.profiles.<name>.userDataDir` so Chrome DevTools MCP can attach to Brave, Edge, and other Chromium-based browsers through their own user data directories. (#48170) Thanks @velvet-shark.
|
||||
- Skills/prompt budget: preserve all registered skills via a compact catalog fallback before dropping entries when the full prompt format exceeds `maxSkillsPromptChars`. (#47553) Thanks @snese.
|
||||
- Models/OpenAI: add native forward-compat support for `gpt-5.4-mini` and `gpt-5.4-nano` in the OpenAI provider catalog, runtime resolution, and reasoning capability gates. Thanks @vincentkoc.
|
||||
- Plugins/bundles: make enabled bundle MCP servers expose runnable tools in embedded Pi, and default relative bundle MCP launches to the bundle root so marketplace bundles like Context7 work through Pi instead of stopping at config import.
|
||||
- Scope message SecretRef resolution and harden doctor/status paths. (#48728) Thanks @joshavant.
|
||||
- Plugins/testing: add a public `openclaw/plugin-sdk/testing` surface for plugin-author test helpers, and move bundled-extension-only test bridges out of `extensions/` into private repo test helpers.
|
||||
- Plugins/Chutes: add a bundled Chutes provider with plugin-owned OAuth/API-key auth, dynamic model discovery, and default-on extension wiring. (#41416) Thanks @Veightor.
|
||||
- Plugins/binding: add `onConversationBindingResolved(...)` so plugins can react immediately after bind approvals or denies without blocking channel interaction acknowledgements. (#48678) Thanks @huntharo.
|
||||
- CLI/config: expand `config set` with SecretRef and provider builder modes, JSON/batch assignment support, and `--dry-run` validation with structured JSON output. (#49296) Thanks @joshavant.
|
||||
- Control UI/appearance: unify theme border radii across Claw, Knot, and Dash, and add a Roundness slider to the Appearance settings so users can adjust corner radius from sharp to fully rounded. Thanks @BunsDev.
|
||||
- Control UI/chat: add an expand-to-canvas button on assistant chat bubbles and in-app session navigation from Sessions and Cron views. Thanks @BunsDev.
|
||||
- Plugins/context engines: expose `delegateCompactionToRuntime(...)` on the public plugin SDK, refactor the legacy engine to use the shared helper, and clarify `ownsCompaction` delegation semantics for non-owning engines. (#49061) Thanks @jalehman.
|
||||
- Install/update: allow package-manager installs from GitHub `main` via `openclaw update --tag main`, installer `--version main`, or direct npm/pnpm git specs.
|
||||
- Plugins/providers: move OpenRouter, GitHub Copilot, and OpenAI Codex provider/runtime logic into bundled plugins, including dynamic model fallback, runtime auth exchange, stream wrappers, capability hints, and cache-TTL policy.
|
||||
- Plugins/bundles: add compatible Codex, Claude, and Cursor bundle discovery/install support, map bundle skills into OpenClaw skills, and apply Claude bundle `settings.json` defaults to embedded Pi with shell overrides sanitized.
|
||||
- Plugins/agent integrations: broaden the plugin surface for app-server integrations with channel-aware commands, interactive callbacks, inbound claims, and Discord/Telegram conversation binding support. (#45318) Thanks @huntharo and @vincentkoc.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Plugins/bundler TDZ: fix `RESERVED_COMMANDS` temporal dead zone error that prevented device-pair, phone-control, and talk-voice plugins from registering when the bundler placed the commands module after call sites in the same output chunk. Thanks @BunsDev.
|
||||
- Plugins/imports: fix stale googlechat runtime-api import paths and signal SDK circular re-exports broken by recent plugin-sdk refactors. Thanks @BunsDev.
|
||||
- Google auth/Node 25: patch `gaxios` to use native fetch without injecting `globalThis.window`, while translating proxy and mTLS transport settings so Google Vertex and Google Chat auth keep working on Node 25. (#47914) Thanks @pdd-cli.
|
||||
- Gateway/startup: load bundled channel plugins from compiled `dist/extensions` entries in built installs, so gateway boot no longer recompiles bundled extension TypeScript on every startup and WhatsApp-class cold starts drop back to seconds instead of tens of seconds or worse. (#47560) Thanks @ngutman.
|
||||
- Plugins/context engines: enforce owner-aware context-engine registration on both loader and public SDK paths so plugins cannot spoof privileged ownership, claim the core `legacy` engine id, or overwrite an existing engine id through direct SDK imports. (#47595) Thanks @vincentkoc.
|
||||
- Group mention gating: reject invalid and unsafe nested-repetition `mentionPatterns`, reuse the shared safe config-regex compiler across mention stripping and detection, and cache strip-time regex compilation so noisy groups avoid repeated recompiles.
|
||||
- Control UI/chat sessions: show human-readable labels in the grouped session dropdown again, keep unique scoped fallbacks when metadata is missing, and disambiguate duplicate labels only when needed. (#45130) thanks @luzhidong.
|
||||
- Control UI: scope persisted session selection per gateway, prevent stale session bleed across tokenized gateway opens, and cap stored gateway session history. (#47453) Thanks @sallyom.
|
||||
- Slack/interactive replies: preserve `channelData.slack.blocks` through live DM delivery and preview-finalized edits so Block Kit button and select directives render instead of falling back to raw text. (#45890) Thanks @vincentkoc.
|
||||
- Feishu/topic threads: fetch full thread context, including prior bot replies, when starting a topic-thread session so follow-up turns in Feishu topics keep the right conversation state. (#45254) Thanks @Coobiw.
|
||||
- Configure/startup: move outbound send-deps resolution into a lightweight helper so `openclaw configure` no longer stalls after the banner while eagerly loading channel plugins. (#46301) thanks @scoootscooob.
|
||||
- Control UI/dashboard: preserve structured gateway shutdown reasons across restart disconnects so config-triggered restarts no longer fall back to `disconnected (1006): no reason`. (#46532) Thanks @vincentkoc.
|
||||
- Android/chat: theme the thinking dropdown and TLS trust dialogs explicitly so popup surfaces match the active app theme instead of falling back to mismatched Material defaults.
|
||||
- Z.AI/onboarding: detect a working default model even for explicit `zai-coding-*` endpoint choices, so Coding Plan setup can keep the selected endpoint while defaulting to `glm-5` when available or `glm-4.7` as fallback. (#45969)
|
||||
- Models/OpenRouter runtime capabilities: fetch uncatalogued OpenRouter model metadata on first use so newly added vision models keep image input instead of silently degrading to text-only, with top-level capability field fallbacks for `/api/v1/models`. (#45824) Thanks @DJjjjhao.
|
||||
- Z.AI/onboarding: add `glm-5-turbo` to the default Z.AI provider catalog so onboarding-generated configs expose the new model alongside the existing GLM defaults. (#46670) Thanks @tomsun28.
|
||||
- Zalo Personal/group gating: stop reapplying `dmPolicy.allowFrom` as a sender gate for already-allowlisted groups when `groupAllowFrom` is unset, so any member of an allowed group can trigger replies while DMs stay restricted. (#40146)
|
||||
- Browser/remote CDP: honor strict browser SSRF policy during remote CDP reachability and `/json/version` discovery checks, redact sensitive `cdpUrl` tokens from status output, and warn when remote CDP targets private/internal hosts.
|
||||
- Gateway/plugins: pin runtime webhook routes to the gateway startup registry so channel webhooks keep working across plugin-registry churn, and make plugin auth + dispatch resolve routes from the same live HTTP-route registry. (#47902) Fixes #46924 and #47041. Thanks @steipete.
|
||||
- Gateway/auth: ignore spoofed loopback hops in trusted forwarding chains and block device approvals that request scopes above the caller session. (#46800) Thanks @vincentkoc.
|
||||
- Gateway/restart: defer externally signaled unmanaged restarts through the in-process idle drain, and preserve the restored subagent run as remap fallback during orphan recovery so resumed sessions do not duplicate work. (#47719) Thanks @joeykrug.
|
||||
- Control UI/session routing: preserve established external delivery routes when webchat views or sends in externally originated sessions, so subagent completions still return to the original channel instead of the dashboard. (#47797) Thanks @brokemac79.
|
||||
- Configure/startup: move outbound send-deps resolution into a lightweight helper so `openclaw configure` no longer stalls after the banner while eagerly loading channel plugins. (#46301) Thanks @scoootscooob.
|
||||
- Plugins/install precedence: keep bundled plugins ahead of auto-discovered globals by default, but let an explicitly installed plugin record win its own duplicate-id tie so installed channel plugins load from `~/.openclaw/extensions` after `openclaw plugins install`.
|
||||
- ACP/acpx: resolve the bundled plugin root from the actual plugin directory so plugin-local installs stay under `dist/extensions/acpx` instead of escaping to `dist/extensions` and failing runtime setup.
|
||||
- Gateway/auth: ignore spoofed loopback hops in trusted forwarding chains and block device approvals that request scopes above the caller session. Thanks @vincentkoc.
|
||||
- Gateway/config views: strip embedded credentials from URL-based endpoint fields before returning read-only account and config snapshots. Thanks @vincentkoc.
|
||||
- Tools/apply-patch: revalidate workspace-only delete and directory targets immediately before mutating host paths. Thanks @vincentkoc.
|
||||
- Webhooks/runtime: move auth earlier and tighten pre-auth body limits and timeouts across bundled webhook handlers, including slow-body handling for Mattermost slash commands. Thanks @vincentkoc.
|
||||
- Subagents/follow-ups: require the same controller ownership checks for `/subagents send` as other control actions, so leaf sessions cannot message nested child runs they do not control. Thanks @vincentkoc.
|
||||
- Inbound policy hardening: tighten callback and webhook sender checks across Mattermost and Google Chat, match Nextcloud Talk rooms by stable room token, and treat explicit empty Twitch allowlists as deny-all. (#46787) Thanks @zpbrent, @ijxpwastaken and @vincentkoc.
|
||||
- macOS/canvas actions: keep unattended local agent actions on trusted in-app canvas surfaces only, and stop exposing the deep-link fallback key to arbitrary page scripts. (#46790) Thanks @vincentkoc.
|
||||
- Agents/compaction: extend the enclosing run deadline once while compaction is actively in flight, and abort the underlying SDK compaction on timeout/cancel so large-session compactions stop freezing mid-run. (#46889) Thanks @asyncjason.
|
||||
- Models/openai-completions: default non-native OpenAI-compatible providers to omit tool-definition `strict` fields unless users explicitly opt back in, so tool calling keeps working on providers that reject that option. (#45497) Thanks @sahancava.
|
||||
- WhatsApp/reconnect: restore the append recency filter in the extension inbox monitor and handle protobuf `Long` timestamps correctly, so fresh post-reconnect append messages are processed while stale history sync stays suppressed. (#42588) thanks @MonkeyLeeT.
|
||||
- WhatsApp/login: wait for pending creds writes before reopening after Baileys `515` pairing restarts in both QR login and `channels login` flows, and keep the restart coverage pinned to the real wrapped error shape plus per-account creds queues. (#27910) Thanks @asyncjason.
|
||||
- Agents/openai-compatible tool calls: deduplicate repeated tool call ids across live assistant messages and replayed history so OpenAI-compatible backends no longer reject duplicate `tool_call_id` values with HTTP 400. (#40996) Thanks @xaeon2026.
|
||||
- Security/device pairing: harden `device.token.rotate` deny handling by keeping public failures generic while logging internal deny reasons and preserving approved-baseline enforcement. (`GHSA-7jrw-x62h-64p8`)
|
||||
- Slack/interactive replies: preserve `channelData.slack.blocks` through live DM delivery and preview-finalized edits so Block Kit button and select directives render instead of falling back to raw text. (#45890) Thanks @vincentkoc.
|
||||
- Zalo/plugin runtime: export `resolveClientIp` from `openclaw/plugin-sdk/zalo` so installed builds no longer crash on startup when the webhook monitor loads from the packaged extension instead of the monorepo source tree. (#46549) Thanks @No898.
|
||||
- CI/channel test routing: move the built-in channel suites into `test:channels` and keep them out of `test:extensions`, so extension CI no longer fails after the channel migration while targeted test routing still sends Slack, Signal, and iMessage suites to the right lane. (#46066) Thanks @scoootscooob.
|
||||
- Browser/profiles: drop the auto-created `chrome-relay` browser profile; users who need the Chrome extension relay must now create their own profile via `openclaw browser create-profile`. (#45777) Thanks @odysseus0.
|
||||
- Docs/Mintlify: fix MDX marker syntax on Perplexity, Model Providers, Moonshot, and exec approvals pages so local docs preview no longer breaks rendering or leaves stale pages unpublished. (#46695) Thanks @velvet-shark.
|
||||
- Email/webhook wrapping: sanitize sender and subject metadata before external-content wrapping so metadata fields cannot break the wrapper structure. Thanks @vincentkoc.
|
||||
- Node/startup: remove leftover debug `console.log("node host PATH: ...")` that printed the resolved PATH on every `openclaw node run` invocation. (#46411)
|
||||
- Nodes/pending actions: re-check queued foreground actions against the current node command policy before returning them to the node. (#46815) Thanks @zpbrent and @vincentkoc.
|
||||
- ACP/approvals: use canonical tool identity for prompting decisions and fail closed when conflicting tool identity hints are present. (#46817) Thanks @zpbrent and @vincentkoc.
|
||||
- Telegram/message send: forward `--force-document` through the `sendPayload` path as well as `sendMedia`, so Telegram payload sends with `channelData` keep uploading images as documents instead of silently falling back to compressed photo sends. (#47119) Thanks @thepagent.
|
||||
- Telegram/message chunking: preserve spaces, paragraph separators, and word boundaries when HTML overflow rechunking splits formatted replies. (#47274)
|
||||
- Plugins/scoped ids: preserve scoped plugin ids during install and config keying, and keep bundled plugins ahead of discovered duplicate ids by default so `@scope/name` plugins no longer collide with unscoped installs. Thanks @vincentkoc.
|
||||
- CLI: avoid loading provider discovery during startup model normalization. (#46522) Thanks @ItsAditya-xyz and @vincentkoc.
|
||||
- Tlon: honor explicit empty allowlists and defer cite expansion. (#46788) Thanks @zpbrent and @vincentkoc.
|
||||
- ACP: require admin scope for mutating internal actions. (#46789) Thanks @tdjackey and @vincentkoc.
|
||||
- Gateway/config validation: stop treating the implicit default memory slot as a required explicit plugin config, so startup no longer fails with `plugins.slots.memory: plugin not found: memory-core` when `memory-core` was only inferred. (#47494) Thanks @ngutman.
|
||||
- CLI/startup: lazy-load channel add and root help startup paths to trim avoidable RSS and help latency on constrained hosts. (#46784) Thanks @vincentkoc.
|
||||
- CLI/onboarding: import static provider definitions directly for onboarding model/config helpers so those paths no longer pull provider discovery just for built-in defaults. (#47467) Thanks @vincentkoc.
|
||||
- CLI/auth choice: lazy-load plugin/provider fallback resolution so mapped auth choices stay on the static path and only unknown choices pay the heavy provider load. (#47495) Thanks @vincentkoc.
|
||||
- CLI: avoid loading provider discovery during startup model normalization. (#46522) Thanks @ItsAditya-xyz and @vincentkoc.
|
||||
- Security/device pairing: harden `device.token.rotate` deny handling by keeping public failures generic while logging internal deny reasons and preserving approved-baseline enforcement. (`GHSA-7jrw-x62h-64p8`)
|
||||
- Inbound policy hardening: tighten callback and webhook sender checks across Mattermost and Google Chat, match Nextcloud Talk rooms by stable room token, and treat explicit empty Twitch allowlists as deny-all. (#46787) Thanks @zpbrent, @ijxpwastaken and @vincentkoc.
|
||||
- Webhooks/runtime: move auth earlier and tighten pre-auth body limits and timeouts across bundled webhook handlers, including slow-body handling for Mattermost slash commands. (#46802) Thanks @vincentkoc.
|
||||
- Email/webhook wrapping: sanitize sender and subject metadata before external-content wrapping so metadata fields cannot break the wrapper structure. (#46816) Thanks @vincentkoc.
|
||||
- Tools/apply-patch: revalidate workspace-only delete and directory targets immediately before mutating host paths. (#46803) Thanks @vincentkoc.
|
||||
- Gateway/config views: strip embedded credentials from URL-based endpoint fields before returning read-only account and config snapshots. (#46799) Thanks @vincentkoc.
|
||||
- ACP/approvals: use canonical tool identity for prompting decisions and fail closed when conflicting tool identity hints are present. (#46817) Thanks @zpbrent and @vincentkoc.
|
||||
- ACP: require admin scope for mutating internal actions. (#46789) Thanks @tdjackey and @vincentkoc.
|
||||
- Subagents/follow-ups: require the same controller ownership checks for `/subagents send` as other control actions, so leaf sessions cannot message nested child runs they do not control. (#46801) Thanks @vincentkoc.
|
||||
- macOS/canvas actions: keep unattended local agent actions on trusted in-app canvas surfaces only, and stop exposing the deep-link fallback key to arbitrary page scripts. (#46790) Thanks @vincentkoc.
|
||||
- Agents/compaction: extend the enclosing run deadline once while compaction is actively in flight, and abort the underlying SDK compaction on timeout/cancel so large-session compactions stop freezing mid-run. (#46889) Thanks @asyncjason.
|
||||
- Agents/openai-compatible tool calls: deduplicate repeated tool call ids across live assistant messages and replayed history so OpenAI-compatible backends no longer reject duplicate `tool_call_id` values with HTTP 400. (#40996) Thanks @xaeon2026.
|
||||
- Models/openai-completions: default non-native OpenAI-compatible providers to omit tool-definition `strict` fields unless users explicitly opt back in, so tool calling keeps working on providers that reject that option. (#45497) Thanks @sahancava.
|
||||
- Models/OpenRouter runtime capabilities: fetch uncatalogued OpenRouter model metadata on first use so newly added vision models keep image input instead of silently degrading to text-only, with top-level capability field fallbacks for `/api/v1/models`. (#45824) Thanks @DJjjjhao.
|
||||
- Channels/plugins: keep shared interactive payloads merge-ready by fixing Slack custom callback routing and repeat-click dedupe, allowing interactive-only sends, and preserving ordered Discord shared text blocks. (#47715) Thanks @vincentkoc.
|
||||
- Slack/interactive replies: preserve `channelData.slack.blocks` through live DM delivery and preview-finalized edits so Block Kit button and select directives render instead of falling back to raw text. (#45890) Thanks @vincentkoc.
|
||||
- Feishu/actions: expand the runtime action surface with message read/edit, explicit thread replies, pinning, and operator-facing chat/member inspection so Feishu can operate more of the workspace directly. (#47968) Thanks @Takhoffman.
|
||||
- Feishu/topic threads: fetch full thread context, including prior bot replies, when starting a topic-thread session so follow-up turns in Feishu topics keep the right conversation state. (#45254) Thanks @Coobiw.
|
||||
- Feishu/media: keep native image, file, audio, and video/media handling aligned across outbound sends, inbound downloads, thread replies, directory/action aliases, and capability docs so unsupported areas are explicit instead of implied. (#47968) Thanks @Takhoffman.
|
||||
- Feishu/webhooks: harden signed webhook verification to use constant-time signature comparison and keep malformed short signatures fail-closed in webhook E2E coverage.
|
||||
- WhatsApp/reconnect: restore the append recency filter in the extension inbox monitor and handle protobuf `Long` timestamps correctly, so fresh post-reconnect append messages are processed while stale history sync stays suppressed. (#42588) Thanks @MonkeyLeeT.
|
||||
- WhatsApp/login: wait for pending creds writes before reopening after Baileys `515` pairing restarts in both QR login and `channels login` flows, and keep the restart coverage pinned to the real wrapped error shape plus per-account creds queues. (#27910) Thanks @asyncjason.
|
||||
- Telegram/message send: forward `--force-document` through the `sendPayload` path as well as `sendMedia`, so Telegram payload sends with `channelData` keep uploading images as documents instead of silently falling back to compressed photo sends. (#47119) Thanks @thepagent.
|
||||
- Telegram/message chunking: preserve spaces, paragraph separators, and word boundaries when HTML overflow rechunking splits formatted replies. (#47274) Thanks @obviyus.
|
||||
- Z.AI/onboarding: detect a working default model even for explicit `zai-coding-*` endpoint choices, so Coding Plan setup can keep the selected endpoint while defaulting to `glm-5` when available or `glm-4.7` as fallback. (#45969) Thanks @obviyus.
|
||||
- Z.AI/onboarding: add `glm-5-turbo` to the default Z.AI provider catalog so onboarding-generated configs expose the new model alongside the existing GLM defaults. (#46670) Thanks @tomsun28.
|
||||
- Zalo Personal/group gating: stop reapplying `dmPolicy.allowFrom` as a sender gate for already-allowlisted groups when `groupAllowFrom` is unset, so any member of an allowed group can trigger replies while DMs stay restricted. (#46663) Fixes #40146. Thanks @Takhoffman.
|
||||
- Zalo/plugin runtime: export `resolveClientIp` from `openclaw/plugin-sdk/zalo` so installed builds no longer crash on startup when the webhook monitor loads from the packaged extension instead of the monorepo source tree. (#46549) Thanks @No898.
|
||||
- Docker/live tests: mount external CLI auth homes into writable container copies, derive Codex OAuth expiry from JWT `exp`, refresh synced CLI creds instead of trusting stale cached expiry, and make gateway live probes wait on transcript output so `pnpm test:docker:all` stays green in Linux.
|
||||
- Plugins/install precedence: keep bundled plugins ahead of auto-discovered globals by default, but let an explicitly installed plugin record win its own duplicate-id tie so installed channel plugins load from `~/.openclaw/extensions` after `openclaw plugins install`. (#46722) Thanks @Takhoffman.
|
||||
- Control UI/logging: make browser-safe logger imports avoid eager temp-dir resolution so the bundled Control UI no longer crashes to a blank screen when logging reaches `tmp-openclaw-dir`. (#48469) Fixes #48062. Thanks @7inspire.
|
||||
- Plugins/scoped ids: preserve scoped plugin ids during install and config keying, and keep bundled plugins ahead of discovered duplicate ids by default so `@scope/name` plugins no longer collide with unscoped installs. (#47413) Thanks @vincentkoc.
|
||||
- Gateway/watch mode: restart on bundled-plugin package and manifest metadata changes, rebuild `dist` for extension source and `tsdown.config.ts` changes, and still ignore extension docs. (#47571) Thanks @gumadeiras.
|
||||
- Gateway/watch mode: recreate bundled plugin runtime metadata after clean or stale `dist` states, so `pnpm gateway:watch` no longer fails on missing `dist/extensions/*/openclaw.plugin.json` manifests after a rebuild. Thanks @gumadeiras.
|
||||
- Control UI/chat sessions: show human-readable labels in the grouped session dropdown again, keep unique scoped fallbacks when metadata is missing, and disambiguate duplicate labels only when needed. (#45130) Thanks @luzhidong.
|
||||
- Control UI: scope persisted session selection per gateway, prevent stale session bleed across tokenized gateway opens, and cap stored gateway session history. (#47453) Thanks @sallyom.
|
||||
- Control UI/dashboard: preserve structured gateway shutdown reasons across restart disconnects so config-triggered restarts no longer fall back to `disconnected (1006): no reason`. (#46580) Fixes #46532. Thanks @vincentkoc.
|
||||
- Android/chat: theme the thinking dropdown and TLS trust dialogs explicitly so popup surfaces match the active app theme instead of falling back to mismatched Material defaults.
|
||||
- Group mention gating: reject invalid and unsafe nested-repetition `mentionPatterns`, reuse the shared safe config-regex compiler across mention stripping and detection, and cache strip-time regex compilation so noisy groups avoid repeated recompiles.
|
||||
- Browser/profiles: drop the auto-created `chrome-relay` browser profile; users who need the Chrome extension relay must now create their own profile via `openclaw browser create-profile`. (#46596) Fixes #45777. Thanks @odysseus0.
|
||||
- CI/channel test routing: move the built-in channel suites into `test:channels` and keep them out of `test:extensions`, so extension CI no longer fails after the channel migration while targeted test routing still sends Slack, Signal, and iMessage suites to the right lane. (#46066) Thanks @scoootscooob.
|
||||
- Docs/Mintlify: fix MDX marker syntax on Perplexity, Model Providers, Moonshot, and exec approvals pages so local docs preview no longer breaks rendering or leaves stale pages unpublished. (#46695) Thanks @velvet-shark.
|
||||
- Gateway/config validation: stop treating the implicit default memory slot as a required explicit plugin config, so startup no longer fails with `plugins.slots.memory: plugin not found: memory-core` when `memory-core` was only inferred. (#47494) Thanks @ngutman.
|
||||
- Tlon: honor explicit empty allowlists and defer cite expansion. (#46788) Thanks @zpbrent and @vincentkoc.
|
||||
- Tlon/DM auth: defer cited-message expansion until after DM authorization and owner command handling, so unauthorized DMs and owner approval/admin commands no longer trigger cross-channel cite fetches before the deny or command path.
|
||||
- Docs/security audit: spell out that `gateway.controlUi.allowedOrigins: ["*"]` is an explicit allow-all browser-origin policy and should be avoided outside tightly controlled local testing.
|
||||
- Gateway/auth: clear self-declared scopes for device-less trusted-proxy Control UI sessions so proxy-authenticated connects cannot claim admin or secrets scopes without a bound device identity.
|
||||
- Nodes/pending actions: re-check queued foreground actions against the current node command policy before returning them to the node. (#46815) Thanks @zpbrent and @vincentkoc.
|
||||
- Node/startup: remove leftover debug `console.log("node host PATH: ...")` that printed the resolved PATH on every `openclaw node run` invocation. (#46515) Fixes #46411. Thanks @ademczuk.
|
||||
- CLI/completion: reduce recursive completion-script string churn and fix nested PowerShell command-path matching so generated nested completions resolve on PowerShell too. (#45537) Thanks @yiShanXin and @vincentkoc.
|
||||
- Slack/startup: harden `@slack/bolt` import interop across current bundled runtime shapes so Slack monitors no longer crash with `App is not a constructor` after plugin-sdk bundling changes. (#45953) Thanks @merc1305.
|
||||
- Windows/gateway status: accept `schtasks` `Last Result` output as an alias for `Last Run Result`, so running scheduled-task installs no longer show `Runtime: unknown`. (#47844) Thanks @MoerAI.
|
||||
- ACP/acpx: resolve the bundled plugin root from the actual plugin directory so plugin-local installs stay under `dist/extensions/acpx` instead of escaping to `dist/extensions` and failing runtime setup. (#47601) Thanks @ngutman.
|
||||
- Gateway/websocket pairing bypass for disabled auth: skip device-pairing enforcement for Control UI operator sessions when `gateway.auth.mode=none`, so reverse-proxied dashboards no longer get stuck on `pairing required` despite auth being explicitly disabled. (#47148) Thanks @ademczuk.
|
||||
- Control UI/model switching: preserve the selected provider prefix when switching models from the chat dropdown, so multi-provider setups no longer send `anthropic/gpt-5.2`-style mismatches when the user picked `openai/gpt-5.2`. (#47581) Thanks @chrishham.
|
||||
- Control UI/storage: scope persisted settings keys by gateway base path, with migration from the legacy shared key, so multiple gateways under one domain stop overwriting each other's dashboard preferences. (#47932) Thanks @bobBot-claw.
|
||||
- Agents/usage tracking: stop forcing `supportsUsageInStreaming: false` on non-native OpenAI-completions providers so compatible backends report token usage and cost again instead of showing all zeros. (#46500) Fixes #46142. Thanks @ademczuk.
|
||||
- ACP/acpx: keep plugin-local backend installs under `extensions/acpx` in live repo checkouts so rebuilds no longer delete the runtime binary, and avoid package-lock churn during runtime repair.
|
||||
- Plugins/subagents: preserve gateway-owned plugin subagent access across runtime, tool, and embedded-runner load paths so gateway plugin tools and context engines can still spawn and manage subagents after the loader cache split. (#46648) Thanks @jalehman.
|
||||
- Control UI/overview: keep the language dropdown aligned with the persisted locale during dashboard startup so refreshing the page does not fall back to English before locale hydration completes. (#48019) Thanks @git-jxj.
|
||||
- Agents/compaction: rerun transcript repair after `session.compact()` so orphaned `tool_result` blocks cannot survive compaction and break later Anthropic requests. (#16095) thanks @claw-sylphx.
|
||||
- Agents/compaction: trigger overflow recovery from the tool-result guard once post-compaction context still exceeds the safe threshold, so long tool loops compact before the next model call hard-fails. (#29371) thanks @keshav55.
|
||||
- macOS/exec approvals: harden exec-host request HMAC verification to use a timing-safe compare and keep malformed or truncated signatures fail-closed in focused IPC auth coverage.
|
||||
- Gateway/exec approvals: surface requested env override keys in gateway-host approval prompts so operators can review surviving env context without inheriting noisy base host env.
|
||||
- Telegram/network: preserve sticky IPv4 fallback state across polling restarts so hosts with unstable IPv6 to `api.telegram.org` stop re-triggering repeated Telegram timeouts after each restart. (#48282) Thanks @yassinebkr.
|
||||
- Plugins/subagents: forward per-run provider and model overrides through gateway plugin subagent dispatch so plugin-launched agent delegations honor explicit model selection again. (#48277) Thanks @jalehman.
|
||||
- Agents/compaction: write minimal boundary summaries for empty preparations while keeping split-turn prefixes on the normal path, so no-summarizable-message sessions stop retriggering the safeguard loop. (#42215) thanks @lml2468.
|
||||
- Models/chat commands: keep `/model ...@YYYYMMDD` version suffixes intact by default, but still honor matching stored numeric auth-profile overrides for the same provider. (#48896) Thanks @Alix-007.
|
||||
- Gateway/channels: serialize per-account channel startup so overlapping starts do not boot the same provider twice, preventing MS Teams `EADDRINUSE` crash loops during startup and restart. (#49583) Thanks @sudie-codes.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Agents/bootstrap warnings: move bootstrap truncation warnings out of the system prompt and into the per-turn prompt body so prompt-cache reuse stays stable when truncation warnings appear or disappear. (#48753) Thanks @scoootscooob and @obviyus.
|
||||
- Telegram/DM topic session keys: route named-account DM topics through the same per-account base session key across inbound messages, native commands, and session-state lookups so `/status` and thread recovery stop creating phantom `agent:main:main:thread:...` sessions. (#48204) Thanks @vincentkoc.
|
||||
- macOS/node service startup: use `openclaw node start/stop --json` from the Mac app instead of the removed `openclaw service node ...` command shape, so current CLI installs expose the full node exec surface again. (#46843) Fixes #43171. Thanks @Br1an67.
|
||||
- macOS/launch at login: stop emitting `KeepAlive` for the desktop app launch agent so OpenClaw no longer relaunches immediately after a manual quit while launch at login remains enabled. (#40213) Thanks @stablegenius49.
|
||||
- ACP/gateway startup: use direct Telegram and Discord startup/status helpers instead of routing probes through the plugin runtime, and prepend the selected daemon Node bin dir to service PATH so plugin-local installs can still find `npm` and `pnpm`.
|
||||
- ACP/configured bindings: reinitialize configured ACP sessions that are stuck in `error` state instead of reusing the failed runtime.
|
||||
- Mattermost/DM send: retry transient direct-channel creation failures for DM deliveries, with configurable backoff and per-request timeout. (#42398) Thanks @JonathanJing.
|
||||
- Telegram/network: unify API and media fetches under the same sticky IPv4 and pinned-IP fallback chain, and re-validate pinned override addresses against SSRF policy. (#49148) Thanks @obviyus.
|
||||
- Agents/prompt composition: append bootstrap truncation warnings to the current-turn prompt and add regression coverage for stable system-prompt cache invariants. (#49237) Thanks @scoootscooob.
|
||||
- Gateway/auth: add regression coverage that keeps device-less trusted-proxy Control UI sessions off privileged pairing approval RPCs. Thanks @vincentkoc.
|
||||
- Plugins/runtime-api: pin extension runtime-api export surfaces with explicit guardrail coverage so future surface creep becomes a deliberate diff. Thanks @vincentkoc.
|
||||
- Telegram/security: add regression coverage proving pinned fallback host overrides stay bound to Telegram and delegate non-matching hostnames back to the original lookup path. Thanks @vincentkoc.
|
||||
- Secrets/exec refs: require explicit `--allow-exec` for `secrets apply` write plans that contain exec SecretRefs/providers, and align audit/configure/apply dry-run behavior to skip exec checks unless opted in to prevent unexpected command side effects. (#49417) Thanks @restriction and @joshavant.
|
||||
- Tools/image generation: add bundled fal image generation support so `image_generate` can target `fal/*` models with `FAL_KEY`, including single-image edit flows via FLUX image-to-image. Thanks @vincentkoc.
|
||||
- xAI/web search: add missing Grok credential metadata so the bundled provider registration type-checks again. (#49472) thanks @scoootscooob.
|
||||
- Signal/runtime API: re-export `SignalAccountConfig` so Signal account resolution type-checks again. (#49470) Thanks @scoootscooob.
|
||||
- Google Chat/runtime API: thin the private runtime barrel onto the curated public SDK surface while keeping public Google Chat exports intact. (#49504) Thanks @scoootscooob.
|
||||
|
||||
### Breaking
|
||||
|
||||
- Skills/image generation: remove the bundled `nano-banana-pro` skill wrapper. Use `agents.defaults.imageGenerationModel.primary: "google/gemini-3-pro-image-preview"` for the native Nano Banana-style path instead.
|
||||
|
||||
- Browser/Chrome MCP: remove the legacy Chrome extension relay path, bundled extension assets, `driver: "extension"`, and `browser.relayBindHost`. Run `openclaw doctor --fix` to migrate host-local browser config to `existing-session` / `user`; Docker, headless, sandbox, and remote browser flows still use raw CDP. (#47893) Thanks @vincentkoc.
|
||||
- Plugins/runtime: remove the public `openclaw/extension-api` surface with no compatibility shim. Bundled plugins must use injected runtime for host-side operations (for example `api.runtime.agent.runEmbeddedPiAgent`) and any remaining direct imports must come from narrow `openclaw/plugin-sdk/*` subpaths instead of the monolithic SDK root.
|
||||
- Tools/image generation: standardize the stock image create/edit path on the core `image_generate` tool. The old `nano-banana-pro` docs/examples are gone; if you previously copied that sample-skill config, switch to `agents.defaults.imageGenerationModel` for built-in image generation or install a separate third-party skill explicitly.
|
||||
- Skills/image generation: remove the bundled `nano-banana-pro` skill wrapper. Use `agents.defaults.imageGenerationModel.primary: "google/gemini-3-pro-image-preview"` for the native Nano Banana-style path instead.
|
||||
- Plugins/message discovery: require `ChannelMessageActionAdapter.describeMessageTool(...)` for shared `message` tool discovery. The legacy `listActions`, `getCapabilities`, and `getToolSchema` adapter methods are removed. Plugin authors should migrate message discovery to `describeMessageTool(...)` and keep channel-specific action runtime code inside the owning plugin package. Thanks @gumadeiras.
|
||||
- Exec/env sandbox: block build-tool JVM injection (`MAVEN_OPTS`, `SBT_OPTS`, `GRADLE_OPTS`, `ANT_OPTS`), glibc tunable exploitation (`GLIBC_TUNABLES`), and .NET dependency resolution hijack (`DOTNET_ADDITIONAL_DEPS`) from the host exec environment, and restrict Gradle init script redirect (`GRADLE_USER_HOME`) as an override-only block so user-configured Gradle homes still propagate. (#49702)
|
||||
- Gateway/startup: load bundled channel plugins from compiled `dist/extensions` entries in built installs, so gateway boot no longer recompiles bundled extension TypeScript on every startup and WhatsApp-class cold starts drop back to seconds instead of tens of seconds or worse.
|
||||
- Gateway/watch mode: restart on bundled-plugin package and manifest metadata changes, rebuild `dist` for extension source and `tsdown.config.ts` changes, and still ignore extension docs. (#47571) thanks @gumadeiras.
|
||||
- Gateway/watch mode: recreate bundled plugin runtime metadata after clean or stale `dist` states, so `pnpm gateway:watch` no longer fails on missing `dist/extensions/*/openclaw.plugin.json` manifests after a rebuild. Thanks @gumadeiras.
|
||||
- Plugins/context engines: enforce owner-aware context-engine registration on both loader and public SDK paths so plugins cannot spoof privileged ownership, claim the core `legacy` engine id, or overwrite an existing engine id through direct SDK imports. (#47595) Thanks @vincentkoc.
|
||||
|
||||
## 2026.3.13
|
||||
|
||||
@@ -175,6 +88,10 @@ Docs: https://docs.openclaw.ai
|
||||
- Cron/sessions: add `sessionTarget: "current"` and `session:<id>` support so cron jobs can bind to the creating session or a persistent named session instead of only `main` or `isolated`. Thanks @kkhomej33-netizen and @ImLukeF.
|
||||
- Telegram/message send: add `--force-document` so Telegram image and GIF sends can upload as documents without compression. (#45111) Thanks @thepagent.
|
||||
|
||||
### Breaking
|
||||
|
||||
- **BREAKING:** Agents now load at most one root memory bootstrap file. `MEMORY.md` wins; `memory.md` is only used when `MEMORY.md` is absent. If you intentionally kept both files and depended on both being injected, merge them before upgrade. This also fixes duplicate memory injection on case-insensitive Docker mounts. (#26054) Thanks @Lanfei.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Dashboard/chat UI: stop reloading full chat history on every live tool result in dashboard v2 so tool-heavy runs no longer trigger UI freeze/re-render storms while the final event still refreshes persisted history. (#45541) Thanks @BunsDev.
|
||||
@@ -234,12 +151,6 @@ Docs: https://docs.openclaw.ai
|
||||
- Auth/login lockout recovery: clear stale `auth_permanent` and `billing` disabled state for all profiles matching the target provider when `openclaw models auth login` is invoked, so users locked out by expired or revoked OAuth tokens can recover by re-authenticating instead of waiting for the cooldown timer to expire. (#43057)
|
||||
- Auto-reply/context-engine compaction: persist the exact embedded-run metadata compaction count for main and followup runner session accounting, so metadata-only auto-compactions no longer undercount multi-compaction runs. (#42629) thanks @uf-hy.
|
||||
- Auth/Codex CLI reuse: sync reused Codex CLI credentials into the supported `openai-codex:default` OAuth profile instead of reviving the deprecated `openai-codex:codex-cli` slot, so doctor cleanup no longer loops. (#45353) thanks @Gugu-sugar.
|
||||
- Deps/audit: bump the pinned `fast-xml-parser` override to the first patched release so `pnpm audit --prod --audit-level=high` no longer fails on the AWS Bedrock XML builder path. Thanks @vincentkoc.
|
||||
- Hooks/after_compaction: forward `sessionFile` for direct/manual compaction events and add `sessionFile` plus `sessionKey` to wired auto-compaction hook context so plugins receive the session metadata already declared in the hook types. (#40781) Thanks @jarimustonen.
|
||||
|
||||
### Breaking
|
||||
|
||||
- **BREAKING:** Agents now load at most one root memory bootstrap file. `MEMORY.md` wins; `memory.md` is only used when `MEMORY.md` is absent. If you intentionally kept both files and depended on both being injected, merge them before upgrade. This also fixes duplicate memory injection on case-insensitive Docker mounts. (#26054) Thanks @Lanfei.
|
||||
|
||||
## 2026.3.12
|
||||
|
||||
@@ -331,16 +242,13 @@ Docs: https://docs.openclaw.ai
|
||||
- Agents/Anthropic replay: drop replayed assistant thinking blocks for native Anthropic and Bedrock Claude providers so persisted follow-up turns no longer fail on stored thinking blocks. (#44843) Thanks @jmcte.
|
||||
- Docs/Brave pricing: escape literal dollar signs in Brave Search cost text so the docs render the free credit and per-request pricing correctly. (#44989) Thanks @keelanfh.
|
||||
- Feishu/file uploads: preserve literal UTF-8 filenames in `im.file.create` so Chinese and other non-ASCII filenames no longer appear percent-encoded in chat. (#34262) Thanks @fabiaodemianyang and @KangShuaiFu.
|
||||
- Agents/compaction safeguard: trim large kept `toolResult` payloads consistently for budgeting, pruning, and identifier seeding, then restore preserved payloads after prune so oversized safeguard summaries stay stable. (#44133) thanks @SayrWolfridge.
|
||||
- Agents/compaction: compare post-compaction token sanity checks against full-session pre-compaction totals and skip the check when token estimation fails, so sessions with large bootstrap context keep real token counts instead of falling back to unknown. (#28347) thanks @efe-arv.
|
||||
- Discord/gateway startup: treat plain-text and transient `/gateway/bot` metadata fetch failures as transient startup errors so Discord gateway boot no longer crashes on unhandled rejections. (#44397) Thanks @jalehman.
|
||||
- Agents/Ollama overflow: rewrite Ollama `prompt too long` API payloads through the normal context-overflow sanitizer so embedded sessions keep the friendly overflow copy and auto-compaction trigger. (#34019) thanks @lishuaigit.
|
||||
|
||||
- Control UI/auth: restore one-time legacy `?token=` imports for shared Control UI links while keeping `#token=` preferred, and carry pending query tokens through gateway URL confirmation so compatibility links still authenticate after confirmation. (#43979) Thanks @stim64045-spec.
|
||||
- Plugins/context engines: retry legacy lifecycle calls once without `sessionKey` when older plugins reject that field, memoize legacy mode after the first strict-schema fallback, and preserve non-compat runtime errors without retry. (#44779) thanks @hhhhao28.
|
||||
|
||||
## 2026.3.11
|
||||
|
||||
### Security
|
||||
|
||||
- Gateway/WebSocket: enforce browser origin validation for all browser-originated connections regardless of whether proxy headers are present, closing a cross-site WebSocket hijacking path in `trusted-proxy` mode that could grant untrusted origins `operator.admin` access. (GHSA-5wcw-8jjv-m286)
|
||||
|
||||
### Changes
|
||||
|
||||
- OpenRouter/models: add temporary Hunter Alpha and Healer Alpha entries to the built-in catalog so OpenRouter users can try the new free stealth models during their roughly one-week availability window. (#43642) Thanks @ping-Toven.
|
||||
@@ -362,6 +270,10 @@ Docs: https://docs.openclaw.ai
|
||||
- Mattermost/reply threading: add `channels.mattermost.replyToMode` for channel and group messages so top-level posts can start thread-scoped sessions without the manual reply-then-thread workaround. (#29587) Thanks @teconomix.
|
||||
- iOS/push relay: add relay-backed official-build push delivery with App Attest + receipt verification, gateway-bound send delegation, and config-based relay URL setup on the gateway. (#43369) Thanks @ngutman.
|
||||
|
||||
### Breaking
|
||||
|
||||
- Cron/doctor: tighten isolated cron delivery so cron jobs can no longer notify through ad hoc agent sends or fallback main-session summaries, and add `openclaw doctor --fix` migration for legacy cron storage and legacy notify/webhook delivery metadata. (#40998) Thanks @mbelinky.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Windows/install: stop auto-installing `node-llama-cpp` during normal npm CLI installs so `openclaw@latest` no longer fails on Windows while building optional local-embedding dependencies.
|
||||
@@ -472,15 +384,6 @@ Docs: https://docs.openclaw.ai
|
||||
- Memory/QMD Windows: fail closed when `qmd.cmd` or `mcporter.cmd` wrappers cannot be resolved to a direct entrypoint, so memory search no longer falls back to shell execution on Windows.
|
||||
- macOS/remote gateway: stop PortGuardian from killing Docker Desktop and other external listeners on the gateway port in remote mode, so containerized and tunneled gateway setups no longer lose their port-forward owner on app startup. (#6755) Thanks @teslamint.
|
||||
- Feishu/streaming recovery: clear stale `streamingStartPromise` when card creation fails (HTTP 400) so subsequent messages can retry streaming instead of silently dropping all future replies. Fixes #43322.
|
||||
- Exec/env sandbox: block JVM agent injection (`JAVA_TOOL_OPTIONS`, `_JAVA_OPTIONS`, `JDK_JAVA_OPTIONS`), Python breakpoint hijack (`PYTHONBREAKPOINT`), and .NET startup hooks (`DOTNET_STARTUP_HOOKS`) from the host exec environment. (#49025)
|
||||
|
||||
### Security
|
||||
|
||||
- Gateway/WebSocket: enforce browser origin validation for all browser-originated connections regardless of whether proxy headers are present, closing a cross-site WebSocket hijacking path in `trusted-proxy` mode that could grant untrusted origins `operator.admin` access. (GHSA-5wcw-8jjv-m286)
|
||||
|
||||
### Breaking
|
||||
|
||||
- Cron/doctor: tighten isolated cron delivery so cron jobs can no longer notify through ad hoc agent sends or fallback main-session summaries, and add `openclaw doctor --fix` migration for legacy cron storage and legacy notify/webhook delivery metadata. (#40998) Thanks @mbelinky.
|
||||
|
||||
## 2026.3.8
|
||||
|
||||
@@ -594,6 +497,10 @@ Docs: https://docs.openclaw.ai
|
||||
- Google/Gemini 3.1 Flash-Lite: add first-class `google/gemini-3.1-flash-lite-preview` support across model-id normalization, default aliases, media-understanding image lookups, Google Gemini CLI forward-compat fallback, and docs.
|
||||
- Agents/compaction model override: allow `agents.defaults.compaction.model` to route compaction summarization through a different model than the main session, and document the override across config help/reference surfaces. (#38753) thanks @starbuck100.
|
||||
|
||||
### Breaking
|
||||
|
||||
- **BREAKING:** Gateway auth now requires explicit `gateway.auth.mode` when both `gateway.auth.token` and `gateway.auth.password` are configured (including SecretRefs). Set `gateway.auth.mode` to `token` or `password` before upgrade to avoid startup/pairing/TUI failures. (#35094) Thanks @joshavant.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Models/MiniMax: stop advertising removed `MiniMax-M2.5-Lightning` in built-in provider catalogs, onboarding metadata, and docs; keep the supported fast-tier model as `MiniMax-M2.5-highspeed`.
|
||||
@@ -664,7 +571,6 @@ Docs: https://docs.openclaw.ai
|
||||
- Control UI/markdown fallback regression coverage: add explicit regression assertions for parser-error fallback behavior so malformed markdown no longer risks reintroducing hard-crash rendering paths in future markdown/parser upgrades. (#36445) Thanks @BinHPdev.
|
||||
- Web UI/config form: treat `additionalProperties: true` object schemas as editable map entries instead of unsupported fields so Accounts-style maps stay editable in form mode. (#35380, supersedes #32072) Thanks @stakeswky and @liuxiaopai-ai.
|
||||
- Feishu/streaming card delivery synthesis: unify snapshot and delta streaming merge semantics, apply overlap-aware final merge, suppress duplicate final text delivery (including text+media final packets), prefer topic-thread `message.reply` routing when a reply target exists, and tune card print cadence to avoid duplicate incremental rendering. (from #33245, #32896, #33840) Thanks @rexl2018, @kcinzgg, and @aerelune.
|
||||
- macOS/tray menu: keep injected sessions and device rows below the controls section so toggles and action buttons stay visible even when many sessions are active. (#38079) Thanks @bernesto.
|
||||
- Feishu/group mention detection: carry startup-probed bot display names through monitor dispatch so `requireMention` checks compare against current bot identity instead of stale config names, fixing missed `@bot` handling in groups while preserving multi-bot false-positive guards. (#36317, #34271) Thanks @liuxiaopai-ai.
|
||||
- Security/dependency audit: patch transitive Hono vulnerabilities by pinning `hono` to `4.12.5` and `@hono/node-server` to `1.19.10` in production resolution paths. Thanks @shakkernerd.
|
||||
- Security/dependency audit: bump `tar` to `7.5.10` (from `7.5.9`) to address the high-severity hardlink path traversal advisory (`GHSA-qffp-2rhf-9h96`). Thanks @shakkernerd.
|
||||
@@ -919,10 +825,6 @@ Docs: https://docs.openclaw.ai
|
||||
- Mattermost/DM media uploads: resolve bare 26-character Mattermost IDs user-first for direct messages so media sends no longer fail with `403 Forbidden` when targets are configured as unprefixed user IDs. (#29925) Thanks @teconomix.
|
||||
- Voice-call/OpenAI TTS config parity: add missing `speed`, `instructions`, and `baseUrl` fields to the OpenAI TTS config schema and gate `instructions` to supported models so voice-call overrides validate and route cleanly through core TTS. (#39226) Thanks @ademczuk.
|
||||
|
||||
### Breaking
|
||||
|
||||
- **BREAKING:** Gateway auth now requires explicit `gateway.auth.mode` when both `gateway.auth.token` and `gateway.auth.password` are configured (including SecretRefs). Set `gateway.auth.mode` to `token` or `password` before upgrade to avoid startup/pairing/TUI failures. (#35094) Thanks @joshavant.
|
||||
|
||||
## 2026.3.2
|
||||
|
||||
### Changes
|
||||
@@ -951,6 +853,13 @@ Docs: https://docs.openclaw.ai
|
||||
- Gateway/input_image MIME validation: sniff uploaded image bytes before MIME allowlist enforcement again so declared image types cannot mask concrete non-image payloads, while keeping HEIC/HEIF normalization behavior scoped to actual HEIC inputs. Thanks @vincentkoc.
|
||||
- Zalo Personal plugin (`@openclaw/zalouser`): keep canonical DM routing while preserving legacy DM session continuity on upgrade, and preserve provider-native `g-`/`u-` target ids in outbound send and directory flows so #33992 lands without breaking existing sessions or stored targets. (#33992) Thanks @darkamenosa.
|
||||
|
||||
### Breaking
|
||||
|
||||
- **BREAKING:** Onboarding now defaults `tools.profile` to `messaging` for new local installs (interactive + non-interactive). New setups no longer start with broad coding/system tools unless explicitly configured.
|
||||
- **BREAKING:** ACP dispatch now defaults to enabled unless explicitly disabled (`acp.dispatch.enabled=false`). If you need to pause ACP turn routing while keeping `/acp` controls, set `acp.dispatch.enabled=false`. Docs: https://docs.openclaw.ai/tools/acp-agents
|
||||
- **BREAKING:** Plugin SDK removed `api.registerHttpHandler(...)`. Plugins must register explicit HTTP routes via `api.registerHttpRoute({ path, auth, match, handler })`, and dynamic webhook lifecycles should use `registerPluginHttpRoute(...)`.
|
||||
- **BREAKING:** Zalo Personal plugin (`@openclaw/zalouser`) no longer depends on external `zca`-compatible CLI binaries (`openzca`, `zca-cli`) for runtime send/listen/login; operators should use `openclaw channels login --channel zalouser` after upgrade to refresh sessions in the new JS-native path.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Feishu/Outbound render mode: respect Feishu account `renderMode` in outbound sends so card mode (and auto-detected markdown tables/code blocks) uses markdown card delivery instead of always sending plain text. (#31562) Thanks @arkyu2077.
|
||||
@@ -1137,13 +1046,6 @@ Docs: https://docs.openclaw.ai
|
||||
- Tests/Subagent announce: set `OPENCLAW_TEST_FAST=1` before importing `subagent-announce` format suites so module-level fast-mode constants are captured deterministically on Windows CI, preventing timeout flakes in nested completion announce coverage. (#31370) Thanks @zwffff.
|
||||
- Control UI/markdown recursion fallback: catch markdown parser failures and safely render escaped plain-text fallback instead of crashing the Control UI on pathological markdown history payloads. (#36445, fixes #36213) Thanks @BinHPdev.
|
||||
|
||||
### Breaking
|
||||
|
||||
- **BREAKING:** Onboarding now defaults `tools.profile` to `messaging` for new local installs (interactive + non-interactive). New setups no longer start with broad coding/system tools unless explicitly configured.
|
||||
- **BREAKING:** ACP dispatch now defaults to enabled unless explicitly disabled (`acp.dispatch.enabled=false`). If you need to pause ACP turn routing while keeping `/acp` controls, set `acp.dispatch.enabled=false`. Docs: https://docs.openclaw.ai/tools/acp-agents
|
||||
- **BREAKING:** Plugin SDK removed `api.registerHttpHandler(...)`. Plugins must register explicit HTTP routes via `api.registerHttpRoute({ path, auth, match, handler })`, and dynamic webhook lifecycles should use `registerPluginHttpRoute(...)`.
|
||||
- **BREAKING:** Zalo Personal plugin (`@openclaw/zalouser`) no longer depends on external `zca`-compatible CLI binaries (`openzca`, `zca-cli`) for runtime send/listen/login; operators should use `openclaw channels login --channel zalouser` after upgrade to refresh sessions in the new JS-native path.
|
||||
|
||||
## 2026.3.1
|
||||
|
||||
### Changes
|
||||
@@ -1171,6 +1073,11 @@ Docs: https://docs.openclaw.ai
|
||||
- OpenAI/WebSocket warm-up: add optional OpenAI Responses WebSocket warm-up (`response.create` with `generate:false`), enable it by default for `openai/*`, and expose `params.openaiWsWarmup` for per-model enable/disable control.
|
||||
- Agents/Subagents runtime events: replace ad-hoc subagent completion system-message handoff with typed internal completion events (`task_completion`) that are rendered consistently across direct and queued announce paths, with gateway/CLI plumbing for structured `internalEvents`.
|
||||
|
||||
### Breaking
|
||||
|
||||
- **BREAKING:** Node exec approval payloads now require `systemRunPlan`. `host=node` approval requests without that plan are rejected.
|
||||
- **BREAKING:** Node `system.run` execution now pins path-token commands to the canonical executable path (`realpath`) in both allowlist and approval execution flows. Integrations/tests that asserted token-form argv (for example `tr`) must now accept canonical paths (for example `/usr/bin/tr`).
|
||||
|
||||
### Fixes
|
||||
|
||||
- Feishu/Streaming card text fidelity: merge throttled/fragmented partial updates without dropping content and avoid newline injection when stitching chunk-style deltas so card-stream output matches final reply text. (#29616) Thanks @HaoHuaqing.
|
||||
@@ -1265,12 +1172,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Signal/Sync message null-handling: treat `syncMessage` presence (including `null`) as sync envelope traffic so replayed sentTranscript payloads cannot bypass loop guards after daemon restart. Landed from contributor PR #31138 by @Sid-Qin. Thanks @Sid-Qin.
|
||||
- Infra/fs-safe: sanitize directory-read failures so raw `EISDIR` text never leaks to messaging surfaces, with regression tests for both root-scoped and direct safe reads. Landed from contributor PR #31205 by @polooooo. Thanks @polooooo.
|
||||
|
||||
### Breaking
|
||||
|
||||
- **BREAKING:** Node exec approval payloads now require `systemRunPlan`. `host=node` approval requests without that plan are rejected.
|
||||
- **BREAKING:** Node `system.run` execution now pins path-token commands to the canonical executable path (`realpath`) in both allowlist and approval execution flows. Integrations/tests that asserted token-form argv (for example `tr`) must now accept canonical paths (for example `/usr/bin/tr`).
|
||||
|
||||
## 2026.2.27
|
||||
## Unreleased
|
||||
|
||||
### Changes
|
||||
|
||||
@@ -1545,6 +1447,10 @@ Docs: https://docs.openclaw.ai
|
||||
- Agents/Config: remind agents to call `config.schema` before config edits or config-field questions to avoid guessing. Thanks @thewilloftheshadow.
|
||||
- Dependencies: update workspace dependency pins and lockfile (Bedrock SDK `3.998.0`, `@mariozechner/pi-*` `0.55.1`, TypeScript native preview `7.0.0-dev.20260225.1`) while keeping `@buape/carbon` pinned.
|
||||
|
||||
### Breaking
|
||||
|
||||
- **BREAKING:** Heartbeat direct/DM delivery default is now `allow` again. To keep DM-blocked behavior from `2026.2.24`, set `agents.defaults.heartbeat.directPolicy: "block"` (or per-agent override).
|
||||
|
||||
### Fixes
|
||||
|
||||
- Slack/Identity: thread agent outbound identity (`chat:write.customize` overrides) through the channel reply delivery path so per-agent username, icon URL, and icon emoji are applied to all Slack replies including media messages. (#27134) Thanks @hou-rong.
|
||||
@@ -1608,10 +1514,6 @@ Docs: https://docs.openclaw.ai
|
||||
- Tests/Low-memory stability: disable Vitest `vmForks` by default on low-memory local hosts (`<64 GiB`), keep low-profile extension lane parallelism at 4 workers, and align cron isolated-agent tests with `setSessionRuntimeModel` usage to avoid deterministic suite failures. (#26324) Thanks @ngutman.
|
||||
- Feishu/WebSocket proxy: pass a proxy agent to Feishu WS clients from standard proxy environment variables and include plugin-local runtime dependency wiring so websocket mode works in proxy-constrained installs. (#26397) Thanks @colin719.
|
||||
|
||||
### Breaking
|
||||
|
||||
- **BREAKING:** Heartbeat direct/DM delivery default is now `allow` again. To keep DM-blocked behavior from `2026.2.24`, set `agents.defaults.heartbeat.directPolicy: "block"` (or per-agent override).
|
||||
|
||||
## 2026.2.24
|
||||
|
||||
### Changes
|
||||
@@ -1622,6 +1524,11 @@ Docs: https://docs.openclaw.ai
|
||||
- Security/Audit: add `security.trust_model.multi_user_heuristic` to flag likely shared-user ingress and clarify the personal-assistant trust model, with hardening guidance for intentional multi-user setups (`sandbox.mode="all"`, workspace-scoped FS, reduced tool surface, no personal/private identities on shared runtimes).
|
||||
- Dependencies: refresh key runtime and tooling packages across the workspace (Bedrock SDK, pi runtime stack, OpenAI, Google auth, and oxlint/oxfmt), while intentionally keeping `@buape/carbon` pinned.
|
||||
|
||||
### Breaking
|
||||
|
||||
- **BREAKING:** Heartbeat delivery now blocks direct/DM targets when destination parsing identifies a direct chat (for example `user:<id>`, Telegram user chat IDs, or WhatsApp direct numbers/JIDs). Heartbeat runs still execute, but direct-message delivery is skipped and only non-DM destinations (for example channel/group targets) can receive outbound heartbeat messages.
|
||||
- **BREAKING:** Security/Sandbox: block Docker `network: "container:<id>"` namespace-join mode by default for sandbox and sandbox-browser containers. To keep that behavior intentionally, set `agents.defaults.sandbox.docker.dangerouslyAllowContainerNamespaceJoin: true` (break-glass). Thanks @tdjackey for reporting.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Routing/Session isolation: harden followup routing so explicit cross-channel origin replies never fall back to the active dispatcher on route failure, preserve queued overflow summary routing metadata (`channel`/`to`/`thread`) across followup drain, and prefer originating channel context over internal provider tags for embedded followup runs. This prevents webchat/control-ui context from hijacking Discord-targeted replies in shared sessions. (#25864) Thanks @Gamedesigner.
|
||||
@@ -1701,11 +1608,6 @@ Docs: https://docs.openclaw.ai
|
||||
- Agents/Compaction: harden summarization prompts to preserve opaque identifiers verbatim (UUIDs, IDs, tokens, host/IP/port, URLs), reducing post-compaction identifier drift and hallucinated identifier reconstruction.
|
||||
- Security/Sandbox: canonicalize bind-mount source paths via existing-ancestor realpath so symlink-parent + non-existent-leaf paths cannot bypass allowed-source-roots or blocked-path checks. Thanks @tdjackey.
|
||||
|
||||
### Breaking
|
||||
|
||||
- **BREAKING:** Heartbeat delivery now blocks direct/DM targets when destination parsing identifies a direct chat (for example `user:<id>`, Telegram user chat IDs, or WhatsApp direct numbers/JIDs). Heartbeat runs still execute, but direct-message delivery is skipped and only non-DM destinations (for example channel/group targets) can receive outbound heartbeat messages.
|
||||
- **BREAKING:** Security/Sandbox: block Docker `network: "container:<id>"` namespace-join mode by default for sandbox and sandbox-browser containers. To keep that behavior intentionally, set `agents.defaults.sandbox.docker.dangerouslyAllowContainerNamespaceJoin: true` (break-glass). Thanks @tdjackey for reporting.
|
||||
|
||||
## 2026.2.23
|
||||
|
||||
### Changes
|
||||
@@ -1720,6 +1622,10 @@ Docs: https://docs.openclaw.ai
|
||||
- Agents/Config: support per-agent `params` overrides merged on top of model defaults (including `cacheRetention`) so mixed-traffic agents can tune cache behavior independently. (#17470, #17112) Thanks @rrenamed.
|
||||
- Agents/Bootstrap: cache bootstrap file snapshots per session key and clear them on session reset/delete, reducing prompt-cache invalidations from in-session `AGENTS.md`/`MEMORY.md` writes. (#22220) Thanks @anisoptera.
|
||||
|
||||
### Breaking
|
||||
|
||||
- **BREAKING:** browser SSRF policy now defaults to trusted-network mode (`browser.ssrfPolicy.dangerouslyAllowPrivateNetwork=true` when unset), and canonical config uses `browser.ssrfPolicy.dangerouslyAllowPrivateNetwork` instead of `browser.ssrfPolicy.allowPrivateNetwork`. `openclaw doctor --fix` migrates the legacy key automatically.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Security/Config: redact sensitive-looking dynamic catchall keys in `config.get` snapshots (for example `env.*` and `skills.entries.*.env.*`) and preserve round-trip restore behavior for those redacted sentinels. Thanks @merc1305.
|
||||
@@ -1765,10 +1671,6 @@ Docs: https://docs.openclaw.ai
|
||||
- Skills/Python: harden skill script packaging and validation edge cases (self-including `.skill` outputs, CRLF frontmatter parsing, strict `--days` validation, and safer image file loading), with expanded Python regression coverage. Thanks @vincentkoc.
|
||||
- Skills/Python: add CI + pre-commit linting (`ruff`) and pytest discovery coverage for Python scripts/tests under `skills/`, including package test execution from repo root. Thanks @vincentkoc.
|
||||
|
||||
### Breaking
|
||||
|
||||
- **BREAKING:** browser SSRF policy now defaults to trusted-network mode (`browser.ssrfPolicy.dangerouslyAllowPrivateNetwork=true` when unset), and canonical config uses `browser.ssrfPolicy.dangerouslyAllowPrivateNetwork` instead of `browser.ssrfPolicy.allowPrivateNetwork`. `openclaw doctor --fix` migrates the legacy key automatically.
|
||||
|
||||
## 2026.2.22
|
||||
|
||||
### Changes
|
||||
@@ -1793,6 +1695,14 @@ Docs: https://docs.openclaw.ai
|
||||
- Skills: remove bundled `food-order` skill from this repo; manage/install it from ClawHub instead.
|
||||
- Docs/Subagents: make thread-bound session guidance channel-first instead of Discord-specific, and list thread-supporting channels explicitly. (#23589) Thanks @osolmaz.
|
||||
|
||||
### Breaking
|
||||
|
||||
- **BREAKING:** removed Google Antigravity provider support and the bundled `google-antigravity-auth` plugin. Existing `google-antigravity/*` model/profile configs no longer work; migrate to `google-gemini-cli` or other supported providers.
|
||||
- **BREAKING:** tool-failure replies now hide raw error details by default. OpenClaw still sends a failure summary, but detailed error suffixes (for example provider/runtime messages and local path fragments) now require `/verbose on` or `/verbose full`.
|
||||
- **BREAKING:** CLI local onboarding now sets `session.dmScope` to `per-channel-peer` by default for new/implicit DM scope configuration. If you depend on shared DM continuity across senders, explicitly set `session.dmScope` to `main`. (#23468) Thanks @bmendonca3.
|
||||
- **BREAKING:** unify channel preview-streaming config to `channels.<channel>.streaming` with enum values `off | partial | block | progress`, and move Slack native stream toggle to `channels.slack.nativeStreaming`. Legacy keys (`streamMode`, Slack boolean `streaming`) are still read and migrated by `openclaw doctor --fix`, but canonical saved config/docs now use the unified names.
|
||||
- **BREAKING:** remove legacy Gateway device-auth signature `v1`. Device-auth clients must now sign `v2` payloads with the per-connection `connect.challenge` nonce and send `device.nonce`; nonce-less connects are rejected.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Sessions/Resilience: ignore invalid persisted `sessionFile` metadata and fall back to the derived safe transcript path instead of aborting session resolution for handlers and tooling. (#16061) Thanks @haoyifan and @vincentkoc.
|
||||
@@ -2019,14 +1929,6 @@ Docs: https://docs.openclaw.ai
|
||||
- Gateway/Daemon: verify gateway health after daemon restart.
|
||||
- Agents/UI text: stop rewriting normal assistant billing/payment language outside explicit error contexts. (#17834) Thanks @niceysam.
|
||||
|
||||
### Breaking
|
||||
|
||||
- **BREAKING:** removed Google Antigravity provider support and the bundled `google-antigravity-auth` plugin. Existing `google-antigravity/*` model/profile configs no longer work; migrate to `google-gemini-cli` or other supported providers.
|
||||
- **BREAKING:** tool-failure replies now hide raw error details by default. OpenClaw still sends a failure summary, but detailed error suffixes (for example provider/runtime messages and local path fragments) now require `/verbose on` or `/verbose full`.
|
||||
- **BREAKING:** CLI local onboarding now sets `session.dmScope` to `per-channel-peer` by default for new/implicit DM scope configuration. If you depend on shared DM continuity across senders, explicitly set `session.dmScope` to `main`. (#23468) Thanks @bmendonca3.
|
||||
- **BREAKING:** unify channel preview-streaming config to `channels.<channel>.streaming` with enum values `off | partial | block | progress`, and move Slack native stream toggle to `channels.slack.nativeStreaming`. Legacy keys (`streamMode`, Slack boolean `streaming`) are still read and migrated by `openclaw doctor --fix`, but canonical saved config/docs now use the unified names.
|
||||
- **BREAKING:** remove legacy Gateway device-auth signature `v1`. Device-auth clients must now sign `v2` payloads with the per-connection `connect.challenge` nonce and send `device.nonce`; nonce-less connects are rejected.
|
||||
|
||||
## 2026.2.21
|
||||
|
||||
### Changes
|
||||
@@ -2675,6 +2577,10 @@ Docs: https://docs.openclaw.ai
|
||||
- Onboarding/Providers: add first-class Hugging Face Inference provider support (provider wiring, onboarding auth choice/API key flow, and default-model selection), and preserve Hugging Face auth intent in auth-choice remapping (`tokenProvider=huggingface` with `authChoice=apiKey`) while skipping env-override prompts when an explicit token is provided. (#13472) Thanks @Josephrp.
|
||||
- Onboarding/Providers: add `minimax-api-key-cn` auth choice for the MiniMax China API endpoint. (#15191) Thanks @liuy.
|
||||
|
||||
### Breaking
|
||||
|
||||
- Config/State: removed legacy `.moltbot` auto-detection/migration and `moltbot.json` config candidates. If you still have state/config under `~/.moltbot`, move it to `~/.openclaw` (recommended) or set `OPENCLAW_STATE_DIR` / `OPENCLAW_CONFIG_PATH` explicitly.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Gateway/Auth: add trusted-proxy mode hardening follow-ups by keeping `OPENCLAW_GATEWAY_*` env compatibility, auto-normalizing invalid setup combinations in interactive `gateway configure` (trusted-proxy forces `bind=lan` and disables Tailscale serve/funnel), and suppressing shared-secret/rate-limit audit findings that do not apply to trusted-proxy deployments. (#15940) Thanks @nickytonline.
|
||||
@@ -2777,10 +2683,6 @@ Docs: https://docs.openclaw.ai
|
||||
- Docs/Mermaid: remove hardcoded Mermaid init theme blocks from four docs diagrams so dark mode inherits readable theme defaults. (#15157) Thanks @heytulsiprasad.
|
||||
- Security/Pairing: generate 256-bit base64url device and node pairing tokens and use byte-safe constant-time verification to avoid token-compare edge-case failures. (#16535) Thanks @FaizanKolega, @gumadeiras.
|
||||
|
||||
### Breaking
|
||||
|
||||
- Config/State: removed legacy `.moltbot` auto-detection/migration and `moltbot.json` config candidates. If you still have state/config under `~/.moltbot`, move it to `~/.openclaw` (recommended) or set `OPENCLAW_STATE_DIR` / `OPENCLAW_CONFIG_PATH` explicitly.
|
||||
|
||||
## 2026.2.12
|
||||
|
||||
### Changes
|
||||
@@ -2792,6 +2694,10 @@ Docs: https://docs.openclaw.ai
|
||||
- Discord: add role-based allowlists and role-based agent routing. (#10650) Thanks @Minidoracat.
|
||||
- Config: avoid redacting `maxTokens`-like fields during config snapshot redaction, preventing round-trip validation failures in `/config`. (#14006) Thanks @constansino.
|
||||
|
||||
### Breaking
|
||||
|
||||
- Hooks: `POST /hooks/agent` now rejects payload `sessionKey` overrides by default. To keep fixed hook context, set `hooks.defaultSessionKey` (recommended with `hooks.allowedSessionKeyPrefixes: ["hook:"]`). If you need legacy behavior, explicitly set `hooks.allowRequestSessionKey: true`. Thanks @alpernae for reporting.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Gateway/OpenResponses: harden URL-based `input_file`/`input_image` handling with explicit SSRF deny policy, hostname allowlists (`files.urlAllowlist` / `images.urlAllowlist`), per-request URL input caps (`maxUrlParts`), blocked-fetch audit logging, and regression coverage/docs updates.
|
||||
@@ -2874,10 +2780,6 @@ Docs: https://docs.openclaw.ai
|
||||
- Tests: update thread ID handling in Slack message collection tests. (#14108) Thanks @swizzmagik.
|
||||
- Update/Daemon: fix post-update restart compatibility by generating `dist/cli/daemon-cli.js` with alias-aware exports from hashed daemon bundles, preventing `registerDaemonCli` import failures during `openclaw update`.
|
||||
|
||||
### Breaking
|
||||
|
||||
- Hooks: `POST /hooks/agent` now rejects payload `sessionKey` overrides by default. To keep fixed hook context, set `hooks.defaultSessionKey` (recommended with `hooks.allowedSessionKeyPrefixes: ["hook:"]`). If you need legacy behavior, explicitly set `hooks.allowRequestSessionKey: true`. Thanks @alpernae for reporting.
|
||||
|
||||
## 2026.2.9
|
||||
|
||||
### Added
|
||||
@@ -2967,12 +2869,6 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
## 2026.2.6
|
||||
|
||||
### Added
|
||||
|
||||
- Cron: run history deep-links to session chat from the dashboard. (#10776) Thanks @tyler6204.
|
||||
- Cron: per-run session keys in run log entries and default labels for cron sessions. (#10776) Thanks @tyler6204.
|
||||
- Cron: legacy payload field compatibility (`deliver`, `channel`, `to`, `bestEffortDeliver`) in schema. (#10776) Thanks @tyler6204.
|
||||
|
||||
### Changes
|
||||
|
||||
- Cron: default `wakeMode` is now `"now"` for new jobs (was `"next-heartbeat"`). (#10776) Thanks @tyler6204.
|
||||
@@ -2988,6 +2884,12 @@ Docs: https://docs.openclaw.ai
|
||||
- CI: optimize pipeline throughput (macOS consolidation, Windows perf, workflow concurrency). (#10784) Thanks @mcaxtr.
|
||||
- Agents: bump pi-mono to 0.52.7; add embedded forward-compat fallback for Opus 4.6 model ids.
|
||||
|
||||
### Added
|
||||
|
||||
- Cron: run history deep-links to session chat from the dashboard. (#10776) Thanks @tyler6204.
|
||||
- Cron: per-run session keys in run log entries and default labels for cron sessions. (#10776) Thanks @tyler6204.
|
||||
- Cron: legacy payload field compatibility (`deliver`, `channel`, `to`, `bestEffortDeliver`) in schema. (#10776) Thanks @tyler6204.
|
||||
|
||||
### Fixes
|
||||
|
||||
- TTS: add missing OpenAI voices (ballad, cedar, juniper, marin, verse) to the allowlist so they are recognized instead of silently falling back to Edge TTS. (#2393)
|
||||
@@ -3286,6 +3188,10 @@ Docs: https://docs.openclaw.ai
|
||||
- Docs: keep docs header sticky so navbar stays visible while scrolling. (#2445) Thanks @chenyuan99.
|
||||
- Docs: update exe.dev install instructions. (#https://github.com/openclaw/openclaw/pull/3047) Thanks @zackerthescar.
|
||||
|
||||
### Breaking
|
||||
|
||||
- **BREAKING:** Gateway auth mode "none" is removed; gateway now requires token/password (Tailscale Serve identity still allowed).
|
||||
|
||||
### Fixes
|
||||
|
||||
- Skills: update session-logs paths to use ~/.openclaw. (#4502) Thanks @bonald.
|
||||
@@ -3338,10 +3244,6 @@ Docs: https://docs.openclaw.ai
|
||||
- Gateway: treat loopback + non-local Host connections as remote unless trusted proxy headers are present.
|
||||
- Onboarding: remove unsupported gateway auth "off" choice from onboarding/configure flows and CLI flags.
|
||||
|
||||
### Breaking
|
||||
|
||||
- **BREAKING:** Gateway auth mode "none" is removed; gateway now requires token/password (Tailscale Serve identity still allowed).
|
||||
|
||||
## 2026.1.24-3
|
||||
|
||||
### Fixes
|
||||
@@ -3573,6 +3475,11 @@ Docs: https://docs.openclaw.ai
|
||||
- Docs: add /model allowlist troubleshooting note. (#1405)
|
||||
- Docs: add per-message Gmail search example for gog. (#1220) Thanks @mbelinky.
|
||||
|
||||
### Breaking
|
||||
|
||||
- **BREAKING:** Control UI now rejects insecure HTTP without device identity by default. Use HTTPS (Tailscale Serve) or set `gateway.controlUi.allowInsecureAuth: true` to allow token-only auth. https://docs.openclaw.ai/web/control-ui#insecure-http
|
||||
- **BREAKING:** Envelope and system event timestamps now default to host-local time (was UTC) so agents don’t have to constantly convert.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Nodes/macOS: prompt on allowlist miss for node exec approvals, persist allowlist decisions, and flatten node invoke errors. (#1394) Thanks @ngutman.
|
||||
@@ -3595,11 +3502,6 @@ Docs: https://docs.openclaw.ai
|
||||
- macOS: default distribution packaging to universal binaries. (#1396) Thanks @JustYannicc.
|
||||
- Embedded runner: forward sender identity into attempt execution so Feishu doc auto-grant receives requester context again. (#32915) Thanks @cszhouwei.
|
||||
|
||||
### Breaking
|
||||
|
||||
- **BREAKING:** Control UI now rejects insecure HTTP without device identity by default. Use HTTPS (Tailscale Serve) or set `gateway.controlUi.allowInsecureAuth: true` to allow token-only auth. https://docs.openclaw.ai/web/control-ui#insecure-http
|
||||
- **BREAKING:** Envelope and system event timestamps now default to host-local time (was UTC) so agents don’t have to constantly convert.
|
||||
|
||||
## 2026.1.20
|
||||
|
||||
### Changes
|
||||
@@ -3681,6 +3583,10 @@ Docs: https://docs.openclaw.ai
|
||||
- macOS: stop syncing Peekaboo in postinstall.
|
||||
- Swabble: use the tagged Commander Swift package release.
|
||||
|
||||
### Breaking
|
||||
|
||||
- **BREAKING:** Reject invalid/unknown config entries and refuse to start the gateway for safety. Run `openclaw doctor --fix` to repair, then update plugins (`openclaw plugins update`) if you use any.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Discovery: shorten Bonjour DNS-SD service type to `_moltbot-gw._tcp` and update discovery clients/docs.
|
||||
@@ -3779,10 +3685,6 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @NicholaiVogel, @RyanLisse, @ThePickle31, @VACInc, @Whoaa512, @YuriNachos, @aaronveklabs, @abdaraxus, @alauppe, @ameno-, @artuskg, @austinm911, @bradleypriest, @cheeeee, @dougvk, @fogboots, @gnarco, @gumadeiras, @jdrhyne, @joelklabo, @longmaba, @mukhtharcm, @odysseus0, @oscargavin, @rhjoh, @sebslight, @sibbl, @sleontenko, @steipete, @suminhthanh, @thewilloftheshadow, @tyler6204, @vignesh07, @visionik, @ysqander, @zerone0x.
|
||||
|
||||
### Breaking
|
||||
|
||||
- **BREAKING:** Reject invalid/unknown config entries and refuse to start the gateway for safety. Run `openclaw doctor --fix` to repair, then update plugins (`openclaw plugins update`) if you use any.
|
||||
|
||||
## 2026.1.16-2
|
||||
|
||||
### Changes
|
||||
@@ -3801,6 +3703,15 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
|
||||
- Sessions: add `session.identityLinks` for cross-platform DM session li nking. (#1033) — thanks @thewilloftheshadow. https://docs.openclaw.ai/concepts/session
|
||||
- Web search: add `country`/`language` parameters (schema + Brave API) and docs. (#1046) — thanks @YuriNachos. https://docs.openclaw.ai/tools/web
|
||||
|
||||
### Breaking
|
||||
|
||||
- **BREAKING:** `openclaw message` and message tool now require `target` (dropping `to`/`channelId` for destinations). (#1034) — thanks @tobalsan.
|
||||
- **BREAKING:** Channel auth now prefers config over env for Discord/Telegram/Matrix (env is fallback only). (#1040) — thanks @thewilloftheshadow.
|
||||
- **BREAKING:** Drop legacy `chatType: "room"` support; use `chatType: "channel"`.
|
||||
- **BREAKING:** remove legacy provider-specific target resolution fallbacks; target resolution is centralized with plugin hints + directory lookups.
|
||||
- **BREAKING:** `openclaw hooks` is now `openclaw webhooks`; hooks live under `openclaw hooks`. https://docs.openclaw.ai/cli/webhooks
|
||||
- **BREAKING:** `openclaw plugins install <path>` now copies into `~/.openclaw/extensions` (use `--link` to keep path-based loading).
|
||||
|
||||
### Changes
|
||||
|
||||
- Plugins: ship bundled plugins disabled by default and allow overrides by installed versions. (#1066) — thanks @ItzR3NO.
|
||||
@@ -3892,15 +3803,6 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
|
||||
- Discord: preserve whitespace when chunking long lines so message splits keep spacing intact.
|
||||
- Skills: fix skills watcher ignored list typing (tsc).
|
||||
|
||||
### Breaking
|
||||
|
||||
- **BREAKING:** `openclaw message` and message tool now require `target` (dropping `to`/`channelId` for destinations). (#1034) — thanks @tobalsan.
|
||||
- **BREAKING:** Channel auth now prefers config over env for Discord/Telegram/Matrix (env is fallback only). (#1040) — thanks @thewilloftheshadow.
|
||||
- **BREAKING:** Drop legacy `chatType: "room"` support; use `chatType: "channel"`.
|
||||
- **BREAKING:** remove legacy provider-specific target resolution fallbacks; target resolution is centralized with plugin hints + directory lookups.
|
||||
- **BREAKING:** `openclaw hooks` is now `openclaw webhooks`; hooks live under `openclaw hooks`. https://docs.openclaw.ai/cli/webhooks
|
||||
- **BREAKING:** `openclaw plugins install <path>` now copies into `~/.openclaw/extensions` (use `--link` to keep path-based loading).
|
||||
|
||||
## 2026.1.15
|
||||
|
||||
### Highlights
|
||||
@@ -3910,6 +3812,11 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
|
||||
- Heartbeat: per-agent configuration + 24h duplicate suppression. (#980) — thanks @voidserf.
|
||||
- Security: audit warns on weak model tiers; app nodes store auth tokens encrypted (Keychain/SecurePrefs).
|
||||
|
||||
### Breaking
|
||||
|
||||
- **BREAKING:** iOS minimum version is now 18.0 to support Textual markdown rendering in native chat. (#702)
|
||||
- **BREAKING:** Microsoft Teams is now a plugin; install `@openclaw/msteams` via `openclaw plugins install @openclaw/msteams`.
|
||||
|
||||
### Changes
|
||||
|
||||
- UI/Apps: move channel/config settings to schema-driven forms and rename Connections → Channels. (#1040) — thanks @thewilloftheshadow.
|
||||
@@ -3982,11 +3889,6 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
|
||||
- Fix: allow local Tailscale Serve hostnames without treating tailnet clients as direct. (#885) — thanks @oswalpalash.
|
||||
- Fix: reset sessions after role-ordering conflicts to recover from consecutive user turns. (#998)
|
||||
|
||||
### Breaking
|
||||
|
||||
- **BREAKING:** iOS minimum version is now 18.0 to support Textual markdown rendering in native chat. (#702)
|
||||
- **BREAKING:** Microsoft Teams is now a plugin; install `@openclaw/msteams` via `openclaw plugins install @openclaw/msteams`.
|
||||
|
||||
## 2026.1.14-1
|
||||
|
||||
### Highlights
|
||||
@@ -4123,6 +4025,10 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
|
||||
- Gateway: allow Tailscale Serve identity headers to satisfy token auth; rebuild Control UI assets when protocol schema is newer. (#823) — thanks @roshanasingh4; (#786) — thanks @meaningfool.
|
||||
- Heartbeat: default `ackMaxChars` to 300 so short `HEARTBEAT_OK` replies stay internal.
|
||||
|
||||
### Installer
|
||||
|
||||
- Install: run `openclaw doctor --non-interactive` after git installs/updates and nudge daemon restarts when detected.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Doctor: warn on pnpm workspace mismatches, missing Control UI assets, and missing tsx binaries; offer UI rebuilds.
|
||||
@@ -4148,10 +4054,6 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
|
||||
- Tools/UI: harden tool input schemas for strict providers; drop null-only union variants for Gemini schema cleanup; treat `maxChars: 0` as unlimited; keep TUI last streamed response instead of "(no output)". (#782) — thanks @AbhisekBasu1; (#796) — thanks @gabriel-trigo; (#747) — thanks @thewilloftheshadow.
|
||||
- Connections UI: polish multi-account account cards. (#816) — thanks @steipete.
|
||||
|
||||
### Installer
|
||||
|
||||
- Install: run `openclaw doctor --non-interactive` after git installs/updates and nudge daemon restarts when detected.
|
||||
|
||||
### Maintenance
|
||||
|
||||
- Dependencies: bump Pi packages to 0.45.3 and refresh patched pi-ai.
|
||||
@@ -4203,6 +4105,15 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
|
||||
- Gateway: require `client.id` in WebSocket connect params; use `client.instanceId` for presence de-dupe; update docs/tests.
|
||||
- macOS: remove the attach-only gateway setting; local mode now always manages launchd while still attaching to an existing gateway if present.
|
||||
|
||||
### Installer
|
||||
|
||||
- Postinstall: replace `git apply` with builtin JS patcher (works npm/pnpm/bun; no git dependency) plus regression tests.
|
||||
- Postinstall: skip pnpm patch fallback when the new patcher is active.
|
||||
- Installer tests: add root+non-root docker smokes, CI workflow to fetch openclaw.ai scripts and run install sh/cli with onboarding skipped.
|
||||
- Installer UX: support `CLAWDBOT_NO_ONBOARD=1` for non-interactive installs; fix npm prefix on Linux and auto-install git.
|
||||
- Installer UX: add `install.sh --help` with flags/env and git install hint.
|
||||
- Installer UX: add `--install-method git|npm` and auto-detect source checkouts (prompt to update git checkout vs migrate to npm).
|
||||
|
||||
### Fixes
|
||||
|
||||
- Models/Onboarding: configure MiniMax (minimax.io) via Anthropic-compatible `/anthropic` endpoint by default (keep `minimax-api` as a legacy alias).
|
||||
@@ -4241,15 +4152,6 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
|
||||
- Sandbox/Gateway: treat `agent:<id>:main` as a main-session alias when `session.mainKey` is customized (backwards compatible).
|
||||
- Auto-reply: fast-path allowlisted slash commands (inline `/help`/`/commands`/`/status`/`/whoami` stripped before model).
|
||||
|
||||
### Installer
|
||||
|
||||
- Postinstall: replace `git apply` with builtin JS patcher (works npm/pnpm/bun; no git dependency) plus regression tests.
|
||||
- Postinstall: skip pnpm patch fallback when the new patcher is active.
|
||||
- Installer tests: add root+non-root docker smokes, CI workflow to fetch openclaw.ai scripts and run install sh/cli with onboarding skipped.
|
||||
- Installer UX: support `CLAWDBOT_NO_ONBOARD=1` for non-interactive installs; fix npm prefix on Linux and auto-install git.
|
||||
- Installer UX: add `install.sh --help` with flags/env and git install hint.
|
||||
- Installer UX: add `--install-method git|npm` and auto-detect source checkouts (prompt to update git checkout vs migrate to npm).
|
||||
|
||||
## 2026.1.10
|
||||
|
||||
### Highlights
|
||||
@@ -4358,6 +4260,11 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
|
||||
- Auto-reply + status: block-streaming controls, reasoning handling, usage/cost reporting.
|
||||
- Control UI/TUI: queued messages, session links, reasoning view, mobile polish, logs UX.
|
||||
|
||||
### Breaking
|
||||
|
||||
- CLI: `openclaw message` now subcommands (`message send|poll|...`) and requires `--provider` unless only one provider configured.
|
||||
- Commands/Tools: `/restart` and gateway restart tool disabled by default; enable with `commands.restart=true`.
|
||||
|
||||
### New Features and Changes
|
||||
|
||||
- Models/Auth: OpenCode Zen onboarding (#623) — thanks @magimetal; MiniMax Anthropic-compatible API + hosted onboarding (#590, #495) — thanks @mneves75, @tobiasbischoff.
|
||||
@@ -4399,11 +4306,6 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
|
||||
- Onboarding/Configure: QuickStart single-select provider picker; avoid Codex CLI false-expiry warnings; clarify WhatsApp owner prompt; fix Minimax hosted onboarding (agents.defaults + msteams heartbeat target); remove configure Control UI prompt; honor gateway --dev flag.
|
||||
- Agent loop: guard overflow compaction throws and restore compaction hooks for engine-owned context engines. (#41361) — thanks @davidrudduck
|
||||
|
||||
### Breaking
|
||||
|
||||
- CLI: `openclaw message` now subcommands (`message send|poll|...`) and requires `--provider` unless only one provider configured.
|
||||
- Commands/Tools: `/restart` and gateway restart tool disabled by default; enable with `commands.restart=true`.
|
||||
|
||||
### Maintenance
|
||||
|
||||
- Dependencies: bump pi-\* stack to 0.42.2.
|
||||
@@ -4423,18 +4325,6 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
|
||||
- Control UI: logs tab, streaming stability, focus mode, and large-output rendering fixes.
|
||||
- CLI/Gateway/Doctor: daemon/logs/status, auth migration, and diagnostics significantly expanded.
|
||||
|
||||
### Fixes
|
||||
|
||||
- **CLI/Gateway/Doctor:** daemon runtime selection + improved logs/status/health/errors; auth/password handling for local CLI; richer close/timeout details; auto-migrate legacy config/sessions/state; integrity checks + repair prompts; `--yes`/`--non-interactive`; `--deep` gateway scans; better restart/service hints.
|
||||
- **Agent loop + compaction:** compaction/pruning tuning, overflow handling, safer bootstrap context, and per-provider threading/confirmations; opt-in tool-result pruning + compact tracking.
|
||||
- **Sandbox + tools:** per-agent sandbox overrides, workspaceAccess controls, session tool visibility, tool policy overrides, process isolation, and tool schema/timeout/reaction unification.
|
||||
- **Providers (Telegram/WhatsApp/Discord/Slack/Signal/iMessage):** retry/backoff, threading, reactions, media groups/attachments, mention gating, typing behavior, and error/log stability; long polling + forum topic isolation for Telegram.
|
||||
- **Gateway/CLI UX:** `openclaw logs`, cron list colors/aliases, docs search, agents list/add/delete flows, status usage snapshots, runtime/auth source display, and `/status`/commands auth unification.
|
||||
- **Control UI/Web:** logs tab, focus mode polish, config form resilience, streaming stability, tool output caps, windowed chat history, and reconnect/password URL auth.
|
||||
- **macOS/Android/TUI/Build:** macOS gateway races, QR bundling, JSON5 config safety, Voice Wake hardening; Android EXIF rotation + APK naming/versioning; TUI key handling; tooling/bundling fixes.
|
||||
- **Packaging/compat:** npm dist folder coverage, Node 25 qrcode-terminal import fixes, Bun/Playwright/WebSocket patches, and Docker Bun install.
|
||||
- **Docs:** new FAQ/ClawHub/config examples/showcase entries and clarified auth, sandbox, and systemd docs.
|
||||
|
||||
### Breaking
|
||||
|
||||
- **SECURITY (update ASAP):** inbound DMs are now **locked down by default** on Telegram/WhatsApp/Signal/iMessage/Discord/Slack.
|
||||
@@ -4450,6 +4340,18 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
|
||||
- Auto-reply: removed `autoReply` from Discord/Slack/Telegram channel configs; use `requireMention` instead (Telegram topics now support `requireMention` overrides).
|
||||
- CLI: remove `update`, `gateway-daemon`, `gateway {install|uninstall|start|stop|restart|daemon status|wake|send|agent}`, and `telegram` commands; move `login/logout` to `providers login/logout` (top-level aliases hidden); use `daemon` for service control, `send`/`agent`/`wake` for RPC, and `nodes canvas` for canvas ops.
|
||||
|
||||
### Fixes
|
||||
|
||||
- **CLI/Gateway/Doctor:** daemon runtime selection + improved logs/status/health/errors; auth/password handling for local CLI; richer close/timeout details; auto-migrate legacy config/sessions/state; integrity checks + repair prompts; `--yes`/`--non-interactive`; `--deep` gateway scans; better restart/service hints.
|
||||
- **Agent loop + compaction:** compaction/pruning tuning, overflow handling, safer bootstrap context, and per-provider threading/confirmations; opt-in tool-result pruning + compact tracking.
|
||||
- **Sandbox + tools:** per-agent sandbox overrides, workspaceAccess controls, session tool visibility, tool policy overrides, process isolation, and tool schema/timeout/reaction unification.
|
||||
- **Providers (Telegram/WhatsApp/Discord/Slack/Signal/iMessage):** retry/backoff, threading, reactions, media groups/attachments, mention gating, typing behavior, and error/log stability; long polling + forum topic isolation for Telegram.
|
||||
- **Gateway/CLI UX:** `openclaw logs`, cron list colors/aliases, docs search, agents list/add/delete flows, status usage snapshots, runtime/auth source display, and `/status`/commands auth unification.
|
||||
- **Control UI/Web:** logs tab, focus mode polish, config form resilience, streaming stability, tool output caps, windowed chat history, and reconnect/password URL auth.
|
||||
- **macOS/Android/TUI/Build:** macOS gateway races, QR bundling, JSON5 config safety, Voice Wake hardening; Android EXIF rotation + APK naming/versioning; TUI key handling; tooling/bundling fixes.
|
||||
- **Packaging/compat:** npm dist folder coverage, Node 25 qrcode-terminal import fixes, Bun/Playwright/WebSocket patches, and Docker Bun install.
|
||||
- **Docs:** new FAQ/ClawHub/config examples/showcase entries and clarified auth, sandbox, and systemd docs.
|
||||
|
||||
### Maintenance
|
||||
|
||||
- Skills additions (Himalaya email, CodexBar, 1Password).
|
||||
|
||||
@@ -47,7 +47,7 @@ Welcome to the lobster tank! 🦞
|
||||
- **Christoph Nakazawa** - JS Infra
|
||||
- GitHub: [@cpojer](https://github.com/cpojer) · X: [@cnakazawa](https://x.com/cnakazawa)
|
||||
|
||||
- **Gustavo Madeira Santana** - Multi-agents, CLI, Performance, Plugins, Matrix
|
||||
- **Gustavo Madeira Santana** - Multi-agents, CLI, web UI
|
||||
- GitHub: [@gumadeiras](https://github.com/gumadeiras) · X: [@gumadeiras](https://x.com/gumadeiras)
|
||||
|
||||
- **Onur Solmaz** - Agents, dev workflows, ACP integrations, MS Teams
|
||||
@@ -89,12 +89,6 @@ Welcome to the lobster tank! 🦞
|
||||
|
||||
- Test locally with your OpenClaw instance
|
||||
- Run tests: `pnpm build && pnpm check && pnpm test`
|
||||
- For extension/plugin changes, run the fast local lane first:
|
||||
- `pnpm test:extension <extension-name>`
|
||||
- `pnpm test:extension --list` to see valid extension ids
|
||||
- If you changed shared plugin or channel surfaces, run `pnpm test:contracts`
|
||||
- For targeted shared-surface work, use `pnpm test:contracts:channels` or `pnpm test:contracts:plugins`
|
||||
- If you changed broader runtime behavior, still run the relevant wider lanes (`pnpm test:extensions`, `pnpm test:channels`, or `pnpm test`) before asking for review
|
||||
- If you have access to Codex, run `codex review --base origin/main` locally before opening or updating your PR. Treat this as the current highest standard of AI review, even if GitHub Codex review also runs.
|
||||
- Ensure CI checks pass
|
||||
- Keep PRs focused (one thing per PR; do not mix unrelated concerns)
|
||||
|
||||
18
README.md
18
README.md
@@ -23,10 +23,10 @@ It answers you on the channels you already use (WhatsApp, Telegram, Slack, Disco
|
||||
|
||||
If you want a personal, single-user assistant that feels local, fast, and always-on, this is it.
|
||||
|
||||
[Website](https://openclaw.ai) · [Docs](https://docs.openclaw.ai) · [Vision](VISION.md) · [DeepWiki](https://deepwiki.com/openclaw/openclaw) · [Getting Started](https://docs.openclaw.ai/start/getting-started) · [Updating](https://docs.openclaw.ai/install/updating) · [Showcase](https://docs.openclaw.ai/start/showcase) · [FAQ](https://docs.openclaw.ai/help/faq) · [Onboarding](https://docs.openclaw.ai/start/wizard) · [Nix](https://github.com/openclaw/nix-openclaw) · [Docker](https://docs.openclaw.ai/install/docker) · [Discord](https://discord.gg/clawd)
|
||||
[Website](https://openclaw.ai) · [Docs](https://docs.openclaw.ai) · [Vision](VISION.md) · [DeepWiki](https://deepwiki.com/openclaw/openclaw) · [Getting Started](https://docs.openclaw.ai/start/getting-started) · [Updating](https://docs.openclaw.ai/install/updating) · [Showcase](https://docs.openclaw.ai/start/showcase) · [FAQ](https://docs.openclaw.ai/help/faq) · [Wizard](https://docs.openclaw.ai/start/wizard) · [Nix](https://github.com/openclaw/nix-openclaw) · [Docker](https://docs.openclaw.ai/install/docker) · [Discord](https://discord.gg/clawd)
|
||||
|
||||
Preferred setup: run `openclaw onboard` in your terminal.
|
||||
OpenClaw Onboard guides you step by step through setting up the gateway, workspace, channels, and skills. It is the recommended CLI setup path and works on **macOS, Linux, and Windows (via WSL2; strongly recommended)**.
|
||||
Preferred setup: run the onboarding wizard (`openclaw onboard`) in your terminal.
|
||||
The wizard guides you step by step through setting up the gateway, workspace, channels, and skills. The CLI wizard is the recommended path and works on **macOS, Linux, and Windows (via WSL2; strongly recommended)**.
|
||||
Works with npm, pnpm, or bun.
|
||||
New install? Start here: [Getting started](https://docs.openclaw.ai/start/getting-started)
|
||||
|
||||
@@ -58,7 +58,7 @@ npm install -g openclaw@latest
|
||||
openclaw onboard --install-daemon
|
||||
```
|
||||
|
||||
OpenClaw Onboard installs the Gateway daemon (launchd/systemd user service) so it stays running.
|
||||
The wizard installs the Gateway daemon (launchd/systemd user service) so it stays running.
|
||||
|
||||
## Quick start (TL;DR)
|
||||
|
||||
@@ -132,7 +132,7 @@ Run `openclaw doctor` to surface risky/misconfigured DM policies.
|
||||
- **[Live Canvas](https://docs.openclaw.ai/platforms/mac/canvas)** — agent-driven visual workspace with [A2UI](https://docs.openclaw.ai/platforms/mac/canvas#canvas-a2ui).
|
||||
- **[First-class tools](https://docs.openclaw.ai/tools)** — browser, canvas, nodes, cron, sessions, and Discord/Slack actions.
|
||||
- **[Companion apps](https://docs.openclaw.ai/platforms/macos)** — macOS menu bar app + iOS/Android [nodes](https://docs.openclaw.ai/nodes).
|
||||
- **[Onboarding](https://docs.openclaw.ai/start/wizard) + [skills](https://docs.openclaw.ai/tools/skills)** — onboarding-driven setup with bundled/managed/workspace skills.
|
||||
- **[Onboarding](https://docs.openclaw.ai/start/wizard) + [skills](https://docs.openclaw.ai/tools/skills)** — wizard-driven setup with bundled/managed/workspace skills.
|
||||
|
||||
## Star History
|
||||
|
||||
@@ -143,7 +143,7 @@ Run `openclaw doctor` to surface risky/misconfigured DM policies.
|
||||
### Core platform
|
||||
|
||||
- [Gateway WS control plane](https://docs.openclaw.ai/gateway) with sessions, presence, config, cron, webhooks, [Control UI](https://docs.openclaw.ai/web), and [Canvas host](https://docs.openclaw.ai/platforms/mac/canvas#canvas-a2ui).
|
||||
- [CLI surface](https://docs.openclaw.ai/tools/agent-send): gateway, agent, send, [onboarding](https://docs.openclaw.ai/start/wizard), and [doctor](https://docs.openclaw.ai/gateway/doctor).
|
||||
- [CLI surface](https://docs.openclaw.ai/tools/agent-send): gateway, agent, send, [wizard](https://docs.openclaw.ai/start/wizard), and [doctor](https://docs.openclaw.ai/gateway/doctor).
|
||||
- [Pi agent runtime](https://docs.openclaw.ai/concepts/agent) in RPC mode with tool streaming and block streaming.
|
||||
- [Session model](https://docs.openclaw.ai/concepts/session): `main` for direct chats, group isolation, activation modes, queue modes, reply-back. Group rules: [Groups](https://docs.openclaw.ai/channels/groups).
|
||||
- [Media pipeline](https://docs.openclaw.ai/nodes/images): images/audio/video, transcription hooks, size caps, temp file lifecycle. Audio details: [Audio](https://docs.openclaw.ai/nodes/audio).
|
||||
@@ -293,7 +293,7 @@ If you plan to build/run companion apps, follow the platform runbooks below.
|
||||
- WebChat + debug tools.
|
||||
- Remote gateway control over SSH.
|
||||
|
||||
Note: signed builds required for macOS permissions to stick across rebuilds (see [macOS Permissions](https://docs.openclaw.ai/platforms/mac/permissions)).
|
||||
Note: signed builds required for macOS permissions to stick across rebuilds (see `docs/mac/permissions.md`).
|
||||
|
||||
### iOS node (optional)
|
||||
|
||||
@@ -364,7 +364,7 @@ Details: [Security guide](https://docs.openclaw.ai/gateway/security) · [Docker
|
||||
|
||||
### [Discord](https://docs.openclaw.ai/channels/discord)
|
||||
|
||||
- Set `DISCORD_BOT_TOKEN` or `channels.discord.token`.
|
||||
- Set `DISCORD_BOT_TOKEN` or `channels.discord.token` (env wins).
|
||||
- Optional: set `commands.native`, `commands.text`, or `commands.useAccessGroups`, plus `channels.discord.allowFrom`, `channels.discord.guilds`, or `channels.discord.mediaMaxMb` as needed.
|
||||
|
||||
```json5
|
||||
@@ -422,7 +422,7 @@ Use these when you’re past the onboarding flow and want the deeper reference.
|
||||
- [Run the Gateway by the book with the operational runbook.](https://docs.openclaw.ai/gateway)
|
||||
- [Learn how the Control UI/Web surfaces work and how to expose them safely.](https://docs.openclaw.ai/web)
|
||||
- [Understand remote access over SSH tunnels or tailnets.](https://docs.openclaw.ai/gateway/remote)
|
||||
- [Follow OpenClaw Onboard for a guided setup.](https://docs.openclaw.ai/start/wizard)
|
||||
- [Follow the onboarding wizard flow for a guided setup.](https://docs.openclaw.ai/start/wizard)
|
||||
- [Wire external triggers via the webhook surface.](https://docs.openclaw.ai/automation/webhook)
|
||||
- [Set up Gmail Pub/Sub triggers.](https://docs.openclaw.ai/automation/gmail-pubsub)
|
||||
- [Learn the macOS menu bar companion details.](https://docs.openclaw.ai/platforms/mac/menu-bar)
|
||||
|
||||
@@ -18,13 +18,14 @@ import kotlinx.coroutines.launch
|
||||
class MainActivity : ComponentActivity() {
|
||||
private val viewModel: MainViewModel by viewModels()
|
||||
private lateinit var permissionRequester: PermissionRequester
|
||||
private var didAttachRuntimeUi = false
|
||||
private var didStartNodeService = false
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
permissionRequester = PermissionRequester(this)
|
||||
viewModel.camera.attachLifecycleOwner(this)
|
||||
viewModel.camera.attachPermissionRequester(permissionRequester)
|
||||
viewModel.sms.attachPermissionRequester(permissionRequester)
|
||||
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
@@ -38,20 +39,6 @@ class MainActivity : ComponentActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
viewModel.runtimeInitialized.collect { ready ->
|
||||
if (!ready || didAttachRuntimeUi) return@collect
|
||||
viewModel.attachRuntimeUi(owner = this@MainActivity, permissionRequester = permissionRequester)
|
||||
didAttachRuntimeUi = true
|
||||
if (!didStartNodeService) {
|
||||
NodeForegroundService.start(this@MainActivity)
|
||||
didStartNodeService = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setContent {
|
||||
OpenClawTheme {
|
||||
Surface(modifier = Modifier) {
|
||||
@@ -59,6 +46,9 @@ class MainActivity : ComponentActivity() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Keep startup path lean: start foreground service after first frame.
|
||||
window.decorView.post { NodeForegroundService.start(this) }
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
|
||||
@@ -2,268 +2,209 @@ package ai.openclaw.app
|
||||
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import ai.openclaw.app.chat.ChatMessage
|
||||
import ai.openclaw.app.chat.ChatPendingToolCall
|
||||
import ai.openclaw.app.chat.ChatSessionEntry
|
||||
import ai.openclaw.app.chat.OutgoingAttachment
|
||||
import ai.openclaw.app.gateway.GatewayEndpoint
|
||||
import ai.openclaw.app.chat.OutgoingAttachment
|
||||
import ai.openclaw.app.node.CameraCaptureManager
|
||||
import ai.openclaw.app.node.CanvasController
|
||||
import ai.openclaw.app.node.SmsManager
|
||||
import ai.openclaw.app.voice.VoiceConversationEntry
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class MainViewModel(app: Application) : AndroidViewModel(app) {
|
||||
private val nodeApp = app as NodeApp
|
||||
private val prefs = nodeApp.prefs
|
||||
private val runtimeRef = MutableStateFlow<NodeRuntime?>(null)
|
||||
private var foreground = true
|
||||
private val runtime: NodeRuntime = (app as NodeApp).runtime
|
||||
|
||||
private fun ensureRuntime(): NodeRuntime {
|
||||
runtimeRef.value?.let { return it }
|
||||
val runtime = nodeApp.ensureRuntime()
|
||||
runtime.setForeground(foreground)
|
||||
runtimeRef.value = runtime
|
||||
return runtime
|
||||
}
|
||||
val canvas: CanvasController = runtime.canvas
|
||||
val canvasCurrentUrl: StateFlow<String?> = runtime.canvas.currentUrl
|
||||
val canvasA2uiHydrated: StateFlow<Boolean> = runtime.canvasA2uiHydrated
|
||||
val canvasRehydratePending: StateFlow<Boolean> = runtime.canvasRehydratePending
|
||||
val canvasRehydrateErrorText: StateFlow<String?> = runtime.canvasRehydrateErrorText
|
||||
val camera: CameraCaptureManager = runtime.camera
|
||||
val sms: SmsManager = runtime.sms
|
||||
|
||||
private fun <T> runtimeState(
|
||||
initial: T,
|
||||
selector: (NodeRuntime) -> StateFlow<T>,
|
||||
): StateFlow<T> =
|
||||
runtimeRef
|
||||
.flatMapLatest { runtime -> runtime?.let(selector) ?: flowOf(initial) }
|
||||
.stateIn(viewModelScope, SharingStarted.Eagerly, initial)
|
||||
val gateways: StateFlow<List<GatewayEndpoint>> = runtime.gateways
|
||||
val discoveryStatusText: StateFlow<String> = runtime.discoveryStatusText
|
||||
|
||||
val runtimeInitialized: StateFlow<Boolean> =
|
||||
runtimeRef
|
||||
.flatMapLatest { runtime -> flowOf(runtime != null) }
|
||||
.stateIn(viewModelScope, SharingStarted.Eagerly, false)
|
||||
val isConnected: StateFlow<Boolean> = runtime.isConnected
|
||||
val isNodeConnected: StateFlow<Boolean> = runtime.nodeConnected
|
||||
val statusText: StateFlow<String> = runtime.statusText
|
||||
val serverName: StateFlow<String?> = runtime.serverName
|
||||
val remoteAddress: StateFlow<String?> = runtime.remoteAddress
|
||||
val pendingGatewayTrust: StateFlow<NodeRuntime.GatewayTrustPrompt?> = runtime.pendingGatewayTrust
|
||||
val isForeground: StateFlow<Boolean> = runtime.isForeground
|
||||
val seamColorArgb: StateFlow<Long> = runtime.seamColorArgb
|
||||
val mainSessionKey: StateFlow<String> = runtime.mainSessionKey
|
||||
|
||||
val canvasCurrentUrl: StateFlow<String?> = runtimeState(initial = null) { it.canvas.currentUrl }
|
||||
val canvasA2uiHydrated: StateFlow<Boolean> = runtimeState(initial = false) { it.canvasA2uiHydrated }
|
||||
val canvasRehydratePending: StateFlow<Boolean> = runtimeState(initial = false) { it.canvasRehydratePending }
|
||||
val canvasRehydrateErrorText: StateFlow<String?> = runtimeState(initial = null) { it.canvasRehydrateErrorText }
|
||||
val cameraHud: StateFlow<CameraHudState?> = runtime.cameraHud
|
||||
val cameraFlashToken: StateFlow<Long> = runtime.cameraFlashToken
|
||||
|
||||
val gateways: StateFlow<List<GatewayEndpoint>> = runtimeState(initial = emptyList()) { it.gateways }
|
||||
val discoveryStatusText: StateFlow<String> = runtimeState(initial = "Searching…") { it.discoveryStatusText }
|
||||
val instanceId: StateFlow<String> = runtime.instanceId
|
||||
val displayName: StateFlow<String> = runtime.displayName
|
||||
val cameraEnabled: StateFlow<Boolean> = runtime.cameraEnabled
|
||||
val locationMode: StateFlow<LocationMode> = runtime.locationMode
|
||||
val locationPreciseEnabled: StateFlow<Boolean> = runtime.locationPreciseEnabled
|
||||
val preventSleep: StateFlow<Boolean> = runtime.preventSleep
|
||||
val micEnabled: StateFlow<Boolean> = runtime.micEnabled
|
||||
val micCooldown: StateFlow<Boolean> = runtime.micCooldown
|
||||
val micStatusText: StateFlow<String> = runtime.micStatusText
|
||||
val micLiveTranscript: StateFlow<String?> = runtime.micLiveTranscript
|
||||
val micIsListening: StateFlow<Boolean> = runtime.micIsListening
|
||||
val micQueuedMessages: StateFlow<List<String>> = runtime.micQueuedMessages
|
||||
val micConversation: StateFlow<List<VoiceConversationEntry>> = runtime.micConversation
|
||||
val micInputLevel: StateFlow<Float> = runtime.micInputLevel
|
||||
val micIsSending: StateFlow<Boolean> = runtime.micIsSending
|
||||
val speakerEnabled: StateFlow<Boolean> = runtime.speakerEnabled
|
||||
val manualEnabled: StateFlow<Boolean> = runtime.manualEnabled
|
||||
val manualHost: StateFlow<String> = runtime.manualHost
|
||||
val manualPort: StateFlow<Int> = runtime.manualPort
|
||||
val manualTls: StateFlow<Boolean> = runtime.manualTls
|
||||
val gatewayToken: StateFlow<String> = runtime.gatewayToken
|
||||
val onboardingCompleted: StateFlow<Boolean> = runtime.onboardingCompleted
|
||||
val canvasDebugStatusEnabled: StateFlow<Boolean> = runtime.canvasDebugStatusEnabled
|
||||
|
||||
val isConnected: StateFlow<Boolean> = runtimeState(initial = false) { it.isConnected }
|
||||
val isNodeConnected: StateFlow<Boolean> = runtimeState(initial = false) { it.nodeConnected }
|
||||
val statusText: StateFlow<String> = runtimeState(initial = "Offline") { it.statusText }
|
||||
val serverName: StateFlow<String?> = runtimeState(initial = null) { it.serverName }
|
||||
val remoteAddress: StateFlow<String?> = runtimeState(initial = null) { it.remoteAddress }
|
||||
val pendingGatewayTrust: StateFlow<NodeRuntime.GatewayTrustPrompt?> = runtimeState(initial = null) { it.pendingGatewayTrust }
|
||||
val seamColorArgb: StateFlow<Long> = runtimeState(initial = 0xFF0EA5E9) { it.seamColorArgb }
|
||||
val mainSessionKey: StateFlow<String> = runtimeState(initial = "main") { it.mainSessionKey }
|
||||
|
||||
val cameraHud: StateFlow<CameraHudState?> = runtimeState(initial = null) { it.cameraHud }
|
||||
val cameraFlashToken: StateFlow<Long> = runtimeState(initial = 0L) { it.cameraFlashToken }
|
||||
|
||||
val instanceId: StateFlow<String> = prefs.instanceId
|
||||
val displayName: StateFlow<String> = prefs.displayName
|
||||
val cameraEnabled: StateFlow<Boolean> = prefs.cameraEnabled
|
||||
val locationMode: StateFlow<LocationMode> = prefs.locationMode
|
||||
val locationPreciseEnabled: StateFlow<Boolean> = prefs.locationPreciseEnabled
|
||||
val preventSleep: StateFlow<Boolean> = prefs.preventSleep
|
||||
val manualEnabled: StateFlow<Boolean> = prefs.manualEnabled
|
||||
val manualHost: StateFlow<String> = prefs.manualHost
|
||||
val manualPort: StateFlow<Int> = prefs.manualPort
|
||||
val manualTls: StateFlow<Boolean> = prefs.manualTls
|
||||
val gatewayToken: StateFlow<String> = prefs.gatewayToken
|
||||
val onboardingCompleted: StateFlow<Boolean> = prefs.onboardingCompleted
|
||||
val canvasDebugStatusEnabled: StateFlow<Boolean> = prefs.canvasDebugStatusEnabled
|
||||
val speakerEnabled: StateFlow<Boolean> = prefs.speakerEnabled
|
||||
val micEnabled: StateFlow<Boolean> = prefs.talkEnabled
|
||||
|
||||
val micCooldown: StateFlow<Boolean> = runtimeState(initial = false) { it.micCooldown }
|
||||
val micStatusText: StateFlow<String> = runtimeState(initial = "Mic off") { it.micStatusText }
|
||||
val micLiveTranscript: StateFlow<String?> = runtimeState(initial = null) { it.micLiveTranscript }
|
||||
val micIsListening: StateFlow<Boolean> = runtimeState(initial = false) { it.micIsListening }
|
||||
val micQueuedMessages: StateFlow<List<String>> = runtimeState(initial = emptyList()) { it.micQueuedMessages }
|
||||
val micConversation: StateFlow<List<VoiceConversationEntry>> = runtimeState(initial = emptyList()) { it.micConversation }
|
||||
val micInputLevel: StateFlow<Float> = runtimeState(initial = 0f) { it.micInputLevel }
|
||||
val micIsSending: StateFlow<Boolean> = runtimeState(initial = false) { it.micIsSending }
|
||||
|
||||
val chatSessionKey: StateFlow<String> = runtimeState(initial = "main") { it.chatSessionKey }
|
||||
val chatSessionId: StateFlow<String?> = runtimeState(initial = null) { it.chatSessionId }
|
||||
val chatMessages: StateFlow<List<ChatMessage>> = runtimeState(initial = emptyList()) { it.chatMessages }
|
||||
val chatError: StateFlow<String?> = runtimeState(initial = null) { it.chatError }
|
||||
val chatHealthOk: StateFlow<Boolean> = runtimeState(initial = false) { it.chatHealthOk }
|
||||
val chatThinkingLevel: StateFlow<String> = runtimeState(initial = "off") { it.chatThinkingLevel }
|
||||
val chatStreamingAssistantText: StateFlow<String?> = runtimeState(initial = null) { it.chatStreamingAssistantText }
|
||||
val chatPendingToolCalls: StateFlow<List<ChatPendingToolCall>> = runtimeState(initial = emptyList()) { it.chatPendingToolCalls }
|
||||
val chatSessions: StateFlow<List<ChatSessionEntry>> = runtimeState(initial = emptyList()) { it.chatSessions }
|
||||
val pendingRunCount: StateFlow<Int> = runtimeState(initial = 0) { it.pendingRunCount }
|
||||
|
||||
init {
|
||||
if (prefs.onboardingCompleted.value) {
|
||||
ensureRuntime()
|
||||
}
|
||||
}
|
||||
|
||||
val canvas: CanvasController
|
||||
get() = ensureRuntime().canvas
|
||||
|
||||
val camera: CameraCaptureManager
|
||||
get() = ensureRuntime().camera
|
||||
|
||||
val sms: SmsManager
|
||||
get() = ensureRuntime().sms
|
||||
|
||||
fun attachRuntimeUi(owner: LifecycleOwner, permissionRequester: PermissionRequester) {
|
||||
val runtime = runtimeRef.value ?: return
|
||||
runtime.camera.attachLifecycleOwner(owner)
|
||||
runtime.camera.attachPermissionRequester(permissionRequester)
|
||||
runtime.sms.attachPermissionRequester(permissionRequester)
|
||||
}
|
||||
val chatSessionKey: StateFlow<String> = runtime.chatSessionKey
|
||||
val chatSessionId: StateFlow<String?> = runtime.chatSessionId
|
||||
val chatMessages = runtime.chatMessages
|
||||
val chatError: StateFlow<String?> = runtime.chatError
|
||||
val chatHealthOk: StateFlow<Boolean> = runtime.chatHealthOk
|
||||
val chatThinkingLevel: StateFlow<String> = runtime.chatThinkingLevel
|
||||
val chatStreamingAssistantText: StateFlow<String?> = runtime.chatStreamingAssistantText
|
||||
val chatPendingToolCalls = runtime.chatPendingToolCalls
|
||||
val chatSessions = runtime.chatSessions
|
||||
val pendingRunCount: StateFlow<Int> = runtime.pendingRunCount
|
||||
|
||||
fun setForeground(value: Boolean) {
|
||||
foreground = value
|
||||
runtimeRef.value?.setForeground(value)
|
||||
runtime.setForeground(value)
|
||||
}
|
||||
|
||||
fun setDisplayName(value: String) {
|
||||
prefs.setDisplayName(value)
|
||||
runtime.setDisplayName(value)
|
||||
}
|
||||
|
||||
fun setCameraEnabled(value: Boolean) {
|
||||
prefs.setCameraEnabled(value)
|
||||
runtime.setCameraEnabled(value)
|
||||
}
|
||||
|
||||
fun setLocationMode(mode: LocationMode) {
|
||||
prefs.setLocationMode(mode)
|
||||
runtime.setLocationMode(mode)
|
||||
}
|
||||
|
||||
fun setLocationPreciseEnabled(value: Boolean) {
|
||||
prefs.setLocationPreciseEnabled(value)
|
||||
runtime.setLocationPreciseEnabled(value)
|
||||
}
|
||||
|
||||
fun setPreventSleep(value: Boolean) {
|
||||
prefs.setPreventSleep(value)
|
||||
runtime.setPreventSleep(value)
|
||||
}
|
||||
|
||||
fun setManualEnabled(value: Boolean) {
|
||||
prefs.setManualEnabled(value)
|
||||
runtime.setManualEnabled(value)
|
||||
}
|
||||
|
||||
fun setManualHost(value: String) {
|
||||
prefs.setManualHost(value)
|
||||
runtime.setManualHost(value)
|
||||
}
|
||||
|
||||
fun setManualPort(value: Int) {
|
||||
prefs.setManualPort(value)
|
||||
runtime.setManualPort(value)
|
||||
}
|
||||
|
||||
fun setManualTls(value: Boolean) {
|
||||
prefs.setManualTls(value)
|
||||
runtime.setManualTls(value)
|
||||
}
|
||||
|
||||
fun setGatewayToken(value: String) {
|
||||
prefs.setGatewayToken(value)
|
||||
runtime.setGatewayToken(value)
|
||||
}
|
||||
|
||||
fun setGatewayBootstrapToken(value: String) {
|
||||
prefs.setGatewayBootstrapToken(value)
|
||||
runtime.setGatewayBootstrapToken(value)
|
||||
}
|
||||
|
||||
fun setGatewayPassword(value: String) {
|
||||
prefs.setGatewayPassword(value)
|
||||
runtime.setGatewayPassword(value)
|
||||
}
|
||||
|
||||
fun setOnboardingCompleted(value: Boolean) {
|
||||
if (value) {
|
||||
ensureRuntime()
|
||||
}
|
||||
prefs.setOnboardingCompleted(value)
|
||||
runtime.setOnboardingCompleted(value)
|
||||
}
|
||||
|
||||
fun setCanvasDebugStatusEnabled(value: Boolean) {
|
||||
prefs.setCanvasDebugStatusEnabled(value)
|
||||
runtime.setCanvasDebugStatusEnabled(value)
|
||||
}
|
||||
|
||||
fun setVoiceScreenActive(active: Boolean) {
|
||||
ensureRuntime().setVoiceScreenActive(active)
|
||||
runtime.setVoiceScreenActive(active)
|
||||
}
|
||||
|
||||
fun setMicEnabled(enabled: Boolean) {
|
||||
ensureRuntime().setMicEnabled(enabled)
|
||||
runtime.setMicEnabled(enabled)
|
||||
}
|
||||
|
||||
fun setSpeakerEnabled(enabled: Boolean) {
|
||||
ensureRuntime().setSpeakerEnabled(enabled)
|
||||
runtime.setSpeakerEnabled(enabled)
|
||||
}
|
||||
|
||||
fun refreshGatewayConnection() {
|
||||
ensureRuntime().refreshGatewayConnection()
|
||||
runtime.refreshGatewayConnection()
|
||||
}
|
||||
|
||||
fun connect(endpoint: GatewayEndpoint) {
|
||||
ensureRuntime().connect(endpoint)
|
||||
runtime.connect(endpoint)
|
||||
}
|
||||
|
||||
fun connectManual() {
|
||||
ensureRuntime().connectManual()
|
||||
runtime.connectManual()
|
||||
}
|
||||
|
||||
fun disconnect() {
|
||||
runtimeRef.value?.disconnect()
|
||||
runtime.disconnect()
|
||||
}
|
||||
|
||||
fun acceptGatewayTrustPrompt() {
|
||||
runtimeRef.value?.acceptGatewayTrustPrompt()
|
||||
runtime.acceptGatewayTrustPrompt()
|
||||
}
|
||||
|
||||
fun declineGatewayTrustPrompt() {
|
||||
runtimeRef.value?.declineGatewayTrustPrompt()
|
||||
runtime.declineGatewayTrustPrompt()
|
||||
}
|
||||
|
||||
fun handleCanvasA2UIActionFromWebView(payloadJson: String) {
|
||||
ensureRuntime().handleCanvasA2UIActionFromWebView(payloadJson)
|
||||
runtime.handleCanvasA2UIActionFromWebView(payloadJson)
|
||||
}
|
||||
|
||||
fun requestCanvasRehydrate(source: String = "screen_tab") {
|
||||
ensureRuntime().requestCanvasRehydrate(source = source, force = true)
|
||||
runtime.requestCanvasRehydrate(source = source, force = true)
|
||||
}
|
||||
|
||||
fun refreshHomeCanvasOverviewIfConnected() {
|
||||
ensureRuntime().refreshHomeCanvasOverviewIfConnected()
|
||||
runtime.refreshHomeCanvasOverviewIfConnected()
|
||||
}
|
||||
|
||||
fun loadChat(sessionKey: String) {
|
||||
ensureRuntime().loadChat(sessionKey)
|
||||
runtime.loadChat(sessionKey)
|
||||
}
|
||||
|
||||
fun refreshChat() {
|
||||
ensureRuntime().refreshChat()
|
||||
runtime.refreshChat()
|
||||
}
|
||||
|
||||
fun refreshChatSessions(limit: Int? = null) {
|
||||
ensureRuntime().refreshChatSessions(limit = limit)
|
||||
runtime.refreshChatSessions(limit = limit)
|
||||
}
|
||||
|
||||
fun setChatThinkingLevel(level: String) {
|
||||
ensureRuntime().setChatThinkingLevel(level)
|
||||
runtime.setChatThinkingLevel(level)
|
||||
}
|
||||
|
||||
fun switchChatSession(sessionKey: String) {
|
||||
ensureRuntime().switchChatSession(sessionKey)
|
||||
runtime.switchChatSession(sessionKey)
|
||||
}
|
||||
|
||||
fun abortChat() {
|
||||
ensureRuntime().abortChat()
|
||||
runtime.abortChat()
|
||||
}
|
||||
|
||||
fun sendChat(message: String, thinking: String, attachments: List<OutgoingAttachment>) {
|
||||
ensureRuntime().sendChat(message = message, thinking = thinking, attachments = attachments)
|
||||
runtime.sendChat(message = message, thinking = thinking, attachments = attachments)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,18 +4,7 @@ import android.app.Application
|
||||
import android.os.StrictMode
|
||||
|
||||
class NodeApp : Application() {
|
||||
val prefs: SecurePrefs by lazy { SecurePrefs(this) }
|
||||
|
||||
@Volatile private var runtimeInstance: NodeRuntime? = null
|
||||
|
||||
fun ensureRuntime(): NodeRuntime {
|
||||
runtimeInstance?.let { return it }
|
||||
return synchronized(this) {
|
||||
runtimeInstance ?: NodeRuntime(this, prefs).also { runtimeInstance = it }
|
||||
}
|
||||
}
|
||||
|
||||
fun peekRuntime(): NodeRuntime? = runtimeInstance
|
||||
val runtime: NodeRuntime by lazy { NodeRuntime(this) }
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
@@ -28,11 +28,7 @@ class NodeForegroundService : Service() {
|
||||
val initial = buildNotification(title = "OpenClaw Node", text = "Starting…")
|
||||
startForegroundWithTypes(notification = initial)
|
||||
|
||||
val runtime = (application as NodeApp).peekRuntime()
|
||||
if (runtime == null) {
|
||||
stopSelf()
|
||||
return
|
||||
}
|
||||
val runtime = (application as NodeApp).runtime
|
||||
notificationJob =
|
||||
scope.launch {
|
||||
combine(
|
||||
@@ -63,7 +59,7 @@ class NodeForegroundService : Service() {
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
when (intent?.action) {
|
||||
ACTION_STOP -> {
|
||||
(application as NodeApp).peekRuntime()?.disconnect()
|
||||
(application as NodeApp).runtime.disconnect()
|
||||
stopSelf()
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
@@ -43,12 +43,11 @@ import kotlinx.serialization.json.buildJsonObject
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
|
||||
class NodeRuntime(
|
||||
context: Context,
|
||||
val prefs: SecurePrefs = SecurePrefs(context.applicationContext),
|
||||
) {
|
||||
class NodeRuntime(context: Context) {
|
||||
private val appContext = context.applicationContext
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
|
||||
val prefs = SecurePrefs(appContext)
|
||||
private val deviceAuthStore = DeviceAuthStore(prefs)
|
||||
val canvas = CanvasController()
|
||||
val camera = CameraCaptureManager(appContext)
|
||||
|
||||
@@ -265,7 +265,7 @@ class ChatController(
|
||||
}
|
||||
|
||||
val historyJson = session.request("chat.history", """{"sessionKey":"$key"}""")
|
||||
val history = parseHistory(historyJson, sessionKey = key, previousMessages = _messages.value)
|
||||
val history = parseHistory(historyJson, sessionKey = key)
|
||||
_messages.value = history.messages
|
||||
_sessionId.value = history.sessionId
|
||||
history.thinkingLevel?.trim()?.takeIf { it.isNotEmpty() }?.let { _thinkingLevel.value = it }
|
||||
@@ -336,7 +336,7 @@ class ChatController(
|
||||
try {
|
||||
val historyJson =
|
||||
session.request("chat.history", """{"sessionKey":"${_sessionKey.value}"}""")
|
||||
val history = parseHistory(historyJson, sessionKey = _sessionKey.value, previousMessages = _messages.value)
|
||||
val history = parseHistory(historyJson, sessionKey = _sessionKey.value)
|
||||
_messages.value = history.messages
|
||||
_sessionId.value = history.sessionId
|
||||
history.thinkingLevel?.trim()?.takeIf { it.isNotEmpty() }?.let { _thinkingLevel.value = it }
|
||||
@@ -450,11 +450,7 @@ class ChatController(
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseHistory(
|
||||
historyJson: String,
|
||||
sessionKey: String,
|
||||
previousMessages: List<ChatMessage>,
|
||||
): ChatHistory {
|
||||
private fun parseHistory(historyJson: String, sessionKey: String): ChatHistory {
|
||||
val root = json.parseToJsonElement(historyJson).asObjectOrNull() ?: return ChatHistory(sessionKey, null, null, emptyList())
|
||||
val sid = root["sessionId"].asStringOrNull()
|
||||
val thinkingLevel = root["thinkingLevel"].asStringOrNull()
|
||||
@@ -474,12 +470,7 @@ class ChatController(
|
||||
)
|
||||
}
|
||||
|
||||
return ChatHistory(
|
||||
sessionKey = sessionKey,
|
||||
sessionId = sid,
|
||||
thinkingLevel = thinkingLevel,
|
||||
messages = reconcileMessageIds(previous = previousMessages, incoming = messages),
|
||||
)
|
||||
return ChatHistory(sessionKey = sessionKey, sessionId = sid, thinkingLevel = thinkingLevel, messages = messages)
|
||||
}
|
||||
|
||||
private fun parseMessageContent(el: JsonElement): ChatMessageContent? {
|
||||
@@ -528,47 +519,6 @@ class ChatController(
|
||||
}
|
||||
}
|
||||
|
||||
internal fun reconcileMessageIds(previous: List<ChatMessage>, incoming: List<ChatMessage>): List<ChatMessage> {
|
||||
if (previous.isEmpty() || incoming.isEmpty()) return incoming
|
||||
|
||||
val idsByKey = LinkedHashMap<String, ArrayDeque<String>>()
|
||||
for (message in previous) {
|
||||
val key = messageIdentityKey(message) ?: continue
|
||||
idsByKey.getOrPut(key) { ArrayDeque() }.addLast(message.id)
|
||||
}
|
||||
|
||||
return incoming.map { message ->
|
||||
val key = messageIdentityKey(message) ?: return@map message
|
||||
val ids = idsByKey[key] ?: return@map message
|
||||
val reusedId = ids.removeFirstOrNull() ?: return@map message
|
||||
if (ids.isEmpty()) {
|
||||
idsByKey.remove(key)
|
||||
}
|
||||
if (reusedId == message.id) return@map message
|
||||
message.copy(id = reusedId)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun messageIdentityKey(message: ChatMessage): String? {
|
||||
val role = message.role.trim().lowercase()
|
||||
if (role.isEmpty()) return null
|
||||
|
||||
val timestamp = message.timestampMs?.toString().orEmpty()
|
||||
val contentFingerprint =
|
||||
message.content.joinToString(separator = "\u001E") { part ->
|
||||
listOf(
|
||||
part.type.trim().lowercase(),
|
||||
part.text?.trim().orEmpty(),
|
||||
part.mimeType?.trim()?.lowercase().orEmpty(),
|
||||
part.fileName?.trim().orEmpty(),
|
||||
part.base64?.hashCode()?.toString().orEmpty(),
|
||||
).joinToString(separator = "\u001F")
|
||||
}
|
||||
|
||||
if (timestamp.isEmpty() && contentFingerprint.isEmpty()) return null
|
||||
return listOf(role, timestamp, contentFingerprint).joinToString(separator = "|")
|
||||
}
|
||||
|
||||
private fun JsonElement?.asObjectOrNull(): JsonObject? = this as? JsonObject
|
||||
|
||||
private fun JsonElement?.asArrayOrNull(): JsonArray? = this as? JsonArray
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package ai.openclaw.app.ui.chat
|
||||
|
||||
import android.graphics.BitmapFactory
|
||||
import android.util.Base64
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -26,7 +28,8 @@ internal fun rememberBase64ImageState(base64: String): Base64ImageState {
|
||||
image =
|
||||
withContext(Dispatchers.Default) {
|
||||
try {
|
||||
val bitmap = decodeBase64Bitmap(base64) ?: return@withContext null
|
||||
val bytes = Base64.decode(base64, Base64.DEFAULT)
|
||||
val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size) ?: return@withContext null
|
||||
bitmap.asImageBitmap()
|
||||
} catch (_: Throwable) {
|
||||
null
|
||||
|
||||
@@ -1,150 +0,0 @@
|
||||
package ai.openclaw.app.ui.chat
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.net.Uri
|
||||
import android.util.Base64
|
||||
import android.util.LruCache
|
||||
import androidx.core.graphics.scale
|
||||
import ai.openclaw.app.node.JpegSizeLimiter
|
||||
import java.io.ByteArrayOutputStream
|
||||
import kotlin.math.max
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
private const val CHAT_ATTACHMENT_MAX_WIDTH = 1600
|
||||
private const val CHAT_ATTACHMENT_MAX_BASE64_CHARS = 300 * 1024
|
||||
private const val CHAT_ATTACHMENT_START_QUALITY = 85
|
||||
private const val CHAT_DECODE_MAX_DIMENSION = 1600
|
||||
private const val CHAT_IMAGE_CACHE_BYTES = 16 * 1024 * 1024
|
||||
|
||||
private val decodedBitmapCache =
|
||||
object : LruCache<String, Bitmap>(CHAT_IMAGE_CACHE_BYTES) {
|
||||
override fun sizeOf(key: String, value: Bitmap): Int = value.byteCount.coerceAtLeast(1)
|
||||
}
|
||||
|
||||
internal fun loadSizedImageAttachment(resolver: ContentResolver, uri: Uri): PendingImageAttachment {
|
||||
val fileName = normalizeAttachmentFileName((uri.lastPathSegment ?: "image").substringAfterLast('/'))
|
||||
val bitmap = decodeScaledBitmap(resolver, uri, maxDimension = CHAT_ATTACHMENT_MAX_WIDTH)
|
||||
if (bitmap == null) {
|
||||
throw IllegalStateException("unsupported attachment")
|
||||
}
|
||||
val maxBytes = (CHAT_ATTACHMENT_MAX_BASE64_CHARS / 4) * 3
|
||||
val encoded =
|
||||
JpegSizeLimiter.compressToLimit(
|
||||
initialWidth = bitmap.width,
|
||||
initialHeight = bitmap.height,
|
||||
startQuality = CHAT_ATTACHMENT_START_QUALITY,
|
||||
maxBytes = maxBytes,
|
||||
minSize = 240,
|
||||
encode = { width, height, quality ->
|
||||
val working =
|
||||
if (width == bitmap.width && height == bitmap.height) {
|
||||
bitmap
|
||||
} else {
|
||||
bitmap.scale(width, height, true)
|
||||
}
|
||||
try {
|
||||
val out = ByteArrayOutputStream()
|
||||
if (!working.compress(Bitmap.CompressFormat.JPEG, quality, out)) {
|
||||
throw IllegalStateException("attachment encode failed")
|
||||
}
|
||||
out.toByteArray()
|
||||
} finally {
|
||||
if (working !== bitmap) {
|
||||
working.recycle()
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
val base64 = Base64.encodeToString(encoded.bytes, Base64.NO_WRAP)
|
||||
return PendingImageAttachment(
|
||||
id = uri.toString() + "#" + System.currentTimeMillis().toString(),
|
||||
fileName = fileName,
|
||||
mimeType = "image/jpeg",
|
||||
base64 = base64,
|
||||
)
|
||||
}
|
||||
|
||||
internal fun decodeBase64Bitmap(base64: String, maxDimension: Int = CHAT_DECODE_MAX_DIMENSION): Bitmap? {
|
||||
val cacheKey = "$maxDimension:${base64.length}:${base64.hashCode()}"
|
||||
decodedBitmapCache.get(cacheKey)?.let { return it }
|
||||
|
||||
val bytes = Base64.decode(base64, Base64.DEFAULT)
|
||||
if (bytes.isEmpty()) return null
|
||||
|
||||
val bounds = BitmapFactory.Options().apply { inJustDecodeBounds = true }
|
||||
BitmapFactory.decodeByteArray(bytes, 0, bytes.size, bounds)
|
||||
if (bounds.outWidth <= 0 || bounds.outHeight <= 0) return null
|
||||
|
||||
val bitmap =
|
||||
BitmapFactory.decodeByteArray(
|
||||
bytes,
|
||||
0,
|
||||
bytes.size,
|
||||
BitmapFactory.Options().apply {
|
||||
inSampleSize = computeInSampleSize(bounds.outWidth, bounds.outHeight, maxDimension)
|
||||
inPreferredConfig = Bitmap.Config.RGB_565
|
||||
},
|
||||
) ?: return null
|
||||
|
||||
decodedBitmapCache.put(cacheKey, bitmap)
|
||||
return bitmap
|
||||
}
|
||||
|
||||
internal fun computeInSampleSize(width: Int, height: Int, maxDimension: Int): Int {
|
||||
if (width <= 0 || height <= 0 || maxDimension <= 0) return 1
|
||||
|
||||
var sample = 1
|
||||
var longestEdge = max(width, height)
|
||||
while (longestEdge > maxDimension && sample < 64) {
|
||||
sample *= 2
|
||||
longestEdge = max(width / sample, height / sample)
|
||||
}
|
||||
return sample.coerceAtLeast(1)
|
||||
}
|
||||
|
||||
internal fun normalizeAttachmentFileName(raw: String): String {
|
||||
val trimmed = raw.trim()
|
||||
if (trimmed.isEmpty()) return "image.jpg"
|
||||
val stem = trimmed.substringBeforeLast('.', missingDelimiterValue = trimmed).ifEmpty { "image" }
|
||||
return "$stem.jpg"
|
||||
}
|
||||
|
||||
private fun decodeScaledBitmap(
|
||||
resolver: ContentResolver,
|
||||
uri: Uri,
|
||||
maxDimension: Int,
|
||||
): Bitmap? {
|
||||
val bounds = BitmapFactory.Options().apply { inJustDecodeBounds = true }
|
||||
resolver.openInputStream(uri).use { input ->
|
||||
if (input == null) return null
|
||||
BitmapFactory.decodeStream(input, null, bounds)
|
||||
}
|
||||
if (bounds.outWidth <= 0 || bounds.outHeight <= 0) return null
|
||||
|
||||
val decoded =
|
||||
resolver.openInputStream(uri).use { input ->
|
||||
if (input == null) return null
|
||||
BitmapFactory.decodeStream(
|
||||
input,
|
||||
null,
|
||||
BitmapFactory.Options().apply {
|
||||
inSampleSize = computeInSampleSize(bounds.outWidth, bounds.outHeight, maxDimension)
|
||||
inPreferredConfig = Bitmap.Config.ARGB_8888
|
||||
},
|
||||
)
|
||||
} ?: return null
|
||||
|
||||
val longestEdge = max(decoded.width, decoded.height)
|
||||
if (longestEdge <= maxDimension) return decoded
|
||||
|
||||
val scale = maxDimension.toDouble() / longestEdge.toDouble()
|
||||
val targetWidth = max(1, (decoded.width * scale).roundToInt())
|
||||
val targetHeight = max(1, (decoded.height * scale).roundToInt())
|
||||
val scaled = decoded.scale(targetWidth, targetHeight, true)
|
||||
if (scaled !== decoded) {
|
||||
decoded.recycle()
|
||||
}
|
||||
return scaled
|
||||
}
|
||||
@@ -6,14 +6,12 @@ import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -36,19 +34,11 @@ fun ChatMessageListCard(
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val listState = rememberLazyListState()
|
||||
val displayMessages = remember(messages) { messages.asReversed() }
|
||||
val stream = streamingAssistantText?.trim()
|
||||
|
||||
// New list items/tool rows should animate into view, but token streaming should not restart
|
||||
// that animation on every delta.
|
||||
LaunchedEffect(messages.size, pendingRunCount, pendingToolCalls.size) {
|
||||
// With reverseLayout the newest item is at index 0 (bottom of screen).
|
||||
LaunchedEffect(messages.size, pendingRunCount, pendingToolCalls.size, streamingAssistantText) {
|
||||
listState.animateScrollToItem(index = 0)
|
||||
}
|
||||
LaunchedEffect(stream) {
|
||||
if (!stream.isNullOrEmpty()) {
|
||||
listState.scrollToItem(index = 0)
|
||||
}
|
||||
}
|
||||
|
||||
Box(modifier = modifier.fillMaxWidth()) {
|
||||
LazyColumn(
|
||||
@@ -60,6 +50,8 @@ fun ChatMessageListCard(
|
||||
) {
|
||||
// With reverseLayout = true, index 0 renders at the BOTTOM.
|
||||
// So we emit newest items first: streaming → tools → typing → messages (newest→oldest).
|
||||
|
||||
val stream = streamingAssistantText?.trim()
|
||||
if (!stream.isNullOrEmpty()) {
|
||||
item(key = "stream") {
|
||||
ChatStreamingAssistantBubble(text = stream)
|
||||
@@ -78,8 +70,8 @@ fun ChatMessageListCard(
|
||||
}
|
||||
}
|
||||
|
||||
items(items = displayMessages, key = { it.id }) { message ->
|
||||
ChatMessageBubble(message = message)
|
||||
items(count = messages.size, key = { idx -> messages[messages.size - 1 - idx].id }) { idx ->
|
||||
ChatMessageBubble(message = messages[messages.size - 1 - idx])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
package ai.openclaw.app.ui.chat
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.net.Uri
|
||||
import android.util.Base64
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
@@ -44,6 +47,7 @@ import ai.openclaw.app.ui.mobileDanger
|
||||
import ai.openclaw.app.ui.mobileDangerSoft
|
||||
import ai.openclaw.app.ui.mobileText
|
||||
import ai.openclaw.app.ui.mobileTextSecondary
|
||||
import java.io.ByteArrayOutputStream
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -79,7 +83,7 @@ fun ChatSheetContent(viewModel: MainViewModel) {
|
||||
val next =
|
||||
uris.take(8).mapNotNull { uri ->
|
||||
try {
|
||||
loadSizedImageAttachment(resolver, uri)
|
||||
loadImageAttachment(resolver, uri)
|
||||
} catch (_: Throwable) {
|
||||
null
|
||||
}
|
||||
@@ -156,10 +160,7 @@ private fun ChatThreadSelector(
|
||||
mainSessionKey: String,
|
||||
onSelectSession: (String) -> Unit,
|
||||
) {
|
||||
val sessionOptions =
|
||||
remember(sessionKey, sessions, mainSessionKey) {
|
||||
resolveSessionChoices(sessionKey, sessions, mainSessionKey = mainSessionKey)
|
||||
}
|
||||
val sessionOptions = resolveSessionChoices(sessionKey, sessions, mainSessionKey = mainSessionKey)
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().horizontalScroll(rememberScrollState()),
|
||||
@@ -213,3 +214,24 @@ data class PendingImageAttachment(
|
||||
val mimeType: String,
|
||||
val base64: String,
|
||||
)
|
||||
|
||||
private suspend fun loadImageAttachment(resolver: ContentResolver, uri: Uri): PendingImageAttachment {
|
||||
val mimeType = resolver.getType(uri) ?: "image/*"
|
||||
val fileName = (uri.lastPathSegment ?: "image").substringAfterLast('/')
|
||||
val bytes =
|
||||
withContext(Dispatchers.IO) {
|
||||
resolver.openInputStream(uri)?.use { input ->
|
||||
val out = ByteArrayOutputStream()
|
||||
input.copyTo(out)
|
||||
out.toByteArray()
|
||||
} ?: ByteArray(0)
|
||||
}
|
||||
if (bytes.isEmpty()) throw IllegalStateException("empty attachment")
|
||||
val base64 = Base64.encodeToString(bytes, Base64.NO_WRAP)
|
||||
return PendingImageAttachment(
|
||||
id = uri.toString() + "#" + System.currentTimeMillis().toString(),
|
||||
fileName = fileName,
|
||||
mimeType = mimeType,
|
||||
base64 = base64,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
package ai.openclaw.app.chat
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotEquals
|
||||
import org.junit.Test
|
||||
|
||||
class ChatControllerMessageIdentityTest {
|
||||
@Test
|
||||
fun reconcileMessageIdsReusesMatchingIdsAcrossHistoryReload() {
|
||||
val previous =
|
||||
listOf(
|
||||
ChatMessage(
|
||||
id = "msg-1",
|
||||
role = "assistant",
|
||||
content = listOf(ChatMessageContent(type = "text", text = "hello")),
|
||||
timestampMs = 1000L,
|
||||
),
|
||||
ChatMessage(
|
||||
id = "msg-2",
|
||||
role = "user",
|
||||
content = listOf(ChatMessageContent(type = "text", text = "hi")),
|
||||
timestampMs = 2000L,
|
||||
),
|
||||
)
|
||||
|
||||
val incoming =
|
||||
listOf(
|
||||
ChatMessage(
|
||||
id = "new-1",
|
||||
role = "assistant",
|
||||
content = listOf(ChatMessageContent(type = "text", text = "hello")),
|
||||
timestampMs = 1000L,
|
||||
),
|
||||
ChatMessage(
|
||||
id = "new-2",
|
||||
role = "user",
|
||||
content = listOf(ChatMessageContent(type = "text", text = "hi")),
|
||||
timestampMs = 2000L,
|
||||
),
|
||||
)
|
||||
|
||||
val reconciled = reconcileMessageIds(previous = previous, incoming = incoming)
|
||||
|
||||
assertEquals(listOf("msg-1", "msg-2"), reconciled.map { it.id })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun reconcileMessageIdsLeavesNewMessagesUntouched() {
|
||||
val previous =
|
||||
listOf(
|
||||
ChatMessage(
|
||||
id = "msg-1",
|
||||
role = "assistant",
|
||||
content = listOf(ChatMessageContent(type = "text", text = "hello")),
|
||||
timestampMs = 1000L,
|
||||
),
|
||||
)
|
||||
|
||||
val incoming =
|
||||
listOf(
|
||||
ChatMessage(
|
||||
id = "new-1",
|
||||
role = "assistant",
|
||||
content = listOf(ChatMessageContent(type = "text", text = "hello")),
|
||||
timestampMs = 1000L,
|
||||
),
|
||||
ChatMessage(
|
||||
id = "new-2",
|
||||
role = "assistant",
|
||||
content = listOf(ChatMessageContent(type = "text", text = "new reply")),
|
||||
timestampMs = 3000L,
|
||||
),
|
||||
)
|
||||
|
||||
val reconciled = reconcileMessageIds(previous = previous, incoming = incoming)
|
||||
|
||||
assertEquals("msg-1", reconciled[0].id)
|
||||
assertEquals("new-2", reconciled[1].id)
|
||||
assertNotEquals(reconciled[0].id, reconciled[1].id)
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
package ai.openclaw.app.ui.chat
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
class ChatImageCodecTest {
|
||||
@Test
|
||||
fun computeInSampleSizeCapsLongestEdge() {
|
||||
assertEquals(4, computeInSampleSize(width = 4032, height = 3024, maxDimension = 1600))
|
||||
assertEquals(1, computeInSampleSize(width = 800, height = 600, maxDimension = 1600))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun normalizeAttachmentFileNameForcesJpegExtension() {
|
||||
assertEquals("photo.jpg", normalizeAttachmentFileName("photo.png"))
|
||||
assertEquals("image.jpg", normalizeAttachmentFileName(""))
|
||||
}
|
||||
}
|
||||
@@ -8,24 +8,6 @@ final class CanvasA2UIActionMessageHandler: NSObject, WKScriptMessageHandler {
|
||||
static let messageName = "openclawCanvasA2UIAction"
|
||||
static let allMessageNames = [messageName]
|
||||
|
||||
// Compatibility helper for debug/test shims. Runtime dispatch remains
|
||||
// limited to in-app canvas schemes in `didReceive`.
|
||||
static func isLocalNetworkCanvasURL(_ url: URL) -> Bool {
|
||||
guard let scheme = url.scheme?.lowercased(), scheme == "http" || scheme == "https" else {
|
||||
return false
|
||||
}
|
||||
guard let host = url.host?.lowercased(), !host.isEmpty else {
|
||||
return false
|
||||
}
|
||||
if host == "localhost" {
|
||||
return true
|
||||
}
|
||||
guard let ip = Self.parseIPv4(host) else {
|
||||
return false
|
||||
}
|
||||
return Self.isLocalNetworkIPv4(ip)
|
||||
}
|
||||
|
||||
private let sessionKey: String
|
||||
|
||||
init(sessionKey: String) {
|
||||
@@ -122,24 +104,5 @@ final class CanvasA2UIActionMessageHandler: NSObject, WKScriptMessageHandler {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static func parseIPv4(_ host: String) -> (UInt8, UInt8, UInt8, UInt8)? {
|
||||
let parts = host.split(separator: ".", omittingEmptySubsequences: false)
|
||||
guard parts.count == 4 else { return nil }
|
||||
let bytes = parts.compactMap { UInt8($0) }
|
||||
guard bytes.count == 4 else { return nil }
|
||||
return (bytes[0], bytes[1], bytes[2], bytes[3])
|
||||
}
|
||||
|
||||
private static func isLocalNetworkIPv4(_ ip: (UInt8, UInt8, UInt8, UInt8)) -> Bool {
|
||||
let (a, b, _, _) = ip
|
||||
if a == 10 { return true }
|
||||
if a == 172, (16...31).contains(Int(b)) { return true }
|
||||
if a == 192, b == 168 { return true }
|
||||
if a == 127 { return true }
|
||||
if a == 169, b == 254 { return true }
|
||||
if a == 100, (64...127).contains(Int(b)) { return true }
|
||||
return false
|
||||
}
|
||||
// Formatting helpers live in OpenClawKit (`OpenClawCanvasA2UIAction`).
|
||||
}
|
||||
|
||||
@@ -81,23 +81,22 @@ final class CanvasSchemeHandler: NSObject, WKURLSchemeHandler {
|
||||
return self.html("Not Found", title: "Canvas: 404")
|
||||
}
|
||||
|
||||
// Resolve symlinks before enforcing the session-root boundary so links inside
|
||||
// the canvas tree cannot escape to arbitrary host files.
|
||||
let resolvedRoot = sessionRoot.resolvingSymlinksInPath().standardizedFileURL
|
||||
let resolvedFile = fileURL.resolvingSymlinksInPath().standardizedFileURL
|
||||
guard self.isFileURL(resolvedFile, withinDirectory: resolvedRoot) else {
|
||||
// Directory traversal guard: served files must live under the session root.
|
||||
let standardizedRoot = sessionRoot.standardizedFileURL
|
||||
let standardizedFile = fileURL.standardizedFileURL
|
||||
guard standardizedFile.path.hasPrefix(standardizedRoot.path) else {
|
||||
return self.html("Forbidden", title: "Canvas: 403")
|
||||
}
|
||||
|
||||
do {
|
||||
let data = try Data(contentsOf: resolvedFile)
|
||||
let mime = CanvasScheme.mimeType(forExtension: resolvedFile.pathExtension)
|
||||
let servedPath = resolvedFile.path
|
||||
let data = try Data(contentsOf: standardizedFile)
|
||||
let mime = CanvasScheme.mimeType(forExtension: standardizedFile.pathExtension)
|
||||
let servedPath = standardizedFile.path
|
||||
canvasLogger.debug(
|
||||
"served \(session, privacy: .public)/\(path, privacy: .public) -> \(servedPath, privacy: .public)")
|
||||
return CanvasResponse(mime: mime, data: data)
|
||||
} catch {
|
||||
let failedPath = resolvedFile.path
|
||||
let failedPath = standardizedFile.path
|
||||
let errorText = error.localizedDescription
|
||||
canvasLogger
|
||||
.error(
|
||||
@@ -146,11 +145,6 @@ final class CanvasSchemeHandler: NSObject, WKURLSchemeHandler {
|
||||
return nil
|
||||
}
|
||||
|
||||
private func isFileURL(_ fileURL: URL, withinDirectory rootURL: URL) -> Bool {
|
||||
let rootPath = rootURL.path.hasSuffix("/") ? rootURL.path : rootURL.path + "/"
|
||||
return fileURL.path == rootURL.path || fileURL.path.hasPrefix(rootPath)
|
||||
}
|
||||
|
||||
private func html(_ body: String, title: String = "Canvas") -> CanvasResponse {
|
||||
let html = """
|
||||
<!doctype html>
|
||||
|
||||
@@ -254,71 +254,6 @@ struct CronJob: Identifiable, Codable, Equatable {
|
||||
case state
|
||||
}
|
||||
|
||||
init(
|
||||
id: String,
|
||||
agentId: String?,
|
||||
name: String,
|
||||
description: String?,
|
||||
enabled: Bool,
|
||||
deleteAfterRun: Bool?,
|
||||
createdAtMs: Int,
|
||||
updatedAtMs: Int,
|
||||
schedule: CronSchedule,
|
||||
sessionTarget: CronSessionTarget,
|
||||
wakeMode: CronWakeMode,
|
||||
payload: CronPayload,
|
||||
delivery: CronDelivery?,
|
||||
state: CronJobState)
|
||||
{
|
||||
self.init(
|
||||
id: id,
|
||||
agentId: agentId,
|
||||
name: name,
|
||||
description: description,
|
||||
enabled: enabled,
|
||||
deleteAfterRun: deleteAfterRun,
|
||||
createdAtMs: createdAtMs,
|
||||
updatedAtMs: updatedAtMs,
|
||||
schedule: schedule,
|
||||
sessionTarget: .predefined(sessionTarget),
|
||||
wakeMode: wakeMode,
|
||||
payload: payload,
|
||||
delivery: delivery,
|
||||
state: state)
|
||||
}
|
||||
|
||||
init(
|
||||
id: String,
|
||||
agentId: String?,
|
||||
name: String,
|
||||
description: String?,
|
||||
enabled: Bool,
|
||||
deleteAfterRun: Bool?,
|
||||
createdAtMs: Int,
|
||||
updatedAtMs: Int,
|
||||
schedule: CronSchedule,
|
||||
sessionTarget: CronCustomSessionTarget,
|
||||
wakeMode: CronWakeMode,
|
||||
payload: CronPayload,
|
||||
delivery: CronDelivery?,
|
||||
state: CronJobState)
|
||||
{
|
||||
self.id = id
|
||||
self.agentId = agentId
|
||||
self.name = name
|
||||
self.description = description
|
||||
self.enabled = enabled
|
||||
self.deleteAfterRun = deleteAfterRun
|
||||
self.createdAtMs = createdAtMs
|
||||
self.updatedAtMs = updatedAtMs
|
||||
self.schedule = schedule
|
||||
self.sessionTargetRaw = sessionTarget.rawValue
|
||||
self.wakeMode = wakeMode
|
||||
self.payload = payload
|
||||
self.delivery = delivery
|
||||
self.state = state
|
||||
}
|
||||
|
||||
/// Parsed session target (predefined or custom session ID)
|
||||
var parsedSessionTarget: CronCustomSessionTarget {
|
||||
CronCustomSessionTarget.from(self.sessionTargetRaw)
|
||||
|
||||
@@ -89,20 +89,6 @@ private func readLineFromHandle(_ handle: FileHandle, maxBytes: Int) throws -> S
|
||||
return String(data: lineData, encoding: .utf8)
|
||||
}
|
||||
|
||||
func timingSafeHexStringEquals(_ lhs: String, _ rhs: String) -> Bool {
|
||||
let lhsBytes = Array(lhs.utf8)
|
||||
let rhsBytes = Array(rhs.utf8)
|
||||
guard lhsBytes.count == rhsBytes.count else {
|
||||
return false
|
||||
}
|
||||
|
||||
var diff: UInt8 = 0
|
||||
for index in lhsBytes.indices {
|
||||
diff |= lhsBytes[index] ^ rhsBytes[index]
|
||||
}
|
||||
return diff == 0
|
||||
}
|
||||
|
||||
enum ExecApprovalsSocketClient {
|
||||
private struct TimeoutError: LocalizedError {
|
||||
var message: String
|
||||
@@ -868,7 +854,7 @@ private final class ExecApprovalsSocketServer: @unchecked Sendable {
|
||||
error: ExecHostError(code: "INVALID_REQUEST", message: "expired request", reason: "ttl"))
|
||||
}
|
||||
let expected = self.hmacHex(nonce: request.nonce, ts: request.ts, requestJson: request.requestJson)
|
||||
if !timingSafeHexStringEquals(expected, request.hmac) {
|
||||
if expected != request.hmac {
|
||||
return ExecHostResponse(
|
||||
type: "exec-res",
|
||||
id: request.id,
|
||||
|
||||
@@ -23,23 +23,11 @@ enum HostEnvSecurityPolicy {
|
||||
"PS4",
|
||||
"GCONV_PATH",
|
||||
"IFS",
|
||||
"SSLKEYLOGFILE",
|
||||
"JAVA_TOOL_OPTIONS",
|
||||
"_JAVA_OPTIONS",
|
||||
"JDK_JAVA_OPTIONS",
|
||||
"PYTHONBREAKPOINT",
|
||||
"DOTNET_STARTUP_HOOKS",
|
||||
"DOTNET_ADDITIONAL_DEPS",
|
||||
"GLIBC_TUNABLES",
|
||||
"MAVEN_OPTS",
|
||||
"SBT_OPTS",
|
||||
"GRADLE_OPTS",
|
||||
"ANT_OPTS"
|
||||
"SSLKEYLOGFILE"
|
||||
]
|
||||
|
||||
static let blockedOverrideKeys: Set<String> = [
|
||||
"HOME",
|
||||
"GRADLE_USER_HOME",
|
||||
"ZDOTDIR",
|
||||
"GIT_SSH_COMMAND",
|
||||
"GIT_SSH",
|
||||
|
||||
@@ -26,12 +26,7 @@ enum LaunchAgentManager {
|
||||
}
|
||||
|
||||
private static func writePlist(bundlePath: String) {
|
||||
let plist = self.plistContents(bundlePath: bundlePath)
|
||||
try? plist.write(to: self.plistURL, atomically: true, encoding: .utf8)
|
||||
}
|
||||
|
||||
static func plistContents(bundlePath: String) -> String {
|
||||
"""
|
||||
let plist = """
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
@@ -46,6 +41,8 @@ enum LaunchAgentManager {
|
||||
<string>\(FileManager().homeDirectoryForCurrentUser.path)</string>
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
<key>KeepAlive</key>
|
||||
<true/>
|
||||
<key>EnvironmentVariables</key>
|
||||
<dict>
|
||||
<key>PATH</key>
|
||||
@@ -58,6 +55,7 @@ enum LaunchAgentManager {
|
||||
</dict>
|
||||
</plist>
|
||||
"""
|
||||
try? plist.write(to: self.plistURL, atomically: true, encoding: .utf8)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
|
||||
@@ -1099,31 +1099,36 @@ extension MenuSessionsInjector {
|
||||
// MARK: - Width + placement
|
||||
|
||||
private func findInsertIndex(in menu: NSMenu) -> Int? {
|
||||
self.findDynamicSectionInsertIndex(in: menu)
|
||||
}
|
||||
|
||||
private func findNodesInsertIndex(in menu: NSMenu) -> Int? {
|
||||
self.findDynamicSectionInsertIndex(in: menu)
|
||||
}
|
||||
|
||||
private func findDynamicSectionInsertIndex(in menu: NSMenu) -> Int? {
|
||||
// Keep controls and action buttons visible by inserting dynamic rows at the
|
||||
// built-in footer boundary, not by matching localized menu item titles.
|
||||
if let footerSeparatorIndex = menu.items.lastIndex(where: { item in
|
||||
item.isSeparatorItem && !self.isInjectedItem(item)
|
||||
}) {
|
||||
return footerSeparatorIndex
|
||||
// Insert right before the separator above "Send Heartbeats".
|
||||
if let idx = menu.items.firstIndex(where: { $0.title == "Send Heartbeats" }) {
|
||||
if let sepIdx = menu.items[..<idx].lastIndex(where: { $0.isSeparatorItem }) {
|
||||
return sepIdx
|
||||
}
|
||||
return idx
|
||||
}
|
||||
|
||||
if let firstBaseItemIndex = menu.items.firstIndex(where: { !self.isInjectedItem($0) }) {
|
||||
return min(firstBaseItemIndex + 1, menu.items.count)
|
||||
if let sepIdx = menu.items.firstIndex(where: { $0.isSeparatorItem }) {
|
||||
return sepIdx
|
||||
}
|
||||
|
||||
if menu.items.count >= 1 { return 1 }
|
||||
return menu.items.count
|
||||
}
|
||||
|
||||
private func isInjectedItem(_ item: NSMenuItem) -> Bool {
|
||||
item.tag == self.tag || item.tag == self.nodesTag
|
||||
private func findNodesInsertIndex(in menu: NSMenu) -> Int? {
|
||||
if let idx = menu.items.firstIndex(where: { $0.title == "Send Heartbeats" }) {
|
||||
if let sepIdx = menu.items[..<idx].lastIndex(where: { $0.isSeparatorItem }) {
|
||||
return sepIdx
|
||||
}
|
||||
return idx
|
||||
}
|
||||
|
||||
if let sepIdx = menu.items.firstIndex(where: { $0.isSeparatorItem }) {
|
||||
return sepIdx
|
||||
}
|
||||
|
||||
if menu.items.count >= 1 { return 1 }
|
||||
return menu.items.count
|
||||
}
|
||||
|
||||
private func initialWidth(for menu: NSMenu) -> CGFloat {
|
||||
@@ -1231,13 +1236,5 @@ extension MenuSessionsInjector {
|
||||
func injectForTesting(into menu: NSMenu) {
|
||||
self.inject(into: menu)
|
||||
}
|
||||
|
||||
func testingFindInsertIndex(in menu: NSMenu) -> Int? {
|
||||
self.findInsertIndex(in: menu)
|
||||
}
|
||||
|
||||
func testingFindNodesInsertIndex(in menu: NSMenu) -> Int? {
|
||||
self.findNodesInsertIndex(in: menu)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -6,7 +6,7 @@ enum NodeServiceManager {
|
||||
|
||||
static func start() async -> String? {
|
||||
let result = await self.runServiceCommandResult(
|
||||
["start"],
|
||||
["node", "start"],
|
||||
timeout: 20,
|
||||
quiet: false)
|
||||
if let error = self.errorMessage(from: result, treatNotLoadedAsError: true) {
|
||||
@@ -18,7 +18,7 @@ enum NodeServiceManager {
|
||||
|
||||
static func stop() async -> String? {
|
||||
let result = await self.runServiceCommandResult(
|
||||
["stop"],
|
||||
["node", "stop"],
|
||||
timeout: 15,
|
||||
quiet: false)
|
||||
if let error = self.errorMessage(from: result, treatNotLoadedAsError: false) {
|
||||
@@ -30,14 +30,6 @@ enum NodeServiceManager {
|
||||
}
|
||||
|
||||
extension NodeServiceManager {
|
||||
private static func serviceCommand(_ args: [String]) -> [String] {
|
||||
CommandResolver.openclawCommand(
|
||||
subcommand: "node",
|
||||
extraArgs: self.withJsonFlag(args),
|
||||
// Service management must always run locally, even if remote mode is configured.
|
||||
configRoot: ["gateway": ["mode": "local"]])
|
||||
}
|
||||
|
||||
private struct CommandResult {
|
||||
let success: Bool
|
||||
let payload: Data?
|
||||
@@ -60,7 +52,11 @@ extension NodeServiceManager {
|
||||
timeout: Double,
|
||||
quiet: Bool) async -> CommandResult
|
||||
{
|
||||
let command = self.serviceCommand(args)
|
||||
let command = CommandResolver.openclawCommand(
|
||||
subcommand: "service",
|
||||
extraArgs: self.withJsonFlag(args),
|
||||
// Service management must always run locally, even if remote mode is configured.
|
||||
configRoot: ["gateway": ["mode": "local"]])
|
||||
var env = ProcessInfo.processInfo.environment
|
||||
env["PATH"] = CommandResolver.preferredPaths().joined(separator: ":")
|
||||
let response = await ShellExecutor.runDetailed(command: command, cwd: nil, env: env, timeout: timeout)
|
||||
@@ -140,11 +136,3 @@ extension NodeServiceManager {
|
||||
TextSummarySupport.summarizeLastLine(text)
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
extension NodeServiceManager {
|
||||
static func _testServiceCommand(_ args: [String]) -> [String] {
|
||||
self.serviceCommand(args)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -515,8 +515,6 @@ public struct PollParams: Codable, Sendable {
|
||||
public struct AgentParams: Codable, Sendable {
|
||||
public let message: String
|
||||
public let agentid: String?
|
||||
public let provider: String?
|
||||
public let model: String?
|
||||
public let to: String?
|
||||
public let replyto: String?
|
||||
public let sessionid: String?
|
||||
@@ -544,8 +542,6 @@ public struct AgentParams: Codable, Sendable {
|
||||
public init(
|
||||
message: String,
|
||||
agentid: String?,
|
||||
provider: String?,
|
||||
model: String?,
|
||||
to: String?,
|
||||
replyto: String?,
|
||||
sessionid: String?,
|
||||
@@ -572,8 +568,6 @@ public struct AgentParams: Codable, Sendable {
|
||||
{
|
||||
self.message = message
|
||||
self.agentid = agentid
|
||||
self.provider = provider
|
||||
self.model = model
|
||||
self.to = to
|
||||
self.replyto = replyto
|
||||
self.sessionid = sessionid
|
||||
@@ -602,8 +596,6 @@ public struct AgentParams: Codable, Sendable {
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case message
|
||||
case agentid = "agentId"
|
||||
case provider
|
||||
case model
|
||||
case to
|
||||
case replyto = "replyTo"
|
||||
case sessionid = "sessionId"
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import Testing
|
||||
@testable import OpenClaw
|
||||
|
||||
struct ExecApprovalsSocketAuthTests {
|
||||
@Test
|
||||
func `timing safe hex compare matches equal strings`() {
|
||||
#expect(timingSafeHexStringEquals(String(repeating: "a", count: 64), String(repeating: "a", count: 64)))
|
||||
}
|
||||
|
||||
@Test
|
||||
func `timing safe hex compare rejects mismatched strings`() {
|
||||
let expected = String(repeating: "a", count: 63) + "b"
|
||||
let provided = String(repeating: "a", count: 63) + "c"
|
||||
#expect(!timingSafeHexStringEquals(expected, provided))
|
||||
}
|
||||
|
||||
@Test
|
||||
func `timing safe hex compare rejects different length strings`() {
|
||||
#expect(!timingSafeHexStringEquals(String(repeating: "a", count: 64), "deadbeef"))
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import Foundation
|
||||
import Testing
|
||||
@testable import OpenClaw
|
||||
|
||||
struct LaunchAgentManagerTests {
|
||||
@Test func `launch at login plist does not keep app alive after manual quit`() throws {
|
||||
let plist = LaunchAgentManager.plistContents(bundlePath: "/Applications/OpenClaw.app")
|
||||
let data = try #require(plist.data(using: .utf8))
|
||||
let object = try #require(
|
||||
PropertyListSerialization.propertyList(from: data, format: nil) as? [String: Any]
|
||||
)
|
||||
|
||||
#expect(object["RunAtLoad"] as? Bool == true)
|
||||
#expect(object["KeepAlive"] == nil)
|
||||
|
||||
let args = try #require(object["ProgramArguments"] as? [String])
|
||||
#expect(args == ["/Applications/OpenClaw.app/Contents/MacOS/OpenClaw"])
|
||||
}
|
||||
}
|
||||
@@ -216,32 +216,6 @@ struct LowCoverageHelperTests {
|
||||
#expect(handler._testTextEncodingName(for: "application/octet-stream") == nil)
|
||||
}
|
||||
|
||||
@Test @MainActor func `canvas scheme handler blocks symlink escapes`() throws {
|
||||
let root = FileManager().temporaryDirectory
|
||||
.appendingPathComponent("canvas-\(UUID().uuidString)", isDirectory: true)
|
||||
defer { try? FileManager().removeItem(at: root) }
|
||||
try FileManager().createDirectory(at: root, withIntermediateDirectories: true)
|
||||
|
||||
let session = root.appendingPathComponent("main", isDirectory: true)
|
||||
try FileManager().createDirectory(at: session, withIntermediateDirectories: true)
|
||||
|
||||
let outside = root.deletingLastPathComponent().appendingPathComponent("canvas-secret-\(UUID().uuidString).txt")
|
||||
defer { try? FileManager().removeItem(at: outside) }
|
||||
try "top-secret".write(to: outside, atomically: true, encoding: .utf8)
|
||||
|
||||
let symlink = session.appendingPathComponent("index.html")
|
||||
try FileManager().createSymbolicLink(at: symlink, withDestinationURL: outside)
|
||||
|
||||
let handler = CanvasSchemeHandler(root: root)
|
||||
let url = try #require(CanvasScheme.makeURL(session: "main", path: "index.html"))
|
||||
let response = handler._testResponse(for: url)
|
||||
let body = String(data: response.data, encoding: .utf8) ?? ""
|
||||
|
||||
#expect(response.mime == "text/html")
|
||||
#expect(body.contains("Forbidden"))
|
||||
#expect(!body.contains("top-secret"))
|
||||
}
|
||||
|
||||
@Test @MainActor func `menu context card injector inserts and finds index`() {
|
||||
let injector = MenuContextCardInjector()
|
||||
let menu = NSMenu()
|
||||
|
||||
@@ -5,26 +5,7 @@ import Testing
|
||||
@Suite(.serialized)
|
||||
@MainActor
|
||||
struct MenuSessionsInjectorTests {
|
||||
@Test func anchorsDynamicRowsBelowControlsAndActions() throws {
|
||||
let injector = MenuSessionsInjector()
|
||||
|
||||
let menu = NSMenu()
|
||||
menu.addItem(NSMenuItem(title: "Header", action: nil, keyEquivalent: ""))
|
||||
menu.addItem(.separator())
|
||||
menu.addItem(NSMenuItem(title: "Send Heartbeats", action: nil, keyEquivalent: ""))
|
||||
menu.addItem(NSMenuItem(title: "Browser Control", action: nil, keyEquivalent: ""))
|
||||
menu.addItem(.separator())
|
||||
menu.addItem(NSMenuItem(title: "Open Dashboard", action: nil, keyEquivalent: ""))
|
||||
menu.addItem(NSMenuItem(title: "Open Chat", action: nil, keyEquivalent: ""))
|
||||
menu.addItem(.separator())
|
||||
menu.addItem(NSMenuItem(title: "Settings…", action: nil, keyEquivalent: ""))
|
||||
|
||||
let footerSeparatorIndex = try #require(menu.items.lastIndex(where: { $0.isSeparatorItem }))
|
||||
#expect(injector.testingFindInsertIndex(in: menu) == footerSeparatorIndex)
|
||||
#expect(injector.testingFindNodesInsertIndex(in: menu) == footerSeparatorIndex)
|
||||
}
|
||||
|
||||
@Test func injectsDisconnectedMessage() {
|
||||
@Test func `injects disconnected message`() {
|
||||
let injector = MenuSessionsInjector()
|
||||
injector.setTestingControlChannelConnected(false)
|
||||
injector.setTestingSnapshot(nil, errorText: nil)
|
||||
@@ -38,7 +19,7 @@ struct MenuSessionsInjectorTests {
|
||||
#expect(menu.items.contains { $0.tag == 9_415_557 })
|
||||
}
|
||||
|
||||
@Test func injectsSessionRows() throws {
|
||||
@Test func `injects session rows`() {
|
||||
let injector = MenuSessionsInjector()
|
||||
injector.setTestingControlChannelConnected(true)
|
||||
|
||||
@@ -107,22 +88,10 @@ struct MenuSessionsInjectorTests {
|
||||
menu.addItem(NSMenuItem(title: "Header", action: nil, keyEquivalent: ""))
|
||||
menu.addItem(.separator())
|
||||
menu.addItem(NSMenuItem(title: "Send Heartbeats", action: nil, keyEquivalent: ""))
|
||||
menu.addItem(NSMenuItem(title: "Browser Control", action: nil, keyEquivalent: ""))
|
||||
menu.addItem(.separator())
|
||||
menu.addItem(NSMenuItem(title: "Open Dashboard", action: nil, keyEquivalent: ""))
|
||||
menu.addItem(.separator())
|
||||
menu.addItem(NSMenuItem(title: "Settings…", action: nil, keyEquivalent: ""))
|
||||
|
||||
injector.injectForTesting(into: menu)
|
||||
#expect(menu.items.contains { $0.tag == 9_415_557 })
|
||||
#expect(menu.items.contains { $0.tag == 9_415_557 && $0.isSeparatorItem })
|
||||
let sendHeartbeatsIndex = try #require(menu.items.firstIndex(where: { $0.title == "Send Heartbeats" }))
|
||||
let openDashboardIndex = try #require(menu.items.firstIndex(where: { $0.title == "Open Dashboard" }))
|
||||
let firstInjectedIndex = try #require(menu.items.firstIndex(where: { $0.tag == 9_415_557 }))
|
||||
let settingsIndex = try #require(menu.items.firstIndex(where: { $0.title == "Settings…" }))
|
||||
#expect(sendHeartbeatsIndex < firstInjectedIndex)
|
||||
#expect(openDashboardIndex < firstInjectedIndex)
|
||||
#expect(firstInjectedIndex < settingsIndex)
|
||||
}
|
||||
|
||||
@Test func `cost usage submenu does not use injector delegate`() {
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import Foundation
|
||||
import Testing
|
||||
@testable import OpenClaw
|
||||
|
||||
@Suite(.serialized) struct NodeServiceManagerTests {
|
||||
@Test func `builds node service commands with current CLI shape`() throws {
|
||||
let tmp = try makeTempDirForTests()
|
||||
CommandResolver.setProjectRoot(tmp.path)
|
||||
|
||||
let openclawPath = tmp.appendingPathComponent("node_modules/.bin/openclaw")
|
||||
try makeExecutableForTests(at: openclawPath)
|
||||
|
||||
let start = NodeServiceManager._testServiceCommand(["start"])
|
||||
#expect(start == [openclawPath.path, "node", "start", "--json"])
|
||||
|
||||
let stop = NodeServiceManager._testServiceCommand(["stop"])
|
||||
#expect(stop == [openclawPath.path, "node", "stop", "--json"])
|
||||
}
|
||||
}
|
||||
@@ -515,8 +515,6 @@ public struct PollParams: Codable, Sendable {
|
||||
public struct AgentParams: Codable, Sendable {
|
||||
public let message: String
|
||||
public let agentid: String?
|
||||
public let provider: String?
|
||||
public let model: String?
|
||||
public let to: String?
|
||||
public let replyto: String?
|
||||
public let sessionid: String?
|
||||
@@ -544,8 +542,6 @@ public struct AgentParams: Codable, Sendable {
|
||||
public init(
|
||||
message: String,
|
||||
agentid: String?,
|
||||
provider: String?,
|
||||
model: String?,
|
||||
to: String?,
|
||||
replyto: String?,
|
||||
sessionid: String?,
|
||||
@@ -572,8 +568,6 @@ public struct AgentParams: Codable, Sendable {
|
||||
{
|
||||
self.message = message
|
||||
self.agentid = agentid
|
||||
self.provider = provider
|
||||
self.model = model
|
||||
self.to = to
|
||||
self.replyto = replyto
|
||||
self.sessionid = sessionid
|
||||
@@ -602,8 +596,6 @@ public struct AgentParams: Codable, Sendable {
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case message
|
||||
case agentid = "agentId"
|
||||
case provider
|
||||
case model
|
||||
case to
|
||||
case replyto = "replyTo"
|
||||
case sessionid = "sessionId"
|
||||
|
||||
23
assets/chrome-extension/README.md
Normal file
23
assets/chrome-extension/README.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# OpenClaw Chrome Extension (Browser Relay)
|
||||
|
||||
Purpose: attach OpenClaw to an existing Chrome tab so the Gateway can automate it (via the local CDP relay server).
|
||||
|
||||
## Dev / load unpacked
|
||||
|
||||
1. Build/run OpenClaw Gateway with browser control enabled.
|
||||
2. Ensure the relay server is reachable at `http://127.0.0.1:18792/` (default).
|
||||
3. Install the extension to a stable path:
|
||||
|
||||
```bash
|
||||
openclaw browser extension install
|
||||
openclaw browser extension path
|
||||
```
|
||||
|
||||
4. Chrome → `chrome://extensions` → enable “Developer mode”.
|
||||
5. “Load unpacked” → select the path printed above.
|
||||
6. Pin the extension. Click the icon on a tab to attach/detach.
|
||||
|
||||
## Options
|
||||
|
||||
- `Relay port`: defaults to `18792`.
|
||||
- `Gateway token`: required. Set this to `gateway.auth.token` (or `OPENCLAW_GATEWAY_TOKEN`).
|
||||
64
assets/chrome-extension/background-utils.js
Normal file
64
assets/chrome-extension/background-utils.js
Normal file
@@ -0,0 +1,64 @@
|
||||
export function reconnectDelayMs(
|
||||
attempt,
|
||||
opts = { baseMs: 1000, maxMs: 30000, jitterMs: 1000, random: Math.random },
|
||||
) {
|
||||
const baseMs = Number.isFinite(opts.baseMs) ? opts.baseMs : 1000;
|
||||
const maxMs = Number.isFinite(opts.maxMs) ? opts.maxMs : 30000;
|
||||
const jitterMs = Number.isFinite(opts.jitterMs) ? opts.jitterMs : 1000;
|
||||
const random = typeof opts.random === "function" ? opts.random : Math.random;
|
||||
const safeAttempt = Math.max(0, Number.isFinite(attempt) ? attempt : 0);
|
||||
const backoff = Math.min(baseMs * 2 ** safeAttempt, maxMs);
|
||||
return backoff + Math.max(0, jitterMs) * random();
|
||||
}
|
||||
|
||||
export async function deriveRelayToken(gatewayToken, port) {
|
||||
const enc = new TextEncoder();
|
||||
const key = await crypto.subtle.importKey(
|
||||
"raw",
|
||||
enc.encode(gatewayToken),
|
||||
{ name: "HMAC", hash: "SHA-256" },
|
||||
false,
|
||||
["sign"],
|
||||
);
|
||||
const sig = await crypto.subtle.sign(
|
||||
"HMAC",
|
||||
key,
|
||||
enc.encode(`openclaw-extension-relay-v1:${port}`),
|
||||
);
|
||||
return [...new Uint8Array(sig)].map((b) => b.toString(16).padStart(2, "0")).join("");
|
||||
}
|
||||
|
||||
export async function buildRelayWsUrl(port, gatewayToken) {
|
||||
const token = String(gatewayToken || "").trim();
|
||||
if (!token) {
|
||||
throw new Error(
|
||||
"Missing gatewayToken in extension settings (chrome.storage.local.gatewayToken)",
|
||||
);
|
||||
}
|
||||
const relayToken = await deriveRelayToken(token, port);
|
||||
return `ws://127.0.0.1:${port}/extension?token=${encodeURIComponent(relayToken)}`;
|
||||
}
|
||||
|
||||
export function isRetryableReconnectError(err) {
|
||||
const message = err instanceof Error ? err.message : String(err || "");
|
||||
if (message.includes("Missing gatewayToken")) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function isMissingTabError(err) {
|
||||
const message = (err instanceof Error ? err.message : String(err || "")).toLowerCase();
|
||||
return (
|
||||
message.includes("no tab with id") ||
|
||||
message.includes("no tab with given id") ||
|
||||
message.includes("tab not found")
|
||||
);
|
||||
}
|
||||
|
||||
export function isLastRemainingTab(allTabs, tabIdToClose) {
|
||||
if (!Array.isArray(allTabs)) {
|
||||
return true;
|
||||
}
|
||||
return allTabs.filter((tab) => tab && tab.id !== tabIdToClose).length === 0;
|
||||
}
|
||||
1025
assets/chrome-extension/background.js
Normal file
1025
assets/chrome-extension/background.js
Normal file
File diff suppressed because it is too large
Load Diff
25
assets/chrome-extension/manifest.json
Normal file
25
assets/chrome-extension/manifest.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "OpenClaw Browser Relay",
|
||||
"version": "0.1.0",
|
||||
"description": "Attach OpenClaw to your existing Chrome tab via a local CDP relay server.",
|
||||
"icons": {
|
||||
"16": "icons/icon16.png",
|
||||
"32": "icons/icon32.png",
|
||||
"48": "icons/icon48.png",
|
||||
"128": "icons/icon128.png"
|
||||
},
|
||||
"permissions": ["debugger", "tabs", "activeTab", "storage", "alarms", "webNavigation"],
|
||||
"host_permissions": ["http://127.0.0.1/*", "http://localhost/*"],
|
||||
"background": { "service_worker": "background.js", "type": "module" },
|
||||
"action": {
|
||||
"default_title": "OpenClaw Browser Relay (click to attach/detach)",
|
||||
"default_icon": {
|
||||
"16": "icons/icon16.png",
|
||||
"32": "icons/icon32.png",
|
||||
"48": "icons/icon48.png",
|
||||
"128": "icons/icon128.png"
|
||||
}
|
||||
},
|
||||
"options_ui": { "page": "options.html", "open_in_tab": true }
|
||||
}
|
||||
57
assets/chrome-extension/options-validation.js
Normal file
57
assets/chrome-extension/options-validation.js
Normal file
@@ -0,0 +1,57 @@
|
||||
const PORT_GUIDANCE = 'Use gateway port + 3 (for gateway 18789, relay is 18792).'
|
||||
|
||||
function hasCdpVersionShape(data) {
|
||||
return !!data && typeof data === 'object' && 'Browser' in data && 'Protocol-Version' in data
|
||||
}
|
||||
|
||||
export function classifyRelayCheckResponse(res, port) {
|
||||
if (!res) {
|
||||
return { action: 'throw', error: 'No response from service worker' }
|
||||
}
|
||||
|
||||
if (res.status === 401) {
|
||||
return { action: 'status', kind: 'error', message: 'Gateway token rejected. Check token and save again.' }
|
||||
}
|
||||
|
||||
if (res.error) {
|
||||
return { action: 'throw', error: res.error }
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
return { action: 'throw', error: `HTTP ${res.status}` }
|
||||
}
|
||||
|
||||
const contentType = String(res.contentType || '')
|
||||
if (!contentType.includes('application/json')) {
|
||||
return {
|
||||
action: 'status',
|
||||
kind: 'error',
|
||||
message: `Wrong port: this is likely the gateway, not the relay. ${PORT_GUIDANCE}`,
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasCdpVersionShape(res.json)) {
|
||||
return {
|
||||
action: 'status',
|
||||
kind: 'error',
|
||||
message: `Wrong port: expected relay /json/version response. ${PORT_GUIDANCE}`,
|
||||
}
|
||||
}
|
||||
|
||||
return { action: 'status', kind: 'ok', message: `Relay reachable and authenticated at http://127.0.0.1:${port}/` }
|
||||
}
|
||||
|
||||
export function classifyRelayCheckException(err, port) {
|
||||
const message = String(err || '').toLowerCase()
|
||||
if (message.includes('json') || message.includes('syntax')) {
|
||||
return {
|
||||
kind: 'error',
|
||||
message: `Wrong port: this is not a relay endpoint. ${PORT_GUIDANCE}`,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
kind: 'error',
|
||||
message: `Relay not reachable/authenticated at http://127.0.0.1:${port}/. Start OpenClaw browser relay and verify token.`,
|
||||
}
|
||||
}
|
||||
200
assets/chrome-extension/options.html
Normal file
200
assets/chrome-extension/options.html
Normal file
@@ -0,0 +1,200 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>OpenClaw Browser Relay</title>
|
||||
<style>
|
||||
:root {
|
||||
color-scheme: light dark;
|
||||
--accent: #ff5a36;
|
||||
--panel: color-mix(in oklab, canvas 92%, canvasText 8%);
|
||||
--border: color-mix(in oklab, canvasText 18%, transparent);
|
||||
--muted: color-mix(in oklab, canvasText 70%, transparent);
|
||||
--shadow: 0 10px 30px color-mix(in oklab, canvasText 18%, transparent);
|
||||
font-family: ui-rounded, system-ui, -apple-system, BlinkMacSystemFont, "SF Pro Rounded",
|
||||
"SF Pro Display", "Segoe UI", sans-serif;
|
||||
line-height: 1.4;
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
background:
|
||||
radial-gradient(1000px 500px at 10% 0%, color-mix(in oklab, var(--accent) 30%, transparent), transparent 70%),
|
||||
radial-gradient(900px 450px at 90% 0%, color-mix(in oklab, var(--accent) 18%, transparent), transparent 75%),
|
||||
canvas;
|
||||
color: canvasText;
|
||||
}
|
||||
.wrap {
|
||||
max-width: 820px;
|
||||
margin: 36px auto;
|
||||
padding: 0 24px 48px 24px;
|
||||
}
|
||||
header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
.logo {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 14px;
|
||||
background: color-mix(in oklab, var(--accent) 18%, transparent);
|
||||
border: 1px solid color-mix(in oklab, var(--accent) 35%, transparent);
|
||||
box-shadow: var(--shadow);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
.logo img {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
h1 {
|
||||
font-size: 20px;
|
||||
margin: 0;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
.subtitle {
|
||||
margin: 2px 0 0 0;
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 14px;
|
||||
}
|
||||
.card {
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 16px;
|
||||
padding: 16px;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
.card h2 {
|
||||
margin: 0 0 10px 0;
|
||||
font-size: 14px;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
.card p {
|
||||
margin: 8px 0 0 0;
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
label {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
input {
|
||||
width: 160px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--border);
|
||||
background: color-mix(in oklab, canvas 92%, canvasText 8%);
|
||||
color: canvasText;
|
||||
outline: none;
|
||||
}
|
||||
input:focus {
|
||||
border-color: color-mix(in oklab, var(--accent) 70%, transparent);
|
||||
box-shadow: 0 0 0 4px color-mix(in oklab, var(--accent) 20%, transparent);
|
||||
}
|
||||
button {
|
||||
padding: 10px 14px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid color-mix(in oklab, var(--accent) 55%, transparent);
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
color-mix(in oklab, var(--accent) 80%, white 20%),
|
||||
var(--accent)
|
||||
);
|
||||
color: white;
|
||||
font-weight: 650;
|
||||
letter-spacing: 0.01em;
|
||||
cursor: pointer;
|
||||
}
|
||||
button:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
.hint {
|
||||
margin-top: 10px;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
}
|
||||
code {
|
||||
font-family: ui-monospace, Menlo, Monaco, Consolas, "SF Mono", monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
a {
|
||||
color: color-mix(in oklab, var(--accent) 85%, canvasText 15%);
|
||||
}
|
||||
.status {
|
||||
margin-top: 10px;
|
||||
font-size: 12px;
|
||||
color: color-mix(in oklab, var(--accent) 70%, canvasText 30%);
|
||||
min-height: 16px;
|
||||
}
|
||||
.status[data-kind='ok'] {
|
||||
color: color-mix(in oklab, #16a34a 75%, canvasText 25%);
|
||||
}
|
||||
.status[data-kind='error'] {
|
||||
color: color-mix(in oklab, #ef4444 75%, canvasText 25%);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<header>
|
||||
<div class="logo" aria-hidden="true">
|
||||
<img src="icons/icon128.png" alt="" />
|
||||
</div>
|
||||
<div>
|
||||
<h1>OpenClaw Browser Relay</h1>
|
||||
<p class="subtitle">Click the toolbar button on a tab to attach / detach.</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="grid">
|
||||
<div class="card">
|
||||
<h2>Getting started</h2>
|
||||
<p>
|
||||
If you see a red <code>!</code> badge on the extension icon, the relay server is not reachable.
|
||||
Start OpenClaw’s browser relay on this machine (Gateway or node host), then click the toolbar button again.
|
||||
</p>
|
||||
<p>
|
||||
Full guide (install, remote Gateway, security): <a href="https://docs.openclaw.ai/tools/chrome-extension" target="_blank" rel="noreferrer">docs.openclaw.ai/tools/chrome-extension</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Relay connection</h2>
|
||||
<label for="port">Port</label>
|
||||
<div class="row">
|
||||
<input id="port" inputmode="numeric" pattern="[0-9]*" />
|
||||
</div>
|
||||
<label for="token" style="margin-top: 10px">Gateway token</label>
|
||||
<div class="row">
|
||||
<input id="token" type="password" autocomplete="off" style="width: min(520px, 100%)" />
|
||||
<button id="save" type="button">Save</button>
|
||||
</div>
|
||||
<div class="hint">
|
||||
Default port: <code>18792</code>. Extension connects to: <code id="relay-url">http://127.0.0.1:<port>/</code>.
|
||||
Gateway token must match <code>gateway.auth.token</code> (or <code>OPENCLAW_GATEWAY_TOKEN</code>).
|
||||
</div>
|
||||
<div class="status" id="status"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module" src="options.js"></script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
74
assets/chrome-extension/options.js
Normal file
74
assets/chrome-extension/options.js
Normal file
@@ -0,0 +1,74 @@
|
||||
import { deriveRelayToken } from './background-utils.js'
|
||||
import { classifyRelayCheckException, classifyRelayCheckResponse } from './options-validation.js'
|
||||
|
||||
const DEFAULT_PORT = 18792
|
||||
|
||||
function clampPort(value) {
|
||||
const n = Number.parseInt(String(value || ''), 10)
|
||||
if (!Number.isFinite(n)) return DEFAULT_PORT
|
||||
if (n <= 0 || n > 65535) return DEFAULT_PORT
|
||||
return n
|
||||
}
|
||||
|
||||
function updateRelayUrl(port) {
|
||||
const el = document.getElementById('relay-url')
|
||||
if (!el) return
|
||||
el.textContent = `http://127.0.0.1:${port}/`
|
||||
}
|
||||
|
||||
function setStatus(kind, message) {
|
||||
const status = document.getElementById('status')
|
||||
if (!status) return
|
||||
status.dataset.kind = kind || ''
|
||||
status.textContent = message || ''
|
||||
}
|
||||
|
||||
async function checkRelayReachable(port, token) {
|
||||
const url = `http://127.0.0.1:${port}/json/version`
|
||||
const trimmedToken = String(token || '').trim()
|
||||
if (!trimmedToken) {
|
||||
setStatus('error', 'Gateway token required. Save your gateway token to connect.')
|
||||
return
|
||||
}
|
||||
try {
|
||||
const relayToken = await deriveRelayToken(trimmedToken, port)
|
||||
// Delegate the fetch to the background service worker to bypass
|
||||
// CORS preflight on the custom x-openclaw-relay-token header.
|
||||
const res = await chrome.runtime.sendMessage({
|
||||
type: 'relayCheck',
|
||||
url,
|
||||
token: relayToken,
|
||||
})
|
||||
const result = classifyRelayCheckResponse(res, port)
|
||||
if (result.action === 'throw') throw new Error(result.error)
|
||||
setStatus(result.kind, result.message)
|
||||
} catch (err) {
|
||||
const result = classifyRelayCheckException(err, port)
|
||||
setStatus(result.kind, result.message)
|
||||
}
|
||||
}
|
||||
|
||||
async function load() {
|
||||
const stored = await chrome.storage.local.get(['relayPort', 'gatewayToken'])
|
||||
const port = clampPort(stored.relayPort)
|
||||
const token = String(stored.gatewayToken || '').trim()
|
||||
document.getElementById('port').value = String(port)
|
||||
document.getElementById('token').value = token
|
||||
updateRelayUrl(port)
|
||||
await checkRelayReachable(port, token)
|
||||
}
|
||||
|
||||
async function save() {
|
||||
const portInput = document.getElementById('port')
|
||||
const tokenInput = document.getElementById('token')
|
||||
const port = clampPort(portInput.value)
|
||||
const token = String(tokenInput.value || '').trim()
|
||||
await chrome.storage.local.set({ relayPort: port, gatewayToken: token })
|
||||
portInput.value = String(port)
|
||||
tokenInput.value = token
|
||||
updateRelayUrl(port)
|
||||
await checkRelayReachable(port, token)
|
||||
}
|
||||
|
||||
document.getElementById('save').addEventListener('click', () => void save())
|
||||
void load()
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -47,22 +47,6 @@
|
||||
"source": "Quick Start",
|
||||
"target": "快速开始"
|
||||
},
|
||||
{
|
||||
"source": "Capability Cookbook",
|
||||
"target": "能力扩展手册"
|
||||
},
|
||||
{
|
||||
"source": "Setup Wizard Reference",
|
||||
"target": "设置向导参考"
|
||||
},
|
||||
{
|
||||
"source": "CLI Setup Reference",
|
||||
"target": "CLI 设置参考"
|
||||
},
|
||||
{
|
||||
"source": "Setup Wizard (CLI)",
|
||||
"target": "设置向导(CLI)"
|
||||
},
|
||||
{
|
||||
"source": "Docs directory",
|
||||
"target": "文档目录"
|
||||
|
||||
@@ -0,0 +1,636 @@
|
||||
Temporary internal migration note: remove this document once the extension-host migration is complete.
|
||||
|
||||
# OpenClaw Capability Catalog And Arbitration Spec
|
||||
|
||||
Date: 2026-03-15
|
||||
|
||||
## Purpose
|
||||
|
||||
This document defines how the system compiles agent-visible, operator-visible, and runtime-internal catalogs from active contributions and how it resolves conflicting or parallel providers.
|
||||
|
||||
The kernel should expose canonical actions, not raw plugin identities.
|
||||
|
||||
Host-managed install, onboarding, and lightweight channel catalogs remain separate from the kernel capability catalog.
|
||||
|
||||
## TODOs
|
||||
|
||||
- [ ] Implement kernel-owned internal and agent-visible catalogs.
|
||||
- [ ] Implement host-owned operator catalogs and static setup catalogs.
|
||||
- [ ] Implement canonical action registration and review workflow in code.
|
||||
- [ ] Implement arbitration and conflict handling for at least one multi-provider family.
|
||||
- [ ] Migrate the existing tool, provider, setup, and slot-selection surfaces so they no longer act as parallel catalog or arbitration systems.
|
||||
- [ ] Record pilot parity for `thread-ownership` first and `telegram` second before broader catalog publication.
|
||||
- [ ] Track which current `main` actions have been mapped into canonical action ids.
|
||||
|
||||
## Implementation Status
|
||||
|
||||
Current status against this spec:
|
||||
|
||||
- canonical catalogs and arbitration have not started
|
||||
- host-managed static metadata work and early runtime/lifecycle boundary extraction have landed
|
||||
|
||||
What has been implemented:
|
||||
|
||||
- an initial Phase 0 cutover inventory now exists in `src/extension-host/cutover-inventory.md`
|
||||
- channel catalog package metadata parsing now routes through host-owned schema helpers
|
||||
- host-owned resolved-extension records now carry the static metadata needed for install, onboarding, and lightweight operator UX
|
||||
- config doc baseline generation now uses the same host-owned resolved-extension metadata path
|
||||
- plugin SDK alias resolution now routes through `src/extension-host/compat/loader-compat.ts`
|
||||
- loader alias-wired module loader creation now routes through `src/extension-host/activation/loader-module-loader.ts`
|
||||
- loader cache key construction and registry cache control now route through `src/extension-host/activation/loader-cache.ts`
|
||||
- loader lazy runtime proxy creation now routes through `src/extension-host/activation/loader-runtime-proxy.ts`
|
||||
- loader provenance helpers now route through `src/extension-host/policy/loader-provenance.ts`
|
||||
- loader duplicate-order and record/error policy now route through `src/extension-host/policy/loader-policy.ts`
|
||||
- loader discovery policy outcomes now route through `src/extension-host/policy/loader-discovery-policy.ts`
|
||||
- loader initial candidate planning and record creation now route through `src/extension-host/activation/loader-records.ts`
|
||||
- loader entry-path opening and module import now route through `src/extension-host/activation/loader-import.ts`
|
||||
- loader module-export resolution, config validation, and memory-slot load decisions now route through `src/extension-host/activation/loader-runtime.ts`
|
||||
- loader post-import planning and `register(...)` execution now route through `src/extension-host/activation/loader-register.ts`
|
||||
- loader per-candidate orchestration now routes through `src/extension-host/activation/loader-flow.ts`
|
||||
- loader top-level load orchestration now routes through `src/extension-host/activation/loader-orchestrator.ts`
|
||||
- loader host process state now routes through `src/extension-host/activation/loader-host-state.ts`
|
||||
- loader preflight and cache-hit setup now routes through `src/extension-host/activation/loader-preflight.ts`
|
||||
- loader post-preflight pipeline composition now routes through `src/extension-host/activation/loader-pipeline.ts`
|
||||
- loader execution setup composition now routes through `src/extension-host/activation/loader-execution.ts`
|
||||
- loader discovery and manifest bootstrap now routes through `src/extension-host/activation/loader-bootstrap.ts`
|
||||
- loader mutable activation state now routes through `src/extension-host/activation/loader-session.ts`
|
||||
- loader session run and finalization composition now routes through `src/extension-host/activation/loader-run.ts`
|
||||
- loader activation policy outcomes now route through `src/extension-host/policy/loader-activation-policy.ts`
|
||||
- loader record-state transitions now route through `src/extension-host/activation/loader-state.ts`, which now enforces an explicit loader lifecycle state machine while preserving compatibility `PluginRecord.status` values
|
||||
- loader finalization policy results now route through `src/extension-host/policy/loader-finalization-policy.ts`
|
||||
- loader final cache, readiness promotion, and activation finalization now routes through `src/extension-host/activation/loader-finalize.ts`
|
||||
- channel, provider, gateway-method, tool, CLI, service, command, context-engine, and hook registration normalization now has a host-owned helper boundary for future catalog migration
|
||||
- low-risk runtime compatibility writes for channel, provider, gateway-method, HTTP-route, tool, CLI, service, command, context-engine, and hook registrations now route through `src/extension-host/contributions/registry-writes.ts` ahead of broader catalog-backed registry ownership
|
||||
- legacy internal-hook bridging and typed prompt-injection compatibility policy now route through `src/extension-host/compat/hook-compat.ts` ahead of broader catalog-backed registry ownership
|
||||
- compatibility `OpenClawPluginApi` composition and logger shaping now route through `src/extension-host/compat/plugin-api.ts` ahead of broader catalog-backed registry ownership
|
||||
- compatibility plugin-registry facade ownership now routes through `src/extension-host/compat/plugin-registry.ts` ahead of broader catalog-backed registry ownership
|
||||
- compatibility plugin-registry policy now routes through `src/extension-host/compat/plugin-registry-compat.ts` ahead of broader catalog-backed registry ownership
|
||||
- compatibility plugin-registry registration actions now route through `src/extension-host/compat/plugin-registry-registrations.ts` ahead of broader catalog-backed registry ownership
|
||||
- host-owned runtime registry accessors now route through `src/extension-host/contributions/runtime-registry.ts` ahead of broader catalog-backed registry ownership, and the channel, provider, tool, command, HTTP-route, gateway-method, CLI, and service slices now keep host-owned storage there with mirrored legacy compatibility views
|
||||
- plugin command registration, matching, execution, listing, native command-spec projection, and loader reload clearing now route through `src/extension-host/contributions/command-runtime.ts` ahead of broader catalog-backed ownership
|
||||
- service startup, stop ordering, service-context creation, and failure logging now route through `src/extension-host/contributions/service-lifecycle.ts` ahead of broader catalog-backed lifecycle ownership
|
||||
- CLI duplicate detection, registrar invocation, and async failure logging now route through `src/extension-host/contributions/cli-lifecycle.ts` ahead of broader catalog-backed CLI ownership
|
||||
- gateway method-id aggregation, plugin diagnostic shaping, and extra-handler composition now route through `src/extension-host/contributions/gateway-methods.ts` ahead of broader catalog-backed gateway ownership
|
||||
- plugin tool resolution, conflict handling, optional-tool gating, and plugin-tool metadata tracking now route through `src/extension-host/contributions/tool-runtime.ts` ahead of broader catalog-backed tool ownership
|
||||
- plugin provider projection from registry entries into runtime provider objects now routes through `src/extension-host/contributions/provider-runtime.ts` ahead of broader catalog-backed provider ownership
|
||||
- plugin provider discovery filtering, order grouping, and result normalization now route through `src/extension-host/contributions/provider-discovery.ts` ahead of broader catalog-backed provider-discovery ownership
|
||||
- provider matching, auth-method selection, config-patch merging, and default-model application now route through `src/extension-host/contributions/provider-auth.ts` ahead of broader catalog-backed provider-auth ownership
|
||||
- provider onboarding option building, model-picker entry building, and provider-method choice resolution now route through `src/extension-host/contributions/provider-wizard.ts` ahead of broader catalog-backed provider-setup ownership
|
||||
- loaded-provider auth application, plugin-enable gating, auth-method execution, and post-auth default-model handling now route through `src/extension-host/contributions/provider-auth-flow.ts` ahead of broader catalog-backed provider-setup ownership
|
||||
- provider post-selection hook lookup and invocation now route through `src/extension-host/contributions/provider-model-selection.ts` ahead of broader catalog-backed provider-setup ownership
|
||||
|
||||
How it has been implemented:
|
||||
|
||||
- by moving package metadata parsing behind `src/extension-host/manifests/schema.ts`
|
||||
- by keeping the existing catalog behavior intact while shifting metadata ownership into normalized host-owned records
|
||||
- by reusing the resolved-extension registry for static operator/documentation surfaces instead of creating separate metadata caches
|
||||
- by beginning runtime registration migration with host-owned normalization helpers before attempting full canonical catalog publication
|
||||
- by beginning actual low-risk runtime write ownership for channel, provider, gateway-method, HTTP-route, tool, CLI, service, command, context-engine, and hook registrations before attempting full canonical catalog publication
|
||||
- by moving cache-key construction and registry cache control behind host-owned helpers before attempting canonical catalog publication
|
||||
- by beginning loader-path migration with host-owned compatibility, candidate-planning, import-flow, policy, runtime, register-flow, candidate-orchestration, top-level load orchestration, record-state with compatibility lifecycle mapping, and finalization helpers before attempting canonical catalog publication
|
||||
- by extracting lazy runtime proxy creation and alias-wired Jiti module-loader creation into host-owned helpers before catalog publication work
|
||||
- by extracting discovery, manifest loading, manifest diagnostics, discovery-policy logging, provenance building, and candidate ordering into a host-owned loader-bootstrap helper before catalog publication work
|
||||
- by extracting candidate iteration, manifest lookup, per-candidate session processing, and finalization handoff into a host-owned loader-run helper before catalog publication work
|
||||
- by converting the compatibility record-state layer into an enforced loader lifecycle state machine before catalog publication work
|
||||
- by extracting shared discovery warning-cache state and loader reset behavior into a host-owned loader-host-state helper before catalog publication work
|
||||
- by extracting test-default application, config normalization, cache-key construction, cache-hit activation, and command-clear setup into a host-owned loader-preflight helper before catalog publication work
|
||||
- by extracting post-preflight execution setup and session-run composition into a host-owned loader-pipeline helper before catalog publication work
|
||||
- by extracting runtime creation, registry creation, bootstrap setup, module-loader creation, and session creation into a host-owned loader-execution helper before catalog publication work
|
||||
- by moving mutable activation state into a host-owned loader session before catalog publication work
|
||||
- by extracting shared provenance path matching and install-rule evaluation into `src/extension-host/policy/loader-provenance.ts` so activation and finalization policy seams reuse one host-owned implementation
|
||||
- by turning open-allowlist discovery warnings into explicit host-owned discovery-policy results before catalog publication work
|
||||
- by moving duplicate precedence, config enablement, and early memory-slot gating into explicit host-owned activation-policy outcomes before catalog publication work
|
||||
- by turning provenance-based untracked-extension warnings and final memory-slot warnings into explicit host-owned finalization-policy results before catalog publication work
|
||||
- by extracting legacy internal-hook bridging and typed prompt-injection compatibility policy into a host-owned hook-compat helper while leaving actual hook execution ownership unchanged
|
||||
- by extracting compatibility `OpenClawPluginApi` composition and logger shaping into a host-owned plugin-api helper while keeping the concrete registration callbacks in the legacy registry surface
|
||||
- by extracting the remaining compatibility plugin-registry facade into a host-owned helper so `src/plugins/registry.ts` becomes a thin wrapper instead of the real owner
|
||||
- by extracting provider normalization, command duplicate enforcement, and registry-local diagnostic shaping into a host-owned registry-compat helper while leaving the underlying provider-validation and plugin-command subsystems unchanged
|
||||
- by extracting low-risk registry registration actions into a host-owned registry-registrations helper so the compatibility facade composes host-owned actions instead of implementing them inline
|
||||
- by extracting service startup, stop ordering, service-context creation, and failure logging into a host-owned service-lifecycle helper before broader catalog-backed service ownership
|
||||
- by extracting CLI duplicate detection, registrar invocation, and async failure logging into a host-owned CLI-lifecycle helper before broader catalog-backed CLI ownership
|
||||
- by extracting gateway method-id aggregation, plugin diagnostic shaping, and extra-handler composition into a host-owned gateway-methods helper before broader catalog-backed gateway ownership
|
||||
- by extracting plugin tool resolution, conflict handling, optional-tool gating, and plugin-tool metadata tracking into a host-owned tool-runtime helper before broader catalog-backed tool ownership
|
||||
- by extracting provider projection from registry entries into runtime provider objects into a host-owned provider-runtime helper before broader catalog-backed provider ownership
|
||||
- by extracting provider discovery filtering, order grouping, and result normalization into a host-owned provider-discovery helper before broader catalog-backed provider-discovery ownership
|
||||
- by extracting provider matching, auth-method selection, config-patch merging, and default-model application into a host-owned provider-auth helper before broader catalog-backed provider-auth ownership
|
||||
- by extracting provider onboarding option building, model-picker entry building, and provider-method choice resolution into a host-owned provider-wizard helper before broader catalog-backed provider-setup ownership
|
||||
- by extracting loaded-provider auth application, plugin-enable gating, auth-method execution, and post-auth default-model handling into a host-owned provider-auth-flow helper before broader catalog-backed provider-setup ownership
|
||||
- by extracting provider post-selection hook lookup and invocation into a host-owned provider-model-selection helper before broader catalog-backed provider-setup ownership
|
||||
- by extracting provider-id normalization into `src/agents/provider-id.ts` so provider-only host seams do not inherit the heavier agent and browser dependency graph from `src/agents/model-selection.ts`
|
||||
- by extracting model-ref parsing into `src/agents/model-ref.ts` and Google model-id normalization into `src/agents/google-model-id.ts` so provider auth and setup seams can be tested without pulling the heavier provider-loader and browser dependency graph
|
||||
- by introducing host-owned runtime-registry accessors for low-risk runtime consumers first, then moving channel, provider, tool, command, HTTP-route, gateway-method, CLI, and service storage into that host-owned state while keeping mirrored legacy compatibility arrays and handler maps before broader catalog publication or arbitration work
|
||||
- by moving plugin command duplicate enforcement, registration, matching, execution, listing, native command-spec projection, and loader reload clearing into `src/extension-host/contributions/command-runtime.ts` before broader catalog publication or arbitration work
|
||||
|
||||
What remains pending:
|
||||
|
||||
- canonical capability ids
|
||||
- runtime-derived kernel catalogs
|
||||
- host-owned operator catalogs beyond the existing lightweight static paths
|
||||
- arbitration modes and selection logic
|
||||
- tool/provider/slot migration into one canonical catalog and arbitration model
|
||||
|
||||
## Goals
|
||||
|
||||
- agents see a stable, context-aware catalog of what they can do
|
||||
- multiple active providers for the same functional area are supported
|
||||
- collisions are detected and resolved deterministically
|
||||
- operator commands and runtime backends stay separate from agent tools
|
||||
- the catalog covers the broader current action surface, not only send and reply
|
||||
- slot-backed providers such as context engines are selected explicitly
|
||||
- setup and install metadata stay in host-managed catalogs instead of leaking into runtime catalogs
|
||||
|
||||
## Migration Framing
|
||||
|
||||
This spec replaces existing partial catalog and arbitration behavior already present on `main`.
|
||||
|
||||
It is not a standalone greenfield system.
|
||||
|
||||
Current behavior already exists in at least these places:
|
||||
|
||||
- agent-visible plugin tool grouping in `src/gateway/server-methods/tools-catalog.ts:71`
|
||||
- provider auth and setup selection in `src/commands/auth-choice.apply.plugin-provider.ts:106`
|
||||
- slot selection in `src/plugins/slots.ts:39`
|
||||
- channel picker and onboarding metadata in `src/channels/plugins/catalog.ts:26`
|
||||
|
||||
Implementation rule:
|
||||
|
||||
- Phase 5 and Phase 6 are only complete when those legacy paths have been absorbed into the canonical or host-owned catalog model rather than left as a second source of truth
|
||||
|
||||
## Catalog Types
|
||||
|
||||
The system should maintain separate catalogs for:
|
||||
|
||||
- agent-visible capabilities
|
||||
- operator-visible capabilities
|
||||
- runtime-internal providers
|
||||
|
||||
These catalogs may draw from the same contributions but have different visibility and arbitration rules.
|
||||
|
||||
Ownership split:
|
||||
|
||||
- the kernel publishes runtime-derived internal and agent-visible catalogs
|
||||
- the extension host publishes operator-visible catalogs, including host-only surfaces and any runtime-derived entries the operator surface needs
|
||||
|
||||
## Host-Managed Setup And Install Catalogs
|
||||
|
||||
Current `main` also has host-managed metadata that is not a kernel capability catalog:
|
||||
|
||||
- install metadata from `src/plugins/install.ts:48`
|
||||
- channel picker and onboarding metadata from `src/channels/plugins/catalog.ts:26`
|
||||
- lightweight shared channel behavior from `src/channels/dock.ts:228`
|
||||
|
||||
The extension host should keep publishing these static catalogs for setup and operator UX.
|
||||
|
||||
They should not be folded into the agent capability catalog.
|
||||
|
||||
This host-managed layer should also publish:
|
||||
|
||||
- local operator CLI commands from `surface.cli`
|
||||
- setup and onboarding flows from `surface.setup`
|
||||
- static channel picker metadata and lightweight dock-derived operator hints without activating heavy runtimes
|
||||
|
||||
Sequencing rule:
|
||||
|
||||
- these host-managed static catalogs should migrate before broad runtime catalog publication because they depend on static metadata, not heavy activation
|
||||
|
||||
## Canonical Capability Model
|
||||
|
||||
Each catalog entry should contain:
|
||||
|
||||
- `capabilityId`
|
||||
- `kind`
|
||||
- `canonicalAction`
|
||||
- `displayName`
|
||||
- `description`
|
||||
- `providerKey`
|
||||
- `scope`
|
||||
- `availability`
|
||||
- `requiresSelection`
|
||||
- `inputSchema`
|
||||
- `outputSchema`
|
||||
- `policy`
|
||||
- `telemetryTags`
|
||||
|
||||
### `capabilityId`
|
||||
|
||||
Stable runtime id for the contribution-backed capability.
|
||||
|
||||
### `canonicalAction`
|
||||
|
||||
A stable action family such as:
|
||||
|
||||
- `message.send`
|
||||
- `message.reply`
|
||||
- `directory.lookup`
|
||||
- `provider.authenticate`
|
||||
- `provider.configure`
|
||||
- `memory.search`
|
||||
- `memory.store`
|
||||
- `message.broadcast`
|
||||
- `message.poll`
|
||||
- `message.react`
|
||||
- `message.edit`
|
||||
- `message.delete`
|
||||
- `message.pin`
|
||||
- `message.thread.manage`
|
||||
- `voice.call.start`
|
||||
- `diff.render`
|
||||
|
||||
The agent planner reasons over canonical actions first.
|
||||
|
||||
Governance decision:
|
||||
|
||||
- canonical action ids are open, namespaced strings
|
||||
- core action families should still live in one source-of-truth registry in code
|
||||
- if a new capability fits an existing family, reuse it
|
||||
- if semantics are new, add a reviewed canonical action id to that registry
|
||||
- contributions may not define new arbitration modes or planner semantics outside the core catalog and arbitration schema
|
||||
|
||||
### `providerKey`
|
||||
|
||||
Identifies the concrete provider instance behind the action.
|
||||
|
||||
Examples:
|
||||
|
||||
- `messaging:slack:work`
|
||||
- `messaging:telegram:personal`
|
||||
- `memory:lancedb:default`
|
||||
- `runtime-backend:acp:acpx`
|
||||
|
||||
## Visibility Rules
|
||||
|
||||
### Agent-visible
|
||||
|
||||
Used for agent planning and tool calling.
|
||||
|
||||
Includes:
|
||||
|
||||
- agent tools
|
||||
- channel messaging actions such as send, reply, broadcast, poll, react, edit, delete, pin, and thread actions when available in context
|
||||
- memory actions when policy allows them
|
||||
- voice or telephony actions
|
||||
- selected interaction or workflow actions
|
||||
|
||||
Important interaction rule:
|
||||
|
||||
- interaction-driven actions must be filtered by the current binding and route context
|
||||
- a bound conversation should only surface interaction actions that are valid for the owning extension and current adapter capabilities
|
||||
|
||||
### Operator-visible
|
||||
|
||||
Used for admin, control, setup, CLI, and diagnostic surfaces.
|
||||
|
||||
Includes:
|
||||
|
||||
- control commands
|
||||
- setup flows
|
||||
- provider integration and auth flows
|
||||
- status surfaces
|
||||
- CLI commands
|
||||
|
||||
Important distinction:
|
||||
|
||||
- `capability.control-command` is for chat or native commands that bypass the model
|
||||
- `surface.cli` and `surface.setup` are host-managed local operator surfaces and are not kernel runtime capabilities
|
||||
|
||||
Operator-visible control-command surfaces should preserve current command metadata such as:
|
||||
|
||||
- whether the command accepts arguments
|
||||
- provider-specific native command names when a provider supports native slash or menu registration
|
||||
|
||||
### Runtime-internal
|
||||
|
||||
Not shown to agents or operators as catalog actions.
|
||||
|
||||
Includes:
|
||||
|
||||
- runtime backends
|
||||
- context engines
|
||||
- pure event observers
|
||||
- route augmenters
|
||||
|
||||
## Conflict Classes
|
||||
|
||||
The host must resolve different conflict types differently.
|
||||
|
||||
### 1. Runtime id conflict
|
||||
|
||||
Fatal during validation.
|
||||
|
||||
### 2. Canonical action overlap
|
||||
|
||||
Multiple providers implement the same action family.
|
||||
|
||||
This is expected for messaging, auth, or directory.
|
||||
|
||||
### 3. Planner-visible name collision
|
||||
|
||||
Two agent-visible capabilities want the same public name.
|
||||
|
||||
This must be resolved before catalog publication.
|
||||
|
||||
### 4. Singleton slot conflict
|
||||
|
||||
Two contributions claim a slot that is intentionally exclusive.
|
||||
|
||||
Examples:
|
||||
|
||||
- default memory backend
|
||||
- default context engine
|
||||
|
||||
### 5. Route surface conflict
|
||||
|
||||
Two contributions require the same target or routing ownership semantics.
|
||||
|
||||
### 6. Backend selector conflict
|
||||
|
||||
Two runtime backends claim the same selector with incompatible exclusivity.
|
||||
|
||||
## Arbitration Modes
|
||||
|
||||
### `exclusive`
|
||||
|
||||
Exactly one active provider may exist for the slot.
|
||||
|
||||
Examples:
|
||||
|
||||
- one default context engine
|
||||
- one default memory store, unless the operator opts into parallel memory providers
|
||||
|
||||
### `ranked`
|
||||
|
||||
Many providers may exist, but one default is chosen by rank.
|
||||
|
||||
Examples:
|
||||
|
||||
- multiple auth methods for one provider
|
||||
- multiple backends for the same subsystem
|
||||
|
||||
### `parallel`
|
||||
|
||||
Many providers may remain simultaneously available.
|
||||
|
||||
Examples:
|
||||
|
||||
- Slack, Discord, and Telegram messaging providers for the same agent
|
||||
- multiple directory sources
|
||||
|
||||
### `composed`
|
||||
|
||||
Many providers contribute to a single pipeline.
|
||||
|
||||
Examples:
|
||||
|
||||
- context augmentation
|
||||
- prompt guidance
|
||||
- telemetry enrichment
|
||||
|
||||
## Agent Catalog Compilation
|
||||
|
||||
The kernel compiles the agent-visible catalog from:
|
||||
|
||||
- active contributions
|
||||
- current workspace
|
||||
- current agent
|
||||
- active session bindings
|
||||
- route and account context
|
||||
- current adapter action support
|
||||
- policy restrictions
|
||||
- contribution visibility rules
|
||||
|
||||
Catalog compilation is context-sensitive.
|
||||
|
||||
The same agent may see different capability sets in:
|
||||
|
||||
- Slack thread context
|
||||
- Telegram DM context
|
||||
- voice call context
|
||||
- local CLI session
|
||||
|
||||
First-cut migration targets:
|
||||
|
||||
- plugin tools currently exposed by plugin grouping
|
||||
- messaging actions for the first channel pilot
|
||||
- route-affecting behaviors that influence whether an action is available at all
|
||||
|
||||
## Capability Selection Rules
|
||||
|
||||
When the agent or runtime needs one provider for a canonical action, selection should use this order:
|
||||
|
||||
1. explicit target or provider selector
|
||||
2. explicit session binding
|
||||
3. current conversation or thread route binding
|
||||
4. current adapter or account capability support
|
||||
5. policy-forced default
|
||||
6. ranked default provider
|
||||
7. deterministic fallback by extension id and contribution id
|
||||
|
||||
This is especially important for `message.send` and `message.reply`.
|
||||
|
||||
It also applies to interaction and conversation-control actions, which should prefer:
|
||||
|
||||
- current binding owner
|
||||
- current adapter support
|
||||
- explicit target selection only when ownership or adapter support is ambiguous
|
||||
|
||||
## Messaging Example
|
||||
|
||||
One agent may have:
|
||||
|
||||
- Discord adapter on work account
|
||||
- Slack adapter on work account
|
||||
- Telegram adapter on personal account
|
||||
|
||||
The agent should not see three unrelated tools named “send message”.
|
||||
|
||||
Instead it should see canonical action families, with provider resolution handled by:
|
||||
|
||||
- current conversation route
|
||||
- current session binding
|
||||
- explicit target selector when needed
|
||||
|
||||
Examples:
|
||||
|
||||
- `message.send`
|
||||
- `message.reply`
|
||||
- `message.broadcast`
|
||||
- `message.poll`
|
||||
- `message.react`
|
||||
|
||||
If disambiguation is required, the planner or runtime can use structured selectors such as:
|
||||
|
||||
- target channel kind
|
||||
- account id
|
||||
- conversation ref
|
||||
|
||||
## Agent Naming Rules
|
||||
|
||||
Agent-visible names must be stable and minimally ambiguous.
|
||||
|
||||
Rules:
|
||||
|
||||
- canonical names belong to action families
|
||||
- provider labels are attached only when needed for disambiguation
|
||||
- aliases do not create additional planner-visible tools unless explicitly requested
|
||||
- the host rejects duplicate planner-visible names when the runtime cannot disambiguate them
|
||||
|
||||
This avoids exposing raw extension names unless necessary.
|
||||
|
||||
## Operator Command Separation
|
||||
|
||||
Control commands are not agent tools.
|
||||
|
||||
Examples today:
|
||||
|
||||
- `src/extension-host/contributions/command-runtime.ts:1`
|
||||
- `extensions/phone-control/index.ts:330`
|
||||
|
||||
They belong only in operator catalogs and control surfaces.
|
||||
|
||||
## Provider Integration Selection
|
||||
|
||||
Provider integration flows should be modeled as operator-visible capabilities, not agent-visible tools.
|
||||
|
||||
Selection rules:
|
||||
|
||||
- provider id first
|
||||
- method id second
|
||||
- rank or policy third
|
||||
|
||||
Multiple auth methods for one provider may coexist.
|
||||
|
||||
The selected provider integration may also contribute:
|
||||
|
||||
- discovery order
|
||||
- onboarding metadata
|
||||
- token refresh behavior
|
||||
- model-selected hooks
|
||||
|
||||
It should not silently absorb unrelated subsystem runtimes such as embeddings, transcription, media understanding, or TTS.
|
||||
It should also not silently absorb agent-visible search surfaces, which belong in the agent-tool catalog even when they call remote search services.
|
||||
|
||||
## Memory Arbitration
|
||||
|
||||
Memory needs both backend arbitration and agent action arbitration.
|
||||
|
||||
### Backend arbitration
|
||||
|
||||
Usually `exclusive` or `ranked`.
|
||||
|
||||
### Agent action arbitration
|
||||
|
||||
May still expose:
|
||||
|
||||
- `memory.search`
|
||||
- `memory.store`
|
||||
|
||||
If parallel memory providers are enabled, the planner should either target the default store or use explicit selectors.
|
||||
|
||||
## Context Engine Arbitration
|
||||
|
||||
Context engines are runtime-internal providers selected through an explicit exclusive slot.
|
||||
|
||||
Selection rules:
|
||||
|
||||
- explicit configured engine id wins
|
||||
- otherwise use the slot default
|
||||
- if the selected engine is unavailable, fail with a typed configuration error rather than silently picking an arbitrary fallback
|
||||
|
||||
## Runtime Backend Arbitration
|
||||
|
||||
Runtime backends such as ACP are runtime-internal providers.
|
||||
|
||||
Selection rules:
|
||||
|
||||
- explicit backend id wins
|
||||
- otherwise use healthy highest-ranked backend
|
||||
- if a subsystem declares an exclusive slot, the host enforces it before kernel startup
|
||||
|
||||
This is why `capability.runtime-backend` must be a first-class family.
|
||||
|
||||
The same model should be available for other subsystem runtimes discovered during migration:
|
||||
|
||||
- embeddings
|
||||
- audio transcription
|
||||
- image understanding
|
||||
- video understanding
|
||||
- text-to-speech
|
||||
|
||||
Selection rules for these subsystem runtimes should preserve these required behaviors:
|
||||
|
||||
- capability-based selection
|
||||
- normalized provider ids
|
||||
- explicit built-in fallback policy
|
||||
- typed host-injected request envelopes
|
||||
|
||||
Architecture rule:
|
||||
|
||||
- keep those selection and envelope rules inside host-owned subsystem runtime registries for typed backend families
|
||||
- do not widen provider-integration or legacy plugin-provider APIs into a universal surface for unrelated runtime subsystems
|
||||
- if search is agent-visible, publish it through canonical tool catalogs; reserve `capability.runtime-backend` for search backends that are consumed internally by the host or another subsystem
|
||||
|
||||
## Catalog Publication
|
||||
|
||||
The kernel should publish:
|
||||
|
||||
- a full internal catalog
|
||||
- a filtered agent catalog
|
||||
|
||||
The extension host should publish:
|
||||
|
||||
- a filtered operator catalog
|
||||
|
||||
Publication should occur after:
|
||||
|
||||
- dependency resolution
|
||||
- policy approval
|
||||
- contribution activation
|
||||
- route and account context binding
|
||||
|
||||
Host-managed install and onboarding descriptors may move into host ownership earlier because they come from static metadata, not runtime activation.
|
||||
|
||||
Full catalog publication, consolidation, and legacy-path replacement still belong to the catalog-migration phase.
|
||||
|
||||
Performance requirement:
|
||||
|
||||
- publishing host-managed setup and install catalogs must not require activating heavy adapter runtimes
|
||||
- publishing operator-visible static catalogs must preserve current dock-style cheap-path behavior, including prompt hints and shared formatting helpers where those are consumed without runtime activation
|
||||
|
||||
## Telemetry And Auditing
|
||||
|
||||
Capability selection must emit structured events for:
|
||||
|
||||
- conflict detection
|
||||
- provider selection
|
||||
- fallback selection
|
||||
- planner-visible disambiguation
|
||||
- veto or cancellation caused by route augmenters
|
||||
- slot selection for context engines or other exclusive runtime providers
|
||||
|
||||
## Migration Mapping From Today
|
||||
|
||||
- channel capabilities from `extensions/discord/src/channel.ts:74`, `extensions/slack/src/channel.ts:107`, and `extensions/telegram/src/channel.ts:120` collapse into canonical messaging action families
|
||||
- diffs becomes an agent-visible tool family plus a host-managed route surface from `extensions/diffs/index.ts:27`
|
||||
- provider integration from `extensions/google-gemini-cli-auth/index.ts:24` becomes operator-visible setup and auth capabilities
|
||||
- embedding, media-understanding, and TTS provider overrides should become runtime-internal subsystem registries rather than remaining part of a universal plugin-provider API
|
||||
- extension-backed web search should become an agent-visible tool family unless it is only a runtime-internal backend feeding another host-owned surface
|
||||
- voice-call from `extensions/voice-call/index.ts:230` becomes a mix of agent-visible actions, runtime providers, and operator surfaces
|
||||
- ACP backend registration from `extensions/acpx/src/service.ts:55` becomes runtime-internal backend arbitration
|
||||
- context-engine registration becomes runtime-internal slot arbitration from `src/context-engine/registry.ts:60`
|
||||
- native command registration remains an operator or transport surface concern rather than an agent-visible catalog concern
|
||||
|
||||
## Immediate Implementation Work
|
||||
|
||||
1. Add canonical action ids and provider keys to resolved contributions.
|
||||
2. Implement host-side conflict detection for planner-visible names and singleton slots.
|
||||
3. Implement kernel-side context-aware catalog compilation.
|
||||
4. Add host-managed static catalogs for install and onboarding metadata alongside the runtime catalogs.
|
||||
5. Migrate the existing plugin tool grouping path onto canonical agent catalog entries.
|
||||
6. Migrate the existing provider auth and setup selection path onto host-owned setup catalogs and canonical provider metadata.
|
||||
7. Add provider selection logic for the broader messaging action family before migrating all channels.
|
||||
8. Add runtime-backend and context-engine arbitration using the same rank and slot model where appropriate.
|
||||
9. Add host-owned embedding, media-understanding, and TTS subsystem registries with explicit capability routing and built-in fallback policy.
|
||||
10. Decide whether extension-backed search needs only canonical tool publication or also a host-owned runtime registry for internal search backends, and keep those two cases distinct.
|
||||
11. Ensure lightweight setup catalogs can be built from static descriptors alone.
|
||||
12. Add a reviewed core registry for canonical action families and document how new ids are introduced.
|
||||
13. Record catalog and arbitration parity for `thread-ownership` first and `telegram` second before broader rollout.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,659 @@
|
||||
Temporary internal migration note: remove this document once the extension-host migration is complete.
|
||||
|
||||
# OpenClaw Extension Host Implementation Guide
|
||||
|
||||
Date: 2026-03-15
|
||||
|
||||
## Purpose
|
||||
|
||||
This is the main execution guide for implementing the extension-host and kernel transition.
|
||||
|
||||
Use it as the top-level implementation document.
|
||||
|
||||
## How We Fix It
|
||||
|
||||
Fix this as a staged architectural migration, not a broad refactor.
|
||||
|
||||
1. Lock the boundary first by writing the cutover inventory and adding anti-corruption interfaces so no new plugin-specific behavior leaks into the kernel.
|
||||
2. Introduce source-of-truth extension schema types and the `ResolvedExtension` model while preserving current `openclaw/plugin-sdk/*` loading through minimal compatibility support.
|
||||
3. Move discovery, policy, provenance, static metadata, and registration ownership into the extension host, including hooks, channels, providers, tools, routes, CLI, setup, services, and slot-backed providers.
|
||||
4. Prove the path with pilot migrations: `thread-ownership` first for non-channel hook behavior, then `telegram` for channel compatibility.
|
||||
5. After pilot parity is established, move runtime behavior onto canonical event stages and replace the fragmented tool, provider, and slot-selection paths with one catalog and arbitration model.
|
||||
6. Remove the legacy plugin runtime as the default path only after the host path has parity and the duplicate legacy systems are gone or explicitly downgraded to compatibility-only shims.
|
||||
|
||||
The other docs remain the source of truth for their domains:
|
||||
|
||||
- `openclaw-extension-contribution-schema-spec.md`
|
||||
- `openclaw-extension-host-lifecycle-and-security-spec.md`
|
||||
- `openclaw-kernel-event-pipeline-spec.md`
|
||||
- `openclaw-capability-catalog-and-arbitration-spec.md`
|
||||
- `openclaw-kernel-extension-host-transition-plan.md`
|
||||
|
||||
## TODOs
|
||||
|
||||
- [ ] Confirm the implementation order and owners for each phase.
|
||||
- [x] Create the initial code skeleton for kernel and extension-host boundaries.
|
||||
- [x] Write the initial boundary cutover inventory for every current plugin-owned surface.
|
||||
- [ ] Keep the boundary cutover inventory updated as surfaces move.
|
||||
- [ ] Track PRs, migrations, and follow-up gaps by phase.
|
||||
- [ ] Keep the linked spec TODO sections in sync with implementation progress.
|
||||
- [ ] Define the detailed pilot migration matrix and parity checks before Phase 3 starts.
|
||||
- [ ] Mark this guide complete only when the legacy plugin path is no longer the primary runtime path.
|
||||
|
||||
## Implementation Status
|
||||
|
||||
Current status against this guide:
|
||||
|
||||
- Phase 0 has started but is not complete.
|
||||
- Phase 1 has started but is not complete.
|
||||
- Phase 2 has started in a broad, compatibility-preserving form but is not complete.
|
||||
- Phases 3 through 7 have not started in a meaningful way yet.
|
||||
|
||||
What has been implemented so far:
|
||||
|
||||
- a new `src/extension-host/*` boundary now exists in code
|
||||
- active runtime registry ownership moved into `src/extension-host/static/active-registry.ts`
|
||||
- `src/plugins/runtime.ts` now acts as a compatibility facade over the host-owned active registry
|
||||
- registry activation now routes through `src/extension-host/activation.ts`
|
||||
- initial source-of-truth types landed in `src/extension-host/manifests/schema.ts`, including `ResolvedExtension`, `ResolvedContribution`, and `ContributionPolicy`
|
||||
- static manifest and package metadata are now normalized through host-owned helpers rather than being interpreted only inside plugin-era modules
|
||||
- `src/plugins/manifest-registry.ts` now carries a normalized `resolvedExtension` alongside the legacy flat manifest record
|
||||
- `src/extension-host/manifests/resolved-registry.ts` now exposes a host-owned resolved-extension registry view
|
||||
- an initial Phase 0 inventory now exists in `src/extension-host/cutover-inventory.md`
|
||||
- plugin SDK alias resolution now routes through `src/extension-host/compat/loader-compat.ts`
|
||||
- loader alias-wired module loader creation now routes through `src/extension-host/activation/loader-module-loader.ts`
|
||||
- loader cache key construction and registry cache control now route through `src/extension-host/activation/loader-cache.ts`
|
||||
- loader lazy runtime proxy creation now routes through `src/extension-host/activation/loader-runtime-proxy.ts`
|
||||
- loader provenance helpers now route through `src/extension-host/policy/loader-provenance.ts`
|
||||
- loader duplicate-order and record/error policy now route through `src/extension-host/policy/loader-policy.ts`
|
||||
- loader discovery policy outcomes now route through `src/extension-host/policy/loader-discovery-policy.ts`
|
||||
- loader initial candidate planning and record creation now route through `src/extension-host/activation/loader-records.ts`
|
||||
- loader entry-path opening and module import now route through `src/extension-host/activation/loader-import.ts`
|
||||
- loader module-export resolution, config validation, and memory-slot load decisions now route through `src/extension-host/activation/loader-runtime.ts`
|
||||
- loader post-import planning and `register(...)` execution now route through `src/extension-host/activation/loader-register.ts`
|
||||
- loader per-candidate orchestration now routes through `src/extension-host/activation/loader-flow.ts`
|
||||
- loader top-level load orchestration now routes through `src/extension-host/activation/loader-orchestrator.ts`
|
||||
- loader host process state now routes through `src/extension-host/activation/loader-host-state.ts`
|
||||
- loader preflight and cache-hit setup now routes through `src/extension-host/activation/loader-preflight.ts`
|
||||
- loader post-preflight pipeline composition now routes through `src/extension-host/activation/loader-pipeline.ts`
|
||||
- loader execution setup composition now routes through `src/extension-host/activation/loader-execution.ts`
|
||||
- loader discovery and manifest bootstrap now routes through `src/extension-host/activation/loader-bootstrap.ts`
|
||||
- loader mutable activation state now routes through `src/extension-host/activation/loader-session.ts`
|
||||
- loader session run and finalization composition now routes through `src/extension-host/activation/loader-run.ts`
|
||||
- loader activation policy outcomes now route through `src/extension-host/policy/loader-activation-policy.ts`
|
||||
- loader record-state transitions now route through `src/extension-host/activation/loader-state.ts`, which now enforces an explicit loader lifecycle state machine while preserving compatibility `PluginRecord.status` values
|
||||
- loader finalization policy results now route through `src/extension-host/policy/loader-finalization-policy.ts`
|
||||
- loader final cache, readiness promotion, and activation finalization now routes through `src/extension-host/activation/loader-finalize.ts`
|
||||
- runtime registration normalization has started in `src/extension-host/contributions/runtime-registrations.ts` for channel, provider, HTTP-route, gateway-method, tool, CLI, service, command, context-engine, and hook registrations
|
||||
- low-risk runtime compatibility writes for channel, provider, gateway-method, HTTP-route, tool, CLI, service, command, context-engine, and hook registrations now route through `src/extension-host/contributions/registry-writes.ts`
|
||||
- legacy internal-hook bridging and typed prompt-injection compatibility policy now route through `src/extension-host/compat/hook-compat.ts`
|
||||
- compatibility `OpenClawPluginApi` composition and logger shaping now route through `src/extension-host/compat/plugin-api.ts`
|
||||
- compatibility plugin-registry facade ownership now routes through `src/extension-host/compat/plugin-registry.ts`
|
||||
- compatibility plugin-registry policy now routes through `src/extension-host/compat/plugin-registry-compat.ts`
|
||||
- compatibility plugin-registry registration actions now route through `src/extension-host/compat/plugin-registry-registrations.ts`
|
||||
- host-owned runtime registry accessors now route through `src/extension-host/contributions/runtime-registry.ts`, and the channel, provider, tool, command, HTTP-route, gateway-method, CLI, and service slices now keep host-owned storage there with mirrored legacy compatibility views
|
||||
- service startup, stop ordering, service-context creation, and failure logging now route through `src/extension-host/contributions/service-lifecycle.ts`
|
||||
- CLI duplicate detection, registrar invocation, and async failure logging now route through `src/extension-host/contributions/cli-lifecycle.ts`
|
||||
- gateway method-id aggregation, plugin diagnostic shaping, and extra-handler composition now route through `src/extension-host/contributions/gateway-methods.ts`
|
||||
- plugin tool resolution, conflict handling, optional-tool gating, and plugin-tool metadata tracking now route through `src/extension-host/contributions/tool-runtime.ts`
|
||||
- plugin provider projection from registry entries into runtime provider objects now routes through `src/extension-host/contributions/provider-runtime.ts`
|
||||
- plugin provider discovery filtering, order grouping, and result normalization now route through `src/extension-host/contributions/provider-discovery.ts`
|
||||
- provider matching, auth-method selection, config-patch merging, and default-model application now route through `src/extension-host/contributions/provider-auth.ts`
|
||||
- provider onboarding option building, model-picker entry building, and provider-method choice resolution now route through `src/extension-host/contributions/provider-wizard.ts`
|
||||
- loaded-provider auth application, plugin-enable gating, auth-method execution, and post-auth default-model handling now route through `src/extension-host/contributions/provider-auth-flow.ts`
|
||||
- provider post-selection hook lookup and invocation now route through `src/extension-host/contributions/provider-model-selection.ts`
|
||||
- several static and lookup consumers now read through the host boundary or resolved-extension model:
|
||||
- channel registry and dock lookups
|
||||
- message-channel normalization
|
||||
- plugin HTTP route registry default lookup
|
||||
- discovery and install package metadata parsing
|
||||
- channel catalog package metadata parsing
|
||||
- plugin skill discovery
|
||||
- plugin auto-enable
|
||||
- config doc baseline generation
|
||||
- config validation indexing
|
||||
- several runtime consumers now also read through host-owned runtime-registry accessors instead of touching raw plugin-registry arrays or handler maps directly:
|
||||
- channel lookup
|
||||
- provider projection
|
||||
- tool resolution
|
||||
- service lifecycle startup
|
||||
- CLI registration
|
||||
- command runtime entry detection
|
||||
- gateway method aggregation
|
||||
- gateway plugin HTTP route matching
|
||||
- plugin command execution and command-status listing now read through `src/extension-host/contributions/command-runtime.ts` instead of the legacy `src/plugins/commands.ts` implementation
|
||||
- the channel, provider, tool, command, HTTP-route, gateway-method, CLI, and service slices now also keep host-owned runtime-registry storage with mirrored legacy compatibility arrays and handler maps
|
||||
- `src/cli/plugin-registry.ts` now treats any pre-seeded runtime entry surface as already loaded, not just plugins, channels, or tools
|
||||
|
||||
How it has been done:
|
||||
|
||||
- by extracting narrow host-owned modules first and making existing plugin modules delegate to them
|
||||
- by preserving current behavior and import surfaces wherever possible instead of attempting a broad rewrite
|
||||
- by introducing normalized static records before touching heavy runtime activation paths
|
||||
- by converting one static consumer at a time so each call site can move without forcing a loader rewrite
|
||||
- by extracting low-risk runtime registration helpers next and letting `src/plugins/registry.ts` delegate to them as a compatibility facade
|
||||
- by starting actual low-risk runtime write ownership next for channel, provider, gateway-method, HTTP-route, tool, CLI, service, command, context-engine, and hook registrations while keeping lifecycle semantics in legacy owners where that behavior still lives
|
||||
- by moving plugin command duplicate enforcement, registration, matching, execution, listing, native command-spec projection, and loader reload clearing behind `src/extension-host/contributions/command-runtime.ts` while keeping `src/plugins/commands.ts` as the compatibility facade
|
||||
- by starting loader and lifecycle migration with compatibility helpers for activation and SDK alias resolution before changing discovery or policy behavior
|
||||
- by moving cache-key construction, cache reads, cache writes, and cache clearing behind host-owned helpers before changing activation-state ownership
|
||||
- by extracting lazy runtime proxy creation and alias-wired Jiti module-loader creation into host-owned helpers before broader bootstrap or lifecycle ownership changes
|
||||
- by extracting discovery, manifest loading, manifest diagnostics, discovery-policy logging, provenance building, and candidate ordering into a host-owned loader-bootstrap helper before broader lifecycle ownership changes
|
||||
- by extracting candidate iteration, manifest lookup, per-candidate session processing, and finalization handoff into a host-owned loader-run helper before broader lifecycle ownership changes
|
||||
- by moving loader-owned policy helpers next, while keeping module loading and enablement flow behavior unchanged
|
||||
- by moving initial candidate planning and record construction behind host-owned helpers before changing import and registration flow
|
||||
- by moving entry-path opening and module import behind host-owned helpers before changing cache wiring or lifecycle orchestration
|
||||
- by moving loader runtime decisions behind host-owned helpers while preserving lazy loading, config validation behavior, and memory-slot policy behavior
|
||||
- by moving post-import planning and `register(...)` execution behind host-owned helpers before changing entry-path and import flow
|
||||
- by composing those seams into one host-owned per-candidate orchestrator before changing cache and lifecycle finalization behavior
|
||||
- by moving loader record-state transitions into host-owned helpers before enforcing them as a loader lifecycle state machine
|
||||
- by moving cache writes, provenance warnings, final memory-slot warnings, and activation into a host-owned loader finalizer before introducing an explicit lifecycle state machine
|
||||
- by adding explicit compatibility `lifecycleState` mapping on loader-owned plugin records before enforcing the loader lifecycle state machine
|
||||
- by turning that compatibility `lifecycleState` field into an enforced loader lifecycle state machine with readiness promotion during finalization
|
||||
- by moving the remaining top-level loader orchestration into a host-owned module so `src/plugins/loader.ts` becomes a compatibility facade instead of the real owner
|
||||
- by extracting shared discovery warning-cache state and loader reset behavior into a host-owned loader-host-state helper before shrinking the remaining orchestrator surface
|
||||
- by extracting test-default application, config normalization, cache-key construction, cache-hit activation, and command-clear setup into a host-owned loader-preflight helper before shrinking the remaining orchestrator surface
|
||||
- by extracting post-preflight execution setup and session-run composition into a host-owned loader-pipeline helper before shrinking the remaining orchestrator surface
|
||||
- by extracting runtime creation, registry creation, bootstrap setup, module-loader creation, and session creation into a host-owned loader-execution helper before shrinking the remaining orchestrator surface
|
||||
- by moving mutable activation state such as seen-id tracking, memory-slot selection, and finalization inputs into a host-owned loader session instead of leaving them in top-level loader variables
|
||||
- by extracting shared provenance path matching and install-rule evaluation into `src/extension-host/policy/loader-provenance.ts` so activation and finalization policy seams reuse one host-owned implementation
|
||||
- by turning open-allowlist discovery warnings into explicit host-owned discovery-policy results before the orchestrator logs them
|
||||
- by moving duplicate precedence, config enablement, and early memory-slot gating into explicit host-owned activation-policy outcomes instead of leaving them inline in the loader flow
|
||||
- by turning provenance-based untracked-extension warnings and final memory-slot warnings into explicit host-owned finalization-policy results before the finalizer applies them
|
||||
- by extracting legacy internal-hook bridging and typed prompt-injection compatibility policy into a host-owned hook-compat helper while leaving actual hook execution ownership unchanged
|
||||
- by extracting compatibility `OpenClawPluginApi` composition and logger shaping into a host-owned plugin-api helper while keeping the concrete registration callbacks in the legacy registry surface
|
||||
- by extracting the remaining compatibility plugin-registry facade into a host-owned helper so `src/plugins/registry.ts` becomes a thin wrapper instead of the real owner
|
||||
- by extracting provider normalization, command duplicate enforcement, and registry-local diagnostic shaping into a host-owned registry-compat helper while leaving the underlying provider-validation and plugin-command subsystems unchanged
|
||||
- by extracting low-risk registry registration actions into a host-owned registry-registrations helper so the compatibility facade composes host-owned actions instead of implementing them inline
|
||||
- by extracting service startup, stop ordering, service-context creation, and failure logging into a host-owned service-lifecycle helper while `src/plugins/services.ts` remains the compatibility entry point
|
||||
- by extracting CLI duplicate detection, registrar invocation, and async failure logging into a host-owned CLI-lifecycle helper while `src/plugins/cli.ts` remains the compatibility entry point
|
||||
- by extracting gateway method-id aggregation, plugin diagnostic shaping, and extra-handler composition into a host-owned gateway-methods helper while request dispatch semantics remain in the gateway server code
|
||||
- by extracting plugin tool resolution, conflict handling, optional-tool gating, and plugin-tool metadata tracking into a host-owned tool-runtime helper while `src/plugins/tools.ts` remains the loader and config-normalization facade
|
||||
- by extracting provider projection from registry entries into runtime provider objects into a host-owned provider-runtime helper while `src/plugins/providers.ts` remains the loader and config-normalization facade
|
||||
- by extracting provider discovery filtering, order grouping, and result normalization into a host-owned provider-discovery helper while `src/plugins/provider-discovery.ts` remains the compatibility facade around the legacy provider loader path
|
||||
- by extracting provider matching, auth-method selection, config-patch merging, and default-model application into a host-owned provider-auth helper while `src/commands/provider-auth-helpers.ts` remains the command-facing compatibility facade
|
||||
- by extracting provider onboarding option building, model-picker entry building, and provider-method choice resolution into a host-owned provider-wizard helper while `src/plugins/provider-wizard.ts` remains the compatibility facade around loader-backed provider access and post-selection hooks
|
||||
- by extracting loaded-provider auth application, plugin-enable gating, auth-method execution, and post-auth default-model handling into a host-owned provider-auth-flow helper while `src/commands/auth-choice.apply.plugin-provider.ts` remains the compatibility entry point
|
||||
- by extracting provider post-selection hook lookup and invocation into a host-owned provider-model-selection helper while `src/plugins/provider-wizard.ts` remains the compatibility facade and existing command consumers continue migrating onto the host-owned surface
|
||||
- by extracting provider-id normalization into `src/agents/provider-id.ts` so provider-only host seams do not inherit the heavier agent and browser dependency graph from `src/agents/model-selection.ts`
|
||||
- by extracting model-ref parsing into `src/agents/model-ref.ts` and Google model-id normalization into `src/agents/google-model-id.ts` so provider auth and setup seams can be tested without pulling the heavier provider-loader and browser dependency graph
|
||||
- by introducing host-owned runtime-registry accessors for low-risk runtime consumers first, then moving channel, provider, tool, command, HTTP-route, gateway-method, CLI, and service storage into that host-owned state while keeping mirrored legacy compatibility arrays and handler maps
|
||||
- by tightening the CLI pre-load fast path to treat any host-known runtime entry surface as already loaded rather than only plugins, channels, or tools
|
||||
- by moving central readers first, so later lifecycle and compatibility work can land on one boundary instead of many ad hoc call sites
|
||||
- by adding focused tests for each extracted seam before widening the boundary further
|
||||
|
||||
Committed implementation slices so far:
|
||||
|
||||
- `6abf6750ee` `Plugins: add extension host registry boundary`
|
||||
- `1aab89e820` `Plugins: extract loader host seams`
|
||||
- `7bc3135082` `Plugins: extract loader candidate planning`
|
||||
- `3a122c95fa` `Plugins: extract loader register flow`
|
||||
- `fc81454038` `Plugins: extract loader import flow`
|
||||
- `e1b207f4cf` `Plugins: extract loader candidate orchestration`
|
||||
- `0c44d8049b` `Plugins: extract loader finalization`
|
||||
- `33ef55a9ee` `Plugins: add loader lifecycle state mapping`
|
||||
- `6590e19095` `Plugins: extract loader cache control`
|
||||
- `c8d82a8f19` `Plugins: extract loader orchestration`
|
||||
- `d32f65eb5e` `Plugins: add loader lifecycle state machine`
|
||||
- `da9aad0c0f` `Plugins: add loader activation session`
|
||||
- `fc51ce2867` `Plugins: add loader activation policy`
|
||||
- `fd7488e10a` `Plugins: add loader finalization policy`
|
||||
- `97e2af7f97` `Plugins: add loader discovery policy`
|
||||
- `83b18eab72` `Plugins: share loader provenance helpers`
|
||||
- `52495d23d5` `Plugins: extract loader runtime factories`
|
||||
- `6e187ffb62` `Plugins: extract loader bootstrap`
|
||||
- `234a540720` `Plugins: extract loader session runner`
|
||||
- `a98443c39d` `Plugins: extract loader execution setup`
|
||||
- `c9323aa016` `Plugins: extract loader preflight`
|
||||
- `0df51ae6b4` `Plugins: extract loader pipeline`
|
||||
- `e557b39cb2` `Plugins: extract loader host state`
|
||||
- `07c3ae9c87` `Plugins: extract low-risk registry writes`
|
||||
- `bc71592270` `Plugins: extend registry write helpers`
|
||||
- `27fc645484` `Plugins: extend registry writes for hooks`
|
||||
- `b407d7f476` `Plugins: extract hook compatibility`
|
||||
- `a1e1dcc01a` `Plugins: extract plugin api facade`
|
||||
- `0e190d64d4` `Plugins: extract registry compatibility facade`
|
||||
- `944d787df1` `Plugins: extract registry compatibility policy`
|
||||
- `4ca9cd7e5e` `Plugins: extract registry registration actions`
|
||||
- `6b24e65719` `Plugins: extract service lifecycle`
|
||||
- `b5757a6625` `Plugins: extract CLI lifecycle`
|
||||
- `e0e3229bcb` `Gateway: extract extension host method surface`
|
||||
- `af7ac14eed` `Plugins: extract tool runtime`
|
||||
- `19087405d2` `Plugins: extract provider runtime`
|
||||
- `1303419471` `Plugins: extract provider discovery`
|
||||
- `afb6e4b185` `Plugins: extract provider auth and wizard flows`
|
||||
- `cc3d59d59e` `Plugins: extract provider auth application flow`
|
||||
- `e6cd834f8e` `Plugins: extract provider model selection hook`
|
||||
- `11cbe08ec6` `Plugins: add host-owned route and gateway storage`
|
||||
- `89e6b38152` `Docs: refresh runtime registry storage status`
|
||||
- `ad0c235d16` `Plugins: add host-owned CLI and service storage`
|
||||
- `d34a5aa870` `Docs: refresh runtime registry storage progress`
|
||||
- `2be54e9861` `Plugins: add host-owned tool and provider storage`
|
||||
- `235021766c` `Docs: refresh tool and provider storage status`
|
||||
- `e109d5ef1b` `Plugins: add host-owned channel storage`
|
||||
- `24fca48453` `Docs: refresh channel storage status`
|
||||
- `961015f08c` `Channels: finish message-channel host lookup`
|
||||
- `4c7f62649b` `Plugins: extract command runtime`
|
||||
- `89414ed857` `Docs: track extension host migration internally`
|
||||
- `d8af1eceaf` `Docs: refresh extension host migration status`
|
||||
|
||||
What is still missing for these phases:
|
||||
|
||||
- keeping the cutover inventory current as more surfaces move
|
||||
- broader lifecycle ownership beyond the loader state machine, service-lifecycle boundary, CLI-lifecycle boundary, session-owned activation state, and explicit discovery-policy, activation-policy, and finalization-policy outcomes, remaining policy gate ownership, and broad host-owned registries described for Phase 2
|
||||
- minimal SDK compatibility work beyond preserving current behavior indirectly through existing loading
|
||||
- host-owned conversation binding, interaction routing, ingress claim, and generic interactive control surfaces
|
||||
- host-owned subsystem runtime registries for embeddings, media understanding, and TTS
|
||||
- explicit support for extension-backed search, with a generic split between agent-visible tool publication and optional runtime-internal search backends
|
||||
- any pilot migration, event pipeline, canonical catalog, or arbitration implementation
|
||||
|
||||
Recent plan refinements:
|
||||
|
||||
- the plan now explicitly treats conversation binding ownership, approval persistence, restore-on-restart behavior, and detached-binding cleanup as first-class migration surfaces
|
||||
- it now explicitly treats interactive callback routing, namespace ownership, dedupe, and fallback behavior as first-class migration surfaces
|
||||
- it now explicitly treats inbound claim as a canonical ingress-stage concern rather than a permanent plugin-era hook shape
|
||||
- it now explicitly treats Telegram and Discord as the first validated rollout targets for interactive control surfaces while keeping the underlying contracts generic, host-owned, and kernel-agnostic
|
||||
- it now explicitly treats embeddings, media understanding, and TTS as host-owned subsystem runtimes with capability routing, typed request envelopes, provider-id normalization, and fallback policy
|
||||
- it now explicitly rejects widening the legacy `registerProvider(...)` or `ProviderPlugin` surface into a universal runtime API while retaining capability routing, typed request envelopes, provider-id normalization, and fallback behavior where those are part of the target model
|
||||
- it now explicitly treats extension-backed search as either a canonical tool contribution or a host-owned runtime backend depending on whether the search surface is agent-visible
|
||||
|
||||
## Implementation Order
|
||||
|
||||
Implement phases in this order:
|
||||
|
||||
1. Phase 0: boundary inventory and anti-corruption layer
|
||||
2. Phase 1: contribution schema, package metadata, and minimal SDK compatibility
|
||||
3. Phase 2: extension host lifecycle and registries
|
||||
4. Phase 3: broader legacy compatibility bridges
|
||||
5. Phase 4: canonical event pipeline
|
||||
6. Phase 5: capability catalog migration
|
||||
7. Phase 6: arbitration migration
|
||||
8. Phase 7: broader migration and legacy removal
|
||||
|
||||
This order matters because each layer depends on the previous one:
|
||||
|
||||
- catalogs depend on normalized contributions
|
||||
- normalized contributions depend on host discovery and validation
|
||||
- existing extensions must keep loading while the schema and SDK boundary changes
|
||||
- migrated hooks depend on the canonical event pipeline
|
||||
- install, onboarding, and status flows depend on static metadata before runtime activation
|
||||
- catalogs and arbitration already exist in partial forms, so their phases are migrations, not greenfield work
|
||||
- useful ideas from implementation review should be harvested as parity requirements and host-owned capabilities, not by broadening legacy `src/plugins/*` or `src/plugin-sdk/*` surfaces as the target architecture
|
||||
- safe removal of legacy paths depends on compatibility coverage and parity checks
|
||||
|
||||
## Implementation Guardrails
|
||||
|
||||
Do not implement every abstraction in the docs in the first cut.
|
||||
|
||||
Treat some parts of the design as ceilings rather than immediate scope:
|
||||
|
||||
- event taxonomy should start with three execution modes only:
|
||||
- parallel observers
|
||||
- sequential merge or decision handlers
|
||||
- sync transcript hot paths
|
||||
- permission modes should implement `advisory` and `host-enforced` first
|
||||
- `sandbox-enforced` should remain a future contract until real isolation exists
|
||||
- catalog publication should start small:
|
||||
- kernel internal catalog
|
||||
- kernel agent catalog
|
||||
- host operator and static registries
|
||||
- adapter metadata should stay minimal and parity-driven
|
||||
- setup flow typing should start with a small result set:
|
||||
- config patch
|
||||
- credential result
|
||||
- status note
|
||||
- follow-up action
|
||||
- canonical action governance should start as one source file plus tests, not a larger process framework
|
||||
- arbitration should start with:
|
||||
- exclusive slot
|
||||
- ranked provider
|
||||
- parallel provider
|
||||
|
||||
The first implementation goal is parity for pilot migrations, not maximum generality.
|
||||
|
||||
If a design choice is not required to migrate one channel extension and one non-channel extension safely, defer it.
|
||||
|
||||
## Current Runtime Surfaces That Must Be Accounted For
|
||||
|
||||
The current plugin system already owns more than runtime activation.
|
||||
|
||||
Before implementation starts, write and maintain a cutover inventory for these surfaces:
|
||||
|
||||
- manifest loading and static metadata
|
||||
- package-level install and onboarding metadata
|
||||
- discovery, provenance, and origin precedence
|
||||
- config schema and UI hint loading
|
||||
- typed hooks and legacy hook bridges
|
||||
- channels and channel lookup
|
||||
- providers and provider auth/setup flows
|
||||
- tools and agent-visible tool catalogs
|
||||
- HTTP routes and gateway methods
|
||||
- CLI registrars and plugin commands
|
||||
- services and context-engine registrations
|
||||
- slot selection and other existing arbitration paths
|
||||
- status, reload, install, update, and diagnostics surfaces
|
||||
|
||||
Do not treat Phase 5 and Phase 6 as new systems built in isolation.
|
||||
|
||||
They must absorb and replace the existing partial catalog and arbitration behaviors rather than creating a second source of truth.
|
||||
|
||||
## Phase Guide
|
||||
|
||||
### Phase 0: Lock the boundary
|
||||
|
||||
Goal:
|
||||
|
||||
- define the kernel versus extension-host boundary in code and imports
|
||||
- inventory every current plugin-owned surface that crosses that boundary
|
||||
|
||||
Deliverables:
|
||||
|
||||
- boundary cutover inventory
|
||||
- anti-corruption interfaces for host-owned registration surfaces
|
||||
- initial feature flags for host-path versus legacy-path execution
|
||||
- directory and import boundaries for kernel and extension-host code
|
||||
|
||||
Primary docs:
|
||||
|
||||
- `openclaw-kernel-extension-host-transition-plan.md`
|
||||
- `openclaw-extension-contribution-schema-spec.md`
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- kernel code does not take new dependencies on legacy plugin shapes
|
||||
- extension-host directory structure exists
|
||||
- compatibility-only surfaces are identified
|
||||
- each current plugin-owned surface is tagged as kernel-owned, host-owned, or compatibility-only
|
||||
- no new direct writes to global registries are introduced without going through the new boundary
|
||||
|
||||
Current implementation status:
|
||||
|
||||
- partially implemented
|
||||
- the code boundary exists in `src/extension-host/*`
|
||||
- central active-registry ownership now routes through the host boundary
|
||||
- several central runtime readers now consume the host-owned boundary instead of reading directly from `src/plugins/runtime.ts`
|
||||
- the initial cutover inventory now exists in `src/extension-host/cutover-inventory.md` and is being updated as surfaces move, but the phase is still incomplete because loader orchestration, lifecycle ownership, and later compatibility phases have not moved yet
|
||||
|
||||
### Phase 1: Define the schema
|
||||
|
||||
Goal:
|
||||
|
||||
- implement the source-of-truth manifest and contribution types
|
||||
- preserve existing extension loading while the schema and SDK boundary changes
|
||||
|
||||
Primary doc:
|
||||
|
||||
- `openclaw-extension-contribution-schema-spec.md`
|
||||
|
||||
Deliverables:
|
||||
|
||||
- manifest parser
|
||||
- package metadata parser
|
||||
- contribution validators
|
||||
- `ResolvedExtension`
|
||||
- `ResolvedContribution`
|
||||
- typed `ContributionPolicy`
|
||||
- static metadata parser
|
||||
- new versioned SDK contract surface
|
||||
- minimal SDK compatibility loading surface
|
||||
- normalized install and onboarding metadata model
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- extensions can be normalized into static and runtime sections without activating heavy runtime code
|
||||
- existing extension SDK imports still resolve through the compatibility loading path
|
||||
|
||||
Current implementation status:
|
||||
|
||||
- partially implemented
|
||||
- `ResolvedExtension`, `ResolvedContribution`, and `ContributionPolicy` landed as initial code types
|
||||
- legacy manifest and package metadata now converge into a normalized `resolvedExtension` record carried by the manifest registry
|
||||
- discovery, install, and catalog metadata parsing now go through host-owned schema helpers
|
||||
- partial explicit compatibility now exists through host-owned loader-compat and loader-runtime helpers, but full manifest or contribution validators and a versioned SDK compatibility layer are not implemented yet
|
||||
|
||||
### Phase 2: Build the extension host
|
||||
|
||||
Goal:
|
||||
|
||||
- implement discovery, validation, policy, registries, and lifecycle ownership
|
||||
|
||||
Primary doc:
|
||||
|
||||
- `openclaw-extension-host-lifecycle-and-security-spec.md`
|
||||
|
||||
Deliverables:
|
||||
|
||||
- discovery pipeline
|
||||
- activation state machine
|
||||
- policy evaluator
|
||||
- host-owned registries
|
||||
- host-owned adapters for hooks, channels, providers, tools, HTTP routes, gateway methods, CLI, services, commands, and context engines
|
||||
- per-extension state ownership
|
||||
- provenance and origin handling
|
||||
- config redaction-aware schema loading
|
||||
- reload and route ownership handling
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- the host can load bundled and external extensions into normalized registries
|
||||
- the host can populate normalized registries without direct kernel writes except through explicit compatibility adapters
|
||||
|
||||
Current implementation status:
|
||||
|
||||
- partially implemented in a compatibility-preserving form
|
||||
- the host owns the active registry state
|
||||
- the host exposes a resolved-extension registry view for static consumers
|
||||
- plugin skills, plugin auto-enable, and config validation indexing now consume host-owned resolved-extension data
|
||||
- activation, loader cache control, loader policy, loader discovery-policy outcomes, loader activation-policy outcomes, loader finalization-policy outcomes, loader candidate planning, loader import flow, loader runtime decisions, loader post-import register flow, loader candidate orchestration, loader top-level load orchestration, loader session state, loader record-state helpers, and loader finalization now route through `src/extension-host/*`
|
||||
- broader lifecycle state ownership beyond the loader state machine, activation states, policy evaluation, and broad host-owned registries are still not implemented
|
||||
|
||||
### Phase 3: Build compatibility bridges
|
||||
|
||||
Goal:
|
||||
|
||||
- keep current extensions working through the host without leaking legacy contracts into the kernel
|
||||
|
||||
Primary docs:
|
||||
|
||||
- `openclaw-kernel-extension-host-transition-plan.md`
|
||||
- `openclaw-extension-contribution-schema-spec.md`
|
||||
|
||||
Deliverables:
|
||||
|
||||
- `ChannelPlugin` compatibility translators
|
||||
- plugin SDK compatibility loading
|
||||
- runtime-channel namespace translation into the new SDK modules
|
||||
- legacy setup and CLI translation
|
||||
- legacy config schema and UI hint translation
|
||||
- pilot migration matrix with explicit parity labels
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- `thread-ownership` runs through the host path as the first non-channel pilot
|
||||
- `telegram` runs through the host path as the first channel pilot
|
||||
- both pilots have explicit parity results for discovery, config, activation, diagnostics, and runtime behavior
|
||||
|
||||
### Phase 4: Implement the canonical event pipeline
|
||||
|
||||
Goal:
|
||||
|
||||
- move runtime hook behavior onto explicit canonical events
|
||||
|
||||
Primary doc:
|
||||
|
||||
- `openclaw-kernel-event-pipeline-spec.md`
|
||||
|
||||
Deliverables:
|
||||
|
||||
- event type definitions
|
||||
- stage runner
|
||||
- sync transcript-write stages
|
||||
- bridges from legacy hook buses
|
||||
- mapping table from existing typed and legacy hooks to canonical stages
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- migrated extensions can use canonical events without relying directly on old plugin hook execution
|
||||
- pilot hook behaviors have parity coverage against the pre-host path
|
||||
|
||||
### Phase 5: Implement catalogs
|
||||
|
||||
Goal:
|
||||
|
||||
- compile runtime-derived agent and internal catalogs, plus host-owned operator catalogs
|
||||
- replace existing plugin-identity-driven catalog surfaces with canonical family-based catalogs
|
||||
|
||||
Primary doc:
|
||||
|
||||
- `openclaw-capability-catalog-and-arbitration-spec.md`
|
||||
|
||||
Deliverables:
|
||||
|
||||
- kernel internal catalog
|
||||
- kernel agent catalog
|
||||
- host operator catalog
|
||||
- static setup and install catalogs
|
||||
- canonical action registry
|
||||
- migration plan for existing tool, provider, and setup catalog surfaces
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- agent-visible tools are compiled from canonical action families instead of plugin identity
|
||||
- setup and install catalogs no longer depend on duplicated legacy metadata paths
|
||||
|
||||
### Phase 6: Implement arbitration
|
||||
|
||||
Goal:
|
||||
|
||||
- resolve overlap, ranking, selection, and slot conflicts deterministically
|
||||
- absorb the existing slot and provider selection behavior into canonical arbitration
|
||||
|
||||
Primary doc:
|
||||
|
||||
- `openclaw-capability-catalog-and-arbitration-spec.md`
|
||||
|
||||
Deliverables:
|
||||
|
||||
- conflict detection
|
||||
- provider selection
|
||||
- slot arbitration
|
||||
- planner-visible name collision handling
|
||||
- migration plan for existing slot and name-collision behaviors
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- at least one multi-provider family works through canonical arbitration
|
||||
- legacy slot and provider-selection paths no longer act as separate arbitration systems
|
||||
|
||||
### Phase 7: Migrate and remove legacy paths
|
||||
|
||||
Goal:
|
||||
|
||||
- finish migration and shrink compatibility-only surfaces
|
||||
|
||||
Primary docs:
|
||||
|
||||
- `openclaw-kernel-extension-host-transition-plan.md`
|
||||
- all other docs as parity references
|
||||
|
||||
Deliverables:
|
||||
|
||||
- channel migrations
|
||||
- non-channel extension migrations
|
||||
- parity tests
|
||||
- deprecation markers
|
||||
- removal plan for obsolete compatibility shims
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- legacy plugin runtime is no longer the default execution path
|
||||
|
||||
## Pilot Matrix
|
||||
|
||||
Initial pilot set:
|
||||
|
||||
- non-channel pilot: `thread-ownership`
|
||||
- channel pilot: `telegram`
|
||||
|
||||
Why these pilots:
|
||||
|
||||
- `thread-ownership` exercises typed hook loading without introducing CLI, HTTP route, or service migration at the same time
|
||||
- `telegram` exercises the `ChannelPlugin` compatibility path with a minimal top-level plugin registration surface
|
||||
|
||||
Second-wave compatibility candidates after the pilots are stable:
|
||||
|
||||
- `line` for channel plus command registration
|
||||
- `device-pair` for command, service, and setup flow coverage
|
||||
|
||||
Each pilot must record parity for:
|
||||
|
||||
- discovery and precedence
|
||||
- manifest and static metadata loading
|
||||
- config schema and UI hints
|
||||
- enabled and disabled state handling
|
||||
- activation and reload behavior
|
||||
- diagnostics and status output
|
||||
- runtime behavior on the migrated path
|
||||
- compatibility-only gaps that still remain
|
||||
|
||||
## Recommended First Implementation Slice
|
||||
|
||||
If you want the lowest-risk start, do this first:
|
||||
|
||||
1. write the boundary cutover inventory
|
||||
2. add source-of-truth types
|
||||
3. add the static metadata and package metadata parsers
|
||||
4. add `ResolvedExtension`
|
||||
5. add minimal SDK compatibility loading
|
||||
6. add host discovery and validation
|
||||
7. bring `thread-ownership` through the host path first
|
||||
8. bring `telegram` through the host path second
|
||||
|
||||
Status of this slice:
|
||||
|
||||
- steps 2 through 6 are underway
|
||||
- step 1 has landed as `src/extension-host/cutover-inventory.md`
|
||||
- steps 7 and 8 have not started
|
||||
|
||||
Concrete landings from this slice:
|
||||
|
||||
- the host boundary exists
|
||||
- source-of-truth schema types exist
|
||||
- package metadata parsing now routes through the host schema layer
|
||||
- `ResolvedExtension` exists in code and is attached to manifest-registry records
|
||||
- host-owned active-registry and resolved-registry views exist
|
||||
- early static consumers have moved onto the new host-owned data path
|
||||
|
||||
Do not start with catalogs or arbitration first.
|
||||
|
||||
Also avoid these first-cut traps:
|
||||
|
||||
- do not build a broad event scheduling framework before the canonical stages exist
|
||||
- do not turn permission descriptors into fake sandbox guarantees
|
||||
- do not build a large operator catalog publication layer before the host registries are real
|
||||
- do not over-type setup flows before the pilot migrations prove the minimum result model is insufficient
|
||||
|
||||
## Tracking Rules
|
||||
|
||||
When implementation begins:
|
||||
|
||||
- update this guide first with phase status
|
||||
- update the matching spec TODOs when a domain changes
|
||||
- record where the implementation intentionally diverged from the spec
|
||||
- record which behaviors are full parity, partial parity, or compatibility-only
|
||||
- update the pilot parity matrix whenever a migrated surface changes
|
||||
|
||||
## Suggested Status Format
|
||||
|
||||
Use this format in each doc when work starts:
|
||||
|
||||
- `not started`
|
||||
- `in progress`
|
||||
- `implemented`
|
||||
- `verified`
|
||||
- `deferred`
|
||||
|
||||
For example:
|
||||
|
||||
- `ResolvedExtension` registry: `implemented`
|
||||
- setup fallback removal: `deferred`
|
||||
- sync transcript-write parity tests: `in progress`
|
||||
@@ -0,0 +1,750 @@
|
||||
Temporary internal migration note: remove this document once the extension-host migration is complete.
|
||||
|
||||
# OpenClaw Extension Host Lifecycle And Security Spec
|
||||
|
||||
Date: 2026-03-15
|
||||
|
||||
## Purpose
|
||||
|
||||
This document defines how the extension host discovers, validates, activates, isolates, and stops extensions while applying operator policy, permission metadata, persistence boundaries, and contribution dependencies.
|
||||
|
||||
The kernel does not participate in these concerns directly.
|
||||
|
||||
## TODOs
|
||||
|
||||
- [x] Write the initial boundary cutover inventory for every current plugin-owned surface.
|
||||
- [ ] Keep the boundary cutover inventory updated as surfaces move.
|
||||
- [ ] Extend the loader lifecycle state machine into full extension-host lifecycle ownership and document the concrete runtime states in code.
|
||||
- [ ] Implement advisory versus enforced permission handling exactly as specified here.
|
||||
- [ ] Implement host-owned registries for config, setup, CLI, routes, services, slots, and backends.
|
||||
- [ ] Implement per-extension state ownership and migration from current shared plugin state.
|
||||
- [ ] Record pilot parity for `thread-ownership` first and `telegram` second before broad legacy rollout.
|
||||
- [ ] Track which hardening, reload, and provenance rules have reached parity with `main`.
|
||||
|
||||
## Implementation Status
|
||||
|
||||
Current status against this spec:
|
||||
|
||||
- registry ownership and the first compatibility-preserving loader slices have landed
|
||||
- a loader-scoped lifecycle state machine has landed
|
||||
- broader lifecycle orchestration, policy gates, and activation-state management beyond the current loader, service, and CLI seams have not landed
|
||||
|
||||
What has been implemented:
|
||||
|
||||
- an initial Phase 0 cutover inventory now exists in `src/extension-host/cutover-inventory.md`
|
||||
- active registry ownership now lives in the extension host boundary rather than only in plugin-era runtime state
|
||||
- central lookup surfaces now consume the host-owned active registry
|
||||
- registry activation now routes through `src/extension-host/activation.ts`
|
||||
- a host-owned resolved-extension registry exists for static consumers
|
||||
- static config-baseline generation now reads bundled extension metadata through the host-owned resolved-extension registry
|
||||
- channel, provider, HTTP-route, gateway-method, tool, CLI, service, command, context-engine, and hook registration normalization now delegates through `src/extension-host/contributions/runtime-registrations.ts`
|
||||
- low-risk runtime compatibility writes for channel, provider, gateway-method, HTTP-route, tool, CLI, service, command, context-engine, and hook registrations now delegate through `src/extension-host/contributions/registry-writes.ts`
|
||||
- legacy internal-hook bridging and typed prompt-injection compatibility policy now delegate through `src/extension-host/compat/hook-compat.ts`
|
||||
- compatibility `OpenClawPluginApi` composition and logger shaping now delegate through `src/extension-host/compat/plugin-api.ts`
|
||||
- compatibility plugin-registry facade ownership now delegates through `src/extension-host/compat/plugin-registry.ts`
|
||||
- compatibility plugin-registry policy now delegates through `src/extension-host/compat/plugin-registry-compat.ts`
|
||||
- compatibility plugin-registry registration actions now delegate through `src/extension-host/compat/plugin-registry-registrations.ts`
|
||||
- host-owned runtime registry accessors now delegate through `src/extension-host/contributions/runtime-registry.ts`, and the channel, provider, tool, command, HTTP-route, gateway-method, CLI, and service slices now keep host-owned storage there with mirrored legacy compatibility views
|
||||
- plugin command registration, matching, execution, listing, native command-spec projection, and loader reload clearing now delegate through `src/extension-host/contributions/command-runtime.ts`
|
||||
- service startup, stop ordering, service-context creation, and failure logging now delegate through `src/extension-host/contributions/service-lifecycle.ts`
|
||||
- CLI duplicate detection, registrar invocation, and async failure logging now delegate through `src/extension-host/contributions/cli-lifecycle.ts`
|
||||
- gateway method-id aggregation, plugin diagnostic shaping, and extra-handler composition now delegate through `src/extension-host/contributions/gateway-methods.ts`
|
||||
- plugin tool resolution, conflict handling, optional-tool gating, and plugin-tool metadata tracking now delegate through `src/extension-host/contributions/tool-runtime.ts`
|
||||
- plugin provider projection from registry entries into runtime provider objects now delegates through `src/extension-host/contributions/provider-runtime.ts`
|
||||
- plugin provider discovery filtering, order grouping, and result normalization now delegate through `src/extension-host/contributions/provider-discovery.ts`
|
||||
- provider matching, auth-method selection, config-patch merging, and default-model application now delegate through `src/extension-host/contributions/provider-auth.ts`
|
||||
- provider onboarding option building, model-picker entry building, and provider-method choice resolution now delegate through `src/extension-host/contributions/provider-wizard.ts`
|
||||
- loaded-provider auth application, plugin-enable gating, auth-method execution, and post-auth default-model handling now delegate through `src/extension-host/contributions/provider-auth-flow.ts`
|
||||
- provider post-selection hook lookup and invocation now delegate through `src/extension-host/contributions/provider-model-selection.ts`
|
||||
- loader alias-wired module loader creation now routes through `src/extension-host/activation/loader-module-loader.ts`
|
||||
- loader cache key construction and registry cache control now route through `src/extension-host/activation/loader-cache.ts`
|
||||
- loader lazy runtime proxy creation now routes through `src/extension-host/activation/loader-runtime-proxy.ts`
|
||||
- loader provenance helpers now route through `src/extension-host/policy/loader-provenance.ts`
|
||||
- loader duplicate-order and record/error policy now route through `src/extension-host/policy/loader-policy.ts`
|
||||
- loader discovery policy outcomes now route through `src/extension-host/policy/loader-discovery-policy.ts`
|
||||
- loader initial candidate planning and record creation now route through `src/extension-host/activation/loader-records.ts`
|
||||
- loader entry-path opening and module import now route through `src/extension-host/activation/loader-import.ts`
|
||||
- loader module-export resolution, config validation, and memory-slot load decisions now route through `src/extension-host/activation/loader-runtime.ts`
|
||||
- loader post-import planning and `register(...)` execution now route through `src/extension-host/activation/loader-register.ts`
|
||||
- loader per-candidate orchestration now routes through `src/extension-host/activation/loader-flow.ts`
|
||||
- loader top-level load orchestration now routes through `src/extension-host/activation/loader-orchestrator.ts`
|
||||
- loader host process state now routes through `src/extension-host/activation/loader-host-state.ts`
|
||||
- loader preflight and cache-hit setup now routes through `src/extension-host/activation/loader-preflight.ts`
|
||||
- loader post-preflight pipeline composition now routes through `src/extension-host/activation/loader-pipeline.ts`
|
||||
- loader execution setup composition now routes through `src/extension-host/activation/loader-execution.ts`
|
||||
- loader discovery and manifest bootstrap now routes through `src/extension-host/activation/loader-bootstrap.ts`
|
||||
- loader mutable activation state now routes through `src/extension-host/activation/loader-session.ts`
|
||||
- loader session run and finalization composition now routes through `src/extension-host/activation/loader-run.ts`
|
||||
- loader activation policy outcomes now route through `src/extension-host/policy/loader-activation-policy.ts`
|
||||
- loader record-state transitions now route through `src/extension-host/activation/loader-state.ts`, which now enforces an explicit loader lifecycle state machine while preserving compatibility `PluginRecord.status` values
|
||||
- loader finalization policy results now route through `src/extension-host/policy/loader-finalization-policy.ts`
|
||||
- loader final cache, readiness promotion, and activation finalization now routes through `src/extension-host/activation/loader-finalize.ts`
|
||||
|
||||
How it has been implemented:
|
||||
|
||||
- by extracting `src/extension-host/static/active-registry.ts` and making `src/plugins/runtime.ts` delegate to it
|
||||
- by leaving lifecycle behavior unchanged for now and only moving ownership of the shared registry boundary
|
||||
- by moving low-risk readers first, such as channel lookup, dock lookup, message-channel lookup, and default HTTP route registry access
|
||||
- by extending that same host-owned boundary into static consumers instead of introducing separate one-off metadata loaders
|
||||
- by starting runtime-registry migration with low-risk validation and normalization helpers while leaving lifecycle ordering and activation behavior unchanged
|
||||
- by starting actual low-risk runtime write ownership for channel, provider, gateway-method, HTTP-route, tool, CLI, service, command, context-engine, and hook registrations only after normalization helpers existed, while leaving lifecycle ordering and activation behavior unchanged
|
||||
- by leaving start/stop ordering and duplicate-enforcement behavior in legacy subsystems where those subsystems are still the real owner
|
||||
- by treating hook execution and hook registration as separate migration concerns so event-pipeline work does not get conflated with record normalization
|
||||
- by starting loader/lifecycle migration with activation and SDK alias compatibility helpers while leaving discovery and policy flow unchanged
|
||||
- by moving cache-key construction, cache reads, cache writes, and cache clearing next while leaving activation-state ownership unchanged
|
||||
- by moving provenance and duplicate-order policy next, so lifecycle migration can land on host-owned policy helpers instead of loader-local utilities
|
||||
- by extracting lazy runtime proxy creation and alias-wired Jiti module-loader creation into host-owned helpers before broader bootstrap or lifecycle ownership changes
|
||||
- by extracting discovery, manifest loading, manifest diagnostics, discovery-policy logging, provenance building, and candidate ordering into a host-owned loader-bootstrap helper before broader lifecycle ownership changes
|
||||
- by extracting candidate iteration, manifest lookup, per-candidate session processing, and finalization handoff into a host-owned loader-run helper before broader lifecycle ownership changes
|
||||
- by moving initial candidate planning and record construction next while leaving module import and registration flow unchanged
|
||||
- by moving entry-path opening and module import next while leaving cache wiring and lifecycle orchestration unchanged
|
||||
- by moving loader runtime decisions next while preserving the current lazy-load, config-validation, and memory-slot behavior
|
||||
- by moving post-import planning and `register(...)` execution next while leaving entry-path and import flow unchanged
|
||||
- by composing those seams into one host-owned per-candidate loader orchestrator before moving final lifecycle-state behavior
|
||||
- by moving the remaining top-level loader orchestration into a host-owned module before enforcing the loader lifecycle state machine
|
||||
- by extracting shared discovery warning-cache state and loader reset behavior into a host-owned loader-host-state helper before shrinking the remaining orchestrator surface
|
||||
- by extracting test-default application, config normalization, cache-key construction, cache-hit activation, and command-clear setup into a host-owned loader-preflight helper before shrinking the remaining orchestrator surface
|
||||
- by extracting post-preflight execution setup and session-run composition into a host-owned loader-pipeline helper before shrinking the remaining orchestrator surface
|
||||
- by extracting runtime creation, registry creation, bootstrap setup, module-loader creation, and session creation into a host-owned loader-execution helper before shrinking the remaining orchestrator surface
|
||||
- by moving record-state transitions first into a compatibility layer and then into an enforced loader lifecycle state machine
|
||||
- by moving cache writes, provenance warnings, final memory-slot warnings, and activation into a host-owned loader finalizer before introducing an explicit lifecycle state machine
|
||||
- by adding explicit compatibility `lifecycleState` mapping on loader-owned plugin records before enforcing the loader lifecycle state machine
|
||||
- by promoting successfully registered plugins to `ready` during host-owned finalization while leaving broader activation-state semantics for later phases
|
||||
- by moving mutable activation state such as seen-id tracking, memory-slot selection, and finalization inputs into a host-owned loader session before broader activation-state semantics move
|
||||
- by extracting shared provenance path matching and install-rule evaluation into `src/extension-host/policy/loader-provenance.ts` so activation and finalization policy seams reuse one host-owned implementation
|
||||
- by turning open-allowlist discovery warnings into explicit host-owned discovery-policy results before the orchestrator logs them
|
||||
- by moving duplicate precedence, config enablement, and early memory-slot gating into explicit host-owned activation-policy outcomes before broader policy semantics move
|
||||
- by turning provenance-based untracked-extension warnings and final memory-slot warnings into explicit host-owned finalization-policy results before the finalizer applies them
|
||||
- by extracting legacy internal-hook bridging and typed prompt-injection compatibility policy into a host-owned hook-compat helper while leaving actual hook execution ownership unchanged
|
||||
- by extracting compatibility `OpenClawPluginApi` composition and logger shaping into a host-owned plugin-api helper while keeping the concrete registration callbacks in the legacy registry surface
|
||||
- by extracting the remaining compatibility plugin-registry facade into a host-owned helper so `src/plugins/registry.ts` becomes a thin wrapper instead of the real owner
|
||||
- by extracting provider normalization, command duplicate enforcement, and registry-local diagnostic shaping into a host-owned registry-compat helper while leaving the underlying provider-validation and plugin-command subsystems unchanged
|
||||
- by extracting low-risk registry registration actions into a host-owned registry-registrations helper so the compatibility facade composes host-owned actions instead of implementing them inline
|
||||
- by extracting service startup, stop ordering, service-context creation, and failure logging into a host-owned service-lifecycle helper while `src/plugins/services.ts` remains the compatibility entry point
|
||||
- by extracting CLI duplicate detection, registrar invocation, and async failure logging into a host-owned CLI-lifecycle helper while `src/plugins/cli.ts` remains the compatibility entry point
|
||||
- by extracting gateway method-id aggregation, plugin diagnostic shaping, and extra-handler composition into a host-owned gateway-methods helper while request dispatch semantics remain in the gateway server code
|
||||
- by extracting plugin tool resolution, conflict handling, optional-tool gating, and plugin-tool metadata tracking into a host-owned tool-runtime helper while `src/plugins/tools.ts` remains the loader and config-normalization facade
|
||||
- by extracting provider projection from registry entries into runtime provider objects into a host-owned provider-runtime helper while `src/plugins/providers.ts` remains the loader and config-normalization facade
|
||||
- by extracting provider discovery filtering, order grouping, and result normalization into a host-owned provider-discovery helper while `src/plugins/provider-discovery.ts` remains the compatibility facade around the legacy provider loader path
|
||||
- by extracting provider matching, auth-method selection, config-patch merging, and default-model application into a host-owned provider-auth helper while `src/commands/provider-auth-helpers.ts` remains the command-facing compatibility facade
|
||||
- by extracting provider onboarding option building, model-picker entry building, and provider-method choice resolution into a host-owned provider-wizard helper while `src/plugins/provider-wizard.ts` remains the compatibility facade around loader-backed provider access and post-selection hooks
|
||||
- by extracting loaded-provider auth application, plugin-enable gating, auth-method execution, and post-auth default-model handling into a host-owned provider-auth-flow helper while `src/commands/auth-choice.apply.plugin-provider.ts` remains the compatibility entry point
|
||||
- by extracting provider post-selection hook lookup and invocation into a host-owned provider-model-selection helper while `src/plugins/provider-wizard.ts` remains the compatibility facade and existing command consumers continue migrating onto the host-owned surface
|
||||
- by extracting provider-id normalization into `src/agents/provider-id.ts` so provider-only host seams do not inherit the heavier agent and browser dependency graph from `src/agents/model-selection.ts`
|
||||
- by extracting model-ref parsing into `src/agents/model-ref.ts` and Google model-id normalization into `src/agents/google-model-id.ts` so provider auth and setup seams can be tested without pulling the heavier provider-loader and browser dependency graph
|
||||
- by introducing host-owned runtime-registry accessors for channel, provider, tool, service, CLI, command, gateway-method, and HTTP-route consumers first, then moving channel, provider, tool, command, HTTP-route, gateway-method, CLI, and service storage into that host-owned state while keeping mirrored legacy compatibility arrays and handler maps
|
||||
- by moving plugin command duplicate enforcement, registration, matching, execution, listing, native command-spec projection, and loader reload clearing into `src/extension-host/contributions/command-runtime.ts` while keeping the legacy command module as a compatibility facade
|
||||
- by tightening the CLI pre-load fast path to treat any host-known runtime entry surface as already loaded rather than only plugins, channels, or tools
|
||||
|
||||
What is still pending from this spec:
|
||||
|
||||
- broader extension-host lifecycle ownership beyond the loader state machine, service-lifecycle boundary, CLI-lifecycle boundary, session-owned activation state, and explicit discovery-policy, activation-policy, and finalization-policy outcomes
|
||||
- activation pipeline ownership
|
||||
- host-owned registries for setup, CLI, routes, services, slots, and backends
|
||||
- host-owned subsystem runtime registries for embeddings, media understanding, and TTS, including explicit fallback and override policy instead of plugin-era capability reads
|
||||
- a clear host-owned split for extension-backed search between agent-visible tool publication and any optional runtime-internal search backend registry
|
||||
- permission-mode enforcement
|
||||
- per-extension state ownership and migration
|
||||
- provenance, reload, and hardening parity tracking
|
||||
|
||||
## Goals
|
||||
|
||||
- deterministic activation and shutdown
|
||||
- explicit failure states
|
||||
- no hidden privilege escalation
|
||||
- stable persistence ownership rules
|
||||
- truthful security semantics for the current trusted in-process model
|
||||
- safe support for bundled and external extensions under the same model
|
||||
- preserve existing hardening and prompt-mutation policy behavior during the migration
|
||||
|
||||
## Implementation Sequencing Constraints
|
||||
|
||||
This spec is not a greenfield host design.
|
||||
|
||||
The host must absorb existing behavior that already lives in:
|
||||
|
||||
- plugin discovery and manifest loading
|
||||
- config schema and UI hint handling
|
||||
- route and gateway registration
|
||||
- channels and channel lookup
|
||||
- providers and provider auth or setup flows
|
||||
- tools, commands, and CLI registration
|
||||
- services, backends, and slot-backed providers
|
||||
- reload, diagnostics, install, update, and status behavior
|
||||
|
||||
Therefore:
|
||||
|
||||
- Phase 0 must produce a cutover inventory for those surfaces before registry ownership changes begin
|
||||
- Phase 1 must preserve current SDK loading through minimal compatibility support
|
||||
- Phase 2 registry work must be broad enough to cover all currently registered surfaces, not only a narrow runtime subset
|
||||
- Phase 3 must prove parity through `thread-ownership` first and `telegram` second before broader rollout
|
||||
|
||||
## Trust Model Reality
|
||||
|
||||
Current `main` treats installed and enabled extensions as trusted code running in-process:
|
||||
|
||||
- trusted plugin concept in `SECURITY.md:108`
|
||||
- in-process loading in `src/plugins/loader.ts:621`
|
||||
|
||||
That means the initial extension host has two separate jobs:
|
||||
|
||||
- enforce operator policy for activation, route exposure, host-owned registries, and auditing
|
||||
- accurately communicate that this is not yet a hard sandbox against arbitrary extension code
|
||||
|
||||
Recommended enforcement levels:
|
||||
|
||||
- `advisory`
|
||||
Host policy, audit, and compatibility guidance only. This is the current default. Permission mismatch alone should not block activation in this mode, though the host may warn and withhold optional host-published surfaces.
|
||||
- `host-enforced`
|
||||
Host-owned capabilities and registries are gated, but extension code still runs in-process.
|
||||
- `sandbox-enforced`
|
||||
A future mode with real process, VM, or IPC isolation where permissions become a true security boundary.
|
||||
|
||||
## Lifecycle States
|
||||
|
||||
Every extension instance moves through these states:
|
||||
|
||||
1. `discovered`
|
||||
2. `manifest-loaded`
|
||||
3. `validated`
|
||||
4. `dependency-resolved`
|
||||
5. `policy-approved`
|
||||
6. `instantiated`
|
||||
7. `registered`
|
||||
8. `starting`
|
||||
9. `ready`
|
||||
10. `degraded`
|
||||
11. `stopping`
|
||||
12. `stopped`
|
||||
13. `failed`
|
||||
|
||||
The host owns the state machine.
|
||||
|
||||
## Activation Pipeline
|
||||
|
||||
### 1. Discovery
|
||||
|
||||
The host scans:
|
||||
|
||||
- bundled extension inventory
|
||||
- configured external extension paths or packages
|
||||
- disabled extension state
|
||||
|
||||
Discovery is metadata-only. No extension code executes in this phase.
|
||||
|
||||
### 2. Manifest Load
|
||||
|
||||
The host loads and validates manifest syntax.
|
||||
|
||||
Failures here prevent instantiation.
|
||||
|
||||
This phase must cover both:
|
||||
|
||||
- runtime contribution descriptors
|
||||
- package-level static metadata used for install, onboarding, status, and lightweight operator UX
|
||||
|
||||
### 3. Schema Validation
|
||||
|
||||
The host validates:
|
||||
|
||||
- top-level extension manifest
|
||||
- contribution descriptors
|
||||
- config schema
|
||||
- config UI hints and sensitivity metadata
|
||||
- permission declarations
|
||||
- dependency declarations
|
||||
- policy declarations such as prompt-mutation behavior
|
||||
|
||||
### 4. Dependency Resolution
|
||||
|
||||
The host resolves:
|
||||
|
||||
- extension api compatibility
|
||||
- SDK compatibility mode and deprecation requirements
|
||||
- required contribution dependencies
|
||||
- optional dependencies
|
||||
- conflict declarations
|
||||
- singleton slot collisions
|
||||
|
||||
Compatibility decision:
|
||||
|
||||
- the host should support only a short compatibility window, ideally one or two older SDK contract versions at a time
|
||||
- extensions outside that window must fail validation with a clear remediation path
|
||||
|
||||
Sequencing rule:
|
||||
|
||||
- minimal compatibility loading must exist before broader schema or registry changes depend on the new manifest model
|
||||
|
||||
### 5. Policy Gate
|
||||
|
||||
The host computes the requested permission set and compares it against operator policy.
|
||||
|
||||
In `host-enforced` or `sandbox-enforced` mode, extensions that are not allowed to receive all required permissions do not activate or do not register the gated contributions.
|
||||
|
||||
In `advisory` mode, this gate records warnings, informs operator-visible policy state, and may withhold optional host-published surfaces, but permission mismatch alone does not fail activation.
|
||||
|
||||
It does not sandbox arbitrary filesystem, network, or child-process access from trusted in-process extension code.
|
||||
|
||||
### 6. Instantiation
|
||||
|
||||
The host loads the extension entrypoint and asks it to emit contribution descriptors and runtime factories.
|
||||
|
||||
Unless the host is running in a future isolated mode, instantiation still executes trusted extension code inside the OpenClaw process.
|
||||
|
||||
### 7. Registration
|
||||
|
||||
The host resolves runtime ids, arbitration metadata, and activation order, then registers contributions into host-owned registries.
|
||||
|
||||
This includes host-managed operator registries for:
|
||||
|
||||
- CLI commands
|
||||
- setup and onboarding flows
|
||||
- config and status surfaces
|
||||
- dynamic HTTP routes
|
||||
- config reload descriptors and gateway feature advertisement where those surfaces remain host-managed during migration
|
||||
|
||||
Callable gateway or runtime methods are separate from this advertisement layer and should continue to register through the runtime contribution model as `capability.rpc`.
|
||||
|
||||
The registration boundary should cover the full current surface area as one migration set:
|
||||
|
||||
- hooks and event handlers
|
||||
- channels and lightweight channel descriptors
|
||||
- providers and provider-setup surfaces
|
||||
- tools and control commands
|
||||
- CLI, setup, config, and status surfaces
|
||||
- HTTP routes and gateway methods
|
||||
- services, runtime backends, and slot-backed providers
|
||||
|
||||
Do not migrate only a subset and leave the rest writing into the legacy registry model indefinitely.
|
||||
|
||||
### 8. Start
|
||||
|
||||
The host starts host-managed services, assigns per-extension state and route ownership, and activates kernel-facing contributions.
|
||||
|
||||
### 9. Ready
|
||||
|
||||
The extension is active and visible to kernel or operator surfaces as appropriate.
|
||||
|
||||
## Failure Modes
|
||||
|
||||
Supported failure classes:
|
||||
|
||||
- `manifest-invalid`
|
||||
- `api-version-unsupported`
|
||||
- `dependency-missing`
|
||||
- `dependency-conflict`
|
||||
- `policy-denied`
|
||||
- `instantiation-failed`
|
||||
- `registration-conflict`
|
||||
- `startup-failed`
|
||||
- `runtime-degraded`
|
||||
|
||||
The host must record failure class, extension id, contribution ids, and operator-visible remediation.
|
||||
|
||||
## Dependency Rules
|
||||
|
||||
Dependencies must be explicit and machine-checkable.
|
||||
|
||||
### Extension-level dependencies
|
||||
|
||||
Used when one extension package requires another package to be present.
|
||||
|
||||
### Contribution-level dependencies
|
||||
|
||||
Used when a specific runtime contract depends on another contribution.
|
||||
|
||||
Examples:
|
||||
|
||||
- a route augmenter may require a specific adapter family
|
||||
- an auth helper may require a provider contribution
|
||||
- a diagnostics extension may optionally bind to a runtime backend if present
|
||||
|
||||
### Conflict rules
|
||||
|
||||
Extensions may declare:
|
||||
|
||||
- `conflicts`
|
||||
- `supersedes`
|
||||
- `replaces`
|
||||
|
||||
The host resolves these before activation.
|
||||
|
||||
## Discovery And Load Hardening
|
||||
|
||||
The extension host must preserve current path-safety, provenance, and duplicate-resolution protections.
|
||||
|
||||
At minimum, preserve parity with:
|
||||
|
||||
- path and boundary checks during load in `src/plugins/loader.ts:744`
|
||||
- manifest precedence and duplicate-origin handling in `src/plugins/manifest-registry.ts:15`
|
||||
- provenance warnings during activation in `src/plugins/loader.ts:500`
|
||||
|
||||
Security hardening from the current loader is part of the host contract, not an optional implementation detail.
|
||||
|
||||
Parity requirement:
|
||||
|
||||
- the pilot migrations must show that these hardening rules still apply on the host path, not only on the legacy path
|
||||
|
||||
## Policy And Permission Model
|
||||
|
||||
Permissions are granted to extension instances by the host as policy metadata and host capability grants.
|
||||
|
||||
The kernel must never infer privilege from contribution kind alone.
|
||||
|
||||
The host must track both:
|
||||
|
||||
- requested permissions
|
||||
- enforcement level (`advisory`, `host-enforced`, or `sandbox-enforced`)
|
||||
- host-managed policy gates such as prompt mutation and sync hot-path eligibility
|
||||
|
||||
### Recommended permission set
|
||||
|
||||
- `runtime.adapter`
|
||||
- `runtime.route-augment`
|
||||
- `runtime.veto-send`
|
||||
- `runtime.backend-register`
|
||||
- `agent.tool.expose`
|
||||
- `control.command.expose`
|
||||
- `interaction.handle`
|
||||
- `conversation.bind`
|
||||
- `conversation.bind.approve`
|
||||
- `conversation.control`
|
||||
- `rpc.expose`
|
||||
- `service.background`
|
||||
- `http.route.gateway`
|
||||
- `http.route.plugin`
|
||||
- `config.read`
|
||||
- `config.write`
|
||||
- `state.read`
|
||||
- `state.write`
|
||||
- `credentials.read`
|
||||
- `credentials.write`
|
||||
- `network.outbound`
|
||||
- `process.spawn`
|
||||
- `filesystem.workspace.read`
|
||||
- `filesystem.workspace.write`
|
||||
|
||||
Permissions should be independently reviewable and denyable.
|
||||
|
||||
In `advisory` mode they also function as:
|
||||
|
||||
- operator review prompts
|
||||
- activation policy inputs
|
||||
- audit and telemetry tags
|
||||
- documentation of why an extension needs sensitive host-owned surfaces
|
||||
|
||||
### Fine-grained policy gates
|
||||
|
||||
Some behavior should remain under dedicated policy gates instead of being flattened into generic permissions.
|
||||
|
||||
Examples:
|
||||
|
||||
- prompt mutation or prompt injection behavior
|
||||
- sync transcript-write participation
|
||||
- fail-open versus fail-closed route augmentation
|
||||
- whether an extension may bind conversations without per-request operator approval
|
||||
- whether an interaction handler may invoke conversation-control verbs
|
||||
|
||||
This preserves the intent of current controls such as `plugins.entries.<id>.hooks.allowPromptInjection`.
|
||||
|
||||
### High-risk permissions
|
||||
|
||||
These should require explicit operator approval or a strong default policy:
|
||||
|
||||
- `runtime.veto-send`
|
||||
- `runtime.route-augment`
|
||||
- `conversation.bind`
|
||||
- `conversation.bind.approve`
|
||||
- `runtime.backend-register`
|
||||
- `credentials.write`
|
||||
- `process.spawn`
|
||||
- `http.route.plugin`
|
||||
- `filesystem.workspace.write`
|
||||
|
||||
High-risk permissions should still matter in `advisory` mode because they drive operator trust decisions even before real isolation exists.
|
||||
|
||||
### Binding and interaction ownership
|
||||
|
||||
Conversation binding and interactive callback routing should be treated as host-owned lifecycle surfaces.
|
||||
|
||||
The host must own:
|
||||
|
||||
- namespace registration and dedupe for interactive callbacks
|
||||
- approval persistence for extension-requested conversation binds
|
||||
- restore-on-restart behavior for approved bindings
|
||||
- cleanup behavior for detached or stale bindings
|
||||
- channel-surface gating for first-cut conversation-control verbs
|
||||
|
||||
Extensions may own:
|
||||
|
||||
- the logic that decides whether to request a bind
|
||||
- the interaction payload semantics
|
||||
- channel-specific presentation details that fit inside the host-owned adapter contract
|
||||
|
||||
Important migration rule:
|
||||
|
||||
- do not turn `src/plugins/conversation-binding.ts` or `src/plugins/interactive.ts` into the permanent architecture target
|
||||
- those behaviors should migrate into host-owned lifecycle and policy surfaces, with compatibility bridges only where needed
|
||||
|
||||
## Persistence Ownership
|
||||
|
||||
Persistence must be partitioned by owner and intent.
|
||||
|
||||
### Config
|
||||
|
||||
Operator-managed configuration belongs to the host.
|
||||
|
||||
Extensions may contribute:
|
||||
|
||||
- config schema
|
||||
- config UI hints and sensitivity metadata
|
||||
- defaults
|
||||
- migration hints
|
||||
- setup flow outputs such as config patches produced through host-owned setup primitives
|
||||
|
||||
Extensions must not arbitrarily mutate unrelated config keys.
|
||||
|
||||
The host must also preserve current config redaction semantics:
|
||||
|
||||
- config UI hints such as `sensitive` affect host behavior, not only UI decoration
|
||||
- config read, redact, restore, and validate flows must preserve round-trippable secret handling comparable to `src/gateway/server-methods/config.ts:151` and `src/config/redact-snapshot.ts:349`
|
||||
|
||||
### State
|
||||
|
||||
Each extension gets a host-assigned state directory.
|
||||
|
||||
This is where background services and caches persist local state.
|
||||
|
||||
This is a required migration change from the current shared plugin service state shape in `src/plugins/services.ts:18`.
|
||||
|
||||
The host must also define a migration strategy for existing state:
|
||||
|
||||
- detect old shared plugin state layouts
|
||||
- migrate or alias data into per-extension directories
|
||||
- keep rollback behavior explicit
|
||||
|
||||
### Credentials
|
||||
|
||||
Credential persistence is host-owned.
|
||||
|
||||
Provider integration extensions may return credential payloads, but they must not choose final storage shape or bypass the credential store.
|
||||
|
||||
This is required because auth flows like `extensions/google-gemini-cli-auth/index.ts:24` interact with credentials and config together.
|
||||
|
||||
This rule also applies when those flows are invoked through extension-owned CLI or setup flows.
|
||||
|
||||
### Session and transcript state
|
||||
|
||||
Kernel-owned.
|
||||
|
||||
Extensions may observe or augment session state through declared runtime contracts, but they do not own transcript persistence.
|
||||
|
||||
### Backend-owned state
|
||||
|
||||
Runtime backends such as ACP may require separate service state, but ownership still flows through the host-assigned state boundary.
|
||||
|
||||
### Distribution and onboarding metadata
|
||||
|
||||
Install metadata, channel catalog metadata, docs links, and quickstart hints are host-owned static metadata.
|
||||
|
||||
They are not kernel persistence and they are not extension-private state.
|
||||
|
||||
That static metadata should preserve current channel catalog fields from `src/plugins/manifest.ts:121`, including aliases, docs labels, precedence hints, binding hints, picker extras, and announce-target hints.
|
||||
|
||||
## HTTP And Webhook Ownership
|
||||
|
||||
The host owns all HTTP route registration and conflict resolution.
|
||||
|
||||
This is required because routes can conflict across extensions today, as seen in `src/plugins/http-registry.ts:12`.
|
||||
|
||||
### Route classes
|
||||
|
||||
- ingress transport routes
|
||||
- authenticated plugin routes
|
||||
- public callback routes
|
||||
- diagnostic or admin routes
|
||||
- dynamic account-scoped routes
|
||||
|
||||
### Required route metadata
|
||||
|
||||
- path
|
||||
- auth mode
|
||||
- match mode
|
||||
- owner contribution id
|
||||
- whether the route is externally reachable
|
||||
- whether the route is safe to expose when the extension is disabled
|
||||
- lifecycle mode (`static` or `dynamic`)
|
||||
- scope metadata such as account, workspace, or provider binding
|
||||
|
||||
### Conflict rules
|
||||
|
||||
- exact path collisions require explicit resolution
|
||||
- prefix collisions require overlap analysis
|
||||
- auth mismatches are fatal
|
||||
- one extension may not replace another extension's route without explicit policy
|
||||
|
||||
Dynamic route registration must also return an unregister handle so route ownership can be cleaned up during reload, account removal, or degraded shutdown.
|
||||
|
||||
## Runtime Backend Contract
|
||||
|
||||
Some extension contributions provide runtime backends consumed by subsystems rather than directly by the agent.
|
||||
|
||||
ACP is the reference case today:
|
||||
|
||||
- backend type in `src/acp/runtime/registry.ts:4`
|
||||
- registration in `extensions/acpx/src/service.ts:55`
|
||||
|
||||
### Required backend descriptor
|
||||
|
||||
- backend class id
|
||||
- backend instance id
|
||||
- selector key
|
||||
- health probe
|
||||
- capability list
|
||||
- selection rank
|
||||
- arbitration mode
|
||||
|
||||
### Required backend lifecycle
|
||||
|
||||
- register
|
||||
- unregister
|
||||
- probe
|
||||
- health
|
||||
- degrade
|
||||
- recover
|
||||
|
||||
### Backend selection rules
|
||||
|
||||
- explicit requested backend id wins
|
||||
- if none requested, pick the healthiest backend with the best rank
|
||||
- if multiple healthy backends tie, use deterministic ordering by extension id then contribution id
|
||||
- if all backends are unhealthy, expose a typed unavailability error
|
||||
|
||||
### Singleton vs parallel
|
||||
|
||||
Not every backend is singleton.
|
||||
|
||||
ACP may remain effectively singleton at first, but the contract should support future parallel backends with explicit selectors.
|
||||
|
||||
## Slot-Backed Provider Contract
|
||||
|
||||
Not every exclusive runtime provider is a generic backend.
|
||||
|
||||
Current `main` already has slot-backed provider selection in:
|
||||
|
||||
- `src/plugins/slots.ts:12`
|
||||
- `src/context-engine/registry.ts:60`
|
||||
|
||||
The host must model explicit slot-backed providers for cases such as:
|
||||
|
||||
- context engines
|
||||
- default memory providers
|
||||
- future execution or planning engines
|
||||
|
||||
Required slot rules:
|
||||
|
||||
- each slot has a stable slot id
|
||||
- each slot has a host-defined default
|
||||
- explicit config selection wins
|
||||
- only one active provider may own an exclusive slot
|
||||
- migration preserves existing config semantics such as `plugins.slots.memory` and `plugins.slots.contextEngine`
|
||||
|
||||
Migration rule:
|
||||
|
||||
- slot-backed providers must move into host-owned registries before broader catalog and arbitration migration claims are considered complete
|
||||
|
||||
## Isolation Rules
|
||||
|
||||
The host must isolate extension failures from the kernel as much as possible.
|
||||
|
||||
Minimum requirements:
|
||||
|
||||
- one extension failing startup does not block unrelated extensions
|
||||
- one contribution registration failure does not corrupt host state
|
||||
- background-service failures transition the extension to `degraded` or `failed` without leaving stale registrations behind
|
||||
- stop hooks are best-effort and time-bounded
|
||||
|
||||
In the current trusted in-process mode, "isolation" here means lifecycle and registry isolation, not a security sandbox.
|
||||
|
||||
## Reload And Upgrade Rules
|
||||
|
||||
Hot reload is optional. Deterministic restart behavior is required.
|
||||
|
||||
On reload or upgrade:
|
||||
|
||||
1. stop host-managed services
|
||||
2. unregister contributions
|
||||
3. clear host-owned route, command, backend, and slot registrations
|
||||
4. clear dynamic account-scoped routes and stale runtime handles
|
||||
5. instantiate the new version
|
||||
6. reactivate only after validation and policy checks succeed
|
||||
|
||||
If the host continues to support config-driven hot reload during migration, it must also preserve:
|
||||
|
||||
- channel-owned reload prefix behavior equivalent to current `configPrefixes` and `noopPrefixes`
|
||||
- gateway feature advertisement cleanup and re-registration
|
||||
- setup-flow and native-command registrations that depend on account-scoped runtime state
|
||||
|
||||
This advertisement handling does not replace callable RPC registration. If a migrated extension exposes callable gateway-style methods, those should still be re-registered through `capability.rpc`.
|
||||
|
||||
During migration, keep the current built-in onboarding fallback in place until host-owned setup surfaces cover bundled channels with parity.
|
||||
|
||||
Pilot rule:
|
||||
|
||||
- the fallback stays in place until `telegram` parity has been recorded for setup-adjacent host behavior, even if runtime messaging parity lands earlier
|
||||
|
||||
## Operator Policy
|
||||
|
||||
The host should support policy controls for:
|
||||
|
||||
- allowed extension ids
|
||||
- denied permissions
|
||||
- default permission grants for bundled extensions
|
||||
- allowed extension origins and provenance requirements
|
||||
- origin precedence and duplicate resolution
|
||||
- workspace extensions disabled by default unless explicitly allowed
|
||||
- bundled channel auto-enable rules tied to channel config
|
||||
- route exposure policy
|
||||
- network egress policy
|
||||
- backend selection policy
|
||||
- whether external extensions are permitted at all
|
||||
- SDK compatibility level and deprecation mode
|
||||
- prompt-mutation policy defaults
|
||||
- whether interactive extension-owned CLI and setup flows are allowed
|
||||
- whether extension-owned native command registration is allowed on specific providers
|
||||
- whether config-driven hot reload descriptors are honored or downgraded to restart-only behavior
|
||||
|
||||
## Observability
|
||||
|
||||
The host must emit structured telemetry for:
|
||||
|
||||
- activation timings
|
||||
- policy denials
|
||||
- contribution conflicts
|
||||
- route conflicts
|
||||
- backend registration and health
|
||||
- service start and stop
|
||||
- extension degradation and recovery
|
||||
- provenance warnings and origin overrides
|
||||
- state migration outcomes
|
||||
- compatibility-mode activation and deprecated SDK usage
|
||||
- setup flow phase transitions and fallback-path usage
|
||||
- config redaction or restore validation failures
|
||||
- reload descriptor application and gateway feature re-registration
|
||||
|
||||
## Immediate Implementation Work
|
||||
|
||||
1. Write the boundary cutover inventory for every current plugin-owned surface.
|
||||
2. Introduce an extension-host lifecycle state machine.
|
||||
3. Move route registration policy out of plugin internals into host-owned registries.
|
||||
4. Add a policy evaluator that understands advisory versus enforced permission modes.
|
||||
5. Add host-owned credential and per-extension state boundaries for extension services.
|
||||
6. Generalize backend registration into a host-managed `capability.runtime-backend` registry.
|
||||
7. Add host-owned subsystem runtime registries for embeddings, media understanding, and TTS instead of widening `registerProvider(...)`.
|
||||
8. Keep extension-backed search generic by publishing agent-visible search through tool contracts and using runtime-backend only for search backends consumed internally by the host or another subsystem.
|
||||
9. Add slot-backed provider management for context engines and other exclusive runtime providers.
|
||||
10. Preserve provenance, origin precedence, and current workspace and bundled enablement rules in host policy.
|
||||
11. Preserve prompt-mutation policy gates and add explicit state migration handling.
|
||||
12. Add explicit host registries and typed contracts for extension-owned hooks, channels, providers, tools, commands, CLI, setup flows, config surfaces, and status surfaces.
|
||||
13. Preserve config redaction-aware schema behavior and current reload or gateway feature contracts during migration.
|
||||
14. Record lifecycle parity for `thread-ownership` first and `telegram` second before broadening the compatibility bridges.
|
||||
@@ -0,0 +1,835 @@
|
||||
Temporary internal migration note: remove this document once the extension-host migration is complete.
|
||||
|
||||
# OpenClaw Kernel Event Pipeline Spec
|
||||
|
||||
Date: 2026-03-15
|
||||
|
||||
## Purpose
|
||||
|
||||
This document defines the canonical kernel event model, execution stages, handler classes, ordering, mutation rules, and veto semantics.
|
||||
|
||||
The goal is to replace today's mixed plugin hook behavior with one explicit runtime pipeline and a small set of execution modes that match current `main` behavior.
|
||||
|
||||
## TODOs
|
||||
|
||||
- [ ] Implement canonical event types and stage ordering in code.
|
||||
- [ ] Bridge current plugin hooks, internal hooks, and agent event streams into the pipeline.
|
||||
- [ ] Implement sync transcript-write stages with parity for current hot paths.
|
||||
- [ ] Record the legacy-to-canonical mapping table used by the first pilot migrations.
|
||||
- [ ] Record parity for `thread-ownership` first and `telegram` second before broader event migration.
|
||||
- [ ] Document which legacy hook sources are still bridged and which have been retired.
|
||||
- [ ] Add parity tests for veto, resolver, and sync-stage behavior.
|
||||
|
||||
## Implementation Status
|
||||
|
||||
Current status against this spec:
|
||||
|
||||
- no canonical event pipeline work has landed yet
|
||||
- only the prerequisites from earlier phases are underway
|
||||
|
||||
Relevant prerequisite work that has landed:
|
||||
|
||||
- an initial Phase 0 cutover inventory now exists in `src/extension-host/cutover-inventory.md`
|
||||
- the extension-host boundary now owns active registry state
|
||||
- registry activation now routes through `src/extension-host/activation.ts`
|
||||
- initial normalized extension schema types now exist
|
||||
- static consumers can now read host-owned resolved-extension data
|
||||
- config doc baseline generation now uses the same host-owned resolved-extension data path
|
||||
- channel, provider, HTTP-route, gateway-method, tool, CLI, service, command, context-engine, and hook registration normalization now has a host-owned helper boundary
|
||||
- loader cache key construction and registry cache control now have a host-owned helper boundary
|
||||
- loader provenance helpers now have a host-owned helper boundary
|
||||
- loader duplicate-order policy now has a host-owned helper boundary
|
||||
- loader alias-wired module loader creation now has a host-owned helper boundary
|
||||
- loader lazy runtime proxy creation now has a host-owned helper boundary
|
||||
- loader initial candidate planning and record creation now have a host-owned helper boundary
|
||||
- loader entry-path opening and module import now have a host-owned helper boundary
|
||||
- loader module-export resolution, config validation, and memory-slot load decisions now have a host-owned helper boundary
|
||||
- loader post-import planning and `register(...)` execution now have a host-owned helper boundary
|
||||
- loader per-candidate orchestration now has a host-owned helper boundary
|
||||
- loader top-level load orchestration now has a host-owned helper boundary
|
||||
- loader host process state now has a host-owned helper boundary
|
||||
- loader preflight and cache-hit setup now has a host-owned helper boundary
|
||||
- loader post-preflight pipeline composition now has a host-owned helper boundary
|
||||
- loader execution setup composition now has a host-owned helper boundary
|
||||
- loader discovery and manifest bootstrap now has a host-owned helper boundary
|
||||
- loader discovery policy outcomes now have a host-owned helper boundary
|
||||
- loader mutable activation state now has a host-owned helper boundary
|
||||
- loader session run and finalization composition now has a host-owned helper boundary
|
||||
- loader activation policy outcomes now have a host-owned helper boundary
|
||||
- loader record-state transitions now have a host-owned helper boundary and enforced loader lifecycle state machine, while still preserving compatibility `PluginRecord.status` values
|
||||
- loader finalization policy outcomes now have a host-owned helper boundary
|
||||
- loader final cache, readiness promotion, and activation finalization now has a host-owned helper boundary
|
||||
- low-risk channel, provider, gateway-method, HTTP-route, tool, CLI, service, command, context-engine, and hook compatibility writes now have a host-owned helper boundary in `src/extension-host/contributions/registry-writes.ts`
|
||||
- legacy internal-hook bridging and typed prompt-injection compatibility policy now have a host-owned helper boundary in `src/extension-host/compat/hook-compat.ts`
|
||||
- compatibility `OpenClawPluginApi` composition and logger shaping now have a host-owned helper boundary in `src/extension-host/compat/plugin-api.ts`
|
||||
- compatibility plugin-registry facade ownership now has a host-owned helper boundary in `src/extension-host/compat/plugin-registry.ts`
|
||||
- compatibility plugin-registry policy now has a host-owned helper boundary in `src/extension-host/compat/plugin-registry-compat.ts`
|
||||
- compatibility plugin-registry registration actions now have a host-owned helper boundary in `src/extension-host/compat/plugin-registry-registrations.ts`
|
||||
- host-owned runtime registry accessors now have a host-owned helper boundary in `src/extension-host/contributions/runtime-registry.ts`, and the channel, provider, tool, command, HTTP-route, gateway-method, CLI, and service slices now keep host-owned storage there with mirrored legacy compatibility views
|
||||
- plugin command registration, matching, execution, listing, native command-spec projection, and loader reload clearing now have a host-owned helper boundary in `src/extension-host/contributions/command-runtime.ts`
|
||||
- service startup, stop ordering, service-context creation, and failure logging now have a host-owned helper boundary in `src/extension-host/contributions/service-lifecycle.ts`
|
||||
- CLI duplicate detection, registrar invocation, and async failure logging now have a host-owned helper boundary in `src/extension-host/contributions/cli-lifecycle.ts`
|
||||
- gateway method-id aggregation, plugin diagnostic shaping, and extra-handler composition now have a host-owned helper boundary in `src/extension-host/contributions/gateway-methods.ts`
|
||||
- plugin tool resolution, conflict handling, optional-tool gating, and plugin-tool metadata tracking now have a host-owned helper boundary in `src/extension-host/contributions/tool-runtime.ts`
|
||||
- plugin provider projection from registry entries into runtime provider objects now have a host-owned helper boundary in `src/extension-host/contributions/provider-runtime.ts`
|
||||
- plugin provider discovery filtering, order grouping, and result normalization now have a host-owned helper boundary in `src/extension-host/contributions/provider-discovery.ts`
|
||||
- provider matching, auth-method selection, config-patch merging, and default-model application now have a host-owned helper boundary in `src/extension-host/contributions/provider-auth.ts`
|
||||
- provider onboarding option building, model-picker entry building, and provider-method choice resolution now have a host-owned helper boundary in `src/extension-host/contributions/provider-wizard.ts`
|
||||
- loaded-provider auth application, plugin-enable gating, auth-method execution, and post-auth default-model handling now have a host-owned helper boundary in `src/extension-host/contributions/provider-auth-flow.ts`
|
||||
- provider post-selection hook lookup and invocation now have a host-owned helper boundary in `src/extension-host/contributions/provider-model-selection.ts`
|
||||
|
||||
Why this matters for this spec:
|
||||
|
||||
- event work should land on top of a host-owned boundary and normalized contribution model rather than on top of more plugin-era runtime seams
|
||||
- the current implementation has deliberately not started canonical bridge or stage work before those earlier boundaries were in place, including the first loader-runtime, record-state, discovery-policy, activation-policy, finalization-policy, low-risk registry-write, hook-compat, plugin-api, plugin-registry, plugin-registry-compat, plugin-registry-registrations, runtime-registry storage and accessors, command-runtime, service-lifecycle, CLI-lifecycle, gateway-methods, tool-runtime, provider-runtime, provider-discovery, provider-auth, provider-wizard, provider-auth-flow, and provider-model-selection seams
|
||||
|
||||
## Design Goals
|
||||
|
||||
- every inbound and outbound path goes through one canonical pipeline
|
||||
- handler behavior is declared, not inferred
|
||||
- routing-affecting handlers are distinct from passive observers
|
||||
- ordering and merge rules are deterministic
|
||||
- extension failures are isolated and visible
|
||||
- sync transcript-write paths remain explicit rather than being hidden inside generic async stages
|
||||
- current plugin hooks, internal hooks, and agent event streams can be bridged into one model incrementally
|
||||
- the migration path for legacy event buses is explicit rather than accidental
|
||||
|
||||
## Sequencing Constraints
|
||||
|
||||
This pipeline is a migration target, not a prerequisite for every other host change.
|
||||
|
||||
Therefore:
|
||||
|
||||
- minimal SDK compatibility and host registry ownership should land before broad hook migration
|
||||
- the first event migration should prove parity for a small non-channel hook case and a channel case
|
||||
- do not require every event family to be implemented before pilot migrations can bridge the current hook set
|
||||
- do not leave legacy hook buses as undocumented permanent peers to the canonical pipeline
|
||||
|
||||
## Canonical Event Families
|
||||
|
||||
The kernel should emit typed event families instead of raw plugin hook names.
|
||||
|
||||
Recommended families:
|
||||
|
||||
- `runtime.started`
|
||||
- `runtime.stopping`
|
||||
- `gateway.starting`
|
||||
- `gateway.started`
|
||||
- `gateway.stopping`
|
||||
- `command.received`
|
||||
- `command.completed`
|
||||
- `account.started`
|
||||
- `account.stopped`
|
||||
- `ingress.received`
|
||||
- `ingress.normalized`
|
||||
- `ingress.claiming`
|
||||
- `routing.resolving`
|
||||
- `routing.resolved`
|
||||
- `session.starting`
|
||||
- `session.started`
|
||||
- `session.resetting`
|
||||
- `agent.starting`
|
||||
- `agent.model.resolving`
|
||||
- `agent.prompt.building`
|
||||
- `agent.llm.input`
|
||||
- `agent.llm.output`
|
||||
- `agent.tool.calling`
|
||||
- `agent.tool.called`
|
||||
- `transcript.tool-result.persisting`
|
||||
- `transcript.message.writing`
|
||||
- `compaction.before`
|
||||
- `compaction.after`
|
||||
- `agent.completed`
|
||||
- `egress.preparing`
|
||||
- `egress.sending`
|
||||
- `egress.sent`
|
||||
- `egress.cancelled`
|
||||
- `egress.failed`
|
||||
- `interaction.received`
|
||||
- `subagent.spawning`
|
||||
- `subagent.spawned`
|
||||
- `subagent.delivery.resolving`
|
||||
- `subagent.delivery.resolved`
|
||||
- `subagent.completed`
|
||||
|
||||
These families intentionally cover the behavior currently spread across `src/plugins/hooks.ts:1`, `src/hooks/internal-hooks.ts:13`, `src/infra/agent-events.ts:3`, and channel monitors.
|
||||
|
||||
`ingress.claiming` exists to absorb behavior that is currently tempting to model as plugin-specific hooks or direct dispatch short-circuits:
|
||||
|
||||
- bound conversation ownership
|
||||
- first-claim-wins plugin or extension routing
|
||||
- future route-claim or veto decisions that must run before command or agent dispatch
|
||||
|
||||
## Canonical Event Envelope
|
||||
|
||||
Every event should carry:
|
||||
|
||||
- `eventId`
|
||||
- `family`
|
||||
- `occurredAt`
|
||||
- `workspaceId`
|
||||
- `agentId`
|
||||
- `sessionId`
|
||||
- `accountRef`
|
||||
- `conversationRef`
|
||||
- `threadRef`
|
||||
- `messageRef`
|
||||
- `sourceContributionId`
|
||||
- `correlationId`
|
||||
- `payload`
|
||||
- `metadata`
|
||||
- `providerMetadata`
|
||||
- `hotPath`
|
||||
|
||||
The event envelope is immutable. Mutation happens through stage outputs, not by mutating the event object in place.
|
||||
|
||||
## Handler Classes
|
||||
|
||||
Each handler contribution must declare exactly one class:
|
||||
|
||||
- `observer`
|
||||
- `augmenter`
|
||||
- `mutator`
|
||||
- `veto`
|
||||
- `resolver`
|
||||
|
||||
### `observer`
|
||||
|
||||
Side effects only. No runtime decision output.
|
||||
|
||||
### `augmenter`
|
||||
|
||||
May attach additional context for downstream stages.
|
||||
|
||||
Examples:
|
||||
|
||||
- prompt context injection
|
||||
- memory recall summaries
|
||||
- diagnostics enrichment
|
||||
|
||||
### `mutator`
|
||||
|
||||
May modify a typed working object for the current pipeline stage.
|
||||
|
||||
Examples:
|
||||
|
||||
- prompt build additions
|
||||
- model override
|
||||
- tool call decoration
|
||||
|
||||
### `veto`
|
||||
|
||||
May cancel a downstream action with a typed reason.
|
||||
|
||||
Examples today:
|
||||
|
||||
- send cancellation in `extensions/thread-ownership/index.ts:63`
|
||||
|
||||
### `resolver`
|
||||
|
||||
May produce a selected target or route decision.
|
||||
|
||||
Examples today:
|
||||
|
||||
- subagent delivery target selection in `extensions/discord/src/subagent-hooks.ts:103`
|
||||
|
||||
Only `veto` and `resolver` handlers may influence routing or delivery decisions.
|
||||
|
||||
`ingress.claiming` is the first concrete place where a resolver-like route claim is expected to matter during migration.
|
||||
|
||||
First-cut parity rule for `ingress.claiming`:
|
||||
|
||||
- claim handlers run sequentially in deterministic order
|
||||
- the first successful claim wins ownership of the inbound turn
|
||||
- passive observers still run in their own stages instead of being skipped accidentally
|
||||
- the migration bridge may target a single extension when a host-owned binding already resolved the owner
|
||||
|
||||
## Execution Modes
|
||||
|
||||
The semantic handler class is not enough by itself.
|
||||
|
||||
Each stage must also declare one of three execution modes:
|
||||
|
||||
- `parallel`
|
||||
For read-only observers and low-risk side effects.
|
||||
- `sequential`
|
||||
For merge, mutation, veto, and resolver stages.
|
||||
- `sync-sequential`
|
||||
For transcript and persistence hot paths where async handlers are not allowed.
|
||||
|
||||
This mirrors current `main` behavior in `src/plugins/hooks.ts:199`, `src/plugins/hooks.ts:226`, `src/plugins/hooks.ts:465`, and `src/plugins/hooks.ts:528`.
|
||||
|
||||
## Deterministic Ordering
|
||||
|
||||
Within a stage, handlers run in this order:
|
||||
|
||||
1. explicit priority descending
|
||||
2. extension id ascending
|
||||
3. contribution id ascending
|
||||
|
||||
Priority is optional. Ties must resolve deterministically.
|
||||
|
||||
## Stage Execution Model
|
||||
|
||||
Every pipeline stage declares:
|
||||
|
||||
- which handler classes are allowed
|
||||
- execution mode
|
||||
- whether handlers run in parallel or sequentially
|
||||
- how outputs are merged
|
||||
- whether errors fail open or fail closed
|
||||
|
||||
## Gateway And Command Pipeline
|
||||
|
||||
### Stage: `gateway.starting`, `gateway.started`, `gateway.stopping`
|
||||
|
||||
Allowed handler classes:
|
||||
|
||||
- `observer`
|
||||
|
||||
Execution mode:
|
||||
|
||||
- `parallel`
|
||||
|
||||
Purpose:
|
||||
|
||||
- lifecycle telemetry
|
||||
- startup and shutdown side effects
|
||||
|
||||
### Stage: `command.received`, `command.completed`
|
||||
|
||||
Allowed handler classes:
|
||||
|
||||
- `observer`
|
||||
- `augmenter`
|
||||
|
||||
Execution mode:
|
||||
|
||||
- `sequential`
|
||||
|
||||
Purpose:
|
||||
|
||||
- command audit
|
||||
- command lifecycle integration
|
||||
- operator-visible side effects
|
||||
- preserve source-surface metadata for chat commands, native commands, and host CLI invocations when those flows are bridged into canonical command events
|
||||
|
||||
Bridge requirement:
|
||||
|
||||
- the current internal hook bus in `src/hooks/internal-hooks.ts:13`
|
||||
- and the current agent event stream in `src/infra/agent-events.ts:3`
|
||||
|
||||
must be mapped deliberately into canonical families during migration.
|
||||
|
||||
Acceptable end states are:
|
||||
|
||||
- they become compatibility sources that emit canonical events
|
||||
- or they are fully retired after parity is reached
|
||||
|
||||
An undocumented permanent fourth event system is not acceptable.
|
||||
|
||||
## Ingress Pipeline
|
||||
|
||||
### Stage 1: `ingress.received`
|
||||
|
||||
Input:
|
||||
|
||||
- raw adapter payload
|
||||
|
||||
Allowed handler classes:
|
||||
|
||||
- `observer`
|
||||
|
||||
Execution mode:
|
||||
|
||||
- `parallel`
|
||||
|
||||
Purpose:
|
||||
|
||||
- telemetry
|
||||
- raw audit
|
||||
- diagnostics
|
||||
|
||||
### Stage 2: `ingress.normalized`
|
||||
|
||||
Input:
|
||||
|
||||
- normalized inbound envelope from `adapter.runtime.decodeIngress`
|
||||
|
||||
Allowed handler classes:
|
||||
|
||||
- `observer`
|
||||
- `augmenter`
|
||||
- `mutator`
|
||||
|
||||
Execution mode:
|
||||
|
||||
- `sequential`
|
||||
|
||||
Purpose:
|
||||
|
||||
- add normalized metadata
|
||||
- enrich source/account context
|
||||
- attach pre-routing annotations
|
||||
|
||||
This stage must not choose a route.
|
||||
|
||||
### Stage 3: `routing.resolving`
|
||||
|
||||
Allowed handler classes:
|
||||
|
||||
- `augmenter`
|
||||
- `resolver`
|
||||
- `veto`
|
||||
|
||||
Execution mode:
|
||||
|
||||
- `sequential`
|
||||
|
||||
Purpose:
|
||||
|
||||
- route lookup
|
||||
- ownership checks
|
||||
- subagent delivery target resolution
|
||||
- policy application before route finalization
|
||||
|
||||
Merge rules:
|
||||
|
||||
- `resolver` outputs produce candidate route decisions
|
||||
- highest-precedence valid decision wins
|
||||
- `veto` may cancel route selection
|
||||
|
||||
### Stage 4: `routing.resolved`
|
||||
|
||||
Allowed handler classes:
|
||||
|
||||
- `observer`
|
||||
- `augmenter`
|
||||
|
||||
Execution mode:
|
||||
|
||||
- `sequential`
|
||||
|
||||
Purpose:
|
||||
|
||||
- emit resolved route metadata
|
||||
- enrich downstream session context
|
||||
|
||||
### Stage 5: `session.starting`
|
||||
|
||||
Allowed handler classes:
|
||||
|
||||
- `observer`
|
||||
- `augmenter`
|
||||
- `mutator`
|
||||
|
||||
Execution mode:
|
||||
|
||||
- `sequential`
|
||||
|
||||
Purpose:
|
||||
|
||||
- bind session context
|
||||
- attach memory lookup keys
|
||||
- prepare session-scoped metadata
|
||||
|
||||
### Stage 6: `session.started`
|
||||
|
||||
Allowed handler classes:
|
||||
|
||||
- `observer`
|
||||
|
||||
Execution mode:
|
||||
|
||||
- `parallel`
|
||||
|
||||
Purpose:
|
||||
|
||||
- fire lifecycle observers
|
||||
|
||||
### Stage 7: `agent.starting`
|
||||
|
||||
Allowed handler classes:
|
||||
|
||||
- `observer`
|
||||
- `augmenter`
|
||||
|
||||
Execution mode:
|
||||
|
||||
- `sequential`
|
||||
|
||||
Purpose:
|
||||
|
||||
- last pre-run annotations
|
||||
|
||||
## Prompt And Model Pipeline
|
||||
|
||||
### Stage: `agent.model.resolving`
|
||||
|
||||
Allowed handler classes:
|
||||
|
||||
- `mutator`
|
||||
|
||||
Execution mode:
|
||||
|
||||
- `sequential`
|
||||
|
||||
Merge rules:
|
||||
|
||||
- first defined model override wins
|
||||
- first defined provider override wins
|
||||
|
||||
This mirrors current precedence in `src/plugins/hooks.ts:117`.
|
||||
|
||||
### Stage: `agent.prompt.building`
|
||||
|
||||
Allowed handler classes:
|
||||
|
||||
- `augmenter`
|
||||
- `mutator`
|
||||
|
||||
Execution mode:
|
||||
|
||||
- `sequential`
|
||||
|
||||
Merge rules:
|
||||
|
||||
- static system guidance composes in declared order
|
||||
- ephemeral prompt additions compose in declared order
|
||||
- direct system prompt replacement is allowed only for explicitly trusted mutators
|
||||
|
||||
This replaces the ambiguous overlap between `before_prompt_build` and legacy `before_agent_start` in `src/plugins/types.ts:422`.
|
||||
|
||||
### Stage: `agent.llm.input`
|
||||
|
||||
Allowed handler classes:
|
||||
|
||||
- `observer`
|
||||
- `augmenter`
|
||||
|
||||
Execution mode:
|
||||
|
||||
- `sequential`
|
||||
|
||||
Purpose:
|
||||
|
||||
- provider-call audit
|
||||
- input usage and prompt metadata capture
|
||||
|
||||
### Stage: `agent.llm.output`
|
||||
|
||||
Allowed handler classes:
|
||||
|
||||
- `observer`
|
||||
- `augmenter`
|
||||
|
||||
Execution mode:
|
||||
|
||||
- `sequential`
|
||||
|
||||
Purpose:
|
||||
|
||||
- provider response audit
|
||||
- usage capture
|
||||
- output enrichment
|
||||
|
||||
## Tool Pipeline
|
||||
|
||||
### Stage: `agent.tool.calling`
|
||||
|
||||
Allowed handler classes:
|
||||
|
||||
- `observer`
|
||||
- `augmenter`
|
||||
- `mutator`
|
||||
- `veto`
|
||||
|
||||
Execution mode:
|
||||
|
||||
- `sequential`
|
||||
|
||||
Purpose:
|
||||
|
||||
- tool policy checks
|
||||
- argument normalization
|
||||
- tool-call audit
|
||||
|
||||
### Stage: `agent.tool.called`
|
||||
|
||||
Allowed handler classes:
|
||||
|
||||
- `observer`
|
||||
- `augmenter`
|
||||
|
||||
Execution mode:
|
||||
|
||||
- `sequential`
|
||||
|
||||
Purpose:
|
||||
|
||||
- result indexing
|
||||
- memory capture
|
||||
- diagnostics
|
||||
|
||||
### Stage: `agent.completed`
|
||||
|
||||
Allowed handler classes:
|
||||
|
||||
- `observer`
|
||||
- `augmenter`
|
||||
|
||||
Execution mode:
|
||||
|
||||
- `sequential`
|
||||
|
||||
Purpose:
|
||||
|
||||
- end-of-run capture
|
||||
- automatic memory storage
|
||||
- metrics
|
||||
|
||||
## Persistence Pipeline
|
||||
|
||||
### Stage: `transcript.tool-result.persisting`
|
||||
|
||||
Allowed handler classes:
|
||||
|
||||
- `mutator`
|
||||
|
||||
Execution mode:
|
||||
|
||||
- `sync-sequential`
|
||||
|
||||
Purpose:
|
||||
|
||||
- mutate the tool-result message that will be appended to transcripts
|
||||
|
||||
Rules:
|
||||
|
||||
- async handlers are invalid
|
||||
- handlers run in deterministic priority order
|
||||
- each handler sees the previous handler's output
|
||||
|
||||
This is the explicit replacement for today's sync-only `tool_result_persist` hook in `src/plugins/hooks.ts:465`.
|
||||
|
||||
### Stage: `transcript.message.writing`
|
||||
|
||||
Allowed handler classes:
|
||||
|
||||
- `mutator`
|
||||
- `veto`
|
||||
|
||||
Execution mode:
|
||||
|
||||
- `sync-sequential`
|
||||
|
||||
Purpose:
|
||||
|
||||
- final transcript message mutation
|
||||
- transcript write suppression when explicitly requested
|
||||
|
||||
Rules:
|
||||
|
||||
- async handlers are invalid
|
||||
- successful veto decisions are terminal
|
||||
- mutation happens before the final write
|
||||
|
||||
This is the explicit replacement for today's sync-only `before_message_write` hook in `src/plugins/hooks.ts:528`.
|
||||
|
||||
## Compaction And Reset Pipeline
|
||||
|
||||
Canonical stages:
|
||||
|
||||
- `compaction.before`
|
||||
- `compaction.after`
|
||||
- `session.resetting`
|
||||
|
||||
## Egress Pipeline
|
||||
|
||||
### Stage 1: `egress.preparing`
|
||||
|
||||
Input:
|
||||
|
||||
- normalized outbound envelope
|
||||
|
||||
Allowed handler classes:
|
||||
|
||||
- `observer`
|
||||
- `augmenter`
|
||||
- `mutator`
|
||||
- `veto`
|
||||
- `resolver`
|
||||
|
||||
Execution mode:
|
||||
|
||||
- `sequential`
|
||||
|
||||
Purpose:
|
||||
|
||||
- choose provider or account when not explicit
|
||||
- attach send metadata
|
||||
- enforce ownership or safety policy
|
||||
|
||||
This stage replaces today’s mixed send hooks and route checks.
|
||||
|
||||
### Stage 2: `egress.sending`
|
||||
|
||||
Allowed handler classes:
|
||||
|
||||
- `observer`
|
||||
|
||||
Execution mode:
|
||||
|
||||
- `parallel`
|
||||
|
||||
Purpose:
|
||||
|
||||
- telemetry and audit before transport send
|
||||
|
||||
### Stage 3: `egress.sent`, `egress.cancelled`, `egress.failed`
|
||||
|
||||
Allowed handler classes:
|
||||
|
||||
- `observer`
|
||||
- `augmenter`
|
||||
|
||||
Execution mode:
|
||||
|
||||
- `sequential`
|
||||
|
||||
Purpose:
|
||||
|
||||
- post-send side effects
|
||||
- delivery-state indexing
|
||||
|
||||
## Interaction Pipeline
|
||||
|
||||
Interaction events should not be routed through message hooks.
|
||||
|
||||
Canonical stages:
|
||||
|
||||
- `interaction.received`
|
||||
- `interaction.resolved`
|
||||
- `interaction.completed`
|
||||
|
||||
These handle slash commands, button presses, modal submissions, and similar surfaces.
|
||||
|
||||
## Subagent Pipeline
|
||||
|
||||
The current hook set already proves this needs explicit treatment:
|
||||
|
||||
- `subagent_spawning`
|
||||
- `subagent_delivery_target`
|
||||
- `subagent_spawned`
|
||||
- `subagent_ended`
|
||||
|
||||
The canonical form should be:
|
||||
|
||||
- `subagent.spawning`
|
||||
- `subagent.spawned`
|
||||
- `subagent.delivery.resolving`
|
||||
- `subagent.delivery.resolved`
|
||||
- `subagent.completed`
|
||||
|
||||
Resolver semantics:
|
||||
|
||||
- multiple candidates may be proposed
|
||||
- explicit target beats inferred target
|
||||
- otherwise highest-ranked valid candidate wins
|
||||
|
||||
## Merge Rules
|
||||
|
||||
### Observer
|
||||
|
||||
No merge output.
|
||||
|
||||
### Augmenter
|
||||
|
||||
Produces additive metadata only.
|
||||
|
||||
Conflicts merge by:
|
||||
|
||||
- key append for list-like fields
|
||||
- last-writer-wins only for fields explicitly marked replaceable
|
||||
|
||||
### Mutator
|
||||
|
||||
Produces typed patch objects.
|
||||
|
||||
Rules:
|
||||
|
||||
- patch schema is stage-specific
|
||||
- patches apply in deterministic order
|
||||
- later patches see earlier outputs
|
||||
|
||||
### Veto
|
||||
|
||||
Produces:
|
||||
|
||||
- `allow`
|
||||
- `cancel`
|
||||
|
||||
Rules:
|
||||
|
||||
- one `cancel` is terminal if the stage is fail-closed
|
||||
- fail-open stages may ignore veto errors but not successful veto decisions
|
||||
|
||||
### Resolver
|
||||
|
||||
Produces candidate selections.
|
||||
|
||||
Rules:
|
||||
|
||||
- explicit target selectors win
|
||||
- otherwise rank, policy, and deterministic tie-breakers apply
|
||||
|
||||
## Error Handling
|
||||
|
||||
Per-stage error policy must be explicit.
|
||||
|
||||
Recommended defaults:
|
||||
|
||||
- telemetry and observer stages fail open
|
||||
- routing and send veto stages fail open unless the contribution declares `failClosed`
|
||||
- credential or auth mutation stages fail closed
|
||||
- backend selection stages fail closed when no valid provider remains
|
||||
- sync transcript stages fail open on handler failure but must still reject accidental async handlers
|
||||
|
||||
## Legacy Hook Mapping
|
||||
|
||||
Current hook names map approximately like this:
|
||||
|
||||
- `before_model_resolve` -> `agent.model.resolving`
|
||||
- `before_prompt_build` -> `agent.prompt.building`
|
||||
- `before_agent_start` -> split between `agent.model.resolving` and `agent.prompt.building`
|
||||
- `llm_input` -> `agent.llm.input`
|
||||
- `llm_output` -> `agent.llm.output`
|
||||
- `message_received` -> `ingress.normalized`
|
||||
- `message_sending` -> `egress.preparing`
|
||||
- `message_sent` -> `egress.sent`
|
||||
- `before_tool_call` -> `agent.tool.calling`
|
||||
- `after_tool_call` -> `agent.tool.called`
|
||||
- `tool_result_persist` -> `transcript.tool-result.persisting`
|
||||
- `before_message_write` -> `transcript.message.writing`
|
||||
- `before_compaction` -> `compaction.before`
|
||||
- `after_compaction` -> `compaction.after`
|
||||
- `before_reset` -> `session.resetting`
|
||||
- `gateway_start` -> `gateway.started`
|
||||
- `gateway_stop` -> `gateway.stopping`
|
||||
- `subagent_delivery_target` -> `subagent.delivery.resolving`
|
||||
|
||||
First pilot focus:
|
||||
|
||||
- `thread-ownership` should validate `message_received` and `message_sending` migration into canonical ingress and egress stages
|
||||
- `telegram` should validate that channel-path runtime behavior can participate in canonical events without reintroducing plugin-shaped kernel seams
|
||||
|
||||
## Immediate Implementation Work
|
||||
|
||||
1. Add canonical event and stage types to the kernel.
|
||||
2. Build a stage runner with explicit handler-class validation.
|
||||
3. Add typed patch and veto result contracts per stage, including sync-sequential stages.
|
||||
4. Bridge legacy plugin hooks, internal hooks, and agent events into canonical stages in the extension host only.
|
||||
5. Record the exact legacy-to-canonical mapping used by `thread-ownership`.
|
||||
6. Record the exact legacy-to-canonical mapping used by `telegram`.
|
||||
7. Refactor one channel and one non-channel extension through the new pipeline before broader migration.
|
||||
8. Decide and document the retirement plan for any legacy event bus that remains after parity is achieved.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,11 +1,3 @@
|
||||
---
|
||||
title: "Auth Credential Semantics"
|
||||
summary: "Canonical credential eligibility and resolution semantics for auth profiles"
|
||||
read_when:
|
||||
- Working on auth profile resolution or credential routing
|
||||
- Debugging model auth failures or profile order
|
||||
---
|
||||
|
||||
# Auth Credential Semantics
|
||||
|
||||
This document defines the canonical credential eligibility and resolution semantics used across:
|
||||
|
||||
@@ -700,7 +700,7 @@ openclaw system event --mode now --text "Next heartbeat: check battery."
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Nothing runs"
|
||||
### “Nothing runs”
|
||||
|
||||
- Check cron is enabled: `cron.enabled` and `OPENCLAW_SKIP_CRON`.
|
||||
- Check the Gateway is running continuously (cron runs inside the Gateway process).
|
||||
|
||||
@@ -38,7 +38,6 @@ Every request must include the hook token. Prefer headers:
|
||||
- `Authorization: Bearer <token>` (recommended)
|
||||
- `x-openclaw-token: <token>`
|
||||
- Query-string tokens are rejected (`?token=...` returns `400`).
|
||||
- Treat `hooks.token` holders as full-trust callers for the hook ingress surface on that gateway. Hook payload content is still untrusted, but this is not a separate non-owner auth boundary.
|
||||
|
||||
## Endpoints
|
||||
|
||||
@@ -206,7 +205,6 @@ curl -X POST http://127.0.0.1:18789/hooks/gmail \
|
||||
|
||||
- Keep hook endpoints behind loopback, tailnet, or trusted reverse proxy.
|
||||
- Use a dedicated hook token; do not reuse gateway auth tokens.
|
||||
- Prefer a dedicated hook agent with strict `tools.profile` and sandboxing so hook ingress has a narrower blast radius.
|
||||
- Repeated auth failures are rate-limited per client address to slow brute-force attempts.
|
||||
- If you use multi-agent routing, set `hooks.allowedAgentIds` to limit explicit `agentId` selection.
|
||||
- Keep `hooks.allowRequestSessionKey=false` unless you require caller-selected sessions.
|
||||
|
||||
@@ -20,21 +20,11 @@ OpenClaw supports Brave Search API as a `web_search` provider.
|
||||
|
||||
```json5
|
||||
{
|
||||
plugins: {
|
||||
entries: {
|
||||
brave: {
|
||||
config: {
|
||||
webSearch: {
|
||||
apiKey: "BRAVE_API_KEY_HERE",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
tools: {
|
||||
web: {
|
||||
search: {
|
||||
provider: "brave",
|
||||
apiKey: "BRAVE_API_KEY_HERE",
|
||||
maxResults: 5,
|
||||
timeoutSeconds: 30,
|
||||
},
|
||||
@@ -43,9 +33,6 @@ OpenClaw supports Brave Search API as a `web_search` provider.
|
||||
}
|
||||
```
|
||||
|
||||
Provider-specific Brave search settings now live under `plugins.entries.brave.config.webSearch.*`.
|
||||
Legacy `tools.web.search.apiKey` still loads through the compatibility shim, but it is no longer the canonical config path.
|
||||
|
||||
## Tool parameters
|
||||
|
||||
| Parameter | Description |
|
||||
|
||||
@@ -126,7 +126,7 @@ launchctl load ~/Library/LaunchAgents/com.user.poke-messages.plist
|
||||
|
||||
## Onboarding
|
||||
|
||||
BlueBubbles is available in interactive onboarding:
|
||||
BlueBubbles is available in the interactive setup wizard:
|
||||
|
||||
```
|
||||
openclaw onboard
|
||||
|
||||
@@ -96,10 +96,8 @@ You will need to create a new application with a bot, add the bot to your server
|
||||
Your Discord bot token is a secret (like a password). Set it on the machine running OpenClaw before messaging your agent.
|
||||
|
||||
```bash
|
||||
export DISCORD_BOT_TOKEN="YOUR_BOT_TOKEN"
|
||||
openclaw config set channels.discord.token --ref-provider default --ref-source env --ref-id DISCORD_BOT_TOKEN --dry-run
|
||||
openclaw config set channels.discord.token --ref-provider default --ref-source env --ref-id DISCORD_BOT_TOKEN
|
||||
openclaw config set channels.discord.enabled true --strict-json
|
||||
openclaw config set channels.discord.token '"YOUR_BOT_TOKEN"' --json
|
||||
openclaw config set channels.discord.enabled true --json
|
||||
openclaw gateway
|
||||
```
|
||||
|
||||
@@ -123,11 +121,7 @@ openclaw gateway
|
||||
channels: {
|
||||
discord: {
|
||||
enabled: true,
|
||||
token: {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "DISCORD_BOT_TOKEN",
|
||||
},
|
||||
token: "YOUR_BOT_TOKEN",
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -139,7 +133,7 @@ openclaw gateway
|
||||
DISCORD_BOT_TOKEN=...
|
||||
```
|
||||
|
||||
Plaintext `token` values are supported. SecretRef values are also supported for `channels.discord.token` across env/file/exec providers. See [Secrets Management](/gateway/secrets).
|
||||
SecretRef values are also supported for `channels.discord.token` (env/file/exec providers). See [Secrets Management](/gateway/secrets).
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
@@ -174,7 +168,7 @@ openclaw pairing approve discord <CODE>
|
||||
|
||||
<Note>
|
||||
Token resolution is account-aware. Config token values win over env fallback. `DISCORD_BOT_TOKEN` is only used for the default account.
|
||||
For advanced outbound calls (message tool/channel actions), an explicit per-call `token` is used for that call. This applies to send and read/probe-style actions (for example read/search/fetch/thread/pins/permissions). Account policy/retry settings still come from the selected account in the active runtime snapshot.
|
||||
For advanced outbound calls (message tool/channel actions), an explicit per-call `token` is used for that call. Account policy/retry settings still come from the selected account in the active runtime snapshot.
|
||||
</Note>
|
||||
|
||||
## Recommended: Set up a guild workspace
|
||||
|
||||
@@ -30,9 +30,9 @@ openclaw plugins install @openclaw/feishu
|
||||
|
||||
There are two ways to add the Feishu channel:
|
||||
|
||||
### Method 1: onboarding (recommended)
|
||||
### Method 1: onboarding wizard (recommended)
|
||||
|
||||
If you just installed OpenClaw, run onboarding:
|
||||
If you just installed OpenClaw, run the wizard:
|
||||
|
||||
```bash
|
||||
openclaw onboard
|
||||
@@ -711,7 +711,7 @@ Key options:
|
||||
- ✅ Images
|
||||
- ✅ Files
|
||||
- ✅ Audio
|
||||
- ✅ Video/media
|
||||
- ✅ Video
|
||||
- ✅ Stickers
|
||||
|
||||
### Send
|
||||
@@ -720,28 +720,4 @@ Key options:
|
||||
- ✅ Images
|
||||
- ✅ Files
|
||||
- ✅ Audio
|
||||
- ✅ Video/media
|
||||
- ✅ Interactive cards
|
||||
- ⚠️ Rich text (post-style formatting and cards, not arbitrary Feishu authoring features)
|
||||
|
||||
### Threads and replies
|
||||
|
||||
- ✅ Inline replies
|
||||
- ✅ Topic-thread replies where Feishu exposes `reply_in_thread`
|
||||
- ✅ Media replies stay thread-aware when replying to a thread/topic message
|
||||
|
||||
## Runtime action surface
|
||||
|
||||
Feishu currently exposes these runtime actions:
|
||||
|
||||
- `send`
|
||||
- `read`
|
||||
- `edit`
|
||||
- `thread-reply`
|
||||
- `pin`
|
||||
- `list-pins`
|
||||
- `unpin`
|
||||
- `member-info`
|
||||
- `channel-info`
|
||||
- `channel-list`
|
||||
- `react` and `reactions` when reactions are enabled in config
|
||||
- ⚠️ Rich text (partial support)
|
||||
|
||||
@@ -11,7 +11,7 @@ Goal: let Clawd sit in WhatsApp groups, wake up only when pinged, and keep that
|
||||
|
||||
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).
|
||||
|
||||
## Current implementation (2025-12-03)
|
||||
## What’s implemented (2025-12-03)
|
||||
|
||||
- Activation modes: `mention` (default) or `always`. `mention` requires a ping (real WhatsApp @-mentions via `mentionedJids`, safe regex patterns, or the bot’s E.164 anywhere in the text). `always` wakes the agent on every message but it should reply only when it can add meaningful value; otherwise it returns the silent token `NO_REPLY`. Defaults can be set in config (`channels.whatsapp.groups`) and overridden per group via `/activation`. When `channels.whatsapp.groups` is set, it also acts as a group allowlist (include `"*"` to allow all).
|
||||
- Group policy: `channels.whatsapp.groupPolicy` controls whether group messages are accepted (`open|disabled|allowlist`). `allowlist` uses `channels.whatsapp.groupAllowFrom` (fallback: explicit `channels.whatsapp.allowFrom`). Default is `allowlist` (blocked until you add senders).
|
||||
|
||||
@@ -31,7 +31,7 @@ Local checkout (when running from a git repo):
|
||||
openclaw plugins install ./extensions/matrix
|
||||
```
|
||||
|
||||
If you choose Matrix during setup and a git checkout is detected,
|
||||
If you choose Matrix during configure/onboarding and a git checkout is detected,
|
||||
OpenClaw will offer the local install path automatically.
|
||||
|
||||
Details: [Plugins](/tools/plugin)
|
||||
@@ -72,7 +72,7 @@ Details: [Plugins](/tools/plugin)
|
||||
- If both are set, config takes precedence.
|
||||
- With access token: user ID is fetched automatically via `/whoami`.
|
||||
- When set, `channels.matrix.userId` should be the full Matrix ID (example: `@bot:example.org`).
|
||||
5. Restart the gateway (or finish setup).
|
||||
5. Restart the gateway (or finish onboarding).
|
||||
6. Start a DM with the bot or invite it to a room from any Matrix client
|
||||
(Element, Beeper, etc.; see [https://matrix.org/ecosystem/clients/](https://matrix.org/ecosystem/clients/)). Beeper requires E2EE,
|
||||
so set `channels.matrix.encryption: true` and verify the device.
|
||||
|
||||
@@ -28,7 +28,7 @@ Local checkout (when running from a git repo):
|
||||
openclaw plugins install ./extensions/mattermost
|
||||
```
|
||||
|
||||
If you choose Mattermost during setup and a git checkout is detected,
|
||||
If you choose Mattermost during configure/onboarding and a git checkout is detected,
|
||||
OpenClaw will offer the local install path automatically.
|
||||
|
||||
Details: [Plugins](/tools/plugin)
|
||||
@@ -191,35 +191,6 @@ OpenClaw resolves them **user-first**:
|
||||
|
||||
If you need deterministic behavior, always use the explicit prefixes (`user:<id>` / `channel:<id>`).
|
||||
|
||||
## DM channel retry
|
||||
|
||||
When OpenClaw sends to a Mattermost DM target and needs to resolve the direct channel first, it
|
||||
retries transient direct-channel creation failures by default.
|
||||
|
||||
Use `channels.mattermost.dmChannelRetry` to tune that behavior globally for the Mattermost plugin,
|
||||
or `channels.mattermost.accounts.<id>.dmChannelRetry` for one account.
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
mattermost: {
|
||||
dmChannelRetry: {
|
||||
maxRetries: 3,
|
||||
initialDelayMs: 1000,
|
||||
maxDelayMs: 10000,
|
||||
timeoutMs: 30000,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- This applies only to DM channel creation (`/api/v4/channels/direct`), not every Mattermost API call.
|
||||
- Retries apply to transient failures such as rate limits, 5xx responses, and network or timeout errors.
|
||||
- 4xx client errors other than `429` are treated as permanent and are not retried.
|
||||
|
||||
## Reactions (message tool)
|
||||
|
||||
- Use `message action=react` with `channel=mattermost`.
|
||||
|
||||
@@ -33,7 +33,7 @@ Local checkout (when running from a git repo):
|
||||
openclaw plugins install ./extensions/msteams
|
||||
```
|
||||
|
||||
If you choose Teams during setup and a git checkout is detected,
|
||||
If you choose Teams during configure/onboarding and a git checkout is detected,
|
||||
OpenClaw will offer the local install path automatically.
|
||||
|
||||
Details: [Plugins](/tools/plugin)
|
||||
|
||||
@@ -25,7 +25,7 @@ Local checkout (when running from a git repo):
|
||||
openclaw plugins install ./extensions/nextcloud-talk
|
||||
```
|
||||
|
||||
If you choose Nextcloud Talk during setup and a git checkout is detected,
|
||||
If you choose Nextcloud Talk during configure/onboarding and a git checkout is detected,
|
||||
OpenClaw will offer the local install path automatically.
|
||||
|
||||
Details: [Plugins](/tools/plugin)
|
||||
@@ -43,7 +43,7 @@ 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)
|
||||
5. Restart the gateway (or finish setup).
|
||||
5. Restart the gateway (or finish onboarding).
|
||||
|
||||
Minimal config:
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ Nostr is a decentralized protocol for social networking. This channel enables Op
|
||||
|
||||
### Onboarding (recommended)
|
||||
|
||||
- Onboarding (`openclaw onboard`) and `openclaw channels add` list optional channel plugins.
|
||||
- The onboarding wizard (`openclaw onboard`) and `openclaw channels add` list optional channel plugins.
|
||||
- Selecting Nostr prompts you to install the plugin on demand.
|
||||
|
||||
Install defaults:
|
||||
@@ -40,15 +40,6 @@ openclaw plugins install --link <path-to-openclaw>/extensions/nostr
|
||||
|
||||
Restart the Gateway after installing or enabling plugins.
|
||||
|
||||
### Non-interactive setup
|
||||
|
||||
```bash
|
||||
openclaw channels add --channel nostr --private-key "$NOSTR_PRIVATE_KEY"
|
||||
openclaw channels add --channel nostr --private-key "$NOSTR_PRIVATE_KEY" --relay-urls "wss://relay.damus.io,wss://relay.primal.net"
|
||||
```
|
||||
|
||||
Use `--use-env` to keep `NOSTR_PRIVATE_KEY` in the environment instead of storing the key in config.
|
||||
|
||||
## Quick setup
|
||||
|
||||
1. Generate a Nostr keypair (if needed):
|
||||
|
||||
@@ -27,17 +27,13 @@ Details: [Plugins](/tools/plugin)
|
||||
## Quick setup
|
||||
|
||||
1. Install and enable the Synology Chat plugin.
|
||||
- `openclaw onboard` now shows Synology Chat in the same channel setup list as `openclaw channels add`.
|
||||
- Non-interactive setup: `openclaw channels add --channel synology-chat --token <token> --url <incoming-webhook-url>`
|
||||
2. In Synology Chat integrations:
|
||||
- Create an incoming webhook and copy its URL.
|
||||
- Create an outgoing webhook with your secret token.
|
||||
3. Point the outgoing webhook URL to your OpenClaw gateway:
|
||||
- `https://gateway-host/webhook/synology` by default.
|
||||
- Or your custom `channels.synology-chat.webhookPath`.
|
||||
4. Finish setup in OpenClaw.
|
||||
- Guided: `openclaw onboard`
|
||||
- Direct: `openclaw channels add --channel synology-chat --token <token> --url <incoming-webhook-url>`
|
||||
4. Configure `channels.synology-chat` in OpenClaw.
|
||||
5. Restart gateway and send a DM to the Synology Chat bot.
|
||||
|
||||
Minimal config:
|
||||
|
||||
@@ -115,7 +115,7 @@ Token resolution order is account-aware. In practice, config values win over env
|
||||
|
||||
`channels.telegram.allowFrom` accepts numeric Telegram user IDs. `telegram:` / `tg:` prefixes are accepted and normalized.
|
||||
`dmPolicy: "allowlist"` with empty `allowFrom` blocks all DMs and is rejected by config validation.
|
||||
Onboarding accepts `@username` input and resolves it to numeric IDs.
|
||||
The onboarding wizard accepts `@username` input and resolves it to numeric IDs.
|
||||
If you upgraded and your config contains `@username` allowlist entries, run `openclaw doctor --fix` to resolve them (best-effort; requires a Telegram bot token).
|
||||
If you previously relied on pairing-store allowlist files, `openclaw doctor --fix` can recover entries into `channels.telegram.allowFrom` in allowlist flows (for example when `dmPolicy: "allowlist"` has no explicit IDs yet).
|
||||
|
||||
|
||||
@@ -255,7 +255,7 @@ openclaw doctor
|
||||
openclaw channels status --probe
|
||||
```
|
||||
|
||||
### Bot does not respond to messages
|
||||
### Bot doesn't respond to messages
|
||||
|
||||
**Check access control:** Ensure your user ID is in `allowFrom`, or temporarily remove
|
||||
`allowFrom` and set `allowedRoles: ["all"]` to test.
|
||||
|
||||
@@ -76,7 +76,7 @@ openclaw pairing approve whatsapp <CODE>
|
||||
</Steps>
|
||||
|
||||
<Note>
|
||||
OpenClaw recommends running WhatsApp on a separate number when possible. (The channel metadata and setup flow are optimized for that setup, but personal-number setups are also supported.)
|
||||
OpenClaw recommends running WhatsApp on a separate number when possible. (The channel metadata and onboarding flow are optimized for that setup, but personal-number setups are also supported.)
|
||||
</Note>
|
||||
|
||||
## Deployment patterns
|
||||
|
||||
@@ -14,7 +14,7 @@ Status: experimental. DMs are supported. The [Capabilities](#capabilities) secti
|
||||
Zalo ships as a plugin and is not bundled with the core install.
|
||||
|
||||
- Install via CLI: `openclaw plugins install @openclaw/zalo`
|
||||
- Or select **Zalo** during setup and confirm the install prompt
|
||||
- Or select **Zalo** during onboarding and confirm the install prompt
|
||||
- Details: [Plugins](/tools/plugin)
|
||||
|
||||
## Quick setup (beginner)
|
||||
@@ -22,11 +22,11 @@ Zalo ships as a plugin and is not bundled with the core install.
|
||||
1. Install the Zalo plugin:
|
||||
- From a source checkout: `openclaw plugins install ./extensions/zalo`
|
||||
- From npm (if published): `openclaw plugins install @openclaw/zalo`
|
||||
- Or pick **Zalo** in setup and confirm the install prompt
|
||||
- Or pick **Zalo** in onboarding and confirm the install prompt
|
||||
2. Set the token:
|
||||
- Env: `ZALO_BOT_TOKEN=...`
|
||||
- Or config: `channels.zalo.accounts.default.botToken: "..."`.
|
||||
3. Restart the gateway (or finish setup).
|
||||
3. Restart the gateway (or finish onboarding).
|
||||
4. DM access is pairing by default; approve the pairing code on first contact.
|
||||
|
||||
Minimal config:
|
||||
|
||||
@@ -41,7 +41,7 @@ No external `zca`/`openzca` CLI binary is required.
|
||||
}
|
||||
```
|
||||
|
||||
4. Restart the Gateway (or finish setup).
|
||||
4. Restart the Gateway (or finish onboarding).
|
||||
5. DM access defaults to pairing; approve the pairing code on first contact.
|
||||
|
||||
## What it is
|
||||
@@ -74,7 +74,7 @@ openclaw directory groups list --channel zalouser --query "work"
|
||||
|
||||
`channels.zalouser.dmPolicy` supports: `pairing | allowlist | open | disabled` (default: `pairing`).
|
||||
|
||||
`channels.zalouser.allowFrom` accepts user IDs or names. During setup, names are resolved to IDs using the plugin's in-process contact lookup.
|
||||
`channels.zalouser.allowFrom` accepts user IDs or names. During onboarding, names are resolved to IDs using the plugin's in-process contact lookup.
|
||||
|
||||
Approve via:
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ title: "acp"
|
||||
|
||||
# acp
|
||||
|
||||
Run the [Agent Client Protocol (ACP)](https://agentclientprotocol.com/) bridge that talks to an OpenClaw Gateway.
|
||||
Run the [Agent Client Protocol (ACP)](https://agentclientprotocol.com/) bridge that talks to a OpenClaw Gateway.
|
||||
|
||||
This command speaks ACP over stdio for IDEs and forwards prompts to the Gateway
|
||||
over WebSocket. It keeps ACP sessions mapped to Gateway session keys.
|
||||
@@ -102,7 +102,7 @@ Permission model (client debug mode):
|
||||
## How to use this
|
||||
|
||||
Use ACP when an IDE (or other client) speaks Agent Client Protocol and you want
|
||||
it to drive an OpenClaw Gateway session.
|
||||
it to drive a OpenClaw Gateway session.
|
||||
|
||||
1. Ensure the Gateway is running (local or remote).
|
||||
2. Configure the Gateway target (config or flags).
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
---
|
||||
summary: "CLI reference for `openclaw browser` (profiles, tabs, actions, Chrome MCP, and CDP)"
|
||||
summary: "CLI reference for `openclaw browser` (profiles, tabs, actions, extension relay)"
|
||||
read_when:
|
||||
- You use `openclaw browser` and want examples for common tasks
|
||||
- You want to control a browser running on another machine via a node host
|
||||
- You want to attach to your local signed-in Chrome via Chrome MCP
|
||||
- You want to use the Chrome extension relay (attach/detach via toolbar button)
|
||||
title: "browser"
|
||||
---
|
||||
|
||||
@@ -14,6 +14,7 @@ Manage OpenClaw’s browser control server and run browser actions (tabs, snapsh
|
||||
Related:
|
||||
|
||||
- Browser tool + API: [Browser tool](/tools/browser)
|
||||
- Chrome extension relay: [Chrome extension](/tools/chrome-extension)
|
||||
|
||||
## Common flags
|
||||
|
||||
@@ -36,14 +37,13 @@ openclaw browser --browser-profile openclaw snapshot
|
||||
|
||||
Profiles are named browser routing configs. In practice:
|
||||
|
||||
- `openclaw`: launches or attaches to a dedicated OpenClaw-managed Chrome instance (isolated user data dir).
|
||||
- `openclaw`: launches/attaches to a dedicated OpenClaw-managed Chrome instance (isolated user data dir).
|
||||
- `user`: controls your existing signed-in Chrome session via Chrome DevTools MCP.
|
||||
- custom CDP profiles: point at a local or remote CDP endpoint.
|
||||
- `chrome-relay`: controls your existing Chrome tab(s) via the Chrome extension relay.
|
||||
|
||||
```bash
|
||||
openclaw browser profiles
|
||||
openclaw browser create-profile --name work --color "#FF5A36"
|
||||
openclaw browser create-profile --name chrome-live --driver existing-session
|
||||
openclaw browser delete-profile --name work
|
||||
```
|
||||
|
||||
@@ -84,18 +84,20 @@ openclaw browser click <ref>
|
||||
openclaw browser type <ref> "hello"
|
||||
```
|
||||
|
||||
## Existing Chrome via MCP
|
||||
## Chrome extension relay (attach via toolbar button)
|
||||
|
||||
Use the built-in `user` profile, or create your own `existing-session` profile:
|
||||
This mode lets the agent control an existing Chrome tab that you attach manually (it does not auto-attach).
|
||||
|
||||
Install the unpacked extension to a stable path:
|
||||
|
||||
```bash
|
||||
openclaw browser --browser-profile user tabs
|
||||
openclaw browser create-profile --name chrome-live --driver existing-session
|
||||
openclaw browser create-profile --name brave-live --driver existing-session --user-data-dir "~/Library/Application Support/BraveSoftware/Brave-Browser"
|
||||
openclaw browser --browser-profile chrome-live tabs
|
||||
openclaw browser extension install
|
||||
openclaw browser extension path
|
||||
```
|
||||
|
||||
This path is host-only. For Docker, headless servers, Browserless, or other remote setups, use a CDP profile instead.
|
||||
Then Chrome → `chrome://extensions` → enable “Developer mode” → “Load unpacked” → select the printed folder.
|
||||
|
||||
Full guide: [Chrome extension](/tools/chrome-extension)
|
||||
|
||||
## Remote browser control (node host proxy)
|
||||
|
||||
|
||||
@@ -30,11 +30,10 @@ openclaw channels logs --channel all
|
||||
|
||||
```bash
|
||||
openclaw channels add --channel telegram --token <bot-token>
|
||||
openclaw channels add --channel nostr --private-key "$NOSTR_PRIVATE_KEY"
|
||||
openclaw channels remove --channel telegram --delete
|
||||
```
|
||||
|
||||
Tip: `openclaw channels add --help` shows per-channel flags (token, private key, app token, signal-cli paths, etc).
|
||||
Tip: `openclaw channels add --help` shows per-channel flags (token, app token, signal-cli paths, etc).
|
||||
|
||||
When you run `openclaw channels add` without flags, the interactive wizard can prompt:
|
||||
|
||||
|
||||
@@ -7,9 +7,9 @@ title: "config"
|
||||
|
||||
# `openclaw config`
|
||||
|
||||
Config helpers for non-interactive edits in `openclaw.json`: get/set/unset/validate
|
||||
values by path and print the active config file. Run without a subcommand to
|
||||
open the configure wizard (same as `openclaw configure`).
|
||||
Config helpers: get/set/unset/validate values by path and print the active
|
||||
config file. Run without a subcommand to open
|
||||
the configure wizard (same as `openclaw configure`).
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -19,10 +19,7 @@ openclaw config get browser.executablePath
|
||||
openclaw config set browser.executablePath "/usr/bin/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 channels.discord.token --ref-provider default --ref-source env --ref-id DISCORD_BOT_TOKEN
|
||||
openclaw config set secrets.providers.vaultfile --provider-source file --provider-path /etc/openclaw/secrets.json --provider-mode json
|
||||
openclaw config unset plugins.entries.brave.config.webSearch.apiKey
|
||||
openclaw config set channels.discord.token --ref-provider default --ref-source env --ref-id DISCORD_BOT_TOKEN --dry-run
|
||||
openclaw config unset tools.web.search.apiKey
|
||||
openclaw config validate
|
||||
openclaw config validate --json
|
||||
```
|
||||
@@ -54,230 +51,6 @@ openclaw config set gateway.port 19001 --strict-json
|
||||
openclaw config set channels.whatsapp.groups '["*"]' --strict-json
|
||||
```
|
||||
|
||||
## `config set` modes
|
||||
|
||||
`openclaw config set` supports four assignment styles:
|
||||
|
||||
1. Value mode: `openclaw config set <path> <value>`
|
||||
2. SecretRef builder mode:
|
||||
|
||||
```bash
|
||||
openclaw config set channels.discord.token \
|
||||
--ref-provider default \
|
||||
--ref-source env \
|
||||
--ref-id DISCORD_BOT_TOKEN
|
||||
```
|
||||
|
||||
3. Provider builder mode (`secrets.providers.<alias>` path only):
|
||||
|
||||
```bash
|
||||
openclaw config set secrets.providers.vault \
|
||||
--provider-source exec \
|
||||
--provider-command /usr/local/bin/openclaw-vault \
|
||||
--provider-arg read \
|
||||
--provider-arg openai/api-key \
|
||||
--provider-timeout-ms 5000
|
||||
```
|
||||
|
||||
4. Batch mode (`--batch-json` or `--batch-file`):
|
||||
|
||||
```bash
|
||||
openclaw config set --batch-json '[
|
||||
{
|
||||
"path": "secrets.providers.default",
|
||||
"provider": { "source": "env" }
|
||||
},
|
||||
{
|
||||
"path": "channels.discord.token",
|
||||
"ref": { "source": "env", "provider": "default", "id": "DISCORD_BOT_TOKEN" }
|
||||
}
|
||||
]'
|
||||
```
|
||||
|
||||
```bash
|
||||
openclaw config set --batch-file ./config-set.batch.json --dry-run
|
||||
```
|
||||
|
||||
Batch parsing always uses the batch payload (`--batch-json`/`--batch-file`) as the source of truth.
|
||||
`--strict-json` / `--json` do not change batch parsing behavior.
|
||||
|
||||
JSON path/value mode remains supported for both SecretRefs and providers:
|
||||
|
||||
```bash
|
||||
openclaw config set channels.discord.token \
|
||||
'{"source":"env","provider":"default","id":"DISCORD_BOT_TOKEN"}' \
|
||||
--strict-json
|
||||
|
||||
openclaw config set secrets.providers.vaultfile \
|
||||
'{"source":"file","path":"/etc/openclaw/secrets.json","mode":"json"}' \
|
||||
--strict-json
|
||||
```
|
||||
|
||||
## Provider Builder Flags
|
||||
|
||||
Provider builder targets must use `secrets.providers.<alias>` as the path.
|
||||
|
||||
Common flags:
|
||||
|
||||
- `--provider-source <env|file|exec>`
|
||||
- `--provider-timeout-ms <ms>` (`file`, `exec`)
|
||||
|
||||
Env provider (`--provider-source env`):
|
||||
|
||||
- `--provider-allowlist <ENV_VAR>` (repeatable)
|
||||
|
||||
File provider (`--provider-source file`):
|
||||
|
||||
- `--provider-path <path>` (required)
|
||||
- `--provider-mode <singleValue|json>`
|
||||
- `--provider-max-bytes <bytes>`
|
||||
|
||||
Exec provider (`--provider-source exec`):
|
||||
|
||||
- `--provider-command <path>` (required)
|
||||
- `--provider-arg <arg>` (repeatable)
|
||||
- `--provider-no-output-timeout-ms <ms>`
|
||||
- `--provider-max-output-bytes <bytes>`
|
||||
- `--provider-json-only`
|
||||
- `--provider-env <KEY=VALUE>` (repeatable)
|
||||
- `--provider-pass-env <ENV_VAR>` (repeatable)
|
||||
- `--provider-trusted-dir <path>` (repeatable)
|
||||
- `--provider-allow-insecure-path`
|
||||
- `--provider-allow-symlink-command`
|
||||
|
||||
Hardened exec provider example:
|
||||
|
||||
```bash
|
||||
openclaw config set secrets.providers.vault \
|
||||
--provider-source exec \
|
||||
--provider-command /usr/local/bin/openclaw-vault \
|
||||
--provider-arg read \
|
||||
--provider-arg openai/api-key \
|
||||
--provider-json-only \
|
||||
--provider-pass-env VAULT_TOKEN \
|
||||
--provider-trusted-dir /usr/local/bin \
|
||||
--provider-timeout-ms 5000
|
||||
```
|
||||
|
||||
## Dry run
|
||||
|
||||
Use `--dry-run` to validate changes without writing `openclaw.json`.
|
||||
|
||||
```bash
|
||||
openclaw config set channels.discord.token \
|
||||
--ref-provider default \
|
||||
--ref-source env \
|
||||
--ref-id DISCORD_BOT_TOKEN \
|
||||
--dry-run
|
||||
|
||||
openclaw config set channels.discord.token \
|
||||
--ref-provider default \
|
||||
--ref-source env \
|
||||
--ref-id DISCORD_BOT_TOKEN \
|
||||
--dry-run \
|
||||
--json
|
||||
|
||||
openclaw config set channels.discord.token \
|
||||
--ref-provider vault \
|
||||
--ref-source exec \
|
||||
--ref-id discord/token \
|
||||
--dry-run \
|
||||
--allow-exec
|
||||
```
|
||||
|
||||
Dry-run behavior:
|
||||
|
||||
- Builder mode: runs SecretRef resolvability checks for changed refs/providers.
|
||||
- JSON mode (`--strict-json`, `--json`, or batch mode): runs schema validation plus SecretRef resolvability checks.
|
||||
- Exec SecretRef checks are skipped by default during dry-run to avoid command side effects.
|
||||
- Use `--allow-exec` with `--dry-run` to opt in to exec SecretRef checks (this may execute provider commands).
|
||||
- `--allow-exec` is dry-run only and errors if used without `--dry-run`.
|
||||
|
||||
`--dry-run --json` prints a machine-readable report:
|
||||
|
||||
- `ok`: whether dry-run passed
|
||||
- `operations`: number of assignments evaluated
|
||||
- `checks`: whether schema/resolvability checks ran
|
||||
- `checks.resolvabilityComplete`: whether resolvability checks ran to completion (false when exec refs are skipped)
|
||||
- `refsChecked`: number of refs actually resolved during dry-run
|
||||
- `skippedExecRefs`: number of exec refs skipped because `--allow-exec` was not set
|
||||
- `errors`: structured schema/resolvability failures when `ok=false`
|
||||
|
||||
### JSON Output Shape
|
||||
|
||||
```json5
|
||||
{
|
||||
ok: boolean,
|
||||
operations: number,
|
||||
configPath: string,
|
||||
inputModes: ["value" | "json" | "builder", ...],
|
||||
checks: {
|
||||
schema: boolean,
|
||||
resolvability: boolean,
|
||||
resolvabilityComplete: boolean,
|
||||
},
|
||||
refsChecked: number,
|
||||
skippedExecRefs: number,
|
||||
errors?: [
|
||||
{
|
||||
kind: "schema" | "resolvability",
|
||||
message: string,
|
||||
ref?: string, // present for resolvability errors
|
||||
},
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
Success example:
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"operations": 1,
|
||||
"configPath": "~/.openclaw/openclaw.json",
|
||||
"inputModes": ["builder"],
|
||||
"checks": {
|
||||
"schema": false,
|
||||
"resolvability": true,
|
||||
"resolvabilityComplete": true
|
||||
},
|
||||
"refsChecked": 1,
|
||||
"skippedExecRefs": 0
|
||||
}
|
||||
```
|
||||
|
||||
Failure example:
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": false,
|
||||
"operations": 1,
|
||||
"configPath": "~/.openclaw/openclaw.json",
|
||||
"inputModes": ["builder"],
|
||||
"checks": {
|
||||
"schema": false,
|
||||
"resolvability": true,
|
||||
"resolvabilityComplete": true
|
||||
},
|
||||
"refsChecked": 1,
|
||||
"skippedExecRefs": 0,
|
||||
"errors": [
|
||||
{
|
||||
"kind": "resolvability",
|
||||
"message": "Error: Environment variable \"MISSING_TEST_SECRET\" is not set.",
|
||||
"ref": "env:default:MISSING_TEST_SECRET"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
If dry-run fails:
|
||||
|
||||
- `config schema validation failed`: your post-change config shape is invalid; fix path/value or provider/ref object shape.
|
||||
- `SecretRef assignment(s) could not be resolved`: referenced provider/ref currently cannot resolve (missing env var, invalid file pointer, exec provider failure, or provider/source mismatch).
|
||||
- `Dry run note: skipped <n> exec SecretRef resolvability check(s)`: dry-run skipped exec refs; rerun with `--allow-exec` if you need exec resolvability validation.
|
||||
- For batch mode, fix failing entries and rerun `--dry-run` before writing.
|
||||
|
||||
## Subcommands
|
||||
|
||||
- `config file`: Print the active config file path (resolved from `OPENCLAW_CONFIG_PATH` or default location).
|
||||
|
||||
@@ -34,15 +34,13 @@ openclaw daemon uninstall
|
||||
|
||||
## Common options
|
||||
|
||||
- `status`: `--url`, `--token`, `--password`, `--timeout`, `--no-probe`, `--require-rpc`, `--deep`, `--json`
|
||||
- `status`: `--url`, `--token`, `--password`, `--timeout`, `--no-probe`, `--deep`, `--json`
|
||||
- `install`: `--port`, `--runtime <node|bun>`, `--token`, `--force`, `--json`
|
||||
- lifecycle (`uninstall|start|stop|restart`): `--json`
|
||||
|
||||
Notes:
|
||||
|
||||
- `status` resolves configured auth SecretRefs for probe auth when possible.
|
||||
- If a required auth SecretRef is unresolved in this command path, `daemon status --json` reports `rpc.authWarning` when probe connectivity/auth fails; pass `--token`/`--password` explicitly or resolve the secret source first.
|
||||
- If the probe succeeds, unresolved auth-ref warnings are suppressed to avoid false positives.
|
||||
- On Linux systemd installs, `status` token-drift checks include both `Environment=` and `EnvironmentFile=` unit sources.
|
||||
- When token auth requires a token and `gateway.auth.token` is SecretRef-managed, `install` validates that the SecretRef is resolvable but does not persist the resolved token into service environment metadata.
|
||||
- If token auth requires a token and the configured token SecretRef is unresolved, install fails closed.
|
||||
|
||||
@@ -40,7 +40,7 @@ openclaw message send --channel slack --target user:U012ABCDEF --message "hello"
|
||||
- Zalo (plugin): user id (Bot API)
|
||||
- Zalo Personal / `zalouser` (plugin): thread id (DM/group) from `zca` (`me`, `friend list`, `group list`)
|
||||
|
||||
## Self ("me")
|
||||
## Self (“me”)
|
||||
|
||||
```bash
|
||||
openclaw directory self --channel zalouser
|
||||
|
||||
@@ -10,6 +10,6 @@ title: "docs"
|
||||
Search the live docs index.
|
||||
|
||||
```bash
|
||||
openclaw docs browser existing-session
|
||||
openclaw docs browser extension
|
||||
openclaw docs sandbox allowHostControl
|
||||
```
|
||||
|
||||
@@ -31,9 +31,6 @@ Notes:
|
||||
- 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 includes a memory-search readiness check and can recommend `openclaw configure --section model` when embedding credentials are missing.
|
||||
- If sandbox mode is enabled but Docker is unavailable, doctor reports a high-signal warning with remediation (`install Docker` or `openclaw config set agents.defaults.sandbox.mode off`).
|
||||
- If `gateway.auth.token`/`gateway.auth.password` are SecretRef-managed and unavailable in the current command path, doctor reports a read-only warning and does not write plaintext fallback credentials.
|
||||
- If channel SecretRef inspection fails in a fix path, doctor continues and reports a warning instead of exiting early.
|
||||
- Telegram `allowFrom` username auto-resolution (`doctor --fix`) requires a resolvable Telegram token in the current command path. If token inspection is unavailable, doctor reports a warning and skips auto-resolution for that pass.
|
||||
|
||||
## macOS: `launchctl` env overrides
|
||||
|
||||
|
||||
@@ -111,8 +111,7 @@ Options:
|
||||
Notes:
|
||||
|
||||
- `gateway status` resolves configured auth SecretRefs for probe auth when possible.
|
||||
- If a required auth SecretRef is unresolved in this command path, `gateway status --json` reports `rpc.authWarning` when probe connectivity/auth fails; pass `--token`/`--password` explicitly or resolve the secret source first.
|
||||
- If the probe succeeds, unresolved auth-ref warnings are suppressed to avoid false positives.
|
||||
- If a required auth SecretRef is unresolved in this command path, probe auth can fail; pass `--token`/`--password` explicitly or resolve the secret source first.
|
||||
- Use `--require-rpc` in scripts and automation when a listening service is not enough and you need the Gateway RPC itself to be healthy.
|
||||
- On Linux systemd installs, service auth drift checks read both `Environment=` and `EnvironmentFile=` values from the unit (including `%h`, quoted paths, multiple files, and optional `-` files).
|
||||
|
||||
|
||||
@@ -88,7 +88,7 @@ OpenClaw uses a lobster palette for CLI output.
|
||||
- `error` (#E23D2D): errors, failures.
|
||||
- `muted` (#8B7F77): de-emphasis, metadata.
|
||||
|
||||
Palette source of truth: `src/terminal/palette.ts` (the “lobster palette”).
|
||||
Palette source of truth: `src/terminal/palette.ts` (aka “lobster seam”).
|
||||
|
||||
## Command tree
|
||||
|
||||
@@ -101,8 +101,6 @@ openclaw [--dev] [--profile <name>] <command>
|
||||
get
|
||||
set
|
||||
unset
|
||||
file
|
||||
validate
|
||||
completion
|
||||
doctor
|
||||
dashboard
|
||||
@@ -276,18 +274,17 @@ Note: plugins can add additional top-level commands (for example `openclaw voice
|
||||
## Secrets
|
||||
|
||||
- `openclaw secrets reload` — re-resolve refs and atomically swap the runtime snapshot.
|
||||
- `openclaw secrets audit` — scan for plaintext residues, unresolved refs, and precedence drift (`--allow-exec` to execute exec providers during audit).
|
||||
- `openclaw secrets configure` — interactive helper for provider setup + SecretRef mapping + preflight/apply (`--allow-exec` to execute exec providers during preflight and exec-containing apply flows).
|
||||
- `openclaw secrets apply --from <plan.json>` — apply a previously generated plan (`--dry-run` supported; use `--allow-exec` to permit exec providers in dry-run and exec-containing write plans).
|
||||
- `openclaw secrets audit` — scan for plaintext residues, unresolved refs, and precedence drift.
|
||||
- `openclaw secrets configure` — interactive helper for provider setup + SecretRef mapping + preflight/apply.
|
||||
- `openclaw secrets apply --from <plan.json>` — apply a previously generated plan (`--dry-run` supported).
|
||||
|
||||
## Plugins
|
||||
|
||||
Manage extensions and their config:
|
||||
|
||||
- `openclaw plugins list` — discover plugins (use `--json` for machine output).
|
||||
- `openclaw plugins inspect <id>` — show details for a plugin (`info` is an alias).
|
||||
- `openclaw plugins install <path|.tgz|npm-spec|plugin@marketplace>` — install a plugin (or add a plugin path to `plugins.load.paths`).
|
||||
- `openclaw plugins marketplace list <marketplace>` — list marketplace entries before install.
|
||||
- `openclaw plugins info <id>` — show details for a plugin.
|
||||
- `openclaw plugins install <path|.tgz|npm-spec>` — install a plugin (or add a plugin path to `plugins.load.paths`).
|
||||
- `openclaw plugins enable <id>` / `disable <id>` — toggle `plugins.entries.<id>.enabled`.
|
||||
- `openclaw plugins doctor` — report plugin load errors.
|
||||
|
||||
@@ -320,22 +317,22 @@ Initialize config + workspace.
|
||||
Options:
|
||||
|
||||
- `--workspace <dir>`: agent workspace path (default `~/.openclaw/workspace`).
|
||||
- `--wizard`: run onboarding.
|
||||
- `--non-interactive`: run onboarding without prompts.
|
||||
- `--mode <local|remote>`: onboard mode.
|
||||
- `--wizard`: run the onboarding wizard.
|
||||
- `--non-interactive`: run wizard without prompts.
|
||||
- `--mode <local|remote>`: wizard mode.
|
||||
- `--remote-url <url>`: remote Gateway URL.
|
||||
- `--remote-token <token>`: remote Gateway token.
|
||||
|
||||
Onboarding auto-runs when any onboarding flags are present (`--non-interactive`, `--mode`, `--remote-url`, `--remote-token`).
|
||||
Wizard auto-runs when any wizard flags are present (`--non-interactive`, `--mode`, `--remote-url`, `--remote-token`).
|
||||
|
||||
### `onboard`
|
||||
|
||||
Interactive onboarding for gateway, workspace, and skills.
|
||||
Interactive wizard to set up gateway, workspace, and skills.
|
||||
|
||||
Options:
|
||||
|
||||
- `--workspace <dir>`
|
||||
- `--reset` (reset config + credentials + sessions before onboarding)
|
||||
- `--reset` (reset config + credentials + sessions before wizard)
|
||||
- `--reset-scope <config|config+creds+sessions|full>` (default `config+creds+sessions`; use `full` to also remove workspace)
|
||||
- `--non-interactive`
|
||||
- `--mode <local|remote>`
|
||||
@@ -395,15 +392,7 @@ subcommand launches the wizard.
|
||||
Subcommands:
|
||||
|
||||
- `config get <path>`: print a config value (dot/bracket path).
|
||||
- `config set`: supports four assignment modes:
|
||||
- value mode: `config set <path> <value>` (JSON5-or-string parsing)
|
||||
- SecretRef builder mode: `config set <path> --ref-provider <provider> --ref-source <source> --ref-id <id>`
|
||||
- provider builder mode: `config set secrets.providers.<alias> --provider-source <env|file|exec> ...`
|
||||
- batch mode: `config set --batch-json '<json>'` or `config set --batch-file <path>`
|
||||
- `config set --dry-run`: validate assignments without writing `openclaw.json` (exec SecretRef checks are skipped by default).
|
||||
- `config set --allow-exec --dry-run`: opt in to exec SecretRef dry-run checks (may execute provider commands).
|
||||
- `config set --dry-run --json`: emit machine-readable dry-run output (checks + completeness signal, operations, refs checked/skipped, errors).
|
||||
- `config set --strict-json`: require JSON5 parsing for path/value input. `--json` remains a legacy alias for strict parsing outside dry-run output mode.
|
||||
- `config set <path> <value>`: set a value (JSON5 or raw string).
|
||||
- `config unset <path>`: remove a value.
|
||||
- `config file`: print the active config file path.
|
||||
- `config validate`: validate the current config against the schema without starting the gateway.
|
||||
@@ -687,7 +676,7 @@ Surfaces:
|
||||
Notes:
|
||||
|
||||
- Data comes directly from provider usage endpoints (no estimates).
|
||||
- Providers: Anthropic, GitHub Copilot, OpenAI Codex OAuth, plus Gemini CLI via the bundled `google` plugin and Antigravity where configured.
|
||||
- Providers: Anthropic, GitHub Copilot, OpenAI Codex OAuth, plus Gemini CLI/Antigravity when those provider plugins are enabled.
|
||||
- If no matching credentials exist, usage is hidden.
|
||||
- Details: see [Usage tracking](/concepts/usage-tracking).
|
||||
|
||||
@@ -794,7 +783,6 @@ Notes:
|
||||
- `gateway status` supports `--no-probe`, `--deep`, `--require-rpc`, and `--json` for scripting.
|
||||
- `gateway status` also surfaces legacy or extra gateway services when it can detect them (`--deep` adds system-level scans). Profile-named OpenClaw services are treated as first-class and aren't flagged as "extra".
|
||||
- `gateway status` prints which config path the CLI uses vs which config the service likely uses (service env), plus the resolved probe target URL.
|
||||
- If gateway auth SecretRefs are unresolved in the current command path, `gateway status --json` reports `rpc.authWarning` only when probe connectivity/auth fails (warnings are suppressed when probe succeeds).
|
||||
- On Linux systemd installs, status token-drift checks include both `Environment=` and `EnvironmentFile=` unit sources.
|
||||
- `gateway install|uninstall|start|stop|restart` support `--json` for scripting (default output stays human-friendly).
|
||||
- `gateway install` defaults to Node runtime; bun is **not recommended** (WhatsApp/Telegram bugs).
|
||||
|
||||
@@ -50,16 +50,6 @@ Name lookup:
|
||||
- `--dry-run`
|
||||
- `--verbose`
|
||||
|
||||
## SecretRef behavior
|
||||
|
||||
- `openclaw message` resolves supported channel SecretRefs before running the selected action.
|
||||
- Resolution is scoped to the active action target when possible:
|
||||
- channel-scoped when `--channel` is set (or inferred from prefixed targets like `discord:...`)
|
||||
- account-scoped when `--account` is set (channel globals + selected account surfaces)
|
||||
- when `--account` is omitted, OpenClaw does not force a `default` account SecretRef scope
|
||||
- Unresolved SecretRefs on unrelated channels do not block a targeted message action.
|
||||
- If the selected channel/account SecretRef is unresolved, the command fails closed for that action.
|
||||
|
||||
## Actions
|
||||
|
||||
### Core
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
summary: "CLI reference for `openclaw onboard` (interactive onboarding)"
|
||||
summary: "CLI reference for `openclaw onboard` (interactive onboarding wizard)"
|
||||
read_when:
|
||||
- You want guided setup for gateway, workspace, auth, channels, and skills
|
||||
title: "onboard"
|
||||
@@ -7,13 +7,13 @@ title: "onboard"
|
||||
|
||||
# `openclaw onboard`
|
||||
|
||||
Interactive onboarding for local or remote Gateway setup.
|
||||
Interactive onboarding wizard (local or remote Gateway setup).
|
||||
|
||||
## Related guides
|
||||
|
||||
- CLI onboarding hub: [Onboarding (CLI)](/start/wizard)
|
||||
- CLI onboarding hub: [Onboarding Wizard (CLI)](/start/wizard)
|
||||
- Onboarding overview: [Onboarding Overview](/start/onboarding-overview)
|
||||
- CLI onboarding reference: [CLI Setup Reference](/start/wizard-cli-reference)
|
||||
- CLI onboarding reference: [CLI Onboarding Reference](/start/wizard-cli-reference)
|
||||
- CLI automation: [CLI Automation](/start/wizard-cli-automation)
|
||||
- macOS onboarding: [Onboarding (macOS App)](/start/onboarding)
|
||||
|
||||
@@ -140,7 +140,7 @@ Flow notes:
|
||||
|
||||
- `quickstart`: minimal prompts, auto-generates a gateway token.
|
||||
- `manual`: full prompts for port/bind/auth (alias of `advanced`).
|
||||
- Local onboarding DM scope behavior: [CLI Setup Reference](/start/wizard-cli-reference#outputs-and-internals).
|
||||
- Local onboarding DM scope behavior: [CLI Onboarding Reference](/start/wizard-cli-reference#outputs-and-internals).
|
||||
- Fastest first chat: `openclaw dashboard` (Control UI, no channel setup).
|
||||
- Custom Provider: connect any OpenAI or Anthropic compatible endpoint,
|
||||
including hosted providers not listed. Use Unknown to auto-detect.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
summary: "CLI reference for `openclaw plugins` (list, install, marketplace, uninstall, enable/disable, doctor)"
|
||||
summary: "CLI reference for `openclaw plugins` (list, install, uninstall, enable/disable, doctor)"
|
||||
read_when:
|
||||
- You want to install or manage Gateway plugins or compatible bundles
|
||||
- You want to debug plugin load failures
|
||||
@@ -21,15 +21,13 @@ Related:
|
||||
|
||||
```bash
|
||||
openclaw plugins list
|
||||
openclaw plugins install <path-or-spec>
|
||||
openclaw plugins inspect <id>
|
||||
openclaw plugins info <id>
|
||||
openclaw plugins enable <id>
|
||||
openclaw plugins disable <id>
|
||||
openclaw plugins uninstall <id>
|
||||
openclaw plugins doctor
|
||||
openclaw plugins update <id>
|
||||
openclaw plugins update --all
|
||||
openclaw plugins marketplace list <marketplace>
|
||||
```
|
||||
|
||||
Bundled plugins ship with OpenClaw but start disabled. Use `plugins enable` to
|
||||
@@ -48,8 +46,6 @@ capabilities.
|
||||
```bash
|
||||
openclaw plugins install <path-or-spec>
|
||||
openclaw plugins install <npm-spec> --pin
|
||||
openclaw plugins install <plugin>@<marketplace>
|
||||
openclaw plugins install <plugin> --marketplace <marketplace>
|
||||
```
|
||||
|
||||
Security note: treat plugin installs like running code. Prefer pinned versions.
|
||||
@@ -69,31 +65,6 @@ name, use an explicit scoped spec (for example `@scope/diffs`).
|
||||
|
||||
Supported archives: `.zip`, `.tgz`, `.tar.gz`, `.tar`.
|
||||
|
||||
Claude marketplace installs are also supported.
|
||||
|
||||
Use `plugin@marketplace` shorthand when the marketplace name exists in Claude's
|
||||
local registry cache at `~/.claude/plugins/known_marketplaces.json`:
|
||||
|
||||
```bash
|
||||
openclaw plugins marketplace list <marketplace-name>
|
||||
openclaw plugins install <plugin-name>@<marketplace-name>
|
||||
```
|
||||
|
||||
Use `--marketplace` when you want to pass the marketplace source explicitly:
|
||||
|
||||
```bash
|
||||
openclaw plugins install <plugin-name> --marketplace <marketplace-name>
|
||||
openclaw plugins install <plugin-name> --marketplace <owner/repo>
|
||||
openclaw plugins install <plugin-name> --marketplace ./my-marketplace
|
||||
```
|
||||
|
||||
Marketplace sources can be:
|
||||
|
||||
- a Claude known-marketplace name from `~/.claude/plugins/known_marketplaces.json`
|
||||
- a local marketplace root or `marketplace.json` path
|
||||
- a GitHub repo shorthand such as `owner/repo`
|
||||
- a git URL
|
||||
|
||||
For local paths and archives, OpenClaw auto-detects:
|
||||
|
||||
- native OpenClaw plugins (`openclaw.plugin.json`)
|
||||
@@ -143,34 +114,8 @@ openclaw plugins update --all
|
||||
openclaw plugins update <id> --dry-run
|
||||
```
|
||||
|
||||
Updates apply to tracked installs in `plugins.installs`, currently npm and
|
||||
marketplace installs.
|
||||
Updates only apply to plugins installed from npm (tracked in `plugins.installs`).
|
||||
|
||||
When a stored integrity hash exists and the fetched artifact hash changes,
|
||||
OpenClaw prints a warning and asks for confirmation before proceeding. Use
|
||||
global `--yes` to bypass prompts in CI/non-interactive runs.
|
||||
|
||||
### Inspect
|
||||
|
||||
```bash
|
||||
openclaw plugins inspect <id>
|
||||
openclaw plugins inspect <id> --json
|
||||
```
|
||||
|
||||
Deep introspection for a single plugin. Shows identity, load status, source,
|
||||
registered capabilities, hooks, tools, commands, services, gateway methods,
|
||||
HTTP routes, policy flags, diagnostics, and install metadata.
|
||||
|
||||
Each plugin is classified by what it actually registers at runtime:
|
||||
|
||||
- **plain-capability** — one capability type (e.g. a provider-only plugin)
|
||||
- **hybrid-capability** — multiple capability types (e.g. text + speech + images)
|
||||
- **hook-only** — only hooks, no capabilities or surfaces
|
||||
- **non-capability** — tools/commands/services but no capabilities
|
||||
|
||||
See [Plugins](/tools/plugin#plugin-shapes) for more on the capability model.
|
||||
|
||||
The `--json` flag outputs a machine-readable report suitable for scripting and
|
||||
auditing.
|
||||
|
||||
`info` is an alias for `inspect`.
|
||||
|
||||
@@ -1,29 +1,17 @@
|
||||
---
|
||||
title: Sandbox CLI
|
||||
summary: "Manage sandbox runtimes and inspect effective sandbox policy"
|
||||
read_when: "You are managing sandbox runtimes or debugging sandbox/tool-policy behavior."
|
||||
summary: "Manage sandbox containers and inspect effective sandbox policy"
|
||||
read_when: "You are managing sandbox containers or debugging sandbox/tool-policy behavior."
|
||||
status: active
|
||||
---
|
||||
|
||||
# Sandbox CLI
|
||||
|
||||
Manage sandbox runtimes for isolated agent execution.
|
||||
Manage Docker-based sandbox containers for isolated agent execution.
|
||||
|
||||
## Overview
|
||||
|
||||
OpenClaw can run agents in isolated sandbox runtimes for security. The `sandbox` commands help you inspect and recreate those runtimes after updates or configuration changes.
|
||||
|
||||
Today that usually means:
|
||||
|
||||
- Docker sandbox containers
|
||||
- SSH sandbox runtimes when `agents.defaults.sandbox.backend = "ssh"`
|
||||
- OpenShell sandbox runtimes when `agents.defaults.sandbox.backend = "openshell"`
|
||||
|
||||
For `ssh` and OpenShell `remote`, recreate matters more than with Docker:
|
||||
|
||||
- the remote workspace is canonical after the initial seed
|
||||
- `openclaw sandbox recreate` deletes that canonical remote workspace for the selected scope
|
||||
- next use seeds it again from the current local workspace
|
||||
OpenClaw can run agents in isolated Docker containers for security. The `sandbox` commands help you manage these containers, especially after updates or configuration changes.
|
||||
|
||||
## Commands
|
||||
|
||||
@@ -40,7 +28,7 @@ openclaw sandbox explain --json
|
||||
|
||||
### `openclaw sandbox list`
|
||||
|
||||
List all sandbox runtimes with their status and configuration.
|
||||
List all sandbox containers with their status and configuration.
|
||||
|
||||
```bash
|
||||
openclaw sandbox list
|
||||
@@ -50,16 +38,15 @@ openclaw sandbox list --json # JSON output
|
||||
|
||||
**Output includes:**
|
||||
|
||||
- Runtime name and status
|
||||
- Backend (`docker`, `openshell`, etc.)
|
||||
- Config label and whether it matches current config
|
||||
- Container name and status (running/stopped)
|
||||
- Docker image and whether it matches config
|
||||
- Age (time since creation)
|
||||
- Idle time (time since last use)
|
||||
- Associated session/agent
|
||||
|
||||
### `openclaw sandbox recreate`
|
||||
|
||||
Remove sandbox runtimes to force recreation with updated config.
|
||||
Remove sandbox containers to force recreation with updated images/config.
|
||||
|
||||
```bash
|
||||
openclaw sandbox recreate --all # Recreate all containers
|
||||
@@ -77,11 +64,11 @@ openclaw sandbox recreate --all --force # Skip confirmation
|
||||
- `--browser`: Only recreate browser containers
|
||||
- `--force`: Skip confirmation prompt
|
||||
|
||||
**Important:** Runtimes are automatically recreated when the agent is next used.
|
||||
**Important:** Containers are automatically recreated when the agent is next used.
|
||||
|
||||
## Use Cases
|
||||
|
||||
### After updating a Docker image
|
||||
### After updating Docker images
|
||||
|
||||
```bash
|
||||
# Pull new image
|
||||
@@ -104,37 +91,6 @@ openclaw sandbox recreate --all
|
||||
openclaw sandbox recreate --all
|
||||
```
|
||||
|
||||
### After changing SSH target or SSH auth material
|
||||
|
||||
```bash
|
||||
# Edit config:
|
||||
# - agents.defaults.sandbox.backend
|
||||
# - agents.defaults.sandbox.ssh.target
|
||||
# - agents.defaults.sandbox.ssh.workspaceRoot
|
||||
# - agents.defaults.sandbox.ssh.identityFile / certificateFile / knownHostsFile
|
||||
# - agents.defaults.sandbox.ssh.identityData / certificateData / knownHostsData
|
||||
|
||||
openclaw sandbox recreate --all
|
||||
```
|
||||
|
||||
For the core `ssh` backend, recreate deletes the per-scope remote workspace root
|
||||
on the SSH target. The next run seeds it again from the local workspace.
|
||||
|
||||
### After changing OpenShell source, policy, or mode
|
||||
|
||||
```bash
|
||||
# Edit config:
|
||||
# - agents.defaults.sandbox.backend
|
||||
# - plugins.entries.openshell.config.from
|
||||
# - plugins.entries.openshell.config.mode
|
||||
# - plugins.entries.openshell.config.policy
|
||||
|
||||
openclaw sandbox recreate --all
|
||||
```
|
||||
|
||||
For OpenShell `remote` mode, recreate deletes the canonical remote workspace
|
||||
for that scope. The next run seeds it again from the local workspace.
|
||||
|
||||
### After changing setupCommand
|
||||
|
||||
```bash
|
||||
@@ -152,16 +108,16 @@ openclaw sandbox recreate --agent alfred
|
||||
|
||||
## Why is this needed?
|
||||
|
||||
**Problem:** When you update sandbox configuration:
|
||||
**Problem:** When you update sandbox Docker images or configuration:
|
||||
|
||||
- Existing runtimes continue running with old settings
|
||||
- Runtimes are only pruned after 24h of inactivity
|
||||
- Regularly-used agents keep old runtimes alive indefinitely
|
||||
- Existing containers continue running with old settings
|
||||
- Containers are only pruned after 24h of inactivity
|
||||
- Regularly-used agents keep old containers running indefinitely
|
||||
|
||||
**Solution:** Use `openclaw sandbox recreate` to force removal of old runtimes. They'll be recreated automatically with current settings when next needed.
|
||||
**Solution:** Use `openclaw sandbox recreate` to force removal of old containers. They'll be recreated automatically with current settings when next needed.
|
||||
|
||||
Tip: prefer `openclaw sandbox recreate` over manual backend-specific cleanup.
|
||||
It uses the Gateway’s runtime registry and avoids mismatches when scope/session keys change.
|
||||
Tip: prefer `openclaw sandbox recreate` over manual `docker rm`. It uses the
|
||||
Gateway’s container naming and avoids mismatches when scope/session keys change.
|
||||
|
||||
## Configuration
|
||||
|
||||
@@ -173,7 +129,6 @@ Sandbox settings live in `~/.openclaw/openclaw.json` under `agents.defaults.sand
|
||||
"defaults": {
|
||||
"sandbox": {
|
||||
"mode": "all", // off, non-main, all
|
||||
"backend": "docker", // docker, ssh, openshell
|
||||
"scope": "agent", // session, agent, shared
|
||||
"docker": {
|
||||
"image": "openclaw-sandbox:bookworm-slim",
|
||||
|
||||
@@ -14,9 +14,9 @@ Use `openclaw secrets` to manage SecretRefs and keep the active runtime snapshot
|
||||
Command roles:
|
||||
|
||||
- `reload`: gateway RPC (`secrets.reload`) that re-resolves refs and swaps runtime snapshot only on full success (no config writes).
|
||||
- `audit`: read-only scan of configuration/auth/generated-model stores and legacy residues for plaintext, unresolved refs, and precedence drift (exec refs are skipped unless `--allow-exec` is set).
|
||||
- `audit`: read-only scan of configuration/auth/generated-model stores and legacy residues for plaintext, unresolved refs, and precedence drift.
|
||||
- `configure`: interactive planner for provider setup, target mapping, and preflight (TTY required).
|
||||
- `apply`: execute a saved plan (`--dry-run` for validation only; dry-run skips exec checks by default, and write mode rejects exec-containing plans unless `--allow-exec` is set), then scrub targeted plaintext residues.
|
||||
- `apply`: execute a saved plan (`--dry-run` for validation only), then scrub targeted plaintext residues.
|
||||
|
||||
Recommended operator loop:
|
||||
|
||||
@@ -29,8 +29,6 @@ openclaw secrets audit --check
|
||||
openclaw secrets reload
|
||||
```
|
||||
|
||||
If your plan includes `exec` SecretRefs/providers, pass `--allow-exec` on both dry-run and write apply commands.
|
||||
|
||||
Exit code note for CI/gates:
|
||||
|
||||
- `audit --check` returns `1` on findings.
|
||||
@@ -75,7 +73,6 @@ Header residue note:
|
||||
openclaw secrets audit
|
||||
openclaw secrets audit --check
|
||||
openclaw secrets audit --json
|
||||
openclaw secrets audit --allow-exec
|
||||
```
|
||||
|
||||
Exit behavior:
|
||||
@@ -86,7 +83,6 @@ Exit behavior:
|
||||
Report shape highlights:
|
||||
|
||||
- `status`: `clean | findings | unresolved`
|
||||
- `resolution`: `refsChecked`, `skippedExecRefs`, `resolvabilityComplete`
|
||||
- `summary`: `plaintextCount`, `unresolvedRefCount`, `shadowedRefCount`, `legacyResidueCount`
|
||||
- finding codes:
|
||||
- `PLAINTEXT_FOUND`
|
||||
@@ -119,7 +115,6 @@ Flags:
|
||||
- `--providers-only`: configure `secrets.providers` only, skip credential mapping.
|
||||
- `--skip-provider-setup`: skip provider setup and map credentials to existing providers.
|
||||
- `--agent <id>`: scope `auth-profiles.json` target discovery and writes to one agent store.
|
||||
- `--allow-exec`: allow exec SecretRef checks during preflight/apply (may execute provider commands).
|
||||
|
||||
Notes:
|
||||
|
||||
@@ -129,7 +124,6 @@ Notes:
|
||||
- `configure` supports creating new `auth-profiles.json` mappings directly in the picker flow.
|
||||
- Canonical supported surface: [SecretRef Credential Surface](/reference/secretref-credential-surface).
|
||||
- It performs preflight resolution before apply.
|
||||
- If preflight/apply includes exec refs, keep `--allow-exec` set for both steps.
|
||||
- Generated plans default to scrub options (`scrubEnv`, `scrubAuthProfilesForProviderTargets`, `scrubLegacyAuthJson` all enabled).
|
||||
- Apply path is one-way for scrubbed plaintext values.
|
||||
- Without `--apply`, CLI still prompts `Apply this plan now?` after preflight.
|
||||
@@ -147,19 +141,10 @@ Apply or preflight a plan generated previously:
|
||||
|
||||
```bash
|
||||
openclaw secrets apply --from /tmp/openclaw-secrets-plan.json
|
||||
openclaw secrets apply --from /tmp/openclaw-secrets-plan.json --allow-exec
|
||||
openclaw secrets apply --from /tmp/openclaw-secrets-plan.json --dry-run
|
||||
openclaw secrets apply --from /tmp/openclaw-secrets-plan.json --dry-run --allow-exec
|
||||
openclaw secrets apply --from /tmp/openclaw-secrets-plan.json --json
|
||||
```
|
||||
|
||||
Exec behavior:
|
||||
|
||||
- `--dry-run` validates preflight without writing files.
|
||||
- exec SecretRef checks are skipped by default in dry-run.
|
||||
- write mode rejects plans that contain exec SecretRefs/providers unless `--allow-exec` is set.
|
||||
- Use `--allow-exec` to opt in to exec provider checks/execution in either mode.
|
||||
|
||||
Plan contract details (allowed target paths, validation rules, and failure semantics):
|
||||
|
||||
- [Secrets Apply Plan Contract](/gateway/secrets-plan-contract)
|
||||
|
||||
@@ -19,8 +19,6 @@ Related:
|
||||
```bash
|
||||
openclaw security audit
|
||||
openclaw security audit --deep
|
||||
openclaw security audit --deep --password <password>
|
||||
openclaw security audit --deep --token <token>
|
||||
openclaw security audit --fix
|
||||
openclaw security audit --json
|
||||
```
|
||||
@@ -30,7 +28,7 @@ This is for cooperative/shared inbox hardening. A single Gateway shared by mutua
|
||||
It also emits `security.trust_model.multi_user_heuristic` when config suggests likely shared-user ingress (for example open DM/group policy, configured group targets, or wildcard sender rules), and reminds you that OpenClaw is a personal-assistant trust model by default.
|
||||
For intentional shared-user setups, the audit guidance is to sandbox all sessions, keep filesystem access workspace-scoped, and keep personal/private identities or credentials off that runtime.
|
||||
It also warns when small models (`<=300B`) are used without sandboxing and with web/browser tools enabled.
|
||||
For webhook ingress, it warns when `hooks.token` reuses the Gateway token, when `hooks.defaultSessionKey` is unset, when `hooks.allowedAgentIds` is unrestricted, when request `sessionKey` overrides are enabled, and when overrides are enabled without `hooks.allowedSessionKeyPrefixes`.
|
||||
For webhook ingress, it warns when `hooks.defaultSessionKey` is unset, when request `sessionKey` overrides are enabled, and when overrides are enabled without `hooks.allowedSessionKeyPrefixes`.
|
||||
It also warns when sandbox Docker settings are configured while sandbox mode is off, when `gateway.nodes.denyCommands` uses ineffective pattern-like/unknown entries (exact node command-name matching only, not shell-text filtering), when `gateway.nodes.allowCommands` explicitly enables dangerous node commands, when global `tools.profile="minimal"` is overridden by agent tool profiles, when open groups expose runtime/filesystem tools without sandbox/workspace guards, and when installed extension plugin tools may be reachable under permissive tool policy.
|
||||
It also flags `gateway.allowRealIpFallback=true` (header-spoofing risk if proxies are misconfigured) and `discovery.mdns.mode="full"` (metadata leakage via mDNS TXT records).
|
||||
It also warns when sandbox browser uses Docker `bridge` network without `sandbox.browser.cdpSourceRange`.
|
||||
@@ -42,12 +40,6 @@ It warns when `gateway.auth.mode="none"` leaves Gateway HTTP APIs reachable with
|
||||
Settings prefixed with `dangerous`/`dangerously` are explicit break-glass operator overrides; enabling one is not, by itself, a security vulnerability report.
|
||||
For the complete dangerous-parameter inventory, see the "Insecure or dangerous flags summary" section in [Security](/gateway/security).
|
||||
|
||||
SecretRef behavior:
|
||||
|
||||
- `security audit` resolves supported SecretRefs in read-only mode for its targeted paths.
|
||||
- If a SecretRef is unavailable in the current command path, audit continues and reports `secretDiagnostics` (instead of crashing).
|
||||
- `--token` and `--password` only override deep-probe auth for that command invocation; they do not rewrite config or SecretRef mappings.
|
||||
|
||||
## JSON output
|
||||
|
||||
Use `--json` for CI/policy checks:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
summary: "CLI reference for `openclaw setup` (initialize config + workspace)"
|
||||
read_when:
|
||||
- You’re doing first-run setup without full CLI onboarding
|
||||
- You’re doing first-run setup without the full onboarding wizard
|
||||
- You want to set the default workspace path
|
||||
title: "setup"
|
||||
---
|
||||
@@ -13,7 +13,7 @@ Initialize `~/.openclaw/openclaw.json` and the agent workspace.
|
||||
Related:
|
||||
|
||||
- Getting started: [Getting started](/start/getting-started)
|
||||
- CLI onboarding: [Onboarding (CLI)](/start/wizard)
|
||||
- Wizard: [Onboarding](/start/onboarding)
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -22,7 +22,7 @@ openclaw setup
|
||||
openclaw setup --workspace ~/.openclaw/workspace
|
||||
```
|
||||
|
||||
To run onboarding via setup:
|
||||
To run the wizard via setup:
|
||||
|
||||
```bash
|
||||
openclaw setup --wizard
|
||||
|
||||
@@ -26,5 +26,3 @@ Notes:
|
||||
- Update info surfaces in the Overview; if an update is available, status prints a hint to run `openclaw update` (see [Updating](/install/updating)).
|
||||
- Read-only status surfaces (`status`, `status --json`, `status --all`) resolve supported SecretRefs for their targeted config paths when possible.
|
||||
- If a supported channel SecretRef is configured but unavailable in the current command path, status stays read-only and reports degraded output instead of crashing. Human output shows warnings such as “configured token unavailable in this command path”, and JSON output includes `secretDiagnostics`.
|
||||
- When command-local SecretRef resolution succeeds, status prefers the resolved snapshot and clears transient “secret unavailable” channel markers from the final output.
|
||||
- `status --all` includes a Secrets overview row and a diagnosis section that summarizes secret diagnostics (truncated for readability) without stopping report generation.
|
||||
|
||||
@@ -97,25 +97,6 @@ compaction and can run alongside it.
|
||||
|
||||
See [OpenAI provider](/providers/openai) for model params and overrides.
|
||||
|
||||
## Custom context engines
|
||||
|
||||
Compaction behavior is owned by the active
|
||||
[context engine](/concepts/context-engine). The legacy engine uses the built-in
|
||||
summarization described above. Plugin engines (selected via
|
||||
`plugins.slots.contextEngine`) can implement any compaction strategy — DAG
|
||||
summaries, vector retrieval, incremental condensation, etc.
|
||||
|
||||
When a plugin engine sets `ownsCompaction: true`, OpenClaw delegates all
|
||||
compaction decisions to the engine and does not run built-in auto-compaction.
|
||||
|
||||
When `ownsCompaction` is `false` or unset, OpenClaw may still use Pi's
|
||||
built-in in-attempt auto-compaction, but the active engine's `compact()` method
|
||||
still handles `/compact` and overflow recovery. There is no automatic fallback
|
||||
to the legacy engine's compaction path.
|
||||
|
||||
If you are building a non-owning context engine, implement `compact()` by
|
||||
calling `delegateCompactionToRuntime(...)` from `openclaw/plugin-sdk/core`.
|
||||
|
||||
## Tips
|
||||
|
||||
- Use `/compact` when sessions feel stale or context is bloated.
|
||||
|
||||
@@ -1,268 +0,0 @@
|
||||
---
|
||||
summary: "Context engine: pluggable context assembly, compaction, and subagent lifecycle"
|
||||
read_when:
|
||||
- You want to understand how OpenClaw assembles model context
|
||||
- You are switching between the legacy engine and a plugin engine
|
||||
- You are building a context engine plugin
|
||||
title: "Context Engine"
|
||||
---
|
||||
|
||||
# Context Engine
|
||||
|
||||
A **context engine** controls how OpenClaw builds model context for each run.
|
||||
It decides which messages to include, how to summarize older history, and how
|
||||
to manage context across subagent boundaries.
|
||||
|
||||
OpenClaw ships with a built-in `legacy` engine. Plugins can register
|
||||
alternative engines that replace the active context-engine lifecycle.
|
||||
|
||||
## Quick start
|
||||
|
||||
Check which engine is active:
|
||||
|
||||
```bash
|
||||
openclaw doctor
|
||||
# or inspect config directly:
|
||||
cat ~/.openclaw/openclaw.json | jq '.plugins.slots.contextEngine'
|
||||
```
|
||||
|
||||
### Installing a context engine plugin
|
||||
|
||||
Context engine plugins are installed like any other OpenClaw plugin. Install
|
||||
first, then select the engine in the slot:
|
||||
|
||||
```bash
|
||||
# Install from npm
|
||||
openclaw plugins install @martian-engineering/lossless-claw
|
||||
|
||||
# Or install from a local path (for development)
|
||||
openclaw plugins install -l ./my-context-engine
|
||||
```
|
||||
|
||||
Then enable the plugin and select it as the active engine in your config:
|
||||
|
||||
```json5
|
||||
// openclaw.json
|
||||
{
|
||||
plugins: {
|
||||
slots: {
|
||||
contextEngine: "lossless-claw", // must match the plugin's registered engine id
|
||||
},
|
||||
entries: {
|
||||
"lossless-claw": {
|
||||
enabled: true,
|
||||
// Plugin-specific config goes here (see the plugin's docs)
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Restart the gateway after installing and configuring.
|
||||
|
||||
To switch back to the built-in engine, set `contextEngine` to `"legacy"` (or
|
||||
remove the key entirely — `"legacy"` is the default).
|
||||
|
||||
## How it works
|
||||
|
||||
Every time OpenClaw runs a model prompt, the context engine participates at
|
||||
four lifecycle points:
|
||||
|
||||
1. **Ingest** — called when a new message is added to the session. The engine
|
||||
can store or index the message in its own data store.
|
||||
2. **Assemble** — called before each model run. The engine returns an ordered
|
||||
set of messages (and an optional `systemPromptAddition`) that fit within
|
||||
the token budget.
|
||||
3. **Compact** — called when the context window is full, or when the user runs
|
||||
`/compact`. The engine summarizes older history to free space.
|
||||
4. **After turn** — called after a run completes. The engine can persist state,
|
||||
trigger background compaction, or update indexes.
|
||||
|
||||
### Subagent lifecycle (optional)
|
||||
|
||||
OpenClaw currently calls one subagent lifecycle hook:
|
||||
|
||||
- **onSubagentEnded** — clean up when a subagent session completes or is swept.
|
||||
|
||||
The `prepareSubagentSpawn` hook is part of the interface for future use, but
|
||||
the runtime does not invoke it yet.
|
||||
|
||||
### System prompt addition
|
||||
|
||||
The `assemble` method can return a `systemPromptAddition` string. OpenClaw
|
||||
prepends this to the system prompt for the run. This lets engines inject
|
||||
dynamic recall guidance, retrieval instructions, or context-aware hints
|
||||
without requiring static workspace files.
|
||||
|
||||
## The legacy engine
|
||||
|
||||
The built-in `legacy` engine preserves OpenClaw's original behavior:
|
||||
|
||||
- **Ingest**: no-op (the session manager handles message persistence directly).
|
||||
- **Assemble**: pass-through (the existing sanitize → validate → limit pipeline
|
||||
in the runtime handles context assembly).
|
||||
- **Compact**: delegates to the built-in summarization compaction, which creates
|
||||
a single summary of older messages and keeps recent messages intact.
|
||||
- **After turn**: no-op.
|
||||
|
||||
The legacy engine does not register tools or provide a `systemPromptAddition`.
|
||||
|
||||
When no `plugins.slots.contextEngine` is set (or it's set to `"legacy"`), this
|
||||
engine is used automatically.
|
||||
|
||||
## Plugin engines
|
||||
|
||||
A plugin can register a context engine using the plugin API:
|
||||
|
||||
```ts
|
||||
export default function register(api) {
|
||||
api.registerContextEngine("my-engine", () => ({
|
||||
info: {
|
||||
id: "my-engine",
|
||||
name: "My Context Engine",
|
||||
ownsCompaction: true,
|
||||
},
|
||||
|
||||
async ingest({ sessionId, message, isHeartbeat }) {
|
||||
// Store the message in your data store
|
||||
return { ingested: true };
|
||||
},
|
||||
|
||||
async assemble({ sessionId, messages, tokenBudget }) {
|
||||
// Return messages that fit the budget
|
||||
return {
|
||||
messages: buildContext(messages, tokenBudget),
|
||||
estimatedTokens: countTokens(messages),
|
||||
systemPromptAddition: "Use lcm_grep to search history...",
|
||||
};
|
||||
},
|
||||
|
||||
async compact({ sessionId, force }) {
|
||||
// Summarize older context
|
||||
return { ok: true, compacted: true };
|
||||
},
|
||||
}));
|
||||
}
|
||||
```
|
||||
|
||||
Then enable it in config:
|
||||
|
||||
```json5
|
||||
{
|
||||
plugins: {
|
||||
slots: {
|
||||
contextEngine: "my-engine",
|
||||
},
|
||||
entries: {
|
||||
"my-engine": {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### The ContextEngine interface
|
||||
|
||||
Required members:
|
||||
|
||||
| Member | Kind | Purpose |
|
||||
| ------------------ | -------- | -------------------------------------------------------- |
|
||||
| `info` | Property | Engine id, name, version, and whether it owns compaction |
|
||||
| `ingest(params)` | Method | Store a single message |
|
||||
| `assemble(params)` | Method | Build context for a model run (returns `AssembleResult`) |
|
||||
| `compact(params)` | Method | Summarize/reduce context |
|
||||
|
||||
`assemble` returns an `AssembleResult` with:
|
||||
|
||||
- `messages` — the ordered messages to send to the model.
|
||||
- `estimatedTokens` (required, `number`) — the engine's estimate of total
|
||||
tokens in the assembled context. OpenClaw uses this for compaction threshold
|
||||
decisions and diagnostic reporting.
|
||||
- `systemPromptAddition` (optional, `string`) — prepended to the system prompt.
|
||||
|
||||
Optional members:
|
||||
|
||||
| Member | Kind | Purpose |
|
||||
| ------------------------------ | ------ | --------------------------------------------------------------------------------------------------------------- |
|
||||
| `bootstrap(params)` | Method | Initialize engine state for a session. Called once when the engine first sees a session (e.g., import history). |
|
||||
| `ingestBatch(params)` | Method | Ingest a completed turn as a batch. Called after a run completes, with all messages from that turn at once. |
|
||||
| `afterTurn(params)` | Method | Post-run lifecycle work (persist state, trigger background compaction). |
|
||||
| `prepareSubagentSpawn(params)` | Method | Set up shared state for a child session. |
|
||||
| `onSubagentEnded(params)` | Method | Clean up after a subagent ends. |
|
||||
| `dispose()` | Method | Release resources. Called during gateway shutdown or plugin reload — not per-session. |
|
||||
|
||||
### ownsCompaction
|
||||
|
||||
`ownsCompaction` controls whether Pi's built-in in-attempt auto-compaction stays
|
||||
enabled for the run:
|
||||
|
||||
- `true` — the engine owns compaction behavior. OpenClaw disables Pi's built-in
|
||||
auto-compaction for that run, and the engine's `compact()` implementation is
|
||||
responsible for `/compact`, overflow recovery compaction, and any proactive
|
||||
compaction it wants to do in `afterTurn()`.
|
||||
- `false` or unset — Pi's built-in auto-compaction may still run during prompt
|
||||
execution, but the active engine's `compact()` method is still called for
|
||||
`/compact` and overflow recovery.
|
||||
|
||||
`ownsCompaction: false` does **not** mean OpenClaw automatically falls back to
|
||||
the legacy engine's compaction path.
|
||||
|
||||
That means there are two valid plugin patterns:
|
||||
|
||||
- **Owning mode** — implement your own compaction algorithm and set
|
||||
`ownsCompaction: true`.
|
||||
- **Delegating mode** — set `ownsCompaction: false` and have `compact()` call
|
||||
`delegateCompactionToRuntime(...)` from `openclaw/plugin-sdk/core` to use
|
||||
OpenClaw's built-in compaction behavior.
|
||||
|
||||
A no-op `compact()` is unsafe for an active non-owning engine because it
|
||||
disables the normal `/compact` and overflow-recovery compaction path for that
|
||||
engine slot.
|
||||
|
||||
## Configuration reference
|
||||
|
||||
```json5
|
||||
{
|
||||
plugins: {
|
||||
slots: {
|
||||
// Select the active context engine. Default: "legacy".
|
||||
// Set to a plugin id to use a plugin engine.
|
||||
contextEngine: "legacy",
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
The slot is exclusive at run time — only one registered context engine is
|
||||
resolved for a given run or compaction operation. Other enabled
|
||||
`kind: "context-engine"` plugins can still load and run their registration
|
||||
code; `plugins.slots.contextEngine` only selects which registered engine id
|
||||
OpenClaw resolves when it needs a context engine.
|
||||
|
||||
## Relationship to compaction and memory
|
||||
|
||||
- **Compaction** is one responsibility of the context engine. The legacy engine
|
||||
delegates to OpenClaw's built-in summarization. Plugin engines can implement
|
||||
any compaction strategy (DAG summaries, vector retrieval, etc.).
|
||||
- **Memory plugins** (`plugins.slots.memory`) are separate from context engines.
|
||||
Memory plugins provide search/retrieval; context engines control what the
|
||||
model sees. They can work together — a context engine might use memory
|
||||
plugin data during assembly.
|
||||
- **Session pruning** (trimming old tool results in-memory) still runs
|
||||
regardless of which context engine is active.
|
||||
|
||||
## Tips
|
||||
|
||||
- Use `openclaw doctor` to verify your engine is loading correctly.
|
||||
- If switching engines, existing sessions continue with their current history.
|
||||
The new engine takes over for future runs.
|
||||
- Engine errors are logged and surfaced in diagnostics. If a plugin engine
|
||||
fails to register or the selected engine id cannot be resolved, OpenClaw
|
||||
does not fall back automatically; runs fail until you fix the plugin or
|
||||
switch `plugins.slots.contextEngine` back to `"legacy"`.
|
||||
- For development, use `openclaw plugins install -l ./my-engine` to link a
|
||||
local plugin directory without copying.
|
||||
|
||||
See also: [Compaction](/concepts/compaction), [Context](/concepts/context),
|
||||
[Plugins](/tools/plugin), [Plugin manifest](/plugins/manifest).
|
||||
@@ -116,7 +116,7 @@ Large files are truncated per-file using `agents.defaults.bootstrapMaxChars` (de
|
||||
|
||||
When truncation occurs, the runtime can inject an in-prompt warning block under Project Context. Configure this with `agents.defaults.bootstrapPromptTruncationWarning` (`off`, `once`, `always`; default `once`).
|
||||
|
||||
## Skills: injected vs loaded on-demand
|
||||
## Skills: what’s injected vs loaded on-demand
|
||||
|
||||
The system prompt includes a compact **skills list** (name + description + location). This list has real overhead.
|
||||
|
||||
@@ -131,7 +131,7 @@ Tools affect context in two ways:
|
||||
|
||||
`/context detail` breaks down the biggest tool schemas so you can see what dominates.
|
||||
|
||||
## Commands, directives, and "inline shortcuts"
|
||||
## Commands, directives, and “inline shortcuts”
|
||||
|
||||
Slash commands are handled by the Gateway. There are a few different behaviors:
|
||||
|
||||
@@ -157,10 +157,7 @@ By default, OpenClaw uses the built-in `legacy` context engine for assembly and
|
||||
compaction. If you install a plugin that provides `kind: "context-engine"` and
|
||||
select it with `plugins.slots.contextEngine`, OpenClaw delegates context
|
||||
assembly, `/compact`, and related subagent context lifecycle hooks to that
|
||||
engine instead. `ownsCompaction: false` does not auto-fallback to the legacy
|
||||
engine; the active engine must still implement `compact()` correctly. See
|
||||
[Context Engine](/concepts/context-engine) for the full
|
||||
pluggable interface, lifecycle hooks, and configuration.
|
||||
engine instead.
|
||||
|
||||
## What `/context` actually reports
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ they are tried first, but OpenClaw may rotate to another profile on rate limits/
|
||||
User‑pinned profiles stay locked to that profile; if it fails and model fallbacks
|
||||
are configured, OpenClaw moves to the next model instead of switching profiles.
|
||||
|
||||
### Why OAuth can "look lost"
|
||||
### Why OAuth can “look lost”
|
||||
|
||||
If you have both an OAuth profile and an API key profile for the same provider, round‑robin can switch between them across messages unless pinned. To force a single profile:
|
||||
|
||||
|
||||
@@ -19,23 +19,10 @@ For model selection rules, see [/concepts/models](/concepts/models).
|
||||
- Provider plugins can inject model catalogs via `registerProvider({ catalog })`;
|
||||
OpenClaw merges that output into `models.providers` before writing
|
||||
`models.json`.
|
||||
- Provider manifests can declare `providerAuthEnvVars` so generic env-based
|
||||
auth probes do not need to load plugin runtime. The remaining core env-var
|
||||
map is now just for non-plugin/core providers and a few generic-precedence
|
||||
cases such as Anthropic API-key-first onboarding.
|
||||
- Provider plugins can also own provider runtime behavior via
|
||||
`resolveDynamicModel`, `prepareDynamicModel`, `normalizeResolvedModel`,
|
||||
`capabilities`, `prepareExtraParams`, `wrapStreamFn`, `formatApiKey`,
|
||||
`refreshOAuth`, `buildAuthDoctorHint`,
|
||||
`isCacheTtlEligible`, `buildMissingAuthMessage`,
|
||||
`suppressBuiltInModel`, `augmentModelCatalog`, `isBinaryThinking`,
|
||||
`supportsXHighThinking`, `resolveDefaultThinkingLevel`,
|
||||
`isModernModelRef`, `prepareRuntimeAuth`, `resolveUsageAuth`, and
|
||||
`fetchUsageSnapshot`.
|
||||
- Note: provider runtime `capabilities` is shared runner metadata (provider
|
||||
family, transcript/tooling quirks, transport/cache hints). It is not the
|
||||
same as the [public capability model](/tools/plugin#public-capability-model)
|
||||
which describes what a plugin registers (text inference, speech, etc.).
|
||||
`capabilities`, `prepareExtraParams`, `wrapStreamFn`,
|
||||
`isCacheTtlEligible`, and `prepareRuntimeAuth`.
|
||||
|
||||
## Plugin-owned provider behavior
|
||||
|
||||
@@ -44,10 +31,6 @@ the generic inference loop.
|
||||
|
||||
Typical split:
|
||||
|
||||
- `auth[].run` / `auth[].runNonInteractive`: provider owns onboarding/login
|
||||
flows for `openclaw onboard`, `openclaw models auth`, and headless setup
|
||||
- `wizard.setup` / `wizard.modelPicker`: provider owns auth-choice labels,
|
||||
legacy aliases, onboarding allowlist hints, and setup entries in onboarding/model pickers
|
||||
- `catalog`: provider appears in `models.providers`
|
||||
- `resolveDynamicModel`: provider accepts model ids not present in the local
|
||||
static catalog yet
|
||||
@@ -57,62 +40,25 @@ Typical split:
|
||||
- `capabilities`: provider publishes transcript/tooling/provider-family quirks
|
||||
- `prepareExtraParams`: provider defaults or normalizes per-model request params
|
||||
- `wrapStreamFn`: provider applies request headers/body/model compat wrappers
|
||||
- `formatApiKey`: provider formats stored auth profiles into the runtime
|
||||
`apiKey` string expected by the transport
|
||||
- `refreshOAuth`: provider owns OAuth refresh when the shared `pi-ai`
|
||||
refreshers are not enough
|
||||
- `buildAuthDoctorHint`: provider appends repair guidance when OAuth refresh
|
||||
fails
|
||||
- `isCacheTtlEligible`: provider decides which upstream model ids support prompt-cache TTL
|
||||
- `buildMissingAuthMessage`: provider replaces the generic auth-store error
|
||||
with a provider-specific recovery hint
|
||||
- `suppressBuiltInModel`: provider hides stale upstream rows and can return a
|
||||
vendor-owned error for direct resolution failures
|
||||
- `augmentModelCatalog`: provider appends synthetic/final catalog rows after
|
||||
discovery and config merging
|
||||
- `isBinaryThinking`: provider owns binary on/off thinking UX
|
||||
- `supportsXHighThinking`: provider opts selected models into `xhigh`
|
||||
- `resolveDefaultThinkingLevel`: provider owns default `/think` policy for a
|
||||
model family
|
||||
- `isModernModelRef`: provider owns live/smoke preferred-model matching
|
||||
- `prepareRuntimeAuth`: provider turns a configured credential into a short
|
||||
lived runtime token
|
||||
- `resolveUsageAuth`: provider resolves usage/quota credentials for `/usage`
|
||||
and related status/reporting surfaces
|
||||
- `fetchUsageSnapshot`: provider owns the usage endpoint fetch/parsing while
|
||||
core still owns the summary shell and formatting
|
||||
|
||||
Current bundled examples:
|
||||
|
||||
- `anthropic`: Claude 4.6 forward-compat fallback, auth repair hints, usage
|
||||
endpoint fetching, and cache-TTL/provider-family metadata
|
||||
- `openrouter`: pass-through model ids, request wrappers, provider capability
|
||||
hints, and cache-TTL policy
|
||||
- `github-copilot`: onboarding/device login, forward-compat model fallback,
|
||||
Claude-thinking transcript hints, runtime token exchange, and usage endpoint
|
||||
fetching
|
||||
- `openai`: GPT-5.4 forward-compat fallback, direct OpenAI transport
|
||||
normalization, Codex-aware missing-auth hints, Spark suppression, synthetic
|
||||
OpenAI/Codex catalog rows, thinking/live-model policy, and
|
||||
provider-family metadata
|
||||
- `google` and `google-gemini-cli`: Gemini 3.1 forward-compat fallback and
|
||||
modern-model matching; Gemini CLI OAuth also owns auth-profile token
|
||||
formatting, usage-token parsing, and quota endpoint fetching for usage
|
||||
surfaces
|
||||
- `github-copilot`: forward-compat model fallback, Claude-thinking transcript
|
||||
hints, and runtime token exchange
|
||||
- `openai-codex`: forward-compat model fallback, transport normalization, and
|
||||
default transport params
|
||||
- `moonshot`: shared transport, plugin-owned thinking payload normalization
|
||||
- `kilocode`: shared transport, plugin-owned request headers, reasoning payload
|
||||
normalization, Gemini transcript hints, and cache-TTL policy
|
||||
- `zai`: GLM-5 forward-compat fallback, `tool_stream` defaults, cache-TTL
|
||||
policy, binary-thinking/live-model policy, and usage auth + quota fetching
|
||||
- `mistral`, `opencode`, and `opencode-go`: plugin-owned capability metadata
|
||||
- `byteplus`, `cloudflare-ai-gateway`, `huggingface`, `kimi-coding`,
|
||||
`modelstudio`, `nvidia`, `qianfan`, `synthetic`, `together`, `venice`,
|
||||
`vercel-ai-gateway`, and `volcengine`: plugin-owned catalogs only
|
||||
- `qwen-portal`: plugin-owned catalog, OAuth login, and OAuth refresh
|
||||
- `minimax` and `xiaomi`: plugin-owned catalogs plus usage auth/snapshot logic
|
||||
|
||||
The bundled `openai` plugin now owns both provider ids: `openai` and
|
||||
`openai-codex`.
|
||||
`minimax`, `minimax-portal`, `modelstudio`, `nvidia`, `qianfan`,
|
||||
`qwen-portal`, `synthetic`, `together`, `venice`, `vercel-ai-gateway`,
|
||||
`volcengine`, and `xiaomi`: plugin-owned catalogs only
|
||||
|
||||
That covers providers that still fit OpenClaw's normal transports. A provider
|
||||
that needs a totally custom request executor is a separate, deeper extension
|
||||
@@ -215,13 +161,16 @@ OpenClaw ships with the pi‑ai catalog. These providers require **no**
|
||||
- Compatibility: legacy OpenClaw config using `google/gemini-3.1-flash-preview` is normalized to `google/gemini-3-flash-preview`
|
||||
- CLI: `openclaw onboard --auth-choice gemini-api-key`
|
||||
|
||||
### Google Vertex and Gemini CLI
|
||||
### Google Vertex, Antigravity, and Gemini CLI
|
||||
|
||||
- Providers: `google-vertex`, `google-gemini-cli`
|
||||
- Auth: Vertex uses gcloud ADC; Gemini CLI uses its OAuth flow
|
||||
- Caution: Gemini CLI OAuth in OpenClaw is an unofficial integration. Some users have reported Google account restrictions after using third-party clients. Review Google terms and use a non-critical account if you choose to proceed.
|
||||
- Gemini CLI OAuth is shipped as part of the bundled `google` plugin.
|
||||
- Enable: `openclaw plugins enable google`
|
||||
- Providers: `google-vertex`, `google-antigravity`, `google-gemini-cli`
|
||||
- Auth: Vertex uses gcloud ADC; Antigravity/Gemini CLI use their respective auth flows
|
||||
- Caution: Antigravity and Gemini CLI OAuth in OpenClaw are unofficial integrations. Some users have reported Google account restrictions after using third-party clients. Review Google terms and use a non-critical account if you choose to proceed.
|
||||
- Antigravity OAuth is shipped as a bundled plugin (`google-antigravity-auth`, disabled by default).
|
||||
- Enable: `openclaw plugins enable google-antigravity-auth`
|
||||
- Login: `openclaw models auth login --provider google-antigravity --set-default`
|
||||
- Gemini CLI OAuth is shipped as a bundled plugin (`google-gemini-cli-auth`, disabled by default).
|
||||
- Enable: `openclaw plugins enable google-gemini-cli-auth`
|
||||
- Login: `openclaw models auth login --provider google-gemini-cli --set-default`
|
||||
- Note: you do **not** paste a client id or secret into `openclaw.json`. The CLI login flow stores
|
||||
tokens in auth profiles on the gateway host.
|
||||
|
||||
@@ -26,7 +26,6 @@ Related:
|
||||
|
||||
- `agents.defaults.models` is the allowlist/catalog of models OpenClaw can use (plus aliases).
|
||||
- `agents.defaults.imageModel` is used **only when** the primary model can’t accept images.
|
||||
- `agents.defaults.imageGenerationModel` is used by the shared image-generation capability. If omitted, `image_generate` can still infer a provider default from compatible auth-backed image-generation plugins.
|
||||
- Per-agent defaults can override `agents.defaults.model` via `agents.list[].model` plus bindings (see [/concepts/multi-agent](/concepts/multi-agent)).
|
||||
|
||||
## Quick model policy
|
||||
@@ -35,9 +34,9 @@ Related:
|
||||
- Use fallbacks for cost/latency-sensitive tasks and lower-stakes chat.
|
||||
- For tool-enabled agents or untrusted inputs, avoid older/weaker model tiers.
|
||||
|
||||
## Onboarding (recommended)
|
||||
## Setup wizard (recommended)
|
||||
|
||||
If you don’t want to hand-edit config, run onboarding:
|
||||
If you don’t want to hand-edit config, run the onboarding wizard:
|
||||
|
||||
```bash
|
||||
openclaw onboard
|
||||
@@ -50,7 +49,6 @@ subscription** (OAuth) and **Anthropic** (API key or `claude setup-token`).
|
||||
|
||||
- `agents.defaults.model.primary` and `agents.defaults.model.fallbacks`
|
||||
- `agents.defaults.imageModel.primary` and `agents.defaults.imageModel.fallbacks`
|
||||
- `agents.defaults.imageGenerationModel.primary` and `agents.defaults.imageGenerationModel.fallbacks`
|
||||
- `agents.defaults.models` (allowlist + aliases + provider params)
|
||||
- `models.providers` (custom providers written into `models.json`)
|
||||
|
||||
@@ -60,7 +58,7 @@ to `zai/*`.
|
||||
Provider configuration examples (including OpenCode) live in
|
||||
[/gateway/configuration](/gateway/configuration#opencode).
|
||||
|
||||
## "Model is not allowed" (and why replies stop)
|
||||
## “Model is not allowed” (and why replies stop)
|
||||
|
||||
If `agents.defaults.models` is set, it becomes the **allowlist** for `/model` and for
|
||||
session overrides. When a user selects a model that isn’t in that allowlist,
|
||||
|
||||
@@ -9,7 +9,7 @@ status: active
|
||||
|
||||
Goal: multiple _isolated_ agents (separate workspace + `agentDir` + sessions), plus multiple channel accounts (e.g. two WhatsApps) in one running Gateway. Inbound is routed to an agent via bindings.
|
||||
|
||||
## What is "one agent"?
|
||||
## What is “one agent”?
|
||||
|
||||
An **agent** is a fully scoped brain with its own:
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ even before any clients connect.
|
||||
Every WS client begins with a `connect` request. On successful handshake the
|
||||
Gateway upserts a presence entry for that connection.
|
||||
|
||||
#### Why one-off CLI commands do not show up
|
||||
#### Why one‑off CLI commands don’t show up
|
||||
|
||||
The CLI often connects for short, one‑off commands. To avoid spamming the
|
||||
Instances list, `client.mode === "cli"` is **not** turned into a presence entry.
|
||||
|
||||
@@ -90,7 +90,7 @@ more natural.
|
||||
- Modes: `off` (default), `natural` (800–2500ms), `custom` (`minMs`/`maxMs`).
|
||||
- Applies only to **block replies**, not final replies or tool summaries.
|
||||
|
||||
## "Stream chunks or everything"
|
||||
## “Stream chunks or everything”
|
||||
|
||||
This maps to:
|
||||
|
||||
|
||||
@@ -185,7 +185,7 @@ ws.on("message", (data) => {
|
||||
});
|
||||
```
|
||||
|
||||
## Worked example: add a method end-to-end
|
||||
## Worked example: add a method end‑to‑end
|
||||
|
||||
Example: add a new `system.echo` request that returns `{ ok: true, text }`.
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user