mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-07 06:21:32 +08:00
Compare commits
1 Commits
feat/comma
...
codex/runt
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc26444f6d |
@@ -1,37 +0,0 @@
|
||||
# Telegram Maintainer Decisions
|
||||
|
||||
Use this page during Telegram PR review. These are intentional maintainer decisions, not incidental implementation details.
|
||||
|
||||
Verified against Telegram Bot API 10.0, May 8 2026.
|
||||
|
||||
## Streaming
|
||||
|
||||
- Do not reintroduce `sendMessageDraft` for answer streaming. Telegram drafts are ephemeral 30-second previews in private chats; final delivery still requires a separate `sendMessage`. OpenClaw uses `sendMessage` plus `editMessageText`, then finalizes in place so the user sees one persistent answer.
|
||||
- Streaming owns one visible preview message. Edit it forward. Do not send an extra final bubble unless the final edit genuinely failed.
|
||||
- Keep the first-preview debounce. If a provider sends token-sized deltas, coalesce them into cumulative preview text instead of removing the debounce.
|
||||
- Respect Telegram limits in the Telegram layer. Text over 4096 chars chains into continuation messages. Polls keep the current Bot API 12-option cap.
|
||||
|
||||
## Telegram API Ownership
|
||||
|
||||
- Prefer grammY primitives and Telegram-native helpers when they model the behavior directly. Avoid custom Bot API wrappers for behavior grammY already owns.
|
||||
- Throttling is bot-token scoped. All Telegram API clients for the same token share one grammY `apiThrottler()` instance.
|
||||
- Do not silently retry failed topic sends without topic metadata. A wrong-surface success is worse than a loud Telegram error.
|
||||
- DM topics and forum topics are distinct. `direct_messages_topic_id` and `message_thread_id` are not interchangeable.
|
||||
|
||||
## Context And Authorization
|
||||
|
||||
- Reply context comes from OpenClaw-observed messages. Bot API updates expose `reply_to_message`, but there is no arbitrary `getMessage(chat, id)` hydration path later.
|
||||
- Current local chat context must outrank stale reply ancestry in the prompt. Old replied-to messages should not look like the active conversation.
|
||||
- Pairing is DM-only. Group and topic authorization need explicit config allowlists.
|
||||
- Telegram allowlists use numeric sender IDs. Usernames are optional, mutable, and not a reliable arbitrary-user lookup key in the Bot API.
|
||||
- Group and channel visible replies are policy-controlled. Normal room replies stay private unless `messages.groupChat.visibleReplies: "automatic"` is set or the agent explicitly calls `message.send`.
|
||||
|
||||
## Interactive Surfaces
|
||||
|
||||
- Native callbacks stay structured. Approval, native command, plugin, select, and multiselect callbacks must not fall through as raw callback text.
|
||||
- Preserve callback values exactly, including delimiters such as `env|prod`.
|
||||
- Native slash commands should remain fast-pathable before full workspace and agent-turn setup.
|
||||
|
||||
## Review Standard
|
||||
|
||||
Telegram behavior PRs need real Telegram proof when they touch transport, streaming, topics, callbacks, authorization, or reply context. Prefer the bot-to-bot QA lane or an equivalent live Telegram probe over synthetic-only validation.
|
||||
@@ -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
|
||||
|
||||
@@ -1,234 +0,0 @@
|
||||
---
|
||||
name: openclaw-docs
|
||||
description: Write or review high-quality OpenClaw developer documentation.
|
||||
dependencies: []
|
||||
---
|
||||
|
||||
# OpenClaw Docs
|
||||
|
||||
## Overview
|
||||
|
||||
Use this skill when writing, editing, or reviewing OpenClaw developer documentation for APIs, SDKs, CLI tools, integrations, quickstarts, platform guides, or technical product docs.
|
||||
|
||||
Write documentation that is concise, helpful, and comprehensive: fast for first success, precise for production, and easy to scan when debugging.
|
||||
|
||||
## Core Model
|
||||
|
||||
Use an OpenClaw documentation model, strengthened by Write the Docs principles:
|
||||
|
||||
- Lead with what the developer is trying to do.
|
||||
- Give one recommended path before alternatives.
|
||||
- Make examples runnable and realistic.
|
||||
- Keep guides task-oriented and references exhaustive.
|
||||
- Explain production risks exactly where developers can make mistakes.
|
||||
- Link concepts, guides, API references, SDKs, testing, and troubleshooting so readers can move between them without rereading.
|
||||
- Treat docs as part of the product lifecycle: draft them before or alongside implementation, review them with code, and keep them current.
|
||||
- Make each page discoverable, addressable, cumulative, complete within its stated scope, and easy to skim.
|
||||
|
||||
## Structure
|
||||
|
||||
Choose the page type before writing:
|
||||
|
||||
- Overview: route readers to the right product, integration path, or guide.
|
||||
- Quickstart: get a new user to a working result with the fewest safe steps.
|
||||
- Topic page: give an end-to-end overview of a major domain entity, with setup,
|
||||
key subtopics, troubleshooting, and links to deeper references.
|
||||
- Guide: explain one workflow from prerequisites to production readiness.
|
||||
- API reference: define every object, endpoint, parameter, enum, response, error, and version rule.
|
||||
- SDK or CLI reference: document install, auth, commands or methods, options, examples, and failure modes.
|
||||
- Testing guide: show sandbox setup, fixtures, test data, simulated failures, and live-mode differences.
|
||||
- Troubleshooting guide: map symptoms to checks, causes, and fixes.
|
||||
|
||||
Use this default topic page structure:
|
||||
|
||||
1. Title: name the major entity or surface.
|
||||
2. Overview: explain what it is, what it owns, and what it does not own.
|
||||
3. Requirements: include only when setup needs specific accounts, versions,
|
||||
permissions, plugins, operating systems, or credentials.
|
||||
4. Quickstart: show the recommended setup path and smallest reliable verification.
|
||||
5. Configuration: show the minimum configuration needed to use the surface,
|
||||
common variants users must choose between, and where each option is set:
|
||||
CLI, config file, environment variable, plugin manifest, dashboard, or API.
|
||||
6. Subtopics: organize the entity's major concepts, workflows, and decisions by
|
||||
reader intent.
|
||||
7. Troubleshooting: diagnose common observable failures.
|
||||
8. Related: link to guides, references, commands, concepts, and adjacent topics.
|
||||
|
||||
Topic pages may be longer than quickstarts, but they should not become exhaustive
|
||||
references. Move field tables, API contracts, narrow internals, legacy details,
|
||||
and rare debugging workflows to linked reference or troubleshooting pages when
|
||||
they interrupt the end-to-end overview.
|
||||
|
||||
For configuration, keep task-critical options inline. Link to reference docs for
|
||||
full option lists, defaults, enums, generated schemas, and advanced settings. Do
|
||||
not duplicate exhaustive config reference tables in topic pages unless the topic
|
||||
page is itself the reference.
|
||||
|
||||
Use this default guide structure:
|
||||
|
||||
1. Title: name the outcome, not the implementation detail.
|
||||
2. Opening: state what the reader can accomplish in one or two sentences.
|
||||
3. Before you begin: list accounts, keys, permissions, versions, tools, and assumptions.
|
||||
4. Choose a path: compare options only when the reader must decide.
|
||||
5. Steps: use verb-led headings with code, expected output, and checks.
|
||||
6. Test: show the smallest reliable proof that the integration works.
|
||||
7. Production readiness: cover security, idempotency, retries, limits, observability, migrations, and cleanup.
|
||||
8. Troubleshooting: include common errors near the workflow that causes them.
|
||||
9. See also: link to concepts, API references, SDK docs, and adjacent guides.
|
||||
|
||||
Keep navigation user-intent based. Do not force readers to understand internal product taxonomy before they can pick a task.
|
||||
|
||||
## Documentation Lifecycle
|
||||
|
||||
Write and maintain docs with the same discipline as code:
|
||||
|
||||
- Draft docs early enough to expose unclear product, API, CLI, or config design.
|
||||
- Keep docs source near the code, config, command, plugin, or protocol it describes when the repo layout allows it.
|
||||
- Avoid duplicate truth. If the same contract appears in multiple places, pick the canonical page and link to it.
|
||||
- Update docs in the same change as behavior, config, API, CLI, plugin, or troubleshooting changes.
|
||||
- Remove, redirect, or clearly mark stale docs. Incorrect docs are worse than missing docs.
|
||||
- Involve the right reviewers: code owners for behavior, support or QA for user failure modes, and docs maintainers for structure and style.
|
||||
- Preserve older-version guidance only when users need it; otherwise document the current supported behavior.
|
||||
|
||||
Do not use FAQs as a dumping ground for unrelated material. Promote recurring questions into task, concept, troubleshooting, or reference pages.
|
||||
|
||||
## Writing Style
|
||||
|
||||
Write in a direct, practical voice:
|
||||
|
||||
- Use present tense and active voice.
|
||||
- Address the reader as "you" when giving instructions.
|
||||
- Prefer short paragraphs and scannable lists.
|
||||
- Use concrete nouns: "agent profile", "Gateway webhook", "plugin manifest", "session state".
|
||||
- Put caveats exactly where they affect the step.
|
||||
- Avoid marketing language, hype, generic benefits, and vague claims.
|
||||
- Avoid long conceptual lead-ins before the first actionable step.
|
||||
- Do not over-explain common developer concepts unless the product has a nonstandard contract.
|
||||
- Define OpenClaw-specific jargon and abbreviations before first use.
|
||||
- Use sentence case for headings unless an OpenClaw product name, command, or identifier requires capitalization.
|
||||
- Use descriptive link text that names the destination or action; avoid vague links such as "this page" or "click here".
|
||||
- Avoid culturally specific idioms, violent idioms, and jokes that make docs harder to translate or scan.
|
||||
- Write accessible prose: do not rely on color, screenshots, or visual position as the only way to understand an instruction.
|
||||
|
||||
Use headings that describe actions or reference surfaces:
|
||||
|
||||
- Good: "Create an agent", "Configure a Slack channel", "Repair plugin installation"
|
||||
- Avoid: "How it works", "Under the hood", "Important notes" unless the section truly needs that shape
|
||||
|
||||
Use precise modal language:
|
||||
|
||||
- Use "must" for required behavior.
|
||||
- Use "can" for optional capability.
|
||||
- Use "recommended" for the default path.
|
||||
- Use "avoid" for known footguns.
|
||||
- Explain "why" only when it changes a developer decision.
|
||||
|
||||
## Detail Level
|
||||
|
||||
Vary detail by page type:
|
||||
|
||||
- Overview pages: be brief; help readers choose.
|
||||
- Quickstarts: be procedural; include only what is needed for first success.
|
||||
- Guides: be complete for one workflow; include decisions, side effects, and failure handling.
|
||||
- References: be exhaustive; document every field, default, enum, nullable value, constraint, response, and error.
|
||||
- Troubleshooting: be explicit; assume the reader is blocked and needs observable checks.
|
||||
|
||||
Go deep where mistakes are expensive:
|
||||
|
||||
- Authentication and secret handling
|
||||
- Money movement, billing, permissions, and irreversible actions
|
||||
- Webhooks, retries, duplicate events, and ordering
|
||||
- Idempotency and concurrency
|
||||
- Sandbox versus production differences
|
||||
- Versioning, migrations, and backwards compatibility
|
||||
- Limits, rate limits, quotas, and timeouts
|
||||
- Error codes and recovery paths
|
||||
- Data retention, privacy, and compliance-sensitive behavior
|
||||
|
||||
Do not bury this detail in a distant reference if developers need it to complete the task safely.
|
||||
|
||||
## Examples
|
||||
|
||||
Make examples production-shaped, even when using test data:
|
||||
|
||||
- Prefer complete copy-pasteable commands or snippets.
|
||||
- Use realistic variable names and values.
|
||||
- Mark placeholders clearly with angle-bracket names such as `<API_KEY>` or `<CUSTOMER_ID>`.
|
||||
- Show expected success output after commands.
|
||||
- Show full request and response examples for API references when response shape matters.
|
||||
- Keep one conceptual unit per code block.
|
||||
- Use language-specific code fences.
|
||||
- Avoid toy examples that hide required setup, auth, error handling, or cleanup.
|
||||
|
||||
When multiple languages are useful, keep the same scenario across languages so readers can compare equivalents.
|
||||
|
||||
## Discoverability and Navigation
|
||||
|
||||
Design every page so readers can find it, link to it, and decide quickly whether it answers their question:
|
||||
|
||||
- Use goal-oriented titles and headings that match likely search terms.
|
||||
- Start each page with a concise answer to "what can I do here?"
|
||||
- Include metadata or frontmatter required by the OpenClaw docs index.
|
||||
- Add "Read when" hints for docs-list routing when creating or changing OpenClaw docs pages that participate in the docs index.
|
||||
- Link from likely entry points, not only from nearby internal taxonomy pages.
|
||||
- Keep section headings stable enough for links from issues, PRs, support replies, and chat answers.
|
||||
- Order tutorials and examples from prerequisites to advanced tasks; order reference pages alphabetically or topically when that helps lookup.
|
||||
- State scope up front when a page is intentionally partial.
|
||||
|
||||
## API Reference Pattern
|
||||
|
||||
For endpoints, methods, objects, or commands, include:
|
||||
|
||||
1. Short purpose statement.
|
||||
2. Auth or permission requirements.
|
||||
3. Request shape, including path, query, headers, and body fields.
|
||||
4. Parameter table with type, requiredness, default, constraints, enum values, and side effects.
|
||||
5. Return shape with object lifecycle states.
|
||||
6. Error cases with codes, causes, and recovery guidance.
|
||||
7. Runnable example request.
|
||||
8. Representative successful response.
|
||||
9. Related guides and adjacent reference pages.
|
||||
|
||||
For nested objects, document child fields near their parent. Do not make readers jump across pages to understand the shape of a single request.
|
||||
|
||||
## Verification
|
||||
|
||||
Verify docs changes like product changes:
|
||||
|
||||
- Run the relevant docs build, docs index, formatter, link checker, or generated-doc check when available.
|
||||
- Run commands, snippets, and examples that the page tells users to run whenever feasible.
|
||||
- Confirm screenshots, UI labels, CLI output, config keys, flags, defaults, errors, and file paths match current behavior.
|
||||
- Prefer executable checks over prose-only review for API, CLI, config, generated reference, and troubleshooting docs.
|
||||
- If a verification step is not feasible, say what was not verified and why.
|
||||
|
||||
## Completeness Checks
|
||||
|
||||
Before finalizing a page, verify:
|
||||
|
||||
- The first screen tells readers what they can accomplish.
|
||||
- The recommended path is obvious.
|
||||
- Prerequisites are explicit and testable.
|
||||
- Examples can run with documented inputs.
|
||||
- The page has a clear audience: user, operator, plugin author, contributor, or maintainer.
|
||||
- Test-mode and production-mode behavior are separated.
|
||||
- Security-sensitive values are never exposed in examples.
|
||||
- Every warning is attached to the step where it matters.
|
||||
- Edge cases are documented where they affect implementation.
|
||||
- API fields include types, defaults, constraints, and errors.
|
||||
- Troubleshooting starts from observable symptoms.
|
||||
- Related links help the reader continue without duplicating the page.
|
||||
- The page says where to get support, file issues, or contribute when that is relevant to the reader's next step.
|
||||
- The page is complete for the scope it claims, or the limitation is stated up front.
|
||||
|
||||
## Review Pass
|
||||
|
||||
Edit in this order:
|
||||
|
||||
1. Remove repetition and generic explanation.
|
||||
2. Move conceptual background below the first useful action unless it is required to choose correctly.
|
||||
3. Replace passive or abstract wording with concrete instructions.
|
||||
4. Tighten headings until the outline reads like a task map.
|
||||
5. Add missing operational details for production safety.
|
||||
6. Check examples for copy-paste accuracy.
|
||||
7. Add links between guide, reference, SDK, testing, and troubleshooting surfaces.
|
||||
8. Check discoverability, addressability, accessibility, and docs-as-code verification.
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -555,13 +555,6 @@ top-level phase timings for preflight, image build, package prep, lane pools,
|
||||
and cleanup. Use `pnpm test:docker:timings <summary.json>` to rank slow lanes
|
||||
and phases before deciding whether a broader rerun is justified.
|
||||
|
||||
Skill install proof: use `pnpm test:docker:skill-install` or targeted
|
||||
`docker_lanes=skill-install` for live ClawHub skill-install validation. The
|
||||
lane installs the package tarball in a bare runner, keeps
|
||||
`skills.install.allowUploadedArchives=false`, resolves the current live slug
|
||||
from `openclaw skills search`, installs it, and verifies `.clawhub` origin/lock
|
||||
metadata. Prefer this checked-in script over inline heredoc Testbox recipes.
|
||||
|
||||
## Cheap Docker Reruns
|
||||
|
||||
First derive the smallest rerun command from artifacts:
|
||||
|
||||
@@ -39,7 +39,6 @@ runs:
|
||||
- name: Setup pnpm (corepack retry)
|
||||
shell: bash
|
||||
env:
|
||||
COREPACK_ENABLE_DOWNLOAD_PROMPT: "0"
|
||||
PNPM_VERSION: ${{ inputs.pnpm-version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
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:
|
||||
|
||||
@@ -147,8 +147,6 @@ jobs:
|
||||
|
||||
- name: Build dist on cache miss
|
||||
if: steps.dist-cache.outputs.cache-hit != 'true'
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=8192
|
||||
run: pnpm build:ci-artifacts
|
||||
|
||||
- name: Build Control UI on cache miss
|
||||
|
||||
22
.github/workflows/ci.yml
vendored
22
.github/workflows/ci.yml
vendored
@@ -520,8 +520,6 @@ jobs:
|
||||
install-bun: "false"
|
||||
|
||||
- name: Build dist
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=8192
|
||||
run: pnpm build:ci-artifacts
|
||||
|
||||
- name: Build Control UI
|
||||
@@ -604,14 +602,14 @@ jobs:
|
||||
|
||||
if [ "$RUN_CHANNELS" = "true" ]; then
|
||||
start_check "channels" env \
|
||||
NODE_OPTIONS=--max-old-space-size=8192 \
|
||||
NODE_OPTIONS=--max-old-space-size=6144 \
|
||||
OPENCLAW_VITEST_MAX_WORKERS=1 \
|
||||
pnpm test:channels
|
||||
fi
|
||||
|
||||
if [ "$RUN_CORE_SUPPORT_BOUNDARY" = "true" ]; then
|
||||
start_check "core-support-boundary" env \
|
||||
NODE_OPTIONS=--max-old-space-size=8192 \
|
||||
NODE_OPTIONS=--max-old-space-size=6144 \
|
||||
OPENCLAW_VITEST_MAX_WORKERS=2 \
|
||||
node scripts/run-vitest.mjs run --config test/vitest/vitest.full-core-support-boundary.config.ts
|
||||
fi
|
||||
@@ -1122,7 +1120,7 @@ jobs:
|
||||
|
||||
- name: Run Node 22 compatibility
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=8192
|
||||
NODE_OPTIONS: --max-old-space-size=6144
|
||||
run: |
|
||||
pnpm build
|
||||
pnpm ui:build
|
||||
@@ -1202,7 +1200,7 @@ jobs:
|
||||
|
||||
- name: Run Node test shard
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=8192
|
||||
NODE_OPTIONS: --max-old-space-size=6144
|
||||
OPENCLAW_NODE_TEST_CONFIGS_JSON: ${{ toJson(matrix.configs) }}
|
||||
OPENCLAW_NODE_TEST_INCLUDE_PATTERNS_JSON: ${{ toJson(matrix.includePatterns) }}
|
||||
OPENCLAW_VITEST_SHARD_NAME: ${{ matrix.shard_name }}
|
||||
@@ -1744,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:
|
||||
@@ -1797,7 +1785,7 @@ jobs:
|
||||
runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-16vcpu-windows-2025' || 'windows-2025' }}
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=8192
|
||||
NODE_OPTIONS: --max-old-space-size=6144
|
||||
# Keep total concurrency predictable on the smaller Windows runner.
|
||||
OPENCLAW_VITEST_MAX_WORKERS: 1
|
||||
OPENCLAW_TEST_SKIP_FULL_EXTENSIONS_SHARD: 1
|
||||
|
||||
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"
|
||||
|
||||
13
.github/workflows/mantis-scenario.yml
vendored
13
.github/workflows/mantis-scenario.yml
vendored
@@ -12,7 +12,6 @@ on:
|
||||
- discord-status-reactions-tool-only
|
||||
- discord-thread-reply-filepath-attachment
|
||||
- slack-desktop-smoke
|
||||
- telegram-live
|
||||
baseline_ref:
|
||||
description: Optional baseline ref for before/after scenarios
|
||||
required: false
|
||||
@@ -91,18 +90,6 @@ jobs:
|
||||
fi
|
||||
gh "${args[@]}"
|
||||
;;
|
||||
telegram-live)
|
||||
args=(
|
||||
workflow run mantis-telegram-live.yml
|
||||
--repo "$GITHUB_REPOSITORY"
|
||||
--ref main
|
||||
-f "candidate_ref=${CANDIDATE_REF}"
|
||||
)
|
||||
if [[ -n "${PR_NUMBER:-}" ]]; then
|
||||
args+=(-f "pr_number=${PR_NUMBER}")
|
||||
fi
|
||||
gh "${args[@]}"
|
||||
;;
|
||||
*)
|
||||
echo "Unsupported Mantis scenario: ${SCENARIO_ID}" >&2
|
||||
exit 1
|
||||
|
||||
500
.github/workflows/mantis-telegram-live.yml
vendored
500
.github/workflows/mantis-telegram-live.yml
vendored
@@ -1,500 +0,0 @@
|
||||
name: Mantis Telegram Live
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
candidate_ref:
|
||||
description: Ref, tag, or SHA to verify with Telegram live QA
|
||||
required: true
|
||||
default: main
|
||||
type: string
|
||||
pr_number:
|
||||
description: Optional PR number to receive the QA evidence comment
|
||||
required: false
|
||||
type: string
|
||||
scenario:
|
||||
description: Optional comma-separated Telegram scenario ids
|
||||
required: false
|
||||
default: telegram-status-command
|
||||
type: string
|
||||
crabbox_provider:
|
||||
description: Crabbox provider for the desktop transcript capture
|
||||
required: false
|
||||
default: aws
|
||||
type: choice
|
||||
options:
|
||||
- aws
|
||||
- hetzner
|
||||
crabbox_lease_id:
|
||||
description: Optional existing Crabbox desktop/browser lease id or slug to reuse
|
||||
required: false
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
concurrency:
|
||||
group: mantis-telegram-live-${{ github.event.issue.number || inputs.pr_number || inputs.candidate_ref || github.run_id }}-${{ github.run_attempt }}
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||
NODE_VERSION: "24.x"
|
||||
PNPM_VERSION: "10.33.0"
|
||||
OPENCLAW_BUILD_PRIVATE_QA: "1"
|
||||
OPENCLAW_ENABLE_PRIVATE_QA_CLI: "1"
|
||||
CRABBOX_REF: main
|
||||
|
||||
jobs:
|
||||
authorize_actor:
|
||||
name: Authorize workflow actor
|
||||
if: >-
|
||||
${{
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
(
|
||||
github.event_name == 'issue_comment' &&
|
||||
github.event.issue.pull_request &&
|
||||
(
|
||||
contains(github.event.comment.body, '@Mantis') ||
|
||||
contains(github.event.comment.body, '@mantis') ||
|
||||
contains(github.event.comment.body, '/mantis')
|
||||
)
|
||||
)
|
||||
}}
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Require maintainer-level repository access
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
const allowed = new Set(["admin", "maintain", "write"]);
|
||||
const { owner, repo } = context.repo;
|
||||
const { data } = await github.rest.repos.getCollaboratorPermissionLevel({
|
||||
owner,
|
||||
repo,
|
||||
username: context.actor,
|
||||
});
|
||||
const permission = data.permission;
|
||||
core.info(`Actor ${context.actor} permission: ${permission}`);
|
||||
if (!allowed.has(permission)) {
|
||||
core.setFailed(
|
||||
`Workflow requires write/maintain/admin access. Actor "${context.actor}" has "${permission}".`,
|
||||
);
|
||||
}
|
||||
|
||||
resolve_request:
|
||||
name: Resolve Mantis request
|
||||
needs: authorize_actor
|
||||
runs-on: ubuntu-24.04
|
||||
outputs:
|
||||
candidate_ref: ${{ steps.resolve.outputs.candidate_ref }}
|
||||
crabbox_provider: ${{ steps.resolve.outputs.crabbox_provider }}
|
||||
lease_id: ${{ steps.resolve.outputs.lease_id }}
|
||||
pr_number: ${{ steps.resolve.outputs.pr_number }}
|
||||
request_source: ${{ steps.resolve.outputs.request_source }}
|
||||
scenario: ${{ steps.resolve.outputs.scenario }}
|
||||
should_run: ${{ steps.resolve.outputs.should_run }}
|
||||
steps:
|
||||
- name: Resolve refs and target PR
|
||||
id: resolve
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
const eventName = context.eventName;
|
||||
|
||||
function setOutput(name, value) {
|
||||
core.setOutput(name, value ?? "");
|
||||
core.info(`${name}=${value ?? ""}`);
|
||||
}
|
||||
|
||||
if (eventName === "workflow_dispatch") {
|
||||
const inputs = context.payload.inputs ?? {};
|
||||
setOutput("should_run", "true");
|
||||
setOutput("candidate_ref", inputs.candidate_ref || "main");
|
||||
setOutput("pr_number", inputs.pr_number || "");
|
||||
setOutput("scenario", inputs.scenario || "telegram-status-command");
|
||||
setOutput("crabbox_provider", inputs.crabbox_provider || "aws");
|
||||
setOutput("lease_id", inputs.crabbox_lease_id || "");
|
||||
setOutput("request_source", "workflow_dispatch");
|
||||
return;
|
||||
}
|
||||
|
||||
if (eventName !== "issue_comment") {
|
||||
core.setFailed(`Unsupported event: ${eventName}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const issue = context.payload.issue;
|
||||
const body = context.payload.comment?.body ?? "";
|
||||
if (!issue?.pull_request) {
|
||||
core.setFailed("Mantis issue_comment trigger requires a pull request comment.");
|
||||
return;
|
||||
}
|
||||
|
||||
const normalized = body.toLowerCase();
|
||||
const requested =
|
||||
(normalized.includes("@mantis") || normalized.includes("/mantis")) &&
|
||||
normalized.includes("telegram");
|
||||
if (!requested) {
|
||||
core.notice("Comment mentioned Mantis but did not request Telegram live QA.");
|
||||
setOutput("should_run", "false");
|
||||
setOutput("candidate_ref", "");
|
||||
setOutput("pr_number", "");
|
||||
setOutput("scenario", "");
|
||||
setOutput("crabbox_provider", "");
|
||||
setOutput("lease_id", "");
|
||||
setOutput("request_source", "unsupported_issue_comment");
|
||||
return;
|
||||
}
|
||||
|
||||
const { owner, repo } = context.repo;
|
||||
const { data: pr } = await github.rest.pulls.get({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: issue.number,
|
||||
});
|
||||
const candidateMatch = body.match(/(?:candidate|head)[\s:=]+([^\s`]+)/i);
|
||||
const scenarioMatch = body.match(/(?:scenario|scenarios)[\s:=]+([^\s`]+)/i);
|
||||
const providerMatch = body.match(/(?:provider|crabbox_provider)[\s:=]+([^\s`]+)/i);
|
||||
const leaseMatch = body.match(/(?:lease|lease_id|crabbox_lease_id)[\s:=]+([^\s`]+)/i);
|
||||
const rawCandidate = candidateMatch?.[1];
|
||||
const candidate =
|
||||
rawCandidate && !["head", "pr", "pr-head"].includes(rawCandidate.toLowerCase())
|
||||
? rawCandidate
|
||||
: pr.head.sha;
|
||||
const provider = providerMatch?.[1] || "aws";
|
||||
if (!["aws", "hetzner"].includes(provider)) {
|
||||
core.setFailed(`Unsupported Crabbox provider for Mantis Telegram: ${provider}`);
|
||||
return;
|
||||
}
|
||||
|
||||
setOutput("should_run", "true");
|
||||
setOutput("candidate_ref", candidate);
|
||||
setOutput("pr_number", String(issue.number));
|
||||
setOutput("scenario", scenarioMatch?.[1] || "telegram-status-command");
|
||||
setOutput("crabbox_provider", provider);
|
||||
setOutput("lease_id", leaseMatch?.[1] || "");
|
||||
setOutput("request_source", "issue_comment");
|
||||
|
||||
await github.rest.reactions.createForIssueComment({
|
||||
owner,
|
||||
repo,
|
||||
comment_id: context.payload.comment.id,
|
||||
content: "eyes",
|
||||
}).catch((error) => core.warning(`Could not add eyes reaction: ${error.message}`));
|
||||
|
||||
validate_ref:
|
||||
name: Validate candidate ref
|
||||
needs: resolve_request
|
||||
if: ${{ needs.resolve_request.outputs.should_run == 'true' }}
|
||||
runs-on: ubuntu-24.04
|
||||
outputs:
|
||||
candidate_revision: ${{ steps.validate.outputs.candidate_revision }}
|
||||
steps:
|
||||
- name: Checkout harness ref
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Validate ref is trusted
|
||||
id: validate
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
CANDIDATE_REF: ${{ needs.resolve_request.outputs.candidate_ref }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main
|
||||
|
||||
revision="$(git rev-parse "${CANDIDATE_REF}^{commit}")"
|
||||
reason=""
|
||||
if git merge-base --is-ancestor "$revision" refs/remotes/origin/main; then
|
||||
reason="main-ancestor"
|
||||
elif git tag --points-at "$revision" | grep -Eq '^v'; then
|
||||
reason="release-tag"
|
||||
else
|
||||
pr_head_count="$(
|
||||
gh api \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
"repos/${GITHUB_REPOSITORY}/commits/${revision}/pulls" \
|
||||
--jq '[.[] | select(.state == "open" and .head.repo.full_name == "'"${GITHUB_REPOSITORY}"'" and .head.sha == "'"${revision}"'")] | length'
|
||||
)"
|
||||
if [[ "$pr_head_count" != "0" ]]; then
|
||||
reason="open-pr-head"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -z "$reason" ]]; then
|
||||
echo "Candidate ref '${CANDIDATE_REF}' resolved to ${revision}, which is not trusted for this secret-bearing Mantis run." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "candidate_revision=${revision}" >> "$GITHUB_OUTPUT"
|
||||
{
|
||||
echo "candidate: \`${CANDIDATE_REF}\`"
|
||||
echo "candidate SHA: \`${revision}\`"
|
||||
echo "candidate trust reason: \`${reason}\`"
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
run_telegram_live:
|
||||
name: Run Telegram live QA with Crabbox evidence
|
||||
needs: [resolve_request, validate_ref]
|
||||
if: ${{ needs.resolve_request.outputs.should_run == 'true' }}
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 180
|
||||
environment: qa-live-shared
|
||||
outputs:
|
||||
comparison_status: ${{ steps.run_mantis.outputs.comparison_status }}
|
||||
output_dir: ${{ steps.run_mantis.outputs.output_dir }}
|
||||
steps:
|
||||
- name: Checkout harness ref
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Node environment
|
||||
uses: ./.github/actions/setup-node-env
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
pnpm-version: ${{ env.PNPM_VERSION }}
|
||||
install-bun: "true"
|
||||
|
||||
- name: Build Mantis harness
|
||||
run: pnpm build
|
||||
|
||||
- name: Cache Mantis candidate pnpm store
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.local/share/pnpm/store
|
||||
~/.cache/pnpm
|
||||
key: mantis-telegram-pnpm-${{ runner.os }}-${{ env.NODE_VERSION }}-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
mantis-telegram-pnpm-${{ runner.os }}-${{ env.NODE_VERSION }}-
|
||||
|
||||
- name: Setup Go for Crabbox CLI
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.26.x"
|
||||
cache: false
|
||||
|
||||
- name: Install Crabbox CLI
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
install_dir="${RUNNER_TEMP}/crabbox"
|
||||
mkdir -p "$install_dir/src" "$HOME/.local/bin"
|
||||
git init "$install_dir/src"
|
||||
git -C "$install_dir/src" remote add origin https://github.com/openclaw/crabbox.git
|
||||
git -C "$install_dir/src" fetch --depth 1 origin "$CRABBOX_REF"
|
||||
git -C "$install_dir/src" checkout --detach FETCH_HEAD
|
||||
go build -C "$install_dir/src" -o "$HOME/.local/bin/crabbox" ./cmd/crabbox
|
||||
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
|
||||
"$HOME/.local/bin/crabbox" --version
|
||||
"$HOME/.local/bin/crabbox" warmup --help > "$install_dir/warmup-help.txt" 2>&1
|
||||
grep -q -- "-desktop" "$install_dir/warmup-help.txt"
|
||||
"$HOME/.local/bin/crabbox" media preview --help >/dev/null
|
||||
|
||||
- name: Prepare candidate worktree
|
||||
env:
|
||||
CANDIDATE_SHA: ${{ needs.validate_ref.outputs.candidate_revision }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
worktree_root=".artifacts/qa-e2e/mantis/telegram-live-worktrees"
|
||||
mkdir -p "$worktree_root"
|
||||
git worktree add --detach "$worktree_root/candidate" "$CANDIDATE_SHA"
|
||||
pnpm --dir "$worktree_root/candidate" install --frozen-lockfile --prefer-offline
|
||||
pnpm --dir "$worktree_root/candidate" build
|
||||
|
||||
- name: Run Telegram live scenario and capture desktop evidence
|
||||
id: run_mantis
|
||||
env:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
OPENCLAW_QA_CONVEX_SITE_URL: ${{ secrets.OPENCLAW_QA_CONVEX_SITE_URL }}
|
||||
OPENCLAW_QA_CONVEX_SECRET_CI: ${{ secrets.OPENCLAW_QA_CONVEX_SECRET_CI }}
|
||||
OPENCLAW_QA_REDACT_PUBLIC_METADATA: "1"
|
||||
OPENCLAW_QA_TELEGRAM_CAPTURE_CONTENT: "1"
|
||||
CRABBOX_COORDINATOR: ${{ secrets.CRABBOX_COORDINATOR }}
|
||||
CRABBOX_COORDINATOR_TOKEN: ${{ secrets.CRABBOX_COORDINATOR_TOKEN }}
|
||||
OPENCLAW_QA_MANTIS_CRABBOX_COORDINATOR: ${{ secrets.OPENCLAW_QA_MANTIS_CRABBOX_COORDINATOR }}
|
||||
OPENCLAW_QA_MANTIS_CRABBOX_COORDINATOR_TOKEN: ${{ secrets.OPENCLAW_QA_MANTIS_CRABBOX_COORDINATOR_TOKEN }}
|
||||
CRABBOX_ACCESS_CLIENT_ID: ${{ secrets.CRABBOX_ACCESS_CLIENT_ID }}
|
||||
CRABBOX_ACCESS_CLIENT_SECRET: ${{ secrets.CRABBOX_ACCESS_CLIENT_SECRET }}
|
||||
CRABBOX_LEASE_ID: ${{ needs.resolve_request.outputs.lease_id }}
|
||||
CRABBOX_PROVIDER: ${{ needs.resolve_request.outputs.crabbox_provider }}
|
||||
SCENARIO_INPUT: ${{ needs.resolve_request.outputs.scenario }}
|
||||
CANDIDATE_SHA: ${{ needs.validate_ref.outputs.candidate_revision }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
require_var() {
|
||||
local key="$1"
|
||||
if [[ -z "${!key:-}" ]]; then
|
||||
echo "Missing required ${key}." >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
CRABBOX_COORDINATOR="${CRABBOX_COORDINATOR:-${OPENCLAW_QA_MANTIS_CRABBOX_COORDINATOR:-}}"
|
||||
CRABBOX_COORDINATOR_TOKEN="${CRABBOX_COORDINATOR_TOKEN:-${OPENCLAW_QA_MANTIS_CRABBOX_COORDINATOR_TOKEN:-}}"
|
||||
export CRABBOX_COORDINATOR CRABBOX_COORDINATOR_TOKEN
|
||||
|
||||
require_var OPENAI_API_KEY
|
||||
require_var OPENCLAW_QA_CONVEX_SITE_URL
|
||||
require_var OPENCLAW_QA_CONVEX_SECRET_CI
|
||||
require_var CRABBOX_COORDINATOR_TOKEN
|
||||
|
||||
candidate_repo="$(pwd)/.artifacts/qa-e2e/mantis/telegram-live-worktrees/candidate"
|
||||
output_rel=".artifacts/qa-e2e/mantis/telegram-live"
|
||||
root="$candidate_repo/$output_rel"
|
||||
echo "output_dir=${root}" >> "$GITHUB_OUTPUT"
|
||||
model="${OPENCLAW_CI_OPENAI_MODEL:-openai/gpt-5.4}"
|
||||
|
||||
scenario_args=()
|
||||
if [[ -n "${SCENARIO_INPUT// }" ]]; then
|
||||
IFS=',' read -r -a raw_scenarios <<<"${SCENARIO_INPUT}"
|
||||
for raw in "${raw_scenarios[@]}"; do
|
||||
scenario="$(printf '%s' "${raw}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
|
||||
if [[ -n "${scenario}" ]]; then
|
||||
scenario_args+=(--scenario "${scenario}")
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
set +e
|
||||
pnpm --dir "$candidate_repo" openclaw qa telegram \
|
||||
--repo-root "$candidate_repo" \
|
||||
--output-dir "$output_rel" \
|
||||
--provider-mode live-frontier \
|
||||
--model "$model" \
|
||||
--alt-model "$model" \
|
||||
--fast \
|
||||
--credential-source convex \
|
||||
--credential-role ci \
|
||||
--allow-failures \
|
||||
"${scenario_args[@]}"
|
||||
telegram_exit=$?
|
||||
set -e
|
||||
|
||||
if [[ ! -f "$root/telegram-qa-summary.json" ]]; then
|
||||
echo "Telegram live QA did not produce a summary." >&2
|
||||
exit "$telegram_exit"
|
||||
fi
|
||||
echo "telegram_exit=${telegram_exit}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
node "${GITHUB_WORKSPACE}/scripts/mantis/build-telegram-evidence.mjs" \
|
||||
--output-dir "$root" \
|
||||
--candidate-ref "$CANDIDATE_SHA" \
|
||||
--candidate-sha "$CANDIDATE_SHA" \
|
||||
--scenario-label "${SCENARIO_INPUT:-telegram-live}"
|
||||
|
||||
comparison_status="$(jq -r 'if .comparison.pass then "pass" else "fail" end' "$root/mantis-evidence.json")"
|
||||
echo "comparison_status=${comparison_status}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
desktop_args=()
|
||||
if [[ -n "${CRABBOX_LEASE_ID:-}" ]]; then
|
||||
desktop_args+=(--lease-id "$CRABBOX_LEASE_ID")
|
||||
fi
|
||||
pnpm --dir "$candidate_repo" openclaw qa mantis desktop-browser-smoke \
|
||||
--repo-root "$candidate_repo" \
|
||||
--html-file "$output_rel/telegram-live-transcript.html" \
|
||||
--output-dir "$output_rel/desktop-browser" \
|
||||
--provider "$CRABBOX_PROVIDER" \
|
||||
--class standard \
|
||||
--idle-timeout 45m \
|
||||
--ttl 120m \
|
||||
--video-duration 18 \
|
||||
"${desktop_args[@]}"
|
||||
|
||||
cp "$root/desktop-browser/desktop-browser-smoke.png" "$root/telegram-live-desktop.png"
|
||||
if [[ -f "$root/desktop-browser/desktop-browser-smoke.mp4" ]]; then
|
||||
cp "$root/desktop-browser/desktop-browser-smoke.mp4" "$root/telegram-live.mp4"
|
||||
fi
|
||||
|
||||
if [[ -f "$root/telegram-live.mp4" ]]; then
|
||||
if ! command -v ffmpeg >/dev/null 2>&1 || ! command -v ffprobe >/dev/null 2>&1; then
|
||||
sudo apt-get update -y >/tmp/mantis-telegram-ffmpeg-apt.log 2>&1 || true
|
||||
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y ffmpeg >>/tmp/mantis-telegram-ffmpeg-apt.log 2>&1 || true
|
||||
fi
|
||||
if ! crabbox media preview \
|
||||
--input "$root/telegram-live.mp4" \
|
||||
--output "$root/telegram-live-preview.gif" \
|
||||
--trimmed-video-output "$root/telegram-live-change.mp4" \
|
||||
--json > "$root/telegram-live-preview.json"; then
|
||||
rm -f "$root/telegram-live-preview.gif"
|
||||
rm -f "$root/telegram-live-change.mp4"
|
||||
rm -f "$root/telegram-live-preview.json"
|
||||
echo "::warning::Could not generate Telegram motion-trimmed desktop preview."
|
||||
fi
|
||||
fi
|
||||
|
||||
cat "$root/telegram-qa-report.md" >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Upload Mantis Telegram artifacts
|
||||
id: upload_artifact
|
||||
if: ${{ always() && steps.run_mantis.outputs.output_dir != '' }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: mantis-telegram-live-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
path: ${{ steps.run_mantis.outputs.output_dir }}
|
||||
retention-days: 14
|
||||
if-no-files-found: warn
|
||||
|
||||
- name: Create Mantis GitHub App token
|
||||
id: mantis_app_token
|
||||
if: ${{ always() && needs.resolve_request.outputs.pr_number != '' }}
|
||||
uses: actions/create-github-app-token@v3
|
||||
with:
|
||||
app-id: ${{ secrets.MANTIS_GITHUB_APP_ID }}
|
||||
private-key: ${{ secrets.MANTIS_GITHUB_APP_PRIVATE_KEY }}
|
||||
owner: ${{ github.repository_owner }}
|
||||
repositories: ${{ github.event.repository.name }}
|
||||
permission-contents: write
|
||||
permission-issues: write
|
||||
permission-pull-requests: write
|
||||
|
||||
- name: Comment PR with inline QA evidence
|
||||
if: ${{ always() && needs.resolve_request.outputs.pr_number != '' && steps.run_mantis.outputs.output_dir != '' }}
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.mantis_app_token.outputs.token }}
|
||||
TARGET_PR: ${{ needs.resolve_request.outputs.pr_number }}
|
||||
ARTIFACT_URL: ${{ steps.upload_artifact.outputs.artifact-url }}
|
||||
REQUEST_SOURCE: ${{ needs.resolve_request.outputs.request_source }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
root="${{ steps.run_mantis.outputs.output_dir }}"
|
||||
if [[ ! -f "$root/mantis-evidence.json" ]]; then
|
||||
echo "No Mantis evidence manifest found; skipping PR evidence comment."
|
||||
exit 0
|
||||
fi
|
||||
artifact_url_args=()
|
||||
if [[ -n "${ARTIFACT_URL:-}" ]]; then
|
||||
artifact_url_args=(--artifact-url "$ARTIFACT_URL")
|
||||
fi
|
||||
node scripts/mantis/publish-pr-evidence.mjs \
|
||||
--manifest "$root/mantis-evidence.json" \
|
||||
--target-pr "$TARGET_PR" \
|
||||
--artifact-root "mantis/telegram-live/pr-${TARGET_PR}/run-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" \
|
||||
--marker "<!-- mantis-telegram-live -->" \
|
||||
"${artifact_url_args[@]}" \
|
||||
--run-url "https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" \
|
||||
--request-source "$REQUEST_SOURCE"
|
||||
|
||||
- name: Fail when Mantis Telegram failed
|
||||
if: ${{ always() && steps.run_mantis.outputs.output_dir != '' && (steps.run_mantis.outputs.comparison_status != 'pass' || steps.run_mantis.outputs.telegram_exit != '0') }}
|
||||
env:
|
||||
COMPARISON_STATUS: ${{ steps.run_mantis.outputs.comparison_status }}
|
||||
TELEGRAM_EXIT: ${{ steps.run_mantis.outputs.telegram_exit }}
|
||||
run: |
|
||||
echo "Mantis Telegram live failed: comparison=${COMPARISON_STATUS:-unset} telegram_exit=${TELEGRAM_EXIT:-unset}." >&2
|
||||
exit 1
|
||||
@@ -390,6 +390,7 @@ jobs:
|
||||
|
||||
if [[ "$LIVE_MODELS_ONLY" != "true" ]]; then
|
||||
add_suite live-cache
|
||||
add_suite openai-ws-stream-live-e2e
|
||||
|
||||
add_profile_suite native-live-src-agents "stable full"
|
||||
add_profile_suite native-live-src-gateway-core "minimum stable full"
|
||||
@@ -523,8 +524,6 @@ jobs:
|
||||
install-bun: "true"
|
||||
|
||||
- name: Build dist for repo E2E
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=8192
|
||||
run: pnpm build
|
||||
|
||||
- name: Run repo E2E suite
|
||||
@@ -532,7 +531,7 @@ jobs:
|
||||
|
||||
validate_special_e2e:
|
||||
needs: validate_selected_ref
|
||||
if: inputs.include_repo_e2e && (inputs.live_suite_filter == '' || inputs.live_suite_filter == 'openshell-e2e')
|
||||
if: (inputs.include_repo_e2e || (inputs.include_live_suites && !inputs.live_models_only)) && (inputs.live_suite_filter == '' || inputs.live_suite_filter == 'openshell-e2e' || inputs.live_suite_filter == 'openai-ws-stream-live-e2e')
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
timeout-minutes: ${{ matrix.timeout_minutes }}
|
||||
strategy:
|
||||
@@ -545,6 +544,12 @@ jobs:
|
||||
timeout_minutes: 120
|
||||
requires_repo_e2e: true
|
||||
requires_live_suites: false
|
||||
- suite_id: openai-ws-stream-live-e2e
|
||||
label: OpenAI WebSocket live E2E
|
||||
command: pnpm test:e2e src/agents/openai-ws-stream.e2e.test.ts
|
||||
timeout_minutes: 90
|
||||
requires_repo_e2e: false
|
||||
requires_live_suites: true
|
||||
env:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
OPENCLAW_E2E_WORKERS: "1"
|
||||
@@ -570,8 +575,6 @@ jobs:
|
||||
(inputs.include_live_suites && matrix.requires_live_suites)
|
||||
) &&
|
||||
(inputs.live_suite_filter == '' || inputs.live_suite_filter == matrix.suite_id)
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=8192
|
||||
run: pnpm build
|
||||
|
||||
- name: Configure suite-specific env
|
||||
@@ -580,7 +583,9 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
case "${{ matrix.suite_id }}" in
|
||||
openshell-e2e)
|
||||
openai-ws-stream-live-e2e)
|
||||
echo "OPENAI_LIVE_TEST=1" >> "$GITHUB_ENV"
|
||||
echo "OPENCLAW_LIVE_TEST=1" >> "$GITHUB_ENV"
|
||||
;;
|
||||
esac
|
||||
|
||||
@@ -590,7 +595,11 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
case "${{ matrix.suite_id }}" in
|
||||
openshell-e2e)
|
||||
openai-ws-stream-live-e2e)
|
||||
[[ -n "${OPENAI_API_KEY:-}" ]] || {
|
||||
echo "OPENAI_API_KEY is required for the OpenAI WebSocket live E2E suite." >&2
|
||||
exit 1
|
||||
}
|
||||
;;
|
||||
esac
|
||||
|
||||
@@ -618,7 +627,7 @@ jobs:
|
||||
timeout_minutes: 120
|
||||
- chunk_id: package-update-openai
|
||||
label: package/update OpenAI install
|
||||
timeout_minutes: 30
|
||||
timeout_minutes: 180
|
||||
- chunk_id: package-update-anthropic
|
||||
label: package/update Anthropic install
|
||||
timeout_minutes: 180
|
||||
|
||||
28
.github/workflows/openclaw-release-checks.yml
vendored
28
.github/workflows/openclaw-release-checks.yml
vendored
@@ -593,13 +593,13 @@ jobs:
|
||||
source: ${{ needs.resolve_target.outputs.package_acceptance_package_spec != '' && 'npm' || 'artifact' }}
|
||||
package_spec: ${{ needs.resolve_target.outputs.package_acceptance_package_spec || 'openclaw@beta' }}
|
||||
artifact_name: ${{ needs.prepare_release_package.outputs.artifact_name }}
|
||||
package_sha256: ${{ needs.resolve_target.outputs.package_acceptance_package_spec == '' && needs.prepare_release_package.outputs.package_sha256 || '' }}
|
||||
package_sha256: ${{ needs.prepare_release_package.outputs.package_sha256 }}
|
||||
suite_profile: custom
|
||||
docker_lanes: doctor-switch update-channel-switch skill-install update-corrupt-plugin upgrade-survivor published-upgrade-survivor update-restart-auth plugins-offline plugin-update
|
||||
docker_lanes: doctor-switch update-channel-switch update-corrupt-plugin upgrade-survivor published-upgrade-survivor update-restart-auth plugins-offline plugin-update
|
||||
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 }}
|
||||
@@ -693,8 +693,6 @@ jobs:
|
||||
install-bun: "true"
|
||||
|
||||
- name: Build private QA runtime
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=8192
|
||||
run: pnpm build
|
||||
|
||||
- name: Run parity lane
|
||||
@@ -707,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
|
||||
@@ -772,8 +770,6 @@ jobs:
|
||||
merge-multiple: true
|
||||
|
||||
- name: Build private QA runtime
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=8192
|
||||
run: pnpm build
|
||||
|
||||
- name: Generate parity report
|
||||
@@ -783,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
|
||||
@@ -825,8 +821,6 @@ jobs:
|
||||
install-bun: "true"
|
||||
|
||||
- name: Build private QA runtime
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=8192
|
||||
run: pnpm build
|
||||
|
||||
- name: Run Matrix live lane
|
||||
@@ -924,8 +918,6 @@ jobs:
|
||||
require_var OPENCLAW_QA_CONVEX_SECRET_CI
|
||||
|
||||
- name: Build private QA runtime
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=8192
|
||||
run: pnpm build
|
||||
|
||||
- name: Run Telegram live lane
|
||||
@@ -1020,8 +1012,6 @@ jobs:
|
||||
require_var OPENCLAW_QA_CONVEX_SECRET_CI
|
||||
|
||||
- name: Build private QA runtime
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=8192
|
||||
run: pnpm build
|
||||
|
||||
- name: Run Discord live lane
|
||||
@@ -1116,8 +1106,6 @@ jobs:
|
||||
require_var OPENCLAW_QA_CONVEX_SECRET_CI
|
||||
|
||||
- name: Build private QA runtime
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=8192
|
||||
run: pnpm build
|
||||
|
||||
- name: Run WhatsApp live lane
|
||||
@@ -1212,8 +1200,6 @@ jobs:
|
||||
require_var OPENCLAW_QA_CONVEX_SECRET_CI
|
||||
|
||||
- name: Build private QA runtime
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=8192
|
||||
run: pnpm build
|
||||
|
||||
- name: Run Slack live lane
|
||||
|
||||
4
.github/workflows/package-acceptance.yml
vendored
4
.github/workflows/package-acceptance.yml
vendored
@@ -386,10 +386,10 @@ jobs:
|
||||
docker_lanes="npm-onboard-channel-agent gateway-network config-reload"
|
||||
;;
|
||||
package)
|
||||
docker_lanes="npm-onboard-channel-agent doctor-switch update-channel-switch skill-install update-corrupt-plugin upgrade-survivor published-upgrade-survivor update-restart-auth plugins-offline plugin-update"
|
||||
docker_lanes="npm-onboard-channel-agent doctor-switch update-channel-switch update-corrupt-plugin upgrade-survivor published-upgrade-survivor update-restart-auth plugins-offline plugin-update"
|
||||
;;
|
||||
product)
|
||||
docker_lanes="npm-onboard-channel-agent doctor-switch update-channel-switch skill-install update-corrupt-plugin upgrade-survivor published-upgrade-survivor update-restart-auth plugins plugin-update mcp-channels cron-mcp-cleanup openai-web-search-minimal openwebui"
|
||||
docker_lanes="npm-onboard-channel-agent doctor-switch update-channel-switch update-corrupt-plugin upgrade-survivor published-upgrade-survivor update-restart-auth plugins plugin-update mcp-channels cron-mcp-cleanup openai-web-search-minimal openwebui"
|
||||
include_openwebui=true
|
||||
;;
|
||||
full)
|
||||
|
||||
4
.github/workflows/plugin-prerelease.yml
vendored
4
.github/workflows/plugin-prerelease.yml
vendored
@@ -270,7 +270,7 @@ jobs:
|
||||
|
||||
- name: Run release-only plugin Node shard
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=8192
|
||||
NODE_OPTIONS: --max-old-space-size=6144
|
||||
OPENCLAW_NODE_TEST_CONFIGS_JSON: ${{ toJson(matrix.configs) }}
|
||||
OPENCLAW_NODE_TEST_INCLUDE_PATTERNS_JSON: ${{ toJson(matrix.includePatterns) }}
|
||||
OPENCLAW_VITEST_SHARD_NAME: ${{ matrix.shard_name }}
|
||||
@@ -340,7 +340,7 @@ jobs:
|
||||
|
||||
- name: Run extension shard
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=8192
|
||||
NODE_OPTIONS: --max-old-space-size=6144
|
||||
OPENCLAW_EXTENSION_BATCH_PARALLEL: 2
|
||||
OPENCLAW_VITEST_MAX_WORKERS: 1
|
||||
OPENCLAW_EXTENSION_BATCH: ${{ matrix.extensions_csv }}
|
||||
|
||||
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
|
||||
|
||||
202
.github/workflows/website-installer-sync.yml
vendored
202
.github/workflows/website-installer-sync.yml
vendored
@@ -1,202 +0,0 @@
|
||||
name: Website Installer Sync
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- scripts/install.sh
|
||||
- scripts/install-cli.sh
|
||||
- scripts/install.ps1
|
||||
- .github/workflows/website-installer-sync.yml
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- scripts/install.sh
|
||||
- scripts/install-cli.sh
|
||||
- scripts/install.ps1
|
||||
- .github/workflows/website-installer-sync.yml
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
sync_website:
|
||||
description: Sync openclaw.ai after verification
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: website-installer-sync-${{ github.event_name == 'workflow_dispatch' && github.run_id || github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name != 'workflow_dispatch' }}
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||
|
||||
jobs:
|
||||
static:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install ShellCheck
|
||||
run: sudo apt-get update -y && sudo apt-get install -y shellcheck
|
||||
|
||||
- name: Shell syntax
|
||||
run: bash -n scripts/install.sh scripts/install-cli.sh
|
||||
|
||||
- name: ShellCheck
|
||||
run: shellcheck -e SC1091 scripts/install.sh scripts/install-cli.sh
|
||||
|
||||
- name: Installer help and dry-runs
|
||||
run: |
|
||||
bash scripts/install.sh --help >/tmp/install-help.txt
|
||||
bash scripts/install.sh --dry-run --no-onboard --no-prompt
|
||||
bash scripts/install-cli.sh --help >/tmp/install-cli-help.txt
|
||||
|
||||
- name: PowerShell syntax
|
||||
shell: pwsh
|
||||
run: |
|
||||
$errors = $null
|
||||
$null = [System.Management.Automation.PSParser]::Tokenize(
|
||||
(Get-Content -Raw scripts/install.ps1),
|
||||
[ref]$errors
|
||||
)
|
||||
if ($errors -and $errors.Count -gt 0) {
|
||||
$errors | Format-List | Out-String | Write-Error
|
||||
exit 1
|
||||
}
|
||||
|
||||
linux-docker:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: install.sh in Docker
|
||||
run: |
|
||||
docker run --rm \
|
||||
-e OPENCLAW_NO_ONBOARD=1 \
|
||||
-e OPENCLAW_NO_PROMPT=1 \
|
||||
-v "$PWD/scripts/install.sh:/tmp/install.sh:ro" \
|
||||
node:24-bookworm-slim \
|
||||
bash -lc 'bash /tmp/install.sh --no-prompt --no-onboard --version latest && openclaw --version'
|
||||
|
||||
- name: install-cli.sh in Docker
|
||||
run: |
|
||||
docker run --rm \
|
||||
-e OPENCLAW_NO_ONBOARD=1 \
|
||||
-e OPENCLAW_NO_PROMPT=1 \
|
||||
-v "$PWD/scripts/install-cli.sh:/tmp/install-cli.sh:ro" \
|
||||
node:24-bookworm-slim \
|
||||
bash -lc 'apt-get update -y && apt-get install -y curl && bash /tmp/install-cli.sh --prefix /tmp/openclaw --no-onboard --version latest && /tmp/openclaw/bin/openclaw --version'
|
||||
|
||||
macos-installer:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24
|
||||
|
||||
- name: install.sh dry run
|
||||
run: bash scripts/install.sh --dry-run --no-onboard --no-prompt
|
||||
|
||||
- name: install.sh on macOS
|
||||
env:
|
||||
OPENCLAW_NO_ONBOARD: "1"
|
||||
OPENCLAW_NO_PROMPT: "1"
|
||||
run: |
|
||||
bash scripts/install.sh --no-onboard --no-prompt --version latest
|
||||
openclaw --version
|
||||
|
||||
windows-installer:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24
|
||||
|
||||
- name: install.ps1 dry run
|
||||
shell: pwsh
|
||||
run: .\scripts\install.ps1 -DryRun -NoOnboard -InstallMethod npm
|
||||
|
||||
sync-website:
|
||||
needs: [static, linux-docker, macos-installer, windows-installer]
|
||||
if: >
|
||||
(github.event_name == 'push' && github.ref == 'refs/heads/main') ||
|
||||
(github.event_name == 'workflow_dispatch' && inputs.sync_website)
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout OpenClaw
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
path: openclaw
|
||||
|
||||
- name: Checkout openclaw.ai
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
repository: openclaw/openclaw.ai
|
||||
token: ${{ secrets.OPENCLAW_GH_TOKEN }}
|
||||
path: openclaw.ai
|
||||
|
||||
- name: Sync installer scripts
|
||||
run: |
|
||||
cp openclaw/scripts/install.sh openclaw.ai/public/install.sh
|
||||
cp openclaw/scripts/install-cli.sh openclaw.ai/public/install-cli.sh
|
||||
cp openclaw/scripts/install.ps1 openclaw.ai/public/install.ps1
|
||||
rm -f openclaw.ai/public/install.cmd
|
||||
chmod +x openclaw.ai/public/install.sh openclaw.ai/public/install-cli.sh
|
||||
|
||||
- name: Check for changes
|
||||
id: changes
|
||||
working-directory: openclaw.ai
|
||||
run: |
|
||||
if git diff --quiet -- public/install.sh public/install-cli.sh public/install.ps1 public/install.cmd; then
|
||||
echo "changed=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Setup Bun
|
||||
if: steps.changes.outputs.changed == 'true'
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: latest
|
||||
|
||||
- name: Setup Node.js
|
||||
if: steps.changes.outputs.changed == 'true'
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "24"
|
||||
|
||||
- name: Install ShellCheck
|
||||
if: steps.changes.outputs.changed == 'true'
|
||||
run: sudo apt-get update -y && sudo apt-get install -y shellcheck
|
||||
|
||||
- name: Verify website with synced installers
|
||||
if: steps.changes.outputs.changed == 'true'
|
||||
working-directory: openclaw.ai
|
||||
run: |
|
||||
bash -n public/install.sh public/install-cli.sh
|
||||
shellcheck -e SC1091 public/install.sh public/install-cli.sh
|
||||
bun install --frozen-lockfile
|
||||
bun run build
|
||||
|
||||
- name: Commit and push website sync
|
||||
if: steps.changes.outputs.changed == 'true'
|
||||
working-directory: openclaw.ai
|
||||
run: |
|
||||
git config user.name "openclaw-installer-sync[bot]"
|
||||
git config user.email "openclaw-installer-sync[bot]@users.noreply.github.com"
|
||||
git add public/install.sh public/install-cli.sh public/install.ps1 public/install.cmd
|
||||
git commit -m "chore: sync installers from openclaw ${GITHUB_SHA::12}"
|
||||
git pull --rebase origin main
|
||||
git push origin HEAD:main
|
||||
5
.gitignore
vendored
5
.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
|
||||
@@ -117,7 +113,6 @@ USER.md
|
||||
!.agents/skills/crabbox/**
|
||||
!.agents/skills/gitcrawl/
|
||||
!.agents/skills/gitcrawl/**
|
||||
!.agents/skills/openclaw-docs/**
|
||||
!.agents/skills/openclaw-ghsa-maintainer/
|
||||
!.agents/skills/openclaw-ghsa-maintainer/**
|
||||
!.agents/skills/openclaw-parallels-smoke/
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -93,12 +93,3 @@ scripts/run-tests*
|
||||
scripts/lib/test-*
|
||||
scripts/lib/extension-test-*
|
||||
scripts/lib/vitest-*
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Sibling symlinks for scoped guides
|
||||
# ----------------------------------------------------------------------------
|
||||
# Every `AGENTS.md` has a sibling `CLAUDE.md` symlink pointing at it (see
|
||||
# root AGENTS.md: "New AGENTS.md: add sibling CLAUDE.md symlink"). Scanning
|
||||
# the symlinks is redundant with scanning the underlying AGENTS.md and
|
||||
# breaks opengrep's PR-diff scan when a new CLAUDE.md symlink is added.
|
||||
CLAUDE.md
|
||||
|
||||
@@ -71,15 +71,13 @@ 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.
|
||||
- Bare GitHub issue/PR URL or number => `review <ref>`: load repo maintainer skill if available, inspect live with `gh`, report findings in chat. No comments/close/merge/fix unless explicitly asked.
|
||||
- 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.
|
||||
- PR shortlist: `gh pr list ...`; then `gh pr view <n> --json number,title,body,closingIssuesReferences,files,statusCheckRollup,reviewDecision`.
|
||||
- After landing PR: search duplicate open issues/PRs. Before closing: comment why + canonical link.
|
||||
- If an issue/PR is already fixed on current `main` or solved by a new release: comment with proof + canonical commit/PR/release, then close.
|
||||
- `ship` that fixes an issue: after push, comment proof + commit link, then close the issue.
|
||||
- GH comments with markdown backticks, `$`, or shell snippets: avoid inline double-quoted `--body`; use single quotes or `--body-file`.
|
||||
- PR create: description/body always required. Include concise Summary + Verification sections; mention issue/PR refs, behavior changed, and exact local/Testbox/CI proof. Never open an empty-description, empty-body, or placeholder-body PR.
|
||||
- PR execution artifacts/screenshots: attach them to the PR, comment, or an external artifact store. Do not add `.github/pr-assets` or other PR-only assets to the repo.
|
||||
@@ -159,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.
|
||||
|
||||
|
||||
1147
CHANGELOG.md
1147
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!
|
||||
|
||||
11
Dockerfile
11
Dockerfile
@@ -101,7 +101,7 @@ RUN pnpm canvas:a2ui:bundle || \
|
||||
echo "/* A2UI bundle unavailable in this build */" > extensions/canvas/src/host/a2ui/a2ui.bundle.js && \
|
||||
echo "stub" > extensions/canvas/src/host/a2ui/.bundle.hash && \
|
||||
rm -rf vendor/a2ui apps/shared/OpenClawKit/Tools/CanvasA2UI)
|
||||
RUN NODE_OPTIONS=--max-old-space-size=8192 pnpm build:docker
|
||||
RUN pnpm build:docker
|
||||
# Force pnpm for UI build (Bun may fail on ARM/Synology architectures)
|
||||
ENV OPENCLAW_PREFER_PNPM=1
|
||||
RUN pnpm ui:build
|
||||
@@ -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
|
||||
@@ -207,15 +207,15 @@ RUN --mount=type=cache,id=openclaw-bookworm-apt-cache,target=/var/cache/apt,shar
|
||||
# Adds ~300MB but eliminates the 60-90s Playwright install on every container start.
|
||||
# Must run after node_modules COPY so playwright-core is available.
|
||||
ARG OPENCLAW_INSTALL_BROWSER=""
|
||||
ENV PLAYWRIGHT_BROWSERS_PATH=/home/node/.cache/ms-playwright
|
||||
RUN --mount=type=cache,id=openclaw-bookworm-apt-cache,target=/var/cache/apt,sharing=locked \
|
||||
--mount=type=cache,id=openclaw-bookworm-apt-lists,target=/var/lib/apt,sharing=locked \
|
||||
if [ -n "$OPENCLAW_INSTALL_BROWSER" ]; then \
|
||||
apt-get update && \
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends xvfb && \
|
||||
mkdir -p "$PLAYWRIGHT_BROWSERS_PATH" && \
|
||||
mkdir -p /home/node/.cache/ms-playwright && \
|
||||
PLAYWRIGHT_BROWSERS_PATH=/home/node/.cache/ms-playwright \
|
||||
node /app/node_modules/playwright-core/cli.js install --with-deps chromium && \
|
||||
chown -R node:node "$PLAYWRIGHT_BROWSERS_PATH"; \
|
||||
chown -R node:node /home/node/.cache/ms-playwright; \
|
||||
fi
|
||||
|
||||
# Optionally install Docker CLI for sandbox container management.
|
||||
@@ -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 = 2026051000
|
||||
versionName = "2026.5.10"
|
||||
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,13 +1,5 @@
|
||||
# OpenClaw iOS Changelog
|
||||
|
||||
## 2026.5.10 - 2026-05-10
|
||||
|
||||
Maintenance update for the current OpenClaw beta release.
|
||||
|
||||
## 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.10
|
||||
OPENCLAW_MARKETING_VERSION = 2026.5.10
|
||||
OPENCLAW_IOS_VERSION = 2026.5.6
|
||||
OPENCLAW_MARKETING_VERSION = 2026.5.6
|
||||
OPENCLAW_BUILD_VERSION = 1
|
||||
|
||||
#include? "../build/Version.xcconfig"
|
||||
|
||||
@@ -1 +1 @@
|
||||
Maintenance update for the current OpenClaw beta release.
|
||||
Maintenance update for the current OpenClaw development release.
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"version": "2026.5.10"
|
||||
"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)
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ enum GatewayAgentChannel: String, Codable, CaseIterable {
|
||||
case signal
|
||||
case imessage
|
||||
case msteams
|
||||
case bluebubbles
|
||||
case webchat
|
||||
|
||||
init(raw: String?) {
|
||||
|
||||
@@ -471,8 +471,7 @@ actor MacNodeRuntime {
|
||||
}
|
||||
|
||||
private func resolveA2UIHostUrl() async -> String? {
|
||||
let canvasSurfaceUrl = await self.canvasSurfaceUrl()
|
||||
return Self.resolveA2UIHostUrl(from: canvasSurfaceUrl)
|
||||
Self.resolveA2UIHostUrl(from: await self.canvasSurfaceUrl())
|
||||
}
|
||||
|
||||
private static func resolveA2UIHostUrl(from raw: String?) -> String? {
|
||||
@@ -486,8 +485,7 @@ actor MacNodeRuntime {
|
||||
if !forceRefresh, let current = await self.resolveA2UIHostUrl() {
|
||||
return current
|
||||
}
|
||||
let refreshedCanvasSurfaceUrl = await self.refreshCanvasSurfaceUrl()
|
||||
return Self.resolveA2UIHostUrl(from: refreshedCanvasSurfaceUrl)
|
||||
return Self.resolveA2UIHostUrl(from: await self.refreshCanvasSurfaceUrl())
|
||||
}
|
||||
|
||||
private func isA2UIReady(poll: Bool = false) async -> Bool {
|
||||
|
||||
@@ -145,7 +145,10 @@ final class OnboardingWizardModel {
|
||||
self.sessionId = res.sessionid
|
||||
self.status = wizardStatusString(res.status) ?? (res.done ? "done" : "running")
|
||||
self.errorMessage = res.error
|
||||
self.currentStep = res.step
|
||||
self.currentStep = decodeWizardStep(res.step)
|
||||
if self.currentStep == nil, res.step != nil {
|
||||
onboardingWizardLogger.error("wizard step decode failed")
|
||||
}
|
||||
if res.done { self.currentStep = nil }
|
||||
self.restartAttempts = 0
|
||||
}
|
||||
@@ -154,7 +157,10 @@ final class OnboardingWizardModel {
|
||||
let status = wizardStatusString(res.status)
|
||||
self.status = status ?? self.status
|
||||
self.errorMessage = res.error
|
||||
self.currentStep = res.step
|
||||
self.currentStep = decodeWizardStep(res.step)
|
||||
if self.currentStep == nil, res.step != nil {
|
||||
onboardingWizardLogger.error("wizard step decode failed")
|
||||
}
|
||||
if res.done { self.currentStep = nil }
|
||||
if res.done || status == "done" || status == "cancelled" || status == "error" {
|
||||
self.sessionId = 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.10</string>
|
||||
<string>2026.5.6</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>2026051000</string>
|
||||
<string>2026050600</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>OpenClaw</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
|
||||
@@ -207,7 +207,7 @@ actor GatewayWizardClient {
|
||||
let frame = try decodeFrame(message)
|
||||
if case let .res(res) = frame, res.id == id {
|
||||
if res.ok == false {
|
||||
let msg = res.error?.message ?? "gateway error"
|
||||
let msg = (res.error?["message"]?.value as? String) ?? "gateway error"
|
||||
throw WizardCliError.gatewayError(msg)
|
||||
}
|
||||
return res
|
||||
@@ -308,7 +308,7 @@ actor GatewayWizardClient {
|
||||
let frameResponse = try decodeFrame(message)
|
||||
if case let .res(res) = frameResponse, res.id == reqId {
|
||||
if res.ok == false {
|
||||
let msg = res.error?.message ?? "gateway connect failed"
|
||||
let msg = (res.error?["message"]?.value as? String) ?? "gateway connect failed"
|
||||
throw WizardCliError.gatewayError(msg)
|
||||
}
|
||||
_ = try self.decodePayload(res, as: HelloOk.self)
|
||||
@@ -375,7 +375,7 @@ private func runWizard(client: GatewayWizardClient, opts: WizardCliOptions) asyn
|
||||
return
|
||||
}
|
||||
|
||||
if let step = nextResult.step {
|
||||
if let step = decodeWizardStep(nextResult.step) {
|
||||
let answer = try promptAnswer(for: step)
|
||||
var answerPayload: [String: ProtoAnyCodable] = [
|
||||
"stepId": ProtoAnyCodable(step.id),
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -12,7 +12,7 @@ struct GatewayAgentChannelTests {
|
||||
#expect(GatewayAgentChannel.whatsapp.shouldDeliver(true) == true)
|
||||
#expect(GatewayAgentChannel.telegram.shouldDeliver(true) == true)
|
||||
#expect(GatewayAgentChannel.googlechat.shouldDeliver(true) == true)
|
||||
#expect(GatewayAgentChannel.imessage.shouldDeliver(true) == true)
|
||||
#expect(GatewayAgentChannel.bluebubbles.shouldDeliver(true) == true)
|
||||
#expect(GatewayAgentChannel.last.shouldDeliver(false) == false)
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ struct GatewayAgentChannelTests {
|
||||
#expect(GatewayAgentChannel(raw: " ") == .last)
|
||||
#expect(GatewayAgentChannel(raw: "WEBCHAT") == .webchat)
|
||||
#expect(GatewayAgentChannel(raw: "googlechat") == .googlechat)
|
||||
#expect(GatewayAgentChannel(raw: "IMESSAGE") == .imessage)
|
||||
#expect(GatewayAgentChannel(raw: "BLUEBUBBLES") == .bluebubbles)
|
||||
#expect(GatewayAgentChannel(raw: "unknown") == .last)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ enum ChatMarkdownPreprocessor {
|
||||
"Matrix",
|
||||
"Zalo",
|
||||
"Zalo Personal",
|
||||
"BlueBubbles",
|
||||
]
|
||||
|
||||
private static let markdownImagePattern = #"!\[([^\]]*)\]\(([^)]+)\)"#
|
||||
|
||||
@@ -38,68 +38,22 @@ enum DeviceIdentityPaths {
|
||||
|
||||
public enum DeviceIdentityStore {
|
||||
private static let fileName = "device.json"
|
||||
private static let ed25519SPKIPrefix = Data([
|
||||
0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65,
|
||||
0x70, 0x03, 0x21, 0x00,
|
||||
])
|
||||
private static let ed25519PKCS8PrivatePrefix = Data([
|
||||
0x30, 0x2e, 0x02, 0x01, 0x00, 0x30, 0x05, 0x06,
|
||||
0x03, 0x2b, 0x65, 0x70, 0x04, 0x22, 0x04, 0x20,
|
||||
])
|
||||
|
||||
public static func loadOrCreate() -> DeviceIdentity {
|
||||
self.loadOrCreate(fileURL: self.fileURL())
|
||||
}
|
||||
|
||||
static func loadOrCreate(fileURL url: URL) -> DeviceIdentity {
|
||||
if let data = try? Data(contentsOf: url) {
|
||||
switch self.decodeStoredIdentity(data) {
|
||||
case .identity(let decoded):
|
||||
return decoded
|
||||
case .recognizedInvalid:
|
||||
return self.generate()
|
||||
case .unknown:
|
||||
break
|
||||
}
|
||||
let url = self.fileURL()
|
||||
if let data = try? Data(contentsOf: url),
|
||||
let decoded = try? JSONDecoder().decode(DeviceIdentity.self, from: data),
|
||||
!decoded.deviceId.isEmpty,
|
||||
!decoded.publicKey.isEmpty,
|
||||
!decoded.privateKey.isEmpty
|
||||
{
|
||||
return decoded
|
||||
}
|
||||
let identity = self.generate()
|
||||
self.save(identity, to: url)
|
||||
self.save(identity)
|
||||
return identity
|
||||
}
|
||||
|
||||
private enum DecodeResult {
|
||||
case identity(DeviceIdentity)
|
||||
case recognizedInvalid
|
||||
case unknown
|
||||
}
|
||||
|
||||
private static func decodeStoredIdentity(_ data: Data) -> DecodeResult {
|
||||
let decoder = JSONDecoder()
|
||||
if let decoded = try? decoder.decode(DeviceIdentity.self, from: data) {
|
||||
guard let identity = self.normalizedRawIdentity(decoded) else {
|
||||
return .recognizedInvalid
|
||||
}
|
||||
return .identity(identity)
|
||||
}
|
||||
|
||||
if let decoded = try? decoder.decode(PemDeviceIdentity.self, from: data) {
|
||||
guard decoded.version == 1,
|
||||
let publicKeyData = self.rawPublicKey(fromPEM: decoded.publicKeyPem),
|
||||
let privateKeyData = self.rawPrivateKey(fromPEM: decoded.privateKeyPem),
|
||||
self.keyPairMatches(publicKeyData: publicKeyData, privateKeyData: privateKeyData)
|
||||
else {
|
||||
return .recognizedInvalid
|
||||
}
|
||||
return .identity(DeviceIdentity(
|
||||
deviceId: self.deviceId(publicKeyData: publicKeyData),
|
||||
publicKey: publicKeyData.base64EncodedString(),
|
||||
privateKey: privateKeyData.base64EncodedString(),
|
||||
createdAtMs: decoded.createdAtMs))
|
||||
}
|
||||
|
||||
return self.hasRecognizedIdentityShape(data) ? .recognizedInvalid : .unknown
|
||||
}
|
||||
|
||||
public static func signPayload(_ payload: String, identity: DeviceIdentity) -> String? {
|
||||
guard let privateKeyData = Data(base64Encoded: identity.privateKey) else { return nil }
|
||||
do {
|
||||
@@ -116,7 +70,7 @@ public enum DeviceIdentityStore {
|
||||
let publicKey = privateKey.publicKey
|
||||
let publicKeyData = publicKey.rawRepresentation
|
||||
let privateKeyData = privateKey.rawRepresentation
|
||||
let deviceId = self.deviceId(publicKeyData: publicKeyData)
|
||||
let deviceId = SHA256.hash(data: publicKeyData).compactMap { String(format: "%02x", $0) }.joined()
|
||||
return DeviceIdentity(
|
||||
deviceId: deviceId,
|
||||
publicKey: publicKeyData.base64EncodedString(),
|
||||
@@ -137,69 +91,8 @@ public enum DeviceIdentityStore {
|
||||
return self.base64UrlEncode(data)
|
||||
}
|
||||
|
||||
private static func normalizedRawIdentity(_ identity: DeviceIdentity) -> DeviceIdentity? {
|
||||
guard !identity.deviceId.isEmpty,
|
||||
let publicKeyData = Data(base64Encoded: identity.publicKey),
|
||||
let privateKeyData = Data(base64Encoded: identity.privateKey)
|
||||
else { return nil }
|
||||
|
||||
guard publicKeyData.count == 32 && privateKeyData.count == 32,
|
||||
self.keyPairMatches(publicKeyData: publicKeyData, privateKeyData: privateKeyData)
|
||||
else { return nil }
|
||||
return DeviceIdentity(
|
||||
deviceId: self.deviceId(publicKeyData: publicKeyData),
|
||||
publicKey: identity.publicKey,
|
||||
privateKey: identity.privateKey,
|
||||
createdAtMs: identity.createdAtMs)
|
||||
}
|
||||
|
||||
private static func rawPublicKey(fromPEM pem: String) -> Data? {
|
||||
guard let der = self.derData(fromPEM: pem),
|
||||
der.count == self.ed25519SPKIPrefix.count + 32,
|
||||
der.prefix(self.ed25519SPKIPrefix.count) == self.ed25519SPKIPrefix
|
||||
else { return nil }
|
||||
return der.suffix(32)
|
||||
}
|
||||
|
||||
private static func rawPrivateKey(fromPEM pem: String) -> Data? {
|
||||
guard let der = self.derData(fromPEM: pem),
|
||||
der.count == self.ed25519PKCS8PrivatePrefix.count + 32,
|
||||
der.prefix(self.ed25519PKCS8PrivatePrefix.count) == self.ed25519PKCS8PrivatePrefix
|
||||
else { return nil }
|
||||
return der.suffix(32)
|
||||
}
|
||||
|
||||
private static func keyPairMatches(publicKeyData: Data, privateKeyData: Data) -> Bool {
|
||||
guard let privateKey = try? Curve25519.Signing.PrivateKey(rawRepresentation: privateKeyData)
|
||||
else {
|
||||
return false
|
||||
}
|
||||
return privateKey.publicKey.rawRepresentation == publicKeyData
|
||||
}
|
||||
|
||||
private static func derData(fromPEM pem: String) -> Data? {
|
||||
let body = pem
|
||||
.split(whereSeparator: \.isNewline)
|
||||
.filter { !$0.hasPrefix("-----") }
|
||||
.joined()
|
||||
return Data(base64Encoded: body)
|
||||
}
|
||||
|
||||
private static func hasRecognizedIdentityShape(_ data: Data) -> Bool {
|
||||
guard let object = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
|
||||
return false
|
||||
}
|
||||
return object.keys.contains("publicKeyPem")
|
||||
|| object.keys.contains("privateKeyPem")
|
||||
|| object.keys.contains("publicKey")
|
||||
|| object.keys.contains("privateKey")
|
||||
}
|
||||
|
||||
private static func deviceId(publicKeyData: Data) -> String {
|
||||
SHA256.hash(data: publicKeyData).compactMap { String(format: "%02x", $0) }.joined()
|
||||
}
|
||||
|
||||
private static func save(_ identity: DeviceIdentity, to url: URL) {
|
||||
private static func save(_ identity: DeviceIdentity) {
|
||||
let url = self.fileURL()
|
||||
do {
|
||||
try FileManager.default.createDirectory(
|
||||
at: url.deletingLastPathComponent(),
|
||||
@@ -218,11 +111,3 @@ public enum DeviceIdentityStore {
|
||||
.appendingPathComponent(self.fileName, isDirectory: false)
|
||||
}
|
||||
}
|
||||
|
||||
private struct PemDeviceIdentity: Codable {
|
||||
var version: Int
|
||||
var deviceId: String
|
||||
var publicKeyPem: String
|
||||
var privateKeyPem: String
|
||||
var createdAtMs: Int
|
||||
}
|
||||
|
||||
@@ -124,24 +124,6 @@ public enum GatewayAuthSource: String, Sendable {
|
||||
/// Avoid ambiguity with the app's own AnyCodable type.
|
||||
private typealias ProtoAnyCodable = OpenClawProtocol.AnyCodable
|
||||
|
||||
private func gatewayErrorDetails(_ error: ErrorShape?) -> [String: ProtoAnyCodable] {
|
||||
var details: [String: ProtoAnyCodable] = [:]
|
||||
if let nested = error?.details?.value as? [String: ProtoAnyCodable] {
|
||||
details.merge(nested) { _, nestedValue in nestedValue }
|
||||
}
|
||||
if let error {
|
||||
details["code"] = ProtoAnyCodable(error.code)
|
||||
details["message"] = ProtoAnyCodable(error.message)
|
||||
if let retryable = error.retryable {
|
||||
details["retryable"] = ProtoAnyCodable(retryable)
|
||||
}
|
||||
if let retryAfterMs = error.retryafterms {
|
||||
details["retryAfterMs"] = ProtoAnyCodable(retryAfterMs)
|
||||
}
|
||||
}
|
||||
return details
|
||||
}
|
||||
|
||||
private enum ConnectChallengeError: Error {
|
||||
case timeout
|
||||
}
|
||||
@@ -641,22 +623,21 @@ public actor GatewayChannelActor {
|
||||
role: String) async throws
|
||||
{
|
||||
if res.ok == false {
|
||||
let error = res.error
|
||||
let msg = error?.message ?? "gateway connect failed"
|
||||
let details = gatewayErrorDetails(error)
|
||||
let detailCode = details["code"]?.value as? String
|
||||
let canRetryWithDeviceToken = details["canRetryWithDeviceToken"]?.value as? Bool ?? false
|
||||
let recommendedNextStep = details["recommendedNextStep"]?.value as? String
|
||||
let requestId = details["requestId"]?.value as? String
|
||||
let reason = details["reason"]?.value as? String
|
||||
let owner = details["owner"]?.value as? String
|
||||
let title = details["title"]?.value as? String
|
||||
let userMessage = details["userMessage"]?.value as? String
|
||||
let actionLabel = details["actionLabel"]?.value as? String
|
||||
let actionCommand = details["actionCommand"]?.value as? String
|
||||
let docsURLString = details["docsUrl"]?.value as? String
|
||||
let retryableOverride = details["retryable"]?.value as? Bool
|
||||
let pauseReconnectOverride = details["pauseReconnect"]?.value as? Bool
|
||||
let msg = (res.error?["message"]?.value as? String) ?? "gateway connect failed"
|
||||
let details = res.error?["details"]?.value as? [String: ProtoAnyCodable]
|
||||
let detailCode = details?["code"]?.value as? String
|
||||
let canRetryWithDeviceToken = details?["canRetryWithDeviceToken"]?.value as? Bool ?? false
|
||||
let recommendedNextStep = details?["recommendedNextStep"]?.value as? String
|
||||
let requestId = details?["requestId"]?.value as? String
|
||||
let reason = details?["reason"]?.value as? String
|
||||
let owner = details?["owner"]?.value as? String
|
||||
let title = details?["title"]?.value as? String
|
||||
let userMessage = details?["userMessage"]?.value as? String
|
||||
let actionLabel = details?["actionLabel"]?.value as? String
|
||||
let actionCommand = details?["actionCommand"]?.value as? String
|
||||
let docsURLString = details?["docsUrl"]?.value as? String
|
||||
let retryableOverride = details?["retryable"]?.value as? Bool
|
||||
let pauseReconnectOverride = details?["pauseReconnect"]?.value as? Bool
|
||||
throw GatewayConnectAuthError(
|
||||
message: msg,
|
||||
detailCodeRaw: detailCode,
|
||||
@@ -993,9 +974,11 @@ public actor GatewayChannelActor {
|
||||
throw NSError(domain: "Gateway", code: 2, userInfo: [NSLocalizedDescriptionKey: "unexpected frame"])
|
||||
}
|
||||
if res.ok == false {
|
||||
let code = res.error?.code
|
||||
let msg = res.error?.message
|
||||
let details = gatewayErrorDetails(res.error)
|
||||
let code = res.error?["code"]?.value as? String
|
||||
let msg = res.error?["message"]?.value as? String
|
||||
let details: [String: AnyCodable] = (res.error ?? [:]).reduce(into: [:]) { acc, pair in
|
||||
acc[pair.key] = AnyCodable(pair.value.value)
|
||||
}
|
||||
throw GatewayResponseError(method: method, code: code, message: msg, details: details)
|
||||
}
|
||||
if let payload = res.payload {
|
||||
@@ -1030,11 +1013,7 @@ public actor GatewayChannelActor {
|
||||
|
||||
/// Wrap low-level URLSession/WebSocket errors with context so UI can surface them.
|
||||
private func wrap(_ error: Error, context: String) -> Error {
|
||||
if error is GatewayConnectAuthError ||
|
||||
error is GatewayResponseError ||
|
||||
error is GatewayDecodingError ||
|
||||
error is GatewayTLSValidationError
|
||||
{
|
||||
if error is GatewayConnectAuthError || error is GatewayResponseError || error is GatewayDecodingError || error is GatewayTLSValidationError {
|
||||
return error
|
||||
}
|
||||
if let urlError = error as? URLError {
|
||||
|
||||
@@ -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
|
||||
@@ -716,7 +716,6 @@ public struct AgentParams: Codable, Sendable {
|
||||
public let bootstrapcontextmode: AnyCodable?
|
||||
public let bootstrapcontextrunkind: AnyCodable?
|
||||
public let acpturnsource: String?
|
||||
public let internalruntimehandoffid: String?
|
||||
public let internalevents: [[String: AnyCodable]]?
|
||||
public let inputprovenance: [String: AnyCodable]?
|
||||
public let voicewaketrigger: String?
|
||||
@@ -753,7 +752,6 @@ public struct AgentParams: Codable, Sendable {
|
||||
bootstrapcontextmode: AnyCodable?,
|
||||
bootstrapcontextrunkind: AnyCodable?,
|
||||
acpturnsource: String?,
|
||||
internalruntimehandoffid: String?,
|
||||
internalevents: [[String: AnyCodable]]?,
|
||||
inputprovenance: [String: AnyCodable]?,
|
||||
voicewaketrigger: String?,
|
||||
@@ -789,7 +787,6 @@ public struct AgentParams: Codable, Sendable {
|
||||
self.bootstrapcontextmode = bootstrapcontextmode
|
||||
self.bootstrapcontextrunkind = bootstrapcontextrunkind
|
||||
self.acpturnsource = acpturnsource
|
||||
self.internalruntimehandoffid = internalruntimehandoffid
|
||||
self.internalevents = internalevents
|
||||
self.inputprovenance = inputprovenance
|
||||
self.voicewaketrigger = voicewaketrigger
|
||||
@@ -827,7 +824,6 @@ public struct AgentParams: Codable, Sendable {
|
||||
case bootstrapcontextmode = "bootstrapContextMode"
|
||||
case bootstrapcontextrunkind = "bootstrapContextRunKind"
|
||||
case acpturnsource = "acpTurnSource"
|
||||
case internalruntimehandoffid = "internalRuntimeHandoffId"
|
||||
case internalevents = "internalEvents"
|
||||
case inputprovenance = "inputProvenance"
|
||||
case voicewaketrigger = "voiceWakeTrigger"
|
||||
@@ -2247,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?
|
||||
@@ -2259,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?)
|
||||
@@ -2270,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
|
||||
@@ -2283,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 {
|
||||
@@ -2782,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?)
|
||||
{
|
||||
@@ -2809,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?)
|
||||
{
|
||||
@@ -2987,10 +2757,6 @@ public struct TalkClientCreateParams: Codable, Sendable {
|
||||
public let provider: String?
|
||||
public let model: String?
|
||||
public let voice: String?
|
||||
public let vadthreshold: Double?
|
||||
public let silencedurationms: Int?
|
||||
public let prefixpaddingms: Int?
|
||||
public let reasoningeffort: String?
|
||||
public let mode: AnyCodable?
|
||||
public let transport: AnyCodable?
|
||||
public let brain: AnyCodable?
|
||||
@@ -3000,10 +2766,6 @@ public struct TalkClientCreateParams: Codable, Sendable {
|
||||
provider: String?,
|
||||
model: String?,
|
||||
voice: String?,
|
||||
vadthreshold: Double?,
|
||||
silencedurationms: Int?,
|
||||
prefixpaddingms: Int?,
|
||||
reasoningeffort: String?,
|
||||
mode: AnyCodable?,
|
||||
transport: AnyCodable?,
|
||||
brain: AnyCodable?)
|
||||
@@ -3012,10 +2774,6 @@ public struct TalkClientCreateParams: Codable, Sendable {
|
||||
self.provider = provider
|
||||
self.model = model
|
||||
self.voice = voice
|
||||
self.vadthreshold = vadthreshold
|
||||
self.silencedurationms = silencedurationms
|
||||
self.prefixpaddingms = prefixpaddingms
|
||||
self.reasoningeffort = reasoningeffort
|
||||
self.mode = mode
|
||||
self.transport = transport
|
||||
self.brain = brain
|
||||
@@ -3026,10 +2784,6 @@ public struct TalkClientCreateParams: Codable, Sendable {
|
||||
case provider
|
||||
case model
|
||||
case voice
|
||||
case vadthreshold = "vadThreshold"
|
||||
case silencedurationms = "silenceDurationMs"
|
||||
case prefixpaddingms = "prefixPaddingMs"
|
||||
case reasoningeffort = "reasoningEffort"
|
||||
case mode
|
||||
case transport
|
||||
case brain
|
||||
@@ -3183,10 +2937,6 @@ public struct TalkSessionCreateParams: Codable, Sendable {
|
||||
public let provider: String?
|
||||
public let model: String?
|
||||
public let voice: String?
|
||||
public let vadthreshold: Double?
|
||||
public let silencedurationms: Int?
|
||||
public let prefixpaddingms: Int?
|
||||
public let reasoningeffort: String?
|
||||
public let mode: AnyCodable?
|
||||
public let transport: AnyCodable?
|
||||
public let brain: AnyCodable?
|
||||
@@ -3197,10 +2947,6 @@ public struct TalkSessionCreateParams: Codable, Sendable {
|
||||
provider: String?,
|
||||
model: String?,
|
||||
voice: String?,
|
||||
vadthreshold: Double?,
|
||||
silencedurationms: Int?,
|
||||
prefixpaddingms: Int?,
|
||||
reasoningeffort: String?,
|
||||
mode: AnyCodable?,
|
||||
transport: AnyCodable?,
|
||||
brain: AnyCodable?,
|
||||
@@ -3210,10 +2956,6 @@ public struct TalkSessionCreateParams: Codable, Sendable {
|
||||
self.provider = provider
|
||||
self.model = model
|
||||
self.voice = voice
|
||||
self.vadthreshold = vadthreshold
|
||||
self.silencedurationms = silencedurationms
|
||||
self.prefixpaddingms = prefixpaddingms
|
||||
self.reasoningeffort = reasoningeffort
|
||||
self.mode = mode
|
||||
self.transport = transport
|
||||
self.brain = brain
|
||||
@@ -3225,10 +2967,6 @@ public struct TalkSessionCreateParams: Codable, Sendable {
|
||||
case provider
|
||||
case model
|
||||
case voice
|
||||
case vadthreshold = "vadThreshold"
|
||||
case silencedurationms = "silenceDurationMs"
|
||||
case prefixpaddingms = "prefixPaddingMs"
|
||||
case reasoningeffort = "reasoningEffort"
|
||||
case mode
|
||||
case transport
|
||||
case brain
|
||||
@@ -3442,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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4795,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,
|
||||
@@ -4804,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
|
||||
@@ -4914,80 +4648,6 @@ public struct SkillsDetailResult: Codable, Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
public struct SkillsUploadBeginParams: Codable, Sendable {
|
||||
public let kind: String
|
||||
public let slug: String
|
||||
public let sizebytes: Int
|
||||
public let sha256: String?
|
||||
public let force: Bool?
|
||||
public let idempotencykey: String?
|
||||
|
||||
public init(
|
||||
kind: String,
|
||||
slug: String,
|
||||
sizebytes: Int,
|
||||
sha256: String?,
|
||||
force: Bool?,
|
||||
idempotencykey: String?)
|
||||
{
|
||||
self.kind = kind
|
||||
self.slug = slug
|
||||
self.sizebytes = sizebytes
|
||||
self.sha256 = sha256
|
||||
self.force = force
|
||||
self.idempotencykey = idempotencykey
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case kind
|
||||
case slug
|
||||
case sizebytes = "sizeBytes"
|
||||
case sha256
|
||||
case force
|
||||
case idempotencykey = "idempotencyKey"
|
||||
}
|
||||
}
|
||||
|
||||
public struct SkillsUploadChunkParams: Codable, Sendable {
|
||||
public let uploadid: String
|
||||
public let offset: Int
|
||||
public let database64: String
|
||||
|
||||
public init(
|
||||
uploadid: String,
|
||||
offset: Int,
|
||||
database64: String)
|
||||
{
|
||||
self.uploadid = uploadid
|
||||
self.offset = offset
|
||||
self.database64 = database64
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case uploadid = "uploadId"
|
||||
case offset
|
||||
case database64 = "dataBase64"
|
||||
}
|
||||
}
|
||||
|
||||
public struct SkillsUploadCommitParams: Codable, Sendable {
|
||||
public let uploadid: String
|
||||
public let sha256: String?
|
||||
|
||||
public init(
|
||||
uploadid: String,
|
||||
sha256: String?)
|
||||
{
|
||||
self.uploadid = uploadid
|
||||
self.sha256 = sha256
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case uploadid = "uploadId"
|
||||
case sha256
|
||||
}
|
||||
}
|
||||
|
||||
public struct CronJob: Codable, Sendable {
|
||||
public let id: String
|
||||
public let agentid: String?
|
||||
@@ -5472,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?
|
||||
@@ -5495,7 +5154,6 @@ public struct ExecApprovalRequestParams: Codable, Sendable {
|
||||
security: AnyCodable?,
|
||||
ask: AnyCodable?,
|
||||
warningtext: AnyCodable?,
|
||||
commandspans: [[String: AnyCodable]]?,
|
||||
agentid: AnyCodable?,
|
||||
resolvedpath: AnyCodable?,
|
||||
sessionkey: AnyCodable?,
|
||||
@@ -5517,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
|
||||
@@ -5541,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"
|
||||
@@ -5933,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?
|
||||
@@ -5950,7 +5605,6 @@ public struct ChatSendParams: Codable, Sendable {
|
||||
sessionid: String?,
|
||||
message: String,
|
||||
thinking: String?,
|
||||
fastmode: Bool?,
|
||||
deliver: Bool?,
|
||||
originatingchannel: String?,
|
||||
originatingto: String?,
|
||||
@@ -5966,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
|
||||
@@ -5984,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,95 +0,0 @@
|
||||
import CryptoKit
|
||||
import Foundation
|
||||
import Testing
|
||||
@testable import OpenClawKit
|
||||
|
||||
@Suite(.serialized)
|
||||
struct DeviceIdentityStoreTests {
|
||||
@Test("loads TypeScript PEM identity schema without rewriting or regenerating")
|
||||
func loadsTypeScriptPEMIdentitySchema() throws {
|
||||
let tempDir = FileManager.default.temporaryDirectory
|
||||
.appendingPathComponent(UUID().uuidString, isDirectory: true)
|
||||
let identityURL = tempDir
|
||||
.appendingPathComponent("identity", isDirectory: true)
|
||||
.appendingPathComponent("device.json", isDirectory: false)
|
||||
defer { try? FileManager.default.removeItem(at: tempDir) }
|
||||
try FileManager.default.createDirectory(
|
||||
at: identityURL.deletingLastPathComponent(),
|
||||
withIntermediateDirectories: true)
|
||||
let stored = try Self.identityJSON(
|
||||
publicKeyPem: Self.pem(
|
||||
label: "PUBLIC KEY",
|
||||
body: "MCowBQYDK2VwAyEAA6EHv/POEL4dcN0Y50vAmWfk1jCbpQ1fHdyGZBJVMbg="),
|
||||
privateKeyPem: Self.pem(
|
||||
label: "PRIVATE KEY",
|
||||
body: "MC4CAQAwBQYDK2VwBCIEIAABAgMEBQYHCAkKCwwNDg8QERITFBUWFxgZGhscHR4f"))
|
||||
try stored.write(to: identityURL, atomically: true, encoding: .utf8)
|
||||
let before = try String(contentsOf: identityURL, encoding: .utf8)
|
||||
|
||||
let identity = DeviceIdentityStore.loadOrCreate(fileURL: identityURL)
|
||||
|
||||
#expect(identity.deviceId == "56475aa75463474c0285df5dbf2bcab73da651358839e9b77481b2eab107708c")
|
||||
#expect(identity.publicKey == "A6EHv/POEL4dcN0Y50vAmWfk1jCbpQ1fHdyGZBJVMbg=")
|
||||
#expect(identity.privateKey == "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=")
|
||||
#expect(DeviceIdentityStore.publicKeyBase64Url(identity) == "A6EHv_POEL4dcN0Y50vAmWfk1jCbpQ1fHdyGZBJVMbg")
|
||||
let signature = try #require(DeviceIdentityStore.signPayload("hello", identity: identity))
|
||||
let publicKeyData = try #require(Data(base64Encoded: identity.publicKey))
|
||||
let signatureData = try #require(Self.base64UrlDecode(signature))
|
||||
let publicKey = try Curve25519.Signing.PublicKey(rawRepresentation: publicKeyData)
|
||||
#expect(publicKey.isValidSignature(signatureData, for: Data("hello".utf8)))
|
||||
#expect(try String(contentsOf: identityURL, encoding: .utf8) == before)
|
||||
}
|
||||
|
||||
@Test("does not overwrite a recognized invalid TypeScript identity schema")
|
||||
func preservesInvalidTypeScriptPEMIdentitySchema() throws {
|
||||
let tempDir = FileManager.default.temporaryDirectory
|
||||
.appendingPathComponent(UUID().uuidString, isDirectory: true)
|
||||
let identityURL = tempDir
|
||||
.appendingPathComponent("identity", isDirectory: true)
|
||||
.appendingPathComponent("device.json", isDirectory: false)
|
||||
defer { try? FileManager.default.removeItem(at: tempDir) }
|
||||
try FileManager.default.createDirectory(
|
||||
at: identityURL.deletingLastPathComponent(),
|
||||
withIntermediateDirectories: true)
|
||||
let stored = """
|
||||
{
|
||||
"version": 1,
|
||||
"deviceId": "stale-device-id",
|
||||
"publicKeyPem": "not-a-valid-public-key",
|
||||
"privateKeyPem": "not-a-valid-private-key",
|
||||
"createdAtMs": 1700000000000
|
||||
}
|
||||
"""
|
||||
try stored.write(to: identityURL, atomically: true, encoding: .utf8)
|
||||
let before = try String(contentsOf: identityURL, encoding: .utf8)
|
||||
|
||||
let identity = DeviceIdentityStore.loadOrCreate(fileURL: identityURL)
|
||||
|
||||
#expect(identity.deviceId != "stale-device-id")
|
||||
#expect(try String(contentsOf: identityURL, encoding: .utf8) == before)
|
||||
}
|
||||
|
||||
private static func base64UrlDecode(_ value: String) -> Data? {
|
||||
let normalized = value
|
||||
.replacingOccurrences(of: "-", with: "+")
|
||||
.replacingOccurrences(of: "_", with: "/")
|
||||
let padded = normalized + String(repeating: "=", count: (4 - normalized.count % 4) % 4)
|
||||
return Data(base64Encoded: padded)
|
||||
}
|
||||
|
||||
private static func identityJSON(publicKeyPem: String, privateKeyPem: String) throws -> String {
|
||||
let object: [String: Any] = [
|
||||
"version": 1,
|
||||
"deviceId": "stale-device-id",
|
||||
"publicKeyPem": publicKeyPem,
|
||||
"privateKeyPem": privateKeyPem,
|
||||
"createdAtMs": 1_700_000_000_000,
|
||||
]
|
||||
let data = try JSONSerialization.data(withJSONObject: object, options: [.prettyPrinted, .sortedKeys])
|
||||
return String(decoding: data, as: UTF8.self) + "\n"
|
||||
}
|
||||
|
||||
private static func pem(label: String, body: String) -> String {
|
||||
"-----BEGIN \(label)-----\n\(body)\n-----END \(label)-----\n"
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
- Signal/container mode: add REST API support for bbernhard/signal-cli-rest-api containerized deployments via a unified adapter layer, with automatic mode detection and `channels.signal.apiMode` config. (#10240) Thanks @Hua688.
|
||||
@@ -1,4 +1,4 @@
|
||||
da702349b376821e0bc1420a945287dea0bccc79298e269abb028718983e94a5 config-baseline.json
|
||||
8c647da77392bd4e87aac07fbdfc7592bbd656dc09f8844759d2c65dc374bd0d config-baseline.core.json
|
||||
80f0f51caedf14dc2138d975b62852ff7c5cf085df1c734c9de279f5859a7eeb config-baseline.channel.json
|
||||
dba159f639977bb96d79f0b78de2c6de48d25ed6ba1590f55812affb7ca6e4b0 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 @@
|
||||
32f0b7801c9e5e0b7ec8d7da11cec62713e968abf056560ad6372aac877fdf14 plugin-sdk-api-baseline.json
|
||||
e26cfb7da5e6e8addd0bd4669bd53a4188c53f8371cb20216d854f7dd0154b1b 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"
|
||||
@@ -59,22 +39,6 @@
|
||||
"source": "Channel message API",
|
||||
"target": "频道消息 API"
|
||||
},
|
||||
{
|
||||
"source": "Channel ingress API",
|
||||
"target": "频道入口 API"
|
||||
},
|
||||
{
|
||||
"source": "Channel access cleanup",
|
||||
"target": "频道访问清理"
|
||||
},
|
||||
{
|
||||
"source": "Ingress core shrink plan",
|
||||
"target": "入口核心精简计划"
|
||||
},
|
||||
{
|
||||
"source": "Ingress core shrink",
|
||||
"target": "入口核心精简"
|
||||
},
|
||||
{
|
||||
"source": "Talk mode",
|
||||
"target": "Talk 模式"
|
||||
@@ -127,26 +91,6 @@
|
||||
"source": "Codex harness",
|
||||
"target": "Codex harness"
|
||||
},
|
||||
{
|
||||
"source": "Codex harness reference",
|
||||
"target": "Codex harness reference"
|
||||
},
|
||||
{
|
||||
"source": "Codex harness runtime",
|
||||
"target": "Codex harness runtime"
|
||||
},
|
||||
{
|
||||
"source": "Native Codex plugins",
|
||||
"target": "Native Codex plugins"
|
||||
},
|
||||
{
|
||||
"source": "Codex Computer Use",
|
||||
"target": "Codex Computer Use"
|
||||
},
|
||||
{
|
||||
"source": "Diagnostics export",
|
||||
"target": "诊断导出"
|
||||
},
|
||||
{
|
||||
"source": "Agent harness plugins",
|
||||
"target": "Agent harness plugins"
|
||||
@@ -647,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 权限"
|
||||
@@ -727,10 +659,6 @@
|
||||
"source": "Migrate",
|
||||
"target": "迁移"
|
||||
},
|
||||
{
|
||||
"source": "Migrate CLI",
|
||||
"target": "迁移 CLI"
|
||||
},
|
||||
{
|
||||
"source": "Migrating",
|
||||
"target": "迁移"
|
||||
@@ -807,10 +735,6 @@
|
||||
"source": "Matrix QA",
|
||||
"target": "Matrix QA"
|
||||
},
|
||||
{
|
||||
"source": "Matrix presentation metadata",
|
||||
"target": "Matrix 呈现元数据"
|
||||
},
|
||||
{
|
||||
"source": "QA overview",
|
||||
"target": "QA overview"
|
||||
@@ -823,10 +747,6 @@
|
||||
"source": "Rich Output Protocol",
|
||||
"target": "富输出协议"
|
||||
},
|
||||
{
|
||||
"source": "Trajectory export",
|
||||
"target": "轨迹导出"
|
||||
},
|
||||
{
|
||||
"source": "Tencent Cloud (TokenHub)",
|
||||
"target": "腾讯云(TokenHub)"
|
||||
@@ -862,25 +782,5 @@
|
||||
{
|
||||
"source": "fs-safe Cleanup Plan",
|
||||
"target": "fs-safe Cleanup Plan"
|
||||
},
|
||||
{
|
||||
"source": "Tool Search",
|
||||
"target": "工具搜索"
|
||||
},
|
||||
{
|
||||
"source": "Tools and plugins",
|
||||
"target": "工具和插件"
|
||||
},
|
||||
{
|
||||
"source": "Multi-agent sandbox and tools",
|
||||
"target": "多 Agent 沙盒和工具"
|
||||
},
|
||||
{
|
||||
"source": "Exec tool",
|
||||
"target": "Exec 工具"
|
||||
},
|
||||
{
|
||||
"source": "ACP agents setup",
|
||||
"target": "ACP Agents 设置"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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`).
|
||||
|
||||
@@ -48,12 +48,11 @@ Cron is the Gateway's built-in scheduler. It persists jobs, wakes the agent at t
|
||||
- On Gateway startup, overdue isolated agent-turn jobs are rescheduled out of the channel-connect window instead of replaying immediately, so Discord/Telegram startup and native-command setup stay responsive after restarts.
|
||||
- One-shot jobs (`--at`) auto-delete after success by default.
|
||||
- Isolated cron runs best-effort close tracked browser tabs/processes for their `cron:<jobId>` session when the run completes, so detached browser automation does not leave orphaned processes behind.
|
||||
- Isolated cron runs that receive the narrow cron self-cleanup grant can still read scheduler status, a self-filtered list of their current job, and that job's run history, so status/heartbeat checks can inspect their own schedule without gaining broader cron mutation access.
|
||||
- Isolated cron runs that receive the narrow cron self-cleanup grant can still read scheduler status and a self-filtered list of their current job, so status/heartbeat checks can inspect their own schedule without gaining broader cron mutation access.
|
||||
- Isolated cron runs also guard against stale acknowledgement replies. If the first result is just an interim status update (`on it`, `pulling everything together`, and similar hints) and no descendant subagent run is still responsible for the final answer, OpenClaw re-prompts once for the actual result before delivery.
|
||||
- Isolated cron runs prefer structured execution-denial metadata from the embedded run, then fall back to known final summary/output markers such as `SYSTEM_RUN_DENIED` and `INVALID_REQUEST`, so a blocked command is not reported as a green run.
|
||||
- Isolated cron runs also treat run-level agent failures as job errors even when no reply payload is produced, so model/provider failures increment error counters and trigger failure notifications instead of clearing the job as successful.
|
||||
- When an isolated agent-turn job reaches `timeoutSeconds`, cron aborts the underlying agent run and gives it a short cleanup window. If the run does not drain, Gateway-owned cleanup force-clears that run's session ownership before cron records the timeout, so queued chat work is not left behind a stale processing session.
|
||||
- If an isolated agent-turn stalls before the runner starts or before the first model call, cron records a phase-specific timeout such as `setup timed out before runner start` or `stalled before first model call (last phase: context-engine)`. These watchdogs cover embedded providers and CLI-backed providers before their external CLI process is actually started, and are capped independently from long `timeoutSeconds` values so cold-start/auth/context failures surface quickly instead of waiting for the full job budget.
|
||||
|
||||
<a id="maintenance"></a>
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -243,7 +243,7 @@ openclaw tasks notify <lookup> state_changes
|
||||
openclaw tasks maintenance --apply [--json]
|
||||
```
|
||||
|
||||
Use this to preview or apply reconciliation, cleanup stamping, and pruning for tasks, Task Flow state, and stale cron run session registry rows.
|
||||
Use this to preview or apply reconciliation, cleanup stamping, and pruning for tasks and Task Flow state.
|
||||
|
||||
Reconciliation is runtime-aware:
|
||||
|
||||
@@ -260,8 +260,6 @@ openclaw tasks notify <lookup> state_changes
|
||||
- Subagent completion delivery prefers the latest visible assistant text; if that is empty it falls back to sanitized latest tool/toolResult text, and timeout-only tool-call runs can collapse to a short partial-progress summary. Terminal failed runs announce failure status without replaying captured reply text.
|
||||
- Cleanup failures do not mask the real task outcome.
|
||||
|
||||
When applying maintenance, OpenClaw also removes stale `cron:<jobId>:run:<uuid>` session registry rows older than 7 days, while preserving rows for currently running cron jobs and leaving non-cron session rows untouched.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="tasks flow list | show | cancel">
|
||||
```bash
|
||||
|
||||
@@ -125,26 +125,7 @@ Access groups are available in shared message-channel authorization paths, inclu
|
||||
- channel-specific per-room sender allowlists that use the same sender matching rules
|
||||
- command authorization paths that reuse message-channel sender allowlists
|
||||
|
||||
Channel support depends on whether that channel is wired through the shared OpenClaw sender-authorization helpers. Current bundled support includes Discord, Feishu, Google Chat, iMessage, LINE, Mattermost, Microsoft Teams, Nextcloud Talk, Nostr, QQBot, Signal, WhatsApp, Zalo, and Zalo Personal. Static `message.senders` groups are designed to be channel-agnostic, so new message channels should support them by using the shared plugin SDK helpers instead of custom allowlist expansion.
|
||||
|
||||
## Plugin diagnostics
|
||||
|
||||
Plugin authors can inspect structured access-group state without expanding it back into a flat allowlist:
|
||||
|
||||
```typescript
|
||||
import { resolveAccessGroupAllowFromState } from "openclaw/plugin-sdk/security-runtime";
|
||||
|
||||
const state = await resolveAccessGroupAllowFromState({
|
||||
accessGroups: cfg.accessGroups,
|
||||
allowFrom: channelConfig.allowFrom,
|
||||
channel: "my-channel",
|
||||
accountId: "default",
|
||||
senderId,
|
||||
isSenderAllowed,
|
||||
});
|
||||
```
|
||||
|
||||
The result reports referenced, matched, missing, unsupported, and failed groups. Use this when you need diagnostics or conformance tests. Use `expandAllowFromWithAccessGroups(...)` only for compatibility paths that still expect a flat `allowFrom` array.
|
||||
Channel support depends on whether that channel is wired through the shared OpenClaw sender-authorization helpers. Current bundled support includes Discord, Google Chat, Nostr, WhatsApp, Zalo, and Zalo Personal. Static `message.senders` groups are designed to be channel-agnostic, so new message channels should support them by using the shared plugin SDK helpers instead of custom allowlist expansion.
|
||||
|
||||
## Discord channel audiences
|
||||
|
||||
|
||||
@@ -1,138 +0,0 @@
|
||||
---
|
||||
summary: "ClickClack bot-token channel setup and target syntax"
|
||||
read_when:
|
||||
- Connecting OpenClaw to a ClickClack workspace
|
||||
- Testing ClickClack bot identities
|
||||
title: "ClickClack"
|
||||
---
|
||||
|
||||
ClickClack connects OpenClaw to a self-hosted ClickClack workspace through first-class ClickClack bot tokens.
|
||||
|
||||
Use this when you want an OpenClaw agent to appear as a ClickClack bot user. ClickClack supports independent service bots and user-owned bots; user-owned bots keep an `owner_user_id` and receive only the token scopes you grant.
|
||||
|
||||
## Quick setup
|
||||
|
||||
Create a bot token in ClickClack:
|
||||
|
||||
```bash
|
||||
clickclack admin bot create \
|
||||
--workspace <workspace_id_or_slug> \
|
||||
--name "OpenClaw" \
|
||||
--handle openclaw \
|
||||
--scopes bot:write \
|
||||
--plain
|
||||
```
|
||||
|
||||
For a user-owned bot, add `--owner <user_id>`.
|
||||
|
||||
Configure OpenClaw:
|
||||
|
||||
```json5
|
||||
{
|
||||
plugins: {
|
||||
entries: {
|
||||
clickclack: {
|
||||
llm: {
|
||||
allowAgentIdOverride: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
channels: {
|
||||
clickclack: {
|
||||
enabled: true,
|
||||
baseUrl: "https://app.clickclack.chat",
|
||||
token: { source: "env", provider: "default", id: "CLICKCLACK_BOT_TOKEN" },
|
||||
workspace: "default",
|
||||
defaultTo: "channel:general",
|
||||
agentId: "clickclack-bot",
|
||||
replyMode: "model",
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Then run:
|
||||
|
||||
```bash
|
||||
export CLICKCLACK_BOT_TOKEN="ccb_..."
|
||||
openclaw gateway
|
||||
```
|
||||
|
||||
## Multiple bots
|
||||
|
||||
Each account opens its own ClickClack realtime connection and uses its own bot token.
|
||||
|
||||
```json5
|
||||
{
|
||||
plugins: {
|
||||
entries: {
|
||||
clickclack: {
|
||||
llm: {
|
||||
allowAgentIdOverride: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
channels: {
|
||||
clickclack: {
|
||||
enabled: true,
|
||||
baseUrl: "https://app.clickclack.chat",
|
||||
defaultAccount: "service",
|
||||
accounts: {
|
||||
service: {
|
||||
token: { source: "env", provider: "default", id: "CLICKCLACK_SERVICE_BOT_TOKEN" },
|
||||
workspace: "default",
|
||||
defaultTo: "channel:general",
|
||||
agentId: "service-bot",
|
||||
replyMode: "model",
|
||||
},
|
||||
peter: {
|
||||
token: { source: "env", provider: "default", id: "CLICKCLACK_PETER_BOT_TOKEN" },
|
||||
workspace: "default",
|
||||
defaultTo: "dm:usr_...",
|
||||
agentId: "peter-bot",
|
||||
replyMode: "model",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
`replyMode: "model"` uses `api.runtime.llm.complete` directly for short bot replies.
|
||||
When an account sets `agentId`, OpenClaw requires the explicit
|
||||
`plugins.entries.clickclack.llm.allowAgentIdOverride` trust bit so the plugin
|
||||
can run completions for that bot agent. Keep it off if you only use the default
|
||||
agent route.
|
||||
|
||||
## Targets
|
||||
|
||||
- `channel:<name-or-id>` sends to a workspace channel. Bare targets default to `channel:`.
|
||||
- `dm:<user_id>` creates or reuses a direct conversation with that user.
|
||||
- `thread:<message_id>` replies in an existing thread.
|
||||
|
||||
Examples:
|
||||
|
||||
```bash
|
||||
openclaw message send --channel clickclack --target channel:general --message "hello"
|
||||
openclaw message send --channel clickclack --target dm:usr_123 --message "hello"
|
||||
openclaw message send --channel clickclack --target thread:msg_123 --message "following up"
|
||||
```
|
||||
|
||||
## Permissions
|
||||
|
||||
ClickClack token scopes are enforced by the ClickClack API.
|
||||
|
||||
- `bot:read`: read workspace/channel/message/thread/DM/realtime/profile data.
|
||||
- `bot:write`: `bot:read` plus channel messages, thread replies, DMs, and uploads.
|
||||
- `bot:admin`: `bot:write` plus channel creation.
|
||||
|
||||
OpenClaw only needs `bot:write` for normal agent chat.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- `ClickClack is not configured`: set `channels.clickclack.token` or `CLICKCLACK_BOT_TOKEN`.
|
||||
- `workspace not found`: set `workspace` to the workspace id or slug returned by ClickClack.
|
||||
- No inbound replies: confirm the token has realtime read access and the bot is not replying to its own messages.
|
||||
- Channel sends fail: verify the bot is a member of the workspace and has `bot:write`.
|
||||
@@ -352,7 +352,7 @@ By default, components are single use. Set `components.reusable=true` to allow b
|
||||
|
||||
To restrict who can click a button, set `allowedUsers` on that button (Discord user IDs, tags, or `*`). When configured, unmatched users receive an ephemeral denial.
|
||||
|
||||
The `/model` and `/models` slash commands open an interactive model picker with provider, model, and compatible runtime dropdowns plus a Submit step. `/models add` is deprecated and now returns a deprecation message instead of registering models from chat. The picker reply is ephemeral and only the invoking user can use it. Discord select menus are limited to 25 options, so add `provider/*` entries to `agents.defaults.models` when you want the picker to show dynamically discovered models only for selected providers such as `openai-codex` or `vllm`.
|
||||
The `/model` and `/models` slash commands open an interactive model picker with provider, model, and compatible runtime dropdowns plus a Submit step. `/models add` is deprecated and now returns a deprecation message instead of registering models from chat. The picker reply is ephemeral and only the invoking user can use it.
|
||||
|
||||
File attachments:
|
||||
|
||||
@@ -451,8 +451,8 @@ Example:
|
||||
|
||||
</Tab>
|
||||
|
||||
<Tab title="Access groups">
|
||||
Discord DMs and text command authorization can use dynamic `accessGroup:<name>` entries in `channels.discord.allowFrom`.
|
||||
<Tab title="DM access groups">
|
||||
Discord DMs can use dynamic `accessGroup:<name>` entries in `channels.discord.allowFrom`.
|
||||
|
||||
Access group names are shared across message channels. Use `type: "message.senders"` for a static group whose members are expressed in each channel's normal `allowFrom` syntax, or `type: "discord.channelAudience"` when a Discord channel's current `ViewChannel` audience should define membership dynamically. Shared access-group behavior is documented here: [Access groups](/channels/access-groups).
|
||||
|
||||
@@ -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,55 +1194,26 @@ 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 that default `always` mode, the realtime layer does not auto-speak filler before the consult answer; it captures and transcribes speech, then speaks the routed OpenClaw answer. If multiple forced consult answers finish while Discord is still playing the first answer, later exact-speech answers are queued until playback idles instead of replacing speech mid-sentence.
|
||||
- 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"`.
|
||||
- The OpenAI realtime provider accepts current Realtime 2 event names and legacy Codex-compatible aliases for output audio and transcript events, so compatible provider snapshots can drift without dropping assistant audio.
|
||||
- `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.
|
||||
- OpenClaw defaults to the pure-JS `opusscript` decoder for Discord voice receive. The optional native `@discordjs/opus` package is ignored by the repo pnpm install policy so normal installs, Docker lanes, and unrelated tests do not compile a native addon. Dedicated voice-performance hosts can opt in with `OPENCLAW_DISCORD_OPUS_DECODER=native` after installing the native addon.
|
||||
- `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.
|
||||
- If receive logs repeatedly show `DecryptionFailed(UnencryptedWhenPassthroughDisabled)` after updating, collect a dependency report and logs. The bundled `@discordjs/voice` line includes the upstream padding fix from discord.js PR #11449, which closed discord.js issue #11419.
|
||||
- `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.
|
||||
- In `agent-proxy` mode, forced consult fallback skips likely incomplete transcript fragments such as text ending in `...` or a trailing connector like `and`, plus obvious non-actionable closings like “be right back” or “bye”. Logs show `forced agent consult skipped reason=...` when this prevents a stale queued answer.
|
||||
|
||||
Native opus setup for source checkouts:
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
mise exec node@22 -- pnpm discord:opus:install
|
||||
```
|
||||
|
||||
Use Node 22 for the gateway when you want the upstream macOS arm64 prebuilt native addon. If you use another Node runtime, the opt-in installer may need a local `node-gyp` source-build toolchain.
|
||||
|
||||
After installing the native addon, start the Gateway with:
|
||||
|
||||
```bash
|
||||
OPENCLAW_DISCORD_OPUS_DECODER=native pnpm gateway:watch
|
||||
```
|
||||
|
||||
Verbose voice logs should show `discord voice: opus decoder: @discordjs/opus`. Without the env opt-in, or if the native addon is missing or cannot load on the host, OpenClaw logs `discord voice: opus decoder: opusscript` and keeps receiving voice through the pure-JS fallback.
|
||||
|
||||
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`.
|
||||
@@ -1252,184 +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 ... autoRespond=false interruptResponse=false bargeIn=false minBargeInAudioEndMs=...`
|
||||
- On speaker audio: `discord voice: realtime speaker turn opened ...`, `discord voice: realtime input audio started ... outputAudioMs=... outputActive=...`, and `discord voice: realtime speaker turn closed ... chunks=... discordBytes=... realtimeBytes=... interruptedPlayback=...`
|
||||
- On skipped stale speech: `discord voice: realtime forced agent consult skipped reason=incomplete-transcript ...` or `reason=non-actionable-closing ...`
|
||||
- On realtime response completion: `discord voice: realtime audio playback finishing reason=response.done ... audioMs=... chunks=...`
|
||||
- On playback stop/reset: `discord voice: realtime audio playback stopped reason=... audioMs=... elapsedMs=... chunks=...`
|
||||
- On realtime consult: `discord voice: realtime consult requested ... voiceSession=... supervisorSession=... question=...`
|
||||
- On agent answer: `discord voice: agent turn answer ...`
|
||||
- On queued exact speech: `discord voice: realtime exact speech queued ... queued=... outputAudioMs=... outputActive=...`, followed by `discord voice: realtime exact speech dequeued reason=player-idle ...`
|
||||
- On barge-in detection: `discord voice: realtime barge-in detected source=speaker-start ...` or `discord voice: realtime barge-in detected source=active-speaker-audio ...`, followed by `discord voice: realtime barge-in requested reason=... outputAudioMs=... outputActive=...`
|
||||
- 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) ...`
|
||||
- On idle playback: `discord voice: realtime barge-in ignored reason=... outputActive=false ... playbackChunks=0`
|
||||
|
||||
To debug cut-off audio, read the realtime voice logs as a timeline:
|
||||
|
||||
1. `realtime audio playback started` means Discord has begun playing assistant audio. The bridge starts counting assistant output chunks, Discord PCM bytes, provider realtime bytes, and synthesized audio duration from this point.
|
||||
2. `realtime speaker turn opened` marks a Discord speaker becoming active. If playback is already active and `bargeIn` is enabled, this can be followed by `barge-in detected source=speaker-start`.
|
||||
3. `realtime input audio started` marks the first actual audio frame received for that speaker turn. `outputActive=true` or a nonzero `outputAudioMs` here means the mic is sending input while assistant playback is still active.
|
||||
4. `barge-in detected source=active-speaker-audio` means OpenClaw saw live speaker audio while assistant playback was active. This is useful for distinguishing a real interruption from a Discord speaker-start event with no useful audio.
|
||||
5. `barge-in requested reason=...` means OpenClaw asked the realtime provider to cancel or truncate the active response. It includes `outputAudioMs`, `outputActive`, and `playbackChunks` so you can see how much assistant audio had actually played before the interruption.
|
||||
6. `realtime audio playback stopped reason=...` is the local Discord playback reset point. The reason says who stopped playback: `barge-in`, `player-idle`, `provider-clear-audio`, `forced-agent-consult`, `stream-close`, or `session-close`.
|
||||
7. `realtime speaker turn closed` summarizes the captured input turn. `chunks=0` or `hasAudio=false` means the speaker turn opened but no usable audio reached the realtime bridge. `interruptedPlayback=true` means that input turn overlapped assistant output and triggered barge-in logic.
|
||||
|
||||
Useful fields:
|
||||
|
||||
- `outputAudioMs`: assistant audio duration generated by the realtime provider before the log line.
|
||||
- `audioMs`: assistant audio duration that OpenClaw counted before playback stopped.
|
||||
- `elapsedMs`: wall-clock time between opening and closing the playback stream or speaker turn.
|
||||
- `discordBytes`: 48 kHz stereo PCM bytes sent to or received from Discord voice.
|
||||
- `realtimeBytes`: provider-format PCM bytes sent to or received from the realtime provider.
|
||||
- `playbackChunks`: assistant audio chunks forwarded to Discord for the active response.
|
||||
- `sinceLastAudioMs`: gap between the last captured speaker audio frame and the speaker turn closing.
|
||||
|
||||
Common patterns:
|
||||
|
||||
- Immediate cut-off with `source=active-speaker-audio`, small `outputAudioMs`, and the same user nearby usually points to speaker echo entering the mic. Raise `voice.realtime.minBargeInAudioEndMs`, lower speaker volume, use headphones, or set `voice.realtime.providers.openai.interruptResponseOnInputAudio: false`.
|
||||
- `source=speaker-start` followed by `speaker turn closed ... hasAudio=false` means Discord reported a speaker start but no audio reached OpenClaw. That can be a transient Discord voice event, noise gate behavior, or a client briefly keying the mic.
|
||||
- `audio playback stopped reason=stream-close` without a nearby barge-in or `provider-clear-audio` means the local Discord playback stream ended unexpectedly. Check the preceding provider and Discord player logs.
|
||||
- `capture ignored during playback (barge-in disabled)` means OpenClaw intentionally dropped input while assistant audio was active. Enable `voice.realtime.bargeIn` if you want speech to interrupt playback.
|
||||
- `barge-in ignored ... outputActive=false` means Discord or provider VAD reported speech, but OpenClaw had no active playback to interrupt. This should not cut off audio.
|
||||
|
||||
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>
|
||||
@@ -217,7 +144,7 @@ If SIP-disabled isn't acceptable for your threat model:
|
||||
|
||||
Allowlist field: `channels.imessage.allowFrom`.
|
||||
|
||||
Allowlist entries can be handles, static sender access groups (`accessGroup:<name>`), or chat targets (`chat_id:*`, `chat_guid:*`, `chat_identifier:*`).
|
||||
Allowlist entries can be handles or chat targets (`chat_id:*`, `chat_guid:*`, `chat_identifier:*`).
|
||||
|
||||
</Tab>
|
||||
|
||||
@@ -230,41 +157,9 @@ If SIP-disabled isn't acceptable for your threat model:
|
||||
|
||||
Group sender allowlist: `channels.imessage.groupAllowFrom`.
|
||||
|
||||
`groupAllowFrom` entries can also reference static sender access groups (`accessGroup:<name>`).
|
||||
|
||||
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
|
||||
@@ -273,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">
|
||||
@@ -400,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.
|
||||
@@ -436,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)
|
||||
@@ -468,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>
|
||||
|
||||
@@ -557,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>
|
||||
|
||||
@@ -761,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`.
|
||||
|
||||
@@ -780,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).
|
||||
|
||||
@@ -139,7 +139,6 @@ Allowlists and policies:
|
||||
- `channels.line.groupPolicy`: `allowlist | open | disabled`
|
||||
- `channels.line.groupAllowFrom`: allowlisted LINE user IDs for groups
|
||||
- Per-group overrides: `channels.line.groups.<groupId>.allowFrom`
|
||||
- Static sender access groups can be referenced from `allowFrom`, `groupAllowFrom`, and per-group `allowFrom` with `accessGroup:<name>`.
|
||||
- Runtime note: if `channels.line` is completely missing, runtime falls back to `groupPolicy="allowlist"` for group checks (even if `channels.defaults.groupPolicy` is set).
|
||||
|
||||
LINE IDs are case-sensitive. Valid IDs look like:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -189,13 +189,11 @@ Notes:
|
||||
- `openclaw pairing list mattermost`
|
||||
- `openclaw pairing approve mattermost <CODE>`
|
||||
- Public DMs: `channels.mattermost.dmPolicy="open"` plus `channels.mattermost.allowFrom=["*"]`.
|
||||
- `channels.mattermost.allowFrom` accepts `accessGroup:<name>` entries. See [Access groups](/channels/access-groups).
|
||||
|
||||
## Channels (groups)
|
||||
|
||||
- Default: `channels.mattermost.groupPolicy = "allowlist"` (mention-gated).
|
||||
- Allowlist senders with `channels.mattermost.groupAllowFrom` (user IDs recommended).
|
||||
- `channels.mattermost.groupAllowFrom` accepts `accessGroup:<name>` entries. See [Access groups](/channels/access-groups).
|
||||
- Per-channel mention overrides live under `channels.mattermost.groups.<channelId>.requireMention` or `channels.mattermost.groups["*"].requireMention` for a default.
|
||||
- `@username` matching is mutable and only enabled when `channels.mattermost.dangerouslyAllowNameMatching: true`.
|
||||
- Open channels: `channels.mattermost.groupPolicy="open"` (mention-gated).
|
||||
|
||||
@@ -146,14 +146,14 @@ Disable with:
|
||||
**DM access**
|
||||
|
||||
- Default: `channels.msteams.dmPolicy = "pairing"`. Unknown senders are ignored until approved.
|
||||
- `channels.msteams.allowFrom` should use stable AAD object IDs or static sender access groups such as `accessGroup:core-team`.
|
||||
- `channels.msteams.allowFrom` should use stable AAD object IDs.
|
||||
- Do not rely on UPN/display-name matching for allowlists - they can change. OpenClaw disables direct name matching by default; opt in explicitly with `channels.msteams.dangerouslyAllowNameMatching: true`.
|
||||
- The wizard can resolve names to IDs via Microsoft Graph when credentials allow.
|
||||
|
||||
**Group access**
|
||||
|
||||
- Default: `channels.msteams.groupPolicy = "allowlist"` (blocked unless you add `groupAllowFrom`). Use `channels.defaults.groupPolicy` to override the default when unset.
|
||||
- `channels.msteams.groupAllowFrom` controls which senders or static sender access groups can trigger in group chats/channels (falls back to `channels.msteams.allowFrom`).
|
||||
- `channels.msteams.groupAllowFrom` controls which senders can trigger in group chats/channels (falls back to `channels.msteams.allowFrom`).
|
||||
- Set `groupPolicy: "open"` to allow any member (still mention-gated by default).
|
||||
- To allow **no channels**, set `channels.msteams.groupPolicy: "disabled"`.
|
||||
|
||||
@@ -164,7 +164,7 @@ Example:
|
||||
channels: {
|
||||
msteams: {
|
||||
groupPolicy: "allowlist",
|
||||
groupAllowFrom: ["00000000-0000-0000-0000-000000000000", "accessGroup:core-team"],
|
||||
groupAllowFrom: ["user@org.com"],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
@@ -157,7 +157,6 @@ Provider options:
|
||||
- `channels.nextcloud-talk.groupPolicy`: `allowlist | open | disabled`.
|
||||
- `channels.nextcloud-talk.groupAllowFrom`: group allowlist (user IDs).
|
||||
- `channels.nextcloud-talk.rooms`: per-room settings and allowlist.
|
||||
- Static sender access groups can be referenced from `allowFrom` and `groupAllowFrom` with `accessGroup:<name>`.
|
||||
- `channels.nextcloud-talk.historyLimit`: group history limit (0 disables).
|
||||
- `channels.nextcloud-talk.dmHistoryLimit`: DM history limit (0 disables).
|
||||
- `channels.nextcloud-talk.dms`: per-DM overrides (historyLimit).
|
||||
|
||||
@@ -44,14 +44,7 @@ Account keys:
|
||||
- `botUserId` - Matrix-style bot user id used in target grammar.
|
||||
- `botDisplayName` - display name for outbound messages.
|
||||
- `pollTimeoutMs` - long-poll wait window. Integer between 100 and 30000.
|
||||
- `allowFrom` - sender allowlist (user ids or `"*"`). Direct messages and
|
||||
allowlisted group policy both use these synthetic sender ids.
|
||||
- `groupPolicy` - shared-room policy: `"open"` (default), `"allowlist"`, or
|
||||
`"disabled"`.
|
||||
- `groupAllowFrom` - optional shared-room sender allowlist. When omitted under
|
||||
`"allowlist"`, QA Channel falls back to `allowFrom`.
|
||||
- `groups.<room>.requireMention` - require a bot mention before replying in a
|
||||
specific group/channel room. `groups."*"` sets the default.
|
||||
- `allowFrom` - sender allowlist (user ids or `"*"`).
|
||||
- `defaultTo` - fallback target when none is supplied.
|
||||
- `actions.messages` / `actions.reactions` / `actions.search` / `actions.threads` - per-action tool gating.
|
||||
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
---
|
||||
summary: "Signal support via signal-cli (native daemon or bbernhard container), setup paths, and number model"
|
||||
summary: "Signal support via signal-cli (JSON-RPC + SSE), setup paths, and number model"
|
||||
read_when:
|
||||
- Setting up Signal support
|
||||
- Debugging Signal send/receive
|
||||
title: "Signal"
|
||||
---
|
||||
|
||||
Status: external CLI integration. Gateway talks to `signal-cli` over HTTP — either native daemon (JSON-RPC + SSE) or bbernhard/signal-cli-rest-api container (REST + WebSocket).
|
||||
Status: external CLI integration. Gateway talks to `signal-cli` over HTTP JSON-RPC + SSE.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- OpenClaw installed on your server (Linux flow below tested on Ubuntu 24).
|
||||
- One of:
|
||||
- `signal-cli` available on the host (native mode), **or**
|
||||
- `bbernhard/signal-cli-rest-api` Docker container (container mode).
|
||||
- `signal-cli` available on the host where the gateway runs.
|
||||
- A phone number that can receive one verification SMS (for SMS registration path).
|
||||
- Browser access for Signal captcha (`signalcaptchas.org`) during registration.
|
||||
|
||||
@@ -181,63 +179,6 @@ If you want to manage `signal-cli` yourself (slow JVM cold starts, container ini
|
||||
|
||||
This skips auto-spawn and the startup wait inside OpenClaw. For slow starts when auto-spawning, set `channels.signal.startupTimeoutMs`.
|
||||
|
||||
## Container mode (bbernhard/signal-cli-rest-api)
|
||||
|
||||
Instead of running `signal-cli` natively, you can use the [bbernhard/signal-cli-rest-api](https://github.com/bbernhard/signal-cli-rest-api) Docker container. This wraps `signal-cli` behind a REST API and WebSocket interface.
|
||||
|
||||
Requirements:
|
||||
|
||||
- The container **must** run with `MODE=json-rpc` for real-time message receiving.
|
||||
- Register or link your Signal account inside the container before connecting OpenClaw.
|
||||
|
||||
Example `docker-compose.yml` service:
|
||||
|
||||
```yaml
|
||||
signal-cli:
|
||||
image: bbernhard/signal-cli-rest-api:latest
|
||||
environment:
|
||||
MODE: json-rpc
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- signal-cli-data:/home/.local/share/signal-cli
|
||||
```
|
||||
|
||||
OpenClaw config:
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
signal: {
|
||||
enabled: true,
|
||||
account: "+15551234567",
|
||||
httpUrl: "http://signal-cli:8080",
|
||||
autoStart: false,
|
||||
apiMode: "container", // or "auto" to detect automatically
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
The `apiMode` field controls which protocol OpenClaw uses:
|
||||
|
||||
| Value | Behavior |
|
||||
| ------------- | ------------------------------------------------------------------------------------ |
|
||||
| `"auto"` | (Default) Probes both transports; streaming validates container WebSocket receive |
|
||||
| `"native"` | Force native signal-cli (JSON-RPC at `/api/v1/rpc`, SSE at `/api/v1/events`) |
|
||||
| `"container"` | Force bbernhard container (REST at `/v2/send`, WebSocket at `/v1/receive/{account}`) |
|
||||
|
||||
When `apiMode` is `"auto"`, OpenClaw caches the detected mode for 30 seconds to avoid repeated probes. Container receive is only selected for streaming after `/v1/receive/{account}` upgrades to WebSocket, which requires `MODE=json-rpc`.
|
||||
|
||||
Container mode supports the same Signal channel operations as native mode where the container exposes matching APIs: sends, receives, attachments, typing indicators, read/viewed receipts, reactions, groups, and styled text. OpenClaw translates its native Signal RPC calls into the container's REST payloads, including `group.{base64(internal_id)}` group IDs and `text_mode: "styled"` for formatted text.
|
||||
|
||||
Operational notes:
|
||||
|
||||
- Use `autoStart: false` with container mode. OpenClaw should not spawn a native daemon when `apiMode: "container"` is selected.
|
||||
- Use `MODE=json-rpc` for receiving. `MODE=normal` can make `/v1/about` look healthy, but `/v1/receive/{account}` does not WebSocket-upgrade, so OpenClaw will not select container receive streaming in `auto` mode.
|
||||
- Set `apiMode: "container"` when you know the `httpUrl` points at bbernhard's REST API. Set `apiMode: "native"` when you know it points at native `signal-cli` JSON-RPC/SSE. Use `"auto"` when the deployment may vary.
|
||||
- Container attachment downloads honor the same media byte limits as native mode. Oversized responses are rejected before being fully buffered when the server sends `Content-Length`, and while streaming otherwise.
|
||||
|
||||
## Access control (DMs + groups)
|
||||
|
||||
DMs:
|
||||
@@ -261,8 +202,7 @@ Groups:
|
||||
|
||||
## How it works (behavior)
|
||||
|
||||
- Native mode: `signal-cli` runs as a daemon; the gateway reads events via SSE.
|
||||
- Container mode: the gateway sends via REST API and receives via WebSocket.
|
||||
- `signal-cli` runs as a daemon; the gateway reads events via SSE.
|
||||
- Inbound messages are normalized into the shared channel envelope.
|
||||
- Replies always route back to the same number or group.
|
||||
|
||||
@@ -362,7 +302,6 @@ Full configuration: [Configuration](/gateway/configuration)
|
||||
Provider options:
|
||||
|
||||
- `channels.signal.enabled`: enable/disable channel startup.
|
||||
- `channels.signal.apiMode`: `auto | native | container` (default: auto). See [Container mode](#container-mode-bbernhardsignal-cli-rest-api).
|
||||
- `channels.signal.account`: E.164 for the bot account.
|
||||
- `channels.signal.cliPath`: path to `signal-cli`.
|
||||
- `channels.signal.httpUrl`: full daemon URL (overrides host/port).
|
||||
|
||||
@@ -930,7 +930,6 @@ Current Slack message actions include `send`, `upload-file`, `download-file`, `r
|
||||
- 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`
|
||||
|
||||
@@ -81,7 +81,7 @@ openclaw directory groups list --channel zalouser --query "work"
|
||||
|
||||
`channels.zalouser.dmPolicy` supports: `pairing | allowlist | open | disabled` (default: `pairing`).
|
||||
|
||||
`channels.zalouser.allowFrom` should use stable Zalo user IDs. It can also reference static sender access groups (`accessGroup:<name>`). During interactive setup, entered names can be resolved to IDs using the plugin's in-process contact lookup.
|
||||
`channels.zalouser.allowFrom` should use stable Zalo user IDs. During interactive setup, entered names can be resolved to IDs using the plugin's in-process contact lookup.
|
||||
|
||||
If a raw name remains in config, startup resolves it only when `channels.zalouser.dangerouslyAllowNameMatching: true` is enabled. Without that opt-in, runtime sender checks are ID-only and raw names are ignored for authorization.
|
||||
|
||||
@@ -96,7 +96,7 @@ Approve via:
|
||||
- Restrict to an allowlist with:
|
||||
- `channels.zalouser.groupPolicy = "allowlist"`
|
||||
- `channels.zalouser.groups` (keys should be stable group IDs; names are resolved to IDs on startup only when `channels.zalouser.dangerouslyAllowNameMatching: true` is enabled)
|
||||
- `channels.zalouser.groupAllowFrom` (controls which senders in allowed groups can trigger the bot; static sender access groups can be referenced with `accessGroup:<name>`)
|
||||
- `channels.zalouser.groupAllowFrom` (controls which senders in allowed groups can trigger the bot)
|
||||
- Block all groups: `channels.zalouser.groupPolicy = "disabled"`.
|
||||
- The configure wizard can prompt for group allowlists.
|
||||
- On startup, OpenClaw resolves group/user names in allowlists to IDs and logs the mapping only when `channels.zalouser.dangerouslyAllowNameMatching: true` is enabled.
|
||||
|
||||
@@ -258,7 +258,7 @@ Keep `workflow_ref` and `package_ref` separate. `workflow_ref` is the trusted wo
|
||||
### Suite profiles
|
||||
|
||||
- `smoke` — `npm-onboard-channel-agent`, `gateway-network`, `config-reload`
|
||||
- `package` — `npm-onboard-channel-agent`, `doctor-switch`, `update-channel-switch`, `skill-install`, `update-corrupt-plugin`, `upgrade-survivor`, `published-upgrade-survivor`, `update-restart-auth`, `plugins-offline`, `plugin-update`
|
||||
- `package` — `npm-onboard-channel-agent`, `doctor-switch`, `update-channel-switch`, `upgrade-survivor`, `published-upgrade-survivor`, `plugins-offline`, `plugin-update`
|
||||
- `product` — `package` plus `mcp-channels`, `cron-mcp-cleanup`, `openai-web-search-minimal`, `openwebui`
|
||||
- `full` — full Docker release-path chunks with OpenWebUI
|
||||
- `custom` — exact `docker_lanes`; required when `suite_profile=custom`
|
||||
@@ -269,7 +269,7 @@ For the dedicated update and plugin testing policy, including local commands,
|
||||
Docker lanes, Package Acceptance inputs, release defaults, and failure triage,
|
||||
see [Testing updates and plugins](/help/testing-updates-plugins).
|
||||
|
||||
Release checks call Package Acceptance with `source=artifact`, the prepared release package artifact, `suite_profile=custom`, `docker_lanes='doctor-switch update-channel-switch skill-install update-corrupt-plugin upgrade-survivor published-upgrade-survivor update-restart-auth plugins-offline plugin-update'`, and `telegram_mode=mock-openai`. This keeps package migration, update, live ClawHub skill install, stale-plugin-dependency cleanup, configured-plugin install repair, offline plugin, plugin-update, and Telegram proof on the same resolved package tarball. Set `package_acceptance_package_spec` on Full Release Validation or OpenClaw Release Checks to run that same matrix against a shipped npm package instead of the SHA-built artifact. Cross-OS release checks still cover OS-specific onboarding, installer, and platform behavior; package/update product validation should start with Package Acceptance. The `published-upgrade-survivor` Docker lane validates one published package baseline per run in the blocking release path. In Package Acceptance, the resolved `package-under-test` tarball is always the candidate and `published_upgrade_survivor_baseline` selects the fallback published baseline, defaulting to `openclaw@latest`; failed-lane rerun commands preserve that baseline. Full Release Validation with `run_release_soak=true` or `release_profile=full` sets `published_upgrade_survivor_baselines='last-stable-4 2026.4.23 2026.5.2 2026.4.15'` and `published_upgrade_survivor_scenarios=reported-issues` to expand across the four latest stable npm releases plus pinned plugin-compatibility boundary releases and issue-shaped fixtures for Feishu config, preserved bootstrap/persona files, configured OpenClaw plugin installs, tilde log paths, and stale legacy plugin dependency roots. Multi-baseline published-upgrade survivor selections are sharded by baseline into separate targeted Docker runner jobs. The separate `Update Migration` workflow uses the `update-migration` Docker lane with `all-since-2026.4.23` and `plugin-deps-cleanup` when the question is exhaustive published update cleanup, not normal Full Release CI breadth. Local aggregate runs can pass exact package specs with `OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPECS`, keep a single lane with `OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC` such as `openclaw@2026.4.15`, or set `OPENCLAW_UPGRADE_SURVIVOR_SCENARIOS` for the scenario matrix. The published lane configures the baseline with a baked `openclaw config set` command recipe, records recipe steps in `summary.json`, and probes `/healthz`, `/readyz`, plus RPC status after Gateway start. The Windows packaged and installer fresh lanes also verify that an installed package can import a browser-control override from a raw absolute Windows path. The OpenAI cross-OS agent-turn smoke defaults to `OPENCLAW_CROSS_OS_OPENAI_MODEL` when set, otherwise `openai/gpt-5.4`, so the install and gateway proof stays on a GPT-5 test model while avoiding GPT-4.x defaults.
|
||||
Release checks call Package Acceptance with `source=artifact`, the prepared release package artifact, `suite_profile=custom`, `docker_lanes='doctor-switch update-channel-switch upgrade-survivor published-upgrade-survivor plugins-offline plugin-update'`, and `telegram_mode=mock-openai`. This keeps package migration, update, stale-plugin-dependency cleanup, configured-plugin install repair, offline plugin, plugin-update, and Telegram proof on the same resolved package tarball. Set `package_acceptance_package_spec` on Full Release Validation or OpenClaw Release Checks to run that same matrix against a shipped npm package instead of the SHA-built artifact. Cross-OS release checks still cover OS-specific onboarding, installer, and platform behavior; package/update product validation should start with Package Acceptance. The `published-upgrade-survivor` Docker lane validates one published package baseline per run in the blocking release path. In Package Acceptance, the resolved `package-under-test` tarball is always the candidate and `published_upgrade_survivor_baseline` selects the fallback published baseline, defaulting to `openclaw@latest`; failed-lane rerun commands preserve that baseline. Full Release Validation with `run_release_soak=true` or `release_profile=full` sets `published_upgrade_survivor_baselines='last-stable-4 2026.4.23 2026.5.2 2026.4.15'` and `published_upgrade_survivor_scenarios=reported-issues` to expand across the four latest stable npm releases plus pinned plugin-compatibility boundary releases and issue-shaped fixtures for Feishu config, preserved bootstrap/persona files, configured OpenClaw plugin installs, tilde log paths, and stale legacy plugin dependency roots. Multi-baseline published-upgrade survivor selections are sharded by baseline into separate targeted Docker runner jobs. The separate `Update Migration` workflow uses the `update-migration` Docker lane with `all-since-2026.4.23` and `plugin-deps-cleanup` when the question is exhaustive published update cleanup, not normal Full Release CI breadth. Local aggregate runs can pass exact package specs with `OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPECS`, keep a single lane with `OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC` such as `openclaw@2026.4.15`, or set `OPENCLAW_UPGRADE_SURVIVOR_SCENARIOS` for the scenario matrix. The published lane configures the baseline with a baked `openclaw config set` command recipe, records recipe steps in `summary.json`, and probes `/healthz`, `/readyz`, plus RPC status after Gateway start. The Windows packaged and installer fresh lanes also verify that an installed package can import a browser-control override from a raw absolute Windows path. The OpenAI cross-OS agent-turn smoke defaults to `OPENCLAW_CROSS_OS_OPENAI_MODEL` when set, otherwise `openai/gpt-5.4`, so the install and gateway proof stays on a GPT-5 test model while avoiding GPT-4.x defaults.
|
||||
|
||||
### Legacy compatibility windows
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -80,7 +80,7 @@ When you run `openclaw channels add` without flags, the interactive wizard can p
|
||||
|
||||
- account ids per selected channel
|
||||
- optional display names for those accounts
|
||||
- `Route these channel accounts to agents now?`
|
||||
- `Bind configured channel accounts to agents now?`
|
||||
|
||||
If you confirm bind now, the wizard asks which agent should own each configured channel account and writes account-scoped routing bindings.
|
||||
|
||||
|
||||
@@ -7,9 +7,7 @@ title: "Configure"
|
||||
|
||||
# `openclaw configure`
|
||||
|
||||
Interactive prompt for targeted changes to an existing setup: credentials, devices, agent defaults, gateway, channels, plugins, skills, and health checks.
|
||||
|
||||
Use `openclaw onboard` for the full guided first-run journey, `openclaw setup` for the baseline config/workspace only, and `openclaw channels add` when you only need channel account setup.
|
||||
Interactive prompt to set up credentials, devices, and agent defaults.
|
||||
|
||||
<Note>
|
||||
The **Model** section includes a multi-select for the `agents.defaults.models` allowlist (what shows up in `/model` and the model picker). Provider-scoped setup choices merge their selected models into the existing allowlist instead of replacing unrelated providers already in the config.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -70,14 +70,6 @@ Note: isolated cron runs treat run-level agent failures as job errors even when
|
||||
no reply payload is produced, so model/provider failures still increment error
|
||||
counters and trigger failure notifications.
|
||||
|
||||
If an isolated run times out before the first model request, `openclaw cron show`
|
||||
and `openclaw cron runs` include a phase-specific error such as
|
||||
`setup timed out before runner start` or
|
||||
`stalled before first model call (last phase: context-engine)`.
|
||||
For CLI-backed providers, the pre-model watchdog stays active until the external
|
||||
CLI turn starts, so session lookup, hook, auth, prompt, and CLI setup stalls are
|
||||
reported as pre-model cron failures.
|
||||
|
||||
## Scheduling
|
||||
|
||||
### One-shot jobs
|
||||
|
||||
@@ -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.
|
||||
@@ -67,7 +67,6 @@ Notes:
|
||||
- Doctor includes a memory-search readiness check and can recommend `openclaw configure --section model` when embedding credentials are missing.
|
||||
- Doctor warns when no command owner is configured. The command owner is the human operator account allowed to run owner-only commands and approve dangerous actions. DM pairing only lets someone talk to the bot; if you approved a sender before first-owner bootstrap existed, set `commands.ownerAllowFrom` explicitly.
|
||||
- Doctor warns when Codex-mode agents are configured and personal Codex CLI assets exist in the operator's Codex home. Local Codex app-server launches use isolated per-agent homes, so use `openclaw migrate codex --dry-run` to inventory assets that should be promoted deliberately.
|
||||
- Doctor removes retired `plugins.entries.codex.config.codexDynamicToolsProfile`; Codex app-server always keeps Codex-native workspace tools native.
|
||||
- Doctor warns when skills allowed for the default agent are unavailable in the current runtime environment because bins, env vars, config, or OS requirements are missing. `doctor --fix` can disable those unavailable skills with `skills.entries.<skill>.enabled=false`; install/configure the missing requirement instead when you want to keep the skill active.
|
||||
- If sandbox mode is enabled but Docker is unavailable, doctor reports a high-signal warning with remediation (`install Docker` or `openclaw config set agents.defaults.sandbox.mode off`).
|
||||
- If legacy sandbox registry files (`~/.openclaw/sandbox/containers.json` or `~/.openclaw/sandbox/browsers.json`) are present, doctor reports them; `openclaw doctor --fix` migrates valid entries into sharded registry directories and quarantines invalid legacy files.
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -11,13 +11,6 @@ dedicated reference page or is documented with the command it aliases; this
|
||||
index lists the commands, the global flags, and the output styling rules that
|
||||
apply across the CLI.
|
||||
|
||||
Use the setup commands by intent:
|
||||
|
||||
- `openclaw setup` creates the baseline config and workspace without walking the full guided onboarding flow.
|
||||
- `openclaw onboard` is the full guided first-run path for gateway, model auth, workspace, channels, skills, and health.
|
||||
- `openclaw configure` changes targeted parts of an existing setup, such as model auth, gateway, channels, plugins, or skills.
|
||||
- `openclaw channels add` configures channel accounts after the baseline exists; run it without flags for guided channel setup or with channel-specific flags for scripts.
|
||||
|
||||
## Command pages
|
||||
|
||||
| Area | Commands |
|
||||
@@ -35,7 +28,7 @@ Use the setup commands by intent:
|
||||
| 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
|
||||
|
||||
@@ -128,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
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ title: "Onboard"
|
||||
|
||||
# `openclaw onboard`
|
||||
|
||||
Full guided onboarding for local or remote Gateway setup. Use this when you want OpenClaw to walk through model auth, workspace, gateway, channels, skills, and health in one flow.
|
||||
Interactive onboarding for local or remote Gateway setup.
|
||||
|
||||
## Related guides
|
||||
|
||||
@@ -212,13 +212,10 @@ openclaw onboard --non-interactive \
|
||||
## Common follow-up commands
|
||||
|
||||
```bash
|
||||
openclaw channels add
|
||||
openclaw configure
|
||||
openclaw agents add <name>
|
||||
```
|
||||
|
||||
Use `openclaw setup` instead when you only need the baseline config/workspace. Use `openclaw configure` later for targeted changes and `openclaw channels add` for channel-only setup.
|
||||
|
||||
<Note>
|
||||
`--json` does not imply non-interactive mode. Use `--non-interactive` for scripts.
|
||||
</Note>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user