mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-17 11:38:44 +08:00
Compare commits
1 Commits
codex/slac
...
codex/runt
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc26444f6d |
@@ -7,7 +7,7 @@ description: "Use for all ClawSweeper work: OpenClaw issue/PR sweep reports, com
|
||||
|
||||
ClawSweeper lives at `~/Projects/clawsweeper`. It is the one OpenClaw
|
||||
maintenance bot for sweeping, commit review, repair jobs, and guarded fix PRs.
|
||||
Use this skill whenever asked about reports, findings, dispatch health,
|
||||
Use this skill whenever Peter asks about reports, findings, dispatch health,
|
||||
repair/cloud PR creation, comment commands, automerge, permissions, or gates.
|
||||
|
||||
## Start
|
||||
@@ -20,7 +20,7 @@ pnpm run build:all
|
||||
```
|
||||
|
||||
Do not overwrite unrelated edits. If the tree is dirty, inspect first and keep
|
||||
read-only report work read-only unless the requester asked to commit.
|
||||
read-only report work read-only unless Peter asked to commit.
|
||||
|
||||
## One Bot, One App
|
||||
|
||||
@@ -79,7 +79,7 @@ gh workflow run commit-review.yml --repo openclaw/clawsweeper \
|
||||
-f enabled=true
|
||||
```
|
||||
|
||||
Use `create_checks=true` only when the requester explicitly wants target commit Check
|
||||
Use `create_checks=true` only when Peter explicitly wants target commit Check
|
||||
Runs. Add `-f additional_prompt="..."` for focused one-off review instructions.
|
||||
|
||||
## Sweep Reports
|
||||
@@ -175,7 +175,7 @@ gh variable set CLAWSWEEPER_ALLOW_MERGE --repo openclaw/clawsweeper --body 1
|
||||
gh variable set CLAWSWEEPER_ALLOW_AUTOMERGE --repo openclaw/clawsweeper --body 1
|
||||
```
|
||||
|
||||
Reset gates only when explicitly requested; the active maintainer window may intentionally
|
||||
Reset gates only when Peter asks; the active maintainer window may intentionally
|
||||
leave them at `1`.
|
||||
|
||||
Important gates:
|
||||
@@ -255,16 +255,15 @@ loop. The router:
|
||||
- never merges autofix PRs or draft PRs;
|
||||
- merges automerge PRs only when ClawSweeper passed the exact current head,
|
||||
checks are green, GitHub says mergeable, no human-review label is present,
|
||||
the PR is not draft, and both merge gates are open.
|
||||
|
||||
Missing changelog is not a review finding or merge blocker. If repairing a user-facing change, add/update changelog automatically when practical; never ask or block solely on it.
|
||||
the PR is not draft, required user-facing OpenClaw changelog entries are
|
||||
present, and both merge gates are open.
|
||||
|
||||
If ClawSweeper passes while merge gates are closed, it labels
|
||||
`clawsweeper:merge-ready` and comments instead of merging. `@clawsweeper stop`
|
||||
adds `clawsweeper:human-review`.
|
||||
|
||||
When asked to create a PR and enable ClawSweeper automerge, do not
|
||||
leave the local OpenClaw checkout on the PR branch. After the PR is created,
|
||||
When Peter asks Codex to create a PR and enable ClawSweeper automerge, do not
|
||||
leave his local OpenClaw checkout on the PR branch. After the PR is created,
|
||||
pushed, and the `@clawsweeper automerge` request is posted or otherwise
|
||||
confirmed, return the local checkout to `main` and fast-forward it when the
|
||||
working tree is clean:
|
||||
|
||||
@@ -31,13 +31,6 @@ pnpm crabbox:run -- --help | sed -n '1,120p'
|
||||
- Check `.crabbox.yaml` for repo defaults, but override provider explicitly.
|
||||
Even if config still says AWS, maintainer validation should normally pass
|
||||
`--provider blacksmith-testbox`.
|
||||
- For live/provider bugs, check keys on the local Mac before downgrading to
|
||||
mocks: source local `~/.profile` and test only presence/length. If Crabbox
|
||||
does not already have the key, copy only the exact needed key into the remote
|
||||
process environment for that one command. Do not print it, do not sync it as a
|
||||
repo file, and do not leave it in remote shell history or logs. If no
|
||||
secret-safe injection path is available, say true live provider auth is
|
||||
blocked instead of silently using a fake key.
|
||||
- Prefer local targeted tests for tight edit loops. Broad gates belong remote.
|
||||
- Do not treat inherited shell env as operator intent. In particular,
|
||||
`OPENCLAW_LOCAL_CHECK_MODE=throttled` from the local shell is not permission
|
||||
@@ -138,81 +131,6 @@ unclear:
|
||||
blacksmith testbox list
|
||||
```
|
||||
|
||||
## Efficient Bug E2E Verification
|
||||
|
||||
Use the smallest Crabbox lane that proves the reported user path, not just the
|
||||
touched code. Aim for one after-fix E2E proof before commenting, closing, or
|
||||
opening a PR for a user-visible bug.
|
||||
|
||||
Pick the lane by symptom:
|
||||
|
||||
- Docker/setup/install bug: build a package tarball and run the matching
|
||||
`scripts/e2e/*-docker.sh` or package script. This proves npm packaging,
|
||||
install paths, runtime deps, config writes, and container behavior.
|
||||
- Provider/model/auth bug: prefer true live E2E. First source local Mac
|
||||
`~/.profile`, then inject the single needed key into Crabbox if needed. Scrub
|
||||
unrelated provider env vars in the child command so interactive defaults do
|
||||
not drift to another provider. If only a dummy key is used, label the proof
|
||||
narrowly, e.g. "UI/install path only; live provider auth not exercised."
|
||||
- Channel delivery bug: use the channel Docker/live lane when available; include
|
||||
setup, config, gateway start, send/receive or agent-turn proof, and redacted
|
||||
logs.
|
||||
- Gateway/session/tool bug: prefer an end-to-end CLI or Gateway RPC command that
|
||||
creates real state and inspects the resulting files/API output.
|
||||
- Pure parser/config bug: targeted tests may be enough, but still run a
|
||||
Crabbox command when OS, package, Docker, secrets, or service lifecycle could
|
||||
change behavior.
|
||||
|
||||
Efficient flow:
|
||||
|
||||
1. Reproduce or prove the pre-fix symptom when feasible. If the issue cannot be
|
||||
reproduced, capture the exact command and observed behavior instead.
|
||||
2. Patch locally and run narrow local tests for edit speed.
|
||||
3. Run one Crabbox E2E command that starts from the user-facing entrypoint:
|
||||
package install, Docker setup, onboarding, channel add, gateway start, or
|
||||
agent turn as appropriate.
|
||||
4. Record proof as: Testbox id, command, environment shape, redacted secret
|
||||
source, and copied success/failure output.
|
||||
5. If the issue says "cannot reproduce", ask for the missing config/log fields
|
||||
that would distinguish the tested path from the reporter's path.
|
||||
|
||||
Keep it efficient:
|
||||
|
||||
- Reuse existing E2E scripts and helper assertions before writing ad hoc shell.
|
||||
- Use one-shot Crabbox for a single proof; use a reusable Testbox only when
|
||||
several commands must share built images, installed packages, or live state.
|
||||
- Prefer `OPENCLAW_CURRENT_PACKAGE_TGZ` with Docker/package lanes when testing a
|
||||
candidate tarball; prefer the repo's package helper instead of direct source
|
||||
execution when the bug might be packaging/install related.
|
||||
- Keep secrets redacted. It is fine to report key presence, source, and length;
|
||||
never print secret values.
|
||||
- Include `--timing-json` on broad or flaky runs when command duration or sync
|
||||
behavior matters.
|
||||
|
||||
Interactive CLI/onboarding:
|
||||
|
||||
- For full-screen or prompt-heavy CLI flows, run the target command inside tmux
|
||||
on the Crabbox and drive it with `tmux send-keys`; capture proof with
|
||||
`tmux capture-pane`, redacted through `sed`.
|
||||
- Prefer deterministic arrow navigation over search typing for Clack-style
|
||||
searchable selects. Raw `send-keys -l openai` may not trigger filtering in a
|
||||
tmux pane; inspect option order locally or on-box and send exact Down/Enter
|
||||
sequences.
|
||||
- Isolate mutable state with `OPENCLAW_STATE_DIR=$(mktemp -d)`. Plugin npm
|
||||
installs live under that state dir (`npm/node_modules/...`), not under
|
||||
`OPENCLAW_CONFIG_DIR`. Verify downloads by checking the state dir, package
|
||||
lock, and installed package metadata.
|
||||
- To test automatic setup installs against local package artifacts, use
|
||||
`OPENCLAW_ALLOW_PLUGIN_INSTALL_OVERRIDES=1` plus
|
||||
`OPENCLAW_PLUGIN_INSTALL_OVERRIDES='{"plugin-id":"npm-pack:/tmp/plugin.tgz"}'`.
|
||||
Pack with `npm pack`, set an isolated `OPENCLAW_STATE_DIR`, and verify the
|
||||
package under `npm/node_modules`. Overrides are test-only and must not be
|
||||
treated as official/trusted-source installs.
|
||||
- For OpenAI/Codex onboarding proof, the useful markers are the UI line
|
||||
`Installed Codex plugin`, `npm/node_modules/@openclaw/codex`, and the
|
||||
package-lock entry showing the bundled `@openai/codex` dependency. A dummy
|
||||
OpenAI-shaped key can prove only UI/install behavior; it is not live auth.
|
||||
|
||||
## Reuse And Keepalive
|
||||
|
||||
For most Blacksmith-backed Crabbox calls, one-shot is enough. Use reuse only
|
||||
|
||||
@@ -48,7 +48,7 @@ gitcrawl cluster-detail openclaw/openclaw --id <cluster-id> --member-limit 20 --
|
||||
|
||||
## Suppress top-maintainer items in issue triage
|
||||
|
||||
When asked for issue triage, hot issues, pressing bugs, Discord-correlated issues, or "what is still open", do not surface issues or PRs authored by top maintainers by default. Prefer external/user-reported hot issues and external PRs, not maintainer-owned work queues.
|
||||
When Peter asks for issue triage, hot issues, pressing bugs, Discord-correlated issues, or "what is still open", do not surface issues or PRs authored by top maintainers by default. He wants external/user-reported hot issues and external PRs, not maintainer-owned work queues.
|
||||
|
||||
Suppress by default when the opener/author is one of:
|
||||
|
||||
@@ -77,7 +77,7 @@ Also suppress lower-priority maintainer-owned noise from the broader keep/top-ma
|
||||
|
||||
Exceptions:
|
||||
|
||||
- Show maintainer-authored items when the requester explicitly asks for maintainer PRs/issues, PR landing candidates, release-blocking maintainer work, or a specific PR/issue number.
|
||||
- Show maintainer-authored items when Peter explicitly asks for maintainer PRs/issues, PR landing candidates, release-blocking maintainer work, or a specific PR/issue number.
|
||||
- Show a maintainer-authored item when it is the canonical fix for an external hot issue, but frame it as the fix path rather than as a user-facing issue candidate.
|
||||
- Do not close, label, or deprioritize solely because an item is maintainer-authored; this section only controls what appears in triage shortlists.
|
||||
|
||||
@@ -103,18 +103,11 @@ Exceptions:
|
||||
|
||||
When asked for `X` issues or PRs to triage, `X` means qualified candidates, not sampled threads.
|
||||
|
||||
Issue triage is review/prove/patch-local by default:
|
||||
|
||||
1. Review the issue body, comments, related threads, current code, and adjacent tests.
|
||||
2. Fix only issues that are easy, high-confidence, and narrowly owned by the implicated path.
|
||||
3. Add focused regression proof when practical.
|
||||
4. Stop with the dirty diff, touched files, and test/gate output for maintainer review.
|
||||
5. After maintainer approval to ship, make one commit per accepted fix, with its own changelog entry when user-facing.
|
||||
6. Pull/rebase, push, then comment and close only the issues that were fixed or explicitly triaged closed.
|
||||
|
||||
Do not batch unrelated issue fixes into one commit. Do not publish, comment, close, or label during the review/prove phase.
|
||||
|
||||
Missing changelog is not a PR review finding or merge blocker. If landing/fixing a user-visible change, add/update changelog automatically when practical; never ask or block solely on it.
|
||||
Triage is read/prove/patch-local by default. Do not commit unless Peter writes
|
||||
`commit` in the current instruction for the exact diff being handled. Do not
|
||||
treat earlier messages, inferred intent, "next", sweep momentum, or bundled
|
||||
publish language as commit permission. If Peter asks for follow-up work without
|
||||
saying `commit`, keep the files dirty after local fixes and proof.
|
||||
|
||||
Only list candidates that pass all gates:
|
||||
|
||||
@@ -134,29 +127,9 @@ Loop:
|
||||
|
||||
Output only qualifying candidates, with: ref, surface, proof, cause, fix sketch, why small, expected test/gate. If none qualify, say so; do not pad.
|
||||
|
||||
## Structure PR review output
|
||||
|
||||
- Start every PR review with 1-3 plain sentences explaining what the change does and why it matters. Put this before `Findings`.
|
||||
- Then list findings first. If none, say `No blocking findings` or `No findings`.
|
||||
- Always answer: bug/behavior being fixed, PR/issue URL and affected surface, and best-fix verdict.
|
||||
- Keep summaries compact, but include enough proof that the verdict is auditable without rereading the PR.
|
||||
|
||||
## Read beyond the diff
|
||||
|
||||
- Review the surrounding code path, not just changed lines. Open the caller, callee, data contracts, adjacent tests, and owner module.
|
||||
- For large-codebase PRs, sample enough related files to understand the runtime boundary before deciding. Default to more code reading when the change touches agents, gateway, plugins, auth, sessions, process, config, or provider/runtime seams.
|
||||
- Compare the PR against current `origin/main` behavior. Check whether recent main already changed the same surface.
|
||||
- Dependency-backed behavior: MUST read upstream docs/source/types before judging API use, defaults, output shapes, errors, timeouts, memory behavior, or compatibility. Do not assume dependency contracts from memory or PR text.
|
||||
- Judge solution quality, not only correctness. Ask whether the PR is the clean owner-boundary fix or a wart/workaround that should be replaced by a small refactor, moved seam, contract change, or deletion of duplicate logic.
|
||||
- Mention the main files read when the verdict depends on code-path evidence.
|
||||
|
||||
## Enforce the bug-fix evidence bar
|
||||
|
||||
- Never merge a bug-fix PR based only on issue text, PR text, or AI rationale.
|
||||
- Whenever feasible, use Crabbox (`$crabbox`) for end-to-end verification before
|
||||
commenting that a bug is unreproducible, closing an issue, or opening/landing
|
||||
a fix PR. Prefer a real packaged/Docker/live lane that exercises the reported
|
||||
user flow over unit-only proof.
|
||||
- Before landing, require:
|
||||
1. symptom evidence such as a repro, logs, or a failing test
|
||||
2. a verified root cause in code with file/line
|
||||
@@ -164,9 +137,6 @@ Output only qualifying candidates, with: ref, surface, proof, cause, fix sketch,
|
||||
4. a regression test when feasible, or explicit manual verification plus a reason no test was added
|
||||
- If the claim is unsubstantiated or likely wrong, request evidence or changes instead of merging.
|
||||
- If the linked issue appears outdated or incorrect, correct triage first. Do not merge a speculative fix.
|
||||
- If Crabbox/E2E proof is blocked, say exactly why and use the closest available
|
||||
local, Docker, mocked, or targeted proof. Do not present unit tests as real
|
||||
behavior proof.
|
||||
|
||||
## Close low-signal manual PRs carefully
|
||||
|
||||
@@ -209,9 +179,6 @@ gh search issues --repo openclaw/openclaw --match title,body --limit 50 \
|
||||
|
||||
## Follow PR review and landing hygiene
|
||||
|
||||
- Never mention merge conflicts that are relatively easy to resolve, such as
|
||||
`CHANGELOG.md` entries, in review-only output. These are landing mechanics,
|
||||
not correctness findings.
|
||||
- If bot review conversations exist on your PR, address them and resolve them yourself once fixed.
|
||||
- Leave a review conversation unresolved only when reviewer or maintainer judgment is still needed.
|
||||
- When landing or merging any PR, follow the global `/landpr` process.
|
||||
|
||||
@@ -7,19 +7,17 @@ description: Fix only small, high-certainty OpenClaw bugs from a pasted issue/PR
|
||||
|
||||
Batch workflow for pasted OpenClaw issue/PR refs.
|
||||
Execute, do not summarize.
|
||||
Triage reviews, proves, and patches local fixes first; publishing waits for Peter's manual review.
|
||||
Triage does not commit, push, create PRs, comment, close, label, land, or merge.
|
||||
|
||||
## Peter Review Gate
|
||||
|
||||
Peter always wants to review code before commits.
|
||||
Default flow:
|
||||
1. Review each issue deeply enough to prove current behavior and root cause.
|
||||
2. Fix only easy, high-confidence bugs with narrow ownership and focused proof.
|
||||
3. Stop with the dirty diff summary, touched files, and test/gate output for Peter's manual review.
|
||||
4. After Peter approves shipping, make one commit per accepted fix, with a changelog entry for each user-facing fix.
|
||||
5. Pull/rebase, push, then comment and close only the fixed or explicitly triaged-closed issues.
|
||||
|
||||
Do not batch unrelated issue fixes into one commit. Do not push, create PRs, comment, close, label, land, merge, or otherwise publish during the review/prove phase.
|
||||
After local fixes and proof, stop with the diff summary, touched files, and test/gate output.
|
||||
Do not commit unless Peter writes `commit` in the current instruction for the exact diff being handled.
|
||||
Do not treat earlier messages, inferred intent, "next", sweep momentum, or bundled publish language as commit permission.
|
||||
If Peter asks for follow-up work without saying `commit`, keep the files dirty after local fixes and proof.
|
||||
Do not push, comment, close, label, land, merge, or otherwise publish until Peter explicitly asks for that exact action after the code has been reviewed.
|
||||
If Peter asks for a bundled action like `commit push close`, first confirm the code has already been reviewed in chat; if not, stop with the dirty diff and ask for review/approval.
|
||||
|
||||
## Companion Skills
|
||||
|
||||
@@ -60,9 +58,8 @@ Skip with terse reason. Do not pad with low-confidence fixes.
|
||||
- no drive-by refactors
|
||||
- tests near failing surface
|
||||
- docs only for changed public behavior
|
||||
- no commit during the review/prove phase
|
||||
- after Peter approves shipping, one commit plus changelog per accepted user-facing fix
|
||||
- no push/create PR/comment/close/label/land/merge until Peter approves shipping after review
|
||||
- no commit unless Peter writes `commit` in the current instruction
|
||||
- no push/create PR/comment/close/label/land/merge unless explicitly asked for that exact action after review
|
||||
|
||||
## PR Rules
|
||||
|
||||
|
||||
4
.github/labeler.yml
vendored
4
.github/labeler.yml
vendored
@@ -276,10 +276,6 @@
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "extensions/memory-wiki/**"
|
||||
"extensions: oc-path":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "extensions/oc-path/**"
|
||||
"extensions: open-prose":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
|
||||
10
.github/workflows/ci.yml
vendored
10
.github/workflows/ci.yml
vendored
@@ -1742,17 +1742,7 @@ jobs:
|
||||
with:
|
||||
install-bun: "false"
|
||||
|
||||
- name: Checkout ClawHub docs source
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
repository: openclaw/clawhub
|
||||
path: clawhub-source
|
||||
fetch-depth: 1
|
||||
persist-credentials: false
|
||||
|
||||
- name: Check docs
|
||||
env:
|
||||
OPENCLAW_DOCS_SYNC_CLAWHUB_REPO: ${{ github.workspace }}/clawhub-source
|
||||
run: pnpm check:docs
|
||||
|
||||
skills-python:
|
||||
|
||||
16
.github/workflows/docs-sync-publish.yml
vendored
16
.github/workflows/docs-sync-publish.yml
vendored
@@ -22,15 +22,6 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Checkout ClawHub docs source
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
repository: openclaw/clawhub
|
||||
path: clawhub-source
|
||||
fetch-depth: 1
|
||||
persist-credentials: false
|
||||
token: ${{ secrets.OPENCLAW_DOCS_SYNC_TOKEN || github.token }}
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
@@ -57,17 +48,12 @@ jobs:
|
||||
|
||||
- name: Sync docs into publish repo
|
||||
run: |
|
||||
clawhub_sha="$(git -C "$GITHUB_WORKSPACE/clawhub-source" rev-parse HEAD)"
|
||||
node scripts/docs-sync-publish.mjs \
|
||||
--target "$GITHUB_WORKSPACE/publish" \
|
||||
--source-repo "$GITHUB_REPOSITORY" \
|
||||
--source-sha "$GITHUB_SHA" \
|
||||
--clawhub-repo "$GITHUB_WORKSPACE/clawhub-source" \
|
||||
--clawhub-source-repo "openclaw/clawhub" \
|
||||
--clawhub-source-sha "$clawhub_sha"
|
||||
--source-sha "$GITHUB_SHA"
|
||||
|
||||
- name: Install docs MDX checker dependency
|
||||
working-directory: publish
|
||||
run: npm install --no-save --package-lock=false @mdx-js/mdx@3.1.1
|
||||
|
||||
- name: Check publish docs MDX
|
||||
|
||||
2
.github/workflows/macos-release.yml
vendored
2
.github/workflows/macos-release.yml
vendored
@@ -98,5 +98,5 @@ jobs:
|
||||
echo "- Run \`openclaw/releases-private/.github/workflows/openclaw-macos-validate.yml\` with tag \`${RELEASE_TAG}\` and wait for the private mac validation lane to pass."
|
||||
echo "- Run \`openclaw/releases-private/.github/workflows/openclaw-macos-publish.yml\` with tag \`${RELEASE_TAG}\` and \`preflight_only=true\` for the full private mac preflight."
|
||||
echo "- For the real publish path, run the same private mac publish workflow from \`main\` with the successful private preflight \`preflight_run_id\` so it promotes the prepared artifacts instead of rebuilding them."
|
||||
echo "- For stable releases, the private publish workflow also publishes the signed \`appcast.xml\` to public \`main\`, or opens an appcast PR if direct push is blocked."
|
||||
echo "- For stable releases, also download \`macos-appcast-${RELEASE_TAG}\` from the successful private run and commit \`appcast.xml\` back to \`main\` in \`openclaw/openclaw\`."
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
@@ -2135,8 +2135,8 @@ jobs:
|
||||
# inside the already-isolated container to keep MCP cron/tool
|
||||
# execution representative instead of failing on nested sandbox
|
||||
# setup.
|
||||
echo 'OPENCLAW_LIVE_CLI_BACKEND_ARGS=["exec","--json","--color","never","--sandbox","danger-full-access","-c","service_tier=\"priority\"","--skip-git-repo-check"]' >> "$GITHUB_ENV"
|
||||
echo 'OPENCLAW_LIVE_CLI_BACKEND_RESUME_ARGS=["exec","resume","{sessionId}","-c","sandbox_mode=\"danger-full-access\"","-c","service_tier=\"priority\"","--skip-git-repo-check"]' >> "$GITHUB_ENV"
|
||||
echo 'OPENCLAW_LIVE_CLI_BACKEND_ARGS=["exec","--json","--color","never","--sandbox","danger-full-access","-c","service_tier=\"fast\"","--skip-git-repo-check"]' >> "$GITHUB_ENV"
|
||||
echo 'OPENCLAW_LIVE_CLI_BACKEND_RESUME_ARGS=["exec","resume","{sessionId}","-c","sandbox_mode=\"danger-full-access\"","-c","service_tier=\"fast\"","--skip-git-repo-check"]' >> "$GITHUB_ENV"
|
||||
echo "OPENCLAW_LIVE_CLI_BACKEND_DEBUG=1" >> "$GITHUB_ENV"
|
||||
echo "OPENCLAW_CLI_BACKEND_LOG_OUTPUT=1" >> "$GITHUB_ENV"
|
||||
echo "OPENCLAW_TEST_CONSOLE=1" >> "$GITHUB_ENV"
|
||||
@@ -2354,8 +2354,8 @@ jobs:
|
||||
live-cli-backend-docker)
|
||||
echo "OPENCLAW_LIVE_CLI_BACKEND_MODEL=codex-cli/gpt-5.4" >> "$GITHUB_ENV"
|
||||
echo "OPENCLAW_LIVE_CLI_BACKEND_AUTH=api-key" >> "$GITHUB_ENV"
|
||||
echo 'OPENCLAW_LIVE_CLI_BACKEND_ARGS=["exec","--json","--color","never","--sandbox","danger-full-access","-c","service_tier=\"priority\"","--skip-git-repo-check"]' >> "$GITHUB_ENV"
|
||||
echo 'OPENCLAW_LIVE_CLI_BACKEND_RESUME_ARGS=["exec","resume","{sessionId}","-c","sandbox_mode=\"danger-full-access\"","-c","service_tier=\"priority\"","--skip-git-repo-check"]' >> "$GITHUB_ENV"
|
||||
echo 'OPENCLAW_LIVE_CLI_BACKEND_ARGS=["exec","--json","--color","never","--sandbox","danger-full-access","-c","service_tier=\"fast\"","--skip-git-repo-check"]' >> "$GITHUB_ENV"
|
||||
echo 'OPENCLAW_LIVE_CLI_BACKEND_RESUME_ARGS=["exec","resume","{sessionId}","-c","sandbox_mode=\"danger-full-access\"","-c","service_tier=\"fast\"","--skip-git-repo-check"]' >> "$GITHUB_ENV"
|
||||
echo "OPENCLAW_LIVE_CLI_BACKEND_DEBUG=1" >> "$GITHUB_ENV"
|
||||
echo "OPENCLAW_CLI_BACKEND_LOG_OUTPUT=1" >> "$GITHUB_ENV"
|
||||
echo "OPENCLAW_TEST_CONSOLE=1" >> "$GITHUB_ENV"
|
||||
|
||||
10
.github/workflows/openclaw-release-checks.yml
vendored
10
.github/workflows/openclaw-release-checks.yml
vendored
@@ -599,7 +599,7 @@ jobs:
|
||||
published_upgrade_survivor_baselines: ${{ needs.resolve_target.outputs.run_release_soak == 'true' && 'last-stable-4 2026.4.23 2026.5.2 2026.4.15' || '' }}
|
||||
published_upgrade_survivor_scenarios: ${{ needs.resolve_target.outputs.run_release_soak == 'true' && 'reported-issues' || '' }}
|
||||
telegram_mode: mock-openai
|
||||
telegram_scenarios: telegram-help-command,telegram-commands-command,telegram-tools-compact-command,telegram-whoami-command,telegram-status-command,telegram-other-bot-command-gating,telegram-context-command,telegram-mentioned-message-reply,telegram-reply-chain-exact-marker,telegram-stream-final-single-message,telegram-long-final-reuses-preview,telegram-mention-gating
|
||||
telegram_scenarios: telegram-help-command,telegram-commands-command,telegram-tools-compact-command,telegram-whoami-command,telegram-context-command,telegram-current-session-status-tool,telegram-mention-gating
|
||||
secrets:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
|
||||
@@ -705,11 +705,11 @@ jobs:
|
||||
case "${QA_PARITY_LANE}" in
|
||||
candidate)
|
||||
model="${OPENCLAW_CI_OPENAI_MODEL}"
|
||||
alt_model="openai/gpt-5.5-alt"
|
||||
alt_model="openai/gpt-5.4-alt"
|
||||
;;
|
||||
baseline)
|
||||
model="anthropic/claude-opus-4-7"
|
||||
alt_model="anthropic/claude-sonnet-4-7"
|
||||
model="anthropic/claude-opus-4-6"
|
||||
alt_model="anthropic/claude-sonnet-4-6"
|
||||
;;
|
||||
*)
|
||||
echo "Unknown QA parity lane: ${QA_PARITY_LANE}" >&2
|
||||
@@ -779,7 +779,7 @@ jobs:
|
||||
--candidate-summary .artifacts/qa-e2e/gpt54/qa-suite-summary.json \
|
||||
--baseline-summary .artifacts/qa-e2e/opus46/qa-suite-summary.json \
|
||||
--candidate-label "${OPENCLAW_CI_OPENAI_MODEL}" \
|
||||
--baseline-label anthropic/claude-opus-4-7 \
|
||||
--baseline-label anthropic/claude-opus-4-6 \
|
||||
--output-dir .artifacts/qa-e2e/parity
|
||||
|
||||
- name: Upload parity artifacts
|
||||
|
||||
10
.github/workflows/qa-live-transports-convex.yml
vendored
10
.github/workflows/qa-live-transports-convex.yml
vendored
@@ -187,17 +187,17 @@ jobs:
|
||||
--parity-pack agentic \
|
||||
--concurrency "${QA_PARITY_CONCURRENCY}" \
|
||||
--model "${OPENCLAW_CI_OPENAI_MODEL}" \
|
||||
--alt-model openai/gpt-5.5-alt \
|
||||
--alt-model openai/gpt-5.4-alt \
|
||||
--output-dir .artifacts/qa-e2e/gpt54
|
||||
|
||||
- name: Run Opus 4.7 lane
|
||||
- name: Run Opus 4.6 lane
|
||||
run: |
|
||||
pnpm openclaw qa suite \
|
||||
--provider-mode mock-openai \
|
||||
--parity-pack agentic \
|
||||
--concurrency "${QA_PARITY_CONCURRENCY}" \
|
||||
--model anthropic/claude-opus-4-7 \
|
||||
--alt-model anthropic/claude-sonnet-4-7 \
|
||||
--model anthropic/claude-opus-4-6 \
|
||||
--alt-model anthropic/claude-sonnet-4-6 \
|
||||
--output-dir .artifacts/qa-e2e/opus46
|
||||
|
||||
- name: Generate parity report
|
||||
@@ -207,7 +207,7 @@ jobs:
|
||||
--candidate-summary .artifacts/qa-e2e/gpt54/qa-suite-summary.json \
|
||||
--baseline-summary .artifacts/qa-e2e/opus46/qa-suite-summary.json \
|
||||
--candidate-label "${OPENCLAW_CI_OPENAI_MODEL}" \
|
||||
--baseline-label anthropic/claude-opus-4-7 \
|
||||
--baseline-label anthropic/claude-opus-4-6 \
|
||||
--output-dir .artifacts/qa-e2e/parity
|
||||
|
||||
- name: Upload parity artifacts
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -95,10 +95,6 @@ docs/internal/
|
||||
tmp/
|
||||
IDENTITY.md
|
||||
USER.md
|
||||
# Exception: oc-path real-world test fixtures need to be tracked even
|
||||
# though the bare names match the local-untracked rule above.
|
||||
!extensions/oc-path/src/oc-path/tests/fixtures/real/IDENTITY.md
|
||||
!extensions/oc-path/src/oc-path/tests/fixtures/real/USER.md
|
||||
*.tgz
|
||||
*.tar.gz
|
||||
*.zip
|
||||
|
||||
@@ -30,7 +30,6 @@
|
||||
"eslint/no-useless-computed-key": "error",
|
||||
"eslint/no-useless-concat": "error",
|
||||
"eslint/no-useless-constructor": "error",
|
||||
"eslint/no-unused-vars": "off",
|
||||
"eslint/no-warning-comments": "error",
|
||||
"eslint/no-unmodified-loop-condition": "error",
|
||||
"eslint/no-new-wrappers": "error",
|
||||
|
||||
@@ -71,7 +71,7 @@ Telegraph style. Root rules only. Read scoped `AGENTS.md` before subtree work.
|
||||
## GitHub / CI
|
||||
|
||||
- Triage: list first, hydrate few. Use bounded `gh --json --jq`; avoid repeated full comment scans.
|
||||
- Automatic PR/issue discovery: skip maintainer-owned items unless directly relevant. Do not comment, close, label, retitle, rebase, fix up, or land them without explicit maintainer request.
|
||||
- Automatic PR/issue discovery: skip maintainer-owned items unless directly relevant. Do not comment, close, label, retitle, rebase, fix up, or land them without Peter asking.
|
||||
- PR scan/triage: no unsolicited PR comments/reviews. Report in chat only unless explicitly asked, or a close/duplicate action needs a reason comment.
|
||||
- Search/dedupe: prefer `gh search issues 'repo:openclaw/openclaw is:open <terms>' --json number,title,state,updatedAt --limit 20`.
|
||||
- GitHub search boolean text is fussy. If `OR` queries return empty, split exact terms and search title/body/comments separately before concluding no hits.
|
||||
@@ -157,10 +157,8 @@ Telegraph style. Root rules only. Read scoped `AGENTS.md` before subtree work.
|
||||
## Docs / Changelog
|
||||
|
||||
- Docs change with behavior/API. Use docs list/read_when hints; docs links per `docs/AGENTS.md`.
|
||||
- When upgrading the bundled Codex harness (`@openai/codex` in `extensions/codex/package.json`), refresh the model availability snapshot in `docs/plugins/codex-harness.md` from the new harness's `model/list` result.
|
||||
- Docs final answers: when doc files changed, end with the relevant full `https://docs.openclaw.ai/...` URL(s).
|
||||
- Changelog user-facing only; fixing an issue or landing/merging a PR needs one unless pure test/internal.
|
||||
- Missing changelog is not a PR review finding or merge blocker. If landing/fixing a user-visible change, add/update changelog automatically when practical; never ask or block solely on it.
|
||||
- Changelog placement: active version `### Changes`/`### Fixes`; contributor-facing added entries should include at least one `Thanks @author` attribution, using credited human GitHub username(s). Never add `Thanks @codex`, `Thanks @openclaw`, `Thanks @clawsweeper`, or `Thanks @steipete`; if the real credited human is unknown, leave attribution blank instead of guessing or adding a random person.
|
||||
- Changelog bullets are always single-line. No wrapping/continuation across multiple lines. Long entries stay on one long line so dedupe, PR-ref, and credit-audit tooling work and so the visual style stays uniform.
|
||||
|
||||
|
||||
1003
CHANGELOG.md
1003
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
@@ -41,7 +41,7 @@ Welcome to the lobster tank! 🦞
|
||||
- **Vincent Koc** - Agents, Telemetry, Hooks, Security
|
||||
- GitHub: [@vincentkoc](https://github.com/vincentkoc) · X: [@vincent_koc](https://x.com/vincent_koc)
|
||||
|
||||
- **Val Alexander** - UI/UX, Docs, SDK, and Agent DevX
|
||||
- **Val Alexander** - UI/UX, Docs, and Agent DevX
|
||||
- GitHub: [@BunsDev](https://github.com/BunsDev) · X: [@BunsDev](https://x.com/BunsDev)
|
||||
|
||||
- **Seb Slight** - Docs, Agent Reliability, Runtime Hardening
|
||||
@@ -86,9 +86,6 @@ Welcome to the lobster tank! 🦞
|
||||
- **Mason Huang** - Stability, Security, Speed
|
||||
- GitHub: [@hxy91819](https://github.com/hxy91819) · X: [@chenjingtalk](https://x.com/chenjingtalk)
|
||||
|
||||
- **Maurice Niu** - ClawHub, Security, Stability, Data integrity
|
||||
- GitHub: [@momothemage](https://github.com/momothemage) · X: [@MomoPsicasso](https://x.com/MomoPsicasso)
|
||||
|
||||
## How to Contribute
|
||||
|
||||
1. **Bugs & small fixes** → Open a PR!
|
||||
|
||||
@@ -160,7 +160,7 @@ RUN --mount=type=cache,id=openclaw-bookworm-apt-cache,target=/var/cache/apt,shar
|
||||
--mount=type=cache,id=openclaw-bookworm-apt-lists,target=/var/lib/apt,sharing=locked \
|
||||
apt-get update && \
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
||||
ca-certificates procps hostname curl git lsof openssl python3 tini && \
|
||||
ca-certificates procps hostname curl git lsof openssl python3 && \
|
||||
update-ca-certificates
|
||||
|
||||
RUN chown node:node /app
|
||||
@@ -287,5 +287,4 @@ USER node
|
||||
# For external access from host/ingress, override bind to "lan" and set auth.
|
||||
HEALTHCHECK --interval=3m --timeout=10s --start-period=15s --retries=3 \
|
||||
CMD node -e "fetch('http://127.0.0.1:18789/healthz').then((r)=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
|
||||
ENTRYPOINT ["tini", "-s", "--"]
|
||||
CMD ["node", "openclaw.mjs", "gateway", "--allow-unconfigured"]
|
||||
|
||||
339
appcast.xml
339
appcast.xml
@@ -2,53 +2,6 @@
|
||||
<rss xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" version="2.0">
|
||||
<channel>
|
||||
<title>OpenClaw</title>
|
||||
<item>
|
||||
<title>2026.5.7</title>
|
||||
<pubDate>Thu, 07 May 2026 22:36:27 +0000</pubDate>
|
||||
<link>https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml</link>
|
||||
<sparkle:version>2026050790</sparkle:version>
|
||||
<sparkle:shortVersionString>2026.5.7</sparkle:shortVersionString>
|
||||
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
|
||||
<description><![CDATA[<h2>OpenClaw 2026.5.7</h2>
|
||||
<h3>Fixes</h3>
|
||||
<ul>
|
||||
<li>Release/plugin publishing: retry transient ClawHub CLI dependency install failures, keep preview-passing plugins publishable when one preview cell flakes, and verify every expected ClawHub package version after publish so maintenance releases are faster to recover and less likely to hide partial plugin publishes.</li>
|
||||
<li>OpenAI: support <code>openai/chat-latest</code> as an explicit direct API-key model override for trying the moving ChatGPT Instant API alias without changing the stable default model.</li>
|
||||
<li>Cron CLI: include computed <code>status</code> in <code>cron list --json</code> and <code>cron show --json</code> output so external tooling can read disabled/running/ok/error/skipped/idle state without reimplementing cron status derivation. (#78701) Thanks @aweiker.</li>
|
||||
<li>Channels CLI: make <code>openclaw channels list</code> channel-only, add <code>--all</code> for bundled and catalog channels, render installed/configured/enabled state, and move model auth/usage details to <code>openclaw models auth list</code>, <code>openclaw status</code>, and <code>openclaw models list</code>. (#78456) Thanks @sliverp.</li>
|
||||
<li>Native commands: honor owner enforcement for native command handlers. (#78864) Thanks @pgondhi987.</li>
|
||||
<li>Active Memory: require admin scope for global memory toggles. (#78863) Thanks @pgondhi987.</li>
|
||||
<li>Gateway/sessions: clear cached skills snapshots during <code>/new</code> and <code>sessions.reset</code> so long-lived channel sessions rebuild the visible skill list after skills change. (#78873) Thanks @Evizero.</li>
|
||||
<li>Auto-reply: gate inline skill tool dispatch through before-tool-call authorization hooks. (#78517) Thanks @pgondhi987.</li>
|
||||
<li>Tavily: resolve dedicated <code>tavily_search</code> and <code>tavily_extract</code> tool credentials from the active runtime config snapshot, so <code>exec</code> SecretRef-backed API keys do not reach the tools unresolved. (#78610) Thanks @VACInc.</li>
|
||||
<li>Plugins/install: use the same absolute POSIX npm lifecycle shell for managed plugin install, rollback, repair, and uninstall npm operations as staged package updates, preventing restricted PATH shells from breaking cleanup. Thanks @vincentkoc.</li>
|
||||
<li>Agents/context engine: invalidate cached assembled context views when source history shrinks or assembly fails, preventing stale pre-reset history from being reused. Fixes #77968. (#78163) Thanks @brokemac79 and @ChrisBot2026.</li>
|
||||
<li>Discord/message: parse provider-prefixed targets like <code>discord:channel:<id></code> as channel sends instead of legacy Discord DM targets, so cross-channel agent <code>message(action="send")</code> calls no longer misroute channel IDs into misleading <code>Unknown Channel</code> failures. Fixes #78572.</li>
|
||||
<li>Agents/compaction: clamp compaction summary reserve tokens to each model's output limit so high-context compaction no longer requests invalid <code>max_tokens</code> values. (#54392) Thanks @adzendo.</li>
|
||||
<li>Commands/BTW: show the <code>/btw</code> missing-question usage placeholder with brackets so outbound channel sanitization keeps it visible. Fixes #62877. Thanks @RajvardhanPatil07.</li>
|
||||
<li>Cron/doctor: repair persisted cron jobs whose <code>payload.model</code> was stored as <code>"default"</code>, <code>"null"</code>, blank, or JSON <code>null</code> by removing the bad override during <code>openclaw doctor --fix</code> while keeping cron runtime model validation strict. Fixes #78549. Thanks @bizzle12368239.</li>
|
||||
<li>Telegram: honor <code>accessGroup:*</code> sender allowlists for DMs, groups, native commands, and callback authorization before applying Telegram's numeric sender-ID checks. Fixes #78660. Thanks @manugc.</li>
|
||||
<li>Agent delivery: report <code>deliverySucceeded=false</code> when outbound delivery returns no adapter result, so claimed/empty delivery paths no longer masquerade as successful sends. Fixes #78532. Thanks @joeyfrasier.</li>
|
||||
<li>Cron/isolated runs: fail implicit announce delivery before model execution when <code>delivery.channel=last</code> has no previous route, so recurring jobs do not spend tokens before hitting a permanent delivery-target error. Fixes #78608. Thanks @sallyom.</li>
|
||||
<li>Gateway/sessions: persist a new generated transcript file when daily gateway-agent session rollover changes the session id, while preserving custom transcript paths. Fixes #78607. Thanks @nailujac, @zerone0x, and @sallyom.</li>
|
||||
<li>Doctor/Codex OAuth: preserve working <code>openai-codex/*</code> PI routes during <code>doctor --fix</code> and recover 2026.5.5-rewritten <code>openai/*</code> GPT-5 routes when only Codex OAuth auth is available, so update repair does not break subscription-auth setups. Fixes #78407. Thanks @shakkernerd.</li>
|
||||
<li>Telegram: keep the polling watchdog tied to <code>getUpdates</code> liveness so unrelated outbound Bot API calls cannot mask a wedged inbound poller. Fixes #78422. Thanks @ai-hpc.</li>
|
||||
<li>Agents/subagents: have completed session-mode subagent registry rows honor <code>agents.defaults.subagents.archiveAfterMinutes</code> instead of a hardcoded 5-minute TTL, so registry-backed surfaces keep one retention knob across spawn modes. (#78263) Thanks @arniesaha.</li>
|
||||
<li>Plugins/channel setup: forward <code>setChannelRuntime</code> from non-bundled external plugin setup entries so deferred external channel runtime initializers are installed before startup polling. Fixes #77779. (#77799) Thanks @openperf.</li>
|
||||
<li>Telegram: treat successful same-chat <code>message</code> tool outbound sends during an inbound Telegram turn as delivered when deciding whether to emit the rewritten silent reply fallback. (#78685) Thanks @neeravmakwana.</li>
|
||||
<li>Gateway/tasks: reconcile stale CLI run-context tasks whose live run context disappeared and bound channel hot-reload deferrals so stale task records cannot block Discord/Slack/Telegram reloads forever.</li>
|
||||
<li>Discord/voice: audit Discord voice-channel permissions in <code>channels capabilities</code> and <code>channels status --probe</code>, including auto-join targets, so missing Connect/Speak/Read Message History permissions show up before <code>/vc join</code>.</li>
|
||||
<li>Discord/voice: make voice capture less choppy by extending the default post-speech silence grace to 2.5s, add <code>voice.captureSilenceGraceMs</code> for noisy Discord sessions, and tighten the spoken-output prompt around live STT fragments. Thanks @vincentkoc.</li>
|
||||
<li>WhatsApp: route proactive phone-number sends through Baileys LID forward mappings when available, so LID-addressed contacts receive agent messages instead of creating sender-only ghost chats. Fixes #67378. (#74925) Thanks @edenfunf.</li>
|
||||
<li>WhatsApp: send captioned <code>MEDIA:</code> directive auto-replies once instead of emitting an empty media message before the captioned media reply. (#78770) Thanks @ai-hpc.</li>
|
||||
<li>Codex/approvals: in Codex approval modes, stop installing the pre-guardian native <code>PermissionRequest</code> hook by default so Codex's reviewer can approve safe commands before OpenClaw surfaces an approval, remember <code>allow-always</code> decisions for identical Codex native <code>PermissionRequest</code> payloads within the active session window, and make plugin approval requests validate/render their actual allowed decisions so Telegram and other native approval UIs cannot offer stale actions. Thanks @shakkernerd.</li>
|
||||
<li>Model providers: normalize APNG sniffed PNG uploads, preserve Gemini 3 tool-call thought-signature replay with fallback signatures, accept legacy <code>__env__:VAR</code> custom-provider keys, and repair snake_case tool-call transcript sanitization. Fixes #51881, #48915, #77566, and #42858.</li>
|
||||
<li>Telegram/models: parse provider ids containing dots in <code>/models</code> callback buttons so <code>hf.co</code> model lists render as inline keyboard buttons. Fixes #38745.</li>
|
||||
</ul>
|
||||
<p><a href="https://github.com/openclaw/openclaw/blob/main/CHANGELOG.md">View full changelog</a></p>
|
||||
]]></description>
|
||||
<enclosure url="https://github.com/openclaw/openclaw/releases/download/v2026.5.7/OpenClaw-2026.5.7.zip" length="51130645" type="application/octet-stream" sparkle:edSignature="Zu+EzBGMRE1k7N4//L8HUxtUCPdO0ImrfDbgr2GrPMBrj7VGI1tOOl74gxNJoi/wfWvXz3fYVcBz2W/84ojuCw=="/>
|
||||
</item>
|
||||
<item>
|
||||
<title>2026.5.2</title>
|
||||
<pubDate>Sun, 03 May 2026 01:11:51 +0000</pubDate>
|
||||
@@ -812,5 +765,297 @@
|
||||
]]></description>
|
||||
<enclosure url="https://github.com/openclaw/openclaw/releases/download/v2026.4.29/OpenClaw-2026.4.29.zip" length="50896802" type="application/octet-stream" sparkle:edSignature="YfQ25zMGgDv8XvHbdlL/s0SMJXyu763l5ppnfjiKOjSyxZY9sfoLaoXthcctFQDXA8isR1EEb/EEausu+XkFCA=="/>
|
||||
</item>
|
||||
<item>
|
||||
<title>2026.4.27</title>
|
||||
<pubDate>Wed, 29 Apr 2026 23:53:26 +0000</pubDate>
|
||||
<link>https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml</link>
|
||||
<sparkle:version>2026042790</sparkle:version>
|
||||
<sparkle:shortVersionString>2026.4.27</sparkle:shortVersionString>
|
||||
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
|
||||
<description><![CDATA[<h2>OpenClaw 2026.4.27</h2>
|
||||
<h3>Changes</h3>
|
||||
<ul>
|
||||
<li>Sandbox/Docker: add opt-in <code>sandbox.docker.gpus</code> passthrough for Docker sandbox containers so local GPU workloads can run inside sandboxed agents when the host Docker runtime supports <code>--gpus</code>. Fixes #57976; carries forward #58124. Thanks @cyan-ember.</li>
|
||||
<li>iOS/Gateway: add an authenticated <code>node.presence.alive</code> protocol event and <code>node.list</code> last-seen fields so background iOS wakes can mark paired nodes recently alive without treating them as connected. Carries forward #63123. Thanks @ngutman.</li>
|
||||
<li>Android: publish authenticated <code>node.presence.alive</code> events after node connect and background transitions so paired Android nodes retain durable last-seen metadata after disconnects. Carries forward #63123. Thanks @ngutman.</li>
|
||||
<li>Gateway/chat: accept non-image attachments through <code>chat.send</code> by staging them as agent-readable media paths, while keeping unsupported RPC attachment paths explicit instead of silently dropping files. Fixes #48123. (#67572) Thanks @samzong.</li>
|
||||
<li>Security/networking: add opt-in operator-managed outbound proxy routing (proxy.enabled + proxy.proxyUrl/OPENCLAW_PROXY_URL) with strict http:// forward-proxy validation, loopback-only Gateway bypass, and cleanup of proxy env/dispatcher state on exit. (#70044) Thanks @jesse-merhi and @joshavant.</li>
|
||||
<li>Dependencies: refresh provider and tooling dependencies, including AWS SDK, PI runtime packages, AJV, Feishu SDK, Anthropic SDK, tokenjuice, and native TypeScript/oxlint tooling. Thanks @dependabot.</li>
|
||||
<li>Matrix/QA: add live Matrix approval scenarios for exec metadata, chunked fallback, plugin approvals, deny reactions, thread targeting, and <code>target: "both"</code> delivery, with redacted artifacts preserving safe approval summaries. Thanks @gumadeiras.</li>
|
||||
<li>Codex: add Computer Use setup for Codex-mode agents, including <code>/codex computer-use status/install</code>, marketplace discovery, optional auto-install, and fail-closed MCP server checks before Codex-mode turns start. Fixes #72094. (#71842) Thanks @pash-openai.</li>
|
||||
<li>Apps: consume Peekaboo 3.0.0-beta4 and ElevenLabsKit 0.1.1, align Swabble on Commander 0.2.2, and refresh macOS/iOS SwiftPM resolutions against the released dependency graph. Thanks @Blaizzy.</li>
|
||||
<li>Plugin SDK: expose shared channel route normalization, parser-driven target resolution, raw-target compact keys, parsed-target types, and route comparison helpers through <code>openclaw/plugin-sdk/channel-route</code>, switch native approval origin matching onto that route contract with optional delivery and match-only target normalization, and retire the internal channel-route shim behind dated compatibility aliases for legacy key/comparable-target helpers. Thanks @vincentkoc.</li>
|
||||
<li>Docs/Codex: document how Codex Computer Use, direct <code>cua-driver mcp</code>, and OpenClaw.app's PeekabooBridge fit together so desktop-control setup choices are clearer. Thanks @pash-openai and @trycua.</li>
|
||||
<li>Matrix/streaming: stream tool-progress updates into live Matrix preview edits by default when preview streaming is active, with <code>streaming.preview.toolProgress: false</code> to keep answer previews while hiding interim tool lines. Thanks @gumadeiras.</li>
|
||||
<li>Plugins/models: wire manifest <code>modelCatalog.aliases</code> and <code>modelCatalog.suppressions</code> into model-catalog planning and built-in model suppression, with stale Spark and Qwen Coding Plan suppressions now declared in plugin manifests instead of runtime fallback hooks. Thanks @shakkernerd.</li>
|
||||
<li>Plugin SDK/models: add a shared manifest-backed provider catalog builder and move Qianfan, Xiaomi, NVIDIA, Cerebras, Mistral, Moonshot, DeepSeek, Tencent TokenHub, and StepFun provider catalogs onto their plugin manifest <code>modelCatalog</code> rows. Thanks @shakkernerd.</li>
|
||||
<li>Plugin SDK/models: move BytePlus and Volcano Engine standard and plan-provider catalogs into plugin manifest <code>modelCatalog</code> rows and remove the now-unused Volcengine-family shared catalog SDK subpath. Thanks @shakkernerd.</li>
|
||||
<li>CLI/models: move Fireworks and Together AI fixed provider catalogs into plugin manifest <code>modelCatalog</code> rows so provider-filtered listing can use manifest-backed static rows. Thanks @shakkernerd.</li>
|
||||
<li>Channels/Yuanbao: register the Tencent Yuanbao external channel plugin (<code>openclaw-plugin-yuanbao</code>) in the official channel catalog, contract suites, and community plugin docs, with a new <code>docs/channels/yuanbao.md</code> quick-start guide for WebSocket bot DMs and group chats. (#72756) Thanks @loongfay.</li>
|
||||
<li>Channels/Yuanbao: add a channel docs entrance so the Tencent Yuanbao bot appears in the channel listing and sidebar navigation. (#73443) Thanks @loongfay.</li>
|
||||
<li>Channels/QQBot: add full group chat support (history tracking, @-mention gating, activation modes, per-group config, FIFO message queue with deliver debounce), C2C <code>stream_messages</code> streaming with a <code>StreamingController</code> lifecycle manager, unified <code>sendMedia</code> with chunked upload for large files, and refactor the engine into pipeline stages, focused outbound submodules, builtin slash-command modules, and explicit DI ports via <code>createEngineAdapters()</code>. (#70624) Thanks @cxyhhhhh.</li>
|
||||
<li>Plugins/startup: migrate bundled plugin manifests to explicit <code>activation.onStartup</code> declarations so Gateway startup imports only the bundled plugins that intentionally register startup-time runtime surfaces. Thanks @shakkernerd.</li>
|
||||
<li>Plugins/startup: add an opt-in future-mode gate for disabling deprecated implicit startup sidecar loading while preserving explicit startup and narrower activation triggers. Thanks @shakkernerd.</li>
|
||||
<li>Plugins/startup: add plugin compatibility warnings for deprecated implicit startup loading so authors can migrate to explicit <code>activation.onStartup</code> metadata. Thanks @shakkernerd.</li>
|
||||
<li>Plugins/runtime: load bundled agent tool-result middleware from manifest contracts on demand so tokenjuice stays startup-lazy without losing Pi/Codex tool-output compaction. Thanks @shakkernerd.</li>
|
||||
<li>Plugins/startup: add explicit <code>activation.onStartup</code> metadata so plugins can declare Gateway startup import behavior while the deprecated implicit sidecar fallback remains for legacy plugins. Thanks @shakkernerd.</li>
|
||||
<li>Gateway/startup: reuse lookup-table plugin manifests when loading startup plugins so Gateway boot avoids rebuilding plugin discovery and manifest metadata. Thanks @shakkernerd.</li>
|
||||
<li>CLI/models: declare fixed Qianfan, Xiaomi, NVIDIA, Cerebras, Mistral, Chutes, Kilo, OpenAI, and OpenCode Go model catalogs in refreshable plugin manifests, keep broad <code>models list --all</code> on raw registry and supplement rows without runtime normalization, and avoid duplicate supplement resolution. Thanks @shakkernerd.</li>
|
||||
<li>Gateway/runtime: reuse the current plugin metadata snapshot for provider discovery so repeated model-provider discovery avoids rebuilding plugin manifest metadata. Thanks @shakkernerd.</li>
|
||||
<li>Gateway/startup: pass the plugin metadata snapshot from config validation into plugin bootstrap so startup reuses one manifest product instead of rebuilding plugin metadata. Thanks @shakkernerd.</li>
|
||||
<li>Plugin SDK/testing: move core-only channel contract fixtures under the channel contract test tree and retire the old <code>test/helpers/channels</code> bridge directory so plugin tests stay on focused SDK surfaces. Thanks @vincentkoc.</li>
|
||||
<li>Plugin SDK/testing: expose native agent-runtime contract fixtures through <code>plugin-sdk/agent-runtime-test-contracts</code>, move sandbox config fixtures into the focused generic fixture subpath, and block extension tests from importing repo-only <code>test/helpers</code> bridges. Thanks @vincentkoc.</li>
|
||||
<li>Plugin SDK/testing: expose generic module reload, bundled-path, Node builtin mock, channel pairing/envelope, HTTP server, temp-home, replay-policy, and live STT helpers through focused SDK test subpaths so extension tests no longer depend on repo-only helper bridges. Thanks @vincentkoc.</li>
|
||||
<li>Plugin SDK: move maintained bundled channels off the deprecated <code>channel-config-schema-legacy</code> subpath, add an explicit bundled-channel schema SDK surface, and track both remaining legacy test/config compatibility barrels with dated removal windows. Thanks @vincentkoc.</li>
|
||||
<li>Plugin SDK/testing: expose media provider capability assertions and provider HTTP mocks through focused SDK test subpaths, and retire the repo-only media-generation test helper bridge. Thanks @vincentkoc.</li>
|
||||
<li>Plugin SDK/testing: promote bundled plugin/provider/channel contract helpers to focused SDK test subpaths and retire the repo-only <code>test/helpers/plugins</code> TypeScript bridge. Thanks @vincentkoc.</li>
|
||||
<li>Plugin SDK/testing: expose generic channel action, setup, status, and directory contract helpers through <code>plugin-sdk/channel-test-helpers</code> so bundled extension tests no longer import repo-only channel helper bridges. Thanks @vincentkoc.</li>
|
||||
<li>Plugin SDK/testing: add <code>plugin-sdk/channel-target-testing</code> for shared channel target-resolution cases, document channel reaction helpers on <code>plugin-sdk/channel-feedback</code>, and keep the old <code>plugin-sdk/test-utils</code> alias as compatibility-only. Thanks @vincentkoc.</li>
|
||||
<li>Plugin SDK/testing: add a focused generic fixture subpath for CLI capture, sandbox, skill, agent-message, system-event, terminal, chunking, auth-token, and typed-case helpers. Thanks @vincentkoc.</li>
|
||||
<li>Plugin SDK/testing: add focused plugin runtime and environment fixture subpaths so plugin tests can avoid the broad <code>plugin-sdk/testing</code> barrel for common setup helpers. Thanks @vincentkoc.</li>
|
||||
<li>Plugin SDK/testing: add a focused <code>plugin-sdk/plugin-test-api</code> helper subpath and move bundled plugin registration tests off the repo-only plugin API bridge. Thanks @vincentkoc.</li>
|
||||
<li>Plugin SDK: add generic host hooks for session state, next-turn context, trusted tool policy, UI descriptors, events, scheduler cleanup, and run-scoped plugin context. (#72287) Thanks @100yenadmin.</li>
|
||||
<li>Plugin SDK/testing: expose provider catalog, wizard, registry, manifest, public-artifact, outbound, and TTS contract helpers through documented SDK testing seams so bundled plugin tests no longer import repo <code>src/**</code> internals. Thanks @vincentkoc.</li>
|
||||
<li>Providers/DeepInfra: add a bundled DeepInfra provider with <code>DEEPINFRA_API_KEY</code> onboarding, dynamic OpenAI-compatible model discovery, image generation/editing, image/audio media understanding, TTS, text-to-video, memory embeddings, static catalog metadata, and provider-owned base URL policy. Carries forward #53805, #48088, #37576, #43896, #11533, and #2554. Thanks @ats3v.</li>
|
||||
<li>Matrix: attach versioned structured approval metadata to pending approval messages so capable Matrix clients can render richer approval UI while body text and reaction fallback keep working. (#72432) Thanks @kakahu2015.</li>
|
||||
</ul>
|
||||
<h3>Fixes</h3>
|
||||
<ul>
|
||||
<li>Gateway/sessions: align <code>chat.history</code> and <code>sessions.list</code> thinking defaults with owning-agent and catalog-aware resolution so Control UI session defaults match backend runtime state. (#63418) Thanks @jpreagan.</li>
|
||||
<li>Devices/pairing: recover array-shaped device and node pairing state files before persisting approvals, so UUID-keyed pending and paired entries no longer disappear after a malformed JSON store write. Fixes #63035. Thanks @sar618.</li>
|
||||
<li>Gateway/auth: clear reused stale device tokens and stop reconnecting on device-token mismatch in the Control UI and Node gateway clients, avoiding rate-limit loops after scope-upgrade or token-rotation handoffs. Fixes #71609. Thanks @ricksayhi.</li>
|
||||
<li>Gateway/approvals: treat duplicate same-decision approval resolves as idempotent during the resolved-entry grace window, including consumed <code>allow-once</code> approvals, while returning an explicit already-resolved error for conflicting repeats. Fixes #59162; refs #58479 and #65486. Thanks @wikithoughts, @sajazuniga7-coder, and @mjmai20682068-create.</li>
|
||||
<li>Channels/Telegram: honor <code>approvals.exec/plugin.targets[].accountId</code> when routing native approvals across multi-bot Telegram accounts while preserving unscoped Telegram targets for any account. Fixes #69916. Thanks @joerod26.</li>
|
||||
<li>Telegram/gateway: bound outbound Bot API calls and cache bundled plugin alias lookup so slow Telegram sends or WSL2 filesystem scans no longer wedge gateway replies. (#74210) Thanks @obviyus.</li>
|
||||
<li>Agents/exec: omit the internal session-resume fallback preface from successful async exec completion messages sent directly back to chat. Fixes #67181. Thanks @raistlin88.</li>
|
||||
<li>Agents/media: register detached <code>video_generate</code> and <code>music_generate</code> tool run contexts until terminal status, so Discord-backed provider jobs stay live in <code>/tasks</code> instead of becoming <code>lost</code> when the parent chat run context disappears. Thanks @vincentkoc.</li>
|
||||
<li>Agents/media: prefer OpenAI image and video providers when the default model uses the OpenAI Codex auth alias, so auto media generation no longer falls through to Fal before GPT Image or Sora. Thanks @vincentkoc.</li>
|
||||
<li>Tasks/media: infer agent ownership for session-scoped task records so <code>/tasks</code> agent-local fallback includes session-backed <code>video_generate</code> and other async media jobs even when the current chat session has no linked rows. Thanks @vincentkoc.</li>
|
||||
<li>Agents/media: keep long-running <code>video_generate</code> and <code>music_generate</code> tasks fresh while provider jobs are still pending, so task maintenance does not mark active Discord media renders lost before completion. Thanks @vincentkoc.</li>
|
||||
<li>CLI/status: treat scope-limited gateway probes as reachable-but-degraded in shared status scans, so <code>openclaw status --all</code> no longer reports a live gateway as unreachable after <code>missing scope: operator.read</code>. Fixes #49180; supersedes #47981. Thanks @openjay.</li>
|
||||
<li>CLI/update: skip tracked plugins disabled in config during post-update plugin sync before npm, ClawHub, or marketplace update checks, preserving their install records without failing the update. Fixes #73880. Thanks @islandpreneur007.</li>
|
||||
<li>Slack/Socket Mode: use a 15s Slack SDK pong timeout by default and add <code>channels.slack.socketMode.clientPingTimeout</code>, <code>serverPingTimeout</code>, and <code>pingPongLoggingEnabled</code> overrides so stale-websocket handling no longer depends on app-event health heuristics. Fixes #14248; refs #58519, #64009, and #63488. Thanks @shivasymbl and @freerk.</li>
|
||||
<li>Slack/media: bound private file and forwarded attachment downloads with idle and total timeouts while preserving placeholder fallback, so stalled Slack <code>file_share</code> media no longer wedges inbound message handling. Fixes #61850. Thanks @bassboy2k.</li>
|
||||
<li>Plugins/inspector: keep bundled plugin runtime capture quiet and config-tolerant for Codex, memory-lancedb, Feishu, Mattermost, QQBot, and Tlon so plugin-inspector JSON checks can validate the full bundled set. Thanks @vincentkoc.</li>
|
||||
<li>Slack/auto-reply: keep fully consumed text reset triggers such as <code>new session</code> out of <code>BodyForAgent</code> after directive cleanup, so configured Slack reset phrases do not leak into the fresh model turn. Fixes #73137. Thanks @neeravmakwana.</li>
|
||||
<li>Plugins/runtime deps: prune stale retained bundled runtime deps and keep doctor/secret channel contract scans on lightweight artifacts, so disabled bundled channels stop preserving old dependency trees or importing heavy plugin surfaces. Thanks @SymbolStar and @vincentkoc.</li>
|
||||
<li>Plugins/runtime deps: cache unchanged bundled runtime mirror dist-file materialization decisions and close file-lock handles on owner-write failures, reducing repeated startup chunk scans and avoiding FileHandle-GC recovery stalls. Refs #73532. Thanks @oadiazp and @bstanbury.</li>
|
||||
<li>Auto-reply: bound the post-run pending tool-result delivery drain with a progress-aware idle timeout, so a never-settling tool-result task no longer leaves the session active forever while slow healthy deliveries can keep draining. Fixes #53889; supersedes #64733 and #73434. Thanks @zijunl and @wujiaming88.</li>
|
||||
<li>Gateway/startup: start chat channels without waiting for primary model prewarm, keeping model warmup bounded in the background so Slack and other channels come online promptly when provider discovery is slow. Supersedes #73420. Thanks @dorukardahan.</li>
|
||||
<li>Gateway/install: carry env-backed config SecretRefs such as <code>channels.discord.token</code> into generated service environments when they are present only in the installing shell, while keeping gateway auth SecretRefs non-persisted. Fixes #67817; supersedes #73426. Thanks @wdimaculangan and @ztexydt-cqh.</li>
|
||||
<li>Auto-reply/commands: stop bare <code>/reset</code> and <code>/new</code> after reset hooks acknowledge the command, so non-ACP channels no longer fall through into empty provider calls while <code>/reset <message></code> and <code>/new <message></code> still seed the next model turn. Fixes #73367 and #73412. Thanks @hoyanhan, @wenxu007, and @amdhelper.</li>
|
||||
<li>Providers/DeepSeek: backfill DeepSeek V4 <code>reasoning_content</code> on plain assistant replay messages as well as tool-call turns, so thinking sessions with prior tool use no longer fail follow-up requests with missing reasoning content. Fixes #73417; refs #71372. Thanks @34262315716 and @Bartok9.</li>
|
||||
<li>Agents/gateway tool: strip full config payloads from <code>config.patch</code> and <code>config.apply</code> tool responses while preserving direct RPC responses, so config-heavy sessions no longer replay large redacted configs into transcript history. Fixes #47610; supersedes #73439. Thanks @HanenVit and @juan-flores077.</li>
|
||||
<li>Auto-reply: preserve voice-note media from silent turns while continuing to suppress text and non-voice media, so <code>NO_REPLY</code> TTS replies still deliver the requested audio bubble. (#73406) Thanks @zqchris.</li>
|
||||
<li>Channels/Mattermost: stop enqueueing regular inbound posts as system events, so Mattermost user messages reach the model only as user-role inbound-envelope content instead of also appearing as <code>System: Mattermost message...</code> directives. Fixes #71795. Thanks @juan-flores077.</li>
|
||||
<li>Agents/media: qualify bare <code>agents.defaults.imageModel</code> and <code>pdfModel</code> refs from unique configured image-capable providers, so Ollama vision models such as <code>moondream</code> and <code>qwen2.5vl:7b</code> do not fall through to the default provider. Fixes #38816; supersedes #73396. Thanks @alainasclaw and @vincentkoc.</li>
|
||||
<li>Agents/Anthropic: send implicit Anthropic beta headers only to direct public Anthropic endpoints, including OAuth, so custom Anthropic-compatible providers no longer mis-handle unsupported beta flags unless explicitly configured. Refs #73346. Thanks @byBrodowski.</li>
|
||||
<li>Skills: require explicit <code>skills.entries.coding-agent.enabled</code> before exposing the bundled coding-agent skill, so installs with Codex on PATH but no OpenAI auth do not silently offer Codex delegation. Fixes #73358. Thanks @LaFleurAdvertising and @Sanjays2402.</li>
|
||||
<li>Plugins/startup: treat manifestless Claude bundles as valid installed-plugin registry entries instead of stale missing manifests, so workspace bundles no longer force repeated derived registry rebuilds or noisy <code>plugins.entries.workspace</code> warnings during Gateway startup. Fixes #73433. Thanks @AnneVoss.</li>
|
||||
<li>Agents/subagents: preserve <code>sessions_yield</code> as a paused subagent state and ignore its wait text while freezing completion output, so parent sessions wait for the final post-compaction answer instead of receiving intermediate progress or <code>(no output)</code>. Fixes #73413. Thanks @Ask-sola.</li>
|
||||
<li>Plugins/startup: precompute bundled runtime mirror fingerprints before taking the mirror lock and keep Docker bundled plugin runtime deps/mirrors in a Docker-managed volume instead of the Windows/WSL config bind mount, so cold starts avoid slow host-volume mirror writes. Fixes #73339. Thanks @1yihui.</li>
|
||||
<li>Plugins/runtime deps: refresh bundled runtime mirrors without deleting active import trees, so config-triggered restarts do not see transient missing plugin files during registration. Thanks @shakkernerd.</li>
|
||||
<li>Channels/LINE: persist inbound image, video, audio, and file downloads in <code>~/.openclaw/media/inbound/</code> instead of temporary files so agents can still read LINE media after <code>/tmp</code> cleanup. Fixes #73370. Thanks @hijirii and @wenxu007.</li>
|
||||
<li>CLI/plugins: keep bundled plugin installs out of <code>plugins.load.paths</code> while preserving install records, so install/inspect/doctor loops no longer warn about the current bundled plugin directory. Thanks @vincentkoc.</li>
|
||||
<li>CLI/plugins: scope <code>plugins inspect <id></code> runtime loading to the matched plugin so single-plugin inspection does not load every plugin before checking the target. Thanks @shakkernerd.</li>
|
||||
<li>CLI/plugins: remove managed copied-path plugin directories during uninstall and plan uninstall from metadata instead of runtime-loading plugins, so plugin lifecycle commands avoid unnecessary bundled runtime-deps work. Thanks @shakkernerd.</li>
|
||||
<li>Cron tool: infer the creating session's agentId for <code>cron.add</code> jobs when <code>agentId</code> is omitted or passed as undefined, keeping scheduled agentTurn jobs routed to the session agent; #40571 identified the guard bug and supplied the focused regression coverage. Thanks @ChanningYul.</li>
|
||||
<li>Cron/Telegram: add <code>--thread-id</code> to <code>openclaw cron add</code> and <code>openclaw cron edit</code>, preserving Telegram forum topic delivery targets across scheduled announcements. Carries forward #51581, #60373, and #60890. Thanks @ChunHao-dev.</li>
|
||||
<li>Cron/Telegram: preserve session-derived Telegram topic thread IDs when isolated cron delivery explicitly targets the parent chat, keeping bare chat targets in the active forum topic without leaking stale topics to other chats. Carries forward #64708. Thanks @addelh.</li>
|
||||
<li>Memory/compaction: keep pre-compaction memory-flush prompts runtime-only so session transcripts and <code>chat.history</code> no longer expose them as normal user turns. Fixes #54408 and #58956; refs #43567. Thanks @markgong and @guoyuhang9.</li>
|
||||
<li>Control UI/WebChat: keep large attachment payloads out of Lit state and optimistic chat messages, using object URL previews plus send-time payload serialization so PDF/image uploads no longer trigger <code>RangeError: Maximum call stack size exceeded</code>. Fixes #73360; refs #54378 and #63432. Thanks @hejunhui-73, @Ansub, and @christianhernandez3-afk.</li>
|
||||
<li>Agents/Anthropic: cancel stalled Anthropic Messages SSE body reads when abort signals fire, so active-memory timeouts release transport resources instead of leaving hidden recall runs parked on <code>reader.read()</code>. Refs #72965 and #73120. Thanks @wdeveloper16.</li>
|
||||
<li>Control UI/WebChat: keep pending run and typing state attached to the active client run, so unowned inject/announce/side-result finals no longer unlock unrelated active runs while completed owned runs still clear promptly. Fixes #57795; carries forward the narrow diagnosis from #57887. Thanks @haoyu-haoyu.</li>
|
||||
<li>Sandbox/Docker: stop satisfying a missing default sandbox image by tagging plain Debian as <code>openclaw-sandbox:bookworm-slim</code>, preserving the Python tooling required by sandbox write/edit helpers and directing users to build the default image. Fixes #51185; refs #45108, #51099, #51609, and #57713. Thanks @dpalis, @Tin55FoilDev, @jbcohen2-coder, @macminihal-cyber, and @PraxoOnline.</li>
|
||||
<li>Control UI/WebChat: confirm toolbar New Session button resets before dispatching <code>/new</code> while leaving typed <code>/new</code> and <code>/reset</code> commands immediate. Fixes #45800; refs #27065, #56611, #54499, and #27110. Thanks @aethnova, @kosta228-huli, @adambezemek, and @xss925175263 (xianshishan).</li>
|
||||
<li>Agents/models: keep per-agent primary models strict when <code>fallbacks</code> is omitted, so probe-only custom providers are not tried as hidden fallback candidates unless the agent explicitly opts in. Fixes #73332. Thanks @haumanto.</li>
|
||||
<li>Gateway/models: add <code>models.pricing.enabled</code> so offline or restricted-network installs can skip startup OpenRouter and LiteLLM pricing-catalog fetches while keeping explicit model costs working. Fixes #53639. Thanks @callebtc, @palewire, and @rjdjohnston.</li>
|
||||
<li>Gateway/startup: warn when legacy <code>CLAWDBOT_*</code> or <code>MOLTBOT_*</code> environment variables are still present, pointing users to <code>OPENCLAW_*</code> names instead of failing silently. Fixes #53482; carries forward #53667. Thanks @lndyzwdxhs.</li>
|
||||
<li>Onboarding: pin interactive and non-interactive health checks to the just-configured setup token/password so stale <code>OPENCLAW_GATEWAY_TOKEN</code> or <code>OPENCLAW_GATEWAY_PASSWORD</code> values do not produce false gateway-token-mismatch failures after setup. Fixes #72203. Thanks @galiniliev.</li>
|
||||
<li>Doctor/state: require an interactive confirmation before archiving orphan transcript files, so <code>openclaw doctor --fix</code> no longer silently renames recoverable session history after upgrades regenerate <code>sessions.json</code>. Fixes #73106. Thanks @scottgl9.</li>
|
||||
<li>Cron/Telegram: preserve explicit <code>:topic:</code> delivery targets over stale session-derived thread IDs when isolated cron announces to Telegram forum topics. Carries forward #59069; refs #49704 and #43808. Thanks @roytong9.</li>
|
||||
<li>Build/runtime: write the runtime-postbuild stamp after <code>pnpm build</code> writes the build stamp, so the next CLI invocation does not re-sync runtime artifacts after a successful build. Fixes #73151. Thanks @bittoby.</li>
|
||||
<li>Build/runtime: preserve staged bundled-plugin runtime dependency caches across source-checkout tsdown rebuilds, so local CLI and gateway-watch rebuilds no longer recreate large plugin dependency trees before starting. Refs #73205. Thanks @SymbolStar.</li>
|
||||
<li>CLI/channels: list configured chat channel accounts from read-only setup metadata even when the standalone CLI has not loaded the runtime channel registry, so <code>openclaw channels list</code> shows Telegram accounts before auth providers. Fixes #73319 and #73322. Thanks @mlaihk.</li>
|
||||
<li>CLI/model probes: keep <code>infer model run --gateway</code> raw by skipping prior session transcript, bootstrap context, context-engine assembly, tools, and bundled MCP servers, so local backends can be tested without full agent-context overhead. Fixes #73308. Thanks @ScientificProgrammer.</li>
|
||||
<li>CLI/image describe: pass <code>--prompt</code> and <code>--timeout-ms</code> through <code>infer image describe</code> and <code>describe-many</code>, so custom vision instructions and slow local model budgets reach media-understanding providers such as Ollama, OpenAI, Google, and OpenRouter. Addresses #63700. Thanks @cedricjanssens.</li>
|
||||
<li>Providers/Ollama: reject long non-linguistic Kimi/GLM symbol runs as provider failures instead of storing them as successful visible assistant replies, so fallback or error handling can recover from garbled cloud output. Fixes #64262; refs #67019. Thanks @Kloz813 and @xiaomenger123.</li>
|
||||
<li>CLI/model probes: reject empty or whitespace-only <code>infer model run --prompt</code> values before calling local providers or the Gateway, so smoke checks do not spend provider calls on invalid turns. Fixes #73185. Thanks @iot2edge.</li>
|
||||
<li>Gateway/media: route text-only <code>chat.send</code> image offloads through media-understanding fields so <code>agents.defaults.imageModel</code> can describe WebChat attachments instead of leaving only an opaque <code>media://inbound</code> marker. Fixes #72968. Thanks @vorajeeah.</li>
|
||||
<li>Gateway/Windows: route no-listener restart handoffs through the Windows supervisor without leaving restart tokens in flight, so failed task scheduling can be retried and successful handoffs do not coalesce later restart requests. (#69056) Thanks @Thatgfsj.</li>
|
||||
<li>Gateway/model pricing: skip plugin manifest discovery during background pricing refreshes when <code>plugins.enabled: false</code>, so disabled-plugin setups do not keep rebuilding plugin metadata from the Gateway hot path. Fixes #73291. Thanks @slideshow-dingo and @fishgills.</li>
|
||||
<li>Ollama/thinking: validate <code>/think</code> commands against live Ollama catalog reasoning metadata and preserve explicit native <code>params.think</code>/<code>params.thinking</code>, so models whose <code>/api/show</code> capabilities include <code>thinking</code> expose <code>low</code>, <code>medium</code>, <code>high</code>, and <code>max</code> instead of being stuck on <code>off</code>. Fixes #73366. Thanks @cymise.</li>
|
||||
<li>Gateway/sessions: remove automatic oversized <code>sessions.json</code> rotation backups, deprecate <code>session.maintenance.rotateBytes</code>, and teach <code>openclaw doctor --fix</code> to remove the ignored key so hot session writes no longer copy multi-MB stores. Refs #72338. Thanks @midhunmonachan and @DougButdorf.</li>
|
||||
<li>Channels/Telegram: fail fast when Telegram rejects the startup <code>getMe</code> token probe with 401, so invalid or stale BotFather tokens are reported as token auth failures instead of misleading <code>deleteWebhook</code> cleanup failures. Fixes #47674. Thanks @samaedan-arch.</li>
|
||||
<li>ACPX: keep generated Codex and Claude ACP wrapper startup paths working when remote or special state filesystems reject chmod, since OpenClaw invokes the wrappers through Node instead of executing them directly. Fixes #73333. Thanks @david-garcia-garcia.</li>
|
||||
<li>CLI/onboarding: infer image input for common custom-provider vision model IDs, ask only for unknown models, and keep <code>--custom-image-input</code>/<code>--custom-text-input</code> overrides so vision-capable proxies do not get saved as text-only configs. Fixes #51869. Thanks @Antsoldier1974.</li>
|
||||
<li>Models/OpenAI Codex: stop listing or resolving unsupported <code>openai-codex/gpt-5.4-mini</code> rows through Codex OAuth, keep stale discovery rows suppressed with a clear API-key-route hint, and leave direct <code>openai/gpt-5.4-mini</code> available. Fixes #73242. Thanks @0xCyda.</li>
|
||||
<li>Plugin SDK: restore the root <code>stringEnum</code> and <code>optionalStringEnum</code> exports on both the published SDK entry and runtime root-alias bridge, so older external plugins can keep building and loading while migrating to focused SDK subpaths. Fixes #68279. Thanks @marzliak.</li>
|
||||
<li>Plugin SDK: restore the root-alias bridge for <code>registerContextEngine</code> and expose missing legacy compat helpers <code>normalizeAccountId</code> and <code>resolvePreferredOpenClawTmpDir</code> so older external plugins such as <code>openclaw-weixin</code> can keep loading while migrating to focused SDK subpaths. Fixes #53497. Thanks @alanxchen85.</li>
|
||||
<li>Auth profiles: make <code>openclaw doctor --fix</code> migrate legacy flat <code>auth-profiles.json</code> files such as <code>{ "ollama-windows": { "apiKey": "ollama-local" } }</code> to canonical provider default API-key profiles with a backup, so custom Ollama/OpenAI-compatible providers recover cleanly after upgrading. Fixes #59629; supersedes #59642. Thanks @Xsanders555 and @Linux2010.</li>
|
||||
<li>Memory/Dreaming: retry Dream Diary once with the session default when a configured dreaming model is unavailable, while leaving subagent trust and allowlist errors visible instead of silently masking configuration problems. Refs #67409 and #69209. Thanks @Ghiggins18 and @everySympathy.</li>
|
||||
<li>Feishu/inbound files: recover CJK filenames from plain <code>Content-Disposition: filename=</code> download headers when Feishu exposes UTF-8 bytes through Latin-1 header decoding, while leaving valid Latin-1 and JSON-derived names unchanged. (#48578, #50435, #59431) Thanks @alex-xuweilong, @lishuaigit, and @DoChaoing.</li>
|
||||
<li>Channels/Telegram: normalize accidental full <code>/bot<TOKEN></code> Telegram <code>apiRoot</code> values at runtime and teach <code>openclaw doctor --fix</code> to remove the suffix, so startup control calls no longer 404 when direct Bot API curl commands work. Fixes #55387. Thanks @brendanmatthewjones-cmyk, @techfindubai-ux, and @Sivlerback-Chris.</li>
|
||||
<li>Zalo Personal: persist refreshed <code>zca-js</code> session cookies after QR login, session restore, and successful API calls so gateway restarts restore the freshest local session. (#73277) Thanks @darkamenosa.</li>
|
||||
<li>Logging/security: redact sensitive tokens (sk-\* keys, Bearer/Authorization values, etc.) at the subsystem console sink so <code>createSubsystemLogger().info/warn/error</code> output that bypasses the patched console-capture handler still applies the same redaction the file transport already does. Fixes #73284; refs #67953 and #64046. Thanks @edwin-rivera-dev.</li>
|
||||
<li>Plugins/runtime deps: reuse enclosing versioned cache roots when bundled plugins resolve from nested staged paths, so plugin-runtime-deps no longer mints <code>openclaw-unknown-*</code> directories or loops on <code>ENOTEMPTY</code>. Fixes #72956. (#73205) Thanks @SymbolStar.</li>
|
||||
<li>Agents/failover: classify CJK provider transport, quota, billing, auth, and overload error text so Chinese-language provider failures trigger fallback and user-facing transport copy instead of surfacing as unclassified raw errors. (#56242) Thanks @tomcatzh.</li>
|
||||
<li>Agents/failover: seed non-claude-cli fallback prompts with Claude Code session context when a claude-cli attempt fails, so fallback models do not restart cold after billing or quota failover. (#72069) Thanks @stainlu.</li>
|
||||
<li>Agents/CLI runner: transfer bundle-MCP tempDir cleanup from the per-turn runner finally to the Claude live-session lifecycle, so persistent Claude CLI sessions keep their <code>--mcp-config</code> directory until the live subprocess closes. Fixes #73244. Thanks @edwin-rivera-dev.</li>
|
||||
<li>Gateway/nodes: allow Windows companion nodes to use safe declared commands such as canvas, camera list, location, device info, and screen snapshot by default while keeping dangerous media commands opt-in. (#71884) Thanks @shanselman.</li>
|
||||
<li>Agents/cron: clarify agent-tool and CLI cron timezone guidance so supplied <code>tz</code> values use local wall-clock cron fields and omitted cron <code>tz</code> falls back to the Gateway host local timezone. Fixes #53669; carries forward #46177. (#73372) Thanks @chen-zhang-cs-code and @maranello-o.</li>
|
||||
<li>Providers/Qwen: allow explicitly configured <code>qwen/qwen3.6-plus</code> to resolve on Qwen Coding Plan endpoints while keeping the built-in catalog from advertising it there. Fixes #63654; carries forward #63987. Thanks @jepson-liu.</li>
|
||||
<li>Channels/Telegram: keep Bot API network fallbacks sticky after failed attempts and retry timed-out startup control calls once on the fallback route, so <code>deleteWebhook</code> IPv6 stalls no longer trigger slow multi-account retry storms. Fixes #73255. Thanks @ttomiczek and @sktbrd.</li>
|
||||
<li>Gateway/agents: accept heartbeat, cron, and webhook as internal channel hints for agent runs so <code>sessions_spawn</code> works from non-delivery parent sessions while unknown channel hints still fail closed. Fixes #73237. Thanks @KeWang0622.</li>
|
||||
<li>Gateway/models: merge explicit <code>models.providers.*.models</code> rows into the Gateway model catalog with normalized provider/model dedupe, and use normalized image-capability lookup so custom vision models keep native image attachments even when Pi discovery omits them or model ID casing differs. Fixes #64213 and #65165. Thanks @billonese and @202233a.</li>
|
||||
<li>Gateway/reload: publish canonical post-write source config to in-process reloaders so simple config saves no longer create phantom plugin diffs or trigger unnecessary Gateway restarts. (#73267) Thanks @szsip239.</li>
|
||||
<li>Gateway/Docker: keep config-triggered restarts in-process inside containers instead of spawning a detached child and exiting PID 1 cleanly, so Docker Swarm and other on-failure supervisors do not leave the service stuck at 0/1 replicas. Fixes #73178. Thanks @du-nguyen-IT007.</li>
|
||||
<li>CLI/tasks: ship the task-registry control runtime in npm packages so <code>openclaw tasks cancel</code> can load ACP/subagent cancellation helpers from published builds. Fixes #68997. Thanks @1OAKDesign.</li>
|
||||
<li>Channels/Telegram: preserve unsent generated media after partial reply streaming has already delivered the text, so <code>image_generate</code> outputs still reach Telegram as photos instead of being dropped from the final payload. Fixes #73253. Thanks @mlaihk.</li>
|
||||
<li>Memory-core/dreaming: cap detached Dream Diary narrative subagents across cron sweeps so multi-workspace dreaming no longer fans out unbounded subagent sessions, lock contention, and cascading narrative timeouts. Fixes #73198. (#73287) Thanks @KeWang0622.</li>
|
||||
<li>CLI/agents: close local one-shot Claude live stdio sessions and bundled MCP loopback resources after embedded <code>openclaw agent --local</code> runs, while keeping gateway-owned MCP loopback cleanup internal to the Gateway. Thanks @frankekn.</li>
|
||||
<li>Export/session: keep inline export HTML scripts and vendor libraries injected after template formatting so generated session exports open with the app code, markdown renderer, and syntax highlighter present. Fixes #41862 and #49957; carries forward #41861 and #68947. Thanks @briannewman, @martenzi, and @armanddp.</li>
|
||||
<li>Agents/ACPX: stage the patched Claude ACP adapter as an ACPX runtime dependency and route known Codex/Claude ACP commands through local wrappers, so Gateway runtime no longer depends on live <code>npx</code> adapter resolution. Fixes #73202. Thanks @joerod26.</li>
|
||||
<li>Memory/compaction: let pre-compaction memory flush use an exact <code>agents.defaults.compaction.memoryFlush.model</code> override such as <code>ollama/qwen3:8b</code> without inheriting the active session fallback chain, so local housekeeping can avoid paid conversation models. Fixes #53772. Thanks @limen96.</li>
|
||||
<li>macOS/update: stop managed Gateway services before package replacement and keep LaunchAgent service secrets out of world-readable plist metadata by loading them from owner-only env files. Fixes #72996. Thanks @Mathewb7.</li>
|
||||
<li>Google Meet: keep observe-only Chrome joins and setup checks from requiring BlackHole or audio bridge commands, avoid granting or selecting the microphone in observe-only mode, and make <code>test_speech</code> report fresh realtime output-byte verification instead of only confirming a queued utterance. Refs #72478. Thanks @DougButdorf.</li>
|
||||
<li>Gateway/hooks: route non-delivered hook completion and error summaries to the target agent's main session instead of the default agent session, preserving multi-agent hook isolation. Fixes #24693; carries forward #68667. Thanks @abersonFAC and @bluesky6868.</li>
|
||||
<li>Control UI/models: request the configured Gateway model-list view so dashboards with only <code>models.providers.*.models</code> show those configured models first instead of flooding the picker with the full built-in catalog. Fixes #65405. Thanks @wbyanclaw.</li>
|
||||
<li>CLI/models: keep default-model and allowlist pickers on explicit <code>models.providers.*.models</code> entries when <code>models.mode</code> is <code>replace</code> instead of loading the full built-in catalog. Fixes #64950. Thanks @mrozentsvayg.</li>
|
||||
<li>Media/security: tighten media-understanding MIME sanitization so parameterized MIME values stay end-anchored and malformed whitespace or suffix payloads are rejected before file-context handling. Fixes #9795; carries forward #68225 with related review/test context from #61016/#68456. Thanks @ymaxgit, @bluesky6868, and @shamsulalam1114.</li>
|
||||
<li>Discord: own the Carbon interaction listener and hand off Discord slash/component handling asynchronously, so compaction or long session locks no longer trip <code>InteractionEventListener</code> listener timeouts. Fixes #73204. Thanks @slideshow-dingo.</li>
|
||||
<li>Compaction/diagnostics: keep unknown compaction failure classifications stable while logging sanitized detail for unclassified provider errors such as missing Ollama provider adapters. Thanks @gzsiang.</li>
|
||||
<li>Models/fallbacks: record first-class <code>model.fallback_step</code> trajectory events with from/to models, failure detail, chain position, and final outcome so support exports preserve the primary model failure even when a later fallback also fails. Fixes #71744. Thanks @nikolaykazakovvs-ux.</li>
|
||||
<li>Gateway/agents: block agent <code>exec</code> from launching interactive <code>openclaw channels login</code> flows and abort active agent runs after invalid-config recovery restores last-known-good config, preventing known channel-login and reload paths from wedging replies. Refs #72338. Thanks @midhunmonachan.</li>
|
||||
<li>Gateway/diagnostics: emit payload-free liveness warnings with event-loop delay, event-loop utilization, CPU-core ratio, active-session counts, and OTEL warning metrics/spans so live-but-stalled Gateways capture CPU-spin context in stability bundles and telemetry. Refs #72338. Thanks @midhunmonachan and @DougButdorf.</li>
|
||||
<li>Gateway/startup: keep value-option foreground starts on the gateway fast path and skip proxy bootstrap unless proxy env is configured, reducing normal gateway startup RSS and avoiding full CLI graph loading. Thanks @vincentkoc.</li>
|
||||
<li>Heartbeat/models: show heartbeat model bleed guidance on context-overflow resets when the last runtime model matches configured <code>heartbeat.model</code>, so smaller local heartbeat models point users to <code>isolatedSession</code> or <code>lightContext</code> instead of only compaction-buffer tuning. Fixes #67314. Thanks @Knightmare6890.</li>
|
||||
<li>Subagents/models: persist <code>sessions_spawn.model</code> and configured subagent models as child-session model overrides before the first turn, so spawned subagents actually run on the requested provider/model instead of reverting to the target agent default. Fixes #73180. Thanks @danielzinhu99.</li>
|
||||
<li>Channels/Telegram: keep webhook-mode local listeners alive and retry Telegram <code>setWebhook</code> registration after recoverable startup network failures, so transient Bot API timeouts no longer leave reverse proxies pointing at a closed listener. Fixes #71834. Thanks @jinon86.</li>
|
||||
<li>Agents/ACPX: bundle the Codex ACP adapter and launch it from the isolated <code>CODEX_HOME</code> wrapper before falling back to npm, so Codex ACP startup no longer depends on live <code>npx</code> resolution or the stale <code>@zed-industries/codex-acp@^0.11.1</code> range. Fixes #72037; refs #73202. Thanks @jasonftl, @sazora, and @joerod26.</li>
|
||||
<li>Agents/ACPX: register the embedded ACP backend at Gateway startup through a lightweight ACP backend SDK path and without importing the heavy ACPX runtime until an ACP session or explicit startup probe needs it, reducing baseline Gateway RSS. Thanks @vincentkoc.</li>
|
||||
<li>CLI/update: keep restart health polling when the restarted Gateway is reachable but has not reported its version yet, so macOS service restarts do not fail early with <code>actual unavailable</code>. Thanks @ProspectOre.</li>
|
||||
<li>Backup: skip installed plugin <code>extensions/*/node_modules</code> dependency trees while keeping plugin manifests and source files in archives, so local backups avoid rebuildable npm payload bloat. Fixes #64144. Thanks @BrilliantWang.</li>
|
||||
<li>Cron/models: fail isolated cron runs closed when an explicit <code>payload.model</code> is not allowed or cannot be resolved, so scheduled jobs do not silently fall back to an unrelated agent default or paid route before configured provider proxies such as LiteLLM can run. Fixes #73146. Thanks @oneandrewwang.</li>
|
||||
<li>Memory/QMD: back off repeated chat-turn QMD open failures while still letting memory status and CLI probes recheck immediately, so a broken sidecar dependency cannot trigger active-memory or cron retry storms. Fixes #73188 and #73176. Thanks @leonlushgit and @w3i-William.</li>
|
||||
<li>Talk Mode: resolve <code>messages.tts.providers.<id>.apiKey</code> through the active runtime snapshot for <code>talk.config</code>, so Talk overlays can discover SecretRef-backed speech providers without falling back to local speech. Fixes #73109. (#73111) Thanks @omarshahine.</li>
|
||||
<li>Memory/Ollama: resolve <code>memorySearch.provider</code> custom provider ids through their configured <code>models.providers.<id>.api</code> owner, so multi-GPU Ollama setups can dedicate embeddings to providers such as <code>ollama-5080</code> without losing the Ollama adapter or local auth semantics. Fixes #73150. Thanks @oneandrewwang.</li>
|
||||
<li>CLI/memory: skip eager context-window warmup for <code>openclaw memory</code> commands so memory search does not race unrelated model metadata discovery. Fixes #73123. Thanks @oalansilva and @neeravmakwana.</li>
|
||||
<li>CLI/Telegram: route Telegram <code>message send</code> and poll actions through the running Gateway when available, so packaged installs use the staged <code>grammy</code> runtime deps and CLI sends return instead of hanging after the Telegram channel is active. Fixes #73140. Thanks @oalansilva.</li>
|
||||
<li>Plugins/runtime deps: prepare staged bundled plugin dependencies before loading packaged public surfaces, so OpenClaw's Telegram runtime/test facade loads resolve <code>grammy</code> from the managed runtime-deps stage without copying dependencies into the global package root. Refs #73140. Thanks @oalansilva.</li>
|
||||
<li>Agents/exec: emit <code>(no output)</code> for silent exec update and node-host result blocks so Anthropic-compatible providers no longer reject empty tool-result text after quiet commands. Fixes #73117. Thanks @pfrederiksen and @Sanjays2402.</li>
|
||||
<li>Cron/providers: preflight local Ollama and OpenAI-compatible provider endpoints before isolated cron agent turns, record unreachable local providers as skipped runs, and cache dead-endpoint probes so many jobs do not hammer the same stopped local server. Fixes #58584. Thanks @jpeghead.</li>
|
||||
<li>Gateway/config: let config reload continue in degraded mode when invalidity is scoped to plugin entries, so incompatible plugin configs can be skipped and the Gateway restart can still pick up the rest of the config after rollbacks. Fixes #73131. Thanks @Adam-Researchh.</li>
|
||||
<li>Doctor/channels: suppress disabled bundled-plugin blocker warnings when a trusted external plugin owns the configured channel, so Lark/Feishu installs no longer get Feishu repair noise after switching to <code>openclaw-lark</code>. Fixes #56794. Thanks @wuji-tech-dev.</li>
|
||||
<li>CLI/status: show skipped fast-path memory checks as <code>not checked</code> and report active custom memory plugin runtime status from <code>status --json --all</code> without requiring built-in <code>agents.defaults.memorySearch</code>, so plugins such as memory-lancedb-pro and memory-cms no longer look unavailable when their own runtime is healthy. Fixes #56968. Thanks @Tony-ooo and @aderius.</li>
|
||||
<li>Gateway/channels: record and log unexpected clean channel monitor exits so channels that return without throwing no longer appear stopped with no error. Fixes #73099. Thanks @balaji1968-kingler.</li>
|
||||
<li>Discord/group chats: keep group/channel replies private by default unless the agent explicitly uses the message tool, so always-on rooms can lurk without leaking automatic final, block, preview, or status-reaction output; <code>messages.groupChat.visibleReplies: "automatic"</code> restores legacy auto-posting. (#73046) Thanks @scoootscooob.</li>
|
||||
<li>Plugins/package: force nested bundled-plugin runtime dependency installs out of inherited npm dry-run mode during prepack and package smoke checks, so packed installs materialize required plugin modules instead of reporting missing bundled files. Refs #73128. Thanks @Adam-Researchh.</li>
|
||||
<li>Discord: skip reaction events before REST channel fetch when notifications are off, guild reactions are disabled, or allowlist mode cannot match without channel overrides, reducing reconnect bursts that caused slow listener warnings. Fixes #73133. Thanks @isaacsummers.</li>
|
||||
<li>Channels/Telegram: centralize polling update tracking so accepted offsets remain durable across restarts, same-process handler failures can still retry, and slow offset writes cannot overwrite newer accepted watermarks. Refs #73115. Thanks @vdruts.</li>
|
||||
<li>Agents/models: classify empty, reasoning-only, and planning-only terminal agent runs before accepting a model fallback candidate, so invalid or incompatible models can advance to the next configured fallback instead of returning a 30-second terminal failure. Fixes #73115. Thanks @vdruts.</li>
|
||||
<li>Memory/LanceDB: let embedding config use provider-backed auth profiles, environment credentials, or provider config without a separate plugin <code>embedding.apiKey</code>, so OAuth-capable embedding providers can power auto-recall/capture. Fixes #68950. Thanks @malshaalan-ai.</li>
|
||||
<li>CLI/parents: invoking <code>openclaw <parent></code> (memory, channels, plugins, approvals, devices, cron, mcp) without a subcommand now prints the parent's help and exits <code>0</code>, matching <code><parent> --help</code> and the existing <code>agents</code> / <code>sessions</code> defaults so shell <code>&&</code> chains and pnpm wrappers no longer surface a misleading <code>ELIFECYCLE Command failed with exit code 1.</code> line. Fixes #73077. Thanks @hclsys.</li>
|
||||
<li>Plugins/hooks: time out never-settling <code>agent_end</code> observation hooks after 30 seconds and log the plugin failure, so hung embedding endpoints no longer leave memory capture silently pending forever. Fixes #65544. Thanks @ghoc0099.</li>
|
||||
<li>Gateway/config: serve runtime config schemas from the current plugin metadata snapshot and generated bundled channel schema metadata instead of rebuilding plugin channel config modules on every <code>config.get</code>/<code>config.schema</code>, preventing idle plugin-discovery CPU churn after upgrades. Fixes #73088. Thanks @sleitor and @geovansb.</li>
|
||||
<li>Memory/LanceDB: call OpenAI-compatible embedding endpoints through the raw SDK transport without sending <code>encoding_format</code>, then normalize float-array or base64 responses so providers such as ZhiPu and DashScope no longer fail recall with wrong vector dimensions or rejected parameters. Fixes #63655. Thanks @kinthaiofficial.</li>
|
||||
<li>Plugins/install: run dependency installs with npm error-level logging instead of silent mode so failed plugin or hook installs surface actionable npm errors such as EUNSUPPORTEDPROTOCOL instead of <code>npm install failed:</code> with no detail. (#73093) Thanks @sanctrl.</li>
|
||||
<li>Memory/LanceDB: bound memory recall embedding queries with a new <code>recallMaxChars</code> setting, prefer the latest user message over channel prompt metadata during auto-recall, and document the knob so small Ollama embedding models avoid context-length failures. Fixes #56780. Thanks @rungmc357 and @zak-collaborator.</li>
|
||||
<li>CLI/skills: resolve workspace-backed skills commands from <code>--agent</code>, then the current agent workspace, before falling back to the default agent, so multi-agent ClawHub installs, updates, and status checks stay scoped to the active workspace. Fixes #56161; carries forward #72726. Thanks @langbowang and @luyao618.</li>
|
||||
<li>Plugin SDK: fall back from partial bundled plugin directory overrides to package source public surfaces while preserving <code>OPENCLAW_DISABLE_BUNDLED_PLUGINS</code> as a hard disable. (#72817) Thanks @serkonyc.</li>
|
||||
<li>Agents/ACPX: stop forwarding Codex ACP timeout config controls that Codex rejects while preserving OpenClaw's run-timeout watchdog for ACP subagents. Fixes #73052. Thanks @pfrederiksen and @richa65.</li>
|
||||
<li>Memory Core: stream fallback vector search scoring with a bounded top-K result set so large indexes do not materialize every chunk embedding when sqlite-vec is unavailable. (#73069) Thanks @parkertoddbrooks.</li>
|
||||
<li>Memory Core: stream embedding-cache seeding during safe reindex so large local caches do not materialize every row into the V8 heap before the atomic rebuild. (#73067) Thanks @parkertoddbrooks.</li>
|
||||
<li>Memory/Ollama: add <code>memorySearch.remote.nonBatchConcurrency</code> for inline embedding indexing, default Ollama non-batch indexing to one request at a time, and keep batch concurrency separate from non-batch concurrency so local embedding backfills avoid timeout storms on smaller hosts. Carries forward #57733. Thanks @itilys.</li>
|
||||
<li>macOS app: update Peekaboo, ElevenLabsKit, and MLX TTS helper dependencies, make canvas file watching and config/exec-approval state writes reliable under concurrent app/test activity, and keep the app plus helper builds warning-free. Thanks @Blaizzy.</li>
|
||||
<li>iOS app: refresh SwiftPM/XcodeGen source hygiene, make app, extension, watch, and curated shared Swift files pass the prebuild SwiftFormat and SwiftLint checks, move relay registration off deprecated StoreKit receipt APIs, and keep simulator builds and logic tests warning-free. Thanks @ngutman.</li>
|
||||
<li>Agents/models: keep <code>models.json</code> readiness and provider-hook caches warm across repeated agent and subagent model resolution while preserving external <code>models.json</code> invalidation, reducing repeated provider-plugin loads on slower ARM64 hosts. Fixes #73075. Thanks @jochen.</li>
|
||||
<li>Docs/tools: clarify that <code>tools.profile: "messaging"</code> is intentionally narrow and that <code>tools.profile: "full"</code> is the unrestricted baseline for broader command/control access. Carries forward #39954. Thanks @posigit.</li>
|
||||
<li>Control UI/Agents: redact tool-call args, partial/final results, derived exec output, and configured custom secret patterns before streaming tool events to the Control UI, so tool output cannot expose provider or channel credentials. Fixes #72283. (#72319) Thanks @volcano303 and @BunsDev.</li>
|
||||
<li>Agents/sessions: keep <code>sessions_history</code> recall redaction enabled even when general log redaction is disabled, and clarify that safety-boundary UI/tool/diagnostic payloads still redact independently of <code>logging.redactSensitive</code>. Carries forward #72319. Thanks @volcano303 and @BunsDev.</li>
|
||||
<li>Providers/Codex: pass agent and workspace directories into provider stream wrappers so Codex native <code>web_search</code> activation can evaluate the correct auth context, and smoke-test the built status-message runtime by resolving the emitted bundle name. Carries forward #67843; refs #65909. Thanks @neilofneils404.</li>
|
||||
<li>Cron/models: keep <code>payload.model</code> as a per-job primary that can use configured fallbacks, while still letting <code>payload.fallbacks: []</code> make cron runs strict and avoid hidden agent-primary retries. Refs #73023. Thanks @pavelyortho-cyber.</li>
|
||||
<li>Models/fallbacks: treat user-selected session models as exact choices, so <code>/model ollama/...</code> and model-picker switches fail visibly when the selected provider is unreachable instead of answering from an unrelated configured fallback. Fixes #73023. Thanks @pavelyortho-cyber.</li>
|
||||
<li>Codex harness: keep ChatGPT subscription app-server runs from inheriting <code>CODEX_API_KEY</code> or <code>OPENAI_API_KEY</code>, and fall back to <code>CODEX_API_KEY</code> / <code>OPENAI_API_KEY</code> app-server login only when no Codex account is available. Fixes #73057. Thanks @holgergruenhagen and @pashpashpash.</li>
|
||||
<li>CLI/model probes: fail local <code>infer model run</code> probes when the provider returns no text output, so unreachable local providers and empty completions no longer look like successful smoke tests. Refs #73023. Thanks @pavelyortho-cyber.</li>
|
||||
<li>CLI/Ollama: run local <code>infer model run</code> through the lean provider completion path and skip global model discovery for one-shot local probes, so Ollama smoke tests no longer pay full chat-agent/tool startup cost or hang before the native <code>/api/chat</code> request. Fixes #72851. Thanks @TotalRes2020.</li>
|
||||
<li>Doctor/gateway services: ignore launchd/systemd companion services that only reference the gateway as a dependency, suppress inactive Linux extra-service warnings, and avoid rewriting a running systemd gateway command/entrypoint during doctor repair. Carries forward #39118. Thanks @therk.</li>
|
||||
<li>Daemon/service: only emit hard-coded version-manager paths such as <code>~/.volta/bin</code>, <code>~/.asdf/shims</code>, <code>~/.bun/bin</code>, and fnm/pnpm fallbacks into gateway and node service PATHs when the directories exist, so <code>openclaw doctor</code> no longer flags <code>gateway.path.non-minimal</code> against a PATH the daemon just wrote. Env-driven roots and stable user-bin dirs remain unconditional. Fixes #71944; carries forward #71964. Thanks @Sanjays2402.</li>
|
||||
<li>CLI/startup: disable Node's module compile cache automatically for live source-checkout launchers so in-place <code>pnpm build</code> updates are visible to the next <code>openclaw</code> CLI invocation. Fixes #73037. Thanks @LouisGameDev.</li>
|
||||
<li>Agents/group chat: keep silent-allowed empty and reasoning-only turns on the <code>NO_REPLY</code> path without injecting visible-answer retry prompts, and clarify the group prompt so agents use the exact silent token instead of prose. Thanks @vincentkoc.</li>
|
||||
<li>Agents/group chat: move <code>NO_REPLY</code> mechanics into channel-aware direct/group prompts and suppress the duplicate generic silent-reply section for auto-reply runs, so always-on group agents get one consistent stay-silent instruction. Thanks @vincentkoc.</li>
|
||||
<li>Providers/OpenAI: preserve encrypted empty-summary Responses reasoning items in WebSocket replay and request <code>reasoning.encrypted_content</code> on reasoning turns so GPT-5.4/GPT-5.5 sessions do not lose required <code>rs_*</code> state beside <code>msg_*</code> items. Fixes #73053. Thanks @odb36777.</li>
|
||||
<li>Gateway/startup: treat <code>plugins.enabled=false</code> as an early plugin fast path, skipping plugin auto-enable discovery, gateway plugin lookup/runtime-dependency staging, and stale-plugin cleanup warnings while preserving channel blocker warnings. (#73041) Thanks @WuKongAI-CMU.</li>
|
||||
<li>Channels/commands: make generated <code>/dock-*</code> commands switch the active session reply route through <code>session.identityLinks</code> instead of falling through to normal chat. Fixes #69206; carries forward #73033. Thanks @clawbones and @michaelatamuk.</li>
|
||||
<li>Providers/Cloudflare AI Gateway: strip assistant prefill turns from Anthropic Messages payloads when thinking is enabled, so Claude requests through Cloudflare AI Gateway no longer fail Anthropic conversation-ending validation. Fixes #72905; carries forward #73005. Thanks @AaronFaby and @sahilsatralkar.</li>
|
||||
<li>Gateway/startup: keep primary-model startup prewarm on scoped metadata preparation, let native approval bootstraps retry outside channel startup, and skip the global hook runner when no <code>gateway_start</code> hook is registered, so clean post-ready sidecar work stays off the critical path. Refs #72846. Thanks @RayWoo, @livekm0309, and @mrz1836.</li>
|
||||
<li>Gateway/channels: start bundled channel accounts with a lightweight <code>runtimeContexts</code> surface instead of importing the full reply/routing/session channel runtime before <code>startAccount</code>, so Discord, Telegram, Slack, Matrix, and QQBot startup no longer block on unrelated channel helper graphs. Refs #72846 and #72960. Thanks @mrz1836, @RayWoo, and @rollingshmily.</li>
|
||||
<li>Gateway/supervisor: exit cleanly when a supervised restart finds an existing healthy gateway and bound retries when the existing gateway stays unhealthy, so stale lock contention cannot loop indefinitely. Refs #72846. Thanks @azgardtek.</li>
|
||||
<li>Gateway/startup: scope primary-model provider discovery during channel prewarm to the configured provider owner and add split startup trace timings, so boot avoids staging unrelated bundled provider dependencies while setup discovery remains broad. Fixes #73002. Thanks @Schnup03.</li>
|
||||
<li>Plugins/runtime deps: declare retained staged bundled plugin dependencies in the npm staging manifest while installing only newly missing packages, so Gateway restarts avoid reinstalling the full retained dependency set when one runtime dependency is absent. Fixes #73055. Thanks @GCorp2026.</li>
|
||||
<li>CLI/status: keep default <code>openclaw status</code> off the heavyweight security audit, plugin compatibility, and memory-vector probes while still showing configured Telegram channels through setup metadata, so routine health checks stay fast and no longer render an empty Channels table. Fixes #72993. Thanks @comick1.</li>
|
||||
<li>Channels/Telegram: send a best-effort native typing cue immediately after an inbound message is accepted, so slow pre-dispatch turns show Telegram liveness before queueing, compaction, model, or tool work starts. Fixes #63759. Thanks @alessandropcostabr.</li>
|
||||
<li>Channels/Telegram: stop native approval startup auth failures from retrying every second, while still waiting through retryable Gateway auth handoffs, so Telegram approval setup problems no longer create a reconnect/log loop during channel startup. Refs #72846 and #72867. Thanks @kiranvk-2011 and @porly1985.</li>
|
||||
<li>Channels/Microsoft Teams: unwrap staged CommonJS JWT runtime dependencies before Bot Connector token validation so inbound Teams messages no longer 401 after the bundled runtime-deps move. Fixes #73026. Thanks @kbrown10000.</li>
|
||||
<li>Gateway/auth: allow local direct callers in trusted-proxy mode to use the configured gateway password as an internal fallback while keeping token fallback rejected. Fixes #17761. Thanks @dashed, @vincentkoc, and @jetd1.</li>
|
||||
<li>Gateway/auth: add explicit <code>trustedProxy.allowLoopback</code> support for same-host loopback reverse proxies while keeping loopback trusted-proxy auth fail-closed by default and preserving required-header and allowlist checks. Fixes #59167; carries forward #63379. Thanks @Matir, @jeremyakers, and @mrosmarin.</li>
|
||||
<li>Channels/sessions: prevent guarded inbound session recording from creating route-only phantom sessions while still allowing last-route updates for sessions that already exist. Carries forward #73009. Thanks @jzakirov.</li>
|
||||
<li>Cron: accept <code>delivery.threadId</code> in Gateway cron add/update schemas so scheduled announce delivery can target Telegram forum topics and other threaded channel destinations through the documented delivery path. Fixes #73017. Thanks @coachsootz.</li>
|
||||
<li>Plugins/runtime deps: stage bundled plugin dependencies imported by mirrored root dist chunks, so packaged memory and status commands do not miss <code>chokidar</code> or similar root-chunk dependencies after update. Fixes #72882 and #72970; carries forward #72992. Thanks @shrimpy8, @colin-chang, and @Schnup03.</li>
|
||||
<li>Plugins/runtime deps: reuse unchanged bundled plugin runtime mirrors instead of rebuilding plugin trees on every load, cutting avoidable writes and restart/reconnect I/O on slow storage. Fixes #72933. Thanks @jasonftl.</li>
|
||||
<li>Agents/runtime context: deliver hidden runtime context through prompt-local system context while keeping the transcript-only custom entry out of provider user turns, and strip stale copied runtime-context prefaces from user-facing replies. Fixes #72386; carries forward #72969. Thanks @jhsmith409.</li>
|
||||
<li>Channels/Telegram: skip the optional webhook-info API call during polling-mode status checks and startup bot-label probes so long-polling setups avoid an unnecessary Telegram round trip. Carries forward #72990. Thanks @danielgruneberg.</li>
|
||||
<li>CLI/message: resolve targeted <code>openclaw message</code> channels to their owning plugin before loading the registry, and fall back to configured channel plugins when the channel must be inferred, so scripted sends avoid full bundled plugin registry scans without assuming channel ids match plugin ids. Fixes #73006. Thanks @jasonftl.</li>
|
||||
<li>Plugins/startup: parse strict JSON plugin manifests with native JSON first and keep JSON5 as the compatibility fallback, reducing manifest registry CPU during Gateway boot and CLI startup. Fixes #73011. Thanks @jasonftl.</li>
|
||||
<li>CLI/models: keep route-first <code>models status --json</code> stdout reserved for the JSON payload by routing auth-profile and startup diagnostics to stderr. Fixes #72962. Thanks @vishutdhar.</li>
|
||||
<li>Gateway/runtime: keep dirty-tree status calls from rebuilding live <code>dist</code>, clear stale task and restart state across in-process restarts, retry transient Discord lazy imports, and let channel startup continue after slow model warmup so browser, Discord, and voice-call sidecars come online. Thanks @vincentkoc.</li>
|
||||
<li>Security/CodeQL: replace file SecretRef id gateway schema regex validation with segment-aligned predicates and set empty permissions on release summary/backfill jobs so the narrowed CodeQL profile stays clean. Thanks @vincentkoc.</li>
|
||||
<li>Sessions: ignore future-dated session activity timestamps during reset freshness checks and cap future <code>updatedAt</code> values at the merge boundary so clock-skewed messages cannot keep stale sessions alive forever. Fixes #72989. Thanks @martingarramon.</li>
|
||||
<li>Sessions: apply search, activity filters, and limits before gateway row enrichment so bounded session lists avoid scanning discarded transcripts. Carries forward #72978. Thanks @yeager.</li>
|
||||
<li>Sessions: remove trajectory runtime and pointer sidecars when session maintenance prunes, caps, or disk-evicts their owning session, while preserving sidecars still referenced by live rows. Fixes #73000. Thanks @jared-rebel.</li>
|
||||
<li>Plugins/CLI: allow managed plugin installs when the active extensions root is a symlink to a real state directory, while keeping nested target symlinks blocked and suppressing misleading hook-pack fallback errors for install-boundary failures. Fixes #72946. Thanks @mayank6136.</li>
|
||||
<li>Providers/Ollama: mark discovered Ollama catalog models as supporting streaming usage metadata so token accounting stays enabled for local models. (#72976) Thanks @sdeyang.</li>
|
||||
<li>Media understanding: reject malformed MIME values with trailing junk while preserving standard parameter tails before enrichment uses them. (#72914) Thanks @volcano303.</li>
|
||||
<li>WebChat: keep bare <code>/new</code> and <code>/reset</code> prompts from producing empty transcript text by inserting the hidden session marker when the visible tail is blank. (#72863) Thanks @mahopan.</li>
|
||||
<li>CLI/update: explain completion-cache refresh timeouts with manual refresh guidance instead of surfacing a raw low-level timeout. Fixes #72842. (#72850) Thanks @iot2edge.</li>
|
||||
<li>Memory-core/dreaming: give narrative generation a 60-second timeout so slower local or remote models can finish instead of timing out at 15 seconds. Fixes #72837. (#72852) Thanks @RayWoo.</li>
|
||||
<li>Plugins/hooks: inject each plugin's resolved config into internal hook event context without mutating the shared event object. (#72888) Thanks @jalapeno777.</li>
|
||||
<li>Agents/ACP: pass the resolved ACP agent directory into media understanding so per-agent media caches and config are used for ACP-dispatched image turns. (#72832) Thanks @luyao618.</li>
|
||||
<li>Gateway/Bonjour: truncate mDNS service names and host labels to the 63-byte DNS label limit at valid UTF-8 boundaries. (#72809) Thanks @luyao618.</li>
|
||||
<li>Feishu: treat groups explicitly configured under channels.feishu.groups as admitted even when groupAllowFrom is empty, while preserving groupPolicy: "disabled" as a hard group block and keeping groups.\* wildcard defaults non-admitting. Fixes #67687. (#72789) Thanks @MoerAI.</li>
|
||||
<li>Gateway/startup: keep hot Gateway boot paths on leaf config imports and add max-RSS reporting to the gateway startup bench so low-memory startup regressions are visible before release. Thanks @vincentkoc.</li>
|
||||
<li>WebChat: read <code>chat.history</code> from active transcript branches, drop stale streamed assistant tails once final history catches up, and coalesce duplicate in-flight Control UI submits, so rewritten prompts, completed replies, and rapid send events no longer render or process twice. Fixes #72975, #72963, and #72974. Thanks @dmagdici, @lhtpluto, and @Benjamin5281999.</li>
|
||||
<li>WebChat/TTS: persist automatic final-mode TTS audio as a supplemental audio-only transcript update instead of adding a second assistant message with the same visible text. Fixes #72830. Thanks @lhtpluto.</li>
|
||||
<li>Agents/LSP: terminate bundled stdio LSP process trees during runtime disposal and Gateway shutdown, so nested children such as <code>tsserver</code> do not survive stop or restart. Fixes #72357. Thanks @ai-hpc and @bittoby.</li>
|
||||
<li>Diagnostics/OTEL: capture privacy-safe model-call request payload bytes, streamed response bytes, first-response latency, and total duration in diagnostic events, plugin hooks, stability snapshots, and OTEL model-call spans/metrics without logging raw model content. Fixes #33832. Thanks @wwh830.</li>
|
||||
<li>Logging: write validated diagnostic trace context as top-level <code>traceId</code>, <code>spanId</code>, <code>parentSpanId</code>, and <code>traceFlags</code> fields in file-log JSONL records so traced requests and model calls are easier to correlate in log processors. Refs #40353. Thanks @liangruochong44-ui.</li>
|
||||
<li>Logging/sessions: apply configured redaction patterns to persisted session transcript text and accept escaped character classes in safe custom redaction regexes, so transcript JSONL no longer keeps matching sensitive text in the clear. Fixes #42982. Thanks @panpan0000.</li>
|
||||
<li>Providers/Ollama: honor <code>/api/show</code> capabilities when registering local models so non-tool Ollama models no longer receive the agent tool surface, and keep native Ollama thinking opt-in instead of enabling it by default. Fixes #64710 and duplicate #65343. Thanks @yuan-b, @netherby, @xilopaint, and @Diyforfun2026.</li>
|
||||
<li>Control UI/Agents: remount the Overview model controls when switching agents so the primary-model picker cannot retain stale per-agent selection. Fixes #39392; carries forward #39401, notes the duplicate #39495 approach, and keeps #46275/#54724 broader stabilization out of scope. Thanks @daijunyi002, @SergioChan, @aworki, and @wsyjh8.</li>
|
||||
<li>Auto-reply: poison inbound message dedupe after replay-unsafe provider/runtime failures so retries stay safe before visible progress but cannot duplicate messages after block output, tool side effects, or session progress. Fixes #69303; keeps #58549 and #64606 as duplicate validation. Thanks @martingarramon, @NikolaFC, and @zeroth-blip.</li>
|
||||
<li>Agents/model fallback: jump directly to a known later live-session model redirect instead of walking unrelated fallback candidates, while preserving the already-landed live-session/fallback loop guard. Fixes #57471; related loop family already closed via #58496. Thanks @yuxiaoyang2007-prog.</li>
|
||||
<li>Gateway/Bonjour: keep @homebridge/ciao cancellation handlers registered across advertiser restarts so late probing cancellations cannot crash Linux and other mDNS-churned gateways. Thanks @vincentkoc.</li>
|
||||
<li>Plugins/startup: load the default <code>memory-core</code> slot during Gateway startup when permitted so active-memory recall can call <code>memory_search</code> and <code>memory_get</code> without requiring an explicit <code>plugins.slots.memory</code> entry, while preserving <code>plugins.slots.memory: "none"</code>. Thanks @vincentkoc.</li>
|
||||
<li>Gateway/plugins: resolve <code>gateway_start</code> cron hooks from live Gateway runtime state before the legacy deps fallback, so memory-core dreaming cron reconciliation keeps working on installs where <code>deps.cron</code> is not populated during service startup. Fixes #72835. Thanks @RayWoo.</li>
|
||||
<li>Plugins/CLI: prefer native require for compiled bundled plugin JavaScript before jiti so read-only config, status, device, and node commands avoid unnecessary transform overhead on slow hosts. Fixes #62842. Thanks @Effet.</li>
|
||||
<li>Plugins/compat: inventory doctor-side deprecation migrations separately from runtime plugin compatibility so release sweeps preserve needed repairs while enforcing dated removal windows. Thanks @vincentkoc.</li>
|
||||
<li>Plugins/compat: add missing dated compatibility records for legacy extension-api, memory registration, provider hook/type aliases, runtime aliases, channel SDK helpers, and approval/test utility shims. Thanks @vincentkoc.</li>
|
||||
<li>Plugins/CLI: refresh the persisted registry after managed plugin files are removed so ClawHub uninstall cannot leave stale <code>plugins list</code> entries. Thanks @vincentkoc.</li>
|
||||
<li>Plugins/CLI: make plugin install and uninstall config writes conflict-aware, clear stale denylist entries on explicit reinstall/removal, and delete managed plugin files only after config/index commit succeeds. Thanks @vincentkoc.</li>
|
||||
<li>Plugins: fail <code>plugins update</code> when tracked plugin or hook updates error, keep bundled runtime-dependency repair behind restrictive allowlists, and reject package installs with unloadable extension entries. Thanks @vincentkoc.</li>
|
||||
<li>WebChat/Control UI: support non-video file attachments in chat uploads while preserving the existing image attachment path and MIME-sniff fallback for generic image uploads. (#70947) Thanks @IAMSamuelRodda.</li>
|
||||
<li>Skills/memory: restore Chokidar v5 hot reloads by watching concrete skill and memory roots with filters, including SKILL.md removals and deleted skill folders without broad workspace recursion. Fixes #27404, #33585, and #41606. Thanks @shelvenzhou, @08820048, and @rocke2020.</li>
|
||||
<li>Gateway/chat: keep duplicate attachment-backed <code>chat.send</code> retries with the same idempotency key on the documented in-flight path so aborts still target the real active run. Fixes #70139. Thanks @Feelw00.</li>
|
||||
<li>Gateway/chat: preserve repeated boundary characters while merging assistant chat stream deltas, including repeated digits, CJK characters, and markdown/table tokens. Fixes #63769; carries forward #63994 and #65457. Thanks @yon950905 and @mohuaxiao.</li>
|
||||
<li>Plugins: share package entrypoint resolution between install and discovery, reject mismatched <code>runtimeExtensions</code>, and cache bundled runtime-dependency manifest reads during scans. Thanks @vincentkoc.</li>
|
||||
<li>WhatsApp/Web: keep quiet but healthy linked-device sessions connected by basing the watchdog on WhatsApp Web transport activity, while retaining a longer app-silence cap so frame activity cannot mask a stuck session forever. Fixes #70678; carries forward the focused #71466 approach and keeps #63939 as related configurable-timeout follow-up. Thanks @vincentkoc and @oromeis.</li>
|
||||
<li>Discord/gateway: count failed health-monitor restart attempts toward cooldown and hourly caps, and evict stale account lifecycle state during channel reloads so repeated Discord gateway recovery cannot loop on old status. Fixes #38596. (#40413) Thanks @jellyAI-dev and @vashquez.</li>
|
||||
<li>TTS/BlueBubbles: pre-transcode synthesized MP3 audio to opus-in-CAF (mono, 24 kHz — validated against macOS 15.x Messages.app's native voice-memo CAF descriptor) on macOS hosts before handing the file to BlueBubbles, so iMessage renders the result as a native voice-memo bubble with proper duration and waveform UI instead of a plain file attachment. Adds an opt-in <code>tts.voice.preferAudioFileFormat</code> channel capability and a magic-byte sniff for the CAF container so the host-local-media validator (which uses <code>file-type</code> and didn't recognize CAF natively) can verify the pre-transcoded buffer. Channels that don't opt in are unaffected. (#72586) Fixes #72506. Thanks @omarshahine.</li>
|
||||
<li>Feishu: retry WebSocket startup failures with monitor-owned backoff while preserving SDK-local heartbeat defaults, so persistent-connection startup failures no longer leave the monitor hung. Fixes #68766; related #42354 and #55532. Thanks @alex-xuweilong, @120106835, @sirfengyu, and @tianhaocui.</li>
|
||||
</ul>
|
||||
<p><a href="https://github.com/openclaw/openclaw/blob/main/CHANGELOG.md">View full changelog</a></p>
|
||||
]]></description>
|
||||
<enclosure url="https://github.com/openclaw/openclaw/releases/download/v2026.4.27/OpenClaw-2026.4.27.zip" length="50595360" type="application/octet-stream" sparkle:edSignature="X8DQNQNWVcvtpYLkhZcsKNpnA78ycyzgGlZaG0XBY1GIph3oZNUIpAszGGocJVqTK7+F89Au5ZPb60mOqJQ6DQ=="/>
|
||||
</item>
|
||||
</channel>
|
||||
</rss>
|
||||
@@ -65,8 +65,8 @@ android {
|
||||
applicationId = "ai.openclaw.app"
|
||||
minSdk = 31
|
||||
targetSdk = 36
|
||||
versionCode = 2026050800
|
||||
versionName = "2026.5.8"
|
||||
versionCode = 2026050600
|
||||
versionName = "2026.5.6"
|
||||
ndk {
|
||||
// Support all major ABIs — native libs are tiny (~47 KB per ABI)
|
||||
abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
# OpenClaw iOS Changelog
|
||||
|
||||
## 2026.5.8 - 2026-05-08
|
||||
|
||||
Maintenance update for the current OpenClaw development release.
|
||||
|
||||
## 2026.5.6 - 2026-05-06
|
||||
|
||||
Maintenance update for the current OpenClaw development release.
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
// Source of truth: apps/ios/version.json
|
||||
// Generated by scripts/ios-sync-versioning.ts.
|
||||
|
||||
OPENCLAW_IOS_VERSION = 2026.5.8
|
||||
OPENCLAW_MARKETING_VERSION = 2026.5.8
|
||||
OPENCLAW_IOS_VERSION = 2026.5.6
|
||||
OPENCLAW_MARKETING_VERSION = 2026.5.6
|
||||
OPENCLAW_BUILD_VERSION = 1
|
||||
|
||||
#include? "../build/Version.xcconfig"
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"version": "2026.5.8"
|
||||
"version": "2026.5.6"
|
||||
}
|
||||
|
||||
@@ -8,8 +8,6 @@ import SwiftUI
|
||||
@MainActor
|
||||
@Observable
|
||||
final class AppState {
|
||||
private static let logger = Logger(subsystem: "ai.openclaw", category: "app-state")
|
||||
|
||||
private let isPreview: Bool
|
||||
private var isInitializing = true
|
||||
private var isApplyingRemoteTokenConfig = false
|
||||
@@ -698,10 +696,7 @@ final class AppState {
|
||||
remoteToken: self.remoteToken,
|
||||
remoteTokenDirty: self.remoteTokenDirty))
|
||||
guard synced.changed else { return }
|
||||
guard OpenClawConfigFile.saveDict(synced.root) else {
|
||||
Self.logger.warning("gateway config sync rejected to protect persisted gateway auth/mode")
|
||||
return
|
||||
}
|
||||
OpenClawConfigFile.saveDict(synced.root)
|
||||
}
|
||||
|
||||
func triggerVoiceEars(ttl: TimeInterval? = 5) {
|
||||
|
||||
@@ -8,7 +8,6 @@ enum ConfigStore {
|
||||
var saveLocal: (@MainActor @Sendable ([String: Any]) -> Void)?
|
||||
var loadRemote: (@MainActor @Sendable () async -> [String: Any])?
|
||||
var saveRemote: (@MainActor @Sendable ([String: Any]) async throws -> Void)?
|
||||
var saveGateway: (@MainActor @Sendable ([String: Any]) async throws -> Void)?
|
||||
}
|
||||
|
||||
private actor OverrideStore {
|
||||
@@ -67,19 +66,10 @@ enum ConfigStore {
|
||||
do {
|
||||
try await self.saveToGateway(root)
|
||||
} catch {
|
||||
guard self.shouldFallbackToLocalWrite(afterGatewaySaveError: error) else {
|
||||
self.lastHash = nil
|
||||
throw error
|
||||
}
|
||||
guard OpenClawConfigFile.saveDict(
|
||||
OpenClawConfigFile.saveDict(
|
||||
root,
|
||||
preserveExistingKeys: true,
|
||||
allowGatewayAuthMutation: allowGatewayAuthMutation)
|
||||
else {
|
||||
throw NSError(domain: "ConfigStore", code: 2, userInfo: [
|
||||
NSLocalizedDescriptionKey: "Local config write rejected to protect gateway auth/mode.",
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -99,30 +89,8 @@ enum ConfigStore {
|
||||
}
|
||||
}
|
||||
|
||||
private static func shouldFallbackToLocalWrite(afterGatewaySaveError error: Error) -> Bool {
|
||||
let nsError = error as NSError
|
||||
let message = "\(nsError.domain) \(nsError.localizedDescription)".lowercased()
|
||||
let blockedFragments = [
|
||||
"invalid_request",
|
||||
"invalid request",
|
||||
"invalid config",
|
||||
"config changed since last load",
|
||||
"base hash",
|
||||
"basehash",
|
||||
"unauthorized",
|
||||
"token mismatch",
|
||||
"auth",
|
||||
]
|
||||
return !blockedFragments.contains { message.contains($0) }
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private static func saveToGateway(_ root: [String: Any]) async throws {
|
||||
let overrides = await self.overrideStore.overrides
|
||||
if let saveGateway = overrides.saveGateway {
|
||||
try await saveGateway(root)
|
||||
return
|
||||
}
|
||||
if self.lastHash == nil {
|
||||
_ = await self.loadFromGateway()
|
||||
}
|
||||
|
||||
@@ -779,10 +779,7 @@ struct DebugSettings: View {
|
||||
session["store"] = trimmed.isEmpty ? SessionLoader.defaultStorePath : trimmed
|
||||
root["session"] = session
|
||||
|
||||
guard OpenClawConfigFile.saveDict(root) else {
|
||||
self.sessionStoreSaveError = "Config write rejected to protect gateway auth/mode."
|
||||
return
|
||||
}
|
||||
OpenClawConfigFile.saveDict(root)
|
||||
self.sessionStoreSaveError = nil
|
||||
}
|
||||
|
||||
|
||||
@@ -43,8 +43,7 @@ enum ExecApprovalEvaluator {
|
||||
let allowAlwaysPatterns = ExecCommandResolution.resolveAllowAlwaysPatterns(
|
||||
command: command,
|
||||
cwd: cwd,
|
||||
env: env,
|
||||
rawCommand: allowlistRawCommand)
|
||||
env: env)
|
||||
let allowlistMatches = security == .allowlist
|
||||
? ExecAllowlistMatcher.matchAll(entries: approvals.allowlist, resolutions: allowlistResolutions)
|
||||
: []
|
||||
|
||||
@@ -27,7 +27,7 @@ struct ExecCommandResolution {
|
||||
{
|
||||
// Allowlist resolution must follow actual argv execution for wrappers.
|
||||
// `rawCommand` is caller-supplied display text and may be canonicalized.
|
||||
let shell = ExecShellWrapperParser.extractForAllowlist(command: command, rawCommand: rawCommand)
|
||||
let shell = ExecShellWrapperParser.extract(command: command, rawCommand: nil)
|
||||
if shell.isWrapper {
|
||||
// Fail closed when env modifiers precede a shell wrapper. This mirrors
|
||||
// system-run binding behavior where such invocations must stay bound to
|
||||
@@ -68,8 +68,7 @@ struct ExecCommandResolution {
|
||||
static func resolveAllowAlwaysPatterns(
|
||||
command: [String],
|
||||
cwd: String?,
|
||||
env: [String: String]?,
|
||||
rawCommand: String? = nil) -> [String]
|
||||
env: [String: String]?) -> [String]
|
||||
{
|
||||
var patterns: [String] = []
|
||||
var seen = Set<String>()
|
||||
@@ -77,7 +76,6 @@ struct ExecCommandResolution {
|
||||
command: command,
|
||||
cwd: cwd,
|
||||
env: env,
|
||||
rawCommand: rawCommand,
|
||||
depth: 0,
|
||||
patterns: &patterns,
|
||||
seen: &seen)
|
||||
@@ -154,7 +152,6 @@ struct ExecCommandResolution {
|
||||
command: [String],
|
||||
cwd: String?,
|
||||
env: [String: String]?,
|
||||
rawCommand: String?,
|
||||
depth: Int,
|
||||
patterns: inout [String],
|
||||
seen: inout Set<String>)
|
||||
@@ -165,19 +162,13 @@ struct ExecCommandResolution {
|
||||
|
||||
if let token0 = command.first?.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||
ExecCommandToken.basenameLower(token0) == "env",
|
||||
let envUnwrapped = ExecEnvInvocationUnwrapper.unwrapWithMetadata(command),
|
||||
!envUnwrapped.command.isEmpty
|
||||
let envUnwrapped = ExecEnvInvocationUnwrapper.unwrap(command),
|
||||
!envUnwrapped.isEmpty
|
||||
{
|
||||
if envUnwrapped.usesModifiers,
|
||||
self.isAllowlistShellWrapper(command: envUnwrapped.command, rawCommand: rawCommand)
|
||||
{
|
||||
return
|
||||
}
|
||||
self.collectAllowAlwaysPatterns(
|
||||
command: envUnwrapped.command,
|
||||
command: envUnwrapped,
|
||||
cwd: cwd,
|
||||
env: env,
|
||||
rawCommand: rawCommand,
|
||||
depth: depth + 1,
|
||||
patterns: &patterns,
|
||||
seen: &seen)
|
||||
@@ -189,14 +180,13 @@ struct ExecCommandResolution {
|
||||
command: shellMultiplexer,
|
||||
cwd: cwd,
|
||||
env: env,
|
||||
rawCommand: rawCommand,
|
||||
depth: depth + 1,
|
||||
patterns: &patterns,
|
||||
seen: &seen)
|
||||
return
|
||||
}
|
||||
|
||||
let shell = ExecShellWrapperParser.extractForAllowlist(command: command, rawCommand: rawCommand)
|
||||
let shell = ExecShellWrapperParser.extract(command: command, rawCommand: nil)
|
||||
if shell.isWrapper {
|
||||
guard let shellCommand = shell.command,
|
||||
let segments = self.splitShellCommandChain(shellCommand)
|
||||
@@ -212,7 +202,6 @@ struct ExecCommandResolution {
|
||||
command: tokens,
|
||||
cwd: cwd,
|
||||
env: env,
|
||||
rawCommand: nil,
|
||||
depth: depth + 1,
|
||||
patterns: &patterns,
|
||||
seen: &seen)
|
||||
@@ -229,10 +218,6 @@ struct ExecCommandResolution {
|
||||
patterns.append(pattern)
|
||||
}
|
||||
|
||||
private static func isAllowlistShellWrapper(command: [String], rawCommand: String?) -> Bool {
|
||||
ExecShellWrapperParser.extractForAllowlist(command: command, rawCommand: rawCommand).isWrapper
|
||||
}
|
||||
|
||||
private static func unwrapShellMultiplexerInvocation(_ argv: [String]) -> [String]? {
|
||||
guard let token0 = argv.first?.trimmingCharacters(in: .whitespacesAndNewlines), !token0.isEmpty else {
|
||||
return nil
|
||||
|
||||
@@ -1,278 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
enum ExecInlineCommandParser {
|
||||
struct Match {
|
||||
let tokenIndex: Int
|
||||
let inlineCommand: String?
|
||||
let valueTokenOffset: Int
|
||||
|
||||
init(tokenIndex: Int, inlineCommand: String?, valueTokenOffset: Int = 1) {
|
||||
self.tokenIndex = tokenIndex
|
||||
self.inlineCommand = inlineCommand
|
||||
self.valueTokenOffset = valueTokenOffset
|
||||
}
|
||||
}
|
||||
|
||||
private struct CombinedCommandFlag {
|
||||
let attachedCommand: String?
|
||||
let separateValueCount: Int
|
||||
}
|
||||
|
||||
private static let posixShellOptionsWithSeparateValues = Set([
|
||||
"--init-file",
|
||||
"--rcfile",
|
||||
"-O",
|
||||
"-o",
|
||||
"+O",
|
||||
"+o",
|
||||
])
|
||||
|
||||
static func hasPosixInteractiveStartupBeforeInlineCommand(
|
||||
_ argv: [String],
|
||||
flags: Set<String>) -> Bool
|
||||
{
|
||||
var idx = 1
|
||||
var sawInteractiveMode = false
|
||||
while idx < argv.count {
|
||||
let token = argv[idx].trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if token.isEmpty {
|
||||
idx += 1
|
||||
continue
|
||||
}
|
||||
if token == "--" {
|
||||
return false
|
||||
}
|
||||
if self.isPosixInteractiveModeOption(token) {
|
||||
sawInteractiveMode = true
|
||||
}
|
||||
if flags.contains(token) || self.isCombinedCommandFlag(token) {
|
||||
return sawInteractiveMode
|
||||
}
|
||||
if !token.hasPrefix("-"), !token.hasPrefix("+") {
|
||||
return false
|
||||
}
|
||||
let combinedValueCount = self.combinedSeparateValueOptionCount(token)
|
||||
if combinedValueCount > 0 {
|
||||
idx += 1 + combinedValueCount
|
||||
continue
|
||||
}
|
||||
if self.consumesSeparateValue(token) {
|
||||
idx += 2
|
||||
continue
|
||||
}
|
||||
idx += 1
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
static func hasPosixLoginStartupBeforeInlineCommand(
|
||||
_ argv: [String],
|
||||
flags: Set<String>) -> Bool
|
||||
{
|
||||
var idx = 1
|
||||
var sawLoginMode = false
|
||||
while idx < argv.count {
|
||||
let token = argv[idx].trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if token.isEmpty {
|
||||
idx += 1
|
||||
continue
|
||||
}
|
||||
if token == "--" {
|
||||
return false
|
||||
}
|
||||
if token == "--login" || self.isPosixShortOption(token, containing: "l") {
|
||||
sawLoginMode = true
|
||||
}
|
||||
if flags.contains(token) || self.isCombinedCommandFlag(token) {
|
||||
return sawLoginMode
|
||||
}
|
||||
if !token.hasPrefix("-"), !token.hasPrefix("+") {
|
||||
return false
|
||||
}
|
||||
let combinedValueCount = self.combinedSeparateValueOptionCount(token)
|
||||
if combinedValueCount > 0 {
|
||||
idx += 1 + combinedValueCount
|
||||
continue
|
||||
}
|
||||
if self.consumesSeparateValue(token) {
|
||||
idx += 2
|
||||
continue
|
||||
}
|
||||
idx += 1
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
static func hasFishInitCommandOption(_ argv: [String]) -> Bool {
|
||||
var idx = 1
|
||||
while idx < argv.count {
|
||||
let token = argv[idx].trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if token.isEmpty {
|
||||
idx += 1
|
||||
continue
|
||||
}
|
||||
if token == "--" {
|
||||
return false
|
||||
}
|
||||
if token == "-C" || token == "--init-command" {
|
||||
return true
|
||||
}
|
||||
if token.hasPrefix("-C"), token != "-C" {
|
||||
return true
|
||||
}
|
||||
if token.hasPrefix("--init-command=") {
|
||||
return true
|
||||
}
|
||||
if !token.hasPrefix("-"), !token.hasPrefix("+") {
|
||||
return false
|
||||
}
|
||||
idx += 1
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
static func hasFishAttachedCommandOption(_ argv: [String]) -> Bool {
|
||||
var idx = 1
|
||||
while idx < argv.count {
|
||||
let token = argv[idx].trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if token.isEmpty {
|
||||
idx += 1
|
||||
continue
|
||||
}
|
||||
if token == "--" {
|
||||
return false
|
||||
}
|
||||
if token.hasPrefix("-c"), token != "-c" {
|
||||
return true
|
||||
}
|
||||
if !token.hasPrefix("-"), !token.hasPrefix("+") {
|
||||
return false
|
||||
}
|
||||
idx += 1
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
static func findMatch(
|
||||
_ argv: [String],
|
||||
flags: Set<String>,
|
||||
allowCombinedC: Bool) -> Match?
|
||||
{
|
||||
var idx = 1
|
||||
while idx < argv.count {
|
||||
let token = argv[idx].trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if token.isEmpty {
|
||||
idx += 1
|
||||
continue
|
||||
}
|
||||
if token == "--" {
|
||||
break
|
||||
}
|
||||
let comparableToken = allowCombinedC ? token : token.lowercased()
|
||||
if flags.contains(comparableToken) {
|
||||
return Match(tokenIndex: idx, inlineCommand: nil)
|
||||
}
|
||||
if allowCombinedC, let combined = self.parseCombinedCommandFlag(token) {
|
||||
if let attachedCommand = combined.attachedCommand {
|
||||
return Match(tokenIndex: idx, inlineCommand: attachedCommand, valueTokenOffset: 0)
|
||||
}
|
||||
return Match(
|
||||
tokenIndex: idx,
|
||||
inlineCommand: nil,
|
||||
valueTokenOffset: 1 + combined.separateValueCount)
|
||||
}
|
||||
if allowCombinedC, !token.hasPrefix("-"), !token.hasPrefix("+") {
|
||||
break
|
||||
}
|
||||
let combinedValueCount = allowCombinedC ? self.combinedSeparateValueOptionCount(token) : 0
|
||||
if combinedValueCount > 0 {
|
||||
idx += 1 + combinedValueCount
|
||||
continue
|
||||
}
|
||||
if allowCombinedC, self.consumesSeparateValue(token) {
|
||||
idx += 2
|
||||
continue
|
||||
}
|
||||
idx += 1
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
static func extractInlineCommand(
|
||||
_ argv: [String],
|
||||
flags: Set<String>,
|
||||
allowCombinedC: Bool) -> String?
|
||||
{
|
||||
guard let match = self.findMatch(argv, flags: flags, allowCombinedC: allowCombinedC) else {
|
||||
return nil
|
||||
}
|
||||
if let inlineCommand = match.inlineCommand {
|
||||
return inlineCommand
|
||||
}
|
||||
let nextIndex = match.tokenIndex + match.valueTokenOffset
|
||||
let payload = nextIndex < argv.count
|
||||
? argv[nextIndex].trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
: ""
|
||||
return payload.isEmpty ? nil : payload
|
||||
}
|
||||
|
||||
private static func isCombinedCommandFlag(_ token: String) -> Bool {
|
||||
self.parseCombinedCommandFlag(token) != nil
|
||||
}
|
||||
|
||||
private static func parseCombinedCommandFlag(_ token: String) -> CombinedCommandFlag? {
|
||||
let chars = Array(token)
|
||||
guard chars.count >= 2, chars[0] == "-", chars[1] != "-" else {
|
||||
return nil
|
||||
}
|
||||
let optionChars = Array(chars.dropFirst())
|
||||
guard let commandFlagIndex = optionChars.firstIndex(of: "c") else {
|
||||
return nil
|
||||
}
|
||||
if optionChars.contains("-") {
|
||||
return nil
|
||||
}
|
||||
let suffix = String(optionChars.dropFirst(commandFlagIndex + 1))
|
||||
if !suffix.isEmpty,
|
||||
suffix.range(of: #"[^A-Za-z]"#, options: .regularExpression) != nil
|
||||
{
|
||||
return CombinedCommandFlag(attachedCommand: suffix, separateValueCount: 0)
|
||||
}
|
||||
let separateValueCount = optionChars.reduce(0) { count, char in
|
||||
count + ((char == "o" || char == "O") ? 1 : 0)
|
||||
}
|
||||
return CombinedCommandFlag(attachedCommand: nil, separateValueCount: separateValueCount)
|
||||
}
|
||||
|
||||
private static func combinedSeparateValueOptionCount(_ token: String) -> Int {
|
||||
let chars = Array(token)
|
||||
guard chars.count >= 2, chars[0] == "-" || chars[0] == "+", chars[1] != "-" else {
|
||||
return 0
|
||||
}
|
||||
if chars.dropFirst().contains("-") {
|
||||
return 0
|
||||
}
|
||||
return chars.dropFirst().reduce(0) { count, char in
|
||||
count + ((char == "o" || char == "O") ? 1 : 0)
|
||||
}
|
||||
}
|
||||
|
||||
private static func consumesSeparateValue(_ token: String) -> Bool {
|
||||
self.posixShellOptionsWithSeparateValues.contains(token)
|
||||
}
|
||||
|
||||
private static func isPosixInteractiveModeOption(_ token: String) -> Bool {
|
||||
token == "--interactive" || self.isPosixShortOption(token, containing: "i")
|
||||
}
|
||||
|
||||
private static func isPosixShortOption(_ token: String, containing option: Character) -> Bool {
|
||||
let chars = Array(token)
|
||||
guard chars.count >= 2, chars[0] == "-", chars[1] != "-" else {
|
||||
return false
|
||||
}
|
||||
if chars.dropFirst().contains("-") {
|
||||
return false
|
||||
}
|
||||
return chars.dropFirst().contains(option)
|
||||
}
|
||||
}
|
||||
@@ -6,10 +6,9 @@ enum ExecShellWrapperParser {
|
||||
let command: String?
|
||||
|
||||
static let notWrapper = ParsedShellWrapper(isWrapper: false, command: nil)
|
||||
static let blockedWrapper = ParsedShellWrapper(isWrapper: true, command: nil)
|
||||
}
|
||||
|
||||
private enum Kind: Equatable {
|
||||
private enum Kind {
|
||||
case posix
|
||||
case cmd
|
||||
case powershell
|
||||
@@ -28,34 +27,14 @@ enum ExecShellWrapperParser {
|
||||
WrapperSpec(kind: .cmd, names: ["cmd.exe", "cmd"]),
|
||||
WrapperSpec(kind: .powershell, names: ["powershell", "powershell.exe", "pwsh", "pwsh.exe"]),
|
||||
]
|
||||
private static let loginStartupShellNames = Set(["ash", "bash", "dash", "fish", "ksh", "sh", "zsh"])
|
||||
|
||||
static func extract(command: [String], rawCommand: String?) -> ParsedShellWrapper {
|
||||
let trimmedRaw = rawCommand?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
let preferredRaw = trimmedRaw.isEmpty ? nil : trimmedRaw
|
||||
return self.extract(
|
||||
command: command,
|
||||
preferredRaw: preferredRaw,
|
||||
failClosedOnStartupWrappers: false,
|
||||
depth: 0)
|
||||
return self.extract(command: command, preferredRaw: preferredRaw, depth: 0)
|
||||
}
|
||||
|
||||
static func extractForAllowlist(command: [String], rawCommand: String?) -> ParsedShellWrapper {
|
||||
let trimmedRaw = rawCommand?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
let preferredRaw = trimmedRaw.isEmpty ? nil : trimmedRaw
|
||||
return self.extract(
|
||||
command: command,
|
||||
preferredRaw: preferredRaw,
|
||||
failClosedOnStartupWrappers: true,
|
||||
depth: 0)
|
||||
}
|
||||
|
||||
private static func extract(
|
||||
command: [String],
|
||||
preferredRaw: String?,
|
||||
failClosedOnStartupWrappers: Bool,
|
||||
depth: Int) -> ParsedShellWrapper
|
||||
{
|
||||
private static func extract(command: [String], preferredRaw: String?, depth: Int) -> ParsedShellWrapper {
|
||||
guard depth < ExecEnvInvocationUnwrapper.maxWrapperDepth else {
|
||||
return .notWrapper
|
||||
}
|
||||
@@ -68,96 +47,19 @@ enum ExecShellWrapperParser {
|
||||
guard let unwrapped = ExecEnvInvocationUnwrapper.unwrap(command) else {
|
||||
return .notWrapper
|
||||
}
|
||||
return self.extract(
|
||||
command: unwrapped,
|
||||
preferredRaw: preferredRaw,
|
||||
failClosedOnStartupWrappers: failClosedOnStartupWrappers,
|
||||
depth: depth + 1)
|
||||
return self.extract(command: unwrapped, preferredRaw: preferredRaw, depth: depth + 1)
|
||||
}
|
||||
|
||||
guard let spec = self.wrapperSpecs.first(where: { $0.names.contains(base0) }) else {
|
||||
return .notWrapper
|
||||
}
|
||||
if spec.kind == .posix,
|
||||
base0 == "fish",
|
||||
ExecInlineCommandParser.hasFishAttachedCommandOption(command)
|
||||
{
|
||||
return .blockedWrapper
|
||||
}
|
||||
let includeLegacyLoginInlineForm = failClosedOnStartupWrappers &&
|
||||
!self.legacyLoginInlinePayloadMatchesRaw(
|
||||
command: command,
|
||||
spec: spec,
|
||||
base0: base0,
|
||||
preferredRaw: preferredRaw)
|
||||
if self.startupWrapperRequiresFullArgv(
|
||||
command: command,
|
||||
spec: spec,
|
||||
base0: base0,
|
||||
includeLegacyLoginInlineForm: includeLegacyLoginInlineForm)
|
||||
{
|
||||
return .blockedWrapper
|
||||
}
|
||||
guard let payload = self.extractPayload(command: command, spec: spec) else {
|
||||
return .notWrapper
|
||||
}
|
||||
let normalized = failClosedOnStartupWrappers ? payload : preferredRaw ?? payload
|
||||
let normalized = preferredRaw ?? payload
|
||||
return ParsedShellWrapper(isWrapper: true, command: normalized)
|
||||
}
|
||||
|
||||
private static func startupWrapperRequiresFullArgv(
|
||||
command: [String],
|
||||
spec: WrapperSpec,
|
||||
base0: String,
|
||||
includeLegacyLoginInlineForm: Bool) -> Bool
|
||||
{
|
||||
guard spec.kind == .posix else {
|
||||
return false
|
||||
}
|
||||
if base0 == "fish",
|
||||
ExecInlineCommandParser.hasFishInitCommandOption(command)
|
||||
{
|
||||
return true
|
||||
}
|
||||
if self.loginStartupShellNames.contains(base0),
|
||||
ExecInlineCommandParser.hasPosixLoginStartupBeforeInlineCommand(
|
||||
command,
|
||||
flags: self.posixInlineFlags)
|
||||
{
|
||||
return includeLegacyLoginInlineForm || !self.isLegacyShLoginInlineForm(command, base0: base0)
|
||||
}
|
||||
return ExecInlineCommandParser.hasPosixInteractiveStartupBeforeInlineCommand(
|
||||
command,
|
||||
flags: self.posixInlineFlags)
|
||||
}
|
||||
|
||||
private static func isLegacyLoginInlineForm(_ command: [String]) -> Bool {
|
||||
guard command.count > 1 else {
|
||||
return false
|
||||
}
|
||||
return command[1].trimmingCharacters(in: .whitespacesAndNewlines) == "-lc"
|
||||
}
|
||||
|
||||
private static func isLegacyShLoginInlineForm(_ command: [String], base0: String) -> Bool {
|
||||
base0 == "sh" && self.isLegacyLoginInlineForm(command)
|
||||
}
|
||||
|
||||
private static func legacyLoginInlinePayloadMatchesRaw(
|
||||
command: [String],
|
||||
spec: WrapperSpec,
|
||||
base0: String,
|
||||
preferredRaw: String?) -> Bool
|
||||
{
|
||||
guard let preferredRaw,
|
||||
base0 == "sh",
|
||||
self.isLegacyLoginInlineForm(command),
|
||||
let payload = self.extractPayload(command: command, spec: spec)
|
||||
else {
|
||||
return false
|
||||
}
|
||||
return payload == preferredRaw.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
}
|
||||
|
||||
private static func extractPayload(command: [String], spec: WrapperSpec) -> String? {
|
||||
switch spec.kind {
|
||||
case .posix:
|
||||
@@ -170,10 +72,12 @@ enum ExecShellWrapperParser {
|
||||
}
|
||||
|
||||
private static func extractPosixInlineCommand(_ command: [String]) -> String? {
|
||||
ExecInlineCommandParser.extractInlineCommand(
|
||||
command,
|
||||
flags: self.posixInlineFlags,
|
||||
allowCombinedC: true)
|
||||
let flag = command.count > 1 ? command[1].trimmingCharacters(in: .whitespacesAndNewlines) : ""
|
||||
guard self.posixInlineFlags.contains(flag.lowercased()) else {
|
||||
return nil
|
||||
}
|
||||
let payload = command.count > 2 ? command[2].trimmingCharacters(in: .whitespacesAndNewlines) : ""
|
||||
return payload.isEmpty ? nil : payload
|
||||
}
|
||||
|
||||
private static func extractCmdInlineCommand(_ command: [String]) -> String? {
|
||||
@@ -193,10 +97,10 @@ enum ExecShellWrapperParser {
|
||||
if token.isEmpty { continue }
|
||||
if token == "--" { break }
|
||||
if self.powershellInlineFlags.contains(token) {
|
||||
return ExecInlineCommandParser.extractInlineCommand(
|
||||
command,
|
||||
flags: self.powershellInlineFlags,
|
||||
allowCombinedC: false)
|
||||
let payload = idx + 1 < command.count
|
||||
? command[idx + 1].trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
: ""
|
||||
return payload.isEmpty ? nil : payload
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -326,12 +326,40 @@ enum ExecSystemRunCommandValidator {
|
||||
return current
|
||||
}
|
||||
|
||||
private struct InlineCommandTokenMatch {
|
||||
var tokenIndex: Int
|
||||
var inlineCommand: String?
|
||||
}
|
||||
|
||||
private static func findInlineCommandTokenMatch(
|
||||
_ argv: [String],
|
||||
flags: Set<String>,
|
||||
allowCombinedC: Bool) -> ExecInlineCommandParser.Match?
|
||||
allowCombinedC: Bool) -> InlineCommandTokenMatch?
|
||||
{
|
||||
ExecInlineCommandParser.findMatch(argv, flags: flags, allowCombinedC: allowCombinedC)
|
||||
var idx = 1
|
||||
while idx < argv.count {
|
||||
let token = argv[idx].trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if token.isEmpty {
|
||||
idx += 1
|
||||
continue
|
||||
}
|
||||
let lower = token.lowercased()
|
||||
if lower == "--" {
|
||||
break
|
||||
}
|
||||
if flags.contains(lower) {
|
||||
return InlineCommandTokenMatch(tokenIndex: idx, inlineCommand: nil)
|
||||
}
|
||||
if allowCombinedC, let inlineOffset = self.combinedCommandInlineOffset(token) {
|
||||
let inline = String(token.dropFirst(inlineOffset))
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
return InlineCommandTokenMatch(
|
||||
tokenIndex: idx,
|
||||
inlineCommand: inline.isEmpty ? nil : inline)
|
||||
}
|
||||
idx += 1
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private static func resolveInlineCommandTokenIndex(
|
||||
@@ -345,10 +373,24 @@ enum ExecSystemRunCommandValidator {
|
||||
if match.inlineCommand != nil {
|
||||
return match.tokenIndex
|
||||
}
|
||||
let nextIndex = match.tokenIndex + match.valueTokenOffset
|
||||
let nextIndex = match.tokenIndex + 1
|
||||
return nextIndex < argv.count ? nextIndex : nil
|
||||
}
|
||||
|
||||
private static func combinedCommandInlineOffset(_ token: String) -> Int? {
|
||||
let chars = Array(token.lowercased())
|
||||
guard chars.count >= 2, chars[0] == "-", chars[1] != "-" else {
|
||||
return nil
|
||||
}
|
||||
if chars.dropFirst().contains("-") {
|
||||
return nil
|
||||
}
|
||||
guard let commandIndex = chars.firstIndex(of: "c"), commandIndex > 0 else {
|
||||
return nil
|
||||
}
|
||||
return commandIndex + 1
|
||||
}
|
||||
|
||||
private static func extractShellInlinePayload(
|
||||
_ argv: [String],
|
||||
normalizedWrapper: String) -> String?
|
||||
@@ -379,7 +421,7 @@ enum ExecSystemRunCommandValidator {
|
||||
if let inlineCommand = match.inlineCommand {
|
||||
return inlineCommand
|
||||
}
|
||||
let nextIndex = match.tokenIndex + match.valueTokenOffset
|
||||
let nextIndex = match.tokenIndex + 1
|
||||
return self.trimmedNonEmpty(nextIndex < argv.count ? argv[nextIndex] : nil)
|
||||
}
|
||||
|
||||
|
||||
@@ -52,16 +52,14 @@ enum OpenClawConfigFile {
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
static func saveDict(
|
||||
_ dict: [String: Any],
|
||||
preserveExistingKeys: Bool = false,
|
||||
allowGatewayAuthMutation: Bool = false)
|
||||
-> Bool
|
||||
{
|
||||
self.withFileLock {
|
||||
// Nix mode disables config writes in production, but tests rely on saving temp configs.
|
||||
if ProcessInfo.processInfo.isNixMode, !ProcessInfo.processInfo.isRunningTests { return false }
|
||||
if ProcessInfo.processInfo.isNixMode, !ProcessInfo.processInfo.isRunningTests { return }
|
||||
let url = self.url()
|
||||
let previousData = try? Data(contentsOf: url)
|
||||
let previousRoot = previousData.flatMap { self.parseConfigData($0) }
|
||||
@@ -83,7 +81,12 @@ enum OpenClawConfigFile {
|
||||
|
||||
do {
|
||||
let data = try JSONSerialization.data(withJSONObject: output, options: [.prettyPrinted, .sortedKeys])
|
||||
try FileManager().createDirectory(
|
||||
at: url.deletingLastPathComponent(),
|
||||
withIntermediateDirectories: true)
|
||||
try data.write(to: url, options: [.atomic])
|
||||
let nextBytes = data.count
|
||||
let nextAttributes = try? FileManager().attributesOfItem(atPath: url.path)
|
||||
let gatewayModeAfter = self.gatewayMode(output)
|
||||
var suspicious = self.configWriteSuspiciousReasons(
|
||||
existsBefore: previousData != nil,
|
||||
@@ -95,44 +98,6 @@ enum OpenClawConfigFile {
|
||||
if preservedGatewayAuth {
|
||||
suspicious.append("gateway-auth-preserved")
|
||||
}
|
||||
let blocking = self.configWriteBlockingReasons(suspicious)
|
||||
if !blocking.isEmpty {
|
||||
let rejectedPath = self.persistRejectedConfigWrite(data: data, configURL: url)
|
||||
self.logger.warning("config write rejected (\(blocking.joined(separator: ", "))) at \(url.path)")
|
||||
self.appendConfigWriteAudit([
|
||||
"result": "rejected",
|
||||
"configPath": url.path,
|
||||
"existsBefore": previousData != nil,
|
||||
"previousBytes": previousBytes ?? NSNull(),
|
||||
"nextBytes": nextBytes,
|
||||
"previousDev": self.fileSystemNumber(previousAttributes?[.systemNumber]) ?? NSNull(),
|
||||
"nextDev": NSNull(),
|
||||
"previousIno": self.fileSystemNumber(previousAttributes?[.systemFileNumber]) ?? NSNull(),
|
||||
"nextIno": NSNull(),
|
||||
"previousMode": self.posixMode(previousAttributes?[.posixPermissions]) ?? NSNull(),
|
||||
"nextMode": NSNull(),
|
||||
"previousNlink": self.fileAttributeInt(previousAttributes?[.referenceCount]) ?? NSNull(),
|
||||
"nextNlink": NSNull(),
|
||||
"previousUid": self.fileAttributeInt(previousAttributes?[.ownerAccountID]) ?? NSNull(),
|
||||
"nextUid": NSNull(),
|
||||
"previousGid": self.fileAttributeInt(previousAttributes?[.groupOwnerAccountID]) ?? NSNull(),
|
||||
"nextGid": NSNull(),
|
||||
"hasMetaBefore": hadMetaBefore,
|
||||
"hasMetaAfter": self.hasMeta(output),
|
||||
"gatewayModeBefore": gatewayModeBefore ?? NSNull(),
|
||||
"gatewayModeAfter": gatewayModeAfter ?? NSNull(),
|
||||
"preservedGatewayAuth": preservedGatewayAuth,
|
||||
"suspicious": suspicious,
|
||||
"blocking": blocking,
|
||||
"rejectedPath": rejectedPath ?? NSNull(),
|
||||
])
|
||||
return false
|
||||
}
|
||||
try FileManager().createDirectory(
|
||||
at: url.deletingLastPathComponent(),
|
||||
withIntermediateDirectories: true)
|
||||
try data.write(to: url, options: [.atomic])
|
||||
let nextAttributes = try? FileManager().attributesOfItem(atPath: url.path)
|
||||
if !suspicious.isEmpty {
|
||||
self.logger.warning("config write anomaly (\(suspicious.joined(separator: ", "))) at \(url.path)")
|
||||
}
|
||||
@@ -158,11 +123,9 @@ enum OpenClawConfigFile {
|
||||
"hasMetaAfter": self.hasMeta(output),
|
||||
"gatewayModeBefore": gatewayModeBefore ?? NSNull(),
|
||||
"gatewayModeAfter": gatewayModeAfter ?? NSNull(),
|
||||
"preservedGatewayAuth": preservedGatewayAuth,
|
||||
"suspicious": suspicious,
|
||||
])
|
||||
self.observeConfigRead(data: data, root: output, configURL: url, valid: true)
|
||||
return true
|
||||
} catch {
|
||||
self.logger.error("config save failed: \(error.localizedDescription)")
|
||||
self.appendConfigWriteAudit([
|
||||
@@ -175,11 +138,9 @@ enum OpenClawConfigFile {
|
||||
"hasMetaAfter": self.hasMeta(output),
|
||||
"gatewayModeBefore": gatewayModeBefore ?? NSNull(),
|
||||
"gatewayModeAfter": self.gatewayMode(output) ?? NSNull(),
|
||||
"preservedGatewayAuth": preservedGatewayAuth,
|
||||
"suspicious": preservedGatewayAuth ? ["gateway-auth-preserved"] : [],
|
||||
"error": error.localizedDescription,
|
||||
])
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -455,12 +416,6 @@ enum OpenClawConfigFile {
|
||||
return reasons
|
||||
}
|
||||
|
||||
private static func configWriteBlockingReasons(_ suspicious: [String]) -> [String] {
|
||||
suspicious.filter { reason in
|
||||
reason.hasPrefix("size-drop:") || reason == "gateway-mode-removed"
|
||||
}
|
||||
}
|
||||
|
||||
private static func configAuditLogURL() -> URL {
|
||||
self.stateDirURL()
|
||||
.appendingPathComponent("logs", isDirectory: true)
|
||||
@@ -639,26 +594,6 @@ enum OpenClawConfigFile {
|
||||
}
|
||||
}
|
||||
|
||||
private static func persistRejectedConfigWrite(data: Data, configURL: URL) -> String? {
|
||||
let timestamp = ISO8601DateFormatter().string(from: Date())
|
||||
let url = configURL.deletingLastPathComponent()
|
||||
.appendingPathComponent("\(configURL.lastPathComponent).rejected.\(self.configTimestampToken(timestamp))")
|
||||
let fileManager = FileManager()
|
||||
let privatePermissions: NSNumber = 0o600
|
||||
if fileManager.fileExists(atPath: url.path) {
|
||||
try? fileManager.setAttributes([.posixPermissions: privatePermissions], ofItemAtPath: url.path)
|
||||
return url.path
|
||||
}
|
||||
guard fileManager.createFile(
|
||||
atPath: url.path,
|
||||
contents: data,
|
||||
attributes: [.posixPermissions: privatePermissions])
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
return url.path
|
||||
}
|
||||
|
||||
private static func observeConfigRead(data: Data, root: [String: Any]?, configURL: URL, valid: Bool) {
|
||||
let observedAt = ISO8601DateFormatter().string(from: Date())
|
||||
let current = self.configFingerprint(data: data, root: root, configURL: configURL, observedAt: observedAt)
|
||||
|
||||
@@ -15,9 +15,9 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>2026.5.8</string>
|
||||
<string>2026.5.6</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>2026050800</string>
|
||||
<string>2026050600</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>OpenClaw</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
|
||||
@@ -259,37 +259,4 @@ struct AppStateRemoteConfigTests {
|
||||
remoteTokenDirty: true))
|
||||
#expect((cleared["token"] as? String) == nil)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `synced gateway root preserves gateway auth across mode changes`() {
|
||||
let initialRoot: [String: Any] = [
|
||||
"gateway": [
|
||||
"mode": "remote",
|
||||
"auth": [
|
||||
"mode": "token",
|
||||
"token": "test-token", // pragma: allowlist secret
|
||||
],
|
||||
"remote": [
|
||||
"transport": "direct",
|
||||
"url": "wss://old-gateway.example",
|
||||
],
|
||||
],
|
||||
]
|
||||
|
||||
let localRoot = AppState._testSyncedGatewayRoot(
|
||||
currentRoot: initialRoot,
|
||||
draft: .init(
|
||||
connectionMode: .local,
|
||||
remoteTransport: .ssh,
|
||||
remoteTarget: "",
|
||||
remoteIdentity: "",
|
||||
remoteUrl: "",
|
||||
remoteToken: "",
|
||||
remoteTokenDirty: false))
|
||||
let localGateway = localRoot["gateway"] as? [String: Any]
|
||||
let auth = localGateway?["auth"] as? [String: Any]
|
||||
#expect(localGateway?["mode"] as? String == "local")
|
||||
#expect(auth?["mode"] as? String == "token")
|
||||
#expect(auth?["token"] as? String == "test-token") // pragma: allowlist secret
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import Foundation
|
||||
import Testing
|
||||
@testable import OpenClaw
|
||||
|
||||
@@ -66,76 +65,4 @@ struct ConfigStoreTests {
|
||||
#expect(localHit)
|
||||
#expect(!remoteHit)
|
||||
}
|
||||
|
||||
@Test func `local save does not fall back to direct write after stale gateway rejection`() async throws {
|
||||
let stateDir = FileManager().temporaryDirectory
|
||||
.appendingPathComponent("openclaw-state-\(UUID().uuidString)", isDirectory: true)
|
||||
let configPath = stateDir.appendingPathComponent("openclaw.json")
|
||||
defer { try? FileManager().removeItem(at: stateDir) }
|
||||
|
||||
try await TestIsolation.withEnvValues([
|
||||
"OPENCLAW_STATE_DIR": stateDir.path,
|
||||
"OPENCLAW_CONFIG_PATH": configPath.path,
|
||||
]) {
|
||||
OpenClawConfigFile.saveDict([
|
||||
"gateway": [
|
||||
"mode": "local",
|
||||
"auth": [
|
||||
"mode": "token",
|
||||
"token": "test-token", // pragma: allowlist secret
|
||||
],
|
||||
],
|
||||
])
|
||||
let before = try String(contentsOf: configPath, encoding: .utf8)
|
||||
await ConfigStore._testSetOverrides(.init(
|
||||
isRemoteMode: { false },
|
||||
saveGateway: { _ in
|
||||
throw NSError(domain: "Gateway", code: 0, userInfo: [
|
||||
NSLocalizedDescriptionKey: "config changed since last load; re-run config.get and retry",
|
||||
])
|
||||
}))
|
||||
|
||||
var didThrow = false
|
||||
do {
|
||||
try await ConfigStore.save(["browser": ["enabled": false]])
|
||||
} catch {
|
||||
didThrow = true
|
||||
}
|
||||
await ConfigStore._testClearOverrides()
|
||||
|
||||
#expect(didThrow)
|
||||
let after = try String(contentsOf: configPath, encoding: .utf8)
|
||||
#expect(after == before)
|
||||
}
|
||||
}
|
||||
|
||||
@Test func `local save can fall back to protected direct write when gateway is unavailable`() async throws {
|
||||
let stateDir = FileManager().temporaryDirectory
|
||||
.appendingPathComponent("openclaw-state-\(UUID().uuidString)", isDirectory: true)
|
||||
let configPath = stateDir.appendingPathComponent("openclaw.json")
|
||||
defer { try? FileManager().removeItem(at: stateDir) }
|
||||
|
||||
try await TestIsolation.withEnvValues([
|
||||
"OPENCLAW_STATE_DIR": stateDir.path,
|
||||
"OPENCLAW_CONFIG_PATH": configPath.path,
|
||||
]) {
|
||||
await ConfigStore._testSetOverrides(.init(
|
||||
isRemoteMode: { false },
|
||||
saveGateway: { _ in
|
||||
throw NSError(domain: "Gateway", code: 0, userInfo: [
|
||||
NSLocalizedDescriptionKey: "gateway not configured",
|
||||
])
|
||||
}))
|
||||
try await ConfigStore.save([
|
||||
"gateway": ["mode": "local"],
|
||||
"browser": ["enabled": false],
|
||||
])
|
||||
await ConfigStore._testClearOverrides()
|
||||
|
||||
let data = try Data(contentsOf: configPath)
|
||||
let root = try JSONSerialization.jsonObject(with: data) as? [String: Any]
|
||||
#expect(((root?["browser"] as? [String: Any])?["enabled"] as? Bool) == false)
|
||||
#expect((root?["meta"] as? [String: Any]) != nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,7 +111,7 @@ struct ExecAllowlistTests {
|
||||
}
|
||||
|
||||
@Test func `resolve for allowlist splits shell chains`() {
|
||||
let command = ["/bin/sh", "-c", "echo allowlisted && /usr/bin/touch /tmp/openclaw-allowlist-test"]
|
||||
let command = ["/bin/sh", "-lc", "echo allowlisted && /usr/bin/touch /tmp/openclaw-allowlist-test"]
|
||||
let resolutions = ExecCommandResolution.resolveForAllowlist(
|
||||
command: command,
|
||||
rawCommand: "echo allowlisted && /usr/bin/touch /tmp/openclaw-allowlist-test",
|
||||
@@ -122,109 +122,9 @@ struct ExecAllowlistTests {
|
||||
#expect(resolutions[1].executableName == "touch")
|
||||
}
|
||||
|
||||
@Test func `resolve for allowlist splits posix combined c flag payloads`() {
|
||||
for command in [
|
||||
["/bin/bash", "-xc", "/usr/bin/printf safe_marker"],
|
||||
["/bin/bash", "-ec", "/usr/bin/printf safe_marker"],
|
||||
["/bin/bash", "-euxc", "/usr/bin/printf safe_marker"],
|
||||
["/bin/bash", "-cx", "/usr/bin/printf safe_marker"],
|
||||
["/bin/bash", "-O", "extglob", "-xc", "/usr/bin/printf safe_marker"],
|
||||
["/bin/bash", "-co", "vi", "/usr/bin/printf safe_marker"],
|
||||
["/bin/bash", "-oc", "vi", "/usr/bin/printf safe_marker"],
|
||||
["/bin/bash", "-cO", "extglob", "/usr/bin/printf safe_marker"],
|
||||
["/bin/bash", "-xo", "vi", "-c", "/usr/bin/printf safe_marker"],
|
||||
["/bin/bash", "-xO", "extglob", "-c", "/usr/bin/printf safe_marker"],
|
||||
["/bin/bash", "+xo", "vi", "-c", "/usr/bin/printf safe_marker"],
|
||||
["/bin/bash", "--rcfile", "/tmp/rc", "-c", "/usr/bin/printf safe_marker"],
|
||||
["/bin/bash", "--init-file=/tmp/rc", "-c", "/usr/bin/printf safe_marker"],
|
||||
] {
|
||||
let resolutions = ExecCommandResolution.resolveForAllowlist(
|
||||
command: command,
|
||||
rawCommand: nil,
|
||||
cwd: nil,
|
||||
env: ["PATH": "/usr/bin:/bin"])
|
||||
#expect(resolutions.count == 1)
|
||||
#expect(resolutions[0].resolvedPath == "/usr/bin/printf")
|
||||
#expect(resolutions[0].executableName == "printf")
|
||||
}
|
||||
}
|
||||
|
||||
@Test func `resolve for allowlist treats c after posix shell operand as direct exec`() {
|
||||
for command in [
|
||||
["/bin/bash", "./script.sh", "-c", "/usr/bin/printf safe_marker"],
|
||||
["/bin/bash", "-x", "-C", "echo ok", "-c", "/usr/bin/printf safe_marker"],
|
||||
] {
|
||||
let resolutions = ExecCommandResolution.resolveForAllowlist(
|
||||
command: command,
|
||||
rawCommand: nil,
|
||||
cwd: "/tmp",
|
||||
env: ["PATH": "/usr/bin:/bin"])
|
||||
#expect(resolutions.count == 1)
|
||||
#expect(resolutions[0].resolvedPath == "/bin/bash")
|
||||
#expect(resolutions[0].executableName == "bash")
|
||||
}
|
||||
}
|
||||
|
||||
@Test func `resolve for allowlist fails closed for interactive posix shell wrappers`() {
|
||||
for command in [
|
||||
["/bin/bash", "-i", "-c", "/usr/bin/printf safe_marker"],
|
||||
["/bin/bash", "-ic", "/usr/bin/printf safe_marker"],
|
||||
["/bin/bash", "--rcfile", "/tmp/payload.sh", "-i", "-c", "/usr/bin/printf safe_marker"],
|
||||
["/usr/bin/fish", "--interactive", "-c", "/usr/bin/printf safe_marker"],
|
||||
] {
|
||||
let resolutions = ExecCommandResolution.resolveForAllowlist(
|
||||
command: command,
|
||||
rawCommand: nil,
|
||||
cwd: nil,
|
||||
env: ["PATH": "/usr/bin:/bin"])
|
||||
#expect(resolutions.isEmpty)
|
||||
}
|
||||
}
|
||||
|
||||
@Test func `resolve for allowlist fails closed for login shell wrappers`() {
|
||||
for command in [
|
||||
["/bin/bash", "-l", "-c", "/usr/bin/printf safe_marker"],
|
||||
["/bin/bash", "--login", "-c", "/usr/bin/printf safe_marker"],
|
||||
["/bin/bash", "-xlc", "/usr/bin/printf safe_marker"],
|
||||
["/bin/dash", "-lc", "/usr/bin/printf safe_marker"],
|
||||
["ash", "-lc", "/usr/bin/printf safe_marker"],
|
||||
["/usr/bin/fish", "-l", "-c", "/usr/bin/printf safe_marker"],
|
||||
["/usr/bin/fish", "--login", "-c", "/usr/bin/printf safe_marker"],
|
||||
["/bin/sh", "-lc", "/usr/bin/printf safe_marker"],
|
||||
["/bin/sh", "-x", "-lc", "/usr/bin/printf safe_marker"],
|
||||
["/usr/bin/env", "/bin/sh", "-lc", "/usr/bin/printf safe_marker"],
|
||||
] {
|
||||
let resolutions = ExecCommandResolution.resolveForAllowlist(
|
||||
command: command,
|
||||
rawCommand: nil,
|
||||
cwd: nil,
|
||||
env: ["PATH": "/usr/bin:/bin"])
|
||||
#expect(resolutions.isEmpty)
|
||||
}
|
||||
}
|
||||
|
||||
@Test func `resolve for allowlist fails closed for fish init command wrappers`() {
|
||||
for command in [
|
||||
["/usr/bin/fish", "--init-command=/tmp/payload.fish", "-c", "/usr/bin/printf safe_marker"],
|
||||
["/usr/bin/fish", "--init-command", "/tmp/payload.fish", "-c", "/usr/bin/printf safe_marker"],
|
||||
["/usr/bin/fish", "-C", "/tmp/payload.fish", "-c", "/usr/bin/printf safe_marker"],
|
||||
["/usr/bin/fish", "-C/tmp/payload.fish", "-c", "/usr/bin/printf safe_marker"],
|
||||
["/usr/bin/fish", "--init-command", "-c; /tmp/payload.fish", "/usr/bin/printf safe_marker"],
|
||||
["/usr/bin/fish", "-C", "-c", "/usr/bin/printf safe_marker"],
|
||||
["/usr/bin/fish", "-c/tmp/payload.fish", "/usr/bin/printf safe_marker"],
|
||||
] {
|
||||
let resolutions = ExecCommandResolution.resolveForAllowlist(
|
||||
command: command,
|
||||
rawCommand: nil,
|
||||
cwd: nil,
|
||||
env: ["PATH": "/usr/bin:/bin"])
|
||||
#expect(resolutions.isEmpty)
|
||||
}
|
||||
}
|
||||
|
||||
@Test func `resolve for allowlist uses wrapper argv payload even with canonical raw command`() {
|
||||
let command = ["/bin/sh", "-c", "echo allowlisted && /usr/bin/touch /tmp/openclaw-allowlist-test"]
|
||||
let canonicalRaw = "/bin/sh -c \"echo allowlisted && /usr/bin/touch /tmp/openclaw-allowlist-test\""
|
||||
let command = ["/bin/sh", "-lc", "echo allowlisted && /usr/bin/touch /tmp/openclaw-allowlist-test"]
|
||||
let canonicalRaw = "/bin/sh -lc \"echo allowlisted && /usr/bin/touch /tmp/openclaw-allowlist-test\""
|
||||
let resolutions = ExecCommandResolution.resolveForAllowlist(
|
||||
command: command,
|
||||
rawCommand: canonicalRaw,
|
||||
@@ -235,25 +135,6 @@ struct ExecAllowlistTests {
|
||||
#expect(resolutions[1].executableName == "touch")
|
||||
}
|
||||
|
||||
@Test func `resolve for allowlist preserves generated sh lc raw payload binding`() {
|
||||
let command = ["/bin/sh", "-lc", "/usr/bin/printf safe_marker"]
|
||||
let resolutions = ExecCommandResolution.resolveForAllowlist(
|
||||
command: command,
|
||||
rawCommand: "/usr/bin/printf safe_marker",
|
||||
cwd: nil,
|
||||
env: ["PATH": "/usr/bin:/bin"])
|
||||
#expect(resolutions.count == 1)
|
||||
#expect(resolutions[0].resolvedPath == "/usr/bin/printf")
|
||||
#expect(resolutions[0].executableName == "printf")
|
||||
|
||||
let rawlessResolutions = ExecCommandResolution.resolveForAllowlist(
|
||||
command: command,
|
||||
rawCommand: nil,
|
||||
cwd: nil,
|
||||
env: ["PATH": "/usr/bin:/bin"])
|
||||
#expect(rawlessResolutions.isEmpty)
|
||||
}
|
||||
|
||||
@Test func `resolve for allowlist fails closed for env modified shell wrappers`() {
|
||||
let command = ["/usr/bin/env", "BASH_ENV=/tmp/payload.sh", "bash", "-lc", "echo allowlisted"]
|
||||
let canonicalRaw = "/usr/bin/env BASH_ENV=/tmp/payload.sh bash -lc \"echo allowlisted\""
|
||||
@@ -277,7 +158,7 @@ struct ExecAllowlistTests {
|
||||
}
|
||||
|
||||
@Test func `resolve for allowlist keeps quoted operators in single segment`() {
|
||||
let command = ["/bin/sh", "-c", "echo \"a && b\""]
|
||||
let command = ["/bin/sh", "-lc", "echo \"a && b\""]
|
||||
let resolutions = ExecCommandResolution.resolveForAllowlist(
|
||||
command: command,
|
||||
rawCommand: "echo \"a && b\"",
|
||||
@@ -288,7 +169,7 @@ struct ExecAllowlistTests {
|
||||
}
|
||||
|
||||
@Test func `resolve for allowlist fails closed on command substitution`() {
|
||||
let command = ["/bin/sh", "-c", "echo $(/usr/bin/touch /tmp/openclaw-allowlist-test-subst)"]
|
||||
let command = ["/bin/sh", "-lc", "echo $(/usr/bin/touch /tmp/openclaw-allowlist-test-subst)"]
|
||||
let resolutions = ExecCommandResolution.resolveForAllowlist(
|
||||
command: command,
|
||||
rawCommand: "echo $(/usr/bin/touch /tmp/openclaw-allowlist-test-subst)",
|
||||
@@ -298,7 +179,7 @@ struct ExecAllowlistTests {
|
||||
}
|
||||
|
||||
@Test func `resolve for allowlist fails closed on quoted command substitution`() {
|
||||
let command = ["/bin/sh", "-c", "echo \"ok $(/usr/bin/touch /tmp/openclaw-allowlist-test-quoted-subst)\""]
|
||||
let command = ["/bin/sh", "-lc", "echo \"ok $(/usr/bin/touch /tmp/openclaw-allowlist-test-quoted-subst)\""]
|
||||
let resolutions = ExecCommandResolution.resolveForAllowlist(
|
||||
command: command,
|
||||
rawCommand: "echo \"ok $(/usr/bin/touch /tmp/openclaw-allowlist-test-quoted-subst)\"",
|
||||
@@ -308,7 +189,7 @@ struct ExecAllowlistTests {
|
||||
}
|
||||
|
||||
@Test func `resolve for allowlist fails closed on line-continued command substitution`() {
|
||||
let command = ["/bin/sh", "-c", "echo $\\\n(/usr/bin/touch /tmp/openclaw-allowlist-test-line-cont-subst)"]
|
||||
let command = ["/bin/sh", "-lc", "echo $\\\n(/usr/bin/touch /tmp/openclaw-allowlist-test-line-cont-subst)"]
|
||||
let resolutions = ExecCommandResolution.resolveForAllowlist(
|
||||
command: command,
|
||||
rawCommand: "echo $\\\n(/usr/bin/touch /tmp/openclaw-allowlist-test-line-cont-subst)",
|
||||
@@ -320,7 +201,7 @@ struct ExecAllowlistTests {
|
||||
@Test func `resolve for allowlist fails closed on chained line-continued command substitution`() {
|
||||
let command = [
|
||||
"/bin/sh",
|
||||
"-c",
|
||||
"-lc",
|
||||
"echo ok && $\\\n(/usr/bin/touch /tmp/openclaw-allowlist-test-chained-line-cont-subst)",
|
||||
]
|
||||
let resolutions = ExecCommandResolution.resolveForAllowlist(
|
||||
@@ -332,7 +213,7 @@ struct ExecAllowlistTests {
|
||||
}
|
||||
|
||||
@Test func `resolve for allowlist fails closed on quoted backticks`() {
|
||||
let command = ["/bin/sh", "-c", "echo \"ok `/usr/bin/id`\""]
|
||||
let command = ["/bin/sh", "-lc", "echo \"ok `/usr/bin/id`\""]
|
||||
let resolutions = ExecCommandResolution.resolveForAllowlist(
|
||||
command: command,
|
||||
rawCommand: "echo \"ok `/usr/bin/id`\"",
|
||||
@@ -345,7 +226,7 @@ struct ExecAllowlistTests {
|
||||
let fixtures = try Self.loadShellParserParityCases()
|
||||
for fixture in fixtures {
|
||||
let resolutions = ExecCommandResolution.resolveForAllowlist(
|
||||
command: ["/bin/sh", "-c", fixture.command],
|
||||
command: ["/bin/sh", "-lc", fixture.command],
|
||||
rawCommand: fixture.command,
|
||||
cwd: nil,
|
||||
env: ["PATH": "/usr/bin:/bin"])
|
||||
@@ -395,7 +276,7 @@ struct ExecAllowlistTests {
|
||||
let command = [
|
||||
"/usr/bin/env",
|
||||
"/bin/sh",
|
||||
"-c",
|
||||
"-lc",
|
||||
"echo allowlisted && /usr/bin/touch /tmp/openclaw-allowlist-test",
|
||||
]
|
||||
let resolutions = ExecCommandResolution.resolveForAllowlist(
|
||||
@@ -409,7 +290,7 @@ struct ExecAllowlistTests {
|
||||
}
|
||||
|
||||
@Test func `resolve for allowlist unwraps env dispatch wrappers inside shell segments`() {
|
||||
let command = ["/bin/sh", "-c", "env /usr/bin/touch /tmp/openclaw-allowlist-test"]
|
||||
let command = ["/bin/sh", "-lc", "env /usr/bin/touch /tmp/openclaw-allowlist-test"]
|
||||
let resolutions = ExecCommandResolution.resolveForAllowlist(
|
||||
command: command,
|
||||
rawCommand: "env /usr/bin/touch /tmp/openclaw-allowlist-test",
|
||||
@@ -421,7 +302,7 @@ struct ExecAllowlistTests {
|
||||
}
|
||||
|
||||
@Test func `resolve for allowlist preserves env assignments inside shell segments`() {
|
||||
let command = ["/bin/sh", "-c", "env FOO=bar /usr/bin/touch /tmp/openclaw-allowlist-test"]
|
||||
let command = ["/bin/sh", "-lc", "env FOO=bar /usr/bin/touch /tmp/openclaw-allowlist-test"]
|
||||
let resolutions = ExecCommandResolution.resolveForAllowlist(
|
||||
command: command,
|
||||
rawCommand: "env FOO=bar /usr/bin/touch /tmp/openclaw-allowlist-test",
|
||||
@@ -445,8 +326,8 @@ struct ExecAllowlistTests {
|
||||
}
|
||||
|
||||
@Test func `approval evaluator resolves shell payload from canonical wrapper text`() async {
|
||||
let command = ["/bin/sh", "-c", "/usr/bin/printf ok"]
|
||||
let rawCommand = "/bin/sh -c \"/usr/bin/printf ok\""
|
||||
let command = ["/bin/sh", "-lc", "/usr/bin/printf ok"]
|
||||
let rawCommand = "/bin/sh -lc \"/usr/bin/printf ok\""
|
||||
let evaluation = await ExecApprovalEvaluator.evaluate(
|
||||
command: command,
|
||||
rawCommand: rawCommand,
|
||||
@@ -469,32 +350,6 @@ struct ExecAllowlistTests {
|
||||
#expect(patterns == ["/usr/bin/printf"])
|
||||
}
|
||||
|
||||
@Test func `allow always patterns fail closed for env modified shell wrappers`() {
|
||||
let patterns = ExecCommandResolution.resolveAllowAlwaysPatterns(
|
||||
command: [
|
||||
"/usr/bin/env",
|
||||
"BASH_ENV=/tmp/payload.sh",
|
||||
"/bin/sh",
|
||||
"-lc",
|
||||
"/usr/bin/printf ok",
|
||||
],
|
||||
cwd: nil,
|
||||
env: ["PATH": "/usr/bin:/bin"],
|
||||
rawCommand: "/usr/bin/printf ok")
|
||||
|
||||
#expect(patterns.isEmpty)
|
||||
}
|
||||
|
||||
@Test func `allow always patterns preserve generated sh lc raw payload binding`() {
|
||||
let patterns = ExecCommandResolution.resolveAllowAlwaysPatterns(
|
||||
command: ["/bin/sh", "-lc", "/usr/bin/printf safe_marker"],
|
||||
cwd: nil,
|
||||
env: ["PATH": "/usr/bin:/bin"],
|
||||
rawCommand: "/usr/bin/printf safe_marker")
|
||||
|
||||
#expect(patterns == ["/usr/bin/printf"])
|
||||
}
|
||||
|
||||
@Test func `match all requires every segment to match`() {
|
||||
let first = ExecCommandResolution(
|
||||
rawExecutable: "echo",
|
||||
|
||||
@@ -85,48 +85,6 @@ struct ExecSystemRunCommandValidatorTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test func `fish attached c command requires canonical raw command binding`() {
|
||||
let command = ["/usr/bin/fish", "-c/tmp/payload.fish", "/usr/bin/printf safe_marker"]
|
||||
let result = ExecSystemRunCommandValidator.resolve(
|
||||
command: command,
|
||||
rawCommand: "/usr/bin/printf safe_marker")
|
||||
|
||||
switch result {
|
||||
case .ok:
|
||||
Issue.record("expected rawCommand mismatch for attached fish command payload")
|
||||
case let .invalid(message):
|
||||
#expect(message.contains("rawCommand does not match command"))
|
||||
}
|
||||
}
|
||||
|
||||
@Test func `startup shell wrappers require canonical raw command binding`() {
|
||||
for command in [
|
||||
["/bin/bash", "-lc", "/usr/bin/printf safe_marker"],
|
||||
["/bin/bash", "--rcfile", "/tmp/payload.sh", "-i", "-c", "/usr/bin/printf safe_marker"],
|
||||
["/bin/bash", "--login", "-c", "/usr/bin/printf safe_marker"],
|
||||
["/usr/bin/fish", "--init-command=/tmp/payload.fish", "-c", "/usr/bin/printf safe_marker"],
|
||||
] {
|
||||
let legacy = ExecSystemRunCommandValidator.resolve(
|
||||
command: command,
|
||||
rawCommand: "/usr/bin/printf safe_marker")
|
||||
switch legacy {
|
||||
case .ok:
|
||||
Issue.record("expected rawCommand mismatch for startup shell wrapper")
|
||||
case let .invalid(message):
|
||||
#expect(message.contains("rawCommand does not match command"))
|
||||
}
|
||||
|
||||
let canonicalRaw = ExecCommandFormatter.displayString(for: command)
|
||||
let canonical = ExecSystemRunCommandValidator.resolve(command: command, rawCommand: canonicalRaw)
|
||||
switch canonical {
|
||||
case let .ok(resolved):
|
||||
#expect(resolved.displayCommand == canonicalRaw)
|
||||
case let .invalid(message):
|
||||
Issue.record("unexpected invalid result for canonical raw command: \(message)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static func loadContractCases() throws -> [SystemRunCommandContractCase] {
|
||||
let fixtureURL = try self.findContractFixtureURL()
|
||||
let data = try Data(contentsOf: fixtureURL)
|
||||
|
||||
@@ -336,118 +336,4 @@ struct OpenClawConfigFileTests {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@Test
|
||||
func `save dict records preserved gateway auth in audit`() async throws {
|
||||
let stateDir = FileManager().temporaryDirectory
|
||||
.appendingPathComponent("openclaw-state-\(UUID().uuidString)", isDirectory: true)
|
||||
let configPath = stateDir.appendingPathComponent("openclaw.json")
|
||||
let auditPath = stateDir.appendingPathComponent("logs/config-audit.jsonl")
|
||||
|
||||
defer { try? FileManager().removeItem(at: stateDir) }
|
||||
|
||||
try await TestIsolation.withEnvValues([
|
||||
"OPENCLAW_STATE_DIR": stateDir.path,
|
||||
"OPENCLAW_CONFIG_PATH": configPath.path,
|
||||
]) {
|
||||
OpenClawConfigFile.saveDict([
|
||||
"gateway": [
|
||||
"mode": "local",
|
||||
"auth": [
|
||||
"mode": "token",
|
||||
"token": "test-token", // pragma: allowlist secret
|
||||
],
|
||||
],
|
||||
])
|
||||
|
||||
let saved = OpenClawConfigFile.saveDict([
|
||||
"gateway": [
|
||||
"mode": "local",
|
||||
],
|
||||
"browser": [
|
||||
"enabled": false,
|
||||
],
|
||||
])
|
||||
|
||||
#expect(saved)
|
||||
let data = try Data(contentsOf: configPath)
|
||||
let root = try JSONSerialization.jsonObject(with: data) as? [String: Any]
|
||||
let gateway = root?["gateway"] as? [String: Any]
|
||||
let auth = gateway?["auth"] as? [String: Any]
|
||||
#expect(gateway?["mode"] as? String == "local")
|
||||
#expect(auth?["mode"] as? String == "token")
|
||||
#expect(auth?["token"] as? String == "test-token") // pragma: allowlist secret
|
||||
#expect((root?["meta"] as? [String: Any]) != nil)
|
||||
|
||||
let rawAudit = try String(contentsOf: auditPath, encoding: .utf8)
|
||||
let last = rawAudit.split(whereSeparator: \.isNewline).map(String.init).last
|
||||
let auditRoot = try JSONSerialization.jsonObject(with: Data((last ?? "{}").utf8)) as? [String: Any]
|
||||
#expect(auditRoot?["result"] as? String == "success")
|
||||
#expect(auditRoot?["preservedGatewayAuth"] as? Bool == true)
|
||||
let suspicious = auditRoot?["suspicious"] as? [String] ?? []
|
||||
#expect(suspicious.contains("gateway-auth-preserved"))
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@Test
|
||||
func `save dict rejects gateway mode removal and keeps previous config`() async throws {
|
||||
let stateDir = FileManager().temporaryDirectory
|
||||
.appendingPathComponent("openclaw-state-\(UUID().uuidString)", isDirectory: true)
|
||||
let configPath = stateDir.appendingPathComponent("openclaw.json")
|
||||
let auditPath = stateDir.appendingPathComponent("logs/config-audit.jsonl")
|
||||
|
||||
defer { try? FileManager().removeItem(at: stateDir) }
|
||||
|
||||
try await TestIsolation.withEnvValues([
|
||||
"OPENCLAW_STATE_DIR": stateDir.path,
|
||||
"OPENCLAW_CONFIG_PATH": configPath.path,
|
||||
]) {
|
||||
OpenClawConfigFile.saveDict([
|
||||
"gateway": [
|
||||
"mode": "local",
|
||||
"auth": [
|
||||
"mode": "token",
|
||||
"token": "test-token", // pragma: allowlist secret
|
||||
],
|
||||
],
|
||||
"browser": [
|
||||
"enabled": true,
|
||||
],
|
||||
])
|
||||
let before = try String(contentsOf: configPath, encoding: .utf8)
|
||||
|
||||
let saved = OpenClawConfigFile.saveDict([
|
||||
"browser": [
|
||||
"enabled": false,
|
||||
],
|
||||
])
|
||||
|
||||
#expect(!saved)
|
||||
let after = try String(contentsOf: configPath, encoding: .utf8)
|
||||
#expect(after == before)
|
||||
|
||||
let rawAudit = try String(contentsOf: auditPath, encoding: .utf8)
|
||||
let lines = rawAudit.split(whereSeparator: \.isNewline).map(String.init)
|
||||
guard let last = lines.last else {
|
||||
Issue.record("Missing rejected config audit line")
|
||||
return
|
||||
}
|
||||
let auditRoot = try JSONSerialization.jsonObject(with: Data(last.utf8)) as? [String: Any]
|
||||
#expect(auditRoot?["result"] as? String == "rejected")
|
||||
let suspicious = auditRoot?["suspicious"] as? [String] ?? []
|
||||
let blocking = auditRoot?["blocking"] as? [String] ?? []
|
||||
#expect(suspicious.contains("gateway-mode-removed"))
|
||||
#expect(blocking.contains("gateway-mode-removed"))
|
||||
if let rejectedPath = auditRoot?["rejectedPath"] as? String {
|
||||
#expect(FileManager().fileExists(atPath: rejectedPath))
|
||||
let attributes = try FileManager().attributesOfItem(atPath: rejectedPath)
|
||||
let mode = attributes[.posixPermissions] as? NSNumber
|
||||
#expect(mode?.intValue == 0o600)
|
||||
} else {
|
||||
Issue.record("Missing rejected payload path")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,14 +165,14 @@ public struct ResponseFrame: Codable, Sendable {
|
||||
public let id: String
|
||||
public let ok: Bool
|
||||
public let payload: AnyCodable?
|
||||
public let error: ErrorShape?
|
||||
public let error: [String: AnyCodable]?
|
||||
|
||||
public init(
|
||||
type: String,
|
||||
id: String,
|
||||
ok: Bool,
|
||||
payload: AnyCodable?,
|
||||
error: ErrorShape?)
|
||||
error: [String: AnyCodable]?)
|
||||
{
|
||||
self.type = type
|
||||
self.id = id
|
||||
@@ -195,14 +195,14 @@ public struct EventFrame: Codable, Sendable {
|
||||
public let event: String
|
||||
public let payload: AnyCodable?
|
||||
public let seq: Int?
|
||||
public let stateversion: StateVersion?
|
||||
public let stateversion: [String: AnyCodable]?
|
||||
|
||||
public init(
|
||||
type: String,
|
||||
event: String,
|
||||
payload: AnyCodable?,
|
||||
seq: Int?,
|
||||
stateversion: StateVersion?)
|
||||
stateversion: [String: AnyCodable]?)
|
||||
{
|
||||
self.type = type
|
||||
self.event = event
|
||||
@@ -2243,9 +2243,6 @@ public struct SessionsUsageParams: Codable, Sendable {
|
||||
public let startdate: String?
|
||||
public let enddate: String?
|
||||
public let mode: AnyCodable?
|
||||
public let range: AnyCodable?
|
||||
public let groupby: AnyCodable?
|
||||
public let includehistorical: Bool?
|
||||
public let utcoffset: String?
|
||||
public let limit: Int?
|
||||
public let includecontextweight: Bool?
|
||||
@@ -2255,9 +2252,6 @@ public struct SessionsUsageParams: Codable, Sendable {
|
||||
startdate: String?,
|
||||
enddate: String?,
|
||||
mode: AnyCodable?,
|
||||
range: AnyCodable?,
|
||||
groupby: AnyCodable?,
|
||||
includehistorical: Bool?,
|
||||
utcoffset: String?,
|
||||
limit: Int?,
|
||||
includecontextweight: Bool?)
|
||||
@@ -2266,9 +2260,6 @@ public struct SessionsUsageParams: Codable, Sendable {
|
||||
self.startdate = startdate
|
||||
self.enddate = enddate
|
||||
self.mode = mode
|
||||
self.range = range
|
||||
self.groupby = groupby
|
||||
self.includehistorical = includehistorical
|
||||
self.utcoffset = utcoffset
|
||||
self.limit = limit
|
||||
self.includecontextweight = includecontextweight
|
||||
@@ -2279,229 +2270,12 @@ public struct SessionsUsageParams: Codable, Sendable {
|
||||
case startdate = "startDate"
|
||||
case enddate = "endDate"
|
||||
case mode
|
||||
case range
|
||||
case groupby = "groupBy"
|
||||
case includehistorical = "includeHistorical"
|
||||
case utcoffset = "utcOffset"
|
||||
case limit
|
||||
case includecontextweight = "includeContextWeight"
|
||||
}
|
||||
}
|
||||
|
||||
public struct TaskSummary: Codable, Sendable {
|
||||
public let id: String
|
||||
public let kind: String?
|
||||
public let runtime: String?
|
||||
public let status: AnyCodable
|
||||
public let title: String?
|
||||
public let agentid: String?
|
||||
public let sessionkey: String?
|
||||
public let childsessionkey: String?
|
||||
public let ownerkey: String?
|
||||
public let runid: String?
|
||||
public let taskid: String?
|
||||
public let flowid: String?
|
||||
public let parenttaskid: String?
|
||||
public let sourceid: String?
|
||||
public let createdat: AnyCodable?
|
||||
public let updatedat: AnyCodable?
|
||||
public let startedat: AnyCodable?
|
||||
public let endedat: AnyCodable?
|
||||
public let progresssummary: String?
|
||||
public let terminalsummary: String?
|
||||
public let error: String?
|
||||
|
||||
public init(
|
||||
id: String,
|
||||
kind: String?,
|
||||
runtime: String?,
|
||||
status: AnyCodable,
|
||||
title: String?,
|
||||
agentid: String?,
|
||||
sessionkey: String?,
|
||||
childsessionkey: String?,
|
||||
ownerkey: String?,
|
||||
runid: String?,
|
||||
taskid: String?,
|
||||
flowid: String?,
|
||||
parenttaskid: String?,
|
||||
sourceid: String?,
|
||||
createdat: AnyCodable?,
|
||||
updatedat: AnyCodable?,
|
||||
startedat: AnyCodable?,
|
||||
endedat: AnyCodable?,
|
||||
progresssummary: String?,
|
||||
terminalsummary: String?,
|
||||
error: String?)
|
||||
{
|
||||
self.id = id
|
||||
self.kind = kind
|
||||
self.runtime = runtime
|
||||
self.status = status
|
||||
self.title = title
|
||||
self.agentid = agentid
|
||||
self.sessionkey = sessionkey
|
||||
self.childsessionkey = childsessionkey
|
||||
self.ownerkey = ownerkey
|
||||
self.runid = runid
|
||||
self.taskid = taskid
|
||||
self.flowid = flowid
|
||||
self.parenttaskid = parenttaskid
|
||||
self.sourceid = sourceid
|
||||
self.createdat = createdat
|
||||
self.updatedat = updatedat
|
||||
self.startedat = startedat
|
||||
self.endedat = endedat
|
||||
self.progresssummary = progresssummary
|
||||
self.terminalsummary = terminalsummary
|
||||
self.error = error
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case kind
|
||||
case runtime
|
||||
case status
|
||||
case title
|
||||
case agentid = "agentId"
|
||||
case sessionkey = "sessionKey"
|
||||
case childsessionkey = "childSessionKey"
|
||||
case ownerkey = "ownerKey"
|
||||
case runid = "runId"
|
||||
case taskid = "taskId"
|
||||
case flowid = "flowId"
|
||||
case parenttaskid = "parentTaskId"
|
||||
case sourceid = "sourceId"
|
||||
case createdat = "createdAt"
|
||||
case updatedat = "updatedAt"
|
||||
case startedat = "startedAt"
|
||||
case endedat = "endedAt"
|
||||
case progresssummary = "progressSummary"
|
||||
case terminalsummary = "terminalSummary"
|
||||
case error
|
||||
}
|
||||
}
|
||||
|
||||
public struct TasksListParams: Codable, Sendable {
|
||||
public let status: AnyCodable?
|
||||
public let agentid: String?
|
||||
public let sessionkey: String?
|
||||
public let limit: Int?
|
||||
public let cursor: String?
|
||||
|
||||
public init(
|
||||
status: AnyCodable?,
|
||||
agentid: String?,
|
||||
sessionkey: String?,
|
||||
limit: Int?,
|
||||
cursor: String?)
|
||||
{
|
||||
self.status = status
|
||||
self.agentid = agentid
|
||||
self.sessionkey = sessionkey
|
||||
self.limit = limit
|
||||
self.cursor = cursor
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case status
|
||||
case agentid = "agentId"
|
||||
case sessionkey = "sessionKey"
|
||||
case limit
|
||||
case cursor
|
||||
}
|
||||
}
|
||||
|
||||
public struct TasksListResult: Codable, Sendable {
|
||||
public let tasks: [TaskSummary]
|
||||
public let nextcursor: String?
|
||||
|
||||
public init(
|
||||
tasks: [TaskSummary],
|
||||
nextcursor: String?)
|
||||
{
|
||||
self.tasks = tasks
|
||||
self.nextcursor = nextcursor
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case tasks
|
||||
case nextcursor = "nextCursor"
|
||||
}
|
||||
}
|
||||
|
||||
public struct TasksGetParams: Codable, Sendable {
|
||||
public let taskid: String
|
||||
|
||||
public init(
|
||||
taskid: String)
|
||||
{
|
||||
self.taskid = taskid
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case taskid = "taskId"
|
||||
}
|
||||
}
|
||||
|
||||
public struct TasksGetResult: Codable, Sendable {
|
||||
public let task: TaskSummary
|
||||
|
||||
public init(
|
||||
task: TaskSummary)
|
||||
{
|
||||
self.task = task
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case task
|
||||
}
|
||||
}
|
||||
|
||||
public struct TasksCancelParams: Codable, Sendable {
|
||||
public let taskid: String
|
||||
public let reason: String?
|
||||
|
||||
public init(
|
||||
taskid: String,
|
||||
reason: String?)
|
||||
{
|
||||
self.taskid = taskid
|
||||
self.reason = reason
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case taskid = "taskId"
|
||||
case reason
|
||||
}
|
||||
}
|
||||
|
||||
public struct TasksCancelResult: Codable, Sendable {
|
||||
public let found: Bool
|
||||
public let cancelled: Bool
|
||||
public let reason: String?
|
||||
public let task: TaskSummary?
|
||||
|
||||
public init(
|
||||
found: Bool,
|
||||
cancelled: Bool,
|
||||
reason: String?,
|
||||
task: TaskSummary?)
|
||||
{
|
||||
self.found = found
|
||||
self.cancelled = cancelled
|
||||
self.reason = reason
|
||||
self.task = task
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case found
|
||||
case cancelled
|
||||
case reason
|
||||
case task
|
||||
}
|
||||
}
|
||||
|
||||
public struct ConfigGetParams: Codable, Sendable {}
|
||||
|
||||
public struct ConfigSetParams: Codable, Sendable {
|
||||
@@ -2778,13 +2552,13 @@ public struct WizardStep: Codable, Sendable {
|
||||
|
||||
public struct WizardNextResult: Codable, Sendable {
|
||||
public let done: Bool
|
||||
public let step: WizardStep?
|
||||
public let step: [String: AnyCodable]?
|
||||
public let status: AnyCodable?
|
||||
public let error: String?
|
||||
|
||||
public init(
|
||||
done: Bool,
|
||||
step: WizardStep?,
|
||||
step: [String: AnyCodable]?,
|
||||
status: AnyCodable?,
|
||||
error: String?)
|
||||
{
|
||||
@@ -2805,14 +2579,14 @@ public struct WizardNextResult: Codable, Sendable {
|
||||
public struct WizardStartResult: Codable, Sendable {
|
||||
public let sessionid: String
|
||||
public let done: Bool
|
||||
public let step: WizardStep?
|
||||
public let step: [String: AnyCodable]?
|
||||
public let status: AnyCodable?
|
||||
public let error: String?
|
||||
|
||||
public init(
|
||||
sessionid: String,
|
||||
done: Bool,
|
||||
step: WizardStep?,
|
||||
step: [String: AnyCodable]?,
|
||||
status: AnyCodable?,
|
||||
error: String?)
|
||||
{
|
||||
@@ -3406,25 +3180,21 @@ public struct TalkSessionSubmitToolResultParams: Codable, Sendable {
|
||||
public let sessionid: String
|
||||
public let callid: String
|
||||
public let result: AnyCodable
|
||||
public let options: [String: AnyCodable]?
|
||||
|
||||
public init(
|
||||
sessionid: String,
|
||||
callid: String,
|
||||
result: AnyCodable,
|
||||
options: [String: AnyCodable]?)
|
||||
result: AnyCodable)
|
||||
{
|
||||
self.sessionid = sessionid
|
||||
self.callid = callid
|
||||
self.result = result
|
||||
self.options = options
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case sessionid = "sessionId"
|
||||
case callid = "callId"
|
||||
case result
|
||||
case options
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4759,7 +4529,7 @@ public struct ToolsInvokeResult: Codable, Sendable {
|
||||
public let requiresapproval: Bool?
|
||||
public let approvalid: String?
|
||||
public let source: AnyCodable?
|
||||
public let error: ToolsInvokeError?
|
||||
public let error: [String: AnyCodable]?
|
||||
|
||||
public init(
|
||||
ok: Bool,
|
||||
@@ -4768,7 +4538,7 @@ public struct ToolsInvokeResult: Codable, Sendable {
|
||||
requiresapproval: Bool?,
|
||||
approvalid: String?,
|
||||
source: AnyCodable?,
|
||||
error: ToolsInvokeError?)
|
||||
error: [String: AnyCodable]?)
|
||||
{
|
||||
self.ok = ok
|
||||
self.toolname = toolname
|
||||
@@ -5362,7 +5132,6 @@ public struct ExecApprovalRequestParams: Codable, Sendable {
|
||||
public let security: AnyCodable?
|
||||
public let ask: AnyCodable?
|
||||
public let warningtext: AnyCodable?
|
||||
public let commandspans: [[String: AnyCodable]]?
|
||||
public let agentid: AnyCodable?
|
||||
public let resolvedpath: AnyCodable?
|
||||
public let sessionkey: AnyCodable?
|
||||
@@ -5385,7 +5154,6 @@ public struct ExecApprovalRequestParams: Codable, Sendable {
|
||||
security: AnyCodable?,
|
||||
ask: AnyCodable?,
|
||||
warningtext: AnyCodable?,
|
||||
commandspans: [[String: AnyCodable]]?,
|
||||
agentid: AnyCodable?,
|
||||
resolvedpath: AnyCodable?,
|
||||
sessionkey: AnyCodable?,
|
||||
@@ -5407,7 +5175,6 @@ public struct ExecApprovalRequestParams: Codable, Sendable {
|
||||
self.security = security
|
||||
self.ask = ask
|
||||
self.warningtext = warningtext
|
||||
self.commandspans = commandspans
|
||||
self.agentid = agentid
|
||||
self.resolvedpath = resolvedpath
|
||||
self.sessionkey = sessionkey
|
||||
@@ -5431,7 +5198,6 @@ public struct ExecApprovalRequestParams: Codable, Sendable {
|
||||
case security
|
||||
case ask
|
||||
case warningtext = "warningText"
|
||||
case commandspans = "commandSpans"
|
||||
case agentid = "agentId"
|
||||
case resolvedpath = "resolvedPath"
|
||||
case sessionkey = "sessionKey"
|
||||
@@ -5823,7 +5589,6 @@ public struct ChatSendParams: Codable, Sendable {
|
||||
public let sessionid: String?
|
||||
public let message: String
|
||||
public let thinking: String?
|
||||
public let fastmode: Bool?
|
||||
public let deliver: Bool?
|
||||
public let originatingchannel: String?
|
||||
public let originatingto: String?
|
||||
@@ -5840,7 +5605,6 @@ public struct ChatSendParams: Codable, Sendable {
|
||||
sessionid: String?,
|
||||
message: String,
|
||||
thinking: String?,
|
||||
fastmode: Bool?,
|
||||
deliver: Bool?,
|
||||
originatingchannel: String?,
|
||||
originatingto: String?,
|
||||
@@ -5856,7 +5620,6 @@ public struct ChatSendParams: Codable, Sendable {
|
||||
self.sessionid = sessionid
|
||||
self.message = message
|
||||
self.thinking = thinking
|
||||
self.fastmode = fastmode
|
||||
self.deliver = deliver
|
||||
self.originatingchannel = originatingchannel
|
||||
self.originatingto = originatingto
|
||||
@@ -5874,7 +5637,6 @@ public struct ChatSendParams: Codable, Sendable {
|
||||
case sessionid = "sessionId"
|
||||
case message
|
||||
case thinking
|
||||
case fastmode = "fastMode"
|
||||
case deliver
|
||||
case originatingchannel = "originatingChannel"
|
||||
case originatingto = "originatingTo"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
a1523d01a78c449ee064761ccc065088584237879925d61a670d246aeb078b3f config-baseline.json
|
||||
0f1af8c72fe0c0ba9219e35b4547b8884d0556fc6e72b24ab9dd8fbcc2ff76a8 config-baseline.core.json
|
||||
60040c66e8cc488b85a10250936354ea1411015dab17af1b52b533c54a83a722 config-baseline.channel.json
|
||||
1da42cb10427fb08510f29732493d24851ab915a424f91556569febdd450d9c3 config-baseline.plugin.json
|
||||
7238265b921affbb481198f603293c9b1c988025713c55ee19fdbf132a8339ab config-baseline.json
|
||||
97579293de31bc607194bce3e22c16d140c08ab9e6f1e38298f3ce47fbc9d68b config-baseline.core.json
|
||||
463c45a79d02598184caccbc6f316692df962fe6b0e84d1a3e3cc1809f862b15 config-baseline.channel.json
|
||||
b6d36d17e554a2ec5a1a6c6d32107a9a1113c274a700100962d97b6afbdafb25 config-baseline.plugin.json
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
1aa92e012261d21474fde2aea8b1724bea369b5bc730e9fb943ff82de73f38de plugin-sdk-api-baseline.json
|
||||
f076fb0cc1d09b5d50502741428e88a52725d73db950745801ae4fe782d709e2 plugin-sdk-api-baseline.jsonl
|
||||
28e280d21693216c99cfa8da553589b41741d37c0ada956e316ee01d3d6c202c plugin-sdk-api-baseline.json
|
||||
633dae33da97f6a073c5561709c57d5c0b7ff67af0512d0261f05455c24b38de plugin-sdk-api-baseline.jsonl
|
||||
|
||||
@@ -3,26 +3,6 @@
|
||||
"source": "OpenClaw",
|
||||
"target": "OpenClaw"
|
||||
},
|
||||
{
|
||||
"source": "iMessage",
|
||||
"target": "iMessage"
|
||||
},
|
||||
{
|
||||
"source": "Coming from BlueBubbles",
|
||||
"target": "Coming from BlueBubbles"
|
||||
},
|
||||
{
|
||||
"source": "BlueBubbles",
|
||||
"target": "BlueBubbles"
|
||||
},
|
||||
{
|
||||
"source": "Pairing",
|
||||
"target": "配对"
|
||||
},
|
||||
{
|
||||
"source": "Channel Routing",
|
||||
"target": "频道路由"
|
||||
},
|
||||
{
|
||||
"source": "ClawHub",
|
||||
"target": "ClawHub"
|
||||
@@ -611,18 +591,6 @@
|
||||
"source": "Plugin path ownership",
|
||||
"target": "插件路径所有权"
|
||||
},
|
||||
{
|
||||
"source": "Path",
|
||||
"target": "路径"
|
||||
},
|
||||
{
|
||||
"source": "OC Path plugin",
|
||||
"target": "OC Path 插件"
|
||||
},
|
||||
{
|
||||
"source": "CLI reference",
|
||||
"target": "CLI 参考"
|
||||
},
|
||||
{
|
||||
"source": "Docker permissions",
|
||||
"target": "Docker 权限"
|
||||
@@ -767,10 +735,6 @@
|
||||
"source": "Matrix QA",
|
||||
"target": "Matrix QA"
|
||||
},
|
||||
{
|
||||
"source": "Matrix presentation metadata",
|
||||
"target": "Matrix 呈现元数据"
|
||||
},
|
||||
{
|
||||
"source": "QA overview",
|
||||
"target": "QA overview"
|
||||
|
||||
@@ -203,6 +203,7 @@
|
||||
"zh-CN/tools/slash-commands",
|
||||
"zh-CN/tools/skills",
|
||||
"zh-CN/tools/skills-config",
|
||||
"zh-CN/tools/clawhub",
|
||||
"zh-CN/tools/plugin"
|
||||
]
|
||||
},
|
||||
|
||||
@@ -16,14 +16,6 @@ This directory owns docs authoring, Mintlify link rules, and docs i18n policy.
|
||||
- For docs, UI copy, and picker lists, order services/providers alphabetically unless the section is explicitly describing runtime order or auto-detection order.
|
||||
- Keep bundled plugin naming consistent with the repo-wide plugin terminology rules in the root `AGENTS.md`.
|
||||
|
||||
## Internal Docs
|
||||
|
||||
- Long-lived private operator docs belong in `~/Projects/manager/docs/`.
|
||||
- Repo-local internal scratch/mirror docs may live under ignored `docs/internal/`.
|
||||
- Never add `docs/internal/**` pages to `docs/docs.json` navigation or link them from public docs.
|
||||
- `scripts/docs-sync-publish.mjs` excludes and prunes `docs/internal/**` from the public `openclaw/docs` publish repo if a page is force-added later.
|
||||
- Internal docs may mention repo paths, private app names, 1Password item names, and runbooks, but never include secret values.
|
||||
|
||||
## Docs i18n
|
||||
|
||||
- Foreign-language docs are not maintained in this repo. The generated publish output lives in the separate `openclaw/docs` repo (often cloned locally as `../openclaw-docs`).
|
||||
|
||||
@@ -90,7 +90,7 @@ Recommended data provenance fields for every collected item:
|
||||
|
||||
Have the workflow reject or mark stale items before summarization. The LLM step should receive only structured JSON and should be asked to preserve `sourceUrl`, `retrievedAt`, and `asOf` in its output. Use [LLM Task](/tools/llm-task) when you need a schema-validated model step inside the workflow.
|
||||
|
||||
For reusable team or community workflows, package the CLI, `.lobster` files, and any setup notes as a skill or plugin and publish it through [ClawHub](/clawhub). Keep workflow-specific guardrails in that package unless the plugin API is missing a needed generic capability.
|
||||
For reusable team or community workflows, package the CLI, `.lobster` files, and any setup notes as a skill or plugin and publish it through [ClawHub](/tools/clawhub). Keep workflow-specific guardrails in that package unless the plugin API is missing a needed generic capability.
|
||||
|
||||
## Sync modes
|
||||
|
||||
|
||||
@@ -662,7 +662,7 @@ Default slash command settings:
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Live stream preview">
|
||||
OpenClaw can stream draft replies by sending a temporary message and editing it as text arrives. `channels.discord.streaming` takes `off` | `partial` | `block` | `progress` (default). `progress` keeps one editable status draft and updates it with tool progress until final delivery; the shared starter label is a rolling line, so it scrolls away like the rest once enough work appears. `streamMode` is a legacy runtime alias. Run `openclaw doctor --fix` to rewrite persisted config to the canonical key.
|
||||
OpenClaw can stream draft replies by sending a temporary message and editing it as text arrives. `channels.discord.streaming` takes `off` | `partial` | `block` | `progress` (default). `progress` keeps one editable status draft and updates it with tool progress until final delivery; `streamMode` is a legacy runtime alias. Run `openclaw doctor --fix` to rewrite persisted config to the canonical key.
|
||||
|
||||
Set `channels.discord.streaming.mode` to `off` to disable Discord preview edits. If Discord block streaming is explicitly enabled, OpenClaw skips the preview stream to avoid double-streaming.
|
||||
|
||||
@@ -687,7 +687,6 @@ Default slash command settings:
|
||||
- `block` emits draft-sized chunks (use `draftChunk` to tune size and breakpoints, clamped to `textChunkLimit`).
|
||||
- Media, error, and explicit-reply finals cancel pending preview edits.
|
||||
- `streaming.preview.toolProgress` (default `true`) controls whether tool/progress updates reuse the preview message.
|
||||
- Tool/progress rows render as compact emoji + title + detail when available, for example `🛠️ Bash: run tests` or `🔎 Web Search: for "query"`.
|
||||
- `streaming.preview.commandText` / `streaming.progress.commandText` controls command/exec detail in compact progress lines: `raw` (default) or `status` (tool label only).
|
||||
|
||||
Hide raw command/exec text while keeping compact progress lines:
|
||||
@@ -1172,7 +1171,7 @@ Auto-join example:
|
||||
discord: {
|
||||
voice: {
|
||||
enabled: true,
|
||||
model: "openai-codex/gpt-5.5",
|
||||
model: "openai/gpt-5.4-mini",
|
||||
autoJoin: [
|
||||
{
|
||||
guildId: "123456789012345678",
|
||||
@@ -1183,10 +1182,9 @@ Auto-join example:
|
||||
decryptionFailureTolerance: 24,
|
||||
connectTimeoutMs: 30000,
|
||||
reconnectGraceMs: 15000,
|
||||
realtime: {
|
||||
tts: {
|
||||
provider: "openai",
|
||||
model: "gpt-realtime-2",
|
||||
voice: "cedar",
|
||||
openai: { voice: "onyx" },
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -1196,27 +1194,18 @@ Auto-join example:
|
||||
|
||||
Notes:
|
||||
|
||||
- `voice.tts` overrides `messages.tts` for `stt-tts` voice playback only. Realtime modes use `voice.realtime.voice`.
|
||||
- `voice.mode` controls the conversation path. The default is `agent-proxy`: a realtime voice front end handles turn timing, interruption, and playback, delegates substantive work to the routed OpenClaw agent through `openclaw_agent_consult`, and treats the result like a typed Discord prompt from that speaker. `stt-tts` keeps the older batch STT plus TTS flow. `bidi` lets the realtime model converse directly while exposing `openclaw_agent_consult` for the OpenClaw brain.
|
||||
- `voice.agentSession` controls which OpenClaw conversation receives voice turns. Leave it unset for the voice channel's own session, or set `{ mode: "target", target: "channel:<text-channel-id>" }` to make the voice channel act as the microphone/speaker extension of an existing Discord text channel session such as `#maintainers`.
|
||||
- `voice.model` overrides the OpenClaw agent brain for Discord voice responses and realtime consults. Leave it unset to inherit the routed agent model. It is separate from `voice.realtime.model`.
|
||||
- `agent-proxy` routes speech through `discord-voice`, which preserves normal owner/tool authorization for the speaker and target session but hides the agent `tts` tool because Discord voice owns playback. By default, `agent-proxy` gives the consult full owner-equivalent tool access for owner speakers (`voice.realtime.toolPolicy: "owner"`) and strongly prefers consulting the OpenClaw agent before substantive answers (`voice.realtime.consultPolicy: "always"`).
|
||||
- In `stt-tts` mode, STT uses `tools.media.audio`; `voice.model` does not affect transcription.
|
||||
- In realtime modes, `voice.realtime.provider`, `voice.realtime.model`, and `voice.realtime.voice` configure the realtime audio session. For OpenAI Realtime 2 plus the Codex brain, use `voice.realtime.model: "gpt-realtime-2"` and `voice.model: "openai-codex/gpt-5.5"`.
|
||||
- `voice.realtime.bargeIn` controls whether Discord speaker-start events interrupt active realtime playback. If unset, it follows the realtime provider's input-audio interruption setting.
|
||||
- `voice.realtime.minBargeInAudioEndMs` controls the minimum assistant playback duration before an OpenAI realtime barge-in truncates audio. Default: `250`. Set `0` for immediate interruption in low-echo rooms, or raise it for echo-heavy speaker setups.
|
||||
- For an OpenAI voice on Discord playback, set `voice.tts.provider: "openai"` and choose a Text-to-speech voice under `voice.tts.openai.voice` or `voice.tts.providers.openai.voice`. `cedar` is a good masculine-sounding choice on the current OpenAI TTS model.
|
||||
- `voice.tts` overrides `messages.tts` for voice playback only.
|
||||
- `voice.model` overrides the LLM used for Discord voice channel responses only. Leave it unset to inherit the routed agent model.
|
||||
- STT uses `tools.media.audio`; `voice.model` does not affect transcription.
|
||||
- Per-channel Discord `systemPrompt` overrides apply to voice transcript turns for that voice channel.
|
||||
- Voice transcript turns derive owner status from Discord `allowFrom` (or `dm.allowFrom`); non-owner speakers cannot access owner-only tools (for example `gateway` and `cron`).
|
||||
- Discord voice is opt-in for text-only configs; set `channels.discord.voice.enabled=true` (or keep an existing `channels.discord.voice` block) to enable `/vc` commands, the voice runtime, and the `GuildVoiceStates` gateway intent.
|
||||
- `channels.discord.intents.voiceStates` can explicitly override voice-state intent subscription. Leave it unset for the intent to follow effective voice enablement.
|
||||
- If `voice.autoJoin` has multiple entries for the same guild, OpenClaw joins the last configured channel for that guild.
|
||||
- `voice.daveEncryption` and `voice.decryptionFailureTolerance` pass through to `@discordjs/voice` join options.
|
||||
- `@discordjs/voice` defaults are `daveEncryption=true` and `decryptionFailureTolerance=24` if unset.
|
||||
- `voice.connectTimeoutMs` controls the initial `@discordjs/voice` Ready wait for `/vc join` and auto-join attempts. Default: `30000`.
|
||||
- `voice.reconnectGraceMs` controls how long OpenClaw waits for a disconnected voice session to begin reconnecting before destroying it. Default: `15000`.
|
||||
- In `stt-tts` mode, voice playback does not stop just because another user starts speaking. To avoid feedback loops, OpenClaw ignores new voice capture while TTS is playing; speak after playback finishes for the next turn. Realtime modes forward speaker starts as barge-in signals to the realtime provider.
|
||||
- In realtime modes, echo from speakers into an open mic can look like barge-in and interrupt playback. For echo-heavy Discord rooms, set `voice.realtime.providers.openai.interruptResponseOnInputAudio: false` to keep OpenAI from auto-interrupting on input audio. Add `voice.realtime.bargeIn: true` if you still want Discord speaker-start events to interrupt active playback. The OpenAI realtime bridge ignores playback truncations shorter than `voice.realtime.minBargeInAudioEndMs` as likely echo/noise and logs them as skipped instead of clearing Discord playback.
|
||||
- Voice playback does not stop just because another user starts speaking. To avoid feedback loops, OpenClaw ignores new voice capture while TTS is playing; speak after playback finishes for the next turn.
|
||||
- `voice.captureSilenceGraceMs` controls how long OpenClaw waits after Discord reports a speaker has stopped before finalizing that audio segment for STT. Default: `2500`; raise this if Discord splits normal pauses into choppy partial transcripts.
|
||||
- When ElevenLabs is the selected TTS provider, Discord voice playback uses streaming TTS and starts from the provider response stream. Providers without streaming support fall back to the synthesized temp-file path.
|
||||
- OpenClaw also watches receive decrypt failures and auto-recovers by leaving/rejoining the voice channel after repeated failures in a short window.
|
||||
@@ -1224,7 +1213,7 @@ Notes:
|
||||
- `The operation was aborted` receive events are expected when OpenClaw finalizes a captured speaker segment; they are verbose diagnostics, not warnings.
|
||||
- Verbose Discord voice logs include a bounded one-line STT transcript preview for each accepted speaker segment, so debugging shows both the user side and the agent reply side without dumping unbounded transcript text.
|
||||
|
||||
STT plus TTS pipeline:
|
||||
Voice channel pipeline:
|
||||
|
||||
- Discord PCM capture is converted to a WAV temp file.
|
||||
- `tools.media.audio` handles STT, for example `openai/gpt-4o-mini-transcribe`.
|
||||
@@ -1232,150 +1221,7 @@ STT plus TTS pipeline:
|
||||
- `voice.model`, when set, overrides only the response LLM for this voice-channel turn.
|
||||
- `voice.tts` is merged over `messages.tts`; streaming-capable providers feed the player directly, otherwise the resulting audio file is played in the joined channel.
|
||||
|
||||
Default agent-proxy voice-channel session example:
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
discord: {
|
||||
voice: {
|
||||
enabled: true,
|
||||
model: "openai-codex/gpt-5.5",
|
||||
realtime: {
|
||||
provider: "openai",
|
||||
model: "gpt-realtime-2",
|
||||
voice: "cedar",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
With no `voice.agentSession` block, each voice channel gets its own routed OpenClaw session. For example, `/vc join channel:234567890123456789` talks to the session for that Discord voice channel. The realtime model is only the voice front end; substantive requests are handed to the configured OpenClaw agent. If the realtime model produces a final transcript without calling the consult tool, OpenClaw forces the consult as a fallback so the default still behaves like talking to the agent.
|
||||
|
||||
Legacy STT plus TTS example:
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
discord: {
|
||||
voice: {
|
||||
enabled: true,
|
||||
mode: "stt-tts",
|
||||
model: "openai/gpt-5.4-mini",
|
||||
tts: {
|
||||
provider: "openai",
|
||||
openai: {
|
||||
model: "gpt-4o-mini-tts",
|
||||
voice: "cedar",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Realtime bidi example:
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
discord: {
|
||||
voice: {
|
||||
enabled: true,
|
||||
mode: "bidi",
|
||||
model: "openai-codex/gpt-5.5",
|
||||
realtime: {
|
||||
provider: "openai",
|
||||
model: "gpt-realtime-2",
|
||||
voice: "cedar",
|
||||
toolPolicy: "safe-read-only",
|
||||
consultPolicy: "always",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Voice as an extension of an existing Discord channel session:
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
discord: {
|
||||
voice: {
|
||||
enabled: true,
|
||||
mode: "agent-proxy",
|
||||
model: "openai-codex/gpt-5.5",
|
||||
agentSession: {
|
||||
mode: "target",
|
||||
target: "channel:123456789012345678",
|
||||
},
|
||||
realtime: {
|
||||
provider: "openai",
|
||||
model: "gpt-realtime-2",
|
||||
voice: "cedar",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
In `agent-proxy` mode the bot joins the configured voice channel, but OpenClaw agent turns use the target channel's normal routed session and agent. The realtime voice session speaks the returned result back into the voice channel. The supervisor agent can still use normal message tools according to its tool policy, including sending a separate Discord message if that is the right action.
|
||||
|
||||
Useful target forms:
|
||||
|
||||
- `target: "channel:123456789012345678"` routes through a Discord text channel session.
|
||||
- `target: "123456789012345678"` is treated as a channel target.
|
||||
- `target: "dm:123456789012345678"` or `target: "user:123456789012345678"` routes through that direct-message session.
|
||||
|
||||
Echo-heavy OpenAI Realtime example:
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
discord: {
|
||||
voice: {
|
||||
enabled: true,
|
||||
mode: "bidi",
|
||||
model: "openai-codex/gpt-5.5",
|
||||
realtime: {
|
||||
provider: "openai",
|
||||
model: "gpt-realtime-2",
|
||||
voice: "cedar",
|
||||
bargeIn: true,
|
||||
minBargeInAudioEndMs: 500,
|
||||
consultPolicy: "always",
|
||||
providers: {
|
||||
openai: {
|
||||
interruptResponseOnInputAudio: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Use this when the model hears its own Discord playback through an open mic, but you still want to interrupt it by speaking. OpenClaw keeps OpenAI from auto-interrupting on raw input audio, while `bargeIn: true` lets Discord speaker-start events and already-active speaker audio cancel active realtime responses before the next captured turn reaches OpenAI. Very early barge-in signals with `audioEndMs` below `minBargeInAudioEndMs` are treated as likely echo/noise and ignored so the model does not cut off at the first playback frame.
|
||||
|
||||
Expected voice logs:
|
||||
|
||||
- On join: `discord voice: joining ... voiceSession=... supervisorSession=... agentSessionMode=... voiceModel=... realtimeModel=...`
|
||||
- On realtime start: `discord voice: realtime bridge starting ... interruptResponse=false bargeIn=true minBargeInAudioEndMs=...`
|
||||
- On realtime consult: `discord voice: realtime consult requested ... voiceSession=... supervisorSession=... question=...`
|
||||
- On agent answer: `discord voice: agent turn answer ...`
|
||||
- On same-speaker interruption: `discord voice: realtime barge-in from active speaker audio ...`
|
||||
- On realtime interruption: `discord voice: realtime model interrupt requested client:response.cancel reason=barge-in`, followed by either `discord voice: realtime model audio truncated client:conversation.item.truncate reason=barge-in audioEndMs=...` or `discord voice: realtime model interrupt confirmed server:response.done status=cancelled ...`
|
||||
- On ignored echo/noise: `discord voice: realtime model interrupt ignored client:conversation.item.truncate.skipped reason=barge-in audioEndMs=0 minAudioEndMs=250`
|
||||
- On disabled barge-in: `discord voice: realtime capture ignored during playback (barge-in disabled) ...`
|
||||
|
||||
Credentials are resolved per component: LLM route auth for `voice.model`, STT auth for `tools.media.audio`, TTS auth for `messages.tts`/`voice.tts`, and realtime provider auth for `voice.realtime.providers` or the provider's normal auth config.
|
||||
Credentials are resolved per component: LLM route auth for `voice.model`, STT auth for `tools.media.audio`, and TTS auth for `messages.tts`/`voice.tts`.
|
||||
|
||||
### Voice messages
|
||||
|
||||
|
||||
@@ -1,242 +0,0 @@
|
||||
---
|
||||
summary: "Migrate old BlueBubbles configs to the bundled iMessage plugin without losing pairing, allowlists, or group bindings."
|
||||
read_when:
|
||||
- Planning a move from BlueBubbles to the bundled iMessage plugin
|
||||
- Translating BlueBubbles config keys to iMessage equivalents
|
||||
- Verifying imsg before enabling the iMessage plugin
|
||||
title: "Coming from BlueBubbles"
|
||||
---
|
||||
|
||||
The bundled `imessage` plugin now reaches the same private API surface as BlueBubbles (`react`, `edit`, `unsend`, `reply`, `sendWithEffect`, group management, attachments) by driving [`steipete/imsg`](https://github.com/steipete/imsg) over JSON-RPC. If you already run a Mac with `imsg` installed, you can drop the BlueBubbles server and let the plugin talk to Messages.app directly.
|
||||
|
||||
BlueBubbles support was removed. OpenClaw supports iMessage through `imsg` only. This guide is for migrating old `channels.bluebubbles` configs to `channels.imessage`; there is no other supported migration path.
|
||||
|
||||
## When this migration makes sense
|
||||
|
||||
- You already run `imsg` on the same Mac (or one reachable over SSH) where Messages.app is signed in.
|
||||
- You want one fewer moving part — no separate BlueBubbles server, no REST endpoint to authenticate, no webhook plumbing. Single CLI binary instead of a server + client app + helper.
|
||||
- You are on a [supported macOS / `imsg` build](/channels/imessage#requirements-and-permissions-macos) where the private API probe reports `available: true`.
|
||||
|
||||
## What imsg does
|
||||
|
||||
`imsg` is a local macOS CLI for Messages. OpenClaw starts `imsg rpc` as a child process and talks JSON-RPC over stdin/stdout. There is no HTTP server, webhook URL, background daemon, launch agent, or port to expose.
|
||||
|
||||
- Reads come from `~/Library/Messages/chat.db` using a read-only SQLite handle.
|
||||
- Live inbound messages come from `imsg watch` / `watch.subscribe`, which follows `chat.db` filesystem events with a polling fallback.
|
||||
- Sends use Messages.app automation for normal text and file sends.
|
||||
- Advanced actions use `imsg launch` to inject the `imsg` helper into Messages.app. That is what unlocks read receipts, typing indicators, rich sends, edit, unsend, threaded reply, tapbacks, and group management.
|
||||
- Linux builds can inspect a copied `chat.db`, but cannot send, watch the live Mac database, or drive Messages.app. For OpenClaw iMessage, run `imsg` on the signed-in Mac or through an SSH wrapper to that Mac.
|
||||
|
||||
## Before you start
|
||||
|
||||
1. Install `imsg` on the Mac that runs Messages.app:
|
||||
|
||||
```bash
|
||||
brew install steipete/tap/imsg
|
||||
imsg --version
|
||||
imsg chats --limit 3
|
||||
```
|
||||
|
||||
If `imsg chats` fails with `unable to open database file`, empty output, or `authorization denied`, grant Full Disk Access to the terminal, editor, Node process, Gateway service, or SSH parent process that launches `imsg`, then reopen that parent process.
|
||||
|
||||
2. Verify the read, watch, send, and RPC surfaces before changing OpenClaw config:
|
||||
|
||||
```bash
|
||||
imsg chats --limit 10 --json | jq -s
|
||||
imsg history --chat-id 42 --limit 10 --attachments --json | jq -s
|
||||
imsg watch --chat-id 42 --reactions --json
|
||||
imsg send --chat-id 42 --text "OpenClaw imsg test"
|
||||
imsg rpc --help
|
||||
```
|
||||
|
||||
Replace `42` with a real chat id from `imsg chats`. Sending requires Automation permission for Messages.app. If OpenClaw will run through SSH, run these commands through the same SSH wrapper or user context that OpenClaw will use.
|
||||
|
||||
3. Enable the private API bridge when you need advanced actions:
|
||||
|
||||
```bash
|
||||
imsg launch
|
||||
imsg status --json
|
||||
```
|
||||
|
||||
`imsg launch` requires SIP to be disabled. Basic send, history, and watch work without `imsg launch`; advanced actions do not.
|
||||
|
||||
4. Verify the bridge through OpenClaw:
|
||||
|
||||
```bash
|
||||
openclaw channels status --probe
|
||||
```
|
||||
|
||||
You want `imessage.privateApi.available: true`. If it reports `false`, fix that first — see [Capability detection](/channels/imessage#private-api-actions).
|
||||
|
||||
5. Snapshot your config:
|
||||
|
||||
```bash
|
||||
cp ~/.openclaw/openclaw.json5 ~/.openclaw/openclaw.json5.bak
|
||||
```
|
||||
|
||||
## Config translation
|
||||
|
||||
iMessage and BlueBubbles share a lot of channel-level config. The keys that change are mostly transport (REST server vs local CLI). Behavior keys (`dmPolicy`, `groupPolicy`, `allowFrom`, etc.) keep the same meaning.
|
||||
|
||||
| BlueBubbles | bundled iMessage | Notes |
|
||||
| ---------------------------------------------------------- | ----------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `channels.bluebubbles.enabled` | `channels.imessage.enabled` | Same semantics. |
|
||||
| `channels.bluebubbles.serverUrl` | _(removed)_ | No REST server — the plugin spawns `imsg rpc` over stdio. |
|
||||
| `channels.bluebubbles.password` | _(removed)_ | No webhook authentication needed. |
|
||||
| _(implicit)_ | `channels.imessage.cliPath` | Path to `imsg` (default `imsg`); use a wrapper script for SSH. |
|
||||
| _(implicit)_ | `channels.imessage.dbPath` | Optional Messages.app `chat.db` override; auto-detected when omitted. |
|
||||
| _(implicit)_ | `channels.imessage.remoteHost` | `host` or `user@host` — only needed when `cliPath` is an SSH wrapper and you want SCP attachment fetches. |
|
||||
| `channels.bluebubbles.dmPolicy` | `channels.imessage.dmPolicy` | Same values (`pairing` / `allowlist` / `open` / `disabled`). |
|
||||
| `channels.bluebubbles.allowFrom` | `channels.imessage.allowFrom` | Pairing approvals carry over by handle, not by token. |
|
||||
| `channels.bluebubbles.groupPolicy` | `channels.imessage.groupPolicy` | Same values (`allowlist` / `open` / `disabled`). |
|
||||
| `channels.bluebubbles.groupAllowFrom` | `channels.imessage.groupAllowFrom` | Same. |
|
||||
| `channels.bluebubbles.groups` | `channels.imessage.groups` | **Copy this verbatim, including any `groups: { "*": { ... } }` wildcard entry.** Per-group `requireMention`, `tools`, `toolsBySender` carry over. With `groupPolicy: "allowlist"`, an empty or missing `groups` block silently drops every group message — see "Group registry footgun" below. |
|
||||
| `channels.bluebubbles.sendReadReceipts` | `channels.imessage.sendReadReceipts` | Default `true`. With the bundled plugin this only fires when the private API probe is up. |
|
||||
| `channels.bluebubbles.includeAttachments` | `channels.imessage.includeAttachments` | Same shape, **same off-by-default**. If you had attachments flowing on BlueBubbles you must re-set this explicitly on the iMessage block — it does not carry over implicitly, and inbound photos/media will be silently dropped with no `Inbound message` log line until you do. |
|
||||
| `channels.bluebubbles.attachmentRoots` | `channels.imessage.attachmentRoots` | Local roots; same wildcard rules. |
|
||||
| _(N/A)_ | `channels.imessage.remoteAttachmentRoots` | Only used when `remoteHost` is set for SCP fetches. |
|
||||
| `channels.bluebubbles.mediaMaxMb` | `channels.imessage.mediaMaxMb` | Default 16 MB on iMessage (BlueBubbles default was 8 MB). Set explicitly if you want to keep the lower cap. |
|
||||
| `channels.bluebubbles.textChunkLimit` | `channels.imessage.textChunkLimit` | Default 4000 on both. |
|
||||
| `channels.bluebubbles.coalesceSameSenderDms` | `channels.imessage.coalesceSameSenderDms` | Same opt-in. DM-only — group chats keep instant per-message dispatch on both channels. Widens the default inbound debounce to 2500 ms when enabled without an explicit `messages.inbound.byChannel.imessage`. See [iMessage docs § Coalescing split-send DMs](/channels/imessage#coalescing-split-send-dms-command--url-in-one-composition). |
|
||||
| `channels.bluebubbles.enrichGroupParticipantsFromContacts` | _(N/A)_ | iMessage already reads sender display names from `chat.db`. |
|
||||
| `channels.bluebubbles.actions.*` | `channels.imessage.actions.*` | Per-action toggles: `reactions`, `edit`, `unsend`, `reply`, `sendWithEffect`, `renameGroup`, `setGroupIcon`, `addParticipant`, `removeParticipant`, `leaveGroup`, `sendAttachment`. |
|
||||
|
||||
Multi-account configs (`channels.bluebubbles.accounts.*`) translate one-to-one to `channels.imessage.accounts.*`.
|
||||
|
||||
## Group registry footgun
|
||||
|
||||
The bundled iMessage plugin runs **two** separate group allowlist gates back-to-back. Both must pass for a group message to reach the agent:
|
||||
|
||||
1. **Sender / chat-target allowlist** (`channels.imessage.groupAllowFrom`) — checked by `isAllowedIMessageSender`. Matches inbound messages by sender handle, `chat_guid`, `chat_identifier`, or `chat_id`. Same shape as BlueBubbles.
|
||||
2. **Group registry** (`channels.imessage.groups`) — checked by `resolveChannelGroupPolicy` from `inbound-processing.ts:199`. With `groupPolicy: "allowlist"`, this gate requires either:
|
||||
- a `groups: { "*": { ... } }` wildcard entry (sets `allowAll = true`), or
|
||||
- an explicit per-`chat_id` entry under `groups`.
|
||||
|
||||
If gate 1 passes but gate 2 fails, the message is dropped. The plugin emits two `warn`-level signals so this is no longer silent at default log level:
|
||||
|
||||
- A one-time startup `warn` per account when `groupPolicy: "allowlist"` is set but `channels.imessage.groups` is empty (no `"*"` wildcard, no per-`chat_id` entries) — fired before any messages land.
|
||||
- A one-time per-`chat_id` `warn` the first time a specific group is dropped at runtime, naming the chat_id and the exact key to add to `groups` to allow it.
|
||||
|
||||
DMs continue to work because they take a different code path.
|
||||
|
||||
This is the most common BlueBubbles → bundled-iMessage migration failure mode: operators copy `groupAllowFrom` and `groupPolicy` but skip the `groups` block, because BlueBubbles' `groups: { "*": { "requireMention": true } }` looks like an unrelated mention setting. It's actually load-bearing for the registry gate.
|
||||
|
||||
The minimum config to keep group messages flowing after `groupPolicy: "allowlist"`:
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
imessage: {
|
||||
groupPolicy: "allowlist",
|
||||
groupAllowFrom: ["+15555550123", "chat_guid:any;-;..."],
|
||||
groups: {
|
||||
"*": { requireMention: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
`requireMention: true` under `*` is harmless when no mention patterns are configured: the runtime sets `canDetectMention = false` and short-circuits the mention drop at `inbound-processing.ts:512`. With mention patterns configured (`agents.list[].groupChat.mentionPatterns`), it works as expected.
|
||||
|
||||
If the gateway logs `imessage: dropping group message from chat_id=<id>` or the startup line `imessage: groupPolicy="allowlist" but channels.imessage.groups is empty`, gate 2 is dropping — add the `groups` block.
|
||||
|
||||
## Step-by-step
|
||||
|
||||
1. Add an iMessage block alongside the existing BlueBubbles block. Keep the old block only as a copy source until the new path is verified:
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
bluebubbles: {
|
||||
enabled: true,
|
||||
// ... existing config ...
|
||||
},
|
||||
imessage: {
|
||||
enabled: false, // turn on after the dry run below
|
||||
cliPath: "/opt/homebrew/bin/imsg",
|
||||
dmPolicy: "pairing",
|
||||
allowFrom: ["+15555550123"], // copy from bluebubbles.allowFrom
|
||||
groupPolicy: "allowlist",
|
||||
groupAllowFrom: [], // copy from bluebubbles.groupAllowFrom
|
||||
groups: { "*": { requireMention: true } }, // copy from bluebubbles.groups — silently drops groups if missing, see "Group registry footgun" above
|
||||
actions: {
|
||||
reactions: true,
|
||||
edit: true,
|
||||
unsend: true,
|
||||
reply: true,
|
||||
sendWithEffect: true,
|
||||
sendAttachment: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
2. **Dry-run probe** — start the gateway and confirm iMessage reports healthy:
|
||||
|
||||
```bash
|
||||
openclaw gateway
|
||||
openclaw channels status
|
||||
openclaw channels status --probe # expect imessage.privateApi.available: true
|
||||
```
|
||||
|
||||
Because `imessage.enabled` is still `false`, no inbound iMessage traffic is routed yet — but `--probe` exercises the bridge so you catch permission/install issues before the cutover.
|
||||
|
||||
3. **Cut over.** Remove the BlueBubbles config and enable iMessage in one config edit:
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
imessage: { enabled: true /* ... */ },
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Restart the gateway. Inbound iMessage traffic now flows through the bundled plugin.
|
||||
|
||||
4. **Verify DMs.** Send the agent a direct message; confirm the reply lands.
|
||||
|
||||
5. **Verify groups separately.** DMs and groups take different code paths — DM success does not prove groups are routing. Send the agent a message in a paired group chat and confirm the reply lands. If the group goes silent (no agent reply, no error), check the gateway log for `imessage: dropping group message from chat_id=<id>` or the startup `imessage: groupPolicy="allowlist" but channels.imessage.groups is empty` line — both fire at the default log level. If either appears, your `groups` block is missing or empty — see "Group registry footgun" above.
|
||||
|
||||
6. **Verify the action surface** — from a paired DM, ask the agent to react, edit, unsend, reply, send a photo, and (in a group) rename the group / add or remove a participant. Each action should land natively in Messages.app. If any throws "iMessage `<action>` requires the imsg private API bridge", run `imsg launch` again and refresh `channels status --probe`.
|
||||
|
||||
7. **Remove the BlueBubbles server and config** once iMessage DMs, groups, and actions are verified. OpenClaw will not use `channels.bluebubbles`.
|
||||
|
||||
## Action parity at a glance
|
||||
|
||||
| Action | legacy BlueBubbles | bundled iMessage |
|
||||
| ---------------------------------------------------------- | ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
|
||||
| Send text / SMS fallback | ✅ | ✅ |
|
||||
| Send media (photo, video, file, voice) | ✅ | ✅ |
|
||||
| Threaded reply (`reply_to_guid`) | ✅ | ✅ (closes [#51892](https://github.com/openclaw/openclaw/issues/51892)) |
|
||||
| Tapback (`react`) | ✅ | ✅ |
|
||||
| Edit / unsend (macOS 13+ recipients) | ✅ | ✅ |
|
||||
| Send with screen effect | ✅ | ✅ (closes part of [#9394](https://github.com/openclaw/openclaw/issues/9394)) |
|
||||
| Rich text bold / italic / underline / strikethrough | ✅ | ✅ (typed-run formatting via attributedBody) |
|
||||
| Rename group / set group icon | ✅ | ✅ |
|
||||
| Add / remove participant, leave group | ✅ | ✅ |
|
||||
| Read receipts and typing indicator | ✅ | ✅ (gated on private API probe) |
|
||||
| Same-sender DM coalescing | ✅ | ✅ (DM-only; opt-in via `channels.imessage.coalesceSameSenderDms`) |
|
||||
| Catchup of inbound messages received while gateway is down | ✅ (webhook replay + history fetch) | ✅ (opt-in via `channels.imessage.catchup.enabled`; closes [#78649](https://github.com/openclaw/openclaw/issues/78649)) |
|
||||
|
||||
iMessage catchup is now available as an opt-in feature on the bundled plugin. On gateway startup, if `channels.imessage.catchup.enabled` is `true`, the gateway runs one `chats.list` + per-chat `messages.history` pass against the same JSON-RPC client used by `imsg watch`, replays each missed inbound row through the live dispatch path (allowlists, group policy, debouncer, echo cache), and persists a per-account cursor so subsequent startups pick up where they left off. See [Catching up after gateway downtime](/channels/imessage#catching-up-after-gateway-downtime) for tuning.
|
||||
|
||||
## Pairing, sessions, and ACP bindings
|
||||
|
||||
- **Pairing approvals** carry over by handle. You do not need to re-approve known senders — `channels.imessage.allowFrom` recognizes the same `+15555550123` / `user@example.com` strings BlueBubbles used.
|
||||
- **Sessions** stay scoped per agent + chat. DMs collapse into the agent main session under default `session.dmScope=main`; group sessions stay isolated per `chat_id`. The session keys differ (`agent:<id>:imessage:group:<chat_id>` vs the BlueBubbles equivalent) — old conversation history under BlueBubbles session keys does not carry into iMessage sessions.
|
||||
- **ACP bindings** referencing `match.channel: "bluebubbles"` need to be updated to `"imessage"`. The `match.peer.id` shapes (`chat_id:`, `chat_guid:`, `chat_identifier:`, bare handle) are identical.
|
||||
|
||||
## No rollback channel
|
||||
|
||||
There is no supported BlueBubbles runtime to switch back to. If iMessage verification fails, set `channels.imessage.enabled: false`, restart the Gateway, fix the `imsg` blocker, and retry the cutover.
|
||||
|
||||
The reply cache lives at `~/.openclaw/state/imessage/reply-cache.jsonl` (mode `0600`, parent dir `0700`). It is safe to delete if you want a clean slate.
|
||||
|
||||
## Related
|
||||
|
||||
- [iMessage](/channels/imessage) — full iMessage channel reference, including `imsg launch` setup and capability detection.
|
||||
- `/channels/bluebubbles` — legacy URL that redirects to this migration guide.
|
||||
- [Pairing](/channels/pairing) — DM authentication and pairing flow.
|
||||
- [Channel Routing](/channels/channel-routing) — how the gateway picks a channel for outbound replies.
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
summary: "Native iMessage support via imsg (JSON-RPC over stdio), with private API actions for replies, tapbacks, effects, attachments, and group management. Preferred for new OpenClaw iMessage setups when host requirements fit."
|
||||
summary: "Native iMessage support via imsg (JSON-RPC over stdio). Preferred for new OpenClaw iMessage setups when host requirements fit."
|
||||
read_when:
|
||||
- Setting up iMessage support
|
||||
- Debugging iMessage send/receive
|
||||
@@ -8,20 +8,15 @@ title: "iMessage"
|
||||
|
||||
<Note>
|
||||
For OpenClaw iMessage deployments, use `imsg` on a signed-in macOS Messages host. If your Gateway runs on Linux or Windows, point `channels.imessage.cliPath` at an SSH wrapper that runs `imsg` on the Mac.
|
||||
|
||||
**Gateway-downtime catchup is opt-in.** When enabled (`channels.imessage.catchup.enabled: true`), the gateway replays inbound messages that landed in `chat.db` while it was offline (crash, restart, Mac sleep) on next startup. Disabled by default — see [Catching up after gateway downtime](#catching-up-after-gateway-downtime). Closes [openclaw#78649](https://github.com/openclaw/openclaw/issues/78649).
|
||||
</Note>
|
||||
|
||||
<Warning>
|
||||
BlueBubbles support was removed. Migrate `channels.bluebubbles` configs to `channels.imessage`; OpenClaw supports iMessage through `imsg` only.
|
||||
BlueBubbles is deprecated and no longer ships as a bundled OpenClaw channel. Migrate `channels.bluebubbles` configs to `channels.imessage`; OpenClaw now supports iMessage through `imsg` only. If you still need a BlueBubbles-backed bridge, publish or install it as a third-party plugin outside core.
|
||||
</Warning>
|
||||
|
||||
Status: native external CLI integration. Gateway spawns `imsg rpc` and communicates over JSON-RPC on stdio (no separate daemon/port). Advanced actions require `imsg launch` and a successful private API probe.
|
||||
Status: native external CLI integration. Gateway spawns `imsg rpc` and communicates over JSON-RPC on stdio (no separate daemon/port).
|
||||
|
||||
<CardGroup cols={3}>
|
||||
<Card title="Private API actions" icon="wand-sparkles" href="#private-api-actions">
|
||||
Replies, tapbacks, effects, attachments, and group management.
|
||||
</Card>
|
||||
<Card title="Pairing" icon="link" href="/channels/pairing">
|
||||
iMessage DMs default to pairing mode.
|
||||
</Card>
|
||||
@@ -43,8 +38,6 @@ Status: native external CLI integration. Gateway spawns `imsg rpc` and communica
|
||||
```bash
|
||||
brew install steipete/tap/imsg
|
||||
imsg rpc --help
|
||||
imsg launch
|
||||
openclaw channels status --probe
|
||||
```
|
||||
|
||||
</Step>
|
||||
@@ -126,7 +119,6 @@ exec ssh -T gateway-host imsg "$@"
|
||||
- Messages must be signed in on the Mac running `imsg`.
|
||||
- Full Disk Access is required for the process context running OpenClaw/`imsg` (Messages DB access).
|
||||
- Automation permission is required to send messages through Messages.app.
|
||||
- For advanced actions (react / edit / unsend / threaded reply / effects / group ops), System Integrity Protection must be disabled — see [Enabling the imsg private API](#enabling-the-imsg-private-api) below. Basic text and media send/receive work without it.
|
||||
|
||||
<Tip>
|
||||
Permissions are granted per process context. If gateway runs headless (LaunchAgent/SSH), run a one-time interactive command in that same context to trigger prompts:
|
||||
@@ -139,71 +131,6 @@ imsg send <handle> "test"
|
||||
|
||||
</Tip>
|
||||
|
||||
## Enabling the imsg private API
|
||||
|
||||
`imsg` ships in two operational modes:
|
||||
|
||||
- **Basic mode** (default, no SIP changes needed): outbound text and media via `send`, inbound watch/history, chat list. This is what you get out of the box from a fresh `brew install steipete/tap/imsg` plus the standard macOS permissions above.
|
||||
- **Private API mode**: `imsg` injects a helper dylib into `Messages.app` to call internal `IMCore` functions. This is what unlocks `react`, `edit`, `unsend`, `reply` (threaded), `sendWithEffect`, `renameGroup`, `setGroupIcon`, `addParticipant`, `removeParticipant`, `leaveGroup`, plus typing indicators and read receipts.
|
||||
|
||||
To reach the advanced action surface that this channel page documents, you need Private API mode. The `imsg` README is explicit about the requirement:
|
||||
|
||||
> Advanced features such as `read`, `typing`, `launch`, bridge-backed rich send, message mutation, and chat management are opt-in. They require SIP to be disabled and a helper dylib to be injected into `Messages.app`. `imsg launch` refuses to inject when SIP is enabled.
|
||||
|
||||
The helper-injection technique uses `imsg`'s own dylib to reach Messages private APIs. There is no third-party server or BlueBubbles runtime in the OpenClaw iMessage path.
|
||||
|
||||
<Warning>
|
||||
**Disabling SIP is a real security tradeoff.** SIP is one of macOS's core protections against running modified system code; turning it off system-wide opens up additional attack surface and side effects. Notably, **disabling SIP on Apple Silicon Macs also disables the ability to install and run iOS apps on your Mac**.
|
||||
|
||||
Treat this as a deliberate operational choice, not a default. If your threat model can't tolerate SIP being off, bundled iMessage is limited to basic mode — text and media send/receive only, no reactions / edit / unsend / effects / group ops.
|
||||
</Warning>
|
||||
|
||||
### Setup
|
||||
|
||||
1. **Install (or upgrade) `imsg`** on the Mac that runs Messages.app:
|
||||
|
||||
```bash
|
||||
brew install steipete/tap/imsg
|
||||
imsg --version
|
||||
imsg status --json
|
||||
```
|
||||
|
||||
The `imsg status --json` output reports `bridge_version`, `rpc_methods`, and per-method `selectors` so you can see what the current build supports before you start.
|
||||
|
||||
2. **Disable System Integrity Protection.** This is macOS-version-specific because the underlying Apple requirement depends on the OS and hardware:
|
||||
- **macOS 10.13–10.15 (Sierra–Catalina):** disable Library Validation via Terminal, reboot to Recovery Mode, run `csrutil disable`, restart.
|
||||
- **macOS 11+ (Big Sur and later), Intel:** Recovery Mode (or Internet Recovery), `csrutil disable`, restart.
|
||||
- **macOS 11+, Apple Silicon:** power-button startup sequence to enter Recovery; on recent macOS versions hold the **Left Shift** key when you click Continue, then `csrutil disable`. Virtual-machine setups follow a separate flow — take a VM snapshot first.
|
||||
- **macOS 26 / Tahoe:** library-validation policies and `imagent` private-entitlement checks have tightened further; `imsg` may need an updated build to keep up. If `imsg launch` injection or specific `selectors` start returning false after a macOS major upgrade, check `imsg`'s release notes before assuming the SIP step succeeded.
|
||||
|
||||
Follow Apple's Recovery-mode flow for your Mac to disable SIP before running `imsg launch`.
|
||||
|
||||
3. **Inject the helper.** With SIP disabled and Messages.app signed in:
|
||||
|
||||
```bash
|
||||
imsg launch
|
||||
```
|
||||
|
||||
`imsg launch` refuses to inject when SIP is still enabled, so this also doubles as a confirmation that step 2 took.
|
||||
|
||||
4. **Verify the bridge from OpenClaw:**
|
||||
|
||||
```bash
|
||||
openclaw channels status --probe
|
||||
```
|
||||
|
||||
The iMessage entry should report `works`, and `imsg status --json | jq '.selectors'` should show `retractMessagePart: true` plus whichever edit / typing / read selectors your macOS build exposes. The OpenClaw plugin per-method gating in `actions.ts` only advertises actions whose underlying selector is `true`, so the action surface you see in the agent's tool list reflects what the bridge can actually do on this host.
|
||||
|
||||
If `openclaw channels status --probe` reports the channel as `works` but specific actions throw "iMessage `<action>` requires the imsg private API bridge" at dispatch time, run `imsg launch` again — the helper can fall out (Messages.app restart, OS update, etc.) and the cached `available: true` status will keep advertising actions until the next probe refreshes.
|
||||
|
||||
### When you can't disable SIP
|
||||
|
||||
If SIP-disabled isn't acceptable for your threat model:
|
||||
|
||||
- `imsg` falls back to basic mode — text + media + receive only.
|
||||
- The OpenClaw plugin still advertises text/media send and inbound monitoring; it just hides `react`, `edit`, `unsend`, `reply`, `sendWithEffect`, and group ops from the action surface (per the per-method capability gate).
|
||||
- You can run a separate non-Apple-Silicon Mac (or a dedicated bot Mac) with SIP off for the iMessage workload, while keeping SIP enabled on your primary devices. See [Dedicated bot macOS user (separate iMessage identity)](#deployment-patterns) below.
|
||||
|
||||
## Access control and routing
|
||||
|
||||
<Tabs>
|
||||
@@ -233,36 +160,6 @@ If SIP-disabled isn't acceptable for your threat model:
|
||||
Runtime fallback: if `groupAllowFrom` is unset, iMessage group sender checks fall back to `allowFrom` when available.
|
||||
Runtime note: if `channels.imessage` is completely missing, runtime falls back to `groupPolicy="allowlist"` and logs a warning (even if `channels.defaults.groupPolicy` is set).
|
||||
|
||||
<Warning>
|
||||
Group routing has **two** allowlist gates running back-to-back, and both must pass:
|
||||
|
||||
1. **Sender / chat-target allowlist** (`channels.imessage.groupAllowFrom`) — handle, `chat_guid`, `chat_identifier`, or `chat_id`.
|
||||
2. **Group registry** (`channels.imessage.groups`) — with `groupPolicy: "allowlist"`, this gate requires either a `groups: { "*": { ... } }` wildcard entry (sets `allowAll = true`), or an explicit per-`chat_id` entry under `groups`.
|
||||
|
||||
If gate 2 has nothing in it, every group message is dropped. The plugin emits two `warn`-level signals at the default log level:
|
||||
|
||||
- one-time per account at startup: `imessage: groupPolicy="allowlist" but channels.imessage.groups is empty for account "<id>"`
|
||||
- one-time per `chat_id` at runtime: `imessage: dropping group message from chat_id=<id> ...`
|
||||
|
||||
DMs continue to work because they take a different code path.
|
||||
|
||||
Minimum config to keep groups flowing under `groupPolicy: "allowlist"`:
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
imessage: {
|
||||
groupPolicy: "allowlist",
|
||||
groupAllowFrom: ["+15555550123"],
|
||||
groups: { "*": { "requireMention": true } },
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
If those `warn` lines appear in the gateway log, gate 2 is dropping — add the `groups` block.
|
||||
</Warning>
|
||||
|
||||
Mention gating for groups:
|
||||
|
||||
- iMessage has no native mention metadata
|
||||
@@ -271,37 +168,6 @@ If SIP-disabled isn't acceptable for your threat model:
|
||||
|
||||
Control commands from authorized senders can bypass mention gating in groups.
|
||||
|
||||
Per-group `systemPrompt`:
|
||||
|
||||
Each entry under `channels.imessage.groups.*` accepts an optional `systemPrompt` string. The value is injected into the agent's system prompt on every turn that handles a message in that group. Resolution mirrors the per-group prompt resolution used by `channels.whatsapp.groups`:
|
||||
|
||||
1. **Group-specific system prompt** (`groups["<chat_id>"].systemPrompt`): used when the specific group entry exists in the map **and** its `systemPrompt` key is defined. If `systemPrompt` is an empty string (`""`) the wildcard is suppressed and no system prompt is applied to that group.
|
||||
2. **Group wildcard system prompt** (`groups["*"].systemPrompt`): used when the specific group entry is absent from the map entirely, or when it exists but defines no `systemPrompt` key.
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
imessage: {
|
||||
groupPolicy: "allowlist",
|
||||
groupAllowFrom: ["+15555550123"],
|
||||
groups: {
|
||||
"*": { systemPrompt: "Use British spelling." },
|
||||
"8421": {
|
||||
requireMention: true,
|
||||
systemPrompt: "This is the on-call rotation chat. Keep replies under 3 sentences.",
|
||||
},
|
||||
"9907": {
|
||||
// explicit suppression: the wildcard "Use British spelling." does not apply here
|
||||
systemPrompt: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Per-group prompts only apply to group messages — direct messages in this channel are unaffected.
|
||||
|
||||
</Tab>
|
||||
|
||||
<Tab title="Sessions and deterministic replies">
|
||||
@@ -398,24 +264,24 @@ See [ACP Agents](/tools/acp-agents) for shared ACP binding behavior.
|
||||
|
||||
Example:
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
imessage: {
|
||||
enabled: true,
|
||||
cliPath: "~/.openclaw/scripts/imsg-ssh",
|
||||
remoteHost: "bot@mac-mini.tailnet-1234.ts.net",
|
||||
includeAttachments: true,
|
||||
dbPath: "/Users/bot/Library/Messages/chat.db",
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
imessage: {
|
||||
enabled: true,
|
||||
cliPath: "~/.openclaw/scripts/imsg-ssh",
|
||||
remoteHost: "bot@mac-mini.tailnet-1234.ts.net",
|
||||
includeAttachments: true,
|
||||
dbPath: "/Users/bot/Library/Messages/chat.db",
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
exec ssh -T bot@mac-mini.tailnet-1234.ts.net imsg "$@"
|
||||
```
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
exec ssh -T bot@mac-mini.tailnet-1234.ts.net imsg "$@"
|
||||
```
|
||||
|
||||
Use SSH keys so both SSH and SCP are non-interactive.
|
||||
Ensure the host key is trusted first (for example `ssh bot@mac-mini.tailnet-1234.ts.net`) so `known_hosts` is populated.
|
||||
@@ -434,7 +300,7 @@ See [ACP Agents](/tools/acp-agents) for shared ACP binding behavior.
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Attachments and media">
|
||||
- inbound attachment ingestion is **off by default** — set `channels.imessage.includeAttachments: true` to forward photos, voice memos, video, and other attachments to the agent. With it disabled, attachment-only iMessages are dropped before reaching the agent and may produce no `Inbound message` log line at all.
|
||||
- inbound attachment ingestion is optional: `channels.imessage.includeAttachments`
|
||||
- remote attachment paths can be fetched via SCP when `remoteHost` is set
|
||||
- attachment paths must match allowed roots:
|
||||
- `channels.imessage.attachmentRoots` (local)
|
||||
@@ -466,76 +332,10 @@ See [ACP Agents](/tools/acp-agents) for shared ACP binding behavior.
|
||||
- `sms:+1555...`
|
||||
- `user@example.com`
|
||||
|
||||
```bash
|
||||
imsg chats --limit 20
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
## Private API actions
|
||||
|
||||
When `imsg launch` is running and `openclaw channels status --probe` reports `privateApi.available: true`, the message tool can use iMessage-native actions in addition to normal text sends.
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
imessage: {
|
||||
actions: {
|
||||
reactions: true,
|
||||
edit: true,
|
||||
unsend: true,
|
||||
reply: true,
|
||||
sendWithEffect: true,
|
||||
sendAttachment: true,
|
||||
renameGroup: true,
|
||||
setGroupIcon: true,
|
||||
addParticipant: true,
|
||||
removeParticipant: true,
|
||||
leaveGroup: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```bash
|
||||
imsg chats --limit 20
|
||||
```
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Available actions">
|
||||
- **react**: Add/remove iMessage tapbacks (`messageId`, `emoji`, `remove`). Supported tapbacks map to love, like, dislike, laugh, emphasize, and question.
|
||||
- **reply**: Send a threaded reply to an existing message (`messageId`, `text` or `message`, plus `chatGuid`, `chatId`, `chatIdentifier`, or `to`).
|
||||
- **sendWithEffect**: Send text with an iMessage effect (`text` or `message`, `effect` or `effectId`).
|
||||
- **edit**: Edit a sent message on supported macOS/private API versions (`messageId`, `text` or `newText`).
|
||||
- **unsend**: Retract a sent message on supported macOS/private API versions (`messageId`).
|
||||
- **upload-file**: Send media/files (`buffer` as base64 or a hydrated `media`/`path`/`filePath`, `filename`, optional `asVoice`). Legacy alias: `sendAttachment`.
|
||||
- **renameGroup**, **setGroupIcon**, **addParticipant**, **removeParticipant**, **leaveGroup**: Manage group chats when the current target is a group conversation.
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Message IDs">
|
||||
Inbound iMessage context includes both short `MessageSid` values and full message GUIDs when available. Short IDs are scoped to the recent in-memory reply cache and are checked against the current chat before use. If a short ID has expired or belongs to another chat, retry with the full `MessageSidFull`.
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Capability detection">
|
||||
OpenClaw hides private API actions only when the cached probe status says the bridge is unavailable. If the status is unknown, actions remain visible and dispatch probes lazily so the first action can succeed after `imsg launch` without a separate manual status refresh.
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Read receipts and typing">
|
||||
When the private API bridge is up, accepted inbound chats are marked read before dispatch and a typing bubble is shown to the sender while the agent generates. Disable read-marking with:
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
imessage: {
|
||||
sendReadReceipts: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Older `imsg` builds that pre-date the per-method capability list will gate off typing/read silently; OpenClaw logs a one-time warning per restart so the missing receipt is attributable.
|
||||
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
@@ -555,158 +355,18 @@ Disable:
|
||||
}
|
||||
```
|
||||
|
||||
<a id="coalescing-split-send-dms-command--url-in-one-composition"></a>
|
||||
|
||||
## Coalescing split-send DMs (command + URL in one composition)
|
||||
|
||||
When a user types a command and a URL together — e.g. `Dump https://example.com/article` — Apple's Messages app splits the send into **two separate `chat.db` rows**:
|
||||
|
||||
1. A text message (`"Dump"`).
|
||||
2. A URL-preview balloon (`"https://..."`) with OG-preview images as attachments.
|
||||
|
||||
The two rows arrive at OpenClaw ~0.8-2.0 s apart on most setups. Without coalescing, the agent receives the command alone on turn 1, replies (often "send me the URL"), and only sees the URL on turn 2 — at which point the command context is already lost. This is Apple's send pipeline, not anything OpenClaw or `imsg` introduces.
|
||||
|
||||
`channels.imessage.coalesceSameSenderDms` opts a DM into merging consecutive same-sender rows into a single agent turn. Group chats continue to dispatch per-message so multi-user turn structure is preserved.
|
||||
|
||||
<Tabs>
|
||||
<Tab title="When to enable">
|
||||
Enable when:
|
||||
|
||||
- You ship skills that expect `command + payload` in one message (dump, paste, save, queue, etc.).
|
||||
- Your users paste URLs, images, or long content alongside commands.
|
||||
- You can accept the added DM turn latency (see below).
|
||||
|
||||
Leave disabled when:
|
||||
|
||||
- You need minimum command latency for single-word DM triggers.
|
||||
- All your flows are one-shot commands without payload follow-ups.
|
||||
|
||||
</Tab>
|
||||
<Tab title="Enabling">
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
imessage: {
|
||||
coalesceSameSenderDms: true, // opt in (default: false)
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
With the flag on and no explicit `messages.inbound.byChannel.imessage`, the debounce window widens to **2500 ms** (the legacy default is 0 ms — no debouncing). The wider window is required because Apple's split-send cadence of 0.8-2.0 s does not fit in a tighter default.
|
||||
|
||||
To tune the window yourself:
|
||||
|
||||
```json5
|
||||
{
|
||||
messages: {
|
||||
inbound: {
|
||||
byChannel: {
|
||||
// 2500 ms works for most setups; raise to 4000 ms if your Mac is
|
||||
// slow or under memory pressure (observed gap can stretch past 2 s
|
||||
// then).
|
||||
imessage: 2500,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
</Tab>
|
||||
<Tab title="Trade-offs">
|
||||
- **Added latency for DM messages.** With the flag on, every DM (including standalone control commands and single-text follow-ups) waits up to the debounce window before dispatching, in case a payload row is coming. Group-chat messages keep instant dispatch.
|
||||
- **Merged output is bounded.** Merged text caps at 4000 chars with an explicit `…[truncated]` marker; attachments cap at 20; source entries cap at 10 (first-plus-latest retained beyond that). Every source GUID is tracked in `coalescedMessageGuids` for downstream telemetry.
|
||||
- **DM-only.** Group chats fall through to per-message dispatch so the bot stays responsive when multiple people are typing.
|
||||
- **Opt-in, per-channel.** Other channels (Telegram, WhatsApp, Slack, …) are unaffected. Legacy BlueBubbles configs that set `channels.bluebubbles.coalesceSameSenderDms` should migrate that value to `channels.imessage.coalesceSameSenderDms`.
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
### Scenarios and what the agent sees
|
||||
|
||||
| User composes | `chat.db` produces | Flag off (default) | Flag on + 2500 ms window |
|
||||
| ------------------------------------------------------------------ | --------------------- | --------------------------------------- | ----------------------------------------------------------------------- |
|
||||
| `Dump https://example.com` (one send) | 2 rows ~1 s apart | Two agent turns: "Dump" alone, then URL | One turn: merged text `Dump https://example.com` |
|
||||
| `Save this 📎image.jpg caption` (attachment + text) | 2 rows | Two turns (attachment dropped on merge) | One turn: text + image preserved |
|
||||
| `/status` (standalone command) | 1 row | Instant dispatch | **Wait up to window, then dispatch** |
|
||||
| URL pasted alone | 1 row | Instant dispatch | Instant dispatch (only one entry in bucket) |
|
||||
| Text + URL sent as two deliberate separate messages, minutes apart | 2 rows outside window | Two turns | Two turns (window expires between them) |
|
||||
| Rapid flood (>10 small DMs inside window) | N rows | N turns | One turn, bounded output (first + latest, text/attachment caps applied) |
|
||||
| Two people typing in a group chat | N rows from M senders | M+ turns (one per sender bucket) | M+ turns — group chats are not coalesced |
|
||||
|
||||
## Catching up after gateway downtime
|
||||
|
||||
When the gateway is offline (crash, restart, Mac sleep, machine off), `imsg watch` resumes from the current `chat.db` state once the gateway comes back up — anything that arrived during the gap is, by default, never seen. Catchup replays those messages on the next startup so the agent does not silently miss inbound traffic.
|
||||
|
||||
Catchup is **disabled by default**. Enable it per channel:
|
||||
|
||||
```ts
|
||||
channels: {
|
||||
imessage: {
|
||||
catchup: {
|
||||
enabled: true, // master switch (default: false)
|
||||
maxAgeMinutes: 120, // skip rows older than now - 2h (default: 120, clamp 1..720)
|
||||
perRunLimit: 50, // max rows replayed per startup (default: 50, clamp 1..500)
|
||||
firstRunLookbackMinutes: 30, // first run with no cursor: look back 30 min (default: 30)
|
||||
maxFailureRetries: 10, // give up on a wedged guid after 10 dispatch failures (default: 10)
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### How it runs
|
||||
|
||||
One pass per `monitorIMessageProvider` startup, sequenced as `imsg launch` ready → `watch.subscribe` → `performIMessageCatchup` → live dispatch loop. Catchup itself uses `chats.list` + per-chat `messages.history` against the same JSON-RPC client used by `imsg watch`. Anything that arrives during the catchup pass flows through live dispatch normally; the existing inbound-dedupe cache absorbs any overlap with replayed rows.
|
||||
|
||||
Each replayed row is fed through the live dispatch path (`evaluateIMessageInbound` + `dispatchInboundMessage`), so allowlists, group policy, debouncer, echo cache, and read receipts behave identically on replayed and live messages.
|
||||
|
||||
### Cursor and retry semantics
|
||||
|
||||
Catchup keeps a per-account cursor at `<openclawStateDir>/imessage/catchup/<account>__<hash>.json` (the OpenClaw state dir defaults to `~/.openclaw`, overridable with `OPENCLAW_STATE_DIR`):
|
||||
|
||||
```json
|
||||
{
|
||||
"lastSeenMs": 1717900800000,
|
||||
"lastSeenRowid": 482910,
|
||||
"updatedAt": 1717900801234,
|
||||
"failureRetries": { "<guid>": 1 }
|
||||
}
|
||||
```
|
||||
|
||||
- The cursor advances on each successful dispatch and is held when a row's dispatch throws — the next startup retries the same row from the held cursor.
|
||||
- After `maxFailureRetries` consecutive throws against the same `guid`, catchup logs a `warn` and force-advances the cursor past the wedged message so subsequent startups can make progress.
|
||||
- Already-given-up guids are skipped on sight (no dispatch attempt) on later runs and counted under `skippedGivenUp` in the run summary.
|
||||
|
||||
### Operator-visible signals
|
||||
|
||||
```
|
||||
imessage catchup: replayed=N skippedFromMe=… skippedGivenUp=… failed=… givenUp=… fetchedCount=…
|
||||
imessage catchup: giving up on guid=<guid> after <N> failures; advancing cursor past it
|
||||
imessage catchup: fetched <X> rows across chats, capped to perRunLimit=<Y>
|
||||
```
|
||||
|
||||
A `WARN ... capped to perRunLimit` line means a single startup did not drain the full backlog. Raise `perRunLimit` (max 500) if your gaps regularly exceed the default 50-row pass.
|
||||
|
||||
### When to leave it off
|
||||
|
||||
- Gateway runs continuously with watchdog auto-restart and gaps are always < a few seconds — the default of off is fine.
|
||||
- DM volume is low and missed messages would not change agent behavior — the `firstRunLookbackMinutes` initial window can dispatch surprising old context on first enable.
|
||||
|
||||
When you turn catchup on, the first startup with no cursor only looks back `firstRunLookbackMinutes` (30 min default), not the full `maxAgeMinutes` window — this avoids replaying a long history of pre-enable messages.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="imsg not found or RPC unsupported">
|
||||
Validate the binary and RPC support:
|
||||
|
||||
```bash
|
||||
imsg rpc --help
|
||||
imsg status --json
|
||||
openclaw channels status --probe
|
||||
```
|
||||
```bash
|
||||
imsg rpc --help
|
||||
openclaw channels status --probe
|
||||
```
|
||||
|
||||
If probe reports RPC unsupported, update `imsg`. If private API actions are unavailable, run `imsg launch` in the logged-in macOS user session and probe again. If the Gateway is not running on macOS, use the Remote Mac over SSH setup above instead of the default local `imsg` path.
|
||||
If probe reports RPC unsupported, update `imsg`. If the Gateway is not running on macOS, use the Remote Mac over SSH setup above instead of the default local `imsg` path.
|
||||
|
||||
</Accordion>
|
||||
|
||||
@@ -759,10 +419,10 @@ openclaw channels status --probe --channel imessage
|
||||
<Accordion title="macOS permission prompts were missed">
|
||||
Re-run in an interactive GUI terminal in the same user/session context and approve prompts:
|
||||
|
||||
```bash
|
||||
imsg chats --limit 1
|
||||
imsg send <handle> "test"
|
||||
```
|
||||
```bash
|
||||
imsg chats --limit 1
|
||||
imsg send <handle> "test"
|
||||
```
|
||||
|
||||
Confirm Full Disk Access + Automation are granted for the process context that runs OpenClaw/`imsg`.
|
||||
|
||||
@@ -778,7 +438,6 @@ openclaw channels status --probe --channel imessage
|
||||
## Related
|
||||
|
||||
- [Channels Overview](/channels) — all supported channels
|
||||
- [Coming from BlueBubbles](/channels/imessage-from-bluebubbles) — config translation table and step-by-step cutover
|
||||
- [Pairing](/channels/pairing) — DM authentication and pairing flow
|
||||
- [Groups](/channels/groups) — group chat behavior and mention gating
|
||||
- [Channel Routing](/channels/channel-routing) — session routing for messages
|
||||
|
||||
@@ -24,7 +24,7 @@ Text is supported everywhere; media and reactions vary by channel.
|
||||
- [Discord](/channels/discord) - Discord Bot API + Gateway; supports servers, channels, and DMs.
|
||||
- [Feishu](/channels/feishu) - Feishu/Lark bot via WebSocket (bundled plugin).
|
||||
- [Google Chat](/channels/googlechat) - Google Chat API app via HTTP webhook (downloadable plugin).
|
||||
- [iMessage](/channels/imessage) - Native macOS integration via the `imsg` bridge on a signed-in Mac (or SSH wrapper when the Gateway runs elsewhere), including private API actions for replies, tapbacks, effects, attachments, and group management. Preferred for new OpenClaw iMessage setups when host permissions and Messages access fit.
|
||||
- [iMessage](/channels/imessage) - Native macOS integration via the `imsg` CLI on a signed-in Mac; use an SSH wrapper when the Gateway runs elsewhere.
|
||||
- [IRC](/channels/irc) - Classic IRC servers; channels + DMs with pairing/allowlist controls.
|
||||
- [LINE](/channels/line) - LINE Messaging API bot (downloadable plugin).
|
||||
- [Matrix](/channels/matrix) - Matrix protocol (downloadable plugin).
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
---
|
||||
summary: "Matrix MessagePresentation metadata for OpenClaw-aware clients"
|
||||
read_when:
|
||||
- Building Matrix clients that render OpenClaw rich responses
|
||||
- Debugging com.openclaw.presentation event content
|
||||
title: "Matrix presentation metadata"
|
||||
---
|
||||
|
||||
OpenClaw can attach normalized `MessagePresentation` metadata to outbound Matrix `m.room.message` events under `com.openclaw.presentation`.
|
||||
|
||||
Stock Matrix clients continue to render the plain text `body`. OpenClaw-aware clients can read the structured metadata and render native UI such as buttons, selects, context rows, and dividers.
|
||||
|
||||
## Event content
|
||||
|
||||
The metadata is stored in Matrix event content:
|
||||
|
||||
```json
|
||||
{
|
||||
"msgtype": "m.text",
|
||||
"body": "Select model\n\n- DeepSeek: /model deepseek/deepseek-chat",
|
||||
"com.openclaw.presentation": {
|
||||
"version": 1,
|
||||
"type": "message.presentation",
|
||||
"title": "Select model",
|
||||
"tone": "info",
|
||||
"blocks": [
|
||||
{
|
||||
"type": "select",
|
||||
"placeholder": "Choose model",
|
||||
"options": [
|
||||
{
|
||||
"label": "DeepSeek",
|
||||
"value": "/model deepseek/deepseek-chat"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`version` is the Matrix presentation metadata schema version. `type` is a stable discriminator for OpenClaw-aware clients. Clients should ignore unknown `type` values, unknown versions they cannot safely interpret, and unknown block types.
|
||||
|
||||
## Fallback behavior
|
||||
|
||||
OpenClaw always renders a readable plain text fallback into `body`. The structured metadata is additive and must not be required for basic Matrix interoperability.
|
||||
|
||||
Unsupported clients should continue to show the fallback text. OpenClaw-aware clients may prefer the structured metadata for display while preserving the fallback text for copy, search, notifications, and accessibility.
|
||||
|
||||
## Supported blocks
|
||||
|
||||
The Matrix outbound adapter advertises support for:
|
||||
|
||||
- `buttons`
|
||||
- `select`
|
||||
- `context`
|
||||
- `divider`
|
||||
|
||||
Clients should treat these blocks as best-effort presentation hints. Unknown fields and unknown block types should be ignored rather than causing the full message to fail rendering.
|
||||
|
||||
## Interactions
|
||||
|
||||
This metadata does not add Matrix callback semantics. Button and select option values are fallback interaction payloads, usually slash commands or text commands. A Matrix client that wants to support interaction can send the selected value back to the room as a normal message.
|
||||
|
||||
For example, a button with value `/model deepseek/deepseek-chat` can be handled by sending that value as an encrypted Matrix text message in the same room.
|
||||
|
||||
## Relationship to approval metadata
|
||||
|
||||
`com.openclaw.presentation` is for general rich message presentation.
|
||||
|
||||
Approval prompts use the dedicated `com.openclaw.approval` metadata because approvals carry safety-sensitive state, decisions, and exec/plugin details. If both metadata keys are present on the same event, clients should prefer the dedicated approval renderer.
|
||||
|
||||
## Media messages
|
||||
|
||||
When a reply contains multiple media URLs, OpenClaw sends one Matrix event per media URL. Presentation metadata is attached only to the first media event so clients have one stable structured payload and duplicate renderers are avoided.
|
||||
|
||||
Keep presentation metadata compact. Large user-visible text should stay in `body` and use the normal Matrix text chunking path.
|
||||
@@ -11,14 +11,12 @@ It uses the official `matrix-js-sdk` and supports DMs, rooms, threads, media, re
|
||||
|
||||
## Install
|
||||
|
||||
Install Matrix from ClawHub before configuring the channel:
|
||||
Install Matrix before configuring the channel:
|
||||
|
||||
```bash
|
||||
openclaw plugins install @openclaw/matrix
|
||||
```
|
||||
|
||||
Bare plugin specs try ClawHub first, then npm fallback. To force the registry source, use `openclaw plugins install clawhub:@openclaw/matrix` or `openclaw plugins install npm:@openclaw/matrix`.
|
||||
|
||||
From a local checkout:
|
||||
|
||||
```bash
|
||||
|
||||
@@ -40,7 +40,7 @@ Details: [Plugins](/tools/plugin)
|
||||
2. On your Nextcloud server, create a bot:
|
||||
|
||||
```bash
|
||||
./occ talk:bot:install "OpenClaw" "<shared-secret>" "<webhook-url>" --feature webhook --feature response --feature reaction
|
||||
./occ talk:bot:install "OpenClaw" "<shared-secret>" "<webhook-url>" --feature reaction
|
||||
```
|
||||
|
||||
3. Enable the bot in the target room settings.
|
||||
|
||||
@@ -77,10 +77,6 @@ Both transports are production-ready and reach feature parity for messaging, sla
|
||||
"bot": [
|
||||
"app_mentions:read",
|
||||
"assistant:write",
|
||||
"bookmarks:read",
|
||||
"bookmarks:write",
|
||||
"canvases:read",
|
||||
"canvases:write",
|
||||
"channels:history",
|
||||
"channels:read",
|
||||
"chat:write",
|
||||
@@ -100,8 +96,6 @@ Both transports are production-ready and reach feature parity for messaging, sla
|
||||
"pins:write",
|
||||
"reactions:read",
|
||||
"reactions:write",
|
||||
"reminders:read",
|
||||
"reminders:write",
|
||||
"usergroups:read",
|
||||
"users:read"
|
||||
]
|
||||
@@ -274,10 +268,6 @@ openclaw gateway
|
||||
"bot": [
|
||||
"app_mentions:read",
|
||||
"assistant:write",
|
||||
"bookmarks:read",
|
||||
"bookmarks:write",
|
||||
"canvases:read",
|
||||
"canvases:write",
|
||||
"channels:history",
|
||||
"channels:read",
|
||||
"chat:write",
|
||||
@@ -297,8 +287,6 @@ openclaw gateway
|
||||
"pins:write",
|
||||
"reactions:read",
|
||||
"reactions:write",
|
||||
"reminders:read",
|
||||
"reminders:write",
|
||||
"usergroups:read",
|
||||
"users:read"
|
||||
]
|
||||
@@ -506,10 +494,6 @@ Base manifest (Socket Mode default):
|
||||
"bot": [
|
||||
"app_mentions:read",
|
||||
"assistant:write",
|
||||
"bookmarks:read",
|
||||
"bookmarks:write",
|
||||
"canvases:read",
|
||||
"canvases:write",
|
||||
"channels:history",
|
||||
"channels:read",
|
||||
"chat:write",
|
||||
@@ -529,8 +513,6 @@ Base manifest (Socket Mode default):
|
||||
"pins:write",
|
||||
"reactions:read",
|
||||
"reactions:write",
|
||||
"reminders:read",
|
||||
"reminders:write",
|
||||
"usergroups:read",
|
||||
"users:read"
|
||||
]
|
||||
@@ -819,17 +801,15 @@ Slack actions are controlled by `channels.slack.actions.*`.
|
||||
|
||||
Available action groups in current Slack tooling:
|
||||
|
||||
- Default-enabled groups: `messages`, `reactions`, `pins`, `memberInfo`, `emojiList`.
|
||||
- Opt-in advanced groups: `search`, `channelInfo`, `channels`, `files`, `scheduledMessages`, `ephemeralMessages`, `bookmarks`, `reminders`, `canvases`.
|
||||
| Group | Default |
|
||||
| ---------- | ------- |
|
||||
| messages | enabled |
|
||||
| reactions | enabled |
|
||||
| pins | enabled |
|
||||
| memberInfo | enabled |
|
||||
| emojiList | enabled |
|
||||
|
||||
Current Slack message actions include `send`, `upload-file`, `download-file`, `read`, `edit`, `delete`, `get-permalink`, `pin`, `unpin`, `list-pins`, `member-info`, and `emoji-list`. `download-file` accepts Slack file IDs shown in inbound file placeholders and returns image previews for images or local file metadata for other file types.
|
||||
|
||||
Advanced opt-in actions:
|
||||
|
||||
- Search and discovery: `search`, `channel-info`, `channel-list`. `search` uses Slack `search.messages` and requires `channels.slack.userToken` with the user-token `search:read` scope.
|
||||
- Files: `file-list`, `file-delete`.
|
||||
- Delivery controls: `post-ephemeral`, `schedule-message`, `scheduled-list`, `delete-scheduled`; `send` and scheduled sends also accept Slack `replyBroadcast`, `unfurlLinks`, and `unfurlMedia`.
|
||||
- Workspace objects: `bookmark-add`, `bookmark-edit`, `bookmark-list`, `bookmark-remove`, `reminder-add`, `reminder-list`, `reminder-info`, `reminder-complete`, `reminder-delete`, `canvas-create`, `canvas-edit`, `canvas-delete`, `canvas-section-lookup`, `channel-canvas-create`.
|
||||
Current Slack message actions include `send`, `upload-file`, `download-file`, `read`, `edit`, `delete`, `pin`, `unpin`, `list-pins`, `member-info`, and `emoji-list`. `download-file` accepts Slack file IDs shown in inbound file placeholders and returns image previews for images or local file metadata for other file types.
|
||||
|
||||
## Access control and routing
|
||||
|
||||
@@ -950,7 +930,6 @@ Advanced opt-in actions:
|
||||
- With default `session.dmScope=main`, Slack DMs collapse to agent main session.
|
||||
- Channel sessions: `agent:<agentId>:slack:channel:<channelId>`.
|
||||
- Thread replies can create thread session suffixes (`:thread:<threadTs>`) when applicable.
|
||||
- In channels where OpenClaw handles top-level messages without requiring an explicit mention, non-`off` `replyToMode` routes each handled root into `agent:<agentId>:slack:channel:<channelId>:thread:<rootTs>` so the visible Slack thread maps to one OpenClaw session from the first turn.
|
||||
- `channels.slack.thread.historyScope` default is `thread`; `thread.inheritParent` default is `false`.
|
||||
- `channels.slack.thread.initialHistoryLimit` controls how many existing thread messages are fetched when a new thread session starts (default `20`; set `0` to disable).
|
||||
- `channels.slack.thread.requireExplicitMention` (default `false`): when `true`, suppress implicit thread mentions so the bot only responds to explicit `@bot` mentions inside threads, even when the bot already participated in the thread. Without this, replies in a bot-participated thread bypass `requireMention` gating.
|
||||
|
||||
@@ -258,7 +258,7 @@ curl "https://api.telegram.org/bot<bot_token>/getUpdates"
|
||||
|
||||
- Telegram is owned by the gateway process.
|
||||
- Routing is deterministic: Telegram inbound replies back to Telegram (the model does not pick channels).
|
||||
- Inbound messages normalize into the shared channel envelope with reply metadata, media placeholders, and persisted reply-chain context for Telegram replies the gateway has observed.
|
||||
- Inbound messages normalize into the shared channel envelope with reply metadata and media placeholders.
|
||||
- Group sessions are isolated by group ID. Forum topics append `:topic:<threadId>` to keep topics isolated.
|
||||
- DM messages can carry `message_thread_id`; OpenClaw preserves the thread ID for replies but keeps DMs on the flat session by default. Configure `channels.telegram.dm.threadReplies: "inbound"`, `channels.telegram.direct.<chatId>.threadReplies: "inbound"`, `requireTopic: true`, or a matching topic config when you intentionally want DM topic session isolation.
|
||||
- Long polling uses grammY runner with per-chat/per-thread sequencing. Overall runner sink concurrency uses `agents.defaults.maxConcurrent`.
|
||||
@@ -773,7 +773,7 @@ curl "https://api.telegram.org/bot<bot_token>/getUpdates"
|
||||
- `channels.telegram.timeoutSeconds` overrides Telegram API client timeout (if unset, grammY default applies). Bot clients clamp configured values below the 60-second outbound text/typing request guard so grammY does not abort visible reply delivery before OpenClaw's transport guard and fallback can run. Long polling still uses a 45-second `getUpdates` request guard so idle polls are not abandoned indefinitely.
|
||||
- `channels.telegram.pollingStallThresholdMs` defaults to `120000`; tune between `30000` and `600000` only for false-positive polling-stall restarts.
|
||||
- group context history uses `channels.telegram.historyLimit` or `messages.groupChat.historyLimit` (default 50); `0` disables.
|
||||
- reply/quote/forward supplemental context is normalized into a nearest-first reply chain when the gateway has observed the parent messages; the observed-message cache is persisted beside the session store. Telegram only includes one shallow `reply_to_message` in updates, so chains older than the cache are limited to Telegram's current update payload.
|
||||
- reply/quote/forward supplemental context is currently passed as received.
|
||||
- Telegram allowlists primarily gate who can trigger the agent, not a full supplemental-context redaction boundary.
|
||||
- DM history controls:
|
||||
- `channels.telegram.dmHistoryLimit`
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
---
|
||||
summary: "How ClawHub publishing works for skills, plugins, owners, scopes, releases, and review."
|
||||
read_when:
|
||||
- Publishing a skill or plugin
|
||||
- Debugging owner or package scope errors
|
||||
- Adding publish UI, CLI, or backend behavior
|
||||
---
|
||||
|
||||
# Publishing on ClawHub
|
||||
|
||||
ClawHub publishing is owner-scoped: every publish targets a publisher, and the
|
||||
server decides whether the signed-in user is allowed to publish there.
|
||||
|
||||
## Owners
|
||||
|
||||
An owner is a ClawHub publisher handle, such as `@alice` or `@openclaw`.
|
||||
Personal owners are created for users. Org owners can have multiple members.
|
||||
|
||||
When you publish, you either use your personal owner or choose an org owner
|
||||
where you have publisher access.
|
||||
|
||||
## Skills
|
||||
|
||||
Skills are published from a skill folder. The public page is:
|
||||
|
||||
```text
|
||||
https://clawhub.ai/<owner>/<slug>
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
```text
|
||||
https://clawhub.ai/alice/review-helper
|
||||
```
|
||||
|
||||
The publish request includes the selected owner, slug, version, changelog, and
|
||||
files. The server verifies that the actor can publish as that owner before it
|
||||
creates the release.
|
||||
|
||||
## Plugins
|
||||
|
||||
Plugins use npm-style package names. Scoped package names include the owner in
|
||||
the first part of the name:
|
||||
|
||||
```text
|
||||
@owner/package-name
|
||||
```
|
||||
|
||||
The scope must match the selected publish owner. If your package is named
|
||||
`@openclaw/dronzer`, it can only be published as `@openclaw`. If you publish as
|
||||
`@vintageayu`, rename the package to `@vintageayu/dronzer`.
|
||||
|
||||
This prevents a package from claiming an org namespace that the publisher does
|
||||
not control.
|
||||
|
||||
## Release Flow
|
||||
|
||||
1. The UI, CLI, or GitHub workflow gathers package metadata and files.
|
||||
2. The publish request is sent to ClawHub with the selected owner.
|
||||
3. The server validates owner permissions, package scope, package name, version,
|
||||
file limits, and source metadata.
|
||||
4. ClawHub stores the release and starts automated security checks.
|
||||
5. New releases are hidden from normal install/download surfaces until review
|
||||
and verification finish.
|
||||
|
||||
If validation fails, the release is not created.
|
||||
|
||||
## FAQ
|
||||
|
||||
### Package scope must match selected owner
|
||||
|
||||
If the package scope and selected owner do not match, ClawHub rejects the
|
||||
publish:
|
||||
|
||||
```text
|
||||
Package scope "@openclaw" must match selected owner "@vintageayu".
|
||||
Publish as "@openclaw" or rename this package to "@vintageayu/dronzer".
|
||||
```
|
||||
|
||||
To fix it, either choose the owner named by the package scope, or rename the
|
||||
package so the scope matches the owner you can publish as.
|
||||
|
||||
If the package name already has the right scope but the package is owned by the
|
||||
wrong publisher, transfer ownership instead:
|
||||
|
||||
```sh
|
||||
clawhub package transfer @opik/opik-openclaw --to opik
|
||||
```
|
||||
|
||||
Use package transfer only when you have admin access to both the current package
|
||||
owner and the destination publisher. It does not let you publish into a scope you
|
||||
cannot manage.
|
||||
|
||||
This protects org namespaces. A package named `@openclaw/dronzer` claims the
|
||||
`@openclaw` namespace, so only publishers with access to the `@openclaw` owner
|
||||
can publish it.
|
||||
@@ -44,12 +44,11 @@ Quick rule:
|
||||
| `initialize`, `newSession`, `prompt`, `cancel` | Implemented | Core bridge flow over stdio to Gateway chat/send + abort. |
|
||||
| `listSessions`, slash commands | Implemented | Session list works against Gateway session state with bounded cursor pagination and `cwd` filtering where Gateway session rows carry workspace metadata; commands are advertised via `available_commands_update`. |
|
||||
| `resumeSession`, `closeSession` | Implemented | Resume rebinds an ACP session to an existing Gateway session without replaying history. Close cancels active bridge work, resolves pending prompts as cancelled, and releases bridge session state. |
|
||||
| `loadSession` | Partial | Rebinds the ACP session to a Gateway session key and replays ACP event-ledger history for bridge-created sessions. Older/no-ledger sessions fall back to stored user/assistant text. |
|
||||
| `loadSession` | Partial | Rebinds the ACP session to a Gateway session key and replays stored user/assistant text history. Tool/system history is not reconstructed yet. |
|
||||
| Prompt content (`text`, embedded `resource`, images) | Partial | Text/resources are flattened into chat input; images become Gateway attachments. |
|
||||
| Session modes | Partial | `session/set_mode` is supported and the bridge exposes initial Gateway-backed session controls for thought level, tool verbosity, reasoning, usage detail, and elevated actions. Broader ACP-native mode/config surfaces are still out of scope. |
|
||||
| Session info and usage updates | Partial | The bridge emits `session_info_update` and best-effort `usage_update` notifications from cached Gateway session snapshots. Usage is approximate and only sent when Gateway token totals are marked fresh. |
|
||||
| Tool streaming | Partial | `tool_call` / `tool_call_update` events include raw I/O, text content, and best-effort file locations when Gateway tool args/results expose them. Embedded terminals and richer diff-native output are still not exposed. |
|
||||
| Exec approvals | Partial | Gateway exec approval prompts during active ACP prompt turns are relayed to the ACP client with `session/request_permission`. |
|
||||
| Per-session MCP servers (`mcpServers`) | Unsupported | Bridge mode rejects per-session MCP server requests. Configure MCP on the OpenClaw gateway or agent instead. |
|
||||
| Client filesystem methods (`fs/read_text_file`, `fs/write_text_file`) | Unsupported | The bridge does not call ACP client filesystem methods. |
|
||||
| Client terminal methods (`terminal/*`) | Unsupported | The bridge does not create ACP client terminals or stream terminal ids through tool calls. |
|
||||
@@ -57,9 +56,9 @@ Quick rule:
|
||||
|
||||
## Known Limitations
|
||||
|
||||
- `loadSession` can replay complete ACP event-ledger history only for
|
||||
bridge-created sessions. Older/no-ledger sessions still use transcript
|
||||
fallback and do not reconstruct historic tool calls or system notices.
|
||||
- `loadSession` replays stored user and assistant text history, but it does not
|
||||
reconstruct historic tool calls, system notices, or richer ACP-native event
|
||||
types.
|
||||
- If multiple ACP clients share the same Gateway session key, event and cancel
|
||||
routing are best-effort rather than strictly isolated per client. Prefer the
|
||||
default isolated `acp:<uuid>` sessions when you need clean editor-local
|
||||
@@ -77,8 +76,6 @@ Quick rule:
|
||||
- Tool follow-along data is best-effort. The bridge can surface file paths that
|
||||
appear in known tool args/results, but it does not yet emit ACP terminals or
|
||||
structured file diffs.
|
||||
- Exec approval relay is scoped to the active ACP prompt turn; approvals from
|
||||
other Gateway sessions are ignored.
|
||||
|
||||
## Usage
|
||||
|
||||
|
||||
@@ -53,8 +53,6 @@ skipped.
|
||||
|
||||
The archive payload stores file contents from those source trees, and the embedded `manifest.json` records the resolved absolute source paths plus the archive layout used for each asset.
|
||||
|
||||
During archive creation, OpenClaw skips known live-mutation files that do not have restoration value, including active agent session transcripts, cron run logs, rolling logs, delivery queues, socket/pid/temp files under the state directory, and related durable-queue temp files. The JSON result includes `skippedVolatileCount` so automation can see how many files were intentionally omitted.
|
||||
|
||||
Installed plugin source and manifest files under the state directory's
|
||||
`extensions/` tree are included, but their nested `node_modules/` dependency
|
||||
trees are skipped. Those dependencies are rebuildable install artifacts; after
|
||||
|
||||
@@ -170,7 +170,7 @@ configured OpenClaw model. If no configured model is usable yet, it can fall
|
||||
back to local runtimes already present on the machine:
|
||||
|
||||
- Claude Code CLI: `claude-cli/claude-opus-4-7`
|
||||
- Codex app-server harness: `openai/gpt-5.5`
|
||||
- Codex app-server harness: `openai/gpt-5.5` with `agentRuntime.id: "codex"`
|
||||
- Codex CLI: `codex-cli/gpt-5.5`
|
||||
|
||||
The model-assisted planner cannot mutate config directly. It must translate the
|
||||
|
||||
@@ -36,7 +36,7 @@ openclaw daemon uninstall
|
||||
|
||||
- `status`: `--url`, `--token`, `--password`, `--timeout`, `--no-probe`, `--require-rpc`, `--deep`, `--json`
|
||||
- `install`: `--port`, `--runtime <node|bun>`, `--token`, `--force`, `--json`
|
||||
- `restart`: `--safe`, `--skip-deferral`, `--force`, `--wait <duration>`, `--json`
|
||||
- `restart`: `--safe`, `--force`, `--wait <duration>`, `--json`
|
||||
- lifecycle (`uninstall|start|stop`): `--json`
|
||||
|
||||
Notes:
|
||||
@@ -54,7 +54,6 @@ Notes:
|
||||
- On macOS, `install` keeps LaunchAgent plists owner-only and loads managed service environment values through an owner-only file and wrapper instead of serializing API keys or auth-profile env refs into `EnvironmentVariables`.
|
||||
- If you intentionally run multiple gateways on one host, isolate ports, config/state, and workspaces; see [/gateway#multiple-gateways-same-host](/gateway#multiple-gateways-same-host).
|
||||
- `restart --safe` asks the running Gateway to preflight active work and schedule one coalesced restart after active work drains. Plain `restart` keeps the existing service-manager behavior; `--force` remains the immediate override path.
|
||||
- `restart --safe --skip-deferral` runs the OpenClaw-aware safe restart but bypasses the active-work deferral gate so the Gateway emits the restart immediately even when blockers are reported. Operator escape hatch when a stuck task run pins the safe restart; requires `--safe`.
|
||||
|
||||
## Prefer
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ 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.
|
||||
- On Linux, doctor warns when the user's crontab still runs legacy `~/.openclaw/bin/ensure-whatsapp.sh`; that script is no longer maintained and can log false WhatsApp gateway outages when cron lacks the systemd user-bus environment.
|
||||
- When WhatsApp is enabled, doctor checks for a degraded Gateway event loop with local `openclaw-tui` clients still running. `doctor --fix` stops only verified local TUI clients so WhatsApp replies are not queued behind stale TUI refresh loops.
|
||||
- Doctor rewrites legacy `openai-codex/*` model refs to canonical `openai/*` refs across primary models, fallbacks, heartbeat/subagent/compaction overrides, hooks, channel model overrides, and stale session route pins. `--fix` preserves explicit provider/model `agentRuntime` policy, removes stale whole-agent/session runtime pins, and leaves canonical OpenAI agent refs on the default Codex harness when the official OpenAI provider is in use.
|
||||
- Doctor rewrites legacy `openai-codex/*` model refs to canonical `openai/*` refs across primary models, fallbacks, heartbeat/subagent/compaction overrides, hooks, channel model overrides, and stale session route pins. `--fix` selects `agentRuntime.id: "codex"` only when the Codex plugin is installed, enabled, contributes the `codex` harness, and has usable OAuth; otherwise it selects `agentRuntime.id: "pi"` so the route stays on the default OpenClaw runner.
|
||||
- Doctor cleans legacy plugin dependency staging state created by older OpenClaw versions. It also repairs missing downloadable plugins that are referenced by config, such as `plugins.entries`, configured channels, configured provider/search settings, or configured agent runtimes. During package updates, doctor skips package-manager plugin repair until the package swap is complete; rerun `openclaw doctor --fix` afterward if a configured plugin still needs recovery. If the download fails, doctor reports the install error and preserves the configured plugin entry for the next repair attempt.
|
||||
- Doctor repairs stale plugin config by removing missing plugin ids from `plugins.allow`/`plugins.entries`, plus matching dangling channel config, heartbeat targets, and channel model overrides when plugin discovery is healthy.
|
||||
- Doctor quarantines invalid plugin config by disabling the affected `plugins.entries.<id>` entry and removing its invalid `config` payload. Gateway startup already skips only that bad plugin so other plugins and channels can keep running.
|
||||
|
||||
@@ -110,14 +110,11 @@ openclaw gateway run
|
||||
```bash
|
||||
openclaw gateway restart
|
||||
openclaw gateway restart --safe
|
||||
openclaw gateway restart --safe --skip-deferral
|
||||
openclaw gateway restart --force
|
||||
```
|
||||
|
||||
`openclaw gateway restart --safe` asks the running Gateway to preflight active OpenClaw work before restarting. If queued operations, reply delivery, embedded runs, or task runs are active, the Gateway reports the blockers, coalesces duplicate safe restart requests, and restarts once the active work drains. Plain `restart` keeps the existing service-manager behavior for compatibility. Use `--force` only when you explicitly want the immediate override path.
|
||||
|
||||
`openclaw gateway restart --safe --skip-deferral` runs the same OpenClaw-aware coordinated restart as `--safe`, but bypasses the active-work deferral gate so the Gateway emits the restart immediately even when blockers are reported. Use it as the operator escape hatch when a deferral has been pinned by a stuck task run and `--safe` alone would wait indefinitely. `--skip-deferral` requires `--safe`.
|
||||
|
||||
<Warning>
|
||||
Inline `--password` can be exposed in local process listings. Prefer `--password-file`, env, or a SecretRef-backed `gateway.auth.password`.
|
||||
</Warning>
|
||||
@@ -485,17 +482,14 @@ openclaw gateway restart
|
||||
<Accordion title="Command options">
|
||||
- `gateway status`: `--url`, `--token`, `--password`, `--timeout`, `--no-probe`, `--require-rpc`, `--deep`, `--json`
|
||||
- `gateway install`: `--port`, `--runtime <node|bun>`, `--token`, `--wrapper <path>`, `--force`, `--json`
|
||||
- `gateway restart`: `--safe`, `--skip-deferral`, `--force`, `--wait <duration>`, `--json`
|
||||
- `gateway uninstall|start`: `--json`
|
||||
- `gateway stop`: `--disable`, `--json`
|
||||
- `gateway restart`: `--safe`, `--force`, `--wait <duration>`, `--json`
|
||||
- `gateway uninstall|start|stop`: `--json`
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="Lifecycle behavior">
|
||||
- Use `gateway restart` to restart a managed service. Do not chain `gateway stop` and `gateway start` as a restart substitute.
|
||||
- On macOS, `gateway stop` uses `launchctl bootout` by default, which removes the LaunchAgent from the current boot session without persisting a disable — KeepAlive auto-recovery remains active for future crashes and `gateway start` re-enables cleanly without a manual `launchctl enable`. Pass `--disable` to persistently suppress KeepAlive and RunAtLoad so the gateway does not respawn until the next explicit `gateway start`; use this when a manual stop should survive reboots or system restarts.
|
||||
- Use `gateway restart` to restart a managed service. Do not chain `gateway stop` and `gateway start` as a restart substitute; on macOS, `gateway stop` intentionally disables the LaunchAgent before stopping it.
|
||||
- `gateway restart --safe` asks the running Gateway to preflight active OpenClaw work and defer the restart until reply delivery, embedded runs, and task runs drain. `--safe` cannot be combined with `--force` or `--wait`.
|
||||
- `gateway restart --wait 30s` overrides the configured restart drain budget for that restart. Bare numbers are milliseconds; units such as `s`, `m`, and `h` are accepted. `--wait 0` waits indefinitely.
|
||||
- `gateway restart --safe --skip-deferral` runs the OpenClaw-aware safe restart but bypasses the deferral gate so the Gateway emits the restart immediately even when blockers are reported. Operator escape hatch for stuck-task-run deferrals; requires `--safe`.
|
||||
- `gateway restart --force` skips the active-work drain and restarts immediately. Use it when an operator has already inspected the listed task blockers and wants the gateway back now.
|
||||
- Lifecycle commands accept `--json` for scripting.
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ apply across the CLI.
|
||||
| Pairing and channels | [`pairing`](/cli/pairing) · [`qr`](/cli/qr) · [`channels`](/cli/channels) |
|
||||
| Security and plugins | [`security`](/cli/security) · [`secrets`](/cli/secrets) · [`skills`](/cli/skills) · [`plugins`](/cli/plugins) · [`proxy`](/cli/proxy) |
|
||||
| Legacy aliases | [`daemon`](/cli/daemon) (gateway service) · [`clawbot`](/cli/clawbot) (namespace) |
|
||||
| Plugins (optional) | [`path`](/cli/path) · [`voicecall`](/cli/voicecall) (if installed) |
|
||||
| Plugins (optional) | [`voicecall`](/cli/voicecall) (if installed) |
|
||||
|
||||
## Global flags
|
||||
|
||||
@@ -121,12 +121,6 @@ openclaw [--dev] [--profile <name>] <command>
|
||||
status
|
||||
index
|
||||
search
|
||||
path
|
||||
resolve
|
||||
find
|
||||
set
|
||||
validate
|
||||
emit
|
||||
commitments
|
||||
list
|
||||
dismiss
|
||||
|
||||
@@ -126,7 +126,6 @@ This table maps common inference tasks to the corresponding infer command.
|
||||
- `openclaw infer ...` is the primary CLI surface for these workflows.
|
||||
- Use `--json` when the output will be consumed by another command or script.
|
||||
- Use `--provider` or `--model provider/model` when a specific backend is required.
|
||||
- Use `model run --thinking <level>` to pass a one-shot thinking/reasoning level (`off`, `minimal`, `low`, `medium`, `high`, `adaptive`, `xhigh`, or `max`) while keeping the run raw.
|
||||
- For `image describe`, `audio transcribe`, and `video describe`, `--model` must use the form `<provider/model>`.
|
||||
- For `image describe`, an explicit `--model` runs that provider/model directly. The model must be image-capable in the model catalog or provider config. `codex/<model>` runs a bounded Codex app-server image-understanding turn; `openai-codex/<model>` uses the OpenAI Codex OAuth provider path.
|
||||
- Stateless execution commands default to local.
|
||||
@@ -137,7 +136,6 @@ This table maps common inference tasks to the corresponding infer command.
|
||||
- `model run --file` rejects non-image inputs. Use `infer audio transcribe` for audio files and `infer video describe` for video files.
|
||||
- `model run --gateway` exercises Gateway routing, saved auth, provider selection, and the embedded runtime, but still runs as a raw model probe: it sends the supplied prompt and any image attachments without prior session transcript, bootstrap/AGENTS context, context-engine assembly, tools, or bundled MCP servers.
|
||||
- `model run --gateway --model <provider/model>` requires a trusted operator gateway credential because the request asks the Gateway to run a one-off provider/model override.
|
||||
- Local `model run --thinking` uses the lean provider-completion path; provider-specific levels such as `adaptive` and `max` are mapped to the closest portable simple-completion level.
|
||||
|
||||
## Model
|
||||
|
||||
@@ -147,7 +145,6 @@ Use `model` for provider-backed text inference and model/provider inspection.
|
||||
openclaw infer model run --prompt "Reply with exactly: smoke-ok" --json
|
||||
openclaw infer model run --prompt "Summarize this changelog entry" --model openai/gpt-5.4 --json
|
||||
openclaw infer model run --prompt "Describe this image in one sentence" --file ./photo.jpg --model google/gemini-2.5-flash --json
|
||||
openclaw infer model run --prompt "Use more reasoning here" --thinking high --json
|
||||
openclaw infer model providers --json
|
||||
openclaw infer model inspect --name gpt-5.5 --json
|
||||
```
|
||||
@@ -160,7 +157,6 @@ openclaw infer model run --local --model anthropic/claude-sonnet-4-6 --prompt "R
|
||||
openclaw infer model run --local --model cerebras/zai-glm-4.7 --prompt "Reply with exactly: pong" --json
|
||||
openclaw infer model run --local --model google/gemini-2.5-flash --prompt "Reply with exactly: pong" --json
|
||||
openclaw infer model run --local --model groq/llama-3.1-8b-instant --prompt "Reply with exactly: pong" --json
|
||||
openclaw infer model run --local --model mistral/mistral-medium-3-5 --prompt "Reply with exactly: pong" --json
|
||||
openclaw infer model run --local --model mistral/mistral-small-latest --prompt "Reply with exactly: pong" --json
|
||||
openclaw infer model run --local --model openai/gpt-4.1 --prompt "Reply with exactly: pong" --json
|
||||
openclaw infer model run --local --model ollama/qwen2.5vl:7b --prompt "Describe this image." --file ./photo.jpg --json
|
||||
@@ -169,8 +165,6 @@ openclaw infer model run --local --model ollama/qwen2.5vl:7b --prompt "Describe
|
||||
Notes:
|
||||
|
||||
- Local `model run` is the narrowest CLI smoke for provider/model/auth health because, for non-Codex providers, it sends only the supplied prompt to the selected model.
|
||||
- Local `model run --model <provider/model>` can use exact bundled static catalog rows from `models list --all` before that provider is written to config. Provider auth is still required; missing credentials fail as auth errors, not `Unknown model`.
|
||||
- For Mistral Medium 3.5 reasoning probes, leave temperature unset/default. Mistral rejects `reasoning_effort="high"` plus `temperature: 0`; use `mistral/mistral-medium-3-5` with default temperature or a non-zero reasoning-mode value such as `0.7`.
|
||||
- `openai-codex/*` local probes are the narrow exception: OpenClaw adds a minimal system instruction so the Codex Responses transport can populate its required `instructions` field, without adding full agent context, tools, memory, or session transcript.
|
||||
- Local `model run --file` keeps that lean path and attaches image content directly to the single user message. Common image files such as PNG, JPEG, and WebP work when their MIME type is detected as `image/*`; unsupported or unrecognized files fail before the provider is called.
|
||||
- `model run --file` is best when you want to test the selected multimodal text model directly. Use `infer image describe` when you want OpenClaw's image-understanding provider selection and default image-model routing.
|
||||
|
||||
@@ -21,11 +21,9 @@ openclaw migrate list
|
||||
openclaw migrate claude --dry-run
|
||||
openclaw migrate codex --dry-run
|
||||
openclaw migrate codex --skill gog-vault77-google-workspace
|
||||
openclaw migrate codex --plugin google-calendar --dry-run
|
||||
openclaw migrate hermes --dry-run
|
||||
openclaw migrate hermes
|
||||
openclaw migrate apply codex --yes --skill gog-vault77-google-workspace
|
||||
openclaw migrate apply codex --yes --plugin google-calendar
|
||||
openclaw migrate apply codex --yes
|
||||
openclaw migrate apply claude --yes
|
||||
openclaw migrate apply hermes --yes
|
||||
@@ -56,9 +54,6 @@ openclaw onboard --import-from hermes --import-source ~/.hermes
|
||||
<ParamField path="--skill <name>" type="string">
|
||||
Select one skill copy item by skill name or item id. Repeat the flag to migrate multiple skills. When omitted, interactive Codex migrations show a checkbox selector and non-interactive migrations keep all planned skills.
|
||||
</ParamField>
|
||||
<ParamField path="--plugin <name>" type="string">
|
||||
Select one Codex plugin install item by plugin name or item id. Repeat the flag to migrate multiple Codex plugins. When omitted, interactive Codex migrations show a native Codex plugin checkbox selector and non-interactive migrations keep all planned plugins. This only applies to source-installed `openai-curated` Codex plugins discovered by the Codex app-server inventory.
|
||||
</ParamField>
|
||||
<ParamField path="--no-backup" type="boolean">
|
||||
Skip the pre-apply backup. Requires `--force` when local OpenClaw state exists.
|
||||
</ParamField>
|
||||
@@ -123,69 +118,31 @@ launches use per-agent `CODEX_HOME` and `HOME` directories, so they do not read
|
||||
your personal Codex CLI state by default.
|
||||
|
||||
Running `openclaw migrate codex` in an interactive terminal previews the full
|
||||
plan, then opens checkbox selectors before the final apply confirmation. Skill
|
||||
copy items are prompted first. Use `Toggle all on` or `Toggle all off` for bulk
|
||||
selection; planned skills start checked, conflict skills start unchecked, and
|
||||
`Skip for now` skips skill copies for this run while still continuing to plugin
|
||||
selection. When source-installed curated Codex plugins are migratable and
|
||||
`--plugin` was not supplied, migration then prompts for native Codex plugin
|
||||
activation by plugin name. Plugin items
|
||||
start checked unless the target OpenClaw Codex plugin config already has that
|
||||
plugin. Existing target plugins start unchecked and show a conflict hint such as
|
||||
`conflict: plugin exists`; choose `Toggle all off` to migrate no native Codex
|
||||
plugins in that run, or `Skip for now` to stop before applying. For scripted or
|
||||
exact runs, pass `--skill <name>` once per skill, for example:
|
||||
plan, then opens a checkbox selector for skill copy items before the final
|
||||
apply confirmation. Use `Toggle all on` or `Toggle all off` for bulk selection;
|
||||
planned skills start checked, conflict skills start unchecked, and `Skip for now`
|
||||
leaves skills unchanged without applying. For scripted or exact runs, pass
|
||||
`--skill <name>` once per skill, for example:
|
||||
|
||||
```bash
|
||||
openclaw migrate codex --dry-run --skill gog-vault77-google-workspace
|
||||
openclaw migrate apply codex --yes --skill gog-vault77-google-workspace
|
||||
```
|
||||
|
||||
Use `--plugin <name>` to limit native Codex plugin migration non-interactively
|
||||
to one or more source-installed curated plugins:
|
||||
|
||||
```bash
|
||||
openclaw migrate codex --dry-run --plugin google-calendar
|
||||
openclaw migrate apply codex --yes --plugin google-calendar
|
||||
```
|
||||
|
||||
### What Codex imports
|
||||
|
||||
- Codex CLI skill directories under `$CODEX_HOME/skills`, excluding Codex's
|
||||
`.system` cache.
|
||||
- Personal AgentSkills under `$HOME/.agents/skills`, copied into the current
|
||||
OpenClaw agent workspace when you want per-agent ownership.
|
||||
- Source-installed `openai-curated` Codex plugins discovered through Codex
|
||||
app-server `plugin/list`. Apply calls app-server `plugin/install` for each
|
||||
selected plugin, even if the target app-server already reports that plugin as
|
||||
installed and enabled. Migrated Codex plugins are usable only in sessions that
|
||||
select the native Codex harness; they are not exposed to Pi, normal OpenAI
|
||||
provider runs, ACP conversation bindings, or other harnesses.
|
||||
|
||||
### Manual-review Codex state
|
||||
|
||||
Codex `config.toml`, native `hooks/hooks.json`, non-curated marketplaces, and
|
||||
cached plugin bundles that are not source-installed curated plugins are not
|
||||
activated automatically. They are copied or reported in the migration report for
|
||||
manual review.
|
||||
|
||||
For migrated source-installed curated plugins, apply writes:
|
||||
|
||||
- `plugins.entries.codex.enabled: true`
|
||||
- `plugins.entries.codex.config.codexPlugins.enabled: true`
|
||||
- `plugins.entries.codex.config.codexPlugins.allow_destructive_actions: false`
|
||||
- one explicit plugin entry with `marketplaceName: "openai-curated"` and
|
||||
`pluginName` for each selected plugin
|
||||
|
||||
Migration never writes `plugins["*"]` and never stores local marketplace cache
|
||||
paths. Auth-required installs are reported on the affected plugin item with
|
||||
`status: "skipped"`, `reason: "auth_required"`, and sanitized app identifiers.
|
||||
Their explicit config entries are written disabled until you reauthorize and
|
||||
enable them. Other install failures are item-scoped `error` results.
|
||||
|
||||
If Codex app-server plugin inventory is unavailable during planning, migration
|
||||
falls back to cached bundle advisory items instead of failing the whole
|
||||
migration.
|
||||
Codex native plugins, `config.toml`, and native `hooks/hooks.json` are not
|
||||
activated automatically. Plugins may expose MCP servers, apps, hooks, or other
|
||||
executable behavior, so the provider reports them for review instead of loading
|
||||
them into OpenClaw. Config and hook files are copied into the migration report
|
||||
for manual review.
|
||||
|
||||
## Hermes provider
|
||||
|
||||
|
||||
450
docs/cli/path.md
450
docs/cli/path.md
@@ -1,450 +0,0 @@
|
||||
---
|
||||
summary: "CLI reference for `openclaw path` (inspect and edit workspace files via the `oc://` addressing scheme)"
|
||||
read_when:
|
||||
- You want to read or write a leaf inside a workspace file from the terminal
|
||||
- You're scripting against workspace state and want a stable, kind-agnostic addressing scheme
|
||||
- You're debugging a `oc://` path (validate the syntax, see what it resolves to)
|
||||
title: "Path"
|
||||
---
|
||||
|
||||
# `openclaw path`
|
||||
|
||||
Plugin-provided shell access to the `oc://` addressing substrate: one
|
||||
kind-dispatched path scheme for inspecting and editing addressable workspace
|
||||
files (markdown, jsonc, jsonl). Self-hosters, plugin authors, and editor
|
||||
extensions use it to read, find, or update a narrow location without
|
||||
hand-rolling per-file parsers.
|
||||
|
||||
The CLI mirrors the substrate's public verbs:
|
||||
|
||||
- `resolve` is concrete and single-match.
|
||||
- `find` is the multi-match verb for wildcards, unions, predicates, and
|
||||
positional expansion.
|
||||
- `set` only accepts concrete paths or insertion markers; wildcard patterns are
|
||||
rejected before writing.
|
||||
|
||||
`path` is provided by the bundled optional `oc-path` plugin. Enable it before
|
||||
first use:
|
||||
|
||||
```bash
|
||||
openclaw plugins enable oc-path
|
||||
```
|
||||
|
||||
## Why use it
|
||||
|
||||
OpenClaw state is spread across human-edited markdown, commented JSONC config,
|
||||
and append-only JSONL logs. Shell scripts, hooks, and agents often need one
|
||||
small value from those files: a frontmatter key, a plugin setting, a log record
|
||||
field, or a bullet item under a named section.
|
||||
|
||||
`openclaw path` gives those callers a stable address instead of a one-off grep,
|
||||
regex, or parser for each file kind. The same `oc://` path can be validated,
|
||||
resolved, searched, dry-run, and written from the terminal, which makes narrow
|
||||
automation easier to review and safer to replay. It is especially useful when
|
||||
you want to update one leaf while preserving the rest of the file's comments,
|
||||
line endings, and surrounding formatting.
|
||||
|
||||
Use it when the thing you want has a logical address, but the physical file
|
||||
shape varies:
|
||||
|
||||
- A hook wants to read one setting from commented JSONC without losing comments
|
||||
when it writes the value back.
|
||||
- A maintenance script wants to find every matching event field in a JSONL log
|
||||
without loading the whole log into a custom parser.
|
||||
- An editor extension wants to jump to a markdown section or bullet item by
|
||||
slug, then render the exact line it resolved to.
|
||||
- An agent wants to dry-run a tiny workspace edit before applying it, with the
|
||||
changed bytes visible in review.
|
||||
|
||||
You probably do not need `openclaw path` for ordinary whole-file edits, rich
|
||||
config migrations, or memory-specific writes. Those should use the owner
|
||||
command or plugin. `path` is for small, addressable file operations where a
|
||||
repeatable terminal command is clearer than another bespoke parser.
|
||||
|
||||
## How it is used
|
||||
|
||||
Read one value from a human-edited config file:
|
||||
|
||||
```bash
|
||||
openclaw path resolve 'oc://config.jsonc/plugins/github/enabled'
|
||||
```
|
||||
|
||||
Preview a write without touching disk:
|
||||
|
||||
```bash
|
||||
openclaw path set 'oc://config.jsonc/plugins/github/enabled' 'true' --dry-run
|
||||
```
|
||||
|
||||
Find matching records in an append-only JSONL log:
|
||||
|
||||
```bash
|
||||
openclaw path find 'oc://session.jsonl/[event=tool_call]/name'
|
||||
```
|
||||
|
||||
Address an instruction in markdown by section and item instead of by line
|
||||
number:
|
||||
|
||||
```bash
|
||||
openclaw path resolve 'oc://AGENTS.md/runtime-safety/openclaw-gateway'
|
||||
```
|
||||
|
||||
Validate a path in CI or a preflight script before the script reads or writes:
|
||||
|
||||
```bash
|
||||
openclaw path validate 'oc://AGENTS.md/tools/$last/risk'
|
||||
```
|
||||
|
||||
Those commands are meant to be copyable into shell scripts. Use `--json` when a
|
||||
caller needs structured output and `--human` when a person is inspecting the
|
||||
result.
|
||||
|
||||
## How it works
|
||||
|
||||
`openclaw path` does four things:
|
||||
|
||||
1. Parses the `oc://` address into slots: file, section, item, field, and
|
||||
optional session.
|
||||
2. Chooses the file-kind adapter from the target extension (`.md`, `.jsonc`,
|
||||
`.jsonl`, and related aliases).
|
||||
3. Resolves the slots against that file kind's AST: markdown headings/items,
|
||||
JSONC object keys/array indexes, or JSONL line records.
|
||||
4. For `set`, emits edited bytes through the same adapter so the untouched
|
||||
parts of the file keep their comments, line endings, and nearby formatting
|
||||
where the kind supports it.
|
||||
|
||||
`resolve` and `set` require one concrete target. `find` is the exploratory
|
||||
verb: it expands wildcards, unions, predicates, and ordinals into the concrete
|
||||
matches you can inspect before choosing one to write.
|
||||
|
||||
## Subcommands
|
||||
|
||||
| Subcommand | Purpose |
|
||||
| ----------------------- | ---------------------------------------------------------------------------- |
|
||||
| `resolve <oc-path>` | Print the concrete match at the path (or "not found"). |
|
||||
| `find <pattern>` | Enumerate matches for a wildcard / union / predicate path. |
|
||||
| `set <oc-path> <value>` | Write a leaf or insertion target at a concrete path. Supports `--dry-run`. |
|
||||
| `validate <oc-path>` | Parse-only; print structural breakdown (file / section / item / field). |
|
||||
| `emit <file>` | Round-trip a file through `parseXxx` + `emitXxx` (byte-fidelity diagnostic). |
|
||||
|
||||
## Global flags
|
||||
|
||||
| Flag | Purpose |
|
||||
| --------------- | ------------------------------------------------------------------------ |
|
||||
| `--cwd <dir>` | Resolve the file slot against this directory (default: `process.cwd()`). |
|
||||
| `--file <path>` | Override the file slot's resolved path (absolute access). |
|
||||
| `--json` | Force JSON output (default when stdout is not a TTY). |
|
||||
| `--human` | Force human output (default when stdout is a TTY). |
|
||||
| `--dry-run` | (only on `set`) print the bytes that would be written without writing. |
|
||||
|
||||
## `oc://` syntax
|
||||
|
||||
```
|
||||
oc://FILE/SECTION/ITEM/FIELD?session=SCOPE
|
||||
```
|
||||
|
||||
Slot rules: `field` requires `item`, and `item` requires `section`. Across all
|
||||
four slots:
|
||||
|
||||
- **Quoted segments** — `"a/b.c"` survives `/` and `.` separators.
|
||||
Content is byte-literal; `"` and `\` are not allowed inside quotes.
|
||||
The file slot is also quote-aware: `oc://"skills/email-drafter"/Tools/$last`
|
||||
treats `skills/email-drafter` as a single file path.
|
||||
- **Predicates** — `[k=v]`, `[k!=v]`, `[k<v]`, `[k<=v]`, `[k>v]`,
|
||||
`[k>=v]`. Numeric ops require both sides to coerce to finite numbers.
|
||||
- **Unions** — `{a,b,c}` matches any of the alternatives.
|
||||
- **Wildcards** — `*` (single sub-segment) and `**` (zero-or-more,
|
||||
recursive). `find` accepts these; `resolve` and `set` reject them as
|
||||
ambiguous.
|
||||
- **Positional** — `$last` resolves to the last index / last-declared key.
|
||||
- **Ordinal** — `#N` for Nth match by document order.
|
||||
- **Insertion markers** — `+`, `+key`, `+nnn` for keyed / indexed
|
||||
insertion (use with `set`).
|
||||
- **Session scope** — `?session=cron-daily` etc. Orthogonal to slot
|
||||
nesting. Session values are raw, not percent-decoded; they may not contain
|
||||
control characters or reserved query delimiters (`?`, `&`, `%`).
|
||||
|
||||
Reserved characters (`?`, `&`, `%`) outside quoted, predicate, or union
|
||||
segments are rejected. Control characters (U+0000-U+001F, U+007F) are rejected
|
||||
anywhere, including the `session` query value.
|
||||
|
||||
`formatOcPath(parseOcPath(path)) === path` is guaranteed for canonical paths.
|
||||
Non-canonical query parameters are ignored except for the first non-empty
|
||||
`session=` value.
|
||||
|
||||
## Addressing by file kind
|
||||
|
||||
| Kind | Addressing model |
|
||||
| ---------- | ----------------------------------------------------------------------------------------- |
|
||||
| Markdown | H2 sections by slug, bullet items by slug or `#N`, frontmatter via `[frontmatter]`. |
|
||||
| JSONC/JSON | Object keys and array indexes; dots split nested sub-segments unless quoted. |
|
||||
| JSONL | Top-level line addresses (`L1`, `L2`, `$last`), then JSONC-style descent inside the line. |
|
||||
|
||||
`resolve` returns a structured match: `root`, `node`, `leaf`, or
|
||||
`insertion-point`, with a 1-based line number. Leaf values are surfaced as text
|
||||
plus a `leafType` so plugin authors can render previews without depending on
|
||||
the per-kind AST shape.
|
||||
|
||||
## Mutation contract
|
||||
|
||||
`set` writes one concrete target:
|
||||
|
||||
- Markdown frontmatter values and `- key: value` item fields are string leaves.
|
||||
Markdown insertions append sections, frontmatter keys, or section items and
|
||||
render a canonical markdown shape for the changed file.
|
||||
- JSONC leaf writes coerce the string value to the existing leaf type
|
||||
(`string`, finite `number`, `true`/`false`, or `null`). JSONC object and array
|
||||
insertions parse `<value>` as JSON and use the `jsonc-parser` edit path for
|
||||
ordinary leaf writes, preserving comments and nearby formatting.
|
||||
- JSONL leaf writes coerce like JSONC inside a line. Whole-line replacement and
|
||||
append parse `<value>` as JSON. Rendered JSONL preserves the file's dominant
|
||||
LF/CRLF line-ending convention.
|
||||
|
||||
Use `--dry-run` before user-visible writes when the exact bytes matter. The
|
||||
substrate preserves byte-identical output for parse/emit round-trips, but a
|
||||
mutation can canonicalize the edited region or file depending on kind.
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
# Validate a path (no filesystem access)
|
||||
openclaw path validate 'oc://AGENTS.md/Tools/$last/risk'
|
||||
|
||||
# Read a leaf
|
||||
openclaw path resolve 'oc://gateway.jsonc/version'
|
||||
|
||||
# Wildcard search
|
||||
openclaw path find 'oc://session.jsonl/*/event' --file ./logs/session.jsonl
|
||||
|
||||
# Dry-run a write
|
||||
openclaw path set 'oc://gateway.jsonc/version' '2.0' --dry-run
|
||||
|
||||
# Apply the write
|
||||
openclaw path set 'oc://gateway.jsonc/version' '2.0'
|
||||
|
||||
# Byte-fidelity round-trip (diagnostic)
|
||||
openclaw path emit ./AGENTS.md
|
||||
```
|
||||
|
||||
More grammar examples:
|
||||
|
||||
```bash
|
||||
# Quote keys containing / or .
|
||||
openclaw path resolve 'oc://config.jsonc/agents.defaults.models/"anthropic/claude-opus-4-7"/alias'
|
||||
|
||||
# Predicate search over JSONC children
|
||||
openclaw path find 'oc://config.jsonc/plugins/[enabled=true]/id'
|
||||
|
||||
# Insert into a JSONC array
|
||||
openclaw path set 'oc://config.jsonc/items/+1' '{"id":"new","enabled":true}' --dry-run
|
||||
|
||||
# Insert a JSONC object key
|
||||
openclaw path set 'oc://config.jsonc/plugins/+github' '{"enabled":true}' --dry-run
|
||||
|
||||
# Append a JSONL event
|
||||
openclaw path set 'oc://session.jsonl/+' '{"event":"checkpoint","ok":true}' --file ./logs/session.jsonl
|
||||
|
||||
# Resolve the last JSONL value line
|
||||
openclaw path resolve 'oc://session.jsonl/$last/event' --file ./logs/session.jsonl
|
||||
|
||||
# Address markdown frontmatter
|
||||
openclaw path resolve 'oc://AGENTS.md/[frontmatter]/name'
|
||||
|
||||
# Insert markdown frontmatter
|
||||
openclaw path set 'oc://AGENTS.md/[frontmatter]/+description' 'Agent instructions' --dry-run
|
||||
|
||||
# Find markdown item fields
|
||||
openclaw path find 'oc://SKILL.md/Tools/*/send_email'
|
||||
|
||||
# Validate a session-scoped path
|
||||
openclaw path validate 'oc://AGENTS.md/Tools/$last/risk?session=cron-daily'
|
||||
```
|
||||
|
||||
## Recipes by file kind
|
||||
|
||||
The same five verbs work across kinds; the addressing scheme dispatches on the
|
||||
file extension. The examples below use the fixtures from the PR description.
|
||||
|
||||
### Markdown
|
||||
|
||||
```text
|
||||
<!-- frontmatter.md -->
|
||||
---
|
||||
name: drafter
|
||||
description: email drafting agent
|
||||
tier: core
|
||||
---
|
||||
## Tools
|
||||
- gh: GitHub CLI
|
||||
- curl: HTTP client
|
||||
- send_email: enabled
|
||||
```
|
||||
|
||||
```bash
|
||||
$ openclaw path resolve 'oc://x.md/[frontmatter]/tier' --file frontmatter.md --human
|
||||
leaf @ L4: "core" (string)
|
||||
|
||||
$ openclaw path resolve 'oc://x.md/tools/gh/gh' --file frontmatter.md --human
|
||||
leaf @ L9: "GitHub CLI" (string)
|
||||
|
||||
$ openclaw path find 'oc://x.md/tools/*' --file frontmatter.md --human
|
||||
3 matches for oc://x.md/tools/*:
|
||||
oc://x.md/tools/gh → node @ L9 [md-item]
|
||||
oc://x.md/tools/curl → node @ L10 [md-item]
|
||||
oc://x.md/tools/send-email → node @ L11 [md-item]
|
||||
```
|
||||
|
||||
The `[frontmatter]` predicate addresses the YAML frontmatter block; `tools`
|
||||
matches the `## Tools` heading via slug, and item leaves keep their slug form
|
||||
even when the source uses underscores (`send_email` → `send-email`).
|
||||
|
||||
### JSONC
|
||||
|
||||
```text
|
||||
// config.jsonc
|
||||
{
|
||||
"plugins": {
|
||||
"github": {"enabled": true, "role": "vcs"},
|
||||
"slack": {"enabled": false, "role": "chat"}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```bash
|
||||
$ openclaw path resolve 'oc://config.jsonc/plugins/github/enabled' --file config.jsonc --human
|
||||
leaf @ L4: "true" (boolean)
|
||||
|
||||
$ openclaw path set 'oc://config.jsonc/plugins/slack/enabled' 'true' --file config.jsonc --dry-run
|
||||
--dry-run: would write 142 bytes to /…/config.jsonc
|
||||
{
|
||||
"plugins": {
|
||||
"github": {"enabled": true, "role": "vcs"},
|
||||
"slack": {"enabled": true, "role": "chat"}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
JSONC edits go through `jsonc-parser`, so comments and whitespace survive a
|
||||
`set`. Run with `--dry-run` first to inspect the bytes before committing.
|
||||
|
||||
### JSONL
|
||||
|
||||
```text
|
||||
{"event":"start","userId":"u1","ts":1}
|
||||
{"event":"action","userId":"u1","ts":2}
|
||||
{"event":"end","userId":"u1","ts":3}
|
||||
```
|
||||
|
||||
```bash
|
||||
$ openclaw path find 'oc://session.jsonl/[event=action]/userId' --file session.jsonl --human
|
||||
1 match for oc://session.jsonl/[event=action]/userId:
|
||||
oc://session.jsonl/L2/userId → leaf @ L2: "u1" (string)
|
||||
|
||||
$ openclaw path resolve 'oc://session.jsonl/L2/ts' --file session.jsonl --human
|
||||
leaf @ L2: "2" (number)
|
||||
```
|
||||
|
||||
Each line is a record. Address by predicate (`[event=action]`) when you do not
|
||||
know the line number, or by the canonical `LN` segment when you do.
|
||||
|
||||
## Subcommand reference
|
||||
|
||||
### `resolve <oc-path>`
|
||||
|
||||
Read a single leaf or node. Wildcards are rejected — use `find` for those.
|
||||
Exits `0` on a match, `1` on a clean miss, `2` on a parse error or refused
|
||||
pattern.
|
||||
|
||||
```bash
|
||||
openclaw path resolve 'oc://AGENTS.md/tools/gh/risk' --human
|
||||
openclaw path resolve 'oc://gateway.jsonc/server/port' --json
|
||||
```
|
||||
|
||||
### `find <pattern>`
|
||||
|
||||
Enumerate every match for a wildcard / predicate / union pattern. Exits `0`
|
||||
on at least one match, `1` on zero. File-slot wildcards are rejected with
|
||||
`OC_PATH_FILE_WILDCARD_UNSUPPORTED` — pass a concrete file (multi-file
|
||||
globbing is a follow-up feature).
|
||||
|
||||
```bash
|
||||
openclaw path find 'oc://AGENTS.md/tools/**/risk'
|
||||
openclaw path find 'oc://session.jsonl/[event=action]/userId'
|
||||
openclaw path find 'oc://config.jsonc/plugins/{github,slack}/enabled'
|
||||
```
|
||||
|
||||
### `set <oc-path> <value>`
|
||||
|
||||
Write a leaf. Pair with `--dry-run` to preview the bytes that would be
|
||||
written without touching the file. Exits `0` on a successful write, `1` if
|
||||
the substrate refuses (for example, a sentinel guard hit), `2` on parse
|
||||
errors.
|
||||
|
||||
```bash
|
||||
openclaw path set 'oc://gateway.jsonc/version' '2.0' --dry-run
|
||||
openclaw path set 'oc://gateway.jsonc/version' '2.0'
|
||||
openclaw path set 'oc://AGENTS.md/Tools/+gh/risk' 'low'
|
||||
```
|
||||
|
||||
The `+key` insertion marker creates the named child if it does not already
|
||||
exist; `+nnn` and bare `+` work for indexed and append insertion respectively.
|
||||
|
||||
### `validate <oc-path>`
|
||||
|
||||
Parse-only check. No filesystem access. Useful when you want to confirm a
|
||||
template path is well-formed before substituting variables, or when you want
|
||||
the structural breakdown for debugging:
|
||||
|
||||
```bash
|
||||
$ openclaw path validate 'oc://AGENTS.md/tools/gh' --human
|
||||
valid: oc://AGENTS.md/tools/gh
|
||||
file: AGENTS.md
|
||||
section: tools
|
||||
item: gh
|
||||
```
|
||||
|
||||
Exits `0` when valid, `1` when invalid (with a structured `code` and
|
||||
`message`), `2` on argument errors.
|
||||
|
||||
### `emit <file>`
|
||||
|
||||
Round-trip a file through the per-kind parser and emitter. The output should
|
||||
be byte-identical to the input on a sound file — divergence indicates a
|
||||
parser bug or a sentinel hit. Useful for debugging substrate behavior on
|
||||
real-world inputs.
|
||||
|
||||
```bash
|
||||
openclaw path emit ./AGENTS.md
|
||||
openclaw path emit ./gateway.jsonc --json
|
||||
```
|
||||
|
||||
## Exit codes
|
||||
|
||||
| Code | Meaning |
|
||||
| ---- | -------------------------------------------------------------------------- |
|
||||
| `0` | Success. (`resolve` / `find`: at least one match. `set`: write succeeded.) |
|
||||
| `1` | No match, or `set` rejected by the substrate (no system-level error). |
|
||||
| `2` | Argument or parse error. |
|
||||
|
||||
## Output mode
|
||||
|
||||
`openclaw path` is TTY-aware: human-readable output on a terminal, JSON when
|
||||
stdout is piped or redirected. `--json` and `--human` override the
|
||||
auto-detection.
|
||||
|
||||
## Notes
|
||||
|
||||
- `set` writes bytes through the substrate's emit path, which applies the
|
||||
redaction-sentinel guard automatically. A leaf carrying
|
||||
`__OPENCLAW_REDACTED__` (verbatim or as a substring) is refused at write
|
||||
time.
|
||||
- JSONC parsing and leaf edits use the plugin-local `jsonc-parser`
|
||||
dependency, so comments and formatting are preserved on ordinary leaf
|
||||
writes instead of going through a hand-rolled parser/re-render path.
|
||||
- `path` does not know about LKG. If the file is LKG-tracked, the next
|
||||
observe call decides whether to promote / recover. `set --batch` for
|
||||
atomic multi-set through the LKG promote/recover lifecycle is planned
|
||||
alongside the LKG-recovery substrate.
|
||||
|
||||
## Related
|
||||
|
||||
- [CLI reference](/cli)
|
||||
@@ -90,10 +90,6 @@ openclaw plugins install <plugin> --marketplace <name> # marketplace (explicit)
|
||||
openclaw plugins install <plugin> --marketplace https://github.com/<owner>/<repo>
|
||||
```
|
||||
|
||||
Maintainers testing setup-time installs can override automatic plugin install
|
||||
sources with guarded environment variables. See
|
||||
[Plugin install overrides](/plugins/install-overrides).
|
||||
|
||||
<Warning>
|
||||
Bare package names install from npm by default during the launch cutover. Use `clawhub:<package>` for ClawHub. Treat plugin installs like running code. Prefer pinned versions.
|
||||
</Warning>
|
||||
@@ -133,7 +129,7 @@ is available, then fall back to `latest`.
|
||||
|
||||
This CLI flag applies to plugin install/update flows. Gateway-backed skill dependency installs use the matching `dangerouslyForceUnsafeInstall` request override, while `openclaw skills install` remains a separate ClawHub skill download/install flow.
|
||||
|
||||
If a plugin you published on ClawHub is blocked by a registry scan, use the publisher steps in [ClawHub](/clawhub/security).
|
||||
If a plugin you published on ClawHub is blocked by a registry scan, use the publisher steps in [ClawHub](/tools/clawhub).
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="Hook packs and npm specs">
|
||||
@@ -421,4 +417,4 @@ Marketplace list accepts a local marketplace path, a `marketplace.json` path, a
|
||||
|
||||
- [Building plugins](/plugins/building-plugins)
|
||||
- [CLI reference](/cli)
|
||||
- [ClawHub](/clawhub)
|
||||
- [Community plugins](/plugins/community)
|
||||
|
||||
@@ -33,7 +33,7 @@ It also emits `security.trust_model.multi_user_heuristic` when config suggests l
|
||||
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.token` is short, when `hooks.path="/"`, when `hooks.defaultSessionKey` is unset, when `hooks.allowedAgentIds` is unrestricted, 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 write/edit tools are disabled but `exec` is still available without a constraining sandbox filesystem boundary, when open groups expose runtime/filesystem tools without sandbox/workspace guards, and when installed plugin tools may be reachable under permissive tool policy.
|
||||
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 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`.
|
||||
It also flags dangerous sandbox Docker network modes (including `host` and `container:*` namespace joins).
|
||||
|
||||
@@ -15,7 +15,7 @@ Related:
|
||||
|
||||
- Skills system: [Skills](/tools/skills)
|
||||
- Skills config: [Skills config](/tools/skills-config)
|
||||
- ClawHub installs: [ClawHub](/clawhub/cli)
|
||||
- ClawHub installs: [ClawHub](/tools/clawhub)
|
||||
|
||||
## Commands
|
||||
|
||||
|
||||
@@ -332,16 +332,12 @@ flowchart LR
|
||||
I --> M["Main Reply"]
|
||||
```
|
||||
|
||||
The blocking memory sub-agent can use only the configured memory recall tools.
|
||||
By default that is:
|
||||
The blocking memory sub-agent can use only the available memory recall tools:
|
||||
|
||||
- `memory_recall`
|
||||
- `memory_search`
|
||||
- `memory_get`
|
||||
|
||||
When `plugins.slots.memory` is `memory-lancedb`, the default is `memory_recall`
|
||||
instead. Set `config.toolsAllow` when another memory provider exposes a
|
||||
different recall tool contract.
|
||||
|
||||
If the connection is weak, it should return `NONE`.
|
||||
|
||||
## Query modes
|
||||
@@ -466,110 +462,6 @@ skips recall for that turn.
|
||||
`config.modelFallbackPolicy` is retained only as a deprecated compatibility
|
||||
field for older configs. It no longer changes runtime behavior.
|
||||
|
||||
## Memory tools
|
||||
|
||||
By default Active Memory lets the blocking recall sub-agent call
|
||||
`memory_search` and `memory_get`. That matches the built-in `memory-core`
|
||||
contract. When `plugins.slots.memory` selects `memory-lancedb` and
|
||||
`config.toolsAllow` is unset, Active Memory keeps the existing LanceDB behavior
|
||||
and uses `memory_recall` instead.
|
||||
|
||||
If you use another memory plugin, set `config.toolsAllow` to the exact tool
|
||||
names that plugin registers. Active Memory lists those tools in the recall
|
||||
prompt and passes the same list to the embedded sub-agent. If none of the
|
||||
configured tools are available, or the memory sub-agent fails, Active Memory
|
||||
skips recall for that turn and the main reply continues without memory context.
|
||||
`toolsAllow` only accepts concrete memory tool names. Wildcards, `group:*`
|
||||
entries, and core agent tools such as `read`, `exec`, `message`, and
|
||||
`web_search` are ignored before the hidden memory sub-agent starts.
|
||||
|
||||
Default-behavior note: Active Memory no longer includes `memory_recall` in the
|
||||
memory-core default allowlist. Existing `memory-lancedb` setups keep working
|
||||
when `plugins.slots.memory` is set to `memory-lancedb`. Explicit `toolsAllow`
|
||||
always overrides the automatic default.
|
||||
|
||||
### Built-in memory-core
|
||||
|
||||
The default setup does not need an explicit `toolsAllow`:
|
||||
|
||||
```json5
|
||||
{
|
||||
plugins: {
|
||||
entries: {
|
||||
"active-memory": {
|
||||
enabled: true,
|
||||
config: {
|
||||
agents: ["main"],
|
||||
// Default: ["memory_search", "memory_get"]
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### LanceDB memory
|
||||
|
||||
The bundled `memory-lancedb` plugin exposes `memory_recall`. Selecting the
|
||||
memory slot is enough for Active Memory to use that recall tool:
|
||||
|
||||
```json5
|
||||
{
|
||||
plugins: {
|
||||
slots: {
|
||||
memory: "memory-lancedb",
|
||||
},
|
||||
entries: {
|
||||
"memory-lancedb": {
|
||||
enabled: true,
|
||||
config: {
|
||||
embedding: {
|
||||
provider: "openai",
|
||||
model: "text-embedding-3-small",
|
||||
},
|
||||
},
|
||||
},
|
||||
"active-memory": {
|
||||
enabled: true,
|
||||
config: {
|
||||
agents: ["main"],
|
||||
promptAppend: "Use memory_recall for long-term user preferences, past decisions, and previously discussed topics. If recall finds nothing useful, return NONE.",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### Lossless Claw
|
||||
|
||||
Lossless Claw is a context-engine plugin with its own recall tools. Install and
|
||||
configure it as a context engine first; see [Context engine](/concepts/context-engine).
|
||||
Then let Active Memory use the Lossless Claw recall tools:
|
||||
|
||||
```json5
|
||||
{
|
||||
plugins: {
|
||||
entries: {
|
||||
"lossless-claw": {
|
||||
enabled: true,
|
||||
},
|
||||
"active-memory": {
|
||||
enabled: true,
|
||||
config: {
|
||||
agents: ["main"],
|
||||
toolsAllow: ["lcm_grep", "lcm_describe", "lcm_expand_query"],
|
||||
promptAppend: "Use lcm_grep first for compacted conversation recall. Use lcm_describe to inspect a specific summary. Use lcm_expand_query only when the latest user message needs exact details that may have been compacted away. Return NONE if the retrieved context is not clearly useful.",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Do not include `lcm_expand` in `toolsAllow` for the main Active Memory sub-agent.
|
||||
Lossless Claw uses that as a lower-level delegated expansion tool.
|
||||
|
||||
## Advanced escape hatches
|
||||
|
||||
These options are intentionally not part of the recommended setup.
|
||||
@@ -596,9 +488,6 @@ Memory prompt and before the conversation context:
|
||||
promptAppend: "Prefer stable long-term preferences over one-off events."
|
||||
```
|
||||
|
||||
Use `promptAppend` with custom `toolsAllow` when a non-core memory plugin needs
|
||||
provider-specific tool order or query-shaping instructions.
|
||||
|
||||
`config.promptOverride` replaces the default Active Memory prompt. OpenClaw
|
||||
still appends the conversation context afterward:
|
||||
|
||||
@@ -669,26 +558,25 @@ plugins.entries.active-memory
|
||||
|
||||
The most important fields are:
|
||||
|
||||
| Key | Type | Meaning |
|
||||
| ---------------------------- | ---------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `enabled` | `boolean` | Enables the plugin itself |
|
||||
| `config.agents` | `string[]` | Agent ids that may use active memory |
|
||||
| `config.model` | `string` | Optional blocking memory sub-agent model ref; when unset, active memory uses the current session model |
|
||||
| `config.allowedChatTypes` | `("direct" \| "group" \| "channel")[]` | Session types that may run Active Memory; defaults to direct-message style sessions |
|
||||
| `config.allowedChatIds` | `string[]` | Optional per-conversation allowlist applied after `allowedChatTypes`; non-empty lists fail closed |
|
||||
| `config.deniedChatIds` | `string[]` | Optional per-conversation denylist that overrides allowed session types and allowed ids |
|
||||
| `config.queryMode` | `"message" \| "recent" \| "full"` | Controls how much conversation the blocking memory sub-agent sees |
|
||||
| `config.promptStyle` | `"balanced" \| "strict" \| "contextual" \| "recall-heavy" \| "precision-heavy" \| "preference-only"` | Controls how eager or strict the blocking memory sub-agent is when deciding whether to return memory |
|
||||
| `config.toolsAllow` | `string[]` | Concrete memory tool names the blocking memory sub-agent may call; defaults to `["memory_search", "memory_get"]`, or `["memory_recall"]` when `plugins.slots.memory` is `memory-lancedb`; wildcards, `group:*` entries, and core agent tools are ignored |
|
||||
| `config.thinking` | `"off" \| "minimal" \| "low" \| "medium" \| "high" \| "xhigh" \| "adaptive" \| "max"` | Advanced thinking override for the blocking memory sub-agent; default `off` for speed |
|
||||
| `config.promptOverride` | `string` | Advanced full prompt replacement; not recommended for normal use |
|
||||
| `config.promptAppend` | `string` | Advanced extra instructions appended to the default or overridden prompt |
|
||||
| `config.timeoutMs` | `number` | Hard timeout for the blocking memory sub-agent, capped at 120000 ms |
|
||||
| `config.setupGraceTimeoutMs` | `number` | Advanced extra setup budget before the recall timeout expires; defaults to 0 and is capped at 30000 ms. See [Cold-start grace](#cold-start-grace) for v2026.4.x upgrade guidance |
|
||||
| `config.maxSummaryChars` | `number` | Maximum total characters allowed in the active-memory summary |
|
||||
| `config.logging` | `boolean` | Emits active memory logs while tuning |
|
||||
| `config.persistTranscripts` | `boolean` | Keeps blocking memory sub-agent transcripts on disk instead of deleting temp files |
|
||||
| `config.transcriptDir` | `string` | Relative blocking memory sub-agent transcript directory under the agent sessions folder |
|
||||
| Key | Type | Meaning |
|
||||
| ---------------------------- | ---------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `enabled` | `boolean` | Enables the plugin itself |
|
||||
| `config.agents` | `string[]` | Agent ids that may use active memory |
|
||||
| `config.model` | `string` | Optional blocking memory sub-agent model ref; when unset, active memory uses the current session model |
|
||||
| `config.allowedChatTypes` | `("direct" \| "group" \| "channel")[]` | Session types that may run Active Memory; defaults to direct-message style sessions |
|
||||
| `config.allowedChatIds` | `string[]` | Optional per-conversation allowlist applied after `allowedChatTypes`; non-empty lists fail closed |
|
||||
| `config.deniedChatIds` | `string[]` | Optional per-conversation denylist that overrides allowed session types and allowed ids |
|
||||
| `config.queryMode` | `"message" \| "recent" \| "full"` | Controls how much conversation the blocking memory sub-agent sees |
|
||||
| `config.promptStyle` | `"balanced" \| "strict" \| "contextual" \| "recall-heavy" \| "precision-heavy" \| "preference-only"` | Controls how eager or strict the blocking memory sub-agent is when deciding whether to return memory |
|
||||
| `config.thinking` | `"off" \| "minimal" \| "low" \| "medium" \| "high" \| "xhigh" \| "adaptive" \| "max"` | Advanced thinking override for the blocking memory sub-agent; default `off` for speed |
|
||||
| `config.promptOverride` | `string` | Advanced full prompt replacement; not recommended for normal use |
|
||||
| `config.promptAppend` | `string` | Advanced extra instructions appended to the default or overridden prompt |
|
||||
| `config.timeoutMs` | `number` | Hard timeout for the blocking memory sub-agent, capped at 120000 ms |
|
||||
| `config.setupGraceTimeoutMs` | `number` | Advanced extra setup budget before the recall timeout expires; defaults to 0 and is capped at 30000 ms. See [Cold-start grace](#cold-start-grace) for v2026.4.x upgrade guidance |
|
||||
| `config.maxSummaryChars` | `number` | Maximum total characters allowed in the active-memory summary |
|
||||
| `config.logging` | `boolean` | Emits active memory logs while tuning |
|
||||
| `config.persistTranscripts` | `boolean` | Keeps blocking memory sub-agent transcripts on disk instead of deleting temp files |
|
||||
| `config.transcriptDir` | `string` | Relative blocking memory sub-agent transcript directory under the agent sessions folder |
|
||||
|
||||
Useful tuning fields:
|
||||
|
||||
@@ -804,9 +692,8 @@ If active memory is too slow:
|
||||
|
||||
Active Memory rides on the configured memory plugin's recall pipeline, so most
|
||||
recall surprises are embedding-provider problems, not Active Memory bugs. The
|
||||
default `memory-core` path uses `memory_search` and `memory_get`; the
|
||||
`memory-lancedb` slot uses `memory_recall`. If you use another memory plugin,
|
||||
confirm `config.toolsAllow` names the tools that plugin actually registers.
|
||||
default `memory-core` path uses `memory_search`; `memory-lancedb` uses
|
||||
`memory_recall`.
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Embedding provider switched or stopped working">
|
||||
|
||||
@@ -23,11 +23,8 @@ configuration. They are different layers:
|
||||
|
||||
You will also see the word **harness** in code. A harness is the implementation
|
||||
that provides an agent runtime. For example, the bundled Codex harness
|
||||
implements the `codex` runtime. Public config uses `agentRuntime.id` on
|
||||
provider or model entries; whole-agent runtime keys are legacy and ignored.
|
||||
`openclaw doctor --fix` removes old whole-agent runtime pins and rewrites
|
||||
legacy runtime model refs to canonical provider/model refs plus model-scoped
|
||||
runtime policy where needed.
|
||||
implements the `codex` runtime. Public config uses `agentRuntime.id`; `openclaw
|
||||
doctor --fix` rewrites older runtime-policy keys to that shape.
|
||||
|
||||
There are two runtime families:
|
||||
|
||||
@@ -36,9 +33,9 @@ There are two runtime families:
|
||||
`codex`.
|
||||
- **CLI backends** run a local CLI process while keeping the model ref
|
||||
canonical. For example, `anthropic/claude-opus-4-7` with
|
||||
a model-scoped `agentRuntime.id: "claude-cli"` means "select the Anthropic
|
||||
model, execute through Claude CLI." `claude-cli` is not an embedded harness id
|
||||
and must not be passed to AgentHarness selection.
|
||||
`agentRuntime.id: "claude-cli"` means "select the Anthropic model, execute
|
||||
through Claude CLI." `claude-cli` is not an embedded harness id and must not
|
||||
be passed to AgentHarness selection.
|
||||
|
||||
## Codex surfaces
|
||||
|
||||
@@ -90,9 +87,9 @@ This is the agent-facing decision tree:
|
||||
2. If the user asks for **Codex as the embedded runtime** or wants the normal
|
||||
subscription-backed Codex agent experience, use `openai/<model>`.
|
||||
3. If the user explicitly chooses **PI for an OpenAI model**, keep the model ref
|
||||
as `openai/<model>` and set provider/model runtime policy to
|
||||
`agentRuntime.id: "pi"`. A selected `openai-codex` auth profile is routed
|
||||
internally through PI's legacy Codex-auth transport.
|
||||
as `openai/<model>` and set `agentRuntime.id: "pi"`. A selected
|
||||
`openai-codex` auth profile is routed internally through PI's legacy
|
||||
Codex-auth transport.
|
||||
4. If legacy config still contains **`openai-codex/*` model refs**, repair it to
|
||||
`openai/<model>` with `openclaw doctor --fix`.
|
||||
5. If the user explicitly says **ACP**, **acpx**, or **Codex ACP adapter**, use
|
||||
@@ -135,26 +132,21 @@ This ownership split is the main design rule:
|
||||
|
||||
OpenClaw chooses an embedded runtime after provider and model resolution:
|
||||
|
||||
1. Model-scoped runtime policy wins. This can live in a configured provider
|
||||
model entry or in `agents.defaults.models["provider/model"].agentRuntime` /
|
||||
`agents.list[].models["provider/model"].agentRuntime`.
|
||||
2. Provider-scoped runtime policy comes next at
|
||||
`models.providers.<provider>.agentRuntime`.
|
||||
3. In `auto` mode, registered plugin runtimes can claim supported provider/model
|
||||
1. A session's recorded runtime wins. Config changes do not hot-switch an
|
||||
existing transcript to a different native thread system.
|
||||
2. `OPENCLAW_AGENT_RUNTIME=<id>` forces that runtime for new or reset sessions.
|
||||
3. `agents.defaults.agentRuntime.id` or `agents.list[].agentRuntime.id` can set
|
||||
`auto`, `pi`, a registered embedded harness id such as `codex`, or a
|
||||
supported CLI backend alias such as `claude-cli`.
|
||||
4. In `auto` mode, registered plugin runtimes can claim supported provider/model
|
||||
pairs.
|
||||
4. If no runtime claims a turn in `auto` mode, OpenClaw uses PI as the
|
||||
5. If no runtime claims a turn in `auto` mode, OpenClaw uses PI as the
|
||||
compatibility runtime. Use an explicit runtime id when the run must be
|
||||
strict.
|
||||
|
||||
Whole-session and whole-agent runtime pins are ignored. That includes
|
||||
`OPENCLAW_AGENT_RUNTIME`, session `agentHarnessId`/`agentRuntimeOverride` state,
|
||||
`agents.defaults.agentRuntime`, and `agents.list[].agentRuntime`. Run
|
||||
`openclaw doctor --fix` to remove stale whole-agent runtime config and convert
|
||||
legacy runtime model refs where OpenClaw can preserve the intent.
|
||||
|
||||
Explicit provider/model plugin runtimes fail closed. For example,
|
||||
`agentRuntime.id: "codex"` on a provider or model means Codex or a clear
|
||||
selection/runtime error; it is never silently routed back to PI.
|
||||
Explicit plugin runtimes fail closed. For example, `agentRuntime.id: "codex"`
|
||||
means Codex or a clear selection/runtime error; it is never silently routed back
|
||||
to PI.
|
||||
|
||||
CLI backend aliases are different from embedded harness ids. The preferred
|
||||
Claude CLI form is:
|
||||
@@ -164,11 +156,7 @@ Claude CLI form is:
|
||||
agents: {
|
||||
defaults: {
|
||||
model: "anthropic/claude-opus-4-7",
|
||||
models: {
|
||||
"anthropic/claude-opus-4-7": {
|
||||
agentRuntime: { id: "claude-cli" },
|
||||
},
|
||||
},
|
||||
agentRuntime: { id: "claude-cli" },
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -176,15 +164,15 @@ Claude CLI form is:
|
||||
|
||||
Legacy refs such as `claude-cli/claude-opus-4-7` remain supported for
|
||||
compatibility, but new config should keep the provider/model canonical and put
|
||||
the execution backend in provider/model runtime policy.
|
||||
the execution backend in `agentRuntime.id`.
|
||||
|
||||
`auto` mode is intentionally conservative for most providers. OpenAI agent
|
||||
models are the exception: unset runtime and `auto` both resolve to the Codex
|
||||
harness. Explicit PI runtime config remains an opt-in compatibility route for
|
||||
`openai/*` agent turns; when paired with a selected `openai-codex` auth profile,
|
||||
OpenClaw routes PI internally through the legacy Codex-auth transport while
|
||||
keeping the public model ref as `openai/*`. Stale OpenAI PI session pins are
|
||||
ignored by runtime selection and can be cleaned with `openclaw doctor --fix`.
|
||||
keeping the public model ref as `openai/*`. Stale OpenAI PI session pins without
|
||||
explicit config are repaired back to Codex.
|
||||
|
||||
If `openclaw doctor` warns that the `codex` plugin is enabled while
|
||||
`openai-codex/*` remains in config, treat that as legacy route state. Run
|
||||
@@ -218,8 +206,10 @@ diagnostics, not as provider names.
|
||||
- A runtime id such as `codex` tells you which loop is executing the turn.
|
||||
- A channel label such as Telegram or Discord tells you where the conversation is happening.
|
||||
|
||||
If a run still shows an unexpected runtime, inspect the selected provider/model
|
||||
runtime policy first. Legacy session runtime pins no longer decide routing.
|
||||
If a session still shows PI after changing runtime config, start a new session
|
||||
with `/new` or clear the current one with `/reset`. Existing sessions keep their
|
||||
recorded runtime so a transcript is not replayed through two incompatible native
|
||||
session systems.
|
||||
|
||||
## Related
|
||||
|
||||
|
||||
@@ -29,19 +29,19 @@ Reference for **LLM/model providers** (not chat channels like WhatsApp/Telegram)
|
||||
<Accordion title="OpenAI provider/runtime split">
|
||||
OpenAI-family routes are prefix-specific:
|
||||
|
||||
- `openai/<model>` uses the native Codex app-server harness for agent turns by default. This is the usual ChatGPT/Codex subscription setup.
|
||||
- `openai-codex/<model>` is legacy config that doctor rewrites to `openai/<model>`.
|
||||
- `openai/<model>` plus provider/model `agentRuntime.id: "pi"` uses PI for explicit API-key or compatibility routes.
|
||||
- `openai/<model>` plus `agents.defaults.agentRuntime.id: "codex"` uses the native Codex app-server harness. This is the usual ChatGPT/Codex subscription setup.
|
||||
- `openai-codex/<model>` uses Codex OAuth in PI.
|
||||
- `openai/<model>` without a Codex runtime override uses the direct OpenAI API-key provider in PI.
|
||||
|
||||
See [OpenAI](/providers/openai) and [Codex harness](/plugins/codex-harness). If the provider/runtime split is confusing, read [Agent runtimes](/concepts/agent-runtimes) first.
|
||||
|
||||
Plugin auto-enable follows the same boundary: `openai/*` agent refs enable the Codex plugin for the default route, and explicit provider/model `agentRuntime.id: "codex"` or legacy `codex/<model>` refs also require it.
|
||||
Plugin auto-enable follows the same boundary: `openai-codex/<model>` belongs to the OpenAI plugin, while the Codex plugin is enabled by `agentRuntime.id: "codex"` or legacy `codex/<model>` refs.
|
||||
|
||||
GPT-5.5 is available through the native Codex app-server harness by default on `openai/gpt-5.5`, and through PI only when provider/model runtime policy explicitly selects `pi`.
|
||||
GPT-5.5 is available through the native Codex app-server harness when `agentRuntime.id: "codex"` is set, through `openai-codex/gpt-5.5` in PI for Codex OAuth, and through `openai/gpt-5.5` in PI for direct API-key traffic when your account exposes it.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="CLI runtimes">
|
||||
CLI runtimes use the same split: choose canonical model refs such as `anthropic/claude-*`, `google/gemini-*`, or `openai/gpt-*`, then set provider/model runtime policy to `claude-cli`, `google-gemini-cli`, or `codex-cli` when you want a local CLI backend.
|
||||
CLI runtimes use the same split: choose canonical model refs such as `anthropic/claude-*`, `google/gemini-*`, or `openai/gpt-*`, then set `agents.defaults.agentRuntime.id` to `claude-cli`, `google-gemini-cli`, or `codex-cli` when you want a local CLI backend.
|
||||
|
||||
Legacy `claude-cli/*`, `google-gemini-cli/*`, and `codex-cli/*` refs migrate back to canonical provider refs with the runtime recorded separately.
|
||||
|
||||
@@ -92,8 +92,9 @@ OpenClaw ships with the pi-ai catalog. These providers require **no** `models.pr
|
||||
- Example models: `openai/gpt-5.5`, `openai/gpt-5.4-mini`
|
||||
- Verify account/model availability with `openclaw models list --provider openai` if a specific install or API key behaves differently.
|
||||
- CLI: `openclaw onboard --auth-choice openai-api-key`
|
||||
- Default transport is `auto`; OpenClaw passes the transport choice to pi-ai.
|
||||
- Default transport is `auto` (WebSocket-first, SSE fallback)
|
||||
- Override per model via `agents.defaults.models["openai/<model>"].params.transport` (`"sse"`, `"websocket"`, or `"auto"`)
|
||||
- OpenAI Responses WebSocket warm-up defaults to enabled via `params.openaiWsWarmup` (`true`/`false`)
|
||||
- OpenAI priority processing can be enabled via `agents.defaults.models["openai/<model>"].params.serviceTier`
|
||||
- `/fast` and `params.fastMode` map direct `openai/*` Responses requests to `service_tier=priority` on `api.openai.com`
|
||||
- Use `params.serviceTier` when you want an explicit tier instead of the shared `/fast` toggle
|
||||
@@ -117,7 +118,7 @@ OpenClaw ships with the pi-ai catalog. These providers require **no** `models.pr
|
||||
- Direct public Anthropic requests support the shared `/fast` toggle and `params.fastMode`, including API-key and OAuth-authenticated traffic sent to `api.anthropic.com`; OpenClaw maps that to Anthropic `service_tier` (`auto` vs `standard_only`)
|
||||
- Preferred Claude CLI config keeps the model ref canonical and selects the CLI
|
||||
backend separately: `anthropic/claude-opus-4-7` with
|
||||
model-scoped `agentRuntime.id: "claude-cli"`. Legacy
|
||||
`agents.defaults.agentRuntime.id: "claude-cli"`. Legacy
|
||||
`claude-cli/claude-opus-4-7` refs still work for compatibility.
|
||||
|
||||
<Note>
|
||||
@@ -134,8 +135,8 @@ Anthropic staff told us OpenClaw-style Claude CLI usage is allowed again, so Ope
|
||||
|
||||
- Provider: `openai-codex`
|
||||
- Auth: OAuth (ChatGPT)
|
||||
- Legacy PI model ref: `openai-codex/gpt-5.5`
|
||||
- Native Codex app-server harness ref: `openai/gpt-5.5`
|
||||
- PI model ref: `openai-codex/gpt-5.5`
|
||||
- Native Codex app-server harness ref: `openai/gpt-5.5` with `agents.defaults.agentRuntime.id: "codex"`
|
||||
- Native Codex app-server harness docs: [Codex harness](/plugins/codex-harness)
|
||||
- Legacy model refs: `codex/gpt-*`
|
||||
- Plugin boundary: `openai-codex/*` loads the OpenAI plugin; the native Codex app-server plugin is selected only by the Codex harness runtime or legacy `codex/*` refs.
|
||||
@@ -147,8 +148,8 @@ Anthropic staff told us OpenClaw-style Claude CLI usage is allowed again, so Ope
|
||||
- Shares the same `/fast` toggle and `params.fastMode` config as direct `openai/*`; OpenClaw maps that to `service_tier=priority`
|
||||
- `openai-codex/gpt-5.5` uses the Codex catalog native `contextWindow = 400000` and default runtime `contextTokens = 272000`; override the runtime cap with `models.providers.openai-codex.models[].contextTokens`
|
||||
- Policy note: OpenAI Codex OAuth is explicitly supported for external tools/workflows like OpenClaw.
|
||||
- For the common subscription plus native Codex runtime route, sign in with `openai-codex` auth but configure `openai/gpt-5.5`; OpenAI agent turns select Codex by default.
|
||||
- Use provider/model `agentRuntime.id: "pi"` only when you want a compatibility route through PI; otherwise keep `openai/gpt-5.5` on the default Codex harness.
|
||||
- For the common subscription plus native Codex runtime route, sign in with `openai-codex` auth but configure `openai/gpt-5.5` plus `agents.defaults.agentRuntime.id: "codex"`.
|
||||
- Use `openai-codex/gpt-5.5` only when you want the Codex OAuth/subscription route through PI; use `openai/gpt-5.5` without the Codex runtime override when your API-key setup and local catalog expose the public API route.
|
||||
- Older `openai-codex/gpt-5.1*`, `openai-codex/gpt-5.2*`, and `openai-codex/gpt-5.3*` refs are suppressed because ChatGPT/Codex OAuth accounts reject them; use `openai-codex/gpt-5.5` or the native Codex runtime route instead.
|
||||
|
||||
```json5
|
||||
@@ -157,6 +158,7 @@ Anthropic staff told us OpenClaw-style Claude CLI usage is allowed again, so Ope
|
||||
agents: {
|
||||
defaults: {
|
||||
model: { primary: "openai/gpt-5.5" },
|
||||
agentRuntime: { id: "codex" },
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ sidebarTitle: "Models CLI"
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
Model refs choose a provider and model. They do not usually choose the low-level agent runtime. OpenAI agent refs are the main exception: `openai/gpt-5.5` runs through the Codex app-server runtime by default on the official OpenAI provider. Explicit runtime overrides belong on provider/model policy, not on the whole agent or session. In Codex runtime mode, the `openai/gpt-*` ref does not imply API-key billing; auth can come from a Codex account or `openai-codex` auth profile. See [Agent runtimes](/concepts/agent-runtimes).
|
||||
Model refs choose a provider and model. They do not usually choose the low-level agent runtime. For example, `openai/gpt-5.5` can run through the normal OpenAI provider path or through the Codex app-server runtime, depending on `agents.defaults.agentRuntime.id`. In Codex runtime mode, the `openai/gpt-*` ref does not imply API-key billing; auth can come from a Codex account or `openai-codex` auth profile. See [Agent runtimes](/concepts/agent-runtimes).
|
||||
|
||||
## How model selection works
|
||||
|
||||
|
||||
@@ -37,7 +37,6 @@ resources.
|
||||
| `Run.cancel()` | Ready | Calls `sessions.abort` by run id, with session key when available. |
|
||||
| `oc.sessions` | Ready | Creates, resolves, sends to, patches, compacts, and gets session handles. |
|
||||
| `Session.send()` | Ready | Calls `sessions.send` and returns a `Run`. |
|
||||
| `oc.tasks` | Ready | Lists, reads, and cancels Gateway task ledger entries. |
|
||||
| `oc.models` | Ready | Calls `models.list` and the current `models.authStatus` status RPC. |
|
||||
| `oc.tools` | Ready | Lists, scopes, and invokes Gateway tools through the policy pipeline. |
|
||||
| `oc.artifacts` | Ready | Lists, gets, and downloads Gateway transcript artifacts. |
|
||||
@@ -51,9 +50,7 @@ The SDK also exports the core types used by those surfaces:
|
||||
`OpenClawEventType`, `GatewayEvent`, `OpenClawTransport`,
|
||||
`GatewayRequestOptions`, `SessionCreateParams`, `SessionSendParams`,
|
||||
`ArtifactSummary`, `ArtifactQuery`, `ArtifactsListResult`,
|
||||
`ArtifactsGetResult`, `ArtifactsDownloadResult`,
|
||||
`TaskSummary`, `TaskStatus`, `TasksListParams`, `TasksListResult`,
|
||||
`TasksGetResult`, `TasksCancelResult`, `RuntimeSelection`,
|
||||
`ArtifactsGetResult`, `ArtifactsDownloadResult`, `RuntimeSelection`,
|
||||
`EnvironmentSelection`, `WorkspaceSelection`, `ApprovalMode`, and related
|
||||
result types.
|
||||
|
||||
@@ -257,14 +254,6 @@ const approvals = await oc.approvals.list();
|
||||
await oc.approvals.respond("approval-id", { decision: "approve" });
|
||||
```
|
||||
|
||||
Task helpers use the durable task ledger that also backs `openclaw tasks`:
|
||||
|
||||
```typescript
|
||||
const tasks = await oc.tasks.list({ status: "running", sessionKey: "agent:main:main" });
|
||||
const task = await oc.tasks.get(tasks.tasks[0].id);
|
||||
await oc.tasks.cancel(task.task.id, { reason: "user stopped task" });
|
||||
```
|
||||
|
||||
Environment helpers expose read-only Gateway-local and node discovery:
|
||||
|
||||
```typescript
|
||||
@@ -279,6 +268,10 @@ pretend Gateway RPCs exist. These calls currently throw explicit unsupported
|
||||
errors:
|
||||
|
||||
```typescript
|
||||
await oc.tasks.list();
|
||||
await oc.tasks.get("task-id");
|
||||
await oc.tasks.cancel("task-id");
|
||||
|
||||
await oc.environments.create({});
|
||||
await oc.environments.delete("environment-id");
|
||||
```
|
||||
|
||||
@@ -18,9 +18,9 @@ into the final answer when the channel can do that safely.
|
||||
|
||||
```text
|
||||
Shelling...
|
||||
📖 from docs/concepts/progress-drafts.md
|
||||
📖 Read: from docs/concepts/progress-drafts.md
|
||||
🔎 Web Search: for "discord edit message"
|
||||
🛠️ Bash: run tests
|
||||
🛠️ Exec: run tests
|
||||
```
|
||||
|
||||
Use progress drafts when you want one tidy status message during tool-heavy work
|
||||
@@ -51,17 +51,15 @@ progress chatter for that turn.
|
||||
|
||||
A progress draft has two parts:
|
||||
|
||||
| Part | Purpose |
|
||||
| -------------- | ------------------------------------------------------------------------------------- |
|
||||
| Label | A short starter/status line such as `Thinking...` or `Shelling...`. |
|
||||
| Progress lines | Compact run updates using the same tool icons and detail formatter as verbose output. |
|
||||
| Part | Purpose |
|
||||
| -------------- | --------------------------------------------------------------------------- |
|
||||
| Label | A short title such as `Thinking...` or `Shelling...`. |
|
||||
| Progress lines | Compact run updates using the same tool labels and icons as verbose output. |
|
||||
|
||||
The label appears after the agent starts meaningful work and either remains busy
|
||||
for five seconds or emits a second work event. It is part of the rolling progress
|
||||
line list, so the starter status scrolls away once enough concrete work appears.
|
||||
Plain text-only replies do not show a progress draft. Progress lines are added
|
||||
only when the agent emits useful work updates, for example `🛠️ Bash: run tests`,
|
||||
`🔎 Web Search: for "discord edit message"`, or `✍️ Write: to /tmp/file`.
|
||||
for five seconds or emits a second work event. Plain text-only replies do not
|
||||
show a progress draft. Progress lines are added only when the agent emits useful
|
||||
work updates, for example `🛠️ Exec`, `🔎 Web Search`, or `✍️ Write: to /tmp/file`.
|
||||
By default they use the same compact explain mode as `/verbose`; set
|
||||
`agents.defaults.toolProgressDetail: "raw"` when debugging and you also want raw
|
||||
commands/details appended.
|
||||
@@ -191,16 +189,16 @@ OpenClaw uses the same formatter for progress drafts and `/verbose`:
|
||||
```
|
||||
|
||||
`"explain"` is the default and keeps drafts stable with concise labels like
|
||||
`🛠️ check JS syntax for /tmp/app.js`. `"raw"` appends the underlying
|
||||
`🛠️ Exec: check JS syntax for /tmp/app.js`. `"raw"` appends the underlying
|
||||
command/detail when available, which is useful while debugging but noisier in
|
||||
chat.
|
||||
|
||||
For example, the same command appears differently depending on the detail mode:
|
||||
|
||||
| Mode | Progress line |
|
||||
| --------- | -------------------------------------------------------------- |
|
||||
| `explain` | `🛠️ check JS syntax for /tmp/app.js` |
|
||||
| `raw` | `🛠️ check JS syntax for /tmp/app.js, node --check /tmp/app.js` |
|
||||
| Mode | Progress line |
|
||||
| --------- | -------------------------------------------------------------------- |
|
||||
| `explain` | `🛠️ Exec: check JS syntax for /tmp/app.js` |
|
||||
| `raw` | `🛠️ Exec: check JS syntax for /tmp/app.js, node --check /tmp/app.js` |
|
||||
|
||||
Limit how many lines stay visible:
|
||||
|
||||
|
||||
@@ -278,7 +278,7 @@ Optional:
|
||||
|
||||
- `OPENCLAW_QA_TELEGRAM_CAPTURE_CONTENT=1` keeps message bodies in observed-message artifacts (default redacts).
|
||||
|
||||
Scenarios (`extensions/qa-lab/src/live-transports/telegram/telegram-live.runtime.ts`):
|
||||
Scenarios (`extensions/qa-lab/src/live-transports/telegram/telegram-live.runtime.ts:44`):
|
||||
|
||||
- `telegram-canary`
|
||||
- `telegram-mention-gating`
|
||||
@@ -287,18 +287,10 @@ Scenarios (`extensions/qa-lab/src/live-transports/telegram/telegram-live.runtime
|
||||
- `telegram-commands-command`
|
||||
- `telegram-tools-compact-command`
|
||||
- `telegram-whoami-command`
|
||||
- `telegram-status-command`
|
||||
- `telegram-repeated-command-authorization`
|
||||
- `telegram-other-bot-command-gating`
|
||||
- `telegram-context-command`
|
||||
- `telegram-current-session-status-tool`
|
||||
- `telegram-reply-chain-exact-marker`
|
||||
- `telegram-stream-final-single-message`
|
||||
- `telegram-long-final-reuses-preview`
|
||||
- `telegram-long-final-three-chunks`
|
||||
|
||||
The implicit default set always covers canary, mention gating, native command replies, command addressing, and bot-to-bot group replies. `mock-openai` defaults also include deterministic reply-chain and final-message streaming checks. `telegram-current-session-status-tool` remains opt-in because it is only stable when threaded directly after canary, not after arbitrary native command replies. Use `pnpm openclaw qa telegram --list-scenarios --provider-mode mock-openai` to print the current default/optional split with regression refs.
|
||||
|
||||
Output artifacts:
|
||||
|
||||
- `telegram-qa-report.md`
|
||||
@@ -567,17 +559,15 @@ A green run completes in well under 30 seconds and `slack-qa-report.md` shows bo
|
||||
|
||||
### Convex credential pool
|
||||
|
||||
Telegram, Discord, Slack, and WhatsApp lanes can lease credentials from a shared Convex pool instead of reading the env vars above. Pass `--credential-source convex` (or set `OPENCLAW_QA_CREDENTIAL_SOURCE=convex`); QA Lab acquires an exclusive lease, heartbeats it for the duration of the run, and releases it on shutdown. Pool kinds are `"telegram"`, `"discord"`, `"slack"`, and `"whatsapp"`.
|
||||
Telegram, Discord, and Slack lanes can lease credentials from a shared Convex pool instead of reading the env vars above. Pass `--credential-source convex` (or set `OPENCLAW_QA_CREDENTIAL_SOURCE=convex`); QA Lab acquires an exclusive lease, heartbeats it for the duration of the run, and releases it on shutdown. Pool kinds are `"telegram"`, `"discord"`, and `"slack"`.
|
||||
|
||||
Payload shapes the broker validates on `admin/add`:
|
||||
|
||||
- Telegram (`kind: "telegram"`): `{ groupId: string, driverToken: string, sutToken: string }` - `groupId` must be a numeric chat-id string.
|
||||
- Discord (`kind: "discord"`): `{ guildId: string, channelId: string, driverBotToken: string, sutBotToken: string, sutApplicationId: string }`.
|
||||
- WhatsApp (`kind: "whatsapp"`): `{ driverPhoneE164: string, sutPhoneE164: string, driverAuthArchiveBase64: string, sutAuthArchiveBase64: string, groupJid?: string }` - phone numbers must be distinct E.164 strings.
|
||||
- Slack (`kind: "slack"`): `{ channelId: string, driverBotToken: string, sutBotToken: string, sutAppToken: string }` - `channelId` must match `^[A-Z][A-Z0-9]+$` (a Slack id like `Cxxxxxxxxxx`). See [Setting up the Slack workspace](#setting-up-the-slack-workspace) for app and scope provisioning.
|
||||
|
||||
Slack lanes can also use the pool. Slack payload shape checks currently live in the Slack QA runner rather than the broker; use `{ channelId: string, driverBotToken: string, sutBotToken: string, sutAppToken: string }`, with a Slack channel id like `Cxxxxxxxxxx`. See [Setting up the Slack workspace](#setting-up-the-slack-workspace) for app and scope provisioning.
|
||||
|
||||
Operational env vars and the Convex broker endpoint contract live in [Testing → Shared Telegram credentials via Convex](/help/testing#shared-telegram-credentials-via-convex-v1) (the section name predates the multi-channel pool; the lease semantics are shared across kinds).
|
||||
Operational env vars and the Convex broker endpoint contract live in [Testing → Shared Telegram credentials via Convex](/help/testing#shared-telegram-credentials-via-convex-v1) (the section name predates Discord support; the broker semantics are identical for both kinds).
|
||||
|
||||
## Repo-backed seeds
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
"redirects": [
|
||||
{
|
||||
"source": "/channels/bluebubbles",
|
||||
"destination": "/channels/imessage-from-bluebubbles"
|
||||
"destination": "/channels/imessage"
|
||||
},
|
||||
{
|
||||
"source": "/install/migrating-matrix",
|
||||
@@ -393,16 +393,16 @@
|
||||
"destination": "/channels/pairing"
|
||||
},
|
||||
{
|
||||
"source": "/clawdhub",
|
||||
"destination": "/clawhub"
|
||||
"source": "/clawhub",
|
||||
"destination": "/tools/clawhub"
|
||||
},
|
||||
{
|
||||
"source": "/tools/clawhub",
|
||||
"destination": "/clawhub"
|
||||
"source": "/clawdhub",
|
||||
"destination": "/tools/clawhub"
|
||||
},
|
||||
{
|
||||
"source": "/tools/clawdhub",
|
||||
"destination": "/clawhub"
|
||||
"destination": "/tools/clawhub"
|
||||
},
|
||||
{
|
||||
"source": "/configuration",
|
||||
@@ -1063,10 +1063,8 @@
|
||||
"channels/msteams",
|
||||
"channels/googlechat",
|
||||
"channels/imessage",
|
||||
"channels/imessage-from-bluebubbles",
|
||||
"channels/matrix",
|
||||
"channels/matrix-migration",
|
||||
"channels/matrix-presentation",
|
||||
"channels/matrix-push-rules"
|
||||
]
|
||||
},
|
||||
@@ -1195,7 +1193,6 @@
|
||||
"plugins/reference",
|
||||
"plugins/bundles",
|
||||
"plugins/dependency-resolution",
|
||||
"plugins/install-overrides",
|
||||
"plugins/codex-harness",
|
||||
"plugins/codex-computer-use",
|
||||
"plugins/google-meet",
|
||||
@@ -1204,7 +1201,6 @@
|
||||
"plugins/memory-wiki",
|
||||
"plugins/memory-lancedb",
|
||||
"plugins/message-presentation",
|
||||
"plugins/oc-path",
|
||||
"plugins/skill-workshop",
|
||||
"plugins/zalouser",
|
||||
{
|
||||
@@ -1246,6 +1242,7 @@
|
||||
"tools/creating-skills",
|
||||
"tools/skills-config",
|
||||
"tools/slash-commands",
|
||||
"tools/clawhub",
|
||||
"prose"
|
||||
]
|
||||
},
|
||||
@@ -1328,36 +1325,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tab": "ClawHub",
|
||||
"groups": [
|
||||
{
|
||||
"group": "Overview",
|
||||
"pages": ["clawhub/index", "clawhub/quickstart", "clawhub/how-it-works"]
|
||||
},
|
||||
{
|
||||
"group": "Using ClawHub",
|
||||
"pages": [
|
||||
"clawhub/cli",
|
||||
"clawhub/publishing",
|
||||
"clawhub/skill-format",
|
||||
"clawhub/soul-format",
|
||||
"clawhub/auth",
|
||||
"clawhub/telemetry",
|
||||
"clawhub/troubleshooting"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "API and trust",
|
||||
"pages": [
|
||||
"clawhub/api",
|
||||
"clawhub/http-api",
|
||||
"clawhub/security",
|
||||
"clawhub/acceptable-usage"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tab": "Models",
|
||||
"groups": [
|
||||
@@ -1677,7 +1644,7 @@
|
||||
},
|
||||
{
|
||||
"group": "Plugins and skills",
|
||||
"pages": ["cli/plugins", "cli/path", "cli/skills"]
|
||||
"pages": ["cli/plugins", "cli/skills"]
|
||||
},
|
||||
{
|
||||
"group": "Interfaces",
|
||||
|
||||
@@ -336,6 +336,9 @@ Time format in system prompt. Default: `auto` (OS preference).
|
||||
fallbacks: ["openai/gpt-5.4-mini"],
|
||||
},
|
||||
params: { cacheRetention: "long" }, // global default provider params
|
||||
agentRuntime: {
|
||||
id: "pi", // pi | auto | registered harness id, e.g. codex
|
||||
},
|
||||
pdfMaxBytesMb: 10,
|
||||
pdfMaxPages: 20,
|
||||
thinkingDefault: "low",
|
||||
@@ -393,31 +396,27 @@ Time format in system prompt. Default: `auto` (OS preference).
|
||||
- `params` merge precedence (config): `agents.defaults.params` (global base) is overridden by `agents.defaults.models["provider/model"].params` (per-model), then `agents.list[].params` (matching agent id) overrides by key. See [Prompt Caching](/reference/prompt-caching) for details.
|
||||
- `params.extra_body`/`params.extraBody`: advanced pass-through JSON merged into `api: "openai-completions"` request bodies for OpenAI-compatible proxies. If it collides with generated request keys, the extra body wins; non-native completions routes still strip OpenAI-only `store` afterward.
|
||||
- `params.chat_template_kwargs`: vLLM/OpenAI-compatible chat-template arguments merged into top-level `api: "openai-completions"` request bodies. For `vllm/nemotron-3-*` with thinking off, the bundled vLLM plugin automatically sends `enable_thinking: false` and `force_nonempty_content: true`; explicit `chat_template_kwargs` override generated defaults, and `extra_body.chat_template_kwargs` still has final precedence. For vLLM Qwen thinking controls, set `params.qwenThinkingFormat` to `"chat-template"` or `"top-level"` on that model entry.
|
||||
- `compat.thinkingFormat`: OpenAI-compatible thinking payload style. Use `"qwen"` for Qwen-style top-level `enable_thinking`, or `"qwen-chat-template"` for `chat_template_kwargs.enable_thinking` on Qwen-family backends that support request-level chat-template kwargs, such as vLLM. OpenClaw maps disabled thinking to `false` and enabled thinking to `true`.
|
||||
- `compat.supportedReasoningEfforts`: per-model OpenAI-compatible reasoning effort list. Include `"xhigh"` for custom endpoints that truly accept it; OpenClaw then exposes `/think xhigh` in command menus, Gateway session rows, session patch validation, agent CLI validation, and `llm-task` validation for that configured provider/model. Use `compat.reasoningEffortMap` when the backend wants a provider-specific value for a canonical level.
|
||||
- `params.preserveThinking`: Z.AI-only opt-in for preserved thinking. When enabled and thinking is on, OpenClaw sends `thinking.clear_thinking: false` and replays prior `reasoning_content`; see [Z.AI thinking and preserved thinking](/providers/zai#thinking-and-preserved-thinking).
|
||||
- Runtime policy belongs on providers or models, not on `agents.defaults`. Use `models.providers.<provider>.agentRuntime` for provider-wide rules or `agents.defaults.models["provider/model"].agentRuntime` / `agents.list[].models["provider/model"].agentRuntime` for model-specific rules. OpenAI agent models on the official OpenAI provider select Codex by default.
|
||||
- `agentRuntime`: default low-level agent runtime policy. Omitted id defaults to OpenClaw Pi. Use `id: "pi"` to force the built-in PI harness, `id: "auto"` to let registered plugin harnesses claim supported models and use PI when none match, a registered harness id such as `id: "codex"` to require that harness, or a supported CLI backend alias such as `id: "claude-cli"`. Explicit plugin runtimes fail closed when the harness is unavailable or fails. Keep model refs canonical as `provider/model`; select Codex, Claude CLI, Gemini CLI, and other execution backends through runtime config instead of legacy runtime provider prefixes. See [Agent runtimes](/concepts/agent-runtimes) for how this differs from provider/model selection.
|
||||
- Config writers that mutate these fields (for example `/models set`, `/models set-image`, and fallback add/remove commands) save canonical object form and preserve existing fallback lists when possible.
|
||||
- `maxConcurrent`: max parallel agent runs across sessions (each session still serialized). Default: 4.
|
||||
|
||||
### Runtime policy
|
||||
### `agents.defaults.agentRuntime`
|
||||
|
||||
`agentRuntime` controls which low-level executor runs agent turns. Most
|
||||
deployments should keep the default OpenClaw Pi runtime. Use it when a trusted
|
||||
plugin provides a native harness, such as the bundled Codex app-server harness,
|
||||
or when you want a supported CLI backend such as Claude CLI. For the mental
|
||||
model, see [Agent runtimes](/concepts/agent-runtimes).
|
||||
|
||||
```json5
|
||||
{
|
||||
models: {
|
||||
providers: {
|
||||
openai: {
|
||||
agentRuntime: { id: "codex" },
|
||||
},
|
||||
},
|
||||
},
|
||||
agents: {
|
||||
defaults: {
|
||||
model: "openai/gpt-5.5",
|
||||
models: {
|
||||
"anthropic/claude-opus-4-7": {
|
||||
agentRuntime: { id: "claude-cli" },
|
||||
},
|
||||
agentRuntime: {
|
||||
id: "codex",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -426,9 +425,11 @@ Time format in system prompt. Default: `auto` (OS preference).
|
||||
|
||||
- `id`: `"auto"`, `"pi"`, a registered plugin harness id, or a supported CLI backend alias. The bundled Codex plugin registers `codex`; the bundled Anthropic plugin provides the `claude-cli` CLI backend.
|
||||
- `id: "auto"` lets registered plugin harnesses claim supported turns and uses PI when no harness matches. An explicit plugin runtime such as `id: "codex"` requires that harness and fails closed if it is unavailable or fails.
|
||||
- Whole-agent runtime keys are legacy. `agents.defaults.agentRuntime`, `agents.list[].agentRuntime`, session runtime pins, and `OPENCLAW_AGENT_RUNTIME` are ignored by runtime selection. Run `openclaw doctor --fix` to remove stale values.
|
||||
- OpenAI agent models use the Codex harness by default; provider/model `agentRuntime.id: "codex"` remains valid when you want to make that explicit.
|
||||
- For Claude CLI deployments, prefer `model: "anthropic/claude-opus-4-7"` plus model-scoped `agentRuntime.id: "claude-cli"`. Legacy `claude-cli/claude-opus-4-7` model refs still work for compatibility, but new config should keep provider/model selection canonical and put the execution backend in provider/model runtime policy.
|
||||
- Environment override: `OPENCLAW_AGENT_RUNTIME=<id|auto|pi>` overrides `id` for that process.
|
||||
- OpenAI agent models use the Codex harness by default; `agentRuntime.id: "codex"` remains valid when you want to make that explicit.
|
||||
- For Claude CLI deployments, prefer `model: "anthropic/claude-opus-4-7"` plus `agentRuntime.id: "claude-cli"`. Legacy `claude-cli/claude-opus-4-7` model refs still work for compatibility, but new config should keep provider/model selection canonical and put the execution backend in `agentRuntime.id`.
|
||||
- Older runtime-policy keys are rewritten to `agentRuntime` by `openclaw doctor --fix`.
|
||||
- Harness choice is pinned per session id after the first embedded run. Config/env changes affect new or reset sessions, not an existing transcript. Legacy OpenAI sessions with transcript history but no recorded pin use Codex; stale OpenAI PI pins can be repaired with `openclaw doctor --fix`. `/status` reports the effective runtime, for example `Runtime: OpenClaw Pi Default` or `Runtime: OpenAI Codex`.
|
||||
- This only controls text agent-turn execution. Media generation, vision, PDF, music, video, and TTS still use their provider/model settings.
|
||||
|
||||
**Built-in alias shorthands** (only apply when the model is in `agents.defaults.models`):
|
||||
@@ -958,6 +959,7 @@ for provider examples and precedence.
|
||||
thinkingDefault: "high", // per-agent thinking level override
|
||||
reasoningDefault: "on", // per-agent reasoning visibility override
|
||||
fastModeDefault: false, // per-agent fast mode override
|
||||
agentRuntime: { id: "auto" },
|
||||
params: { cacheRetention: "none" }, // overrides matching defaults.models params by key
|
||||
tts: {
|
||||
providers: {
|
||||
@@ -1004,7 +1006,7 @@ for provider examples and precedence.
|
||||
- `thinkingDefault`: optional per-agent default thinking level (`off | minimal | low | medium | high | xhigh | adaptive | max`). Overrides `agents.defaults.thinkingDefault` for this agent when no per-message or session override is set. The selected provider/model profile controls which values are valid; for Google Gemini, `adaptive` keeps provider-owned dynamic thinking (`thinkingLevel` omitted on Gemini 3/3.1, `thinkingBudget: -1` on Gemini 2.5).
|
||||
- `reasoningDefault`: optional per-agent default reasoning visibility (`on | off | stream`). Overrides `agents.defaults.reasoningDefault` for this agent when no per-message or session reasoning override is set.
|
||||
- `fastModeDefault`: optional per-agent default for fast mode (`true | false`). Applies when no per-message or session fast-mode override is set.
|
||||
- `models`: optional per-agent model catalog/runtime overrides keyed by full `provider/model` ids. Use `models["provider/model"].agentRuntime` for per-agent runtime exceptions.
|
||||
- `agentRuntime`: optional per-agent low-level runtime policy override. Use `{ id: "codex" }` to make one agent Codex-only while other agents keep the default PI fallback in `auto` mode.
|
||||
- `runtime`: optional per-agent runtime descriptor. Use `type: "acp"` with `runtime.acp` defaults (`agent`, `backend`, `mode`, `cwd`) when the agent should default to ACP harness sessions.
|
||||
- `identity.avatar`: workspace-relative path, `http(s)` URL, or `data:` URI.
|
||||
- `identity` derives defaults: `ackReaction` from `emoji`, `mentionPatterns` from `name`/`emoji`.
|
||||
@@ -1379,8 +1381,6 @@ Defaults for Talk mode (macOS/iOS/Android).
|
||||
},
|
||||
system: {},
|
||||
},
|
||||
consultThinkingLevel: "low",
|
||||
consultFastMode: true,
|
||||
speechLocale: "ru-RU",
|
||||
silenceTimeoutMs: 1500,
|
||||
interruptOnSpeech: true,
|
||||
@@ -1388,8 +1388,8 @@ Defaults for Talk mode (macOS/iOS/Android).
|
||||
provider: "openai",
|
||||
providers: {
|
||||
openai: {
|
||||
model: "gpt-realtime-2",
|
||||
voice: "cedar",
|
||||
model: "gpt-realtime",
|
||||
voice: "alloy",
|
||||
},
|
||||
},
|
||||
mode: "realtime",
|
||||
@@ -1408,8 +1408,6 @@ Defaults for Talk mode (macOS/iOS/Android).
|
||||
- `providers.*.voiceAliases` lets Talk directives use friendly names.
|
||||
- `providers.mlx.modelId` selects the Hugging Face repo used by the macOS local MLX helper. If omitted, macOS uses `mlx-community/Soprano-80M-bf16`.
|
||||
- macOS MLX playback runs through the bundled `openclaw-mlx-tts` helper when present, or an executable on `PATH`; `OPENCLAW_MLX_TTS_BIN` overrides the helper path for development.
|
||||
- `consultThinkingLevel` controls the thinking level for the full OpenClaw agent run behind Control UI Talk realtime `openclaw_agent_consult` calls. Leave unset to preserve normal session/model behavior.
|
||||
- `consultFastMode` sets a one-shot fast-mode override for Control UI Talk realtime consults without changing the session's normal fast-mode setting.
|
||||
- `speechLocale` sets the BCP 47 locale id used by iOS/macOS Talk speech recognition. Leave unset to use the device default.
|
||||
- `silenceTimeoutMs` controls how long Talk mode waits after user silence before it sends the transcript. Unset keeps the platform default pause window (`700 ms on macOS and Android, 900 ms on iOS`).
|
||||
|
||||
|
||||
@@ -585,7 +585,7 @@ When Mattermost native commands are enabled:
|
||||
|
||||
OpenClaw spawns `imsg rpc` (JSON-RPC over stdio). No daemon or port required. This is the preferred path for new OpenClaw iMessage setups when the host can grant Messages database and Automation permissions.
|
||||
|
||||
BlueBubbles support was removed. Migrate `channels.bluebubbles` configs to `channels.imessage`; OpenClaw supports iMessage through `imsg` only.
|
||||
BlueBubbles is deprecated and no longer ships as a bundled OpenClaw channel. Migrate `channels.bluebubbles` configs to `channels.imessage`; third-party BlueBubbles bridges belong outside core.
|
||||
|
||||
If the Gateway is not running on the signed-in Messages Mac, keep `channels.imessage.enabled=true` and set `channels.imessage.cliPath` to an SSH wrapper that runs `imsg "$@"` on that Mac. The default local `imsg` path is macOS-only.
|
||||
|
||||
|
||||
@@ -474,7 +474,6 @@ OpenClaw uses the built-in model catalog. Add custom providers via `models.provi
|
||||
- `models.providers.*.models.*.contextTokens`: optional runtime context cap. This overrides provider-level `contextTokens`; use it when you want a smaller effective context budget than the model's native `contextWindow`; `openclaw models list` shows both values when they differ.
|
||||
- `models.providers.*.models.*.compat.supportsDeveloperRole`: optional compatibility hint. For `api: "openai-completions"` with a non-empty non-native `baseUrl` (host not `api.openai.com`), OpenClaw forces this to `false` at runtime. Empty/omitted `baseUrl` keeps default OpenAI behavior.
|
||||
- `models.providers.*.models.*.compat.requiresStringContent`: optional compatibility hint for string-only OpenAI-compatible chat endpoints. When `true`, OpenClaw flattens pure text `messages[].content` arrays into plain strings before sending the request.
|
||||
- `models.providers.*.models.*.compat.thinkingFormat`: optional thinking payload hint. Use `"qwen"` for top-level `enable_thinking`, or `"qwen-chat-template"` for `chat_template_kwargs.enable_thinking` on Qwen-family OpenAI-compatible servers that support request-level chat-template kwargs, such as vLLM.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="Amazon Bedrock discovery">
|
||||
|
||||
@@ -48,8 +48,6 @@ Moved to a dedicated page - see
|
||||
- `session.*` (session lifecycle, compaction, pruning)
|
||||
- `messages.*` (message delivery, TTS, markdown rendering)
|
||||
- `talk.*` (Talk mode)
|
||||
- `talk.consultThinkingLevel`: thinking level override for the full OpenClaw agent run behind Control UI Talk realtime consults
|
||||
- `talk.consultFastMode`: one-shot fast-mode override for Control UI Talk realtime consults
|
||||
- `talk.speechLocale`: optional BCP 47 locale id for Talk speech recognition on iOS/macOS
|
||||
- `talk.silenceTimeoutMs`: when unset, Talk keeps the platform default pause window before sending the transcript (`700 ms on macOS and Android, 900 ms on iOS`)
|
||||
|
||||
@@ -200,75 +198,8 @@ See [MCP](/cli/mcp#openclaw-as-an-mcp-client-registry) and
|
||||
- `plugins.entries.<id>.hooks.allowConversationAccess`: when `true`, trusted non-bundled plugins may read raw conversation content from typed hooks such as `llm_input`, `llm_output`, `before_model_resolve`, `before_agent_reply`, `before_agent_run`, `before_agent_finalize`, and `agent_end`.
|
||||
- `plugins.entries.<id>.subagent.allowModelOverride`: explicitly trust this plugin to request per-run `provider` and `model` overrides for background subagent runs.
|
||||
- `plugins.entries.<id>.subagent.allowedModels`: optional allowlist of canonical `provider/model` targets for trusted subagent overrides. Use `"*"` only when you intentionally want to allow any model.
|
||||
- `plugins.entries.<id>.llm.allowModelOverride`: explicitly trust this plugin to request model overrides for `api.runtime.llm.complete`.
|
||||
- `plugins.entries.<id>.llm.allowedModels`: optional allowlist of canonical `provider/model` targets for trusted plugin LLM completion overrides. Use `"*"` only when you intentionally want to allow any model.
|
||||
- `plugins.entries.<id>.llm.allowAgentIdOverride`: explicitly trust this plugin to run `api.runtime.llm.complete` against a non-default agent id.
|
||||
- `plugins.entries.<id>.config`: plugin-defined config object (validated by native OpenClaw plugin schema when available).
|
||||
- Channel plugin account/runtime settings live under `channels.<id>` and should be described by the owning plugin's manifest `channelConfigs` metadata, not by a central OpenClaw option registry.
|
||||
|
||||
### Codex harness plugin config
|
||||
|
||||
The bundled `codex` plugin owns native Codex app-server harness settings under
|
||||
`plugins.entries.codex.config`. See [Codex harness](/plugins/codex-harness) for
|
||||
the full runtime model.
|
||||
|
||||
`codexPlugins` applies only to sessions that select the native Codex harness.
|
||||
It does not enable Codex plugins for Pi, normal OpenAI provider runs, ACP
|
||||
conversation bindings, or any non-Codex harness.
|
||||
|
||||
```json5
|
||||
{
|
||||
plugins: {
|
||||
entries: {
|
||||
codex: {
|
||||
enabled: true,
|
||||
config: {
|
||||
codexPlugins: {
|
||||
enabled: true,
|
||||
allow_destructive_actions: false,
|
||||
plugins: {
|
||||
"google-calendar": {
|
||||
enabled: true,
|
||||
marketplaceName: "openai-curated",
|
||||
pluginName: "google-calendar",
|
||||
allow_destructive_actions: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
- `plugins.entries.codex.config.codexPlugins.enabled`: enables native Codex
|
||||
plugin/app support for the Codex harness. Default: `false`.
|
||||
- `plugins.entries.codex.config.codexPlugins.allow_destructive_actions`:
|
||||
default destructive-action policy for migrated plugin app elicitations.
|
||||
Default: `false`.
|
||||
- `plugins.entries.codex.config.codexPlugins.plugins.<key>.enabled`: enables a
|
||||
migrated plugin entry when global `codexPlugins.enabled` is also true.
|
||||
Default: `true` for explicit entries.
|
||||
- `plugins.entries.codex.config.codexPlugins.plugins.<key>.marketplaceName`:
|
||||
stable marketplace identity. V1 only supports `"openai-curated"`.
|
||||
- `plugins.entries.codex.config.codexPlugins.plugins.<key>.pluginName`: stable
|
||||
Codex plugin identity from migration, for example `"google-calendar"`.
|
||||
- `plugins.entries.codex.config.codexPlugins.plugins.<key>.allow_destructive_actions`:
|
||||
per-plugin destructive-action override. When omitted, the global
|
||||
`allow_destructive_actions` value is used.
|
||||
|
||||
`codexPlugins.enabled` is the global enablement directive. Explicit plugin
|
||||
entries written by migration are the durable install and repair eligibility set.
|
||||
`plugins["*"]` is not supported, there is no `install` switch, and local
|
||||
`marketplacePath` values are intentionally not config fields because they are
|
||||
host-specific.
|
||||
|
||||
`app/list` readiness checks are cached for one hour and refreshed
|
||||
asynchronously when stale. Codex thread app config is computed at Codex harness
|
||||
session establishment, not on every turn; use `/new`, `/reset`, or a gateway
|
||||
restart after changing native plugin config.
|
||||
|
||||
- `plugins.entries.firecrawl.config.webFetch`: Firecrawl web-fetch provider settings.
|
||||
- `apiKey`: Firecrawl API key (accepts SecretRef). Falls back to `plugins.entries.firecrawl.config.webSearch.apiKey`, legacy `tools.web.fetch.firecrawl.apiKey`, or `FIRECRAWL_API_KEY` env var.
|
||||
- `baseUrl`: Firecrawl API base URL (default: `https://api.firecrawl.dev`; self-hosted overrides must target private/internal endpoints).
|
||||
@@ -512,10 +443,6 @@ See [Inferred commitments](/concepts/commitments).
|
||||
value, so repeated failures from one localhost origin do not automatically
|
||||
lock out a different origin.
|
||||
- `tailscale.mode`: `serve` (tailnet only, loopback bind) or `funnel` (public, requires auth).
|
||||
- `tailscale.preserveFunnel`: when `true` and `tailscale.mode = "serve"`, OpenClaw
|
||||
checks `tailscale funnel status` before re-applying Serve at startup and skips
|
||||
it if an externally configured Funnel route already covers the gateway port.
|
||||
Default `false`.
|
||||
- `controlUi.allowedOrigins`: explicit browser-origin allowlist for Gateway WebSocket connects. Required when browser clients are expected from non-loopback origins.
|
||||
- `controlUi.chatMessageMaxWidth`: optional max-width for grouped Control UI chat messages. Accepts constrained CSS width values such as `960px`, `82%`, `min(1280px, 82%)`, and `calc(100% - 2rem)`.
|
||||
- `controlUi.dangerouslyAllowHostHeaderOriginFallback`: dangerous mode that enables Host-header origin fallback for deployments that intentionally rely on Host-header origin policy.
|
||||
|
||||
@@ -87,7 +87,7 @@ cat ~/.openclaw/openclaw.json
|
||||
- Legacy on-disk state migration (sessions/agent dir/WhatsApp auth).
|
||||
- Legacy plugin manifest contract key migration (`speechProviders`, `realtimeTranscriptionProviders`, `realtimeVoiceProviders`, `mediaUnderstandingProviders`, `imageGenerationProviders`, `videoGenerationProviders`, `webFetchProviders`, `webSearchProviders` → `contracts`).
|
||||
- Legacy cron store migration (`jobId`, `schedule.cron`, top-level delivery/payload fields, payload `provider`, simple `notify: true` webhook fallback jobs).
|
||||
- Legacy whole-agent runtime-policy cleanup; provider/model runtime policy is the active route selector.
|
||||
- Legacy agent runtime-policy migration to `agents.defaults.agentRuntime` and `agents.list[].agentRuntime`.
|
||||
- Stale plugin config cleanup when plugins are enabled; when `plugins.enabled=false`, stale plugin references are treated as inert containment config and are preserved.
|
||||
|
||||
</Accordion>
|
||||
@@ -109,7 +109,7 @@ cat ~/.openclaw/openclaw.json
|
||||
- Channel status warnings (probed from the running gateway).
|
||||
- Channel-specific permission checks live under `openclaw channels capabilities`; for example, Discord voice channel permissions are audited with `openclaw channels capabilities --channel discord --target channel:<channel-id>`.
|
||||
- WhatsApp responsiveness checks for degraded Gateway event-loop health with local TUI clients still running; `--fix` stops only verified local TUI clients.
|
||||
- Codex route repair for legacy `openai-codex/*` model refs in primary models, fallbacks, heartbeat/subagent/compaction overrides, hooks, channel model overrides, and session route pins; `--fix` rewrites them to `openai/*`, removes stale session/whole-agent runtime pins, and leaves canonical OpenAI agent refs on the default Codex harness.
|
||||
- Codex route repair for legacy `openai-codex/*` model refs in primary models, fallbacks, heartbeat/subagent/compaction overrides, hooks, channel model overrides, and session route pins; `--fix` rewrites them to `openai/*` and selects `agentRuntime.id: "codex"` only when the Codex plugin is installed, enabled, contributes the `codex` harness, and has usable OAuth. Otherwise it selects `agentRuntime.id: "pi"`.
|
||||
- Supervisor config audit (launchd/systemd/schtasks) with optional repair.
|
||||
- Embedded proxy environment cleanup for gateway services that captured shell `HTTP_PROXY` / `HTTPS_PROXY` / `NO_PROXY` values during install or update.
|
||||
- Gateway runtime best-practice checks (Node vs Bun, version-manager paths).
|
||||
@@ -269,8 +269,8 @@ That stages grounded durable candidates into the short-term dreaming store while
|
||||
In `--fix` / `--repair` mode, doctor rewrites affected default-agent and per-agent refs, including primary models, fallbacks, heartbeat/subagent/compaction overrides, hooks, channel model overrides, and stale persisted session route state:
|
||||
|
||||
- `openai-codex/gpt-*` becomes `openai/gpt-*`.
|
||||
- Stale whole-agent runtime config and persisted session runtime pins are removed because runtime selection is provider/model-scoped.
|
||||
- Explicit provider/model runtime policy is preserved.
|
||||
- The matching agent runtime becomes `agentRuntime.id: "codex"` only when Codex is installed, enabled, contributes the `codex` harness, and has usable OAuth.
|
||||
- Otherwise the matching agent runtime becomes `agentRuntime.id: "pi"`.
|
||||
- Existing model fallback lists are preserved with their legacy entries rewritten; copied per-model settings move from the legacy key to the canonical `openai/*` key.
|
||||
- Persisted session `modelProvider`/`providerOverride`, `model`/`modelOverride`, fallback notices, auth-profile pins, and Codex harness pins are repaired across all discovered agent session stores.
|
||||
- `/codex ...` means "control or bind a native Codex conversation from chat."
|
||||
@@ -489,7 +489,7 @@ That stages grounded durable candidates into the short-term dreaming store while
|
||||
<Accordion title="17. Gateway runtime best practices">
|
||||
Doctor warns when the gateway service runs on Bun or a version-managed Node path (`nvm`, `fnm`, `volta`, `asdf`, etc.). WhatsApp + Telegram channels require Node, and version-manager paths can break after upgrades because the service does not load your shell init. Doctor offers to migrate to a system Node install when available (Homebrew/apt/choco).
|
||||
|
||||
Newly installed or repaired macOS LaunchAgents use a canonical system PATH (`/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin`) instead of copying the interactive shell PATH, so Homebrew-managed system binaries remain available while Volta, asdf, fnm, pnpm, and other version-manager directories do not change which Node child processes resolve. Linux services still keep explicit environment roots (`NVM_DIR`, `FNM_DIR`, `VOLTA_HOME`, `ASDF_DATA_DIR`, `BUN_INSTALL`, `PNPM_HOME`) and stable user-bin directories, but guessed version-manager fallback directories are only written to the service PATH when those directories exist on disk.
|
||||
Newly installed or repaired macOS LaunchAgents use a canonical system PATH (`/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin`) instead of copying the interactive shell PATH, so Volta, asdf, fnm, pnpm, and other version-manager directories do not change which Node child processes resolve. Linux services still keep explicit environment roots (`NVM_DIR`, `FNM_DIR`, `VOLTA_HOME`, `ASDF_DATA_DIR`, `BUN_INSTALL`, `PNPM_HOME`) and stable user-bin directories, but guessed version-manager fallback directories are only written to the service PATH when those directories exist on disk.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="18. Config write + wizard metadata">
|
||||
|
||||
@@ -217,9 +217,7 @@ openclaw gateway restart
|
||||
openclaw gateway stop
|
||||
```
|
||||
|
||||
Use `openclaw gateway restart` for restarts. Do not chain `openclaw gateway stop` and `openclaw gateway start` as a restart substitute.
|
||||
|
||||
On macOS, `gateway stop` uses `launchctl bootout` by default — this removes the LaunchAgent from the current boot session without persisting a disable, so KeepAlive auto-recovery still works after unexpected crashes and `gateway start` re-enables cleanly. To persistently suppress auto-respawn across reboots, pass `--disable`: `openclaw gateway stop --disable`.
|
||||
Use `openclaw gateway restart` for restarts. Do not chain `openclaw gateway stop` and `openclaw gateway start`; on macOS, `gateway stop` intentionally disables the LaunchAgent before stopping it.
|
||||
|
||||
LaunchAgent labels are `ai.openclaw.gateway` (default) or `ai.openclaw.<profile>` (named profile). `openclaw doctor` audits and repairs service config drift.
|
||||
|
||||
|
||||
@@ -377,7 +377,7 @@ enumeration of `src/gateway/server-methods/*.ts`.
|
||||
- `talk.session.appendAudio` appends base64 PCM input audio to Gateway-owned realtime relay and transcription sessions.
|
||||
- `talk.session.startTurn`, `talk.session.endTurn`, and `talk.session.cancelTurn` drive managed-room turn lifecycle with stale-turn rejection before state is cleared.
|
||||
- `talk.session.cancelOutput` stops assistant audio output, primarily for VAD-gated barge-in in Gateway relay sessions.
|
||||
- `talk.session.submitToolResult` completes a provider tool call emitted by a Gateway-owned realtime relay session. Pass `options: { willContinue: true }` for interim tool output when a final result will follow.
|
||||
- `talk.session.submitToolResult` completes a provider tool call emitted by a Gateway-owned realtime relay session.
|
||||
- `talk.session.close` closes a Gateway-owned relay, transcription, or managed-room session and emits terminal Talk events.
|
||||
- `talk.mode` sets/broadcasts the current Talk mode state for WebChat/Control UI clients.
|
||||
- `talk.client.create` creates a client-owned realtime provider session using `webrtc` or `provider-websocket` while the Gateway owns config, credentials, instructions, and tool policy.
|
||||
@@ -411,7 +411,6 @@ enumeration of `src/gateway/server-methods/*.ts`.
|
||||
- `agents.list` returns configured agent entries, including effective model and runtime metadata.
|
||||
- `agents.create`, `agents.update`, and `agents.delete` manage agent records and workspace wiring.
|
||||
- `agents.files.list`, `agents.files.get`, and `agents.files.set` manage the bootstrap workspace files exposed for an agent.
|
||||
- `tasks.list`, `tasks.get`, and `tasks.cancel` expose the Gateway task ledger to SDK and operator clients.
|
||||
- `artifacts.list`, `artifacts.get`, and `artifacts.download` expose transcript-derived artifact summaries and downloads for an explicit `sessionKey`, `runId`, or `taskId` scope. Run and task queries resolve the owning session server-side and only return transcript media with matching provenance; unsafe or local URL sources return unsupported downloads instead of fetching server-side.
|
||||
- `environments.list` and `environments.status` expose read-only Gateway-local and node environment discovery for SDK clients.
|
||||
- `agent.identity.get` returns the effective assistant identity for an agent or session.
|
||||
@@ -500,34 +499,6 @@ enumeration of `src/gateway/server-methods/*.ts`.
|
||||
- Nodes may call `skills.bins` to fetch the current list of skill executables
|
||||
for auto-allow checks.
|
||||
|
||||
### Task ledger RPCs
|
||||
|
||||
Operator clients may inspect and cancel Gateway background task records through
|
||||
the task ledger RPCs. These methods return sanitized task summaries, not raw
|
||||
runtime state.
|
||||
|
||||
- `tasks.list` requires `operator.read`.
|
||||
- Params: optional `status` (`"queued"`, `"running"`, `"completed"`,
|
||||
`"failed"`, `"cancelled"`, or `"timed_out"`) or an array of those statuses,
|
||||
optional `agentId`, optional `sessionKey`, optional `limit` from `1` to
|
||||
`500`, and optional string `cursor`.
|
||||
- Result: `{ "tasks": TaskSummary[], "nextCursor"?: string }`.
|
||||
- `tasks.get` requires `operator.read`.
|
||||
- Params: `{ "taskId": string }`.
|
||||
- Result: `{ "task": TaskSummary }`.
|
||||
- Missing task ids return the Gateway not-found error shape.
|
||||
- `tasks.cancel` requires `operator.write`.
|
||||
- Params: `{ "taskId": string, "reason"?: string }`.
|
||||
- Result:
|
||||
`{ "found": boolean, "cancelled": boolean, "reason"?: string, "task"?: TaskSummary }`.
|
||||
- `found` reports whether the ledger had a matching task. `cancelled`
|
||||
reports whether the runtime accepted or recorded cancellation.
|
||||
|
||||
`TaskSummary` includes `id`, `status`, and optional metadata such as `kind`,
|
||||
`runtime`, `title`, `agentId`, `sessionKey`, `childSessionKey`, `ownerKey`,
|
||||
`runId`, `taskId`, `flowId`, `parentTaskId`, `sourceId`, timestamps, progress,
|
||||
terminal summary, and sanitized error text.
|
||||
|
||||
### Operator helper methods
|
||||
|
||||
- Operators may call `commands.list` (`operator.read`) to fetch the runtime
|
||||
|
||||
@@ -64,7 +64,6 @@ Rules of thumb:
|
||||
- `deny` always wins.
|
||||
- If `allow` is non-empty, everything else is treated as blocked.
|
||||
- Tool policy is the hard stop: `/exec` cannot override a denied `exec` tool.
|
||||
- Tool policy filters tool availability by name; it does not inspect side effects inside `exec`. If `exec` is allowed, denying `write`, `edit`, or `apply_patch` does not make shell commands read-only.
|
||||
- `/exec` only changes session defaults for authorized senders; it does not grant tool access.
|
||||
Provider tool keys accept either `provider` (e.g. `google-antigravity`) or `provider/model` (e.g. `openai/gpt-5.4`).
|
||||
|
||||
@@ -89,7 +88,6 @@ Available groups:
|
||||
- `group:runtime`: `exec`, `process`, `code_execution` (`bash` is accepted as
|
||||
an alias for `exec`)
|
||||
- `group:fs`: `read`, `write`, `edit`, `apply_patch`
|
||||
For read-only agents, deny `group:runtime` as well as mutating filesystem tools unless sandbox filesystem policy or a separate host boundary enforces the read-only constraint.
|
||||
- `group:sessions`: `sessions_list`, `sessions_history`, `sessions_send`, `sessions_spawn`, `sessions_yield`, `subagents`, `session_status`
|
||||
- `group:memory`: `memory_search`, `memory_get`
|
||||
- `group:web`: `web_search`, `x_search`, `web_fetch`
|
||||
|
||||
@@ -91,7 +91,6 @@ exhaustive):
|
||||
| `tools.exec.host_sandbox_no_sandbox_defaults` | warn | `exec host=sandbox` fails closed when sandbox is off | `tools.exec.host`, `agents.defaults.sandbox.mode` | no |
|
||||
| `tools.exec.host_sandbox_no_sandbox_agents` | warn | Per-agent `exec host=sandbox` fails closed when sandbox is off | `agents.list[].tools.exec.host`, `agents.list[].sandbox.mode` | no |
|
||||
| `tools.exec.security_full_configured` | warn/critical | Host exec is running with `security="full"` | `tools.exec.security`, `agents.list[].tools.exec.security` | no |
|
||||
| `tools.exec.fs_tools_disabled_but_exec_enabled` | warn | Filesystem tool policy does not make shell execution read-only | `tools.deny`, `agents.list[].tools.deny`, `agents.*.sandbox.workspaceAccess` | no |
|
||||
| `tools.exec.auto_allow_skills_enabled` | warn | Exec approvals trust skill bins implicitly | `~/.openclaw/exec-approvals.json` | no |
|
||||
| `tools.exec.allowlist_interpreter_without_strict_inline_eval` | warn | Interpreter allowlists permit inline eval without forced reapproval | `tools.exec.strictInlineEval`, `agents.list[].tools.exec.strictInlineEval`, exec approvals allowlist | no |
|
||||
| `tools.exec.safe_bins_interpreter_unprofiled` | warn | Interpreter/runtime bins in `safeBins` without explicit profiles broaden exec risk | `tools.exec.safeBins`, `tools.exec.safeBinProfiles`, `agents.list[].tools.exec.*` | no |
|
||||
|
||||
@@ -220,7 +220,6 @@ Advisory triage guidance:
|
||||
|
||||
- **Inbound access** (DM policies, group policies, allowlists): can strangers trigger the bot?
|
||||
- **Tool blast radius** (elevated tools + open rooms): could prompt injection turn into shell/file/network actions?
|
||||
- **Exec filesystem drift**: are mutating filesystem tools denied while `exec`/`process` remain available without sandbox filesystem constraints?
|
||||
- **Exec approval drift** (`security=full`, `autoAllowSkills`, interpreter allowlists without `strictInlineEval`): are host-exec guardrails still doing what you think they are?
|
||||
- `security="full"` is a broad posture warning, not proof of a bug. It is the chosen default for trusted personal-assistant setups; tighten it only when your threat model needs approval or allowlist guardrails.
|
||||
- **Network exposure** (Gateway bind/auth, Tailscale Serve/Funnel, weak/short auth tokens).
|
||||
|
||||
@@ -116,11 +116,6 @@ openclaw gateway --tailscale funnel --auth password
|
||||
- `tailscale.mode: "funnel"` refuses to start unless auth mode is `password` to avoid public exposure.
|
||||
- Set `gateway.tailscale.resetOnExit` if you want OpenClaw to undo `tailscale serve`
|
||||
or `tailscale funnel` configuration on shutdown.
|
||||
- Set `gateway.tailscale.preserveFunnel: true` to keep an externally configured
|
||||
`tailscale funnel` route alive across gateway restarts. When enabled and the
|
||||
gateway runs in `mode: "serve"`, OpenClaw checks `tailscale funnel status`
|
||||
before re-applying Serve and skips it when a Funnel route already covers the
|
||||
gateway port. The OpenClaw-managed Funnel password-only policy is unchanged.
|
||||
- `gateway.bind: "tailnet"` is a direct Tailnet bind (no HTTPS, no Serve/Funnel).
|
||||
- `gateway.bind: "auto"` prefers loopback; use `tailnet` if you want Tailnet-only.
|
||||
- Serve/Funnel only expose the **Gateway control UI + WS**. Nodes connect over
|
||||
|
||||
@@ -97,7 +97,6 @@ If a tool is not allowed by policy, the endpoint returns **404**.
|
||||
Important boundary notes:
|
||||
|
||||
- Exec approvals are operator guardrails, not a separate authorization boundary for this HTTP endpoint. If a tool is reachable here via Gateway auth + tool policy, `/tools/invoke` does not add an extra per-call approval prompt.
|
||||
- If `exec` is reachable here, treat it as a mutating shell surface. Denying `write`, `edit`, `apply_patch`, or HTTP filesystem-write tools does not make shell execution read-only.
|
||||
- Do not share Gateway bearer credentials with untrusted callers. If you need separation across trust boundaries, run separate gateways (and ideally separate OS users/hosts).
|
||||
|
||||
Gateway HTTP also applies a hard deny list by default (even if session policy allows the tool):
|
||||
|
||||
@@ -96,9 +96,9 @@ add Node's sync I/O trace flag through the source runner:
|
||||
OPENCLAW_TRACE_SYNC_IO=1 pnpm openclaw gateway --force
|
||||
```
|
||||
|
||||
`pnpm gateway:watch` leaves this flag disabled by default for the watched
|
||||
Gateway child. Set `OPENCLAW_TRACE_SYNC_IO=1` when you explicitly want Node
|
||||
sync I/O trace output in watch mode.
|
||||
`pnpm gateway:watch` enables this flag by default for the watched Gateway child.
|
||||
Set `OPENCLAW_TRACE_SYNC_IO=0` to suppress Node sync I/O trace output in watch
|
||||
mode.
|
||||
|
||||
## Gateway watch mode
|
||||
|
||||
|
||||
@@ -594,11 +594,12 @@ and troubleshooting see the main [FAQ](/help/faq).
|
||||
|
||||
<Accordion title="How does Codex auth work?">
|
||||
OpenClaw supports **OpenAI Code (Codex)** via OAuth (ChatGPT sign-in). Use
|
||||
`openai/gpt-5.5` for the common setup: ChatGPT/Codex subscription auth plus
|
||||
native Codex app-server execution. `openai-codex/gpt-*` model refs are
|
||||
legacy config repaired by `openclaw doctor --fix`. Direct OpenAI API-key
|
||||
access remains available for non-agent OpenAI API surfaces and for agent
|
||||
models through an ordered `openai-codex` API-key profile.
|
||||
`openai/gpt-5.5` with `agentRuntime.id: "codex"` for the common setup:
|
||||
ChatGPT/Codex subscription auth plus native Codex app-server execution. Use
|
||||
`openai-codex/gpt-5.5` only when you want Codex OAuth through the default
|
||||
Codex runtime. Direct OpenAI API-key access remains available for non-agent
|
||||
OpenAI API surfaces and for agent models through an ordered
|
||||
`openai-codex` API-key profile.
|
||||
See [Model providers](/concepts/model-providers) and [Onboarding (CLI)](/start/wizard).
|
||||
</Accordion>
|
||||
|
||||
|
||||
@@ -150,7 +150,7 @@ troubleshooting, see the main [FAQ](/help/faq).
|
||||
- **Native Codex coding agent:** set `agents.defaults.model.primary` to `openai/gpt-5.5`. Sign in with `openclaw models auth login --provider openai-codex` when you want ChatGPT/Codex subscription auth.
|
||||
- **Direct OpenAI API tasks outside the agent loop:** configure `OPENAI_API_KEY` for images, embeddings, speech, realtime, and other non-agent OpenAI API surfaces.
|
||||
- **OpenAI agent API-key auth:** use `/model openai/gpt-5.5` with an ordered `openai-codex` API-key profile.
|
||||
- **Sub-agents:** route coding tasks to a Codex-focused agent with its own `openai/gpt-5.5` model.
|
||||
- **Sub-agents:** route coding tasks to a Codex-only agent with its own model and `agentRuntime` default.
|
||||
|
||||
See [Models](/concepts/models) and [Slash commands](/tools/slash-commands).
|
||||
|
||||
|
||||
@@ -409,7 +409,7 @@ lives on the [First-run FAQ](/help/faq-first-run).
|
||||
openclaw skills update --all
|
||||
```
|
||||
|
||||
Native installs land in the active workspace `skills/` directory. For shared skills across agents, place them in `~/.openclaw/skills/<name>/SKILL.md`. If only some agents should see a shared install, configure `agents.defaults.skills` or `agents.list[].skills`. Some skills expect binaries installed via Homebrew; on Linux that means Linuxbrew (see the Homebrew Linux FAQ entry above). See [Skills](/tools/skills), [Skills config](/tools/skills-config), and [ClawHub](/clawhub).
|
||||
Native installs land in the active workspace `skills/` directory. For shared skills across agents, place them in `~/.openclaw/skills/<name>/SKILL.md`. If only some agents should see a shared install, configure `agents.defaults.skills` or `agents.list[].skills`. Some skills expect binaries installed via Homebrew; on Linux that means Linuxbrew (see the Homebrew Linux FAQ entry above). See [Skills](/tools/skills), [Skills config](/tools/skills-config), and [ClawHub](/tools/clawhub).
|
||||
|
||||
</Accordion>
|
||||
|
||||
|
||||
@@ -285,8 +285,8 @@ Docker notes:
|
||||
- Goal: validate the plugin-owned Codex harness through the normal gateway
|
||||
`agent` method:
|
||||
- load the bundled `codex` plugin
|
||||
- select `openai/gpt-5.5`, which routes OpenAI agent turns through Codex by default
|
||||
- send a first gateway agent turn to `openai/gpt-5.5` with the Codex harness selected
|
||||
- select `OPENCLAW_AGENT_RUNTIME=codex`
|
||||
- send a first gateway agent turn to `openai/gpt-5.5` with the Codex harness forced
|
||||
- send a second turn to the same OpenClaw session and verify the app-server
|
||||
thread can resume
|
||||
- run `/codex status` and `/codex models` through the same gateway command
|
||||
@@ -300,8 +300,8 @@ Docker notes:
|
||||
- Optional image probe: `OPENCLAW_LIVE_CODEX_HARNESS_IMAGE_PROBE=1`
|
||||
- Optional MCP/tool probe: `OPENCLAW_LIVE_CODEX_HARNESS_MCP_PROBE=1`
|
||||
- Optional Guardian probe: `OPENCLAW_LIVE_CODEX_HARNESS_GUARDIAN_PROBE=1`
|
||||
- The smoke forces provider/model `agentRuntime.id: "codex"` so a broken Codex
|
||||
harness cannot pass by silently falling back to PI.
|
||||
- The smoke uses `agentRuntime.id: "codex"` so a broken Codex harness cannot
|
||||
pass by silently falling back to PI.
|
||||
- Auth: Codex app-server auth from the local Codex subscription login. Docker
|
||||
smokes can also provide `OPENAI_API_KEY` for non-Codex probes when applicable,
|
||||
plus optional copied `~/.codex/auth.json` and `~/.codex/config.toml`.
|
||||
|
||||
@@ -81,14 +81,6 @@ When debugging real providers/models (requires real creds):
|
||||
`OPENCLAW_LIVE_CODEX_HARNESS_IMAGE_PROBE=0 OPENCLAW_LIVE_CODEX_HARNESS_MCP_PROBE=0 OPENCLAW_LIVE_CODEX_HARNESS_GUARDIAN_PROBE=0 OPENCLAW_LIVE_CODEX_HARNESS_SUBAGENT_PROBE=1 pnpm test:docker:live-codex-harness`.
|
||||
This exits after the sub-agent probe unless
|
||||
`OPENCLAW_LIVE_CODEX_HARNESS_SUBAGENT_ONLY=0` is set.
|
||||
- Codex on-demand install smoke: `pnpm test:docker:codex-on-demand`
|
||||
- Installs the packaged OpenClaw tarball in Docker, runs OpenAI API-key
|
||||
onboarding, and verifies the Codex plugin plus `@openai/codex` dependency
|
||||
were downloaded into the managed npm root on demand.
|
||||
- Live plugin tool dependency smoke: `pnpm test:docker:live-plugin-tool`
|
||||
- Packs a fixture plugin with a real `slugify` dependency, installs it through
|
||||
`npm-pack:`, verifies the dependency under the managed npm root, then asks a
|
||||
live OpenAI model to call the plugin tool and return the hidden slug.
|
||||
- Crestodian rescue command smoke: `pnpm test:live:crestodian-rescue-channel`
|
||||
- Opt-in belt-and-suspenders check for the message-channel rescue command
|
||||
surface. It exercises `/crestodian status`, queues a persistent model
|
||||
@@ -203,10 +195,6 @@ inside every shard.
|
||||
- Installs an OpenClaw package candidate in Docker, runs installed-package
|
||||
onboarding, configures Telegram through the installed CLI, then reuses the
|
||||
live Telegram QA lane with that installed package as the SUT Gateway.
|
||||
- The wrapper mounts only the `qa-lab` harness source from the checkout; the
|
||||
installed package owns `dist`, `openclaw/plugin-sdk`, and bundled plugin
|
||||
runtime so the lane does not mix current checkout plugins into the package
|
||||
under test.
|
||||
- Defaults to `OPENCLAW_NPM_TELEGRAM_PACKAGE_SPEC=openclaw@beta`; set
|
||||
`OPENCLAW_NPM_TELEGRAM_PACKAGE_TGZ=/path/to/openclaw-current.tgz` or
|
||||
`OPENCLAW_CURRENT_PACKAGE_TGZ` to test a resolved local tarball instead of
|
||||
@@ -319,7 +307,6 @@ gh workflow run package-acceptance.yml --ref main \
|
||||
- Runs the Telegram live QA lane against a real private group using the driver and SUT bot tokens from env.
|
||||
- Requires `OPENCLAW_QA_TELEGRAM_GROUP_ID`, `OPENCLAW_QA_TELEGRAM_DRIVER_BOT_TOKEN`, and `OPENCLAW_QA_TELEGRAM_SUT_BOT_TOKEN`. The group id must be the numeric Telegram chat id.
|
||||
- Supports `--credential-source convex` for shared pooled credentials. Use env mode by default, or set `OPENCLAW_QA_CREDENTIAL_SOURCE=convex` to opt into pooled leases.
|
||||
- Defaults cover canary, mention gating, command addressing, `/status`, bot-to-bot mentioned replies, and core native command replies. `mock-openai` defaults also cover deterministic reply-chain and Telegram final-message streaming regressions. Use `--list-scenarios` for optional probes such as `session_status`.
|
||||
- Exits non-zero when any scenario fails. Use `--allow-failures` when you
|
||||
want artifacts without a failing exit code.
|
||||
- Requires two distinct bots in the same private group, with the SUT bot exposing a Telegram username.
|
||||
@@ -331,9 +318,8 @@ Live transport lanes share one standard contract so new transports do not drift;
|
||||
### Shared Telegram credentials via Convex (v1)
|
||||
|
||||
When `--credential-source convex` (or `OPENCLAW_QA_CREDENTIAL_SOURCE=convex`) is enabled for
|
||||
live transport QA, QA lab acquires an exclusive lease from a Convex-backed pool, heartbeats that
|
||||
lease while the lane is running, and releases the lease on shutdown. The section name predates
|
||||
Discord, Slack, and WhatsApp support; the lease contract is shared across kinds.
|
||||
`openclaw qa telegram`, QA lab acquires an exclusive lease from a Convex-backed pool, heartbeats
|
||||
that lease while the lane is running, and releases the lease on shutdown.
|
||||
|
||||
Reference Convex project scaffold:
|
||||
|
||||
@@ -407,16 +393,6 @@ Payload shape for Telegram kind:
|
||||
- `groupId` must be a numeric Telegram chat id string.
|
||||
- `admin/add` validates this shape for `kind: "telegram"` and rejects malformed payloads.
|
||||
|
||||
Broker-validated multi-channel payloads:
|
||||
|
||||
- Discord: `{ guildId: string, channelId: string, driverBotToken: string, sutBotToken: string, sutApplicationId: string, voiceChannelId?: string }`
|
||||
- WhatsApp: `{ driverPhoneE164: string, sutPhoneE164: string, driverAuthArchiveBase64: string, sutAuthArchiveBase64: string, groupJid?: string }`
|
||||
|
||||
Slack lanes can also lease from the pool, but Slack payload validation currently
|
||||
lives in the Slack QA runner rather than the broker. Use
|
||||
`{ channelId: string, driverBotToken: string, sutBotToken: string, sutAppToken: string }`
|
||||
for Slack rows.
|
||||
|
||||
### Adding a channel to QA
|
||||
|
||||
The architecture and scenario-helper names for new channel adapters live in [QA overview → Adding a channel](/concepts/qa-e2e-automation#adding-a-channel). The minimum bar: implement the transport runner on the shared `qa-lab` host seam, declare `qaRunners` in the plugin manifest, mount as `openclaw qa <runner>`, and author scenarios under `qa/scenarios/`.
|
||||
|
||||
@@ -50,18 +50,9 @@ DigitalOcean is the simplest paid VPS path. If you prefer cheaper or free option
|
||||
|
||||
# Install OpenClaw
|
||||
curl -fsSL https://openclaw.ai/install.sh | bash
|
||||
|
||||
# Create the non-root user that will own OpenClaw state and services.
|
||||
adduser openclaw
|
||||
usermod -aG sudo openclaw
|
||||
loginctl enable-linger openclaw
|
||||
|
||||
su - openclaw
|
||||
openclaw --version
|
||||
```
|
||||
|
||||
Use the root shell only for system bootstrap. Run OpenClaw commands as the non-root `openclaw` user so state lives under `/home/openclaw/.openclaw/` and the Gateway installs as that user's systemd service.
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Run onboarding">
|
||||
@@ -106,8 +97,8 @@ DigitalOcean is the simplest paid VPS path. If you prefer cheaper or free option
|
||||
**Option B: Tailscale Serve**
|
||||
|
||||
```bash
|
||||
curl -fsSL https://tailscale.com/install.sh | sudo sh
|
||||
sudo tailscale up
|
||||
curl -fsSL https://tailscale.com/install.sh | sh
|
||||
tailscale up
|
||||
openclaw config set gateway.tailscale.mode serve
|
||||
openclaw gateway restart
|
||||
```
|
||||
|
||||
@@ -427,7 +427,8 @@ See [ClawDock](/install/clawdock) for the full helper guide.
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Base image metadata">
|
||||
The main Docker runtime image uses `node:24-bookworm-slim` and includes `tini` as the entrypoint init process (PID 1) to ensure zombie processes are reaped and signals are handled correctly in long-running containers. It publishes OCI base-image annotations including `org.opencontainers.image.base.name`,
|
||||
The main Docker runtime image uses `node:24-bookworm-slim` and publishes OCI
|
||||
base-image annotations including `org.opencontainers.image.base.name`,
|
||||
`org.opencontainers.image.source`, and others. The Node base digest is
|
||||
refreshed through Dependabot Docker base-image PRs; release builds do not run
|
||||
a distro upgrade layer. See
|
||||
|
||||
@@ -78,8 +78,6 @@ read_when:
|
||||
destination = "/data"
|
||||
```
|
||||
|
||||
The OpenClaw Docker image uses `tini` as its entrypoint. Fly process commands replace Docker `CMD` without replacing `ENTRYPOINT`, so the process still runs under `tini`.
|
||||
|
||||
**Key settings:**
|
||||
|
||||
| Setting | Why |
|
||||
|
||||
@@ -81,8 +81,8 @@ Supported keys:
|
||||
providers: {
|
||||
openai: {
|
||||
apiKey: "openai_api_key",
|
||||
model: "gpt-realtime-2",
|
||||
voice: "cedar",
|
||||
model: "gpt-realtime",
|
||||
voice: "alloy",
|
||||
},
|
||||
},
|
||||
mode: "realtime",
|
||||
@@ -102,11 +102,8 @@ Defaults:
|
||||
- `providers.elevenlabs.modelId`: defaults to `eleven_v3` when unset.
|
||||
- `providers.mlx.modelId`: defaults to `mlx-community/Soprano-80M-bf16` when unset.
|
||||
- `providers.elevenlabs.apiKey`: falls back to `ELEVENLABS_API_KEY` (or gateway shell profile if available).
|
||||
- `consultThinkingLevel`: optional thinking level override for the full OpenClaw agent run behind realtime `openclaw_agent_consult` calls.
|
||||
- `consultFastMode`: optional fast-mode override for realtime `openclaw_agent_consult` calls.
|
||||
- `realtime.provider`: selects the active browser/server realtime voice provider. Use `openai` for WebRTC, `google` for provider WebSocket, or a bridge-only provider through Gateway relay.
|
||||
- `realtime.providers.<provider>` stores provider-owned realtime config. The browser receives only ephemeral or constrained session credentials, never a standard API key.
|
||||
- `realtime.providers.openai.voice`: built-in OpenAI Realtime voice id. Current `gpt-realtime-2` voices are `alloy`, `ash`, `ballad`, `coral`, `echo`, `sage`, `shimmer`, `verse`, `marin`, and `cedar`; `marin` and `cedar` are recommended for best quality.
|
||||
- `realtime.brain`: `agent-consult` routes realtime tool calls through Gateway policy; `direct-tools` is owner-only compatibility behavior; `none` is for transcription or external orchestration.
|
||||
- `talk.catalog` exposes each provider's valid modes, transports, brain strategies, realtime audio formats, and capability flags so first-party Talk clients can avoid unsupported combinations.
|
||||
- Streaming transcription providers are discovered through `talk.catalog.transcription`. The current Gateway relay uses the Voice Call streaming provider config until the dedicated Talk transcription config surface is added.
|
||||
|
||||
@@ -764,21 +764,10 @@ built-in implicit providers:
|
||||
Later providers win on key collision, so plugins can intentionally override a
|
||||
built-in provider entry with the same provider id.
|
||||
|
||||
Plugins can also publish read-only model rows through
|
||||
`api.registerModelCatalogProvider({ provider, kinds, staticCatalog, liveCatalog
|
||||
})`. This is the forward path for list/help/picker surfaces and supports
|
||||
`text`, `image_generation`, `video_generation`, and `music_generation` rows.
|
||||
Provider plugins still own live endpoint calls, token exchange, and vendor
|
||||
response mapping; core owns the common row shape, source labels, and media tool
|
||||
help formatting. Media-generation provider registrations synthesize static
|
||||
catalog rows automatically from `defaultModel`, `models`, and `capabilities`.
|
||||
|
||||
Compatibility:
|
||||
|
||||
- `discovery` still works as a legacy alias, but emits a deprecation warning
|
||||
- `discovery` still works as a legacy alias
|
||||
- if both `catalog` and `discovery` are registered, OpenClaw uses `catalog`
|
||||
- `augmentModelCatalog` is deprecated; bundled providers should publish
|
||||
supplemental rows through `registerModelCatalogProvider`
|
||||
|
||||
## Read-only channel inspection
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ generation, video generation, web fetch, web search, agent tools, or any
|
||||
combination.
|
||||
|
||||
You do not need to add your plugin to the OpenClaw repository. Publish to
|
||||
[ClawHub](/clawhub) and users install with
|
||||
[ClawHub](/tools/clawhub) and users install with
|
||||
`openclaw plugins install clawhub:<package-name>`. Bare package specs still
|
||||
install from npm during the launch cutover.
|
||||
|
||||
|
||||
@@ -96,6 +96,9 @@ Computer Use available before a thread starts:
|
||||
agents: {
|
||||
defaults: {
|
||||
model: "openai/gpt-5.5",
|
||||
agentRuntime: {
|
||||
id: "codex",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -111,8 +114,9 @@ register the bundled Codex marketplace from
|
||||
fails. If setup still cannot make the MCP server available, the turn fails
|
||||
before the thread starts.
|
||||
|
||||
After changing Computer Use config, use `/new` or `/reset` in the affected chat
|
||||
before testing if an existing Codex thread has already started.
|
||||
Existing sessions keep their runtime and Codex thread binding. After changing
|
||||
`agentRuntime` or Computer Use config, use `/new` or `/reset` in the affected
|
||||
chat before testing.
|
||||
|
||||
## Commands
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user