mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-22 23:13:42 +08:00
Compare commits
128 Commits
codex/llm-
...
codex/outb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c51af21281 | ||
|
|
92a1ba88bd | ||
|
|
53a34c39f6 | ||
|
|
3261a2a0b1 | ||
|
|
74b9ad010a | ||
|
|
a2a9a553e1 | ||
|
|
3abffe0967 | ||
|
|
afa95fade0 | ||
|
|
83d284610c | ||
|
|
a98ffa41d0 | ||
|
|
16567ba4e7 | ||
|
|
b8b1e2cf50 | ||
|
|
f6c57edd5c | ||
|
|
79e13e0a5e | ||
|
|
5b7b5529f1 | ||
|
|
126839380c | ||
|
|
74756b91b7 | ||
|
|
f7675eca6b | ||
|
|
59269f3534 | ||
|
|
25015161fe | ||
|
|
19126033dd | ||
|
|
b7ca56f662 | ||
|
|
83c5bc946d | ||
|
|
c86de678f3 | ||
|
|
58cf9b865f | ||
|
|
8404f56841 | ||
|
|
30a94dfd3b | ||
|
|
510f4276b5 | ||
|
|
7b61ca1b06 | ||
|
|
a837ebdd67 | ||
|
|
a290f5e50f | ||
|
|
ffc1d5459c | ||
|
|
6ae68faf5f | ||
|
|
0f0cecd2e8 | ||
|
|
7b151afeeb | ||
|
|
371b3d22f5 | ||
|
|
42b9212eb2 | ||
|
|
f8c70bf1f1 | ||
|
|
de86e25fd4 | ||
|
|
8884643f40 | ||
|
|
002cc07322 | ||
|
|
f19cb738af | ||
|
|
4cc0bb07c1 | ||
|
|
b736a92e19 | ||
|
|
c70837f07d | ||
|
|
62b7b350c9 | ||
|
|
9a9db87952 | ||
|
|
60a55c9cbe | ||
|
|
d7018aaf19 | ||
|
|
07d9f725b6 | ||
|
|
bea90b72e6 | ||
|
|
5f97645382 | ||
|
|
46f49eb6eb | ||
|
|
6e044ace28 | ||
|
|
b9c4db1a77 | ||
|
|
a996f60f11 | ||
|
|
757c2cc2de | ||
|
|
7d8d3d9d77 | ||
|
|
67da67b61a | ||
|
|
2661de384f | ||
|
|
859889aae9 | ||
|
|
91d37ccfc3 | ||
|
|
6ebcd853be | ||
|
|
b526098eb2 | ||
|
|
c749957c93 | ||
|
|
e5a1185796 | ||
|
|
be3f4a7966 | ||
|
|
198de10523 | ||
|
|
63e09f8267 | ||
|
|
2797ae1583 | ||
|
|
cc5bd57bd7 | ||
|
|
e9903c9133 | ||
|
|
e6911f0448 | ||
|
|
ef1346e503 | ||
|
|
ecfa79ee4c | ||
|
|
600f57c979 | ||
|
|
4b5487ee85 | ||
|
|
8f0727d75c | ||
|
|
1746e130f9 | ||
|
|
a0d3dc94d0 | ||
|
|
fa52d122c4 | ||
|
|
62edfdffbd | ||
|
|
152d179302 | ||
|
|
8240fd900a | ||
|
|
505d140aeb | ||
|
|
ea74123ab2 | ||
|
|
7d08070dd7 | ||
|
|
8d73bc77fa | ||
|
|
656679e6e0 | ||
|
|
b49946a67e | ||
|
|
ff326e90c3 | ||
|
|
467ec4d5f3 | ||
|
|
05b1cdec3c | ||
|
|
891e2a3da8 | ||
|
|
b4f16bad32 | ||
|
|
a02bfd30c5 | ||
|
|
f187e8bac4 | ||
|
|
e64cc1983f | ||
|
|
b3ca855283 | ||
|
|
27f655ed11 | ||
|
|
3e02635df3 | ||
|
|
382640e674 | ||
|
|
d8008a9a67 | ||
|
|
3d8afb96bd | ||
|
|
b64f4e313d | ||
|
|
823a09acbe | ||
|
|
10dc4d65d1 | ||
|
|
5fd482d6b0 | ||
|
|
73539ac787 | ||
|
|
947dac48f2 | ||
|
|
cfdc0fdbe1 | ||
|
|
22fc5a5442 | ||
|
|
49b248a333 | ||
|
|
ebb10c0852 | ||
|
|
6a381e80bc | ||
|
|
a0e7a2fcc1 | ||
|
|
f6928617b7 | ||
|
|
7943e83c6c | ||
|
|
c0c3c4824d | ||
|
|
e9b19ca1d1 | ||
|
|
861fcb1575 | ||
|
|
b5d2123156 | ||
|
|
0cddb5fb7c | ||
|
|
ea476de1e4 | ||
|
|
5d41fd4497 | ||
|
|
ca13256913 | ||
|
|
4a44ca8f79 | ||
|
|
c2402e48c9 |
87
.agents/skills/openclaw-ghsa-maintainer/SKILL.md
Normal file
87
.agents/skills/openclaw-ghsa-maintainer/SKILL.md
Normal file
@@ -0,0 +1,87 @@
|
||||
---
|
||||
name: openclaw-ghsa-maintainer
|
||||
description: Maintainer workflow for OpenClaw GitHub Security Advisories (GHSA). Use when Codex needs to inspect, patch, validate, or publish a repo advisory, verify private-fork state, prepare advisory Markdown or JSON payloads safely, handle GHSA API-specific publish constraints, or confirm advisory publish success.
|
||||
---
|
||||
|
||||
# OpenClaw GHSA Maintainer
|
||||
|
||||
Use this skill for repo security advisory workflow only. Keep general release work in `openclaw-release-maintainer`.
|
||||
|
||||
## Respect advisory guardrails
|
||||
|
||||
- Before reviewing or publishing a repo advisory, read `SECURITY.md`.
|
||||
- Ask permission before any publish action.
|
||||
- Treat this skill as GHSA-only. Do not use it for stable or beta release work.
|
||||
|
||||
## Fetch and inspect advisory state
|
||||
|
||||
Fetch the current advisory and the latest published npm version:
|
||||
|
||||
```bash
|
||||
gh api /repos/openclaw/openclaw/security-advisories/<GHSA>
|
||||
npm view openclaw version --userconfig "$(mktemp)"
|
||||
```
|
||||
|
||||
Use the fetch output to confirm the advisory state, linked private fork, and vulnerability payload shape before patching.
|
||||
|
||||
## Verify private fork PRs are closed
|
||||
|
||||
Before publishing, verify that the advisory's private fork has no open PRs:
|
||||
|
||||
```bash
|
||||
fork=$(gh api /repos/openclaw/openclaw/security-advisories/<GHSA> | jq -r .private_fork.full_name)
|
||||
gh pr list -R "$fork" --state open
|
||||
```
|
||||
|
||||
The PR list must be empty before publish.
|
||||
|
||||
## Prepare advisory Markdown and JSON safely
|
||||
|
||||
- Write advisory Markdown via heredoc to a temp file. Do not use escaped `\n` strings.
|
||||
- Build PATCH payload JSON with `jq`, not hand-escaped shell JSON.
|
||||
|
||||
Example pattern:
|
||||
|
||||
```bash
|
||||
cat > /tmp/ghsa.desc.md <<'EOF'
|
||||
<markdown description>
|
||||
EOF
|
||||
|
||||
jq -n --rawfile desc /tmp/ghsa.desc.md \
|
||||
'{summary,severity,description:$desc,vulnerabilities:[...]}' \
|
||||
> /tmp/ghsa.patch.json
|
||||
```
|
||||
|
||||
## Apply PATCH calls in the correct sequence
|
||||
|
||||
- Do not set `severity` and `cvss_vector_string` in the same PATCH call.
|
||||
- Use separate calls when the advisory requires both fields.
|
||||
- Publish by PATCHing the advisory and setting `"state":"published"`. There is no separate `/publish` endpoint.
|
||||
|
||||
Example shape:
|
||||
|
||||
```bash
|
||||
gh api -X PATCH /repos/openclaw/openclaw/security-advisories/<GHSA> \
|
||||
--input /tmp/ghsa.patch.json
|
||||
```
|
||||
|
||||
## Publish and verify success
|
||||
|
||||
After publish, re-fetch the advisory and confirm:
|
||||
|
||||
- `state=published`
|
||||
- `published_at` is set
|
||||
- the description does not contain literal escaped `\\n`
|
||||
|
||||
Verification pattern:
|
||||
|
||||
```bash
|
||||
gh api /repos/openclaw/openclaw/security-advisories/<GHSA>
|
||||
jq -r .description < /tmp/ghsa.refetch.json | rg '\\\\n'
|
||||
```
|
||||
|
||||
## Common GHSA footguns
|
||||
|
||||
- Publishing fails with HTTP 422 if required fields are missing or the private fork still has open PRs.
|
||||
- A payload that looks correct in shell can still be wrong if Markdown was assembled with escaped newline strings.
|
||||
- Advisory PATCH sequencing matters; separate field updates when GHSA API constraints require it.
|
||||
58
.agents/skills/openclaw-parallels-smoke/SKILL.md
Normal file
58
.agents/skills/openclaw-parallels-smoke/SKILL.md
Normal file
@@ -0,0 +1,58 @@
|
||||
---
|
||||
name: openclaw-parallels-smoke
|
||||
description: End-to-end Parallels smoke, upgrade, and rerun workflow for OpenClaw across macOS, Windows, and Linux guests. Use when Codex needs to run, rerun, debug, or interpret VM-based install, onboarding, gateway smoke tests, latest-release-to-main upgrade checks, fresh snapshot retests, or optional Discord roundtrip verification under Parallels.
|
||||
---
|
||||
|
||||
# OpenClaw Parallels Smoke
|
||||
|
||||
Use this skill for Parallels guest workflows and smoke interpretation. Do not load it for normal repo work.
|
||||
|
||||
## Global rules
|
||||
|
||||
- Use the snapshot most closely matching the requested fresh baseline.
|
||||
- Gateway verification in smoke runs should use `openclaw gateway status --deep --require-rpc` unless the stable version being checked does not support it yet.
|
||||
- Stable `2026.3.12` pre-upgrade diagnostics may require a plain `gateway status --deep` fallback.
|
||||
- Treat `precheck=latest-ref-fail` on that stable pre-upgrade lane as baseline, not automatically a regression.
|
||||
- Pass `--json` for machine-readable summaries.
|
||||
- Per-phase logs land under `/tmp/openclaw-parallels-*`.
|
||||
- Do not run local and gateway agent turns in parallel on the same fresh workspace or session.
|
||||
|
||||
## macOS flow
|
||||
|
||||
- Preferred entrypoint: `pnpm test:parallels:macos`
|
||||
- Target the snapshot closest to `macOS 26.3.1 fresh`.
|
||||
- `prlctl exec` is fine for deterministic repo commands, but use the guest Terminal or `prlctl enter` when installer parity or shell-sensitive behavior matters.
|
||||
- On the fresh Tahoe snapshot, `brew` exists but `node` may be missing from PATH in noninteractive exec. Use `/opt/homebrew/bin/node` when needed.
|
||||
- Fresh host-served tgz installs should install as guest root with `HOME=/var/root`, then run onboarding as the desktop user via `prlctl exec --current-user`.
|
||||
- Root-installed tgz smoke can log plugin blocks for world-writable `extensions/*`; do not treat that as an onboarding or gateway failure unless plugin loading is the task.
|
||||
|
||||
## Windows flow
|
||||
|
||||
- Preferred entrypoint: `pnpm test:parallels:windows`
|
||||
- Use the snapshot closest to `pre-openclaw-native-e2e-2026-03-12`.
|
||||
- Always use `prlctl exec --current-user`; plain `prlctl exec` lands in `NT AUTHORITY\\SYSTEM`.
|
||||
- Prefer explicit `npm.cmd` and `openclaw.cmd`.
|
||||
- Use PowerShell only as the transport with `-ExecutionPolicy Bypass`, then call the `.cmd` shims from inside it.
|
||||
- Keep onboarding and status output ASCII-clean in logs; fancy punctuation becomes mojibake in current capture paths.
|
||||
|
||||
## Linux flow
|
||||
|
||||
- Preferred entrypoint: `pnpm test:parallels:linux`
|
||||
- Use the snapshot closest to fresh `Ubuntu 24.04.3 ARM64`.
|
||||
- Use plain `prlctl exec`; `--current-user` is not the right transport on this snapshot.
|
||||
- Fresh snapshots may be missing `curl`, and `apt-get update` can fail on clock skew. Bootstrap with `apt-get -o Acquire::Check-Date=false update` and install `curl ca-certificates`.
|
||||
- Fresh `main` tgz smoke still needs the latest-release installer first because the snapshot has no Node or npm before bootstrap.
|
||||
- This snapshot does not have a usable `systemd --user` session; managed daemon install is unsupported.
|
||||
- `prlctl exec` reaps detached Linux child processes on this snapshot, so detached background gateway runs are not trustworthy smoke signals.
|
||||
|
||||
## Discord roundtrip
|
||||
|
||||
- Discord roundtrip is optional and should be enabled with:
|
||||
- `--discord-token-env`
|
||||
- `--discord-guild-id`
|
||||
- `--discord-channel-id`
|
||||
- Keep the Discord token only in a host env var.
|
||||
- Use installed `openclaw message send/read`, not `node openclaw.mjs message ...`.
|
||||
- Set `channels.discord.guilds` as one JSON object, not dotted config paths with snowflakes.
|
||||
- Avoid long `prlctl enter` or expect-driven Discord config scripts; prefer `prlctl exec --current-user /bin/sh -lc ...` with short commands.
|
||||
- For a narrower macOS-only Discord proof run, the existing `parallels-discord-roundtrip` skill is the deep-dive companion.
|
||||
75
.agents/skills/openclaw-pr-maintainer/SKILL.md
Normal file
75
.agents/skills/openclaw-pr-maintainer/SKILL.md
Normal file
@@ -0,0 +1,75 @@
|
||||
---
|
||||
name: openclaw-pr-maintainer
|
||||
description: Maintainer workflow for reviewing, triaging, preparing, closing, or landing OpenClaw pull requests and related issues. Use when Codex needs to validate bug-fix claims, search for related issues or PRs, apply or recommend close/reason labels, prepare GitHub comments safely, check review-thread follow-up, or perform maintainer-style PR decision making before merge or closure.
|
||||
---
|
||||
|
||||
# OpenClaw PR Maintainer
|
||||
|
||||
Use this skill for maintainer-facing GitHub workflow, not for ordinary code changes.
|
||||
|
||||
## Apply close and triage labels correctly
|
||||
|
||||
- If an issue or PR matches an auto-close reason, apply the label and let `.github/workflows/auto-response.yml` handle the comment/close/lock flow.
|
||||
- Do not manually close plus manually comment for these reasons.
|
||||
- `r:*` labels can be used on both issues and PRs.
|
||||
- Current reasons:
|
||||
- `r: skill`
|
||||
- `r: support`
|
||||
- `r: no-ci-pr`
|
||||
- `r: too-many-prs`
|
||||
- `r: testflight`
|
||||
- `r: third-party-extension`
|
||||
- `r: moltbook`
|
||||
- `r: spam`
|
||||
- `invalid`
|
||||
- `dirty` for PRs only
|
||||
|
||||
## Enforce the bug-fix evidence bar
|
||||
|
||||
- Never merge a bug-fix PR based only on issue text, PR text, or AI rationale.
|
||||
- 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
|
||||
3. a fix that touches the implicated code path
|
||||
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.
|
||||
|
||||
## Handle GitHub text safely
|
||||
|
||||
- For issue comments and PR comments, use literal multiline strings or `-F - <<'EOF'` for real newlines. Never embed `\n`.
|
||||
- Do not use `gh issue/pr comment -b "..."` when the body contains backticks or shell characters. Prefer a single-quoted heredoc.
|
||||
- Do not wrap issue or PR refs like `#24643` in backticks when you want auto-linking.
|
||||
- PR landing comments should include clickable full commit links for landed and source SHAs when present.
|
||||
|
||||
## Search broadly before deciding
|
||||
|
||||
- Prefer targeted keyword search before proposing new work or closing something as duplicate.
|
||||
- Use `--repo openclaw/openclaw` with `--match title,body` first.
|
||||
- Add `--match comments` when triaging follow-up discussion.
|
||||
- Do not stop at the first 500 results when the task requires a full search.
|
||||
|
||||
Examples:
|
||||
|
||||
```bash
|
||||
gh search prs --repo openclaw/openclaw --match title,body --limit 50 -- "auto-update"
|
||||
gh search issues --repo openclaw/openclaw --match title,body --limit 50 -- "auto-update"
|
||||
gh search issues --repo openclaw/openclaw --match title,body --limit 50 \
|
||||
--json number,title,state,url,updatedAt -- "auto update" \
|
||||
--jq '.[] | "\(.number) | \(.state) | \(.title) | \(.url)"'
|
||||
```
|
||||
|
||||
## Follow PR review and landing hygiene
|
||||
|
||||
- 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.
|
||||
- Use `scripts/committer "<msg>" <file...>` for scoped commits instead of manual `git add` and `git commit`.
|
||||
- Keep commit messages concise and action-oriented.
|
||||
- Group related changes; avoid bundling unrelated refactors.
|
||||
- Use `.github/pull_request_template.md` for PR submissions and `.github/ISSUE_TEMPLATE/` for issues.
|
||||
|
||||
## Extra safety
|
||||
|
||||
- If a close or reopen action would affect more than 5 PRs, ask for explicit confirmation with the exact count and target query first.
|
||||
- `sync` means: if the tree is dirty, commit all changes with a sensible Conventional Commit message, then `git pull --rebase`, then `git push`. Stop if rebase conflicts cannot be resolved safely.
|
||||
74
.agents/skills/openclaw-release-maintainer/SKILL.md
Normal file
74
.agents/skills/openclaw-release-maintainer/SKILL.md
Normal file
@@ -0,0 +1,74 @@
|
||||
---
|
||||
name: openclaw-release-maintainer
|
||||
description: Maintainer workflow for OpenClaw releases, prereleases, changelog release notes, and publish validation. Use when Codex needs to prepare or verify stable or beta release steps, align version naming, assemble release notes, check release auth requirements, or validate publish-time commands and artifacts.
|
||||
---
|
||||
|
||||
# OpenClaw Release Maintainer
|
||||
|
||||
Use this skill for release and publish-time workflow. Keep ordinary development changes and GHSA-specific advisory work outside this skill.
|
||||
|
||||
## Respect release guardrails
|
||||
|
||||
- Do not change version numbers without explicit operator approval.
|
||||
- Ask permission before any npm publish or release step.
|
||||
- Use the private maintainer release docs for the actual runbook and `docs/reference/RELEASING.md` for public policy.
|
||||
|
||||
## Keep release channel naming aligned
|
||||
|
||||
- `stable`: tagged releases only, with npm dist-tag `latest`
|
||||
- `beta`: prerelease tags like `vYYYY.M.D-beta.N`, with npm dist-tag `beta`
|
||||
- Prefer `-beta.N`; do not mint new `-1` or `-2` beta suffixes
|
||||
- `dev`: moving head on `main`
|
||||
- When using a beta Git tag, publish npm with the matching beta version suffix so the plain version is not consumed or blocked
|
||||
|
||||
## Handle versions and release files consistently
|
||||
|
||||
- Version locations include:
|
||||
- `package.json`
|
||||
- `apps/android/app/build.gradle.kts`
|
||||
- `apps/ios/Sources/Info.plist`
|
||||
- `apps/ios/Tests/Info.plist`
|
||||
- `apps/macos/Sources/OpenClaw/Resources/Info.plist`
|
||||
- `docs/install/updating.md`
|
||||
- Peekaboo Xcode project and plist version fields
|
||||
- “Bump version everywhere” means all version locations above except `appcast.xml`.
|
||||
- Release signing and notary credentials live outside the repo in the private maintainer docs.
|
||||
|
||||
## Build changelog-backed release notes
|
||||
|
||||
- Changelog entries should be user-facing, not internal release-process notes.
|
||||
- When cutting a mac release with a beta GitHub prerelease:
|
||||
- tag `vYYYY.M.D-beta.N` from the release commit
|
||||
- create a prerelease titled `openclaw YYYY.M.D-beta.N`
|
||||
- use release notes from the matching `CHANGELOG.md` version section
|
||||
- attach at least the zip and dSYM zip, plus dmg if available
|
||||
- Keep the top version entries in `CHANGELOG.md` sorted by impact:
|
||||
- `### Changes` first
|
||||
- `### Fixes` deduped with user-facing fixes first
|
||||
|
||||
## Run publish-time validation
|
||||
|
||||
Before tagging or publishing, run:
|
||||
|
||||
```bash
|
||||
node --import tsx scripts/release-check.ts
|
||||
pnpm release:check
|
||||
pnpm test:install:smoke
|
||||
```
|
||||
|
||||
For a non-root smoke path:
|
||||
|
||||
```bash
|
||||
OPENCLAW_INSTALL_SMOKE_SKIP_NONROOT=1 pnpm test:install:smoke
|
||||
```
|
||||
|
||||
## Use the right auth flow
|
||||
|
||||
- Core `openclaw` publish uses GitHub trusted publishing.
|
||||
- Do not use `NPM_TOKEN` or the plugin OTP flow for core releases.
|
||||
- `@openclaw/*` plugin publishes use a separate maintainer-only flow.
|
||||
- Only publish plugins that already exist on npm; bundled disk-tree-only plugins stay unpublished.
|
||||
|
||||
## GHSA advisory work
|
||||
|
||||
- Use `openclaw-ghsa-maintainer` for GHSA advisory inspection, patch/publish flow, private-fork validation, and GHSA API-specific publish checks.
|
||||
46
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
46
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -7,7 +7,8 @@ body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for filing this report. Keep it concise, reproducible, and evidence-based.
|
||||
Thanks for filing this report. Keep every answer concise, reproducible, and grounded in observed evidence.
|
||||
Do not speculate or infer beyond the evidence. If a narrative section cannot be answered from the available evidence, respond with exactly `NOT_ENOUGH_INFO`.
|
||||
- type: dropdown
|
||||
id: bug_type
|
||||
attributes:
|
||||
@@ -23,35 +24,35 @@ body:
|
||||
id: summary
|
||||
attributes:
|
||||
label: Summary
|
||||
description: One-sentence statement of what is broken.
|
||||
placeholder: After upgrading to <version>, <channel> behavior regressed from <prior version>.
|
||||
description: One-sentence statement of what is broken, based only on observed evidence. If the evidence is insufficient, respond with exactly `NOT_ENOUGH_INFO`.
|
||||
placeholder: After upgrading from 2026.2.10 to 2026.2.17, Telegram thread replies stopped posting; reproduced twice and confirmed by gateway logs.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: repro
|
||||
attributes:
|
||||
label: Steps to reproduce
|
||||
description: Provide the shortest deterministic repro path.
|
||||
description: Provide the shortest deterministic repro path supported by direct observation. If the repro path cannot be grounded from the evidence, respond with exactly `NOT_ENOUGH_INFO`.
|
||||
placeholder: |
|
||||
1. Configure channel X.
|
||||
2. Send message Y.
|
||||
3. Run command Z.
|
||||
1. Start OpenClaw 2026.2.17 with the attached config.
|
||||
2. Send a Telegram thread reply in the affected chat.
|
||||
3. Observe no reply and confirm the attached `reply target not found` log line.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: expected
|
||||
attributes:
|
||||
label: Expected behavior
|
||||
description: What should happen if the bug does not exist.
|
||||
placeholder: Agent posts a reply in the same thread.
|
||||
description: State the expected result using a concrete reference such as prior observed behavior, attached docs, or a known-good version. If no grounded reference exists, respond with exactly `NOT_ENOUGH_INFO`.
|
||||
placeholder: In 2026.2.10, the agent posted replies in the same Telegram thread under the same workflow.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: actual
|
||||
attributes:
|
||||
label: Actual behavior
|
||||
description: What happened instead, including user-visible errors.
|
||||
placeholder: No reply is posted; gateway logs "reply target not found".
|
||||
description: Describe only the observed result, including user-visible errors and cited evidence. If the observed result cannot be grounded from the evidence, respond with exactly `NOT_ENOUGH_INFO`.
|
||||
placeholder: No reply is posted in the thread; the attached gateway log shows `reply target not found` at 14:23:08 UTC.
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
@@ -92,12 +93,6 @@ body:
|
||||
placeholder: openclaw -> cloudflare-ai-gateway -> minimax
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: config_location
|
||||
attributes:
|
||||
label: Config file / key location
|
||||
description: Optional. Relevant config source or key path if this bug depends on overrides or custom provider setup. Redact secrets.
|
||||
placeholder: ~/.openclaw/openclaw.json ; models.providers.cloudflare-ai-gateway.baseUrl ; ~/.openclaw/agents/<agentId>/agent/models.json
|
||||
- type: textarea
|
||||
id: provider_setup_details
|
||||
attributes:
|
||||
@@ -111,27 +106,28 @@ body:
|
||||
id: logs
|
||||
attributes:
|
||||
label: Logs, screenshots, and evidence
|
||||
description: Include redacted logs/screenshots/recordings that prove the behavior.
|
||||
description: Include the redacted logs, screenshots, recordings, docs, or version comparisons that support the grounded answers above.
|
||||
render: shell
|
||||
- type: textarea
|
||||
id: impact
|
||||
attributes:
|
||||
label: Impact and severity
|
||||
description: |
|
||||
Explain who is affected, how severe it is, how often it happens, and the practical consequence.
|
||||
Explain who is affected, how severe it is, how often it happens, and the practical consequence using only observed evidence.
|
||||
If any part cannot be grounded from the evidence, respond with exactly `NOT_ENOUGH_INFO`.
|
||||
Include:
|
||||
- Affected users/systems/channels
|
||||
- Severity (annoying, blocks workflow, data risk, etc.)
|
||||
- Frequency (always/intermittent/edge case)
|
||||
- Consequence (missed messages, failed onboarding, extra cost, etc.)
|
||||
placeholder: |
|
||||
Affected: Telegram group users on <version>
|
||||
Severity: High (blocks replies)
|
||||
Frequency: 100% repro
|
||||
Consequence: Agents cannot respond in threads
|
||||
Affected: Telegram group users on 2026.2.17
|
||||
Severity: High (blocks thread replies)
|
||||
Frequency: 4/4 observed attempts
|
||||
Consequence: Agents do not respond in the affected threads
|
||||
- type: textarea
|
||||
id: additional_information
|
||||
attributes:
|
||||
label: Additional information
|
||||
description: Add any context that helps triage but does not fit above. If this is a regression, include the last known good and first known bad versions.
|
||||
placeholder: Last known good version <...>, first known bad version <...>, temporary workaround is ...
|
||||
description: Add any remaining grounded context that helps triage but does not fit above. If this is a regression, include the last known good and first known bad versions when observed. If there is not enough evidence, respond with exactly `NOT_ENOUGH_INFO`.
|
||||
placeholder: Last known good version 2026.2.10, first known bad version 2026.2.17, temporary workaround is sending a top-level message instead of a thread reply.
|
||||
|
||||
45
.github/workflows/install-smoke.yml
vendored
45
.github/workflows/install-smoke.yml
vendored
@@ -62,24 +62,57 @@ jobs:
|
||||
run: |
|
||||
docker run --rm --entrypoint sh openclaw-dockerfile-smoke:local -lc 'which openclaw && openclaw --version'
|
||||
|
||||
# This smoke only validates that the build-arg path preinstalls selected
|
||||
# extension deps without breaking image build or basic CLI startup. It
|
||||
# does not exercise runtime loading/registration of diagnostics-otel.
|
||||
# This smoke validates that the build-arg path preinstalls selected
|
||||
# extension deps and that matrix plugin discovery stays healthy in the
|
||||
# final runtime image.
|
||||
- name: Build extension Dockerfile smoke image
|
||||
uses: useblacksmith/build-push-action@v2
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
build-args: |
|
||||
OPENCLAW_EXTENSIONS=diagnostics-otel
|
||||
OPENCLAW_EXTENSIONS=matrix
|
||||
tags: openclaw-ext-smoke:local
|
||||
load: true
|
||||
push: false
|
||||
provenance: false
|
||||
|
||||
- name: Smoke test Dockerfile with extension build arg
|
||||
- name: Smoke test Dockerfile with matrix extension build arg
|
||||
run: |
|
||||
docker run --rm --entrypoint sh openclaw-ext-smoke:local -lc 'which openclaw && openclaw --version'
|
||||
docker run --rm --entrypoint sh openclaw-ext-smoke:local -lc '
|
||||
which openclaw &&
|
||||
openclaw --version &&
|
||||
node -e "
|
||||
const Module = require(\"node:module\");
|
||||
const requireFromMatrix = Module.createRequire(\"/app/extensions/matrix/package.json\");
|
||||
requireFromMatrix.resolve(\"@vector-im/matrix-bot-sdk/package.json\");
|
||||
requireFromMatrix.resolve(\"@matrix-org/matrix-sdk-crypto-nodejs/package.json\");
|
||||
const { spawnSync } = require(\"node:child_process\");
|
||||
const run = spawnSync(\"openclaw\", [\"plugins\", \"list\", \"--json\"], { encoding: \"utf8\" });
|
||||
if (run.status !== 0) {
|
||||
process.stderr.write(run.stderr || run.stdout || \"plugins list failed\\n\");
|
||||
process.exit(run.status ?? 1);
|
||||
}
|
||||
const parsed = JSON.parse(run.stdout);
|
||||
const matrix = (parsed.plugins || []).find((entry) => entry.id === \"matrix\");
|
||||
if (!matrix) {
|
||||
throw new Error(\"matrix plugin missing from bundled plugin list\");
|
||||
}
|
||||
const matrixDiag = (parsed.diagnostics || []).filter(
|
||||
(diag) =>
|
||||
typeof diag.source === \"string\" &&
|
||||
diag.source.includes(\"/extensions/matrix\") &&
|
||||
typeof diag.message === \"string\" &&
|
||||
diag.message.includes(\"extension entry escapes package directory\"),
|
||||
);
|
||||
if (matrixDiag.length > 0) {
|
||||
throw new Error(
|
||||
\"unexpected matrix diagnostics: \" +
|
||||
matrixDiag.map((diag) => diag.message).join(\"; \"),
|
||||
);
|
||||
}
|
||||
"
|
||||
'
|
||||
|
||||
- name: Build installer smoke image
|
||||
uses: useblacksmith/build-push-action@v2
|
||||
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -100,8 +100,6 @@ USER.md
|
||||
/local/
|
||||
package-lock.json
|
||||
.claude/
|
||||
.agents/
|
||||
.agents
|
||||
.agent/
|
||||
skills-lock.json
|
||||
|
||||
@@ -135,3 +133,6 @@ ui/src/ui/__screenshots__
|
||||
ui/src/ui/views/__screenshots__
|
||||
ui/.vitest-attachments
|
||||
docs/superpowers
|
||||
|
||||
# Deprecated changelog fragment workflow
|
||||
changelog/fragments/
|
||||
|
||||
3
.npmrc
3
.npmrc
@@ -1 +1,4 @@
|
||||
# pnpm build-script allowlist lives in package.json -> pnpm.onlyBuiltDependencies.
|
||||
# TS 7 native-preview fails to resolve packages reliably from pnpm's isolated linker.
|
||||
# Keep the workspace on a hoisted layout so pnpm check/build stay stable.
|
||||
node-linker=hoisted
|
||||
|
||||
190
AGENTS.md
190
AGENTS.md
@@ -2,45 +2,8 @@
|
||||
|
||||
- Repo: https://github.com/openclaw/openclaw
|
||||
- In chat replies, file references must be repo-root relative only (example: `extensions/bluebubbles/src/channel.ts:80`); never absolute paths or `~/...`.
|
||||
- GitHub issues/comments/PR comments: use literal multiline strings or `-F - <<'EOF'` (or $'...') for real newlines; never embed "\\n".
|
||||
- GitHub comment footgun: never use `gh issue/pr comment -b "..."` when body contains backticks or shell chars. Always use single-quoted heredoc (`-F - <<'EOF'`) so no command substitution/escaping corruption.
|
||||
- GitHub linking footgun: don’t wrap issue/PR refs like `#24643` in backticks when you want auto-linking. Use plain `#24643` (optionally add full URL).
|
||||
- PR landing comments: always make commit SHAs clickable with full commit links (both landed SHA + source SHA when present).
|
||||
- PR review conversations: if a bot leaves review conversations on your PR, address them and resolve those conversations yourself once fixed. Leave a conversation unresolved only when reviewer or maintainer judgment is still needed; do not leave bot-conversation cleanup to maintainers.
|
||||
- GitHub searching footgun: don't limit yourself to the first 500 issues or PRs when wanting to search all. Unless you're supposed to look at the most recent, keep going until you've reached the last page in the search
|
||||
- Security advisory analysis: before triage/severity decisions, read `SECURITY.md` to align with OpenClaw's trust model and design boundaries.
|
||||
- Do not edit files covered by security-focused `CODEOWNERS` rules unless a listed owner explicitly asked for the change or is already reviewing it with you. Treat those paths as restricted surfaces, not drive-by cleanup.
|
||||
|
||||
## Auto-close labels (issues and PRs)
|
||||
|
||||
- If an issue/PR matches one of the reasons below, apply the label and let `.github/workflows/auto-response.yml` handle comment/close/lock.
|
||||
- Do not manually close + manually comment for these reasons.
|
||||
- Why: keeps wording consistent, preserves automation behavior (`state_reason`, locking), and keeps triage/reporting searchable by label.
|
||||
- `r:*` labels can be used on both issues and PRs.
|
||||
|
||||
- `r: skill`: close with guidance to publish skills on Clawhub.
|
||||
- `r: support`: close with redirect to Discord support + stuck FAQ.
|
||||
- `r: no-ci-pr`: close test-fix-only PRs for failing `main` CI and post the standard explanation.
|
||||
- `r: too-many-prs`: close when author exceeds active PR limit.
|
||||
- `r: testflight`: close requests asking for TestFlight access/builds. OpenClaw does not provide TestFlight distribution yet, so use the standard response (“Not available, build from source.”) instead of ad-hoc replies.
|
||||
- `r: third-party-extension`: close with guidance to ship as third-party plugin.
|
||||
- `r: moltbook`: close + lock as off-topic (not affiliated).
|
||||
- `r: spam`: close + lock as spam (`lock_reason: spam`).
|
||||
- `invalid`: close invalid items (issues are closed as `not_planned`; PRs are closed).
|
||||
- `dirty`: close PRs with too many unrelated/unexpected changes (PR-only label).
|
||||
|
||||
## PR truthfulness and bug-fix validation
|
||||
|
||||
- Never merge a bug-fix PR based only on issue text, PR text, or AI rationale.
|
||||
- Before `/landpr`, run `/reviewpr` and require explicit evidence for bug-fix claims.
|
||||
- Minimum merge gate for bug-fix PRs:
|
||||
1. symptom evidence (repro/log/failing test),
|
||||
2. verified root cause in code with file/line,
|
||||
3. fix touches the implicated code path,
|
||||
4. regression test (fail before/pass after) when feasible; if not feasible, include manual verification proof and why no test was added.
|
||||
- If claim is unsubstantiated or likely hallucinated/BS: do not merge. Request evidence/changes, or close with `invalid` when appropriate.
|
||||
- If linked issue appears wrong/outdated, correct triage first; do not merge speculative fixes.
|
||||
|
||||
## Project Structure & Module Organization
|
||||
|
||||
- Source code: `src/` (CLI wiring in `src/cli`, commands in `src/commands`, web provider in `src/provider-web.ts`, infra in `src/infra`, media pipeline in `src/media`).
|
||||
@@ -48,6 +11,7 @@
|
||||
- Docs: `docs/` (images, queue, Pi config). Built output lives in `dist/`.
|
||||
- Plugins/extensions: live under `extensions/*` (workspace packages). Keep plugin-only deps in the extension `package.json`; do not add them to the root `package.json` unless core uses them.
|
||||
- Plugins: install runs `npm install --omit=dev` in plugin dir; runtime deps must live in `dependencies`. Avoid `workspace:*` in `dependencies` (npm install breaks); put `openclaw` in `devDependencies` or `peerDependencies` instead (runtime resolves `openclaw/plugin-sdk` via jiti alias).
|
||||
- Import boundaries: extension production code should treat `openclaw/plugin-sdk/*` plus local `api.ts` / `runtime-api.ts` barrels as the public surface. Do not import core `src/**`, `src/plugin-sdk-internal/**`, or another extension's `src/**` directly.
|
||||
- Installers served from `https://openclaw.ai/*`: live in the sibling repo `../openclaw.ai` (`public/install.sh`, `public/install-cli.sh`, `public/install.ps1`).
|
||||
- Messaging channels: always consider **all** built-in + extension channels when refactoring shared logic (routing, allowlists, pairing, command gating, onboarding, docs).
|
||||
- Core channel docs: `docs/channels/`
|
||||
@@ -106,6 +70,10 @@
|
||||
- Format check: `pnpm format` (oxfmt --check)
|
||||
- Format fix: `pnpm format:fix` (oxfmt --write)
|
||||
- Tests: `pnpm test` (vitest); coverage: `pnpm test:coverage`
|
||||
- Hard gate: before any commit, `pnpm check` MUST be run and MUST pass for the change being committed.
|
||||
- Hard gate: before any push to `main`, `pnpm check` MUST be run and MUST pass, and `pnpm test` MUST be run and MUST pass.
|
||||
- Hard gate: if the change can affect build output, packaging, lazy-loading/module boundaries, or published surfaces, `pnpm build` MUST be run and MUST pass before pushing `main`.
|
||||
- Hard gate: do not commit or push with failing format, lint, type, build, or required test checks.
|
||||
|
||||
## Coding Style & Naming Conventions
|
||||
|
||||
@@ -115,6 +83,8 @@
|
||||
- Dynamic import guardrail: do not mix `await import("x")` and static `import ... from "x"` for the same module in production code paths. If you need lazy loading, create a dedicated `*.runtime.ts` boundary (that re-exports from `x`) and dynamically import that boundary from lazy callers only.
|
||||
- Dynamic import verification: after refactors that touch lazy-loading/module boundaries, run `pnpm build` and check for `[INEFFECTIVE_DYNAMIC_IMPORT]` warnings before submitting.
|
||||
- Extension SDK self-import guardrail: inside an extension package, do not import that same extension via `openclaw/plugin-sdk/<extension>` from production files. Route internal imports through a local barrel such as `./api.ts` or `./runtime-api.ts`, and keep the `plugin-sdk/<extension>` path as the external contract only.
|
||||
- Extension package boundary guardrail: inside `extensions/<id>/**`, do not use relative imports/exports that resolve outside that same `extensions/<id>` package root. If shared code belongs in the plugin SDK, import `openclaw/plugin-sdk/<subpath>` instead of reaching into `src/plugin-sdk/**` or other repo paths via `../`.
|
||||
- Extension API surface rule: `openclaw/plugin-sdk/<subpath>` is the only public cross-package contract for extension-facing SDK code. If an extension needs a new seam, add a public subpath first; do not reach into `src/plugin-sdk/**` by relative path.
|
||||
- Never share class behavior via prototype mutation (`applyPrototypeMixins`, `Object.defineProperty` on `.prototype`, or exporting `Class.prototype` for merges). Use explicit inheritance/composition (`A extends B extends C`) or helper composition so TypeScript can typecheck.
|
||||
- If this pattern is needed, stop and get explicit approval before shipping; default behavior is to split/refactor into an explicit class hierarchy and keep members strongly typed.
|
||||
- In tests, prefer per-instance stubs over prototype mutation (`SomeClass.prototype.method = ...`) unless a test explicitly documents why prototype-level patching is required.
|
||||
@@ -124,18 +94,18 @@
|
||||
- Naming: use **OpenClaw** for product/app/docs headings; use `openclaw` for CLI command, package/binary, paths, and config keys.
|
||||
- Written English: use American spelling and grammar in code, comments, docs, and UI strings (e.g. "color" not "colour", "behavior" not "behaviour", "analyze" not "analyse").
|
||||
|
||||
## Release Channels (Naming)
|
||||
## Release / Advisory Workflows
|
||||
|
||||
- stable: tagged releases only (e.g. `vYYYY.M.D`), npm dist-tag `latest`.
|
||||
- beta: prerelease tags `vYYYY.M.D-beta.N`, npm dist-tag `beta` (may ship without macOS app).
|
||||
- beta naming: prefer `-beta.N`; do not mint new `-1/-2` betas. Legacy `vYYYY.M.D-<patch>` and `vYYYY.M.D.beta.N` remain recognized.
|
||||
- dev: moving head on `main` (no tag; git checkout main).
|
||||
- Use `$openclaw-release-maintainer` at `.agents/skills/openclaw-release-maintainer/SKILL.md` for release naming, version coordination, release auth, and changelog-backed release-note workflows.
|
||||
- Use `$openclaw-ghsa-maintainer` at `.agents/skills/openclaw-ghsa-maintainer/SKILL.md` for GHSA advisory inspection, patch/publish flow, private-fork checks, and GHSA API validation.
|
||||
- Release and publish remain explicit-approval actions even when using the skill.
|
||||
|
||||
## Testing Guidelines
|
||||
|
||||
- Framework: Vitest with V8 coverage thresholds (70% lines/branches/functions/statements).
|
||||
- Naming: match source names with `*.test.ts`; e2e in `*.e2e.test.ts`.
|
||||
- Run `pnpm test` (or `pnpm test:coverage`) before pushing when you touch logic.
|
||||
- Agents MUST NOT modify baseline, inventory, ignore, snapshot, or expected-failure files to silence failing checks without explicit approval in this chat.
|
||||
- For targeted/local debugging, keep using the wrapper: `pnpm test -- <path-or-filter> [vitest args...]` (for example `pnpm test -- src/commands/onboard-search.test.ts -t "shows registered plugin providers"`); do not default to raw `pnpm vitest run ...` because it bypasses wrapper config/profile/pool routing.
|
||||
- Do not set test workers above 16; tried already.
|
||||
- If local Vitest runs cause memory pressure (common on non-Mac-Studio hosts), use `OPENCLAW_TEST_PROFILE=low OPENCLAW_TEST_SERIAL_GATEWAY=1 pnpm test` for land/gate runs.
|
||||
@@ -149,7 +119,9 @@
|
||||
|
||||
## Commit & Pull Request Guidelines
|
||||
|
||||
**Full maintainer PR workflow (optional):** If you want the repo's end-to-end maintainer workflow (triage order, quality bar, rebase rules, commit/changelog conventions, co-contributor policy, and the `review-pr` > `prepare-pr` > `merge-pr` pipeline), see `.agents/skills/PR_WORKFLOW.md`. Maintainers may use other workflows; when a maintainer specifies a workflow, follow that. If no workflow is specified, default to PR_WORKFLOW.
|
||||
- Use `$openclaw-pr-maintainer` at `.agents/skills/openclaw-pr-maintainer/SKILL.md` for maintainer PR triage, review, close, search, and landing workflows.
|
||||
- This includes auto-close labels, bug-fix evidence gates, GitHub comment/search footguns, and maintainer PR decision flow.
|
||||
- For the repo's end-to-end maintainer PR workflow, use `$openclaw-pr-maintainer` at `.agents/skills/openclaw-pr-maintainer/SKILL.md`.
|
||||
|
||||
- `/landpr` lives in the global Codex prompts (`~/.codex/prompts/landpr.md`); when landing or merging any PR, always follow that `/landpr` process.
|
||||
- Create commits with `scripts/committer "<msg>" <file...>`; avoid manual `git add`/`git commit` so staging stays scoped.
|
||||
@@ -158,105 +130,30 @@
|
||||
- PR submission template (canonical): `.github/pull_request_template.md`
|
||||
- Issue submission templates (canonical): `.github/ISSUE_TEMPLATE/`
|
||||
|
||||
## Shorthand Commands
|
||||
|
||||
- `sync`: if working tree is dirty, commit all changes (pick a sensible Conventional Commit message), then `git pull --rebase`; if rebase conflicts and cannot resolve, stop; otherwise `git push`.
|
||||
|
||||
## Git Notes
|
||||
|
||||
- If `git branch -d/-D <branch>` is policy-blocked, delete the local ref directly: `git update-ref -d refs/heads/<branch>`.
|
||||
- Agents MUST NOT create or push merge commits on `main`. If `main` has advanced, rebase local commits onto the latest `origin/main` before pushing.
|
||||
- Bulk PR close/reopen safety: if a close action would affect more than 5 PRs, first ask for explicit user confirmation with the exact PR count and target scope/query.
|
||||
|
||||
## GitHub Search (`gh`)
|
||||
|
||||
- Prefer targeted keyword search before proposing new work or duplicating fixes.
|
||||
- Use `--repo openclaw/openclaw` + `--match title,body` first; add `--match comments` when triaging follow-up threads.
|
||||
- PRs: `gh search prs --repo openclaw/openclaw --match title,body --limit 50 -- "auto-update"`
|
||||
- Issues: `gh search issues --repo openclaw/openclaw --match title,body --limit 50 -- "auto-update"`
|
||||
- Structured output example:
|
||||
`gh search issues --repo openclaw/openclaw --match title,body --limit 50 --json number,title,state,url,updatedAt -- "auto update" --jq '.[] | "\(.number) | \(.state) | \(.title) | \(.url)"'`
|
||||
|
||||
## Security & Configuration Tips
|
||||
|
||||
- Web provider stores creds at `~/.openclaw/credentials/`; rerun `openclaw login` if logged out.
|
||||
- Pi sessions live under `~/.openclaw/sessions/` by default; the base directory is not configurable.
|
||||
- Environment variables: see `~/.profile`.
|
||||
- Never commit or publish real phone numbers, videos, or live configuration values. Use obviously fake placeholders in docs, tests, and examples.
|
||||
- Release flow: use the private [maintainer release docs](https://github.com/openclaw/maintainers/blob/main/release/README.md) for the actual runbook; use `docs/reference/RELEASING.md` for the public release policy.
|
||||
- Release flow: use the private [maintainer release docs](https://github.com/openclaw/maintainers/blob/main/release/README.md) for the actual runbook, `docs/reference/RELEASING.md` for the public release policy, and `$openclaw-release-maintainer` for the maintainership workflow.
|
||||
|
||||
## GHSA (Repo Advisory) Patch/Publish
|
||||
|
||||
- Before reviewing security advisories, read `SECURITY.md`.
|
||||
- Fetch: `gh api /repos/openclaw/openclaw/security-advisories/<GHSA>`
|
||||
- Latest npm: `npm view openclaw version --userconfig "$(mktemp)"`
|
||||
- Private fork PRs must be closed:
|
||||
`fork=$(gh api /repos/openclaw/openclaw/security-advisories/<GHSA> | jq -r .private_fork.full_name)`
|
||||
`gh pr list -R "$fork" --state open` (must be empty)
|
||||
- Description newline footgun: write Markdown via heredoc to `/tmp/ghsa.desc.md` (no `"\\n"` strings)
|
||||
- Build patch JSON via jq: `jq -n --rawfile desc /tmp/ghsa.desc.md '{summary,severity,description:$desc,vulnerabilities:[...]}' > /tmp/ghsa.patch.json`
|
||||
- GHSA API footgun: cannot set `severity` and `cvss_vector_string` in the same PATCH; do separate calls.
|
||||
- Patch + publish: `gh api -X PATCH /repos/openclaw/openclaw/security-advisories/<GHSA> --input /tmp/ghsa.patch.json` (publish = include `"state":"published"`; no `/publish` endpoint)
|
||||
- If publish fails (HTTP 422): missing `severity`/`description`/`vulnerabilities[]`, or private fork has open PRs
|
||||
- Verify: re-fetch; ensure `state=published`, `published_at` set; `jq -r .description | rg '\\\\n'` returns nothing
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- Rebrand/migration issues or legacy config/service warnings: run `openclaw doctor` (see `docs/gateway/doctor.md`).
|
||||
|
||||
## Agent-Specific Notes
|
||||
## Local Runtime / Platform Notes
|
||||
|
||||
- Vocabulary: "makeup" = "mac app".
|
||||
- Parallels macOS retests: use the snapshot most closely named like `macOS 26.3.1 fresh` when the user asks for a clean/fresh macOS rerun; avoid older Tahoe snapshots unless explicitly requested.
|
||||
- Parallels beta smoke: use `--target-package-spec openclaw@<beta-version>` for the beta artifact, and pin the stable side with both `--install-version <stable-version>` and `--latest-version <stable-version>` for upgrade runs. npm dist-tags can move mid-run.
|
||||
- Parallels beta smoke, Windows nuance: old stable `2026.3.12` still prints the Unicode Windows onboarding banner, so mojibake during the stable precheck log is expected there. Judge the beta package by the post-upgrade lane.
|
||||
- Parallels macOS smoke playbook:
|
||||
- `prlctl exec` is fine for deterministic repo commands, but it can misrepresent interactive shell behavior (`PATH`, `HOME`, `curl | bash`, shebang resolution). For installer parity or shell-sensitive repros, prefer the guest Terminal or `prlctl enter`.
|
||||
- Fresh Tahoe snapshot current reality: `brew` exists, `node` may not be on `PATH` in noninteractive guest exec. Use absolute `/opt/homebrew/bin/node` for repo/CLI runs when needed.
|
||||
- Preferred automation entrypoint: `pnpm test:parallels:macos`. It restores the snapshot most closely matching `macOS 26.3.1 fresh`, serves the current `main` tarball from the host, then runs fresh-install and latest-release-to-main smoke lanes.
|
||||
- Discord roundtrip smoke is opt-in. Pass `--discord-token-env <VAR> --discord-guild-id <guild> --discord-channel-id <channel>`; the harness will configure Discord in-guest, post a guest message, verify host-side visibility via the Discord REST API, post a fresh host-side message back into the channel, then verify `openclaw message read` sees it in-guest.
|
||||
- Keep the Discord token in a host env var only. For Peter’s Mac Studio bot, fetch it into a temp env var from `~/.openclaw/openclaw.json` over SSH instead of hardcoding it in repo files/shell history.
|
||||
- For Discord smoke on this snapshot: use `openclaw message send/read` via the installed wrapper, not `node openclaw.mjs message ...`; lazy `message` subcommands do not resolve the same way through the direct module entrypoint.
|
||||
- For Discord guild allowlists: set `channels.discord.guilds` as one JSON object. Do not use dotted `config set channels.discord.guilds.<snowflake>...` paths; numeric snowflakes get treated as array indexes.
|
||||
- Avoid `prlctl enter` / expect for the Discord config phase; long lines get mangled. Use `prlctl exec --current-user /bin/sh -lc ...` with short commands or temp files.
|
||||
- Gateway verification in smoke runs should use `openclaw gateway status --deep --require-rpc`, not plain `--deep`, so probe failures go non-zero.
|
||||
- Latest-release pre-upgrade diagnostics still need compatibility fallback: stable `2026.3.12` does not know `--require-rpc`, so precheck status dumps should fall back to plain `gateway status --deep` until the guest is upgraded.
|
||||
- Harness output: pass `--json` for machine-readable summary; per-phase logs land under `/tmp/openclaw-parallels-smoke.*`.
|
||||
- All-OS parallel runs should share the host `dist` build via `/tmp/openclaw-parallels-build.lock` instead of rebuilding three times.
|
||||
- Current expected outcome on latest stable pre-upgrade: `precheck=latest-ref-fail` is normal on `2026.3.12`; treat it as a baseline signal, not a regression, unless the post-upgrade `main` lane also fails.
|
||||
- Fresh host-served tgz install: restore fresh snapshot, install tgz as guest root with `HOME=/var/root`, then run onboarding as the desktop user via `prlctl exec --current-user`.
|
||||
- For `openclaw onboard --non-interactive --secret-input-mode ref --install-daemon`, expect env-backed auth-profile refs (for example `OPENAI_API_KEY`) to be copied into the service env at install time; this path was fixed and should stay green.
|
||||
- Don’t run local + gateway agent turns in parallel on the same fresh workspace/session; they can collide on the session lock. Run sequentially.
|
||||
- Root-installed tarball smoke on Tahoe can still log plugin blocks for world-writable `extensions/*` under `/opt/homebrew/lib/node_modules/openclaw`; treat that as separate from onboarding/gateway health unless the task is plugin loading.
|
||||
- Parallels Windows smoke playbook:
|
||||
- Preferred automation entrypoint: `pnpm test:parallels:windows`. It restores the snapshot most closely matching `pre-openclaw-native-e2e-2026-03-12`, serves the current `main` tarball from the host, then runs fresh-install and latest-release-to-main smoke lanes.
|
||||
- Gateway verification in smoke runs should use `openclaw gateway status --deep --require-rpc`, not plain `--deep`, so probe failures go non-zero.
|
||||
- Latest-release pre-upgrade diagnostics still need compatibility fallback: stable `2026.3.12` does not know `--require-rpc`, so precheck status dumps should fall back to plain `gateway status --deep` until the guest is upgraded.
|
||||
- Always use `prlctl exec --current-user` for Windows guest runs; plain `prlctl exec` lands in `NT AUTHORITY\SYSTEM` and does not match the real desktop-user install path.
|
||||
- Prefer explicit `npm.cmd` / `openclaw.cmd`. Bare `npm` / `openclaw` in PowerShell can hit the `.ps1` shim and fail under restrictive execution policy.
|
||||
- Use PowerShell only as the transport (`powershell.exe -NoProfile -ExecutionPolicy Bypass`) and call the `.cmd` shims explicitly from inside it.
|
||||
- Harness output: pass `--json` for machine-readable summary; per-phase logs land under `/tmp/openclaw-parallels-windows.*`.
|
||||
- Current expected outcome on latest stable pre-upgrade: `precheck=latest-ref-fail` is normal on `2026.3.12`; treat it as a baseline signal, not a regression, unless the post-upgrade `main` lane also fails.
|
||||
- Keep Windows onboarding/status text ASCII-clean in logs. Fancy punctuation in banners shows up as mojibake through the current guest PowerShell capture path.
|
||||
- Parallels Linux smoke playbook:
|
||||
- Preferred automation entrypoint: `pnpm test:parallels:linux`. It restores the snapshot most closely matching `fresh` on `Ubuntu 24.04.3 ARM64`, serves the current `main` tarball from the host, then runs fresh-install and latest-release-to-main smoke lanes.
|
||||
- Use plain `prlctl exec` on this snapshot. `--current-user` is not the right transport there.
|
||||
- Fresh snapshot reality: `curl` is missing and `apt-get update` can fail on clock skew. Bootstrap with `apt-get -o Acquire::Check-Date=false update` and install `curl ca-certificates` before testing installer paths.
|
||||
- Fresh `main` tgz smoke on Linux still needs the latest-release installer first, because this snapshot has no Node/npm before bootstrap. The harness does stable bootstrap first, then overlays current `main`.
|
||||
- This snapshot does not have a usable `systemd --user` session. Treat managed daemon install as unsupported here; use `--skip-health`, then verify with direct `openclaw gateway run --bind loopback --port 18789 --force`.
|
||||
- Env-backed auth refs are still fine, but any direct shell launch (`openclaw gateway run`, `openclaw agent --local`, Linux `gateway status --deep` against that direct run) must inherit the referenced env vars in the same shell.
|
||||
- `prlctl exec` reaps detached Linux child processes on this snapshot, so a background `openclaw gateway run` launched from automation is not a trustworthy smoke path. The harness verifies installer + `agent --local`; do direct gateway checks only from an interactive guest shell when needed.
|
||||
- When you do run Linux gateway checks manually from an interactive guest shell, use `openclaw gateway status --deep --require-rpc` so an RPC miss is a hard failure.
|
||||
- Prefer direct argv guest commands for fetch/install steps (`curl`, `npm install -g`, `openclaw ...`) over nested `bash -lc` quoting; Linux guest quoting through Parallels was the flaky part.
|
||||
- Harness output: pass `--json` for machine-readable summary; per-phase logs land under `/tmp/openclaw-parallels-linux.*`.
|
||||
- Current expected outcome on Linux smoke: fresh + upgrade should pass installer and `agent --local`; gateway remains `skipped-no-detached-linux-gateway` on this snapshot and should not be treated as a regression by itself.
|
||||
- Rebrand/migration issues or legacy config/service warnings: run `openclaw doctor` (see `docs/gateway/doctor.md`).
|
||||
- Use `$openclaw-parallels-smoke` at `.agents/skills/openclaw-parallels-smoke/SKILL.md` for Parallels smoke, rerun, upgrade, debug, and result-interpretation workflows across macOS, Windows, and Linux guests.
|
||||
- For the macOS Discord roundtrip deep dive, use the narrower `.agents/skills/parallels-discord-roundtrip/SKILL.md` companion skill.
|
||||
- Never edit `node_modules` (global/Homebrew/npm/git installs too). Updates overwrite. Skill notes go in `tools.md` or `AGENTS.md`.
|
||||
- If you need local-only `.agents` ignores, use `.git/info/exclude` instead of repo `.gitignore`.
|
||||
- When adding a new `AGENTS.md` anywhere in the repo, also add a `CLAUDE.md` symlink pointing to it (example: `ln -s AGENTS.md CLAUDE.md`).
|
||||
- Signal: "update fly" => `fly ssh console -a flawd-bot -C "bash -lc 'cd /data/clawd/openclaw && git pull --rebase origin main'"` then `fly machines restart e825232f34d058 -a flawd-bot`.
|
||||
- When working on a GitHub Issue or PR, print the full URL at the end of the task.
|
||||
- When answering questions, respond with high-confidence answers only: verify in code; do not guess.
|
||||
- Never update the Carbon dependency.
|
||||
- Any dependency with `pnpm.patchedDependencies` must use an exact version (no `^`/`~`).
|
||||
- Patching dependencies (pnpm patches, overrides, or vendored changes) requires explicit approval; do not do this by default.
|
||||
- CLI progress: use `src/cli/progress.ts` (`osc-progress` + `@clack/prompts` spinner); don’t hand-roll spinners/bars.
|
||||
- Status output: keep tables + ANSI-safe wrapping (`src/terminal/table.ts`); `status --all` = read-only/pasteable, `status --deep` = probes.
|
||||
- Gateway currently runs only as the menubar app; there is no separate LaunchAgent/helper label installed. Restart via the OpenClaw Mac app or `scripts/restart-mac.sh`; to verify/kill use `launchctl print gui/$UID | grep openclaw` rather than assuming a fixed label. **When debugging on macOS, start/stop the gateway via the app, not ad-hoc tmux sessions; kill any temporary tunnels before handoff.**
|
||||
@@ -271,6 +168,20 @@
|
||||
- iOS Team ID lookup: `security find-identity -p codesigning -v` → use Apple Development (…) TEAMID. Fallback: `defaults read com.apple.dt.Xcode IDEProvisioningTeamIdentifiers`.
|
||||
- A2UI bundle hash: `src/canvas-host/a2ui/.bundle.hash` is auto-generated; ignore unexpected changes, and only regenerate via `pnpm canvas:a2ui:bundle` (or `scripts/bundle-a2ui.sh`) when needed. Commit the hash as a separate commit.
|
||||
- Release signing/notary credentials are managed outside the repo; maintainers keep that setup in the private [maintainer release docs](https://github.com/openclaw/maintainers/tree/main/release).
|
||||
- Lobster palette: use the shared CLI palette in `src/terminal/palette.ts` (no hardcoded colors); apply palette to onboarding/config prompts and other TTY UI output as needed.
|
||||
- When asked to open a “session” file, open the Pi session logs under `~/.openclaw/agents/<agentId>/sessions/*.jsonl` (use the `agent=<id>` value in the Runtime line of the system prompt; newest unless a specific ID is given), not the default `sessions.json`. If logs are needed from another machine, SSH via Tailscale and read the same path there.
|
||||
- Do not rebuild the macOS app over SSH; rebuilds must be run directly on the Mac.
|
||||
- Voice wake forwarding tips:
|
||||
- Command template should stay `openclaw-mac agent --message "${text}" --thinking low`; `VoiceWakeForwarder` already shell-escapes `${text}`. Don’t add extra quotes.
|
||||
- launchd PATH is minimal; ensure the app’s launch agent PATH includes standard system paths plus your pnpm bin (typically `$HOME/Library/pnpm`) so `pnpm`/`openclaw` binaries resolve when invoked via `openclaw-mac`.
|
||||
|
||||
## Collaboration / Safety Notes
|
||||
|
||||
- When working on a GitHub Issue or PR, print the full URL at the end of the task.
|
||||
- When answering questions, respond with high-confidence answers only: verify in code; do not guess.
|
||||
- Never update the Carbon dependency.
|
||||
- Any dependency with `pnpm.patchedDependencies` must use an exact version (no `^`/`~`).
|
||||
- Patching dependencies (pnpm patches, overrides, or vendored changes) requires explicit approval; do not do this by default.
|
||||
- **Multi-agent safety:** do **not** create/apply/drop `git stash` entries unless explicitly requested (this includes `git pull --rebase --autostash`). Assume other agents may be working; keep unrelated WIP untouched and avoid cross-cutting state changes.
|
||||
- **Multi-agent safety:** when the user says "push", you may `git pull --rebase` to integrate latest changes (never discard other agents' work). When the user says "commit", scope to your changes only. When the user says "commit all", commit everything in grouped chunks.
|
||||
- **Multi-agent safety:** do **not** create/remove/modify `git worktree` checkouts (or edit `.worktrees/*`) unless explicitly requested.
|
||||
@@ -281,41 +192,12 @@
|
||||
- If staged+unstaged diffs are formatting-only, auto-resolve without asking.
|
||||
- If commit/push already requested, auto-stage and include formatting-only follow-ups in the same commit (or a tiny follow-up commit if needed), no extra confirmation.
|
||||
- Only ask when changes are semantic (logic/data/behavior).
|
||||
- Lobster palette: use the shared CLI palette in `src/terminal/palette.ts` (no hardcoded colors); apply palette to onboarding/config prompts and other TTY UI output as needed.
|
||||
- **Multi-agent safety:** focus reports on your edits; avoid guard-rail disclaimers unless truly blocked; when multiple agents touch the same file, continue if safe; end with a brief “other files present” note only if relevant.
|
||||
- Bug investigations: read source code of relevant npm dependencies and all related local code before concluding; aim for high-confidence root cause.
|
||||
- Code style: add brief comments for tricky logic; keep files under ~500 LOC when feasible (split/refactor as needed).
|
||||
- Tool schema guardrails (google-antigravity): avoid `Type.Union` in tool input schemas; no `anyOf`/`oneOf`/`allOf`. Use `stringEnum`/`optionalStringEnum` (Type.Unsafe enum) for string lists, and `Type.Optional(...)` instead of `... | null`. Keep top-level tool schema as `type: "object"` with `properties`.
|
||||
- Tool schema guardrails: avoid raw `format` property names in tool schemas; some validators treat `format` as a reserved keyword and reject the schema.
|
||||
- When asked to open a “session” file, open the Pi session logs under `~/.openclaw/agents/<agentId>/sessions/*.jsonl` (use the `agent=<id>` value in the Runtime line of the system prompt; newest unless a specific ID is given), not the default `sessions.json`. If logs are needed from another machine, SSH via Tailscale and read the same path there.
|
||||
- Do not rebuild the macOS app over SSH; rebuilds must be run directly on the Mac.
|
||||
- Never send streaming/partial replies to external messaging surfaces (WhatsApp, Telegram); only final replies should be delivered there. Streaming/tool events may still go to internal UIs/control channel.
|
||||
- Voice wake forwarding tips:
|
||||
- Command template should stay `openclaw-mac agent --message "${text}" --thinking low`; `VoiceWakeForwarder` already shell-escapes `${text}`. Don’t add extra quotes.
|
||||
- launchd PATH is minimal; ensure the app’s launch agent PATH includes standard system paths plus your pnpm bin (typically `$HOME/Library/pnpm`) so `pnpm`/`openclaw` binaries resolve when invoked via `openclaw-mac`.
|
||||
- For manual `openclaw message send` messages that include `!`, use the heredoc pattern noted below to avoid the Bash tool’s escaping.
|
||||
- Release guardrails: do not change version numbers without operator’s explicit consent; always ask permission before running any npm publish/release step.
|
||||
- Beta release guardrail: when using a beta Git tag (for example `vYYYY.M.D-beta.N`), publish npm with a matching beta version suffix (for example `YYYY.M.D-beta.N`) rather than a plain version on `--tag beta`; otherwise the plain version name gets consumed/blocked.
|
||||
|
||||
## Release Auth
|
||||
|
||||
- Core `openclaw` publish uses GitHub trusted publishing; do not use `NPM_TOKEN` or the plugin OTP flow for core releases.
|
||||
- Separate `@openclaw/*` plugin publishes use a different maintainer-only auth flow.
|
||||
- Plugin scope: only publish already-on-npm `@openclaw/*` plugins. Bundled disk-tree-only plugins stay out.
|
||||
- Maintainers: private 1Password item names, tmux rules, plugin publish helpers, and local mac signing/notary setup live in the private [maintainer release docs](https://github.com/openclaw/maintainers/blob/main/release/README.md).
|
||||
|
||||
## Changelog Release Notes
|
||||
|
||||
- When cutting a mac release with beta GitHub prerelease:
|
||||
- Tag `vYYYY.M.D-beta.N` from the release commit (example: `v2026.2.15-beta.1`).
|
||||
- Create prerelease with title `openclaw YYYY.M.D-beta.N`.
|
||||
- Use release notes from `CHANGELOG.md` version section (`Changes` + `Fixes`, no title duplicate).
|
||||
- Attach at least `OpenClaw-YYYY.M.D.zip` and `OpenClaw-YYYY.M.D.dSYM.zip`; include `.dmg` if available.
|
||||
|
||||
- Keep top version entries in `CHANGELOG.md` sorted by impact:
|
||||
- `### Changes` first.
|
||||
- `### Fixes` deduped and ranked with user-facing fixes first.
|
||||
- Before tagging/publishing, run:
|
||||
- `node --import tsx scripts/release-check.ts`
|
||||
- `pnpm release:check`
|
||||
- `pnpm test:install:smoke` or `OPENCLAW_INSTALL_SMOKE_SKIP_NONROOT=1 pnpm test:install:smoke` for non-root smoke path.
|
||||
|
||||
12
CHANGELOG.md
12
CHANGELOG.md
@@ -23,7 +23,8 @@ Docs: https://docs.openclaw.ai
|
||||
- Feishu/cards: add structured interactive approval and quick-action launcher cards, preserve callback user and conversation context through routing, and keep legacy card-action fallback behavior so common actions can run without typing raw commands. (#47873) Thanks @Takhoffman.
|
||||
- Feishu/streaming: add `onReasoningStream` and `onReasoningEnd` support to streaming cards, so `/reasoning stream` renders thinking tokens as markdown blockquotes in the same card — matching the Telegram channel's reasoning lane behavior. (#46029) Thanks @day253.
|
||||
- Feishu/cards: add identity-aware structured card headers and note footers for Feishu replies and direct sends, while keeping that presentation wired through the shared outbound identity path. (#29938) Thanks @nszhsl.
|
||||
- Android/nodes: add `callLog.search` plus shared Call Log permission wiring so Android nodes can search recent call history through the gateway. (#44073) Thanks @lxk7280.
|
||||
- Android/nodes: add `callLog.search` plus shared Call Log permission wiring so Android nodes can search recent call history through the gateway. (#44073) Thanks @lixuankai.
|
||||
- Android/nodes: add `sms.search` plus shared SMS permission wiring so Android nodes can search device text messages through the gateway. (#48299) Thanks @lixuankai.
|
||||
- Plugins/MiniMax: merge the bundled MiniMax API and MiniMax OAuth plugin surfaces into a single default-on `minimax` plugin, while keeping legacy `minimax-portal-auth` config ids aliased for compatibility.
|
||||
- Telegram/actions: add `topic-edit` for forum-topic renames and icon updates while sharing the same Telegram topic-edit transport used by the plugin runtime. (#47798) Thanks @obviyus.
|
||||
- Telegram/error replies: add a default-off `channels.telegram.silentErrorReplies` setting so bot error replies can be delivered silently across regular replies, native commands, and fallback sends. (#19776) Thanks @ImLukeF.
|
||||
@@ -42,9 +43,11 @@ Docs: https://docs.openclaw.ai
|
||||
- Control UI/appearance: unify theme border radii across Claw, Knot, and Dash, and add a Roundness slider to the Appearance settings so users can adjust corner radius from sharp to fully rounded. Thanks @BunsDev.
|
||||
- Control UI/chat: add an expand-to-canvas button on assistant chat bubbles and in-app session navigation from Sessions and Cron views. Thanks @BunsDev.
|
||||
- Plugins/context engines: expose `delegateCompactionToRuntime(...)` on the public plugin SDK, refactor the legacy engine to use the shared helper, and clarify `ownsCompaction` delegation semantics for non-owning engines. (#49061) Thanks @jalehman.
|
||||
- Plugins/MiniMax: add MiniMax-M2.7 and MiniMax-M2.7-highspeed models and update the default model from M2.5 to M2.7. (#49691) Thanks @liyuan97.
|
||||
|
||||
### Fixes
|
||||
|
||||
- CLI/Ollama onboarding: keep the interactive model picker for explicit `openclaw onboard --auth-choice ollama` runs so setup still selects a default model without reintroducing pre-picker auto-pulls. (#49249) Thanks @BruceMacD.
|
||||
- Plugins/bundler TDZ: fix `RESERVED_COMMANDS` temporal dead zone error that prevented device-pair, phone-control, and talk-voice plugins from registering when the bundler placed the commands module after call sites in the same output chunk. Thanks @BunsDev.
|
||||
- Plugins/imports: fix stale googlechat runtime-api import paths and signal SDK circular re-exports broken by recent plugin-sdk refactors. Thanks @BunsDev.
|
||||
- Google auth/Node 25: patch `gaxios` to use native fetch without injecting `globalThis.window`, while translating proxy and mTLS transport settings so Google Vertex and Google Chat auth keep working on Node 25. (#47914) Thanks @pdd-cli.
|
||||
@@ -129,6 +132,9 @@ Docs: https://docs.openclaw.ai
|
||||
- Agents/compaction: write minimal boundary summaries for empty preparations while keeping split-turn prefixes on the normal path, so no-summarizable-message sessions stop retriggering the safeguard loop. (#42215) thanks @lml2468.
|
||||
- Models/chat commands: keep `/model ...@YYYYMMDD` version suffixes intact by default, but still honor matching stored numeric auth-profile overrides for the same provider. (#48896) Thanks @Alix-007.
|
||||
- Gateway/channels: serialize per-account channel startup so overlapping starts do not boot the same provider twice, preventing MS Teams `EADDRINUSE` crash loops during startup and restart. (#49583) Thanks @sudie-codes.
|
||||
- Tests/OpenAI Codex auth: align login expectations with the default `gpt-5.4` model so CI coverage stays consistent with the current OpenAI Codex default. (#44367) Thanks @jrrcdev.
|
||||
- Discord: enforce strict DM component allowlist auth (#49997) Thanks @joshavant.
|
||||
- Stabilize plugin loader and Docker extension smoke (#50058) Thanks @joshavant.
|
||||
|
||||
### Fixes
|
||||
|
||||
@@ -149,6 +155,9 @@ Docs: https://docs.openclaw.ai
|
||||
- xAI/web search: add missing Grok credential metadata so the bundled provider registration type-checks again. (#49472) thanks @scoootscooob.
|
||||
- Signal/runtime API: re-export `SignalAccountConfig` so Signal account resolution type-checks again. (#49470) Thanks @scoootscooob.
|
||||
- Google Chat/runtime API: thin the private runtime barrel onto the curated public SDK surface while keeping public Google Chat exports intact. (#49504) Thanks @scoootscooob.
|
||||
- WhatsApp: stabilize inbound monitor and setup tests (#50007) Thanks @joshavant.
|
||||
- Matrix: make onboarding status runtime-safe (#49995) Thanks @joshavant.
|
||||
- WhatsApp/active-listener: pin the active listener registry to a `globalThis` singleton so split WhatsApp bundle chunks share one listener map and outbound sends stop missing the registered session. (#47433) Thanks @clawdia67.
|
||||
|
||||
### Breaking
|
||||
|
||||
@@ -236,6 +245,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Auth/Codex CLI reuse: sync reused Codex CLI credentials into the supported `openai-codex:default` OAuth profile instead of reviving the deprecated `openai-codex:codex-cli` slot, so doctor cleanup no longer loops. (#45353) thanks @Gugu-sugar.
|
||||
- Deps/audit: bump the pinned `fast-xml-parser` override to the first patched release so `pnpm audit --prod --audit-level=high` no longer fails on the AWS Bedrock XML builder path. Thanks @vincentkoc.
|
||||
- Hooks/after_compaction: forward `sessionFile` for direct/manual compaction events and add `sessionFile` plus `sessionKey` to wired auto-compaction hook context so plugins receive the session metadata already declared in the hook types. (#40781) Thanks @jarimustonen.
|
||||
- Sessions/BlueBubbles/cron: persist outbound session routing and transcript mirroring for new targets, auto-create BlueBubbles chats before attachment sends, and only suppress isolated cron deliveries when the run started hours late instead of merely finishing late. (#50092)
|
||||
|
||||
### Breaking
|
||||
|
||||
|
||||
@@ -83,7 +83,8 @@ Welcome to the lobster tank! 🦞
|
||||
|
||||
1. **Bugs & small fixes** → Open a PR!
|
||||
2. **New features / architecture** → Start a [GitHub Discussion](https://github.com/openclaw/openclaw/discussions) or ask in Discord first
|
||||
3. **Questions** → Discord [#help](https://discord.com/channels/1456350064065904867/1459642797895319552) / [#users-helping-users](https://discord.com/channels/1456350064065904867/1459007081603403828)
|
||||
3. **Test/CI-only PRs for known `main` failures** → Don't open a PR, the Maintainer team is already tracking it and such PRs will be closed automatically. If you've spotted a _new_ regression not yet shown in main CI, report it as an issue first.
|
||||
4. **Questions** → Discord [#help](https://discord.com/channels/1456350064065904867/1459642797895319552) / [#users-helping-users](https://discord.com/channels/1456350064065904867/1459007081603403828)
|
||||
|
||||
## Before You PR
|
||||
|
||||
@@ -96,6 +97,7 @@ Welcome to the lobster tank! 🦞
|
||||
- For targeted shared-surface work, use `pnpm test:contracts:channels` or `pnpm test:contracts:plugins`
|
||||
- If you changed broader runtime behavior, still run the relevant wider lanes (`pnpm test:extensions`, `pnpm test:channels`, or `pnpm test`) before asking for review
|
||||
- If you have access to Codex, run `codex review --base origin/main` locally before opening or updating your PR. Treat this as the current highest standard of AI review, even if GitHub Codex review also runs.
|
||||
- Do not submit test or CI-config fixes for failures already red on `main` CI. If a failure is already visible in the [main branch CI runs](https://github.com/openclaw/openclaw/actions), it's a known issue the Maintainer team is tracking, and a PR that only addresses those failures will be closed automatically. If you spot a _new_ regression not yet shown in main CI, report it as an issue first.
|
||||
- Ensure CI checks pass
|
||||
- Keep PRs focused (one thing per PR; do not mix unrelated concerns)
|
||||
- Describe what & why
|
||||
|
||||
@@ -146,6 +146,10 @@ COPY --from=runtime-assets --chown=node:node /app/extensions ./extensions
|
||||
COPY --from=runtime-assets --chown=node:node /app/skills ./skills
|
||||
COPY --from=runtime-assets --chown=node:node /app/docs ./docs
|
||||
|
||||
# In npm-installed Docker images, prefer the copied source extension tree for
|
||||
# bundled discovery so package metadata that points at source entries stays valid.
|
||||
ENV OPENCLAW_BUNDLED_PLUGINS_DIR=/app/extensions
|
||||
|
||||
# Keep pnpm available in the runtime image for container-local workflows.
|
||||
# Use a shared Corepack home so the non-root `node` user does not need a
|
||||
# first-run network fetch when invoking pnpm.
|
||||
|
||||
@@ -176,6 +176,45 @@ More details: `docs/platforms/android.md`.
|
||||
- `CAMERA` for `camera.snap` and `camera.clip`
|
||||
- `RECORD_AUDIO` for `camera.clip` when `includeAudio=true`
|
||||
|
||||
## Google Play Restricted Permissions
|
||||
|
||||
As of March 19, 2026, these manifest permissions are the main Google Play policy risk for this app:
|
||||
|
||||
- `READ_SMS`
|
||||
- `SEND_SMS`
|
||||
- `READ_CALL_LOG`
|
||||
|
||||
Why these matter:
|
||||
|
||||
- Google Play treats SMS and Call Log access as highly restricted. In most cases, Play only allows them for the default SMS app, default Phone app, default Assistant, or a narrow policy exception.
|
||||
- Review usually involves a `Permissions Declaration Form`, policy justification, and demo video evidence in Play Console.
|
||||
- If we want a Play-safe build, these should be the first permissions removed behind a dedicated product flavor / variant.
|
||||
|
||||
Current OpenClaw Android implication:
|
||||
|
||||
- APK / sideload build can keep SMS and Call Log features.
|
||||
- Google Play build should exclude SMS send/search and Call Log search unless the product is intentionally positioned and approved as a default-handler exception case.
|
||||
|
||||
Policy links:
|
||||
|
||||
- [Google Play SMS and Call Log policy](https://support.google.com/googleplay/android-developer/answer/10208820?hl=en)
|
||||
- [Google Play sensitive permissions policy hub](https://support.google.com/googleplay/android-developer/answer/16558241)
|
||||
- [Android default handlers guide](https://developer.android.com/guide/topics/permissions/default-handlers)
|
||||
|
||||
Other Play-restricted surfaces to watch if added later:
|
||||
|
||||
- `ACCESS_BACKGROUND_LOCATION`
|
||||
- `MANAGE_EXTERNAL_STORAGE`
|
||||
- `QUERY_ALL_PACKAGES`
|
||||
- `REQUEST_INSTALL_PACKAGES`
|
||||
- `AccessibilityService`
|
||||
|
||||
Reference links:
|
||||
|
||||
- [Background location policy](https://support.google.com/googleplay/android-developer/answer/9799150)
|
||||
- [AccessibilityService policy](https://support.google.com/googleplay/android-developer/answer/10964491?hl=en-GB)
|
||||
- [Photo and Video Permissions policy](https://support.google.com/googleplay/android-developer/answer/14594990)
|
||||
|
||||
## Integration Capability Test (Preconditioned)
|
||||
|
||||
This suite assumes setup is already done manually. It does **not** install/run/pair automatically.
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
<uses-permission android:name="android.permission.SEND_SMS" />
|
||||
<uses-permission android:name="android.permission.READ_SMS" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED" />
|
||||
<uses-permission
|
||||
|
||||
@@ -137,7 +137,8 @@ class NodeRuntime(
|
||||
voiceWakeMode = { VoiceWakeMode.Off },
|
||||
motionActivityAvailable = { motionHandler.isActivityAvailable() },
|
||||
motionPedometerAvailable = { motionHandler.isPedometerAvailable() },
|
||||
smsAvailable = { sms.canSendSms() },
|
||||
sendSmsAvailable = { sms.canSendSms() },
|
||||
readSmsAvailable = { sms.canReadSms() },
|
||||
hasRecordAudioPermission = { hasRecordAudioPermission() },
|
||||
manualTls = { manualTls.value },
|
||||
)
|
||||
@@ -160,7 +161,8 @@ class NodeRuntime(
|
||||
isForeground = { _isForeground.value },
|
||||
cameraEnabled = { cameraEnabled.value },
|
||||
locationEnabled = { locationMode.value != LocationMode.Off },
|
||||
smsAvailable = { sms.canSendSms() },
|
||||
sendSmsAvailable = { sms.canSendSms() },
|
||||
readSmsAvailable = { sms.canReadSms() },
|
||||
debugBuild = { BuildConfig.DEBUG },
|
||||
refreshNodeCanvasCapability = { nodeSession.refreshNodeCanvasCapability() },
|
||||
onCanvasA2uiPush = {
|
||||
|
||||
@@ -17,7 +17,8 @@ class ConnectionManager(
|
||||
private val voiceWakeMode: () -> VoiceWakeMode,
|
||||
private val motionActivityAvailable: () -> Boolean,
|
||||
private val motionPedometerAvailable: () -> Boolean,
|
||||
private val smsAvailable: () -> Boolean,
|
||||
private val sendSmsAvailable: () -> Boolean,
|
||||
private val readSmsAvailable: () -> Boolean,
|
||||
private val hasRecordAudioPermission: () -> Boolean,
|
||||
private val manualTls: () -> Boolean,
|
||||
) {
|
||||
@@ -78,7 +79,8 @@ class ConnectionManager(
|
||||
NodeRuntimeFlags(
|
||||
cameraEnabled = cameraEnabled(),
|
||||
locationEnabled = locationMode() != LocationMode.Off,
|
||||
smsAvailable = smsAvailable(),
|
||||
sendSmsAvailable = sendSmsAvailable(),
|
||||
readSmsAvailable = readSmsAvailable(),
|
||||
voiceWakeEnabled = voiceWakeMode() != VoiceWakeMode.Off && hasRecordAudioPermission(),
|
||||
motionActivityAvailable = motionActivityAvailable(),
|
||||
motionPedometerAvailable = motionPedometerAvailable(),
|
||||
|
||||
@@ -18,7 +18,8 @@ import ai.openclaw.app.protocol.OpenClawSystemCommand
|
||||
data class NodeRuntimeFlags(
|
||||
val cameraEnabled: Boolean,
|
||||
val locationEnabled: Boolean,
|
||||
val smsAvailable: Boolean,
|
||||
val sendSmsAvailable: Boolean,
|
||||
val readSmsAvailable: Boolean,
|
||||
val voiceWakeEnabled: Boolean,
|
||||
val motionActivityAvailable: Boolean,
|
||||
val motionPedometerAvailable: Boolean,
|
||||
@@ -29,7 +30,8 @@ enum class InvokeCommandAvailability {
|
||||
Always,
|
||||
CameraEnabled,
|
||||
LocationEnabled,
|
||||
SmsAvailable,
|
||||
SendSmsAvailable,
|
||||
ReadSmsAvailable,
|
||||
MotionActivityAvailable,
|
||||
MotionPedometerAvailable,
|
||||
DebugBuild,
|
||||
@@ -187,7 +189,11 @@ object InvokeCommandRegistry {
|
||||
),
|
||||
InvokeCommandSpec(
|
||||
name = OpenClawSmsCommand.Send.rawValue,
|
||||
availability = InvokeCommandAvailability.SmsAvailable,
|
||||
availability = InvokeCommandAvailability.SendSmsAvailable,
|
||||
),
|
||||
InvokeCommandSpec(
|
||||
name = OpenClawSmsCommand.Search.rawValue,
|
||||
availability = InvokeCommandAvailability.ReadSmsAvailable,
|
||||
),
|
||||
InvokeCommandSpec(
|
||||
name = OpenClawCallLogCommand.Search.rawValue,
|
||||
@@ -213,7 +219,7 @@ object InvokeCommandRegistry {
|
||||
NodeCapabilityAvailability.Always -> true
|
||||
NodeCapabilityAvailability.CameraEnabled -> flags.cameraEnabled
|
||||
NodeCapabilityAvailability.LocationEnabled -> flags.locationEnabled
|
||||
NodeCapabilityAvailability.SmsAvailable -> flags.smsAvailable
|
||||
NodeCapabilityAvailability.SmsAvailable -> flags.sendSmsAvailable || flags.readSmsAvailable
|
||||
NodeCapabilityAvailability.VoiceWakeEnabled -> flags.voiceWakeEnabled
|
||||
NodeCapabilityAvailability.MotionAvailable -> flags.motionActivityAvailable || flags.motionPedometerAvailable
|
||||
}
|
||||
@@ -228,7 +234,8 @@ object InvokeCommandRegistry {
|
||||
InvokeCommandAvailability.Always -> true
|
||||
InvokeCommandAvailability.CameraEnabled -> flags.cameraEnabled
|
||||
InvokeCommandAvailability.LocationEnabled -> flags.locationEnabled
|
||||
InvokeCommandAvailability.SmsAvailable -> flags.smsAvailable
|
||||
InvokeCommandAvailability.SendSmsAvailable -> flags.sendSmsAvailable
|
||||
InvokeCommandAvailability.ReadSmsAvailable -> flags.readSmsAvailable
|
||||
InvokeCommandAvailability.MotionActivityAvailable -> flags.motionActivityAvailable
|
||||
InvokeCommandAvailability.MotionPedometerAvailable -> flags.motionPedometerAvailable
|
||||
InvokeCommandAvailability.DebugBuild -> flags.debugBuild
|
||||
|
||||
@@ -32,7 +32,8 @@ class InvokeDispatcher(
|
||||
private val isForeground: () -> Boolean,
|
||||
private val cameraEnabled: () -> Boolean,
|
||||
private val locationEnabled: () -> Boolean,
|
||||
private val smsAvailable: () -> Boolean,
|
||||
private val sendSmsAvailable: () -> Boolean,
|
||||
private val readSmsAvailable: () -> Boolean,
|
||||
private val debugBuild: () -> Boolean,
|
||||
private val refreshNodeCanvasCapability: suspend () -> Boolean,
|
||||
private val onCanvasA2uiPush: () -> Unit,
|
||||
@@ -162,6 +163,7 @@ class InvokeDispatcher(
|
||||
|
||||
// SMS command
|
||||
OpenClawSmsCommand.Send.rawValue -> smsHandler.handleSmsSend(paramsJson)
|
||||
OpenClawSmsCommand.Search.rawValue -> smsHandler.handleSmsSearch(paramsJson)
|
||||
|
||||
// CallLog command
|
||||
OpenClawCallLogCommand.Search.rawValue -> callLogHandler.handleCallLogSearch(paramsJson)
|
||||
@@ -256,8 +258,17 @@ class InvokeDispatcher(
|
||||
message = "PEDOMETER_UNAVAILABLE: step counter not available",
|
||||
)
|
||||
}
|
||||
InvokeCommandAvailability.SmsAvailable ->
|
||||
if (smsAvailable()) {
|
||||
InvokeCommandAvailability.SendSmsAvailable ->
|
||||
if (sendSmsAvailable()) {
|
||||
null
|
||||
} else {
|
||||
GatewaySession.InvokeResult.error(
|
||||
code = "SMS_UNAVAILABLE",
|
||||
message = "SMS_UNAVAILABLE: SMS not available on this device",
|
||||
)
|
||||
}
|
||||
InvokeCommandAvailability.ReadSmsAvailable ->
|
||||
if (readSmsAvailable()) {
|
||||
null
|
||||
} else {
|
||||
GatewaySession.InvokeResult.error(
|
||||
|
||||
@@ -16,4 +16,16 @@ class SmsHandler(
|
||||
return GatewaySession.InvokeResult.error(code = code, message = error)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun handleSmsSearch(paramsJson: String?): GatewaySession.InvokeResult {
|
||||
val res = sms.search(paramsJson)
|
||||
if (res.ok) {
|
||||
return GatewaySession.InvokeResult.ok(res.payloadJson)
|
||||
} else {
|
||||
val error = res.error ?: "SMS_SEARCH_FAILED"
|
||||
val idx = error.indexOf(':')
|
||||
val code = if (idx > 0) error.substring(0, idx).trim() else "SMS_SEARCH_FAILED"
|
||||
return GatewaySession.InvokeResult.error(code = code, message = error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,19 +3,27 @@ package ai.openclaw.app.node
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.database.Cursor
|
||||
import android.net.Uri
|
||||
import android.provider.ContactsContract
|
||||
import android.provider.Telephony
|
||||
import android.telephony.SmsManager as AndroidSmsManager
|
||||
import androidx.core.content.ContextCompat
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.Serializable
|
||||
import ai.openclaw.app.PermissionRequester
|
||||
|
||||
/**
|
||||
* Sends SMS messages via the Android SMS API.
|
||||
* Requires SEND_SMS permission to be granted.
|
||||
*
|
||||
* Also provides SMS query functionality with READ_SMS permission.
|
||||
*/
|
||||
class SmsManager(private val context: Context) {
|
||||
|
||||
@@ -30,6 +38,30 @@ class SmsManager(private val context: Context) {
|
||||
val payloadJson: String,
|
||||
)
|
||||
|
||||
/**
|
||||
* Represents a single SMS message
|
||||
*/
|
||||
@Serializable
|
||||
data class SmsMessage(
|
||||
val id: Long,
|
||||
val threadId: Long,
|
||||
val address: String?,
|
||||
val person: String?,
|
||||
val date: Long,
|
||||
val dateSent: Long,
|
||||
val read: Boolean,
|
||||
val type: Int,
|
||||
val body: String?,
|
||||
val status: Int,
|
||||
)
|
||||
|
||||
data class SearchResult(
|
||||
val ok: Boolean,
|
||||
val messages: List<SmsMessage>,
|
||||
val error: String? = null,
|
||||
val payloadJson: String,
|
||||
)
|
||||
|
||||
internal data class ParsedParams(
|
||||
val to: String,
|
||||
val message: String,
|
||||
@@ -44,12 +76,30 @@ class SmsManager(private val context: Context) {
|
||||
) : ParseResult()
|
||||
}
|
||||
|
||||
internal data class QueryParams(
|
||||
val startTime: Long? = null,
|
||||
val endTime: Long? = null,
|
||||
val contactName: String? = null,
|
||||
val phoneNumber: String? = null,
|
||||
val keyword: String? = null,
|
||||
val type: Int? = null,
|
||||
val isRead: Boolean? = null,
|
||||
val limit: Int = DEFAULT_SMS_LIMIT,
|
||||
val offset: Int = 0,
|
||||
)
|
||||
|
||||
internal sealed class QueryParseResult {
|
||||
data class Ok(val params: QueryParams) : QueryParseResult()
|
||||
data class Error(val error: String) : QueryParseResult()
|
||||
}
|
||||
|
||||
internal data class SendPlan(
|
||||
val parts: List<String>,
|
||||
val useMultipart: Boolean,
|
||||
)
|
||||
|
||||
companion object {
|
||||
private const val DEFAULT_SMS_LIMIT = 25
|
||||
internal val JsonConfig = Json { ignoreUnknownKeys = true }
|
||||
|
||||
internal fun parseParams(paramsJson: String?, json: Json = JsonConfig): ParseResult {
|
||||
@@ -88,6 +138,52 @@ class SmsManager(private val context: Context) {
|
||||
return ParseResult.Ok(ParsedParams(to = to, message = message))
|
||||
}
|
||||
|
||||
internal fun parseQueryParams(paramsJson: String?, json: Json = JsonConfig): QueryParseResult {
|
||||
val params = paramsJson?.trim().orEmpty()
|
||||
if (params.isEmpty()) {
|
||||
return QueryParseResult.Ok(QueryParams())
|
||||
}
|
||||
|
||||
val obj = try {
|
||||
json.parseToJsonElement(params).jsonObject
|
||||
} catch (_: Throwable) {
|
||||
return QueryParseResult.Error("INVALID_REQUEST: expected JSON object")
|
||||
}
|
||||
|
||||
val startTime = (obj["startTime"] as? JsonPrimitive)?.content?.toLongOrNull()
|
||||
val endTime = (obj["endTime"] as? JsonPrimitive)?.content?.toLongOrNull()
|
||||
val contactName = (obj["contactName"] as? JsonPrimitive)?.content?.trim()
|
||||
val phoneNumber = (obj["phoneNumber"] as? JsonPrimitive)?.content?.trim()
|
||||
val keyword = (obj["keyword"] as? JsonPrimitive)?.content?.trim()
|
||||
val type = (obj["type"] as? JsonPrimitive)?.content?.toIntOrNull()
|
||||
val isRead = (obj["isRead"] as? JsonPrimitive)?.content?.toBooleanStrictOrNull()
|
||||
val limit = ((obj["limit"] as? JsonPrimitive)?.content?.toIntOrNull() ?: DEFAULT_SMS_LIMIT)
|
||||
.coerceIn(1, 200)
|
||||
val offset = ((obj["offset"] as? JsonPrimitive)?.content?.toIntOrNull() ?: 0)
|
||||
.coerceAtLeast(0)
|
||||
|
||||
// Validate time range
|
||||
if (startTime != null && endTime != null && startTime > endTime) {
|
||||
return QueryParseResult.Error("INVALID_REQUEST: startTime must be less than or equal to endTime")
|
||||
}
|
||||
|
||||
return QueryParseResult.Ok(QueryParams(
|
||||
startTime = startTime,
|
||||
endTime = endTime,
|
||||
contactName = contactName,
|
||||
phoneNumber = phoneNumber,
|
||||
keyword = keyword,
|
||||
type = type,
|
||||
isRead = isRead,
|
||||
limit = limit,
|
||||
offset = offset,
|
||||
))
|
||||
}
|
||||
|
||||
private fun normalizePhoneNumber(phone: String): String {
|
||||
return phone.replace(Regex("""[\s\-()]"""), "")
|
||||
}
|
||||
|
||||
internal fun buildSendPlan(
|
||||
message: String,
|
||||
divider: (String) -> List<String>,
|
||||
@@ -112,6 +208,25 @@ class SmsManager(private val context: Context) {
|
||||
}
|
||||
return json.encodeToString(JsonObject.serializer(), JsonObject(payload))
|
||||
}
|
||||
|
||||
internal fun buildQueryPayloadJson(
|
||||
json: Json = JsonConfig,
|
||||
ok: Boolean,
|
||||
messages: List<SmsMessage>,
|
||||
error: String? = null,
|
||||
): String {
|
||||
val messagesArray = json.encodeToString(messages)
|
||||
val messagesElement = json.parseToJsonElement(messagesArray)
|
||||
val payload = mutableMapOf<String, JsonElement>(
|
||||
"ok" to JsonPrimitive(ok),
|
||||
"count" to JsonPrimitive(messages.size),
|
||||
"messages" to messagesElement
|
||||
)
|
||||
if (!ok && error != null) {
|
||||
payload["error"] = JsonPrimitive(error)
|
||||
}
|
||||
return json.encodeToString(JsonObject.serializer(), JsonObject(payload))
|
||||
}
|
||||
}
|
||||
|
||||
fun hasSmsPermission(): Boolean {
|
||||
@@ -121,10 +236,28 @@ class SmsManager(private val context: Context) {
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
|
||||
fun hasReadSmsPermission(): Boolean {
|
||||
return ContextCompat.checkSelfPermission(
|
||||
context,
|
||||
Manifest.permission.READ_SMS
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
|
||||
fun hasReadContactsPermission(): Boolean {
|
||||
return ContextCompat.checkSelfPermission(
|
||||
context,
|
||||
Manifest.permission.READ_CONTACTS
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
|
||||
fun canSendSms(): Boolean {
|
||||
return hasSmsPermission() && hasTelephonyFeature()
|
||||
}
|
||||
|
||||
fun canReadSms(): Boolean {
|
||||
return hasReadSmsPermission() && hasTelephonyFeature()
|
||||
}
|
||||
|
||||
fun hasTelephonyFeature(): Boolean {
|
||||
return context.packageManager?.hasSystemFeature(PackageManager.FEATURE_TELEPHONY) == true
|
||||
}
|
||||
@@ -208,6 +341,20 @@ class SmsManager(private val context: Context) {
|
||||
return results[Manifest.permission.SEND_SMS] == true
|
||||
}
|
||||
|
||||
private suspend fun ensureReadSmsPermission(): Boolean {
|
||||
if (hasReadSmsPermission()) return true
|
||||
val requester = permissionRequester ?: return false
|
||||
val results = requester.requestIfMissing(listOf(Manifest.permission.READ_SMS))
|
||||
return results[Manifest.permission.READ_SMS] == true
|
||||
}
|
||||
|
||||
private suspend fun ensureReadContactsPermission(): Boolean {
|
||||
if (hasReadContactsPermission()) return true
|
||||
val requester = permissionRequester ?: return false
|
||||
val results = requester.requestIfMissing(listOf(Manifest.permission.READ_CONTACTS))
|
||||
return results[Manifest.permission.READ_CONTACTS] == true
|
||||
}
|
||||
|
||||
private fun okResult(to: String, message: String): SendResult {
|
||||
return SendResult(
|
||||
ok = true,
|
||||
@@ -227,4 +374,240 @@ class SmsManager(private val context: Context) {
|
||||
payloadJson = buildPayloadJson(json = json, ok = false, to = to, error = error),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* search SMS messages with the specified parameters.
|
||||
*
|
||||
* @param paramsJson JSON with optional fields:
|
||||
* - startTime (Long): Start time in milliseconds
|
||||
* - endTime (Long): End time in milliseconds
|
||||
* - contactName (String): Contact name to search
|
||||
* - phoneNumber (String): Phone number to search (supports partial matching)
|
||||
* - keyword (String): Keyword to search in message body
|
||||
* - type (Int): SMS type (1=Inbox, 2=Sent, 3=Draft, etc.)
|
||||
* - isRead (Boolean): Read status
|
||||
* - limit (Int): Number of records to return (default: 25, range: 1-200)
|
||||
* - offset (Int): Number of records to skip (default: 0)
|
||||
* @return SearchResult containing the list of SMS messages or an error
|
||||
*/
|
||||
suspend fun search(paramsJson: String?): SearchResult = withContext(Dispatchers.IO) {
|
||||
if (!hasTelephonyFeature()) {
|
||||
return@withContext SearchResult(
|
||||
ok = false,
|
||||
messages = emptyList(),
|
||||
error = "SMS_UNAVAILABLE: telephony not available",
|
||||
payloadJson = buildQueryPayloadJson(json, ok = false, messages = emptyList(), error = "SMS_UNAVAILABLE: telephony not available")
|
||||
)
|
||||
}
|
||||
|
||||
if (!ensureReadSmsPermission()) {
|
||||
return@withContext SearchResult(
|
||||
ok = false,
|
||||
messages = emptyList(),
|
||||
error = "SMS_PERMISSION_REQUIRED: grant READ_SMS permission",
|
||||
payloadJson = buildQueryPayloadJson(json, ok = false, messages = emptyList(), error = "SMS_PERMISSION_REQUIRED: grant READ_SMS permission")
|
||||
)
|
||||
}
|
||||
|
||||
val parseResult = parseQueryParams(paramsJson, json)
|
||||
if (parseResult is QueryParseResult.Error) {
|
||||
return@withContext SearchResult(
|
||||
ok = false,
|
||||
messages = emptyList(),
|
||||
error = parseResult.error,
|
||||
payloadJson = buildQueryPayloadJson(json, ok = false, messages = emptyList(), error = parseResult.error)
|
||||
)
|
||||
}
|
||||
val params = (parseResult as QueryParseResult.Ok).params
|
||||
|
||||
return@withContext try {
|
||||
// Get phone numbers from contact name if provided
|
||||
val phoneNumbers = if (!params.contactName.isNullOrEmpty()) {
|
||||
if (!ensureReadContactsPermission()) {
|
||||
return@withContext SearchResult(
|
||||
ok = false,
|
||||
messages = emptyList(),
|
||||
error = "CONTACTS_PERMISSION_REQUIRED: grant READ_CONTACTS permission",
|
||||
payloadJson = buildQueryPayloadJson(json, ok = false, messages = emptyList(), error = "CONTACTS_PERMISSION_REQUIRED: grant READ_CONTACTS permission")
|
||||
)
|
||||
}
|
||||
getPhoneNumbersFromContactName(params.contactName)
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
|
||||
val messages = querySmsMessages(params, phoneNumbers)
|
||||
SearchResult(
|
||||
ok = true,
|
||||
messages = messages,
|
||||
error = null,
|
||||
payloadJson = buildQueryPayloadJson(json, ok = true, messages = messages)
|
||||
)
|
||||
} catch (e: SecurityException) {
|
||||
SearchResult(
|
||||
ok = false,
|
||||
messages = emptyList(),
|
||||
error = "SMS_PERMISSION_REQUIRED: ${e.message}",
|
||||
payloadJson = buildQueryPayloadJson(json, ok = false, messages = emptyList(), error = "SMS_PERMISSION_REQUIRED: ${e.message}")
|
||||
)
|
||||
} catch (e: Throwable) {
|
||||
SearchResult(
|
||||
ok = false,
|
||||
messages = emptyList(),
|
||||
error = "SMS_QUERY_FAILED: ${e.message ?: "unknown error"}",
|
||||
payloadJson = buildQueryPayloadJson(json, ok = false, messages = emptyList(), error = "SMS_QUERY_FAILED: ${e.message ?: "unknown error"}")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all phone numbers associated with a contact name
|
||||
*/
|
||||
private fun getPhoneNumbersFromContactName(contactName: String): List<String> {
|
||||
val phoneNumbers = mutableListOf<String>()
|
||||
val selection = "${ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME} LIKE ?"
|
||||
val selectionArgs = arrayOf("%$contactName%")
|
||||
|
||||
val cursor = context.contentResolver.query(
|
||||
ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
|
||||
arrayOf(ContactsContract.CommonDataKinds.Phone.NUMBER),
|
||||
selection,
|
||||
selectionArgs,
|
||||
null
|
||||
)
|
||||
|
||||
cursor?.use {
|
||||
val numberIndex = it.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER)
|
||||
while (it.moveToNext()) {
|
||||
val number = it.getString(numberIndex)
|
||||
if (!number.isNullOrBlank()) {
|
||||
phoneNumbers.add(normalizePhoneNumber(number))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return phoneNumbers
|
||||
}
|
||||
|
||||
/**
|
||||
* Query SMS messages based on the provided parameters
|
||||
*/
|
||||
private fun querySmsMessages(params: QueryParams, phoneNumbers: List<String>): List<SmsMessage> {
|
||||
val messages = mutableListOf<SmsMessage>()
|
||||
|
||||
// Build selection and selectionArgs
|
||||
val selections = mutableListOf<String>()
|
||||
val selectionArgs = mutableListOf<String>()
|
||||
|
||||
// Time range
|
||||
if (params.startTime != null) {
|
||||
selections.add("${Telephony.Sms.DATE} >= ?")
|
||||
selectionArgs.add(params.startTime.toString())
|
||||
}
|
||||
if (params.endTime != null) {
|
||||
selections.add("${Telephony.Sms.DATE} <= ?")
|
||||
selectionArgs.add(params.endTime.toString())
|
||||
}
|
||||
|
||||
// Phone numbers (from contact name or direct phone number)
|
||||
val allPhoneNumbers = if (!params.phoneNumber.isNullOrEmpty()) {
|
||||
phoneNumbers + normalizePhoneNumber(params.phoneNumber)
|
||||
} else {
|
||||
phoneNumbers
|
||||
}
|
||||
|
||||
if (allPhoneNumbers.isNotEmpty()) {
|
||||
val addressSelection = allPhoneNumbers.joinToString(" OR ") {
|
||||
"${Telephony.Sms.ADDRESS} LIKE ?"
|
||||
}
|
||||
selections.add("($addressSelection)")
|
||||
allPhoneNumbers.forEach {
|
||||
selectionArgs.add("%$it%")
|
||||
}
|
||||
}
|
||||
|
||||
// Keyword in body
|
||||
if (!params.keyword.isNullOrEmpty()) {
|
||||
selections.add("${Telephony.Sms.BODY} LIKE ?")
|
||||
selectionArgs.add("%${params.keyword}%")
|
||||
}
|
||||
|
||||
// Type
|
||||
if (params.type != null) {
|
||||
selections.add("${Telephony.Sms.TYPE} = ?")
|
||||
selectionArgs.add(params.type.toString())
|
||||
}
|
||||
|
||||
// Read status
|
||||
if (params.isRead != null) {
|
||||
selections.add("${Telephony.Sms.READ} = ?")
|
||||
selectionArgs.add(if (params.isRead) "1" else "0")
|
||||
}
|
||||
|
||||
val selection = if (selections.isNotEmpty()) {
|
||||
selections.joinToString(" AND ")
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val selectionArgsArray = if (selectionArgs.isNotEmpty()) {
|
||||
selectionArgs.toTypedArray()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
// Query SMS with SQL-level LIMIT and OFFSET to avoid loading all matching rows
|
||||
val sortOrder = "${Telephony.Sms.DATE} DESC LIMIT ${params.limit} OFFSET ${params.offset}"
|
||||
val cursor = context.contentResolver.query(
|
||||
Telephony.Sms.CONTENT_URI,
|
||||
arrayOf(
|
||||
Telephony.Sms._ID,
|
||||
Telephony.Sms.THREAD_ID,
|
||||
Telephony.Sms.ADDRESS,
|
||||
Telephony.Sms.PERSON,
|
||||
Telephony.Sms.DATE,
|
||||
Telephony.Sms.DATE_SENT,
|
||||
Telephony.Sms.READ,
|
||||
Telephony.Sms.TYPE,
|
||||
Telephony.Sms.BODY,
|
||||
Telephony.Sms.STATUS
|
||||
),
|
||||
selection,
|
||||
selectionArgsArray,
|
||||
sortOrder
|
||||
)
|
||||
|
||||
cursor?.use {
|
||||
val idIndex = it.getColumnIndex(Telephony.Sms._ID)
|
||||
val threadIdIndex = it.getColumnIndex(Telephony.Sms.THREAD_ID)
|
||||
val addressIndex = it.getColumnIndex(Telephony.Sms.ADDRESS)
|
||||
val personIndex = it.getColumnIndex(Telephony.Sms.PERSON)
|
||||
val dateIndex = it.getColumnIndex(Telephony.Sms.DATE)
|
||||
val dateSentIndex = it.getColumnIndex(Telephony.Sms.DATE_SENT)
|
||||
val readIndex = it.getColumnIndex(Telephony.Sms.READ)
|
||||
val typeIndex = it.getColumnIndex(Telephony.Sms.TYPE)
|
||||
val bodyIndex = it.getColumnIndex(Telephony.Sms.BODY)
|
||||
val statusIndex = it.getColumnIndex(Telephony.Sms.STATUS)
|
||||
|
||||
var count = 0
|
||||
while (it.moveToNext() && count < params.limit) {
|
||||
val message = SmsMessage(
|
||||
id = it.getLong(idIndex),
|
||||
threadId = it.getLong(threadIdIndex),
|
||||
address = it.getString(addressIndex),
|
||||
person = it.getString(personIndex),
|
||||
date = it.getLong(dateIndex),
|
||||
dateSent = it.getLong(dateSentIndex),
|
||||
read = it.getInt(readIndex) == 1,
|
||||
type = it.getInt(typeIndex),
|
||||
body = it.getString(bodyIndex),
|
||||
status = it.getInt(statusIndex)
|
||||
)
|
||||
messages.add(message)
|
||||
count++
|
||||
}
|
||||
}
|
||||
|
||||
return messages
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ enum class OpenClawCameraCommand(val rawValue: String) {
|
||||
|
||||
enum class OpenClawSmsCommand(val rawValue: String) {
|
||||
Send("sms.send"),
|
||||
Search("sms.search"),
|
||||
;
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -287,7 +287,11 @@ fun OnboardingFlow(viewModel: MainViewModel, modifier: Modifier = Modifier) {
|
||||
}
|
||||
var enableSms by
|
||||
rememberSaveable {
|
||||
mutableStateOf(smsAvailable && isPermissionGranted(context, Manifest.permission.SEND_SMS))
|
||||
mutableStateOf(
|
||||
smsAvailable &&
|
||||
isPermissionGranted(context, Manifest.permission.SEND_SMS) &&
|
||||
isPermissionGranted(context, Manifest.permission.READ_SMS)
|
||||
)
|
||||
}
|
||||
var enableCallLog by
|
||||
rememberSaveable {
|
||||
@@ -336,7 +340,9 @@ fun OnboardingFlow(viewModel: MainViewModel, modifier: Modifier = Modifier) {
|
||||
!motionPermissionRequired ||
|
||||
isPermissionGranted(context, Manifest.permission.ACTIVITY_RECOGNITION)
|
||||
PermissionToggle.Sms ->
|
||||
!smsAvailable || isPermissionGranted(context, Manifest.permission.SEND_SMS)
|
||||
!smsAvailable ||
|
||||
(isPermissionGranted(context, Manifest.permission.SEND_SMS) &&
|
||||
isPermissionGranted(context, Manifest.permission.READ_SMS))
|
||||
PermissionToggle.CallLog -> isPermissionGranted(context, Manifest.permission.READ_CALL_LOG)
|
||||
}
|
||||
|
||||
@@ -698,7 +704,7 @@ fun OnboardingFlow(viewModel: MainViewModel, modifier: Modifier = Modifier) {
|
||||
requestPermissionToggle(
|
||||
PermissionToggle.Sms,
|
||||
checked,
|
||||
listOf(Manifest.permission.SEND_SMS),
|
||||
listOf(Manifest.permission.SEND_SMS, Manifest.permission.READ_SMS),
|
||||
)
|
||||
}
|
||||
},
|
||||
@@ -1437,9 +1443,11 @@ private fun PermissionsStep(
|
||||
InlineDivider()
|
||||
PermissionToggleRow(
|
||||
title = "SMS",
|
||||
subtitle = "Send text messages via the gateway",
|
||||
subtitle = "Send and search text messages via the gateway",
|
||||
checked = enableSms,
|
||||
granted = isPermissionGranted(context, Manifest.permission.SEND_SMS),
|
||||
granted =
|
||||
isPermissionGranted(context, Manifest.permission.SEND_SMS) &&
|
||||
isPermissionGranted(context, Manifest.permission.READ_SMS),
|
||||
onCheckedChange = onSmsChange,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -247,12 +247,16 @@ fun SettingsSheet(viewModel: MainViewModel) {
|
||||
remember {
|
||||
mutableStateOf(
|
||||
ContextCompat.checkSelfPermission(context, Manifest.permission.SEND_SMS) ==
|
||||
PackageManager.PERMISSION_GRANTED &&
|
||||
ContextCompat.checkSelfPermission(context, Manifest.permission.READ_SMS) ==
|
||||
PackageManager.PERMISSION_GRANTED,
|
||||
)
|
||||
}
|
||||
val smsPermissionLauncher =
|
||||
rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
|
||||
smsPermissionGranted = granted
|
||||
rememberLauncherForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { perms ->
|
||||
val sendOk = perms[Manifest.permission.SEND_SMS] == true
|
||||
val readOk = perms[Manifest.permission.READ_SMS] == true
|
||||
smsPermissionGranted = sendOk && readOk
|
||||
viewModel.refreshGatewayConnection()
|
||||
}
|
||||
|
||||
@@ -287,6 +291,8 @@ fun SettingsSheet(viewModel: MainViewModel) {
|
||||
PackageManager.PERMISSION_GRANTED
|
||||
smsPermissionGranted =
|
||||
ContextCompat.checkSelfPermission(context, Manifest.permission.SEND_SMS) ==
|
||||
PackageManager.PERMISSION_GRANTED &&
|
||||
ContextCompat.checkSelfPermission(context, Manifest.permission.READ_SMS) ==
|
||||
PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
}
|
||||
@@ -507,7 +513,7 @@ fun SettingsSheet(viewModel: MainViewModel) {
|
||||
colors = listItemColors,
|
||||
headlineContent = { Text("SMS", style = mobileHeadline) },
|
||||
supportingContent = {
|
||||
Text("Send SMS from this device.", style = mobileCallout)
|
||||
Text("Send and search SMS from this device.", style = mobileCallout)
|
||||
},
|
||||
trailingContent = {
|
||||
Button(
|
||||
@@ -515,7 +521,7 @@ fun SettingsSheet(viewModel: MainViewModel) {
|
||||
if (smsPermissionGranted) {
|
||||
openAppSettings(context)
|
||||
} else {
|
||||
smsPermissionLauncher.launch(Manifest.permission.SEND_SMS)
|
||||
smsPermissionLauncher.launch(arrayOf(Manifest.permission.SEND_SMS, Manifest.permission.READ_SMS))
|
||||
}
|
||||
},
|
||||
colors = settingsPrimaryButtonColors(),
|
||||
|
||||
@@ -64,6 +64,7 @@ class InvokeCommandRegistryTest {
|
||||
OpenClawMotionCommand.Activity.rawValue,
|
||||
OpenClawMotionCommand.Pedometer.rawValue,
|
||||
OpenClawSmsCommand.Send.rawValue,
|
||||
OpenClawSmsCommand.Search.rawValue,
|
||||
)
|
||||
|
||||
private val debugCommands = setOf("debug.logs", "debug.ed25519")
|
||||
@@ -83,7 +84,8 @@ class InvokeCommandRegistryTest {
|
||||
defaultFlags(
|
||||
cameraEnabled = true,
|
||||
locationEnabled = true,
|
||||
smsAvailable = true,
|
||||
sendSmsAvailable = true,
|
||||
readSmsAvailable = true,
|
||||
voiceWakeEnabled = true,
|
||||
motionActivityAvailable = true,
|
||||
motionPedometerAvailable = true,
|
||||
@@ -108,7 +110,8 @@ class InvokeCommandRegistryTest {
|
||||
defaultFlags(
|
||||
cameraEnabled = true,
|
||||
locationEnabled = true,
|
||||
smsAvailable = true,
|
||||
sendSmsAvailable = true,
|
||||
readSmsAvailable = true,
|
||||
motionActivityAvailable = true,
|
||||
motionPedometerAvailable = true,
|
||||
debugBuild = true,
|
||||
@@ -125,7 +128,8 @@ class InvokeCommandRegistryTest {
|
||||
NodeRuntimeFlags(
|
||||
cameraEnabled = false,
|
||||
locationEnabled = false,
|
||||
smsAvailable = false,
|
||||
sendSmsAvailable = false,
|
||||
readSmsAvailable = false,
|
||||
voiceWakeEnabled = false,
|
||||
motionActivityAvailable = true,
|
||||
motionPedometerAvailable = false,
|
||||
@@ -137,10 +141,43 @@ class InvokeCommandRegistryTest {
|
||||
assertFalse(commands.contains(OpenClawMotionCommand.Pedometer.rawValue))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun advertisedCommands_splitsSmsSendAndSearchAvailability() {
|
||||
val readOnlyCommands =
|
||||
InvokeCommandRegistry.advertisedCommands(
|
||||
defaultFlags(readSmsAvailable = true),
|
||||
)
|
||||
val sendOnlyCommands =
|
||||
InvokeCommandRegistry.advertisedCommands(
|
||||
defaultFlags(sendSmsAvailable = true),
|
||||
)
|
||||
|
||||
assertTrue(readOnlyCommands.contains(OpenClawSmsCommand.Search.rawValue))
|
||||
assertFalse(readOnlyCommands.contains(OpenClawSmsCommand.Send.rawValue))
|
||||
assertTrue(sendOnlyCommands.contains(OpenClawSmsCommand.Send.rawValue))
|
||||
assertFalse(sendOnlyCommands.contains(OpenClawSmsCommand.Search.rawValue))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun advertisedCapabilities_includeSmsWhenEitherSmsPathIsAvailable() {
|
||||
val readOnlyCapabilities =
|
||||
InvokeCommandRegistry.advertisedCapabilities(
|
||||
defaultFlags(readSmsAvailable = true),
|
||||
)
|
||||
val sendOnlyCapabilities =
|
||||
InvokeCommandRegistry.advertisedCapabilities(
|
||||
defaultFlags(sendSmsAvailable = true),
|
||||
)
|
||||
|
||||
assertTrue(readOnlyCapabilities.contains(OpenClawCapability.Sms.rawValue))
|
||||
assertTrue(sendOnlyCapabilities.contains(OpenClawCapability.Sms.rawValue))
|
||||
}
|
||||
|
||||
private fun defaultFlags(
|
||||
cameraEnabled: Boolean = false,
|
||||
locationEnabled: Boolean = false,
|
||||
smsAvailable: Boolean = false,
|
||||
sendSmsAvailable: Boolean = false,
|
||||
readSmsAvailable: Boolean = false,
|
||||
voiceWakeEnabled: Boolean = false,
|
||||
motionActivityAvailable: Boolean = false,
|
||||
motionPedometerAvailable: Boolean = false,
|
||||
@@ -149,7 +186,8 @@ class InvokeCommandRegistryTest {
|
||||
NodeRuntimeFlags(
|
||||
cameraEnabled = cameraEnabled,
|
||||
locationEnabled = locationEnabled,
|
||||
smsAvailable = smsAvailable,
|
||||
sendSmsAvailable = sendSmsAvailable,
|
||||
readSmsAvailable = readSmsAvailable,
|
||||
voiceWakeEnabled = voiceWakeEnabled,
|
||||
motionActivityAvailable = motionActivityAvailable,
|
||||
motionPedometerAvailable = motionPedometerAvailable,
|
||||
|
||||
@@ -88,4 +88,95 @@ class SmsManagerTest {
|
||||
assertFalse(plan.useMultipart)
|
||||
assertEquals(listOf("hello"), plan.parts)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parseQueryParamsAcceptsEmptyPayload() {
|
||||
val result = SmsManager.parseQueryParams(null, json)
|
||||
assertTrue(result is SmsManager.QueryParseResult.Ok)
|
||||
val ok = result as SmsManager.QueryParseResult.Ok
|
||||
assertEquals(25, ok.params.limit)
|
||||
assertEquals(0, ok.params.offset)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parseQueryParamsRejectsInvalidJson() {
|
||||
val result = SmsManager.parseQueryParams("not-json", json)
|
||||
assertTrue(result is SmsManager.QueryParseResult.Error)
|
||||
val error = result as SmsManager.QueryParseResult.Error
|
||||
assertEquals("INVALID_REQUEST: expected JSON object", error.error)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parseQueryParamsRejectsNonObjectJson() {
|
||||
val result = SmsManager.parseQueryParams("[]", json)
|
||||
assertTrue(result is SmsManager.QueryParseResult.Error)
|
||||
val error = result as SmsManager.QueryParseResult.Error
|
||||
assertEquals("INVALID_REQUEST: expected JSON object", error.error)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parseQueryParamsParsesLimitAndOffset() {
|
||||
val result = SmsManager.parseQueryParams("{\"limit\":10,\"offset\":5}", json)
|
||||
assertTrue(result is SmsManager.QueryParseResult.Ok)
|
||||
val ok = result as SmsManager.QueryParseResult.Ok
|
||||
assertEquals(10, ok.params.limit)
|
||||
assertEquals(5, ok.params.offset)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parseQueryParamsClampsLimitRange() {
|
||||
val result = SmsManager.parseQueryParams("{\"limit\":300}", json)
|
||||
assertTrue(result is SmsManager.QueryParseResult.Ok)
|
||||
val ok = result as SmsManager.QueryParseResult.Ok
|
||||
assertEquals(200, ok.params.limit)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parseQueryParamsParsesPhoneNumber() {
|
||||
val result = SmsManager.parseQueryParams("{\"phoneNumber\":\"+1234567890\"}", json)
|
||||
assertTrue(result is SmsManager.QueryParseResult.Ok)
|
||||
val ok = result as SmsManager.QueryParseResult.Ok
|
||||
assertEquals("+1234567890", ok.params.phoneNumber)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parseQueryParamsParsesContactName() {
|
||||
val result = SmsManager.parseQueryParams("{\"contactName\":\"lixuankai\"}", json)
|
||||
assertTrue(result is SmsManager.QueryParseResult.Ok)
|
||||
val ok = result as SmsManager.QueryParseResult.Ok
|
||||
assertEquals("lixuankai", ok.params.contactName)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parseQueryParamsParsesKeyword() {
|
||||
val result = SmsManager.parseQueryParams("{\"keyword\":\"test\"}", json)
|
||||
assertTrue(result is SmsManager.QueryParseResult.Ok)
|
||||
val ok = result as SmsManager.QueryParseResult.Ok
|
||||
assertEquals("test", ok.params.keyword)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parseQueryParamsParsesTimeRange() {
|
||||
val result = SmsManager.parseQueryParams("{\"startTime\":1000,\"endTime\":2000}", json)
|
||||
assertTrue(result is SmsManager.QueryParseResult.Ok)
|
||||
val ok = result as SmsManager.QueryParseResult.Ok
|
||||
assertEquals(1000L, ok.params.startTime)
|
||||
assertEquals(2000L, ok.params.endTime)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parseQueryParamsParsesType() {
|
||||
val result = SmsManager.parseQueryParams("{\"type\":1}", json)
|
||||
assertTrue(result is SmsManager.QueryParseResult.Ok)
|
||||
val ok = result as SmsManager.QueryParseResult.Ok
|
||||
assertEquals(1, ok.params.type)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parseQueryParamsParsesReadStatus() {
|
||||
val result = SmsManager.parseQueryParams("{\"isRead\":true}", json)
|
||||
assertTrue(result is SmsManager.QueryParseResult.Ok)
|
||||
val ok = result as SmsManager.QueryParseResult.Ok
|
||||
assertEquals(true, ok.params.isRead)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,4 +90,9 @@ class OpenClawProtocolConstantsTest {
|
||||
fun callLogCommandsUseStableStrings() {
|
||||
assertEquals("callLog.search", OpenClawCallLogCommand.Search.rawValue)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun smsCommandsUseStableStrings() {
|
||||
assertEquals("sms.search", OpenClawSmsCommand.Search.rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1326,6 +1326,124 @@ public struct SessionsResolveParams: Codable, Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
public struct SessionsCreateParams: Codable, Sendable {
|
||||
public let key: String?
|
||||
public let agentid: String?
|
||||
public let label: String?
|
||||
public let model: String?
|
||||
public let parentsessionkey: String?
|
||||
public let task: String?
|
||||
public let message: String?
|
||||
|
||||
public init(
|
||||
key: String?,
|
||||
agentid: String?,
|
||||
label: String?,
|
||||
model: String?,
|
||||
parentsessionkey: String?,
|
||||
task: String?,
|
||||
message: String?)
|
||||
{
|
||||
self.key = key
|
||||
self.agentid = agentid
|
||||
self.label = label
|
||||
self.model = model
|
||||
self.parentsessionkey = parentsessionkey
|
||||
self.task = task
|
||||
self.message = message
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case key
|
||||
case agentid = "agentId"
|
||||
case label
|
||||
case model
|
||||
case parentsessionkey = "parentSessionKey"
|
||||
case task
|
||||
case message
|
||||
}
|
||||
}
|
||||
|
||||
public struct SessionsSendParams: Codable, Sendable {
|
||||
public let key: String
|
||||
public let message: String
|
||||
public let thinking: String?
|
||||
public let attachments: [AnyCodable]?
|
||||
public let timeoutms: Int?
|
||||
public let idempotencykey: String?
|
||||
|
||||
public init(
|
||||
key: String,
|
||||
message: String,
|
||||
thinking: String?,
|
||||
attachments: [AnyCodable]?,
|
||||
timeoutms: Int?,
|
||||
idempotencykey: String?)
|
||||
{
|
||||
self.key = key
|
||||
self.message = message
|
||||
self.thinking = thinking
|
||||
self.attachments = attachments
|
||||
self.timeoutms = timeoutms
|
||||
self.idempotencykey = idempotencykey
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case key
|
||||
case message
|
||||
case thinking
|
||||
case attachments
|
||||
case timeoutms = "timeoutMs"
|
||||
case idempotencykey = "idempotencyKey"
|
||||
}
|
||||
}
|
||||
|
||||
public struct SessionsMessagesSubscribeParams: Codable, Sendable {
|
||||
public let key: String
|
||||
|
||||
public init(
|
||||
key: String)
|
||||
{
|
||||
self.key = key
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case key
|
||||
}
|
||||
}
|
||||
|
||||
public struct SessionsMessagesUnsubscribeParams: Codable, Sendable {
|
||||
public let key: String
|
||||
|
||||
public init(
|
||||
key: String)
|
||||
{
|
||||
self.key = key
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case key
|
||||
}
|
||||
}
|
||||
|
||||
public struct SessionsAbortParams: Codable, Sendable {
|
||||
public let key: String
|
||||
public let runid: String?
|
||||
|
||||
public init(
|
||||
key: String,
|
||||
runid: String?)
|
||||
{
|
||||
self.key = key
|
||||
self.runid = runid
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case key
|
||||
case runid = "runId"
|
||||
}
|
||||
}
|
||||
|
||||
public struct SessionsPatchParams: Codable, Sendable {
|
||||
public let key: String
|
||||
public let label: AnyCodable?
|
||||
|
||||
@@ -1326,6 +1326,124 @@ public struct SessionsResolveParams: Codable, Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
public struct SessionsCreateParams: Codable, Sendable {
|
||||
public let key: String?
|
||||
public let agentid: String?
|
||||
public let label: String?
|
||||
public let model: String?
|
||||
public let parentsessionkey: String?
|
||||
public let task: String?
|
||||
public let message: String?
|
||||
|
||||
public init(
|
||||
key: String?,
|
||||
agentid: String?,
|
||||
label: String?,
|
||||
model: String?,
|
||||
parentsessionkey: String?,
|
||||
task: String?,
|
||||
message: String?)
|
||||
{
|
||||
self.key = key
|
||||
self.agentid = agentid
|
||||
self.label = label
|
||||
self.model = model
|
||||
self.parentsessionkey = parentsessionkey
|
||||
self.task = task
|
||||
self.message = message
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case key
|
||||
case agentid = "agentId"
|
||||
case label
|
||||
case model
|
||||
case parentsessionkey = "parentSessionKey"
|
||||
case task
|
||||
case message
|
||||
}
|
||||
}
|
||||
|
||||
public struct SessionsSendParams: Codable, Sendable {
|
||||
public let key: String
|
||||
public let message: String
|
||||
public let thinking: String?
|
||||
public let attachments: [AnyCodable]?
|
||||
public let timeoutms: Int?
|
||||
public let idempotencykey: String?
|
||||
|
||||
public init(
|
||||
key: String,
|
||||
message: String,
|
||||
thinking: String?,
|
||||
attachments: [AnyCodable]?,
|
||||
timeoutms: Int?,
|
||||
idempotencykey: String?)
|
||||
{
|
||||
self.key = key
|
||||
self.message = message
|
||||
self.thinking = thinking
|
||||
self.attachments = attachments
|
||||
self.timeoutms = timeoutms
|
||||
self.idempotencykey = idempotencykey
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case key
|
||||
case message
|
||||
case thinking
|
||||
case attachments
|
||||
case timeoutms = "timeoutMs"
|
||||
case idempotencykey = "idempotencyKey"
|
||||
}
|
||||
}
|
||||
|
||||
public struct SessionsMessagesSubscribeParams: Codable, Sendable {
|
||||
public let key: String
|
||||
|
||||
public init(
|
||||
key: String)
|
||||
{
|
||||
self.key = key
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case key
|
||||
}
|
||||
}
|
||||
|
||||
public struct SessionsMessagesUnsubscribeParams: Codable, Sendable {
|
||||
public let key: String
|
||||
|
||||
public init(
|
||||
key: String)
|
||||
{
|
||||
self.key = key
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case key
|
||||
}
|
||||
}
|
||||
|
||||
public struct SessionsAbortParams: Codable, Sendable {
|
||||
public let key: String
|
||||
public let runid: String?
|
||||
|
||||
public init(
|
||||
key: String,
|
||||
runid: String?)
|
||||
{
|
||||
self.key = key
|
||||
self.runid = runid
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case key
|
||||
case runid = "runId"
|
||||
}
|
||||
}
|
||||
|
||||
public struct SessionsPatchParams: Codable, Sendable {
|
||||
public let key: String
|
||||
public let label: AnyCodable?
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
- tests: align OpenAI Codex auth login expectations with the `gpt-5.4` default model to prevent stale CI failures. (#44367) thanks @jrrcdev
|
||||
@@ -0,0 +1,3 @@
|
||||
### Fixes
|
||||
|
||||
- Gateway/session history: return `404` for unknown session history lookups, unsubscribe session lifecycle listeners during shutdown, add coverage for the new transcript and lifecycle helpers, and tighten session history plus live transcript tests so the Control UI session surfaces stay stable under restart and follow mode.
|
||||
@@ -1 +0,0 @@
|
||||
- runner: infer canonical tool names from malformed `toolCallId` variants (e.g. `functionsread3`, `functionswrite4`) when allowlist is present, preventing `Tool not found` regressions in strict routers.
|
||||
@@ -15230,7 +15230,7 @@
|
||||
"network"
|
||||
],
|
||||
"label": "Feishu",
|
||||
"help": "飞书/Lark enterprise messaging.",
|
||||
"help": "飞书/Lark enterprise messaging with doc/wiki/drive tools.",
|
||||
"hasChildren": true
|
||||
},
|
||||
{
|
||||
@@ -17232,7 +17232,7 @@
|
||||
"network"
|
||||
],
|
||||
"label": "Google Chat",
|
||||
"help": "Google Workspace Chat app with HTTP webhook.",
|
||||
"help": "Google Workspace Chat app via HTTP webhooks.",
|
||||
"hasChildren": true
|
||||
},
|
||||
{
|
||||
@@ -22069,7 +22069,7 @@
|
||||
"network"
|
||||
],
|
||||
"label": "Matrix",
|
||||
"help": "open protocol; configure a homeserver + access token.",
|
||||
"help": "open protocol; install the plugin to enable.",
|
||||
"hasChildren": true
|
||||
},
|
||||
{
|
||||
@@ -26190,7 +26190,7 @@
|
||||
"network"
|
||||
],
|
||||
"label": "Nostr",
|
||||
"help": "Decentralized DMs via Nostr relays (NIP-04)",
|
||||
"help": "Decentralized protocol; encrypted DMs via NIP-04.",
|
||||
"hasChildren": true
|
||||
},
|
||||
{
|
||||
@@ -30798,7 +30798,7 @@
|
||||
"network"
|
||||
],
|
||||
"label": "Synology Chat",
|
||||
"help": "Connect your Synology NAS Chat to OpenClaw",
|
||||
"help": "Connect your Synology NAS Chat to OpenClaw with full agent capabilities.",
|
||||
"hasChildren": true
|
||||
},
|
||||
{
|
||||
@@ -34814,7 +34814,7 @@
|
||||
"network"
|
||||
],
|
||||
"label": "Tlon",
|
||||
"help": "Decentralized messaging on Urbit",
|
||||
"help": "decentralized messaging on Urbit; install the plugin to enable.",
|
||||
"hasChildren": true
|
||||
},
|
||||
{
|
||||
@@ -44903,6 +44903,16 @@
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "models.providers.*.models.*.compat.nativeWebSearchTool",
|
||||
"kind": "core",
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "models.providers.*.models.*.compat.requiresAssistantAfterToolResult",
|
||||
"kind": "core",
|
||||
@@ -45023,6 +45033,26 @@
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "models.providers.*.models.*.compat.toolCallArgumentsEncoding",
|
||||
"kind": "core",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "models.providers.*.models.*.compat.toolSchemaProfile",
|
||||
"kind": "core",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "models.providers.*.models.*.contextWindow",
|
||||
"kind": "core",
|
||||
@@ -46155,6 +46185,52 @@
|
||||
],
|
||||
"label": "@openclaw/brave-plugin Config",
|
||||
"help": "Plugin-defined config payload for brave.",
|
||||
"hasChildren": true
|
||||
},
|
||||
{
|
||||
"path": "plugins.entries.brave.config.webSearch",
|
||||
"kind": "plugin",
|
||||
"type": "object",
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": true
|
||||
},
|
||||
{
|
||||
"path": "plugins.entries.brave.config.webSearch.apiKey",
|
||||
"kind": "plugin",
|
||||
"type": [
|
||||
"object",
|
||||
"string"
|
||||
],
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": true,
|
||||
"tags": [
|
||||
"auth",
|
||||
"security"
|
||||
],
|
||||
"label": "Brave Search API Key",
|
||||
"help": "Brave Search API key (fallback: BRAVE_API_KEY env var).",
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "plugins.entries.brave.config.webSearch.mode",
|
||||
"kind": "plugin",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"enumValues": [
|
||||
"web",
|
||||
"llm-context"
|
||||
],
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [
|
||||
"advanced"
|
||||
],
|
||||
"label": "Brave Search Mode",
|
||||
"help": "Brave Search mode: web or llm-context.",
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
@@ -47690,6 +47766,127 @@
|
||||
"help": "Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.",
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "plugins.entries.fal",
|
||||
"kind": "plugin",
|
||||
"type": "object",
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [
|
||||
"advanced"
|
||||
],
|
||||
"label": "@openclaw/fal-provider",
|
||||
"help": "OpenClaw fal provider plugin (plugin: fal)",
|
||||
"hasChildren": true
|
||||
},
|
||||
{
|
||||
"path": "plugins.entries.fal.config",
|
||||
"kind": "plugin",
|
||||
"type": "object",
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [
|
||||
"advanced"
|
||||
],
|
||||
"label": "@openclaw/fal-provider Config",
|
||||
"help": "Plugin-defined config payload for fal.",
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "plugins.entries.fal.enabled",
|
||||
"kind": "plugin",
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [
|
||||
"advanced"
|
||||
],
|
||||
"label": "Enable @openclaw/fal-provider",
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "plugins.entries.fal.hooks",
|
||||
"kind": "plugin",
|
||||
"type": "object",
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [
|
||||
"advanced"
|
||||
],
|
||||
"label": "Plugin Hook Policy",
|
||||
"help": "Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.",
|
||||
"hasChildren": true
|
||||
},
|
||||
{
|
||||
"path": "plugins.entries.fal.hooks.allowPromptInjection",
|
||||
"kind": "plugin",
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [
|
||||
"access"
|
||||
],
|
||||
"label": "Allow Prompt Injection Hooks",
|
||||
"help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.",
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "plugins.entries.fal.subagent",
|
||||
"kind": "plugin",
|
||||
"type": "object",
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [
|
||||
"advanced"
|
||||
],
|
||||
"label": "Plugin Subagent Policy",
|
||||
"help": "Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.",
|
||||
"hasChildren": true
|
||||
},
|
||||
{
|
||||
"path": "plugins.entries.fal.subagent.allowedModels",
|
||||
"kind": "plugin",
|
||||
"type": "array",
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [
|
||||
"access"
|
||||
],
|
||||
"label": "Plugin Subagent Allowed Models",
|
||||
"help": "Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.",
|
||||
"hasChildren": true
|
||||
},
|
||||
{
|
||||
"path": "plugins.entries.fal.subagent.allowedModels.*",
|
||||
"kind": "plugin",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "plugins.entries.fal.subagent.allowModelOverride",
|
||||
"kind": "plugin",
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [
|
||||
"access"
|
||||
],
|
||||
"label": "Allow Plugin Subagent Model Override",
|
||||
"help": "Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.",
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "plugins.entries.feishu",
|
||||
"kind": "plugin",
|
||||
@@ -47837,6 +48034,48 @@
|
||||
],
|
||||
"label": "@openclaw/firecrawl-plugin Config",
|
||||
"help": "Plugin-defined config payload for firecrawl.",
|
||||
"hasChildren": true
|
||||
},
|
||||
{
|
||||
"path": "plugins.entries.firecrawl.config.webSearch",
|
||||
"kind": "plugin",
|
||||
"type": "object",
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": true
|
||||
},
|
||||
{
|
||||
"path": "plugins.entries.firecrawl.config.webSearch.apiKey",
|
||||
"kind": "plugin",
|
||||
"type": [
|
||||
"object",
|
||||
"string"
|
||||
],
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": true,
|
||||
"tags": [
|
||||
"auth",
|
||||
"security"
|
||||
],
|
||||
"label": "Firecrawl Search API Key",
|
||||
"help": "Firecrawl API key for web search (fallback: FIRECRAWL_API_KEY env var).",
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "plugins.entries.firecrawl.config.webSearch.baseUrl",
|
||||
"kind": "plugin",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [
|
||||
"advanced"
|
||||
],
|
||||
"label": "Firecrawl Search Base URL",
|
||||
"help": "Firecrawl Search base URL override.",
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
@@ -48079,6 +48318,48 @@
|
||||
],
|
||||
"label": "@openclaw/google-plugin Config",
|
||||
"help": "Plugin-defined config payload for google.",
|
||||
"hasChildren": true
|
||||
},
|
||||
{
|
||||
"path": "plugins.entries.google.config.webSearch",
|
||||
"kind": "plugin",
|
||||
"type": "object",
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": true
|
||||
},
|
||||
{
|
||||
"path": "plugins.entries.google.config.webSearch.apiKey",
|
||||
"kind": "plugin",
|
||||
"type": [
|
||||
"object",
|
||||
"string"
|
||||
],
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": true,
|
||||
"tags": [
|
||||
"auth",
|
||||
"security"
|
||||
],
|
||||
"label": "Gemini Search API Key",
|
||||
"help": "Gemini API key for Google Search grounding (fallback: GEMINI_API_KEY env var).",
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "plugins.entries.google.config.webSearch.model",
|
||||
"kind": "plugin",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [
|
||||
"models"
|
||||
],
|
||||
"label": "Gemini Search Model",
|
||||
"help": "Gemini model override for web search grounding.",
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
@@ -50456,6 +50737,62 @@
|
||||
],
|
||||
"label": "@openclaw/moonshot-provider Config",
|
||||
"help": "Plugin-defined config payload for moonshot.",
|
||||
"hasChildren": true
|
||||
},
|
||||
{
|
||||
"path": "plugins.entries.moonshot.config.webSearch",
|
||||
"kind": "plugin",
|
||||
"type": "object",
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": true
|
||||
},
|
||||
{
|
||||
"path": "plugins.entries.moonshot.config.webSearch.apiKey",
|
||||
"kind": "plugin",
|
||||
"type": [
|
||||
"object",
|
||||
"string"
|
||||
],
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": true,
|
||||
"tags": [
|
||||
"auth",
|
||||
"security"
|
||||
],
|
||||
"label": "Kimi Search API Key",
|
||||
"help": "Moonshot/Kimi API key (fallback: KIMI_API_KEY or MOONSHOT_API_KEY env var).",
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "plugins.entries.moonshot.config.webSearch.baseUrl",
|
||||
"kind": "plugin",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [
|
||||
"advanced"
|
||||
],
|
||||
"label": "Kimi Search Base URL",
|
||||
"help": "Kimi base URL override.",
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "plugins.entries.moonshot.config.webSearch.model",
|
||||
"kind": "plugin",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [
|
||||
"models"
|
||||
],
|
||||
"label": "Kimi Search Model",
|
||||
"help": "Kimi model override.",
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
@@ -52075,6 +52412,62 @@
|
||||
],
|
||||
"label": "@openclaw/perplexity-plugin Config",
|
||||
"help": "Plugin-defined config payload for perplexity.",
|
||||
"hasChildren": true
|
||||
},
|
||||
{
|
||||
"path": "plugins.entries.perplexity.config.webSearch",
|
||||
"kind": "plugin",
|
||||
"type": "object",
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": true
|
||||
},
|
||||
{
|
||||
"path": "plugins.entries.perplexity.config.webSearch.apiKey",
|
||||
"kind": "plugin",
|
||||
"type": [
|
||||
"object",
|
||||
"string"
|
||||
],
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": true,
|
||||
"tags": [
|
||||
"auth",
|
||||
"security"
|
||||
],
|
||||
"label": "Perplexity API Key",
|
||||
"help": "Perplexity or OpenRouter API key for web search.",
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "plugins.entries.perplexity.config.webSearch.baseUrl",
|
||||
"kind": "plugin",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [
|
||||
"advanced"
|
||||
],
|
||||
"label": "Perplexity Base URL",
|
||||
"help": "Optional Perplexity/OpenRouter chat-completions base URL override.",
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "plugins.entries.perplexity.config.webSearch.model",
|
||||
"kind": "plugin",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [
|
||||
"models"
|
||||
],
|
||||
"label": "Perplexity Model",
|
||||
"help": "Optional Sonar/OpenRouter model override.",
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
@@ -56010,6 +56403,62 @@
|
||||
],
|
||||
"label": "@openclaw/xai-plugin Config",
|
||||
"help": "Plugin-defined config payload for xai.",
|
||||
"hasChildren": true
|
||||
},
|
||||
{
|
||||
"path": "plugins.entries.xai.config.webSearch",
|
||||
"kind": "plugin",
|
||||
"type": "object",
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": true
|
||||
},
|
||||
{
|
||||
"path": "plugins.entries.xai.config.webSearch.apiKey",
|
||||
"kind": "plugin",
|
||||
"type": [
|
||||
"object",
|
||||
"string"
|
||||
],
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": true,
|
||||
"tags": [
|
||||
"auth",
|
||||
"security"
|
||||
],
|
||||
"label": "Grok Search API Key",
|
||||
"help": "xAI API key for Grok web search (fallback: XAI_API_KEY env var).",
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "plugins.entries.xai.config.webSearch.inlineCitations",
|
||||
"kind": "plugin",
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [
|
||||
"advanced"
|
||||
],
|
||||
"label": "Inline Citations",
|
||||
"help": "Include inline markdown citations in Grok responses.",
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "plugins.entries.xai.config.webSearch.model",
|
||||
"kind": "plugin",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [
|
||||
"models"
|
||||
],
|
||||
"label": "Grok Search Model",
|
||||
"help": "Grok model override for web search.",
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
@@ -62780,8 +63229,6 @@
|
||||
"security",
|
||||
"tools"
|
||||
],
|
||||
"label": "Brave Search API Key",
|
||||
"help": "Brave Search API key (fallback: BRAVE_API_KEY env var).",
|
||||
"hasChildren": true
|
||||
},
|
||||
{
|
||||
@@ -62824,6 +63271,63 @@
|
||||
"tags": [],
|
||||
"hasChildren": true
|
||||
},
|
||||
{
|
||||
"path": "tools.web.search.brave.apiKey",
|
||||
"kind": "core",
|
||||
"type": [
|
||||
"object",
|
||||
"string"
|
||||
],
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": true,
|
||||
"tags": [
|
||||
"auth",
|
||||
"security",
|
||||
"tools"
|
||||
],
|
||||
"hasChildren": true
|
||||
},
|
||||
{
|
||||
"path": "tools.web.search.brave.apiKey.id",
|
||||
"kind": "core",
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "tools.web.search.brave.apiKey.provider",
|
||||
"kind": "core",
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "tools.web.search.brave.apiKey.source",
|
||||
"kind": "core",
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "tools.web.search.brave.baseUrl",
|
||||
"kind": "core",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "tools.web.search.brave.mode",
|
||||
"kind": "core",
|
||||
@@ -62831,11 +63335,17 @@
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [
|
||||
"tools"
|
||||
],
|
||||
"label": "Brave Search Mode",
|
||||
"help": "Brave Search mode: \"web\" (URL results) or \"llm-context\" (pre-extracted page content for LLM grounding).",
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "tools.web.search.brave.model",
|
||||
"kind": "core",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
@@ -62893,8 +63403,6 @@
|
||||
"security",
|
||||
"tools"
|
||||
],
|
||||
"label": "Firecrawl Search API Key",
|
||||
"help": "Firecrawl API key for web search (fallback: FIRECRAWL_API_KEY env var).",
|
||||
"hasChildren": true
|
||||
},
|
||||
{
|
||||
@@ -62934,11 +63442,17 @@
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [
|
||||
"tools"
|
||||
],
|
||||
"label": "Firecrawl Search Base URL",
|
||||
"help": "Firecrawl Search base URL override (default: \"https://api.firecrawl.dev\").",
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "tools.web.search.firecrawl.model",
|
||||
"kind": "core",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
@@ -62966,8 +63480,6 @@
|
||||
"security",
|
||||
"tools"
|
||||
],
|
||||
"label": "Gemini Search API Key",
|
||||
"help": "Gemini API key for Google Search grounding (fallback: GEMINI_API_KEY env var).",
|
||||
"hasChildren": true
|
||||
},
|
||||
{
|
||||
@@ -63000,6 +63512,16 @@
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "tools.web.search.gemini.baseUrl",
|
||||
"kind": "core",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "tools.web.search.gemini.model",
|
||||
"kind": "core",
|
||||
@@ -63007,12 +63529,7 @@
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [
|
||||
"models",
|
||||
"tools"
|
||||
],
|
||||
"label": "Gemini Search Model",
|
||||
"help": "Gemini model override (default: \"gemini-2.5-flash\").",
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
@@ -63040,8 +63557,6 @@
|
||||
"security",
|
||||
"tools"
|
||||
],
|
||||
"label": "Grok Search API Key",
|
||||
"help": "Grok (xAI) API key (fallback: XAI_API_KEY env var).",
|
||||
"hasChildren": true
|
||||
},
|
||||
{
|
||||
@@ -63074,6 +63589,16 @@
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "tools.web.search.grok.baseUrl",
|
||||
"kind": "core",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "tools.web.search.grok.inlineCitations",
|
||||
"kind": "core",
|
||||
@@ -63091,12 +63616,7 @@
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [
|
||||
"models",
|
||||
"tools"
|
||||
],
|
||||
"label": "Grok Search Model",
|
||||
"help": "Grok model override (default: \"grok-4-1-fast\").",
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
@@ -63124,8 +63644,6 @@
|
||||
"security",
|
||||
"tools"
|
||||
],
|
||||
"label": "Kimi Search API Key",
|
||||
"help": "Moonshot/Kimi API key (fallback: KIMI_API_KEY or MOONSHOT_API_KEY env var).",
|
||||
"hasChildren": true
|
||||
},
|
||||
{
|
||||
@@ -63165,11 +63683,7 @@
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [
|
||||
"tools"
|
||||
],
|
||||
"label": "Kimi Search Base URL",
|
||||
"help": "Kimi base URL override (default: \"https://api.moonshot.ai/v1\").",
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
@@ -63179,12 +63693,7 @@
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [
|
||||
"models",
|
||||
"tools"
|
||||
],
|
||||
"label": "Kimi Search Model",
|
||||
"help": "Kimi model override (default: \"moonshot-v1-128k\").",
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
@@ -63227,8 +63736,6 @@
|
||||
"security",
|
||||
"tools"
|
||||
],
|
||||
"label": "Perplexity API Key",
|
||||
"help": "Perplexity or OpenRouter API key (fallback: PERPLEXITY_API_KEY or OPENROUTER_API_KEY env var). Direct Perplexity keys default to the Search API; OpenRouter keys use Sonar chat completions.",
|
||||
"hasChildren": true
|
||||
},
|
||||
{
|
||||
@@ -63268,11 +63775,7 @@
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [
|
||||
"tools"
|
||||
],
|
||||
"label": "Perplexity Base URL",
|
||||
"help": "Optional Perplexity/OpenRouter chat-completions base URL override. Setting this opts Perplexity into the legacy Sonar/OpenRouter compatibility path.",
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
@@ -63282,12 +63785,7 @@
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [
|
||||
"models",
|
||||
"tools"
|
||||
],
|
||||
"label": "Perplexity Model",
|
||||
"help": "Optional Sonar/OpenRouter model override (default: \"perplexity/sonar-pro\"). Setting this opts Perplexity into the legacy chat-completions compatibility path.",
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
@@ -63301,7 +63799,7 @@
|
||||
"tools"
|
||||
],
|
||||
"label": "Web Search Provider",
|
||||
"help": "Search provider (\"brave\", \"firecrawl\", \"gemini\", \"grok\", \"kimi\", or \"perplexity\"). Auto-detected from available API keys if omitted.",
|
||||
"help": "Search provider id. Auto-detected from available API keys if omitted.",
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
@@ -63386,7 +63884,7 @@
|
||||
"advanced"
|
||||
],
|
||||
"label": "Accent Color",
|
||||
"help": "Primary accent/seam color used by UI surfaces for emphasis, badges, and visual identity cues. Use high-contrast values that remain readable across light/dark themes.",
|
||||
"help": "Primary accent color used by UI surfaces for emphasis, badges, and visual identity cues. Use high-contrast values that remain readable across light/dark themes.",
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{"generatedBy":"scripts/generate-config-doc-baseline.ts","recordType":"meta","totalPaths":5476}
|
||||
{"generatedBy":"scripts/generate-config-doc-baseline.ts","recordType":"meta","totalPaths":5518}
|
||||
{"recordType":"path","path":"acp","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"ACP","help":"ACP runtime controls for enabling dispatch, selecting backends, constraining allowed agent targets, and tuning streamed turn projection behavior.","hasChildren":true}
|
||||
{"recordType":"path","path":"acp.allowedAgents","kind":"core","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"ACP Allowed Agents","help":"Allowlist of ACP target agent ids permitted for ACP runtime sessions. Empty means no additional allowlist restriction.","hasChildren":true}
|
||||
{"recordType":"path","path":"acp.allowedAgents.*","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
@@ -1352,7 +1352,7 @@
|
||||
{"recordType":"path","path":"channels.discord.voice.tts.provider","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.discord.voice.tts.summaryModel","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.discord.voice.tts.timeoutMs","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["channels","network"],"label":"Feishu","help":"飞书/Lark enterprise messaging.","hasChildren":true}
|
||||
{"recordType":"path","path":"channels.feishu","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["channels","network"],"label":"Feishu","help":"飞书/Lark enterprise messaging with doc/wiki/drive tools.","hasChildren":true}
|
||||
{"recordType":"path","path":"channels.feishu.accounts","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*.actions","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
@@ -1532,7 +1532,7 @@
|
||||
{"recordType":"path","path":"channels.feishu.webhookHost","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.webhookPath","kind":"channel","type":"string","required":true,"defaultValue":"/feishu/events","deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.webhookPort","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.googlechat","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["channels","network"],"label":"Google Chat","help":"Google Workspace Chat app with HTTP webhook.","hasChildren":true}
|
||||
{"recordType":"path","path":"channels.googlechat","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["channels","network"],"label":"Google Chat","help":"Google Workspace Chat app via HTTP webhooks.","hasChildren":true}
|
||||
{"recordType":"path","path":"channels.googlechat.accounts","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.googlechat.accounts.*","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.googlechat.accounts.*.actions","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
@@ -1980,7 +1980,7 @@
|
||||
{"recordType":"path","path":"channels.line.secretFile","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.line.tokenFile","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.line.webhookPath","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.matrix","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["channels","network"],"label":"Matrix","help":"open protocol; configure a homeserver + access token.","hasChildren":true}
|
||||
{"recordType":"path","path":"channels.matrix","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["channels","network"],"label":"Matrix","help":"open protocol; install the plugin to enable.","hasChildren":true}
|
||||
{"recordType":"path","path":"channels.matrix.accessToken","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.matrix.accounts","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.matrix.accounts.*","kind":"channel","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
@@ -2362,7 +2362,7 @@
|
||||
{"recordType":"path","path":"channels.nextcloud-talk.webhookPath","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.nextcloud-talk.webhookPort","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.nextcloud-talk.webhookPublicUrl","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.nostr","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["channels","network"],"label":"Nostr","help":"Decentralized DMs via Nostr relays (NIP-04)","hasChildren":true}
|
||||
{"recordType":"path","path":"channels.nostr","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["channels","network"],"label":"Nostr","help":"Decentralized protocol; encrypted DMs via NIP-04.","hasChildren":true}
|
||||
{"recordType":"path","path":"channels.nostr.allowFrom","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.nostr.allowFrom.*","kind":"channel","type":["number","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.nostr.defaultAccount","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
@@ -2779,7 +2779,7 @@
|
||||
{"recordType":"path","path":"channels.slack.userToken.source","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.slack.userTokenReadOnly","kind":"channel","type":"boolean","required":true,"defaultValue":true,"deprecated":false,"sensitive":false,"tags":["auth","channels","network","security"],"label":"Slack User Token Read Only","help":"When true, treat configured Slack user token usage as read-only helper behavior where possible. Keep enabled if you only need supplemental reads without user-context writes.","hasChildren":false}
|
||||
{"recordType":"path","path":"channels.slack.webhookPath","kind":"channel","type":"string","required":true,"defaultValue":"/slack/events","deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.synology-chat","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["channels","network"],"label":"Synology Chat","help":"Connect your Synology NAS Chat to OpenClaw","hasChildren":true}
|
||||
{"recordType":"path","path":"channels.synology-chat","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["channels","network"],"label":"Synology Chat","help":"Connect your Synology NAS Chat to OpenClaw with full agent capabilities.","hasChildren":true}
|
||||
{"recordType":"path","path":"channels.synology-chat.*","kind":"channel","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.telegram","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["channels","network"],"label":"Telegram","help":"simplest way to get started — register a bot with @BotFather and get going.","hasChildren":true}
|
||||
{"recordType":"path","path":"channels.telegram.accounts","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
@@ -3139,7 +3139,7 @@
|
||||
{"recordType":"path","path":"channels.telegram.webhookSecret.provider","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.telegram.webhookSecret.source","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.telegram.webhookUrl","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.tlon","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["channels","network"],"label":"Tlon","help":"Decentralized messaging on Urbit","hasChildren":true}
|
||||
{"recordType":"path","path":"channels.tlon","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["channels","network"],"label":"Tlon","help":"decentralized messaging on Urbit; install the plugin to enable.","hasChildren":true}
|
||||
{"recordType":"path","path":"channels.tlon.accounts","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.tlon.accounts.*","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.tlon.accounts.*.allowPrivateNetwork","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
@@ -3986,6 +3986,7 @@
|
||||
{"recordType":"path","path":"models.providers.*.models.*.api","kind":"core","type":"string","required":false,"enumValues":["openai-completions","openai-responses","openai-codex-responses","anthropic-messages","google-generative-ai","github-copilot","bedrock-converse-stream","ollama"],"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"models.providers.*.models.*.compat","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"models.providers.*.models.*.compat.maxTokensField","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"models.providers.*.models.*.compat.nativeWebSearchTool","kind":"core","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"models.providers.*.models.*.compat.requiresAssistantAfterToolResult","kind":"core","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"models.providers.*.models.*.compat.requiresMistralToolIds","kind":"core","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"models.providers.*.models.*.compat.requiresOpenAiAnthropicToolPayload","kind":"core","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
@@ -3998,6 +3999,8 @@
|
||||
{"recordType":"path","path":"models.providers.*.models.*.compat.supportsTools","kind":"core","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"models.providers.*.models.*.compat.supportsUsageInStreaming","kind":"core","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"models.providers.*.models.*.compat.thinkingFormat","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"models.providers.*.models.*.compat.toolCallArgumentsEncoding","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"models.providers.*.models.*.compat.toolSchemaProfile","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"models.providers.*.models.*.contextWindow","kind":"core","type":"number","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"models.providers.*.models.*.cost","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"models.providers.*.models.*.cost.cacheRead","kind":"core","type":"number","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
@@ -4086,7 +4089,10 @@
|
||||
{"recordType":"path","path":"plugins.entries.bluebubbles.subagent.allowedModels.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"plugins.entries.bluebubbles.subagent.allowModelOverride","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Plugin Subagent Model Override","help":"Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.","hasChildren":false}
|
||||
{"recordType":"path","path":"plugins.entries.brave","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/brave-plugin","help":"OpenClaw Brave plugin (plugin: brave)","hasChildren":true}
|
||||
{"recordType":"path","path":"plugins.entries.brave.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/brave-plugin Config","help":"Plugin-defined config payload for brave.","hasChildren":false}
|
||||
{"recordType":"path","path":"plugins.entries.brave.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/brave-plugin Config","help":"Plugin-defined config payload for brave.","hasChildren":true}
|
||||
{"recordType":"path","path":"plugins.entries.brave.config.webSearch","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"plugins.entries.brave.config.webSearch.apiKey","kind":"plugin","type":["object","string"],"required":false,"deprecated":false,"sensitive":true,"tags":["auth","security"],"label":"Brave Search API Key","help":"Brave Search API key (fallback: BRAVE_API_KEY env var).","hasChildren":false}
|
||||
{"recordType":"path","path":"plugins.entries.brave.config.webSearch.mode","kind":"plugin","type":"string","required":false,"enumValues":["web","llm-context"],"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Brave Search Mode","help":"Brave Search mode: web or llm-context.","hasChildren":false}
|
||||
{"recordType":"path","path":"plugins.entries.brave.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/brave-plugin","hasChildren":false}
|
||||
{"recordType":"path","path":"plugins.entries.brave.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true}
|
||||
{"recordType":"path","path":"plugins.entries.brave.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false}
|
||||
@@ -4198,6 +4204,15 @@
|
||||
{"recordType":"path","path":"plugins.entries.elevenlabs.subagent.allowedModels","kind":"plugin","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Plugin Subagent Allowed Models","help":"Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.","hasChildren":true}
|
||||
{"recordType":"path","path":"plugins.entries.elevenlabs.subagent.allowedModels.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"plugins.entries.elevenlabs.subagent.allowModelOverride","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Plugin Subagent Model Override","help":"Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.","hasChildren":false}
|
||||
{"recordType":"path","path":"plugins.entries.fal","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/fal-provider","help":"OpenClaw fal provider plugin (plugin: fal)","hasChildren":true}
|
||||
{"recordType":"path","path":"plugins.entries.fal.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/fal-provider Config","help":"Plugin-defined config payload for fal.","hasChildren":false}
|
||||
{"recordType":"path","path":"plugins.entries.fal.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/fal-provider","hasChildren":false}
|
||||
{"recordType":"path","path":"plugins.entries.fal.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true}
|
||||
{"recordType":"path","path":"plugins.entries.fal.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false}
|
||||
{"recordType":"path","path":"plugins.entries.fal.subagent","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Subagent Policy","help":"Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.","hasChildren":true}
|
||||
{"recordType":"path","path":"plugins.entries.fal.subagent.allowedModels","kind":"plugin","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Plugin Subagent Allowed Models","help":"Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.","hasChildren":true}
|
||||
{"recordType":"path","path":"plugins.entries.fal.subagent.allowedModels.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"plugins.entries.fal.subagent.allowModelOverride","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Plugin Subagent Model Override","help":"Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.","hasChildren":false}
|
||||
{"recordType":"path","path":"plugins.entries.feishu","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/feishu","help":"OpenClaw Feishu/Lark channel plugin (community maintained by @m1heng) (plugin: feishu)","hasChildren":true}
|
||||
{"recordType":"path","path":"plugins.entries.feishu.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/feishu Config","help":"Plugin-defined config payload for feishu.","hasChildren":false}
|
||||
{"recordType":"path","path":"plugins.entries.feishu.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/feishu","hasChildren":false}
|
||||
@@ -4208,7 +4223,10 @@
|
||||
{"recordType":"path","path":"plugins.entries.feishu.subagent.allowedModels.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"plugins.entries.feishu.subagent.allowModelOverride","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Plugin Subagent Model Override","help":"Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.","hasChildren":false}
|
||||
{"recordType":"path","path":"plugins.entries.firecrawl","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/firecrawl-plugin","help":"OpenClaw Firecrawl plugin (plugin: firecrawl)","hasChildren":true}
|
||||
{"recordType":"path","path":"plugins.entries.firecrawl.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/firecrawl-plugin Config","help":"Plugin-defined config payload for firecrawl.","hasChildren":false}
|
||||
{"recordType":"path","path":"plugins.entries.firecrawl.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/firecrawl-plugin Config","help":"Plugin-defined config payload for firecrawl.","hasChildren":true}
|
||||
{"recordType":"path","path":"plugins.entries.firecrawl.config.webSearch","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"plugins.entries.firecrawl.config.webSearch.apiKey","kind":"plugin","type":["object","string"],"required":false,"deprecated":false,"sensitive":true,"tags":["auth","security"],"label":"Firecrawl Search API Key","help":"Firecrawl API key for web search (fallback: FIRECRAWL_API_KEY env var).","hasChildren":false}
|
||||
{"recordType":"path","path":"plugins.entries.firecrawl.config.webSearch.baseUrl","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Firecrawl Search Base URL","help":"Firecrawl Search base URL override.","hasChildren":false}
|
||||
{"recordType":"path","path":"plugins.entries.firecrawl.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/firecrawl-plugin","hasChildren":false}
|
||||
{"recordType":"path","path":"plugins.entries.firecrawl.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true}
|
||||
{"recordType":"path","path":"plugins.entries.firecrawl.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false}
|
||||
@@ -4226,7 +4244,10 @@
|
||||
{"recordType":"path","path":"plugins.entries.github-copilot.subagent.allowedModels.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"plugins.entries.github-copilot.subagent.allowModelOverride","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Plugin Subagent Model Override","help":"Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.","hasChildren":false}
|
||||
{"recordType":"path","path":"plugins.entries.google","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/google-plugin","help":"OpenClaw Google plugin (plugin: google)","hasChildren":true}
|
||||
{"recordType":"path","path":"plugins.entries.google.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/google-plugin Config","help":"Plugin-defined config payload for google.","hasChildren":false}
|
||||
{"recordType":"path","path":"plugins.entries.google.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/google-plugin Config","help":"Plugin-defined config payload for google.","hasChildren":true}
|
||||
{"recordType":"path","path":"plugins.entries.google.config.webSearch","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"plugins.entries.google.config.webSearch.apiKey","kind":"plugin","type":["object","string"],"required":false,"deprecated":false,"sensitive":true,"tags":["auth","security"],"label":"Gemini Search API Key","help":"Gemini API key for Google Search grounding (fallback: GEMINI_API_KEY env var).","hasChildren":false}
|
||||
{"recordType":"path","path":"plugins.entries.google.config.webSearch.model","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":["models"],"label":"Gemini Search Model","help":"Gemini model override for web search grounding.","hasChildren":false}
|
||||
{"recordType":"path","path":"plugins.entries.google.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/google-plugin","hasChildren":false}
|
||||
{"recordType":"path","path":"plugins.entries.google.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true}
|
||||
{"recordType":"path","path":"plugins.entries.google.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false}
|
||||
@@ -4404,7 +4425,11 @@
|
||||
{"recordType":"path","path":"plugins.entries.modelstudio.subagent.allowedModels.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"plugins.entries.modelstudio.subagent.allowModelOverride","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Plugin Subagent Model Override","help":"Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.","hasChildren":false}
|
||||
{"recordType":"path","path":"plugins.entries.moonshot","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/moonshot-provider","help":"OpenClaw Moonshot provider plugin (plugin: moonshot)","hasChildren":true}
|
||||
{"recordType":"path","path":"plugins.entries.moonshot.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/moonshot-provider Config","help":"Plugin-defined config payload for moonshot.","hasChildren":false}
|
||||
{"recordType":"path","path":"plugins.entries.moonshot.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/moonshot-provider Config","help":"Plugin-defined config payload for moonshot.","hasChildren":true}
|
||||
{"recordType":"path","path":"plugins.entries.moonshot.config.webSearch","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"plugins.entries.moonshot.config.webSearch.apiKey","kind":"plugin","type":["object","string"],"required":false,"deprecated":false,"sensitive":true,"tags":["auth","security"],"label":"Kimi Search API Key","help":"Moonshot/Kimi API key (fallback: KIMI_API_KEY or MOONSHOT_API_KEY env var).","hasChildren":false}
|
||||
{"recordType":"path","path":"plugins.entries.moonshot.config.webSearch.baseUrl","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Kimi Search Base URL","help":"Kimi base URL override.","hasChildren":false}
|
||||
{"recordType":"path","path":"plugins.entries.moonshot.config.webSearch.model","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":["models"],"label":"Kimi Search Model","help":"Kimi model override.","hasChildren":false}
|
||||
{"recordType":"path","path":"plugins.entries.moonshot.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/moonshot-provider","hasChildren":false}
|
||||
{"recordType":"path","path":"plugins.entries.moonshot.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true}
|
||||
{"recordType":"path","path":"plugins.entries.moonshot.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false}
|
||||
@@ -4524,7 +4549,11 @@
|
||||
{"recordType":"path","path":"plugins.entries.openshell.subagent.allowedModels.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"plugins.entries.openshell.subagent.allowModelOverride","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Plugin Subagent Model Override","help":"Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.","hasChildren":false}
|
||||
{"recordType":"path","path":"plugins.entries.perplexity","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/perplexity-plugin","help":"OpenClaw Perplexity plugin (plugin: perplexity)","hasChildren":true}
|
||||
{"recordType":"path","path":"plugins.entries.perplexity.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/perplexity-plugin Config","help":"Plugin-defined config payload for perplexity.","hasChildren":false}
|
||||
{"recordType":"path","path":"plugins.entries.perplexity.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/perplexity-plugin Config","help":"Plugin-defined config payload for perplexity.","hasChildren":true}
|
||||
{"recordType":"path","path":"plugins.entries.perplexity.config.webSearch","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"plugins.entries.perplexity.config.webSearch.apiKey","kind":"plugin","type":["object","string"],"required":false,"deprecated":false,"sensitive":true,"tags":["auth","security"],"label":"Perplexity API Key","help":"Perplexity or OpenRouter API key for web search.","hasChildren":false}
|
||||
{"recordType":"path","path":"plugins.entries.perplexity.config.webSearch.baseUrl","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Perplexity Base URL","help":"Optional Perplexity/OpenRouter chat-completions base URL override.","hasChildren":false}
|
||||
{"recordType":"path","path":"plugins.entries.perplexity.config.webSearch.model","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":["models"],"label":"Perplexity Model","help":"Optional Sonar/OpenRouter model override.","hasChildren":false}
|
||||
{"recordType":"path","path":"plugins.entries.perplexity.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/perplexity-plugin","hasChildren":false}
|
||||
{"recordType":"path","path":"plugins.entries.perplexity.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true}
|
||||
{"recordType":"path","path":"plugins.entries.perplexity.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false}
|
||||
@@ -4832,7 +4861,11 @@
|
||||
{"recordType":"path","path":"plugins.entries.whatsapp.subagent.allowedModels.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"plugins.entries.whatsapp.subagent.allowModelOverride","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Plugin Subagent Model Override","help":"Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.","hasChildren":false}
|
||||
{"recordType":"path","path":"plugins.entries.xai","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/xai-plugin","help":"OpenClaw xAI plugin (plugin: xai)","hasChildren":true}
|
||||
{"recordType":"path","path":"plugins.entries.xai.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/xai-plugin Config","help":"Plugin-defined config payload for xai.","hasChildren":false}
|
||||
{"recordType":"path","path":"plugins.entries.xai.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/xai-plugin Config","help":"Plugin-defined config payload for xai.","hasChildren":true}
|
||||
{"recordType":"path","path":"plugins.entries.xai.config.webSearch","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"plugins.entries.xai.config.webSearch.apiKey","kind":"plugin","type":["object","string"],"required":false,"deprecated":false,"sensitive":true,"tags":["auth","security"],"label":"Grok Search API Key","help":"xAI API key for Grok web search (fallback: XAI_API_KEY env var).","hasChildren":false}
|
||||
{"recordType":"path","path":"plugins.entries.xai.config.webSearch.inlineCitations","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Inline Citations","help":"Include inline markdown citations in Grok responses.","hasChildren":false}
|
||||
{"recordType":"path","path":"plugins.entries.xai.config.webSearch.model","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":["models"],"label":"Grok Search Model","help":"Grok model override for web search.","hasChildren":false}
|
||||
{"recordType":"path","path":"plugins.entries.xai.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/xai-plugin","hasChildren":false}
|
||||
{"recordType":"path","path":"plugins.entries.xai.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true}
|
||||
{"recordType":"path","path":"plugins.entries.xai.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false}
|
||||
@@ -5403,55 +5436,64 @@
|
||||
{"recordType":"path","path":"tools.web.fetch.timeoutSeconds","kind":"core","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":["performance","tools"],"label":"Web Fetch Timeout (sec)","help":"Timeout in seconds for web_fetch requests.","hasChildren":false}
|
||||
{"recordType":"path","path":"tools.web.fetch.userAgent","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":["tools"],"label":"Web Fetch User-Agent","help":"Override User-Agent header for web_fetch requests.","hasChildren":false}
|
||||
{"recordType":"path","path":"tools.web.search","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"tools.web.search.apiKey","kind":"core","type":["object","string"],"required":false,"deprecated":false,"sensitive":true,"tags":["auth","security","tools"],"label":"Brave Search API Key","help":"Brave Search API key (fallback: BRAVE_API_KEY env var).","hasChildren":true}
|
||||
{"recordType":"path","path":"tools.web.search.apiKey","kind":"core","type":["object","string"],"required":false,"deprecated":false,"sensitive":true,"tags":["auth","security","tools"],"hasChildren":true}
|
||||
{"recordType":"path","path":"tools.web.search.apiKey.id","kind":"core","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"tools.web.search.apiKey.provider","kind":"core","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"tools.web.search.apiKey.source","kind":"core","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"tools.web.search.brave","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"tools.web.search.brave.mode","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":["tools"],"label":"Brave Search Mode","help":"Brave Search mode: \"web\" (URL results) or \"llm-context\" (pre-extracted page content for LLM grounding).","hasChildren":false}
|
||||
{"recordType":"path","path":"tools.web.search.brave.apiKey","kind":"core","type":["object","string"],"required":false,"deprecated":false,"sensitive":true,"tags":["auth","security","tools"],"hasChildren":true}
|
||||
{"recordType":"path","path":"tools.web.search.brave.apiKey.id","kind":"core","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"tools.web.search.brave.apiKey.provider","kind":"core","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"tools.web.search.brave.apiKey.source","kind":"core","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"tools.web.search.brave.baseUrl","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"tools.web.search.brave.mode","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"tools.web.search.brave.model","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"tools.web.search.cacheTtlMinutes","kind":"core","type":"number","required":false,"deprecated":false,"sensitive":false,"tags":["performance","storage","tools"],"label":"Web Search Cache TTL (min)","help":"Cache TTL in minutes for web_search results.","hasChildren":false}
|
||||
{"recordType":"path","path":"tools.web.search.enabled","kind":"core","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["tools"],"label":"Enable Web Search Tool","help":"Enable the web_search tool (requires a provider API key).","hasChildren":false}
|
||||
{"recordType":"path","path":"tools.web.search.firecrawl","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"tools.web.search.firecrawl.apiKey","kind":"core","type":["object","string"],"required":false,"deprecated":false,"sensitive":true,"tags":["auth","security","tools"],"label":"Firecrawl Search API Key","help":"Firecrawl API key for web search (fallback: FIRECRAWL_API_KEY env var).","hasChildren":true}
|
||||
{"recordType":"path","path":"tools.web.search.firecrawl.apiKey","kind":"core","type":["object","string"],"required":false,"deprecated":false,"sensitive":true,"tags":["auth","security","tools"],"hasChildren":true}
|
||||
{"recordType":"path","path":"tools.web.search.firecrawl.apiKey.id","kind":"core","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"tools.web.search.firecrawl.apiKey.provider","kind":"core","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"tools.web.search.firecrawl.apiKey.source","kind":"core","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"tools.web.search.firecrawl.baseUrl","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":["tools"],"label":"Firecrawl Search Base URL","help":"Firecrawl Search base URL override (default: \"https://api.firecrawl.dev\").","hasChildren":false}
|
||||
{"recordType":"path","path":"tools.web.search.firecrawl.baseUrl","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"tools.web.search.firecrawl.model","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"tools.web.search.gemini","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"tools.web.search.gemini.apiKey","kind":"core","type":["object","string"],"required":false,"deprecated":false,"sensitive":true,"tags":["auth","security","tools"],"label":"Gemini Search API Key","help":"Gemini API key for Google Search grounding (fallback: GEMINI_API_KEY env var).","hasChildren":true}
|
||||
{"recordType":"path","path":"tools.web.search.gemini.apiKey","kind":"core","type":["object","string"],"required":false,"deprecated":false,"sensitive":true,"tags":["auth","security","tools"],"hasChildren":true}
|
||||
{"recordType":"path","path":"tools.web.search.gemini.apiKey.id","kind":"core","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"tools.web.search.gemini.apiKey.provider","kind":"core","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"tools.web.search.gemini.apiKey.source","kind":"core","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"tools.web.search.gemini.model","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":["models","tools"],"label":"Gemini Search Model","help":"Gemini model override (default: \"gemini-2.5-flash\").","hasChildren":false}
|
||||
{"recordType":"path","path":"tools.web.search.gemini.baseUrl","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"tools.web.search.gemini.model","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"tools.web.search.grok","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"tools.web.search.grok.apiKey","kind":"core","type":["object","string"],"required":false,"deprecated":false,"sensitive":true,"tags":["auth","security","tools"],"label":"Grok Search API Key","help":"Grok (xAI) API key (fallback: XAI_API_KEY env var).","hasChildren":true}
|
||||
{"recordType":"path","path":"tools.web.search.grok.apiKey","kind":"core","type":["object","string"],"required":false,"deprecated":false,"sensitive":true,"tags":["auth","security","tools"],"hasChildren":true}
|
||||
{"recordType":"path","path":"tools.web.search.grok.apiKey.id","kind":"core","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"tools.web.search.grok.apiKey.provider","kind":"core","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"tools.web.search.grok.apiKey.source","kind":"core","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"tools.web.search.grok.baseUrl","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"tools.web.search.grok.inlineCitations","kind":"core","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"tools.web.search.grok.model","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":["models","tools"],"label":"Grok Search Model","help":"Grok model override (default: \"grok-4-1-fast\").","hasChildren":false}
|
||||
{"recordType":"path","path":"tools.web.search.grok.model","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"tools.web.search.kimi","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"tools.web.search.kimi.apiKey","kind":"core","type":["object","string"],"required":false,"deprecated":false,"sensitive":true,"tags":["auth","security","tools"],"label":"Kimi Search API Key","help":"Moonshot/Kimi API key (fallback: KIMI_API_KEY or MOONSHOT_API_KEY env var).","hasChildren":true}
|
||||
{"recordType":"path","path":"tools.web.search.kimi.apiKey","kind":"core","type":["object","string"],"required":false,"deprecated":false,"sensitive":true,"tags":["auth","security","tools"],"hasChildren":true}
|
||||
{"recordType":"path","path":"tools.web.search.kimi.apiKey.id","kind":"core","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"tools.web.search.kimi.apiKey.provider","kind":"core","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"tools.web.search.kimi.apiKey.source","kind":"core","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"tools.web.search.kimi.baseUrl","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":["tools"],"label":"Kimi Search Base URL","help":"Kimi base URL override (default: \"https://api.moonshot.ai/v1\").","hasChildren":false}
|
||||
{"recordType":"path","path":"tools.web.search.kimi.model","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":["models","tools"],"label":"Kimi Search Model","help":"Kimi model override (default: \"moonshot-v1-128k\").","hasChildren":false}
|
||||
{"recordType":"path","path":"tools.web.search.kimi.baseUrl","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"tools.web.search.kimi.model","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"tools.web.search.maxResults","kind":"core","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":["performance","tools"],"label":"Web Search Max Results","help":"Number of results to return (1-10).","hasChildren":false}
|
||||
{"recordType":"path","path":"tools.web.search.perplexity","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"tools.web.search.perplexity.apiKey","kind":"core","type":["object","string"],"required":false,"deprecated":false,"sensitive":true,"tags":["auth","security","tools"],"label":"Perplexity API Key","help":"Perplexity or OpenRouter API key (fallback: PERPLEXITY_API_KEY or OPENROUTER_API_KEY env var). Direct Perplexity keys default to the Search API; OpenRouter keys use Sonar chat completions.","hasChildren":true}
|
||||
{"recordType":"path","path":"tools.web.search.perplexity.apiKey","kind":"core","type":["object","string"],"required":false,"deprecated":false,"sensitive":true,"tags":["auth","security","tools"],"hasChildren":true}
|
||||
{"recordType":"path","path":"tools.web.search.perplexity.apiKey.id","kind":"core","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"tools.web.search.perplexity.apiKey.provider","kind":"core","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"tools.web.search.perplexity.apiKey.source","kind":"core","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"tools.web.search.perplexity.baseUrl","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":["tools"],"label":"Perplexity Base URL","help":"Optional Perplexity/OpenRouter chat-completions base URL override. Setting this opts Perplexity into the legacy Sonar/OpenRouter compatibility path.","hasChildren":false}
|
||||
{"recordType":"path","path":"tools.web.search.perplexity.model","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":["models","tools"],"label":"Perplexity Model","help":"Optional Sonar/OpenRouter model override (default: \"perplexity/sonar-pro\"). Setting this opts Perplexity into the legacy chat-completions compatibility path.","hasChildren":false}
|
||||
{"recordType":"path","path":"tools.web.search.provider","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":["tools"],"label":"Web Search Provider","help":"Search provider (\"brave\", \"firecrawl\", \"gemini\", \"grok\", \"kimi\", or \"perplexity\"). Auto-detected from available API keys if omitted.","hasChildren":false}
|
||||
{"recordType":"path","path":"tools.web.search.perplexity.baseUrl","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"tools.web.search.perplexity.model","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"tools.web.search.provider","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":["tools"],"label":"Web Search Provider","help":"Search provider id. Auto-detected from available API keys if omitted.","hasChildren":false}
|
||||
{"recordType":"path","path":"tools.web.search.timeoutSeconds","kind":"core","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":["performance","tools"],"label":"Web Search Timeout (sec)","help":"Timeout in seconds for web_search requests.","hasChildren":false}
|
||||
{"recordType":"path","path":"ui","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"UI","help":"UI presentation settings for accenting and assistant identity shown in control surfaces. Use this for branding and readability customization without changing runtime behavior.","hasChildren":true}
|
||||
{"recordType":"path","path":"ui.assistant","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Assistant Appearance","help":"Assistant display identity settings for name and avatar shown in UI surfaces. Keep these values aligned with your operator-facing persona and support expectations.","hasChildren":true}
|
||||
{"recordType":"path","path":"ui.assistant.avatar","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Assistant Avatar","help":"Assistant avatar image source used in UI surfaces (URL, path, or data URI depending on runtime support). Use trusted assets and consistent branding dimensions for clean rendering.","hasChildren":false}
|
||||
{"recordType":"path","path":"ui.assistant.name","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Assistant Name","help":"Display name shown for the assistant in UI views, chat chrome, and status contexts. Keep this stable so operators can reliably identify which assistant persona is active.","hasChildren":false}
|
||||
{"recordType":"path","path":"ui.seamColor","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Accent Color","help":"Primary accent/seam color used by UI surfaces for emphasis, badges, and visual identity cues. Use high-contrast values that remain readable across light/dark themes.","hasChildren":false}
|
||||
{"recordType":"path","path":"ui.seamColor","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Accent Color","help":"Primary accent color used by UI surfaces for emphasis, badges, and visual identity cues. Use high-contrast values that remain readable across light/dark themes.","hasChildren":false}
|
||||
{"recordType":"path","path":"update","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Updates","help":"Update-channel and startup-check behavior for keeping OpenClaw runtime versions current. Use conservative channels in production and more experimental channels only in controlled environments.","hasChildren":true}
|
||||
{"recordType":"path","path":"update.auto","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"update.auto.betaCheckIntervalHours","kind":"core","type":"number","required":false,"deprecated":false,"sensitive":false,"tags":["performance"],"label":"Auto Update Beta Check Interval (hours)","help":"How often beta-channel checks run in hours (default: 1).","hasChildren":false}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
docs.openclaw.ai
|
||||
@@ -17,7 +17,7 @@ Hooks are small scripts that run when something happens. There are two kinds:
|
||||
- **Hooks** (this page): run inside the Gateway when agent events fire, like `/new`, `/reset`, `/stop`, or lifecycle events.
|
||||
- **Webhooks**: external HTTP webhooks that let other systems trigger work in OpenClaw. See [Webhook Hooks](/automation/webhook) or use `openclaw webhooks` for Gmail helper commands.
|
||||
|
||||
Hooks can also be bundled inside plugins; see [Plugins](/tools/plugin#plugin-hooks).
|
||||
Hooks can also be bundled inside plugins; see [Plugin hooks](/plugins/architecture#provider-runtime-hooks).
|
||||
|
||||
Common uses:
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@ read_when:
|
||||
- You are configuring IRC allowlists, group policy, or mention gating
|
||||
---
|
||||
|
||||
# IRC
|
||||
|
||||
Use IRC when you want OpenClaw in classic channels (`#room`) and direct messages.
|
||||
IRC ships as an extension plugin, but it is configured in the main config under `channels.irc`.
|
||||
|
||||
|
||||
@@ -9,6 +9,21 @@ title: "WhatsApp"
|
||||
|
||||
Status: production-ready via WhatsApp Web (Baileys). Gateway owns linked session(s).
|
||||
|
||||
## Install (on demand)
|
||||
|
||||
- Onboarding (`openclaw onboard`) and `openclaw channels add --channel whatsapp`
|
||||
prompt to install the WhatsApp plugin the first time you select it.
|
||||
- `openclaw channels login --channel whatsapp` also offers the install flow when
|
||||
the plugin is not present yet.
|
||||
- Dev channel + git checkout: defaults to the local plugin path.
|
||||
- Stable/Beta: defaults to the npm package `@openclaw/whatsapp`.
|
||||
|
||||
Manual install stays available:
|
||||
|
||||
```bash
|
||||
openclaw plugins install @openclaw/whatsapp
|
||||
```
|
||||
|
||||
<CardGroup cols={3}>
|
||||
<Card title="Pairing" icon="link" href="/channels/pairing">
|
||||
Default DM policy is pairing for unknown senders.
|
||||
|
||||
@@ -13,7 +13,7 @@ Manage agent hooks (event-driven automations for commands like `/new`, `/reset`,
|
||||
Related:
|
||||
|
||||
- Hooks: [Hooks](/automation/hooks)
|
||||
- Plugin hooks: [Plugins](/tools/plugin#plugin-hooks)
|
||||
- Plugin hooks: [Plugin hooks](/plugins/architecture#provider-runtime-hooks)
|
||||
|
||||
## List All Hooks
|
||||
|
||||
|
||||
@@ -168,7 +168,7 @@ Each plugin is classified by what it actually registers at runtime:
|
||||
- **hook-only** — only hooks, no capabilities or surfaces
|
||||
- **non-capability** — tools/commands/services but no capabilities
|
||||
|
||||
See [Plugins](/tools/plugin#plugin-shapes) for more on the capability model.
|
||||
See [Plugin shapes](/plugins/architecture#plugin-shapes) for more on the capability model.
|
||||
|
||||
The `--json` flag outputs a machine-readable report suitable for scripting and
|
||||
auditing.
|
||||
|
||||
@@ -92,7 +92,7 @@ These run inside the agent loop or gateway pipeline:
|
||||
- **`session_start` / `session_end`**: session lifecycle boundaries.
|
||||
- **`gateway_start` / `gateway_stop`**: gateway lifecycle events.
|
||||
|
||||
See [Plugins](/tools/plugin#plugin-hooks) for the hook API and registration details.
|
||||
See [Plugin hooks](/plugins/architecture#provider-runtime-hooks) for the hook API and registration details.
|
||||
|
||||
## Streaming + partial replies
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ read_when:
|
||||
title: "Features"
|
||||
---
|
||||
|
||||
# Features
|
||||
|
||||
## Highlights
|
||||
|
||||
<Columns>
|
||||
|
||||
@@ -34,7 +34,7 @@ For model selection rules, see [/concepts/models](/concepts/models).
|
||||
`fetchUsageSnapshot`.
|
||||
- Note: provider runtime `capabilities` is shared runner metadata (provider
|
||||
family, transcript/tooling quirks, transport/cache hints). It is not the
|
||||
same as the [public capability model](/tools/plugin#public-capability-model)
|
||||
same as the [public capability model](/plugins/architecture#public-capability-model)
|
||||
which describes what a plugin registers (text inference, speech, etc.).
|
||||
|
||||
## Plugin-owned provider behavior
|
||||
|
||||
@@ -75,6 +75,25 @@ Behavior:
|
||||
- Returns messages array in the raw transcript format.
|
||||
- When given a `sessionId`, OpenClaw resolves it to the corresponding session key (missing ids error).
|
||||
|
||||
## Gateway session history and live transcript APIs
|
||||
|
||||
Control UI and gateway clients can use the lower level history and live transcript surfaces directly.
|
||||
|
||||
HTTP:
|
||||
|
||||
- `GET /sessions/{sessionKey}/history`
|
||||
- Query params: `limit`, `cursor`, `includeTools=1`, `follow=1`
|
||||
- Unknown sessions return HTTP `404` with `error.type = "not_found"`
|
||||
- `follow=1` upgrades the response to an SSE stream of transcript updates for that session
|
||||
|
||||
WebSocket:
|
||||
|
||||
- `sessions.subscribe` subscribes to all session lifecycle and transcript events visible to the client
|
||||
- `sessions.messages.subscribe { key }` subscribes only to `session.message` events for one session
|
||||
- `sessions.messages.unsubscribe { key }` removes that targeted transcript subscription
|
||||
- `session.message` carries appended transcript messages plus live usage metadata when available
|
||||
- `sessions.changed` emits `phase: "message"` for transcript appends so session lists can refresh counters and previews
|
||||
|
||||
## sessions_send
|
||||
|
||||
Send a message into another session.
|
||||
|
||||
@@ -990,9 +990,8 @@
|
||||
"pages": [
|
||||
"tools/apply-patch",
|
||||
"brave-search",
|
||||
"perplexity",
|
||||
"tools/btw",
|
||||
"tools/diffs",
|
||||
"tools/pdf",
|
||||
"tools/elevated",
|
||||
"tools/exec",
|
||||
"tools/exec-approvals",
|
||||
@@ -1000,10 +999,11 @@
|
||||
"tools/llm-task",
|
||||
"tools/lobster",
|
||||
"tools/loop-detection",
|
||||
"tools/pdf",
|
||||
"perplexity",
|
||||
"tools/reactions",
|
||||
"tools/thinking",
|
||||
"tools/web",
|
||||
"tools/btw"
|
||||
"tools/web"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -1037,6 +1037,8 @@
|
||||
{
|
||||
"group": "Extensions",
|
||||
"pages": [
|
||||
"plugins/building-extensions",
|
||||
"plugins/architecture",
|
||||
"plugins/community",
|
||||
"plugins/bundles",
|
||||
"plugins/voice-call",
|
||||
@@ -1101,11 +1103,13 @@
|
||||
"providers/claude-max-api-proxy",
|
||||
"providers/deepgram",
|
||||
"providers/github-copilot",
|
||||
"providers/google",
|
||||
"providers/huggingface",
|
||||
"providers/kilocode",
|
||||
"providers/litellm",
|
||||
"providers/glm",
|
||||
"providers/minimax",
|
||||
"providers/modelstudio",
|
||||
"providers/moonshot",
|
||||
"providers/mistral",
|
||||
"providers/nvidia",
|
||||
@@ -1114,13 +1118,17 @@
|
||||
"providers/opencode-go",
|
||||
"providers/opencode",
|
||||
"providers/openrouter",
|
||||
"providers/perplexity-provider",
|
||||
"providers/qianfan",
|
||||
"providers/qwen",
|
||||
"providers/sglang",
|
||||
"providers/synthetic",
|
||||
"providers/together",
|
||||
"providers/vercel-ai-gateway",
|
||||
"providers/venice",
|
||||
"providers/vllm",
|
||||
"providers/volcengine",
|
||||
"providers/xai",
|
||||
"providers/xiaomi",
|
||||
"providers/zai"
|
||||
]
|
||||
@@ -1201,6 +1209,7 @@
|
||||
"pages": [
|
||||
"gateway/security/index",
|
||||
"gateway/sandboxing",
|
||||
"gateway/openshell",
|
||||
"gateway/sandbox-vs-tool-policy-vs-elevated"
|
||||
]
|
||||
},
|
||||
@@ -1574,13 +1583,13 @@
|
||||
"pages": [
|
||||
"zh-CN/tools/apply-patch",
|
||||
"zh-CN/brave-search",
|
||||
"zh-CN/perplexity",
|
||||
"zh-CN/tools/elevated",
|
||||
"zh-CN/tools/exec",
|
||||
"zh-CN/tools/exec-approvals",
|
||||
"zh-CN/tools/firecrawl",
|
||||
"zh-CN/tools/llm-task",
|
||||
"zh-CN/tools/lobster",
|
||||
"zh-CN/perplexity",
|
||||
"zh-CN/tools/reactions",
|
||||
"zh-CN/tools/thinking",
|
||||
"zh-CN/tools/web"
|
||||
@@ -1616,6 +1625,7 @@
|
||||
{
|
||||
"group": "扩展",
|
||||
"pages": [
|
||||
"zh-CN/plugins/architecture",
|
||||
"zh-CN/plugins/voice-call",
|
||||
"zh-CN/plugins/zalouser",
|
||||
"zh-CN/plugins/manifest",
|
||||
|
||||
@@ -5,6 +5,8 @@ read_when:
|
||||
title: "Network model"
|
||||
---
|
||||
|
||||
# Network Model
|
||||
|
||||
Most operations flow through the Gateway (`openclaw gateway`), a single long-running
|
||||
process that owns channel connections and the WebSocket control plane.
|
||||
|
||||
|
||||
307
docs/gateway/openshell.md
Normal file
307
docs/gateway/openshell.md
Normal file
@@ -0,0 +1,307 @@
|
||||
---
|
||||
title: OpenShell
|
||||
summary: "Use OpenShell as a managed sandbox backend for OpenClaw agents"
|
||||
read_when:
|
||||
- You want cloud-managed sandboxes instead of local Docker
|
||||
- You are setting up the OpenShell plugin
|
||||
- You need to choose between mirror and remote workspace modes
|
||||
---
|
||||
|
||||
# OpenShell
|
||||
|
||||
OpenShell is a managed sandbox backend for OpenClaw. Instead of running Docker
|
||||
containers locally, OpenClaw delegates sandbox lifecycle to the `openshell` CLI,
|
||||
which provisions remote environments with SSH-based command execution.
|
||||
|
||||
The OpenShell plugin reuses the same core SSH transport and remote filesystem
|
||||
bridge as the generic [SSH backend](/gateway/sandboxing#ssh-backend). It adds
|
||||
OpenShell-specific lifecycle (`sandbox create/get/delete`, `sandbox ssh-config`)
|
||||
and an optional `mirror` workspace mode.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- The `openshell` CLI installed and on `PATH` (or set a custom path via
|
||||
`plugins.entries.openshell.config.command`)
|
||||
- An OpenShell account with sandbox access
|
||||
- OpenClaw Gateway running on the host
|
||||
|
||||
## Quick start
|
||||
|
||||
1. Enable the plugin and set the sandbox backend:
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
sandbox: {
|
||||
mode: "all",
|
||||
backend: "openshell",
|
||||
scope: "session",
|
||||
workspaceAccess: "rw",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
entries: {
|
||||
openshell: {
|
||||
enabled: true,
|
||||
config: {
|
||||
from: "openclaw",
|
||||
mode: "remote",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
2. Restart the Gateway. On the next agent turn, OpenClaw creates an OpenShell
|
||||
sandbox and routes tool execution through it.
|
||||
|
||||
3. Verify:
|
||||
|
||||
```bash
|
||||
openclaw sandbox list
|
||||
openclaw sandbox explain
|
||||
```
|
||||
|
||||
## Workspace modes
|
||||
|
||||
This is the most important decision when using OpenShell.
|
||||
|
||||
### `mirror`
|
||||
|
||||
Use `plugins.entries.openshell.config.mode: "mirror"` when you want the **local
|
||||
workspace to stay canonical**.
|
||||
|
||||
Behavior:
|
||||
|
||||
- Before `exec`, OpenClaw syncs the local workspace into the OpenShell sandbox.
|
||||
- After `exec`, OpenClaw syncs the remote workspace back to the local workspace.
|
||||
- File tools still operate through the sandbox bridge, but the local workspace
|
||||
remains the source of truth between turns.
|
||||
|
||||
Best for:
|
||||
|
||||
- You edit files locally outside OpenClaw and want those changes visible in the
|
||||
sandbox automatically.
|
||||
- You want the OpenShell sandbox to behave as much like the Docker backend as
|
||||
possible.
|
||||
- You want the host workspace to reflect sandbox writes after each exec turn.
|
||||
|
||||
Tradeoff: extra sync cost before and after each exec.
|
||||
|
||||
### `remote`
|
||||
|
||||
Use `plugins.entries.openshell.config.mode: "remote"` when you want the
|
||||
**OpenShell workspace to become canonical**.
|
||||
|
||||
Behavior:
|
||||
|
||||
- When the sandbox is first created, OpenClaw seeds the remote workspace from
|
||||
the local workspace once.
|
||||
- After that, `exec`, `read`, `write`, `edit`, and `apply_patch` operate
|
||||
directly against the remote OpenShell workspace.
|
||||
- OpenClaw does **not** sync remote changes back into the local workspace.
|
||||
- Prompt-time media reads still work because file and media tools read through
|
||||
the sandbox bridge.
|
||||
|
||||
Best for:
|
||||
|
||||
- The sandbox should live primarily on the remote side.
|
||||
- You want lower per-turn sync overhead.
|
||||
- You do not want host-local edits to silently overwrite remote sandbox state.
|
||||
|
||||
Important: if you edit files on the host outside OpenClaw after the initial seed,
|
||||
the remote sandbox does **not** see those changes. Use
|
||||
`openclaw sandbox recreate` to re-seed.
|
||||
|
||||
### Choosing a mode
|
||||
|
||||
| | `mirror` | `remote` |
|
||||
| ------------------------ | -------------------------- | ------------------------- |
|
||||
| **Canonical workspace** | Local host | Remote OpenShell |
|
||||
| **Sync direction** | Bidirectional (each exec) | One-time seed |
|
||||
| **Per-turn overhead** | Higher (upload + download) | Lower (direct remote ops) |
|
||||
| **Local edits visible?** | Yes, on next exec | No, until recreate |
|
||||
| **Best for** | Development workflows | Long-running agents, CI |
|
||||
|
||||
## Configuration reference
|
||||
|
||||
All OpenShell config lives under `plugins.entries.openshell.config`:
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
| ------------------------- | ------------------------ | ------------- | ----------------------------------------------------- |
|
||||
| `mode` | `"mirror"` or `"remote"` | `"mirror"` | Workspace sync mode |
|
||||
| `command` | `string` | `"openshell"` | Path or name of the `openshell` CLI |
|
||||
| `from` | `string` | `"openclaw"` | Sandbox source for first-time create |
|
||||
| `gateway` | `string` | — | OpenShell gateway name (`--gateway`) |
|
||||
| `gatewayEndpoint` | `string` | — | OpenShell gateway endpoint URL (`--gateway-endpoint`) |
|
||||
| `policy` | `string` | — | OpenShell policy ID for sandbox creation |
|
||||
| `providers` | `string[]` | `[]` | Provider names to attach when sandbox is created |
|
||||
| `gpu` | `boolean` | `false` | Request GPU resources |
|
||||
| `autoProviders` | `boolean` | `true` | Pass `--auto-providers` during sandbox create |
|
||||
| `remoteWorkspaceDir` | `string` | `"/sandbox"` | Primary writable workspace inside the sandbox |
|
||||
| `remoteAgentWorkspaceDir` | `string` | `"/agent"` | Agent workspace mount path (for read-only access) |
|
||||
| `timeoutSeconds` | `number` | `120` | Timeout for `openshell` CLI operations |
|
||||
|
||||
Sandbox-level settings (`mode`, `scope`, `workspaceAccess`) are configured under
|
||||
`agents.defaults.sandbox` as with any backend. See
|
||||
[Sandboxing](/gateway/sandboxing) for the full matrix.
|
||||
|
||||
## Examples
|
||||
|
||||
### Minimal remote setup
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
sandbox: {
|
||||
mode: "all",
|
||||
backend: "openshell",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
entries: {
|
||||
openshell: {
|
||||
enabled: true,
|
||||
config: {
|
||||
from: "openclaw",
|
||||
mode: "remote",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### Mirror mode with GPU
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
sandbox: {
|
||||
mode: "all",
|
||||
backend: "openshell",
|
||||
scope: "agent",
|
||||
workspaceAccess: "rw",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
entries: {
|
||||
openshell: {
|
||||
enabled: true,
|
||||
config: {
|
||||
from: "openclaw",
|
||||
mode: "mirror",
|
||||
gpu: true,
|
||||
providers: ["openai"],
|
||||
timeoutSeconds: 180,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### Per-agent OpenShell with custom gateway
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
sandbox: { mode: "off" },
|
||||
},
|
||||
list: [
|
||||
{
|
||||
id: "researcher",
|
||||
sandbox: {
|
||||
mode: "all",
|
||||
backend: "openshell",
|
||||
scope: "agent",
|
||||
workspaceAccess: "rw",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
plugins: {
|
||||
entries: {
|
||||
openshell: {
|
||||
enabled: true,
|
||||
config: {
|
||||
from: "openclaw",
|
||||
mode: "remote",
|
||||
gateway: "lab",
|
||||
gatewayEndpoint: "https://lab.example",
|
||||
policy: "strict",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Lifecycle management
|
||||
|
||||
OpenShell sandboxes are managed through the normal sandbox CLI:
|
||||
|
||||
```bash
|
||||
# List all sandbox runtimes (Docker + OpenShell)
|
||||
openclaw sandbox list
|
||||
|
||||
# Inspect effective policy
|
||||
openclaw sandbox explain
|
||||
|
||||
# Recreate (deletes remote workspace, re-seeds on next use)
|
||||
openclaw sandbox recreate --all
|
||||
```
|
||||
|
||||
For `remote` mode, **recreate is especially important**: it deletes the canonical
|
||||
remote workspace for that scope. The next use seeds a fresh remote workspace from
|
||||
the local workspace.
|
||||
|
||||
For `mirror` mode, recreate mainly resets the remote execution environment because
|
||||
the local workspace remains canonical.
|
||||
|
||||
### When to recreate
|
||||
|
||||
Recreate after changing any of these:
|
||||
|
||||
- `agents.defaults.sandbox.backend`
|
||||
- `plugins.entries.openshell.config.from`
|
||||
- `plugins.entries.openshell.config.mode`
|
||||
- `plugins.entries.openshell.config.policy`
|
||||
|
||||
```bash
|
||||
openclaw sandbox recreate --all
|
||||
```
|
||||
|
||||
## Current limitations
|
||||
|
||||
- Sandbox browser is not supported on the OpenShell backend.
|
||||
- `sandbox.docker.binds` does not apply to OpenShell.
|
||||
- Docker-specific runtime knobs under `sandbox.docker.*` apply only to the Docker
|
||||
backend.
|
||||
|
||||
## How it works
|
||||
|
||||
1. OpenClaw calls `openshell sandbox create` (with `--from`, `--gateway`,
|
||||
`--policy`, `--providers`, `--gpu` flags as configured).
|
||||
2. OpenClaw calls `openshell sandbox ssh-config <name>` to get SSH connection
|
||||
details for the sandbox.
|
||||
3. Core writes the SSH config to a temp file and opens an SSH session using the
|
||||
same remote filesystem bridge as the generic SSH backend.
|
||||
4. In `mirror` mode: sync local to remote before exec, run, sync back after exec.
|
||||
5. In `remote` mode: seed once on create, then operate directly on the remote
|
||||
workspace.
|
||||
|
||||
## See also
|
||||
|
||||
- [Sandboxing](/gateway/sandboxing) -- modes, scopes, and backend comparison
|
||||
- [Sandbox vs Tool Policy vs Elevated](/gateway/sandbox-vs-tool-policy-vs-elevated) -- debugging blocked tools
|
||||
- [Multi-Agent Sandbox and Tools](/tools/multi-agent-sandbox-tools) -- per-agent overrides
|
||||
- [Sandbox CLI](/cli/sandbox) -- `openclaw sandbox` commands
|
||||
@@ -126,3 +126,9 @@ Fix-it keys (pick one):
|
||||
### "I thought this was main, why is it sandboxed?"
|
||||
|
||||
In `"non-main"` mode, group/channel keys are _not_ main. Use the main session key (shown by `sandbox explain`) or switch mode to `"off"`.
|
||||
|
||||
## See also
|
||||
|
||||
- [Sandboxing](/gateway/sandboxing) -- full sandbox reference (modes, scopes, backends, images)
|
||||
- [Multi-Agent Sandbox & Tools](/tools/multi-agent-sandbox-tools) -- per-agent overrides and precedence
|
||||
- [Elevated Mode](/tools/elevated)
|
||||
|
||||
@@ -65,6 +65,18 @@ Not sandboxed:
|
||||
SSH-specific config lives under `agents.defaults.sandbox.ssh`.
|
||||
OpenShell-specific config lives under `plugins.entries.openshell.config`.
|
||||
|
||||
### Choosing a backend
|
||||
|
||||
| | Docker | SSH | OpenShell |
|
||||
| ------------------- | -------------------------------- | ------------------------------ | --------------------------------------------------- |
|
||||
| **Where it runs** | Local container | Any SSH-accessible host | OpenShell managed sandbox |
|
||||
| **Setup** | `scripts/sandbox-setup.sh` | SSH key + target host | OpenShell plugin enabled |
|
||||
| **Workspace model** | Bind-mount or copy | Remote-canonical (seed once) | `mirror` or `remote` |
|
||||
| **Network control** | `docker.network` (default: none) | Depends on remote host | Depends on OpenShell |
|
||||
| **Browser sandbox** | Supported | Not supported | Not supported yet |
|
||||
| **Bind mounts** | `docker.binds` | N/A | N/A |
|
||||
| **Best for** | Local dev, full isolation | Offloading to a remote machine | Managed remote sandboxes with optional two-way sync |
|
||||
|
||||
### SSH backend
|
||||
|
||||
Use `backend: "ssh"` when you want OpenClaw to sandbox `exec`, file tools, and media reads on
|
||||
@@ -120,6 +132,18 @@ Important consequences:
|
||||
- Browser sandboxing is not supported on the SSH backend.
|
||||
- `sandbox.docker.*` settings do not apply to the SSH backend.
|
||||
|
||||
### OpenShell backend
|
||||
|
||||
Use `backend: "openshell"` when you want OpenClaw to sandbox tools in an
|
||||
OpenShell-managed remote environment. For the full setup guide, configuration
|
||||
reference, and workspace mode comparison, see the dedicated
|
||||
[OpenShell page](/gateway/openshell).
|
||||
|
||||
OpenShell reuses the same core SSH transport and remote filesystem bridge as the
|
||||
generic SSH backend, and adds OpenShell-specific lifecycle
|
||||
(`sandbox create/get/delete`, `sandbox ssh-config`) plus the optional `mirror`
|
||||
workspace mode.
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
@@ -153,9 +177,6 @@ OpenShell modes:
|
||||
- `mirror` (default): local workspace stays canonical. OpenClaw syncs local files into OpenShell before exec and syncs the remote workspace back after exec.
|
||||
- `remote`: OpenShell workspace is canonical after the sandbox is created. OpenClaw seeds the remote workspace once from the local workspace, then file tools and exec run directly against the remote sandbox without syncing changes back.
|
||||
|
||||
OpenShell reuses the same core SSH transport and remote filesystem bridge as the generic SSH backend.
|
||||
The plugin adds OpenShell-specific lifecycle (`sandbox create/get/delete`, `sandbox ssh-config`) and the optional `mirror` mode.
|
||||
|
||||
Remote transport details:
|
||||
|
||||
- OpenClaw asks OpenShell for sandbox-specific SSH config via `openshell sandbox ssh-config <name>`.
|
||||
@@ -168,11 +189,11 @@ Current OpenShell limitations:
|
||||
- `sandbox.docker.binds` is not supported on the OpenShell backend
|
||||
- Docker-specific runtime knobs under `sandbox.docker.*` still apply only to the Docker backend
|
||||
|
||||
## OpenShell workspace modes
|
||||
#### Workspace modes
|
||||
|
||||
OpenShell has two workspace models. This is the part that matters most in practice.
|
||||
|
||||
### `mirror`
|
||||
##### `mirror`
|
||||
|
||||
Use `plugins.entries.openshell.config.mode: "mirror"` when you want the **local workspace to stay canonical**.
|
||||
|
||||
@@ -192,7 +213,7 @@ Tradeoff:
|
||||
|
||||
- extra sync cost before and after exec
|
||||
|
||||
### `remote`
|
||||
##### `remote`
|
||||
|
||||
Use `plugins.entries.openshell.config.mode: "remote"` when you want the **OpenShell workspace to become canonical**.
|
||||
|
||||
@@ -219,7 +240,7 @@ Use this when:
|
||||
Choose `mirror` if you think of the sandbox as a temporary execution environment.
|
||||
Choose `remote` if you think of the sandbox as the real workspace.
|
||||
|
||||
## OpenShell lifecycle
|
||||
#### OpenShell lifecycle
|
||||
|
||||
OpenShell sandboxes are still managed through the normal sandbox lifecycle:
|
||||
|
||||
@@ -441,6 +462,8 @@ See [Multi-Agent Sandbox & Tools](/tools/multi-agent-sandbox-tools) for preceden
|
||||
|
||||
## Related docs
|
||||
|
||||
- [OpenShell](/gateway/openshell) -- managed sandbox backend setup, workspace modes, and config reference
|
||||
- [Sandbox Configuration](/gateway/configuration#agentsdefaults-sandbox)
|
||||
- [Multi-Agent Sandbox & Tools](/tools/multi-agent-sandbox-tools)
|
||||
- [Sandbox vs Tool Policy vs Elevated](/gateway/sandbox-vs-tool-policy-vs-elevated) -- debugging "why is this blocked?"
|
||||
- [Multi-Agent Sandbox & Tools](/tools/multi-agent-sandbox-tools) -- per-agent overrides and precedence
|
||||
- [Security](/gateway/security)
|
||||
|
||||
@@ -52,6 +52,10 @@ Think of the suites as “increasing realism” (and increasing flakiness/cost):
|
||||
- Runs in CI
|
||||
- No real keys required
|
||||
- Should be fast and stable
|
||||
- Scheduler note:
|
||||
- `pnpm test` now keeps a small checked-in behavioral manifest for true pool/isolation overrides and a separate timing snapshot for the slowest unit files.
|
||||
- Shared unit coverage stays on, but the wrapper peels the heaviest measured files into dedicated lanes instead of relying on a growing hand-maintained exclusion list.
|
||||
- Refresh the timing snapshot with `pnpm test:perf:update-timings` after major suite shape changes.
|
||||
- Embedded runner note:
|
||||
- When you change message-tool discovery inputs or compaction runtime context,
|
||||
keep both levels of coverage.
|
||||
@@ -457,6 +461,55 @@ Future evals should stay deterministic first:
|
||||
- A small suite of skill-focused scenarios (use vs avoid, gating, prompt injection).
|
||||
- Optional live evals (opt-in, env-gated) only after the CI-safe suite is in place.
|
||||
|
||||
## Contract tests (plugin and channel shape)
|
||||
|
||||
Contract tests verify that every registered plugin and channel conforms to its
|
||||
interface contract. They iterate over all discovered plugins and run a suite of
|
||||
shape and behavior assertions.
|
||||
|
||||
### Commands
|
||||
|
||||
- All contracts: `pnpm test:contracts`
|
||||
- Channel contracts only: `pnpm test:contracts:channels`
|
||||
- Provider contracts only: `pnpm test:contracts:plugins`
|
||||
|
||||
### Channel contracts
|
||||
|
||||
Located in `src/channels/plugins/contracts/*.contract.test.ts`:
|
||||
|
||||
- **plugin** - Basic plugin shape (id, name, capabilities)
|
||||
- **setup** - Setup wizard contract
|
||||
- **session-binding** - Session binding behavior
|
||||
- **outbound-payload** - Message payload structure
|
||||
- **inbound** - Inbound message handling
|
||||
- **actions** - Channel action handlers
|
||||
- **threading** - Thread ID handling
|
||||
- **directory** - Directory/roster API
|
||||
- **group-policy** - Group policy enforcement
|
||||
- **status** - Channel status probes
|
||||
- **registry** - Plugin registry shape
|
||||
|
||||
### Provider contracts
|
||||
|
||||
Located in `src/plugins/contracts/*.contract.test.ts`:
|
||||
|
||||
- **auth** - Auth flow contract
|
||||
- **auth-choice** - Auth choice/selection
|
||||
- **catalog** - Model catalog API
|
||||
- **discovery** - Plugin discovery
|
||||
- **loader** - Plugin loading
|
||||
- **runtime** - Provider runtime
|
||||
- **shape** - Plugin shape/interface
|
||||
- **wizard** - Setup wizard
|
||||
|
||||
### When to run
|
||||
|
||||
- After changing plugin-sdk exports or subpaths
|
||||
- After adding or modifying a channel or provider plugin
|
||||
- After refactoring plugin registration or discovery
|
||||
|
||||
Contract tests run in CI and do not require real API keys.
|
||||
|
||||
## Adding regressions (guidance)
|
||||
|
||||
When you fix a provider/model issue discovered in live:
|
||||
|
||||
@@ -63,7 +63,7 @@ Example:
|
||||
}
|
||||
```
|
||||
|
||||
Reference: [/tools/plugin#distribution-npm](/tools/plugin#distribution-npm)
|
||||
Reference: [Plugin architecture](/plugins/architecture)
|
||||
|
||||
## Decision tree
|
||||
|
||||
|
||||
@@ -1,77 +1,119 @@
|
||||
---
|
||||
summary: "Stable, beta, and dev channels: semantics, switching, and tagging"
|
||||
summary: "Stable, beta, and dev channels: semantics, switching, pinning, and tagging"
|
||||
read_when:
|
||||
- You want to switch between stable/beta/dev
|
||||
- You want to pin a specific version, tag, or SHA
|
||||
- You are tagging or publishing prereleases
|
||||
title: "Development Channels"
|
||||
---
|
||||
|
||||
# Development channels
|
||||
|
||||
Last updated: 2026-01-21
|
||||
|
||||
OpenClaw ships three update channels:
|
||||
|
||||
- **stable**: npm dist-tag `latest`.
|
||||
- **stable**: npm dist-tag `latest`. Recommended for most users.
|
||||
- **beta**: npm dist-tag `beta` (builds under test).
|
||||
- **dev**: moving head of `main` (git). npm dist-tag: `dev` (when published).
|
||||
The `main` branch is for experimentation and active development. It may contain
|
||||
incomplete features or breaking changes. Do not use it for production gateways.
|
||||
|
||||
We ship builds to **beta**, test them, then **promote a vetted build to `latest`**
|
||||
without changing the version number — dist-tags are the source of truth for npm installs.
|
||||
without changing the version number -- dist-tags are the source of truth for npm installs.
|
||||
|
||||
## Switching channels
|
||||
|
||||
Git checkout:
|
||||
|
||||
```bash
|
||||
openclaw update --channel stable
|
||||
openclaw update --channel beta
|
||||
openclaw update --channel dev
|
||||
```
|
||||
|
||||
- `stable`/`beta` check out the latest matching tag (often the same tag).
|
||||
- `dev` switches to `main` and rebases on the upstream.
|
||||
`--channel` persists your choice in config (`update.channel`) and aligns the
|
||||
install method:
|
||||
|
||||
npm/pnpm global install:
|
||||
- **`stable`/`beta`** (package installs): updates via the matching npm dist-tag.
|
||||
- **`stable`/`beta`** (git installs): checks out the latest matching git tag.
|
||||
- **`dev`**: ensures a git checkout (default `~/openclaw`, override with
|
||||
`OPENCLAW_GIT_DIR`), switches to `main`, rebases on upstream, builds, and
|
||||
installs the global CLI from that checkout.
|
||||
|
||||
Tip: if you want stable + dev in parallel, keep two clones and point your
|
||||
gateway at the stable one.
|
||||
|
||||
## One-off version or tag targeting
|
||||
|
||||
Use `--tag` to target a specific dist-tag, version, or package spec for a single
|
||||
update **without** changing your persisted channel:
|
||||
|
||||
```bash
|
||||
openclaw update --channel stable
|
||||
openclaw update --channel beta
|
||||
openclaw update --channel dev
|
||||
# Install a specific version
|
||||
openclaw update --tag 2026.3.14
|
||||
|
||||
# Install from the beta dist-tag (one-off, does not persist)
|
||||
openclaw update --tag beta
|
||||
|
||||
# Install from GitHub main branch (npm tarball)
|
||||
openclaw update --tag main
|
||||
|
||||
# Install a specific npm package spec
|
||||
openclaw update --tag openclaw@2026.3.12
|
||||
```
|
||||
|
||||
This updates via the corresponding npm dist-tag (`latest`, `beta`, `dev`).
|
||||
Notes:
|
||||
|
||||
When you **explicitly** switch channels with `--channel`, OpenClaw also aligns
|
||||
the install method:
|
||||
- `--tag` applies to **package (npm) installs only**. Git installs ignore it.
|
||||
- The tag is not persisted. Your next `openclaw update` uses your configured
|
||||
channel as usual.
|
||||
- Downgrade protection: if the target version is older than your current version,
|
||||
OpenClaw prompts for confirmation (skip with `--yes`).
|
||||
|
||||
- `dev` ensures a git checkout (default `~/openclaw`, override with `OPENCLAW_GIT_DIR`),
|
||||
updates it, and installs the global CLI from that checkout.
|
||||
- `stable`/`beta` installs from npm using the matching dist-tag.
|
||||
## Dry run
|
||||
|
||||
Tip: if you want stable + dev in parallel, keep two clones and point your gateway at the stable one.
|
||||
Preview what `openclaw update` would do without making changes:
|
||||
|
||||
```bash
|
||||
openclaw update --dry-run
|
||||
openclaw update --channel beta --dry-run
|
||||
openclaw update --tag 2026.3.14 --dry-run
|
||||
openclaw update --dry-run --json
|
||||
```
|
||||
|
||||
The dry run shows the effective channel, target version, planned actions, and
|
||||
whether a downgrade confirmation would be required.
|
||||
|
||||
## Plugins and channels
|
||||
|
||||
When you switch channels with `openclaw update`, OpenClaw also syncs plugin sources:
|
||||
When you switch channels with `openclaw update`, OpenClaw also syncs plugin
|
||||
sources:
|
||||
|
||||
- `dev` prefers bundled plugins from the git checkout.
|
||||
- `stable` and `beta` restore npm-installed plugin packages.
|
||||
- npm-installed plugins are updated after the core update completes.
|
||||
|
||||
## Checking current status
|
||||
|
||||
```bash
|
||||
openclaw update status
|
||||
```
|
||||
|
||||
Shows the active channel, install kind (git or package), current version, and
|
||||
source (config, git tag, git branch, or default).
|
||||
|
||||
## Tagging best practices
|
||||
|
||||
- Tag releases you want git checkouts to land on (`vYYYY.M.D` for stable, `vYYYY.M.D-beta.N` for beta).
|
||||
- Tag releases you want git checkouts to land on (`vYYYY.M.D` for stable,
|
||||
`vYYYY.M.D-beta.N` for beta).
|
||||
- `vYYYY.M.D.beta.N` is also recognized for compatibility, but prefer `-beta.N`.
|
||||
- Legacy `vYYYY.M.D-<patch>` tags are still recognized as stable (non-beta).
|
||||
- Keep tags immutable: never move or reuse a tag.
|
||||
- npm dist-tags remain the source of truth for npm installs:
|
||||
- `latest` → stable
|
||||
- `beta` → candidate build
|
||||
- `dev` → main snapshot (optional)
|
||||
- `latest` -> stable
|
||||
- `beta` -> candidate build
|
||||
- `dev` -> main snapshot (optional)
|
||||
|
||||
## macOS app availability
|
||||
|
||||
Beta and dev builds may **not** include a macOS app release. That’s OK:
|
||||
Beta and dev builds may **not** include a macOS app release. That is OK:
|
||||
|
||||
- The git tag and npm dist-tag can still be published.
|
||||
- Call out “no macOS build for this beta” in release notes or changelog.
|
||||
- Call out "no macOS build for this beta" in release notes or changelog.
|
||||
|
||||
@@ -286,6 +286,7 @@ Available families:
|
||||
- `contacts.search`, `contacts.add`
|
||||
- `calendar.events`, `calendar.add`
|
||||
- `callLog.search`
|
||||
- `sms.search`
|
||||
- `motion.activity`, `motion.pedometer`
|
||||
|
||||
Example invokes:
|
||||
|
||||
@@ -164,4 +164,5 @@ See [Camera node](/nodes/camera) for parameters and CLI helpers.
|
||||
- `contacts.search`, `contacts.add`
|
||||
- `calendar.events`, `calendar.add`
|
||||
- `callLog.search`
|
||||
- `sms.search`
|
||||
- `motion.activity`, `motion.pedometer`
|
||||
|
||||
1335
docs/plugins/architecture.md
Normal file
1335
docs/plugins/architecture.md
Normal file
File diff suppressed because it is too large
Load Diff
205
docs/plugins/building-extensions.md
Normal file
205
docs/plugins/building-extensions.md
Normal file
@@ -0,0 +1,205 @@
|
||||
---
|
||||
title: "Building Extensions"
|
||||
summary: "Step-by-step guide for creating OpenClaw channel and provider extensions"
|
||||
read_when:
|
||||
- You want to create a new OpenClaw plugin or extension
|
||||
- You need to understand the plugin SDK import patterns
|
||||
- You are adding a new channel or provider to OpenClaw
|
||||
---
|
||||
|
||||
# Building Extensions
|
||||
|
||||
This guide walks through creating an OpenClaw extension from scratch. Extensions
|
||||
can add channels, model providers, tools, or other capabilities.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- OpenClaw repository cloned and dependencies installed (`pnpm install`)
|
||||
- Familiarity with TypeScript (ESM)
|
||||
|
||||
## Extension structure
|
||||
|
||||
Every extension lives under `extensions/<name>/` and follows this layout:
|
||||
|
||||
```
|
||||
extensions/my-channel/
|
||||
├── package.json # npm metadata + openclaw config
|
||||
├── index.ts # Entry point (defineChannelPluginEntry)
|
||||
├── setup-entry.ts # Setup wizard (optional)
|
||||
├── api.ts # Public contract barrel (optional)
|
||||
├── runtime-api.ts # Internal runtime barrel (optional)
|
||||
└── src/
|
||||
├── channel.ts # Channel adapter implementation
|
||||
├── runtime.ts # Runtime wiring
|
||||
└── *.test.ts # Colocated tests
|
||||
```
|
||||
|
||||
## Step 1: Create the package
|
||||
|
||||
Create `extensions/my-channel/package.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "@openclaw/my-channel",
|
||||
"version": "2026.1.1",
|
||||
"description": "OpenClaw My Channel plugin",
|
||||
"type": "module",
|
||||
"dependencies": {},
|
||||
"openclaw": {
|
||||
"extensions": ["./index.ts"],
|
||||
"setupEntry": "./setup-entry.ts",
|
||||
"channel": {
|
||||
"id": "my-channel",
|
||||
"label": "My Channel",
|
||||
"selectionLabel": "My Channel (plugin)",
|
||||
"docsPath": "/channels/my-channel",
|
||||
"docsLabel": "my-channel",
|
||||
"blurb": "Short description of the channel.",
|
||||
"order": 80
|
||||
},
|
||||
"install": {
|
||||
"npmSpec": "@openclaw/my-channel",
|
||||
"localPath": "extensions/my-channel"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `openclaw` field tells the plugin system what your extension provides.
|
||||
For provider plugins, use `providers` instead of `channel`.
|
||||
|
||||
## Step 2: Define the entry point
|
||||
|
||||
Create `extensions/my-channel/index.ts`:
|
||||
|
||||
```typescript
|
||||
import { defineChannelPluginEntry } from "openclaw/plugin-sdk/core";
|
||||
|
||||
export default defineChannelPluginEntry({
|
||||
id: "my-channel",
|
||||
name: "My Channel",
|
||||
description: "Connects OpenClaw to My Channel",
|
||||
plugin: {
|
||||
// Channel adapter implementation
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
For provider plugins, use `definePluginEntry` instead.
|
||||
|
||||
## Step 3: Import from focused subpaths
|
||||
|
||||
The plugin SDK exposes many focused subpaths. Always import from specific
|
||||
subpaths rather than the monolithic root:
|
||||
|
||||
```typescript
|
||||
// Correct: focused subpaths
|
||||
import { defineChannelPluginEntry } from "openclaw/plugin-sdk/core";
|
||||
import { createChannelReplyPipeline } from "openclaw/plugin-sdk/channel-reply-pipeline";
|
||||
import { createChannelPairingController } from "openclaw/plugin-sdk/channel-pairing";
|
||||
import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
|
||||
import { createOptionalChannelSetupSurface } from "openclaw/plugin-sdk/channel-setup";
|
||||
import { resolveChannelGroupRequireMention } from "openclaw/plugin-sdk/channel-policy";
|
||||
|
||||
// Wrong: monolithic root (lint will reject this)
|
||||
import { ... } from "openclaw/plugin-sdk";
|
||||
```
|
||||
|
||||
Common subpaths:
|
||||
|
||||
| Subpath | Purpose |
|
||||
| ----------------------------------- | ------------------------------------ |
|
||||
| `plugin-sdk/core` | Plugin entry definitions, base types |
|
||||
| `plugin-sdk/channel-setup` | Optional setup adapters/wizards |
|
||||
| `plugin-sdk/channel-pairing` | DM pairing primitives |
|
||||
| `plugin-sdk/channel-reply-pipeline` | Prefix + typing reply wiring |
|
||||
| `plugin-sdk/channel-config-schema` | Config schema builders |
|
||||
| `plugin-sdk/channel-policy` | Group/DM policy helpers |
|
||||
| `plugin-sdk/secret-input` | Secret input parsing/helpers |
|
||||
| `plugin-sdk/webhook-ingress` | Webhook request/target helpers |
|
||||
| `plugin-sdk/runtime-store` | Persistent plugin storage |
|
||||
| `plugin-sdk/allow-from` | Allowlist resolution |
|
||||
| `plugin-sdk/reply-payload` | Message reply types |
|
||||
| `plugin-sdk/provider-onboard` | Provider onboarding config patches |
|
||||
| `plugin-sdk/testing` | Test utilities |
|
||||
|
||||
Use the narrowest primitive that matches the job. Reach for `channel-runtime`
|
||||
or other larger helper barrels only when a dedicated subpath does not exist yet.
|
||||
|
||||
## Step 4: Use local barrels for internal imports
|
||||
|
||||
Within your extension, create barrel files for internal code sharing instead
|
||||
of importing through the plugin SDK:
|
||||
|
||||
```typescript
|
||||
// api.ts — public contract for this extension
|
||||
export { MyChannelConfig } from "./src/config.js";
|
||||
export { MyChannelRuntime } from "./src/runtime.js";
|
||||
|
||||
// runtime-api.ts — internal-only exports (not for production consumers)
|
||||
export { internalHelper } from "./src/helpers.js";
|
||||
```
|
||||
|
||||
**Self-import guardrail**: never import your own extension back through its
|
||||
published SDK contract path from production files. Route internal imports
|
||||
through `./api.ts` or `./runtime-api.ts` instead. The SDK contract is for
|
||||
external consumers only.
|
||||
|
||||
## Step 5: Add a plugin manifest
|
||||
|
||||
Create `openclaw.plugin.json` in your extension root:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "my-channel",
|
||||
"kind": "channel",
|
||||
"channels": ["my-channel"],
|
||||
"name": "My Channel Plugin",
|
||||
"description": "Connects OpenClaw to My Channel"
|
||||
}
|
||||
```
|
||||
|
||||
See [Plugin manifest](/plugins/manifest) for the full schema.
|
||||
|
||||
## Step 6: Test with contract tests
|
||||
|
||||
OpenClaw runs contract tests against all registered plugins. After adding your
|
||||
extension, run:
|
||||
|
||||
```bash
|
||||
pnpm test:contracts:channels # channel plugins
|
||||
pnpm test:contracts:plugins # provider plugins
|
||||
```
|
||||
|
||||
Contract tests verify your plugin conforms to the expected interface (setup
|
||||
wizard, session binding, message handling, group policy, etc.).
|
||||
|
||||
For unit tests, import test helpers from the public testing surface:
|
||||
|
||||
```typescript
|
||||
import { createTestRuntime } from "openclaw/plugin-sdk/testing";
|
||||
```
|
||||
|
||||
## Lint enforcement
|
||||
|
||||
Three scripts enforce SDK boundaries:
|
||||
|
||||
1. **No monolithic root imports** — `openclaw/plugin-sdk` root is rejected
|
||||
2. **No direct src/ imports** — extensions cannot import `../../src/` directly
|
||||
3. **No self-imports** — extensions cannot import their own `plugin-sdk/<name>` subpath
|
||||
|
||||
Run `pnpm check` to verify all boundaries before committing.
|
||||
|
||||
## Checklist
|
||||
|
||||
Before submitting your extension:
|
||||
|
||||
- [ ] `package.json` has correct `openclaw` metadata
|
||||
- [ ] Entry point uses `defineChannelPluginEntry` or `definePluginEntry`
|
||||
- [ ] All imports use focused `plugin-sdk/<subpath>` paths
|
||||
- [ ] Internal imports use local barrels, not SDK self-imports
|
||||
- [ ] `openclaw.plugin.json` manifest is present and valid
|
||||
- [ ] Contract tests pass (`pnpm test:contracts`)
|
||||
- [ ] Unit tests colocated as `*.test.ts`
|
||||
- [ ] `pnpm check` passes (lint + format)
|
||||
- [ ] Doc page created under `docs/channels/` or `docs/plugins/`
|
||||
@@ -33,7 +33,7 @@ plugin errors and block config validation.
|
||||
|
||||
See the full plugin system guide: [Plugins](/tools/plugin).
|
||||
For the native capability model and current external-compatibility guidance:
|
||||
[Capability model](/tools/plugin#public-capability-model).
|
||||
[Capability model](/plugins/architecture#public-capability-model).
|
||||
|
||||
## Required fields
|
||||
|
||||
@@ -135,7 +135,7 @@ See [Configuration reference](/configuration) for the full `plugins.*` schema.
|
||||
`--auth-choice` resolution, preferred-provider mapping, and simple onboarding
|
||||
CLI flag registration before provider runtime loads. For runtime wizard
|
||||
metadata that requires provider code, see
|
||||
[Provider runtime hooks](/tools/plugin#provider-runtime-hooks).
|
||||
[Provider runtime hooks](/plugins/architecture#provider-runtime-hooks).
|
||||
- Exclusive plugin kinds are selected through `plugins.slots.*`.
|
||||
- `kind: "memory"` is selected by `plugins.slots.memory`.
|
||||
- `kind: "context-engine"` is selected by `plugins.slots.contextEngine`
|
||||
|
||||
@@ -312,14 +312,21 @@ Auto-responses use the agent system. Tune with:
|
||||
|
||||
```bash
|
||||
openclaw voicecall call --to "+15555550123" --message "Hello from OpenClaw"
|
||||
openclaw voicecall start --to "+15555550123" # alias for call
|
||||
openclaw voicecall continue --call-id <id> --message "Any questions?"
|
||||
openclaw voicecall speak --call-id <id> --message "One moment"
|
||||
openclaw voicecall end --call-id <id>
|
||||
openclaw voicecall status --call-id <id>
|
||||
openclaw voicecall tail
|
||||
openclaw voicecall latency # summarize turn latency from logs
|
||||
openclaw voicecall expose --mode funnel
|
||||
```
|
||||
|
||||
`latency` reads `calls.jsonl` from the default voice-call storage path. Use
|
||||
`--file <path>` to point at a different log and `--last <n>` to limit analysis
|
||||
to the last N records (default 200). Output includes p50/p90/p99 for turn
|
||||
latency and listen-wait times.
|
||||
|
||||
## Agent tool
|
||||
|
||||
Tool name: `voice_call`
|
||||
|
||||
78
docs/providers/google.md
Normal file
78
docs/providers/google.md
Normal file
@@ -0,0 +1,78 @@
|
||||
---
|
||||
title: "Google (Gemini)"
|
||||
summary: "Google Gemini setup (API key + OAuth, image generation, media understanding, web search)"
|
||||
read_when:
|
||||
- You want to use Google Gemini models with OpenClaw
|
||||
- You need the API key or OAuth auth flow
|
||||
---
|
||||
|
||||
# Google (Gemini)
|
||||
|
||||
The Google plugin provides access to Gemini models through Google AI Studio, plus
|
||||
image generation, media understanding (image/audio/video), and web search via
|
||||
Gemini Grounding.
|
||||
|
||||
- Provider: `google`
|
||||
- Auth: `GEMINI_API_KEY` or `GOOGLE_API_KEY`
|
||||
- API: Google Gemini API
|
||||
- Alternative provider: `google-gemini-cli` (OAuth)
|
||||
|
||||
## Quick start
|
||||
|
||||
1. Set the API key:
|
||||
|
||||
```bash
|
||||
openclaw onboard --auth-choice google-api-key
|
||||
```
|
||||
|
||||
2. Set a default model:
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
model: { primary: "google/gemini-3.1-pro-preview" },
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Non-interactive example
|
||||
|
||||
```bash
|
||||
openclaw onboard --non-interactive \
|
||||
--mode local \
|
||||
--auth-choice google-api-key \
|
||||
--gemini-api-key "$GEMINI_API_KEY"
|
||||
```
|
||||
|
||||
## OAuth (Gemini CLI)
|
||||
|
||||
An alternative provider `google-gemini-cli` uses PKCE OAuth instead of an API
|
||||
key. This is an unofficial integration; some users report account
|
||||
restrictions. Use at your own risk.
|
||||
|
||||
Environment variables:
|
||||
|
||||
- `OPENCLAW_GEMINI_OAUTH_CLIENT_ID`
|
||||
- `OPENCLAW_GEMINI_OAUTH_CLIENT_SECRET`
|
||||
|
||||
(Or the `GEMINI_CLI_*` variants.)
|
||||
|
||||
## Capabilities
|
||||
|
||||
| Capability | Supported |
|
||||
| ---------------------- | ----------------- |
|
||||
| Chat completions | Yes |
|
||||
| Image generation | Yes |
|
||||
| Image understanding | Yes |
|
||||
| Audio transcription | Yes |
|
||||
| Video understanding | Yes |
|
||||
| Web search (Grounding) | Yes |
|
||||
| Thinking/reasoning | Yes (Gemini 3.1+) |
|
||||
|
||||
## Environment note
|
||||
|
||||
If the Gateway runs as a daemon (launchd/systemd), make sure `GEMINI_API_KEY`
|
||||
is available to that process (for example, in `~/.openclaw/.env` or via
|
||||
`env.shellEnv`).
|
||||
@@ -30,23 +30,28 @@ Looking for chat channel docs (WhatsApp/Telegram/Discord/Slack/Mattermost (plugi
|
||||
- [Anthropic (API + Claude Code CLI)](/providers/anthropic)
|
||||
- [Cloudflare AI Gateway](/providers/cloudflare-ai-gateway)
|
||||
- [GLM models](/providers/glm)
|
||||
- [Google (Gemini)](/providers/google)
|
||||
- [Hugging Face (Inference)](/providers/huggingface)
|
||||
- [Kilocode](/providers/kilocode)
|
||||
- [LiteLLM (unified gateway)](/providers/litellm)
|
||||
- [MiniMax](/providers/minimax)
|
||||
- [Mistral](/providers/mistral)
|
||||
- [Model Studio (Alibaba Cloud)](/providers/modelstudio)
|
||||
- [Moonshot AI (Kimi + Kimi Coding)](/providers/moonshot)
|
||||
- [NVIDIA](/providers/nvidia)
|
||||
- [Ollama (cloud + local models)](/providers/ollama)
|
||||
- [OpenAI (API + Codex)](/providers/openai)
|
||||
- [OpenCode (Zen + Go)](/providers/opencode)
|
||||
- [OpenRouter](/providers/openrouter)
|
||||
- [Perplexity (web search)](/providers/perplexity-provider)
|
||||
- [Qianfan](/providers/qianfan)
|
||||
- [Qwen (OAuth)](/providers/qwen)
|
||||
- [SGLang (local models)](/providers/sglang)
|
||||
- [Together AI](/providers/together)
|
||||
- [Vercel AI Gateway](/providers/vercel-ai-gateway)
|
||||
- [Venice (Venice AI, privacy-focused)](/providers/venice)
|
||||
- [vLLM (local models)](/providers/vllm)
|
||||
- [Volcengine (Doubao)](/providers/volcengine)
|
||||
- [xAI](/providers/xai)
|
||||
- [Xiaomi](/providers/xiaomi)
|
||||
- [Z.AI](/providers/zai)
|
||||
|
||||
66
docs/providers/modelstudio.md
Normal file
66
docs/providers/modelstudio.md
Normal file
@@ -0,0 +1,66 @@
|
||||
---
|
||||
title: "Model Studio"
|
||||
summary: "Alibaba Cloud Model Studio setup (Coding Plan, dual region endpoints)"
|
||||
read_when:
|
||||
- You want to use Alibaba Cloud Model Studio with OpenClaw
|
||||
- You need the API key env var for Model Studio
|
||||
---
|
||||
|
||||
# Model Studio (Alibaba Cloud)
|
||||
|
||||
The Model Studio provider gives access to Alibaba Cloud Coding Plan models,
|
||||
including Qwen and third-party models hosted on the platform.
|
||||
|
||||
- Provider: `modelstudio`
|
||||
- Auth: `MODELSTUDIO_API_KEY`
|
||||
- API: OpenAI-compatible
|
||||
|
||||
## Quick start
|
||||
|
||||
1. Set the API key:
|
||||
|
||||
```bash
|
||||
openclaw onboard --auth-choice modelstudio-api-key
|
||||
```
|
||||
|
||||
2. Set a default model:
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
model: { primary: "modelstudio/qwen3.5-plus" },
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Region endpoints
|
||||
|
||||
Model Studio has two endpoints based on region:
|
||||
|
||||
| Region | Endpoint |
|
||||
| ---------- | ------------------------------------ |
|
||||
| China (CN) | `coding.dashscope.aliyuncs.com` |
|
||||
| Global | `coding-intl.dashscope.aliyuncs.com` |
|
||||
|
||||
The provider auto-selects based on the auth choice (`modelstudio-api-key` for
|
||||
global, `modelstudio-api-key-cn` for China). You can override with a custom
|
||||
`baseUrl` in config.
|
||||
|
||||
## Available models
|
||||
|
||||
- **qwen3.5-plus** (default) - Qwen 3.5 Plus
|
||||
- **qwen3-max** - Qwen 3 Max
|
||||
- **qwen3-coder** series - Qwen coding models
|
||||
- **GLM-5**, **GLM-4.7** - GLM models via Alibaba
|
||||
- **Kimi K2.5** - Moonshot AI via Alibaba
|
||||
- **MiniMax-M2.5** - MiniMax via Alibaba
|
||||
|
||||
Most models support image input. Context windows range from 200K to 1M tokens.
|
||||
|
||||
## Environment note
|
||||
|
||||
If the Gateway runs as a daemon (launchd/systemd), make sure
|
||||
`MODELSTUDIO_API_KEY` is available to that process (for example, in
|
||||
`~/.openclaw/.env` or via `env.shellEnv`).
|
||||
56
docs/providers/perplexity-provider.md
Normal file
56
docs/providers/perplexity-provider.md
Normal file
@@ -0,0 +1,56 @@
|
||||
---
|
||||
title: "Perplexity (Provider)"
|
||||
summary: "Perplexity web search provider setup (API key, search modes, filtering)"
|
||||
read_when:
|
||||
- You want to configure Perplexity as a web search provider
|
||||
- You need the Perplexity API key or OpenRouter proxy setup
|
||||
---
|
||||
|
||||
# Perplexity (Web Search Provider)
|
||||
|
||||
The Perplexity plugin provides web search capabilities through the Perplexity
|
||||
Search API or Perplexity Sonar via OpenRouter.
|
||||
|
||||
<Note>
|
||||
This page covers the Perplexity **provider** setup. For the Perplexity
|
||||
**tool** (how the agent uses it), see [Perplexity tool](/perplexity).
|
||||
</Note>
|
||||
|
||||
- Type: web search provider (not a model provider)
|
||||
- Auth: `PERPLEXITY_API_KEY` (direct) or `OPENROUTER_API_KEY` (via OpenRouter)
|
||||
- Config path: `tools.web.search.perplexity.apiKey`
|
||||
|
||||
## Quick start
|
||||
|
||||
1. Set the API key:
|
||||
|
||||
```bash
|
||||
openclaw config set tools.web.search.perplexity.apiKey "pplx-xxxxxxxxxxxx"
|
||||
```
|
||||
|
||||
2. The agent will automatically use Perplexity for web searches when configured.
|
||||
|
||||
## Search modes
|
||||
|
||||
The plugin auto-selects the transport based on API key prefix:
|
||||
|
||||
| Key prefix | Transport | Features |
|
||||
| ---------- | ---------------------------- | ------------------------------------------------ |
|
||||
| `pplx-` | Native Perplexity Search API | Structured results, domain/language/date filters |
|
||||
| `sk-or-` | OpenRouter (Sonar) | AI-synthesized answers with citations |
|
||||
|
||||
## Native API filtering
|
||||
|
||||
When using the native Perplexity API (`pplx-` key), searches support:
|
||||
|
||||
- **Country**: 2-letter country code
|
||||
- **Language**: ISO 639-1 language code
|
||||
- **Date range**: day, week, month, year
|
||||
- **Domain filters**: allowlist/denylist (max 20 domains)
|
||||
- **Content budget**: `max_tokens`, `max_tokens_per_page`
|
||||
|
||||
## Environment note
|
||||
|
||||
If the Gateway runs as a daemon (launchd/systemd), make sure
|
||||
`PERPLEXITY_API_KEY` is available to that process (for example, in
|
||||
`~/.openclaw/.env` or via `env.shellEnv`).
|
||||
74
docs/providers/volcengine.md
Normal file
74
docs/providers/volcengine.md
Normal file
@@ -0,0 +1,74 @@
|
||||
---
|
||||
title: "Volcengine (Doubao)"
|
||||
summary: "Volcano Engine setup (Doubao models, general + coding endpoints)"
|
||||
read_when:
|
||||
- You want to use Volcano Engine or Doubao models with OpenClaw
|
||||
- You need the Volcengine API key setup
|
||||
---
|
||||
|
||||
# Volcengine (Doubao)
|
||||
|
||||
The Volcengine provider gives access to Doubao models and third-party models
|
||||
hosted on Volcano Engine, with separate endpoints for general and coding
|
||||
workloads.
|
||||
|
||||
- Providers: `volcengine` (general) + `volcengine-plan` (coding)
|
||||
- Auth: `VOLCANO_ENGINE_API_KEY`
|
||||
- API: OpenAI-compatible
|
||||
|
||||
## Quick start
|
||||
|
||||
1. Set the API key:
|
||||
|
||||
```bash
|
||||
openclaw onboard --auth-choice volcengine-api-key
|
||||
```
|
||||
|
||||
2. Set a default model:
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
model: { primary: "volcengine-plan/ark-code-latest" },
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Non-interactive example
|
||||
|
||||
```bash
|
||||
openclaw onboard --non-interactive \
|
||||
--mode local \
|
||||
--auth-choice volcengine-api-key \
|
||||
--volcengine-api-key "$VOLCANO_ENGINE_API_KEY"
|
||||
```
|
||||
|
||||
## Providers and endpoints
|
||||
|
||||
| Provider | Endpoint | Use case |
|
||||
| ----------------- | ----------------------------------------- | -------------- |
|
||||
| `volcengine` | `ark.cn-beijing.volces.com/api/v3` | General models |
|
||||
| `volcengine-plan` | `ark.cn-beijing.volces.com/api/coding/v3` | Coding models |
|
||||
|
||||
Both providers are configured from a single API key. Setup registers both
|
||||
automatically.
|
||||
|
||||
## Available models
|
||||
|
||||
- **doubao-seed-1-8** - Doubao Seed 1.8 (general, default)
|
||||
- **doubao-seed-code-preview** - Doubao coding model
|
||||
- **ark-code-latest** - Coding plan default
|
||||
- **Kimi K2.5** - Moonshot AI via Volcano Engine
|
||||
- **GLM-4.7** - GLM via Volcano Engine
|
||||
- **DeepSeek V3.2** - DeepSeek via Volcano Engine
|
||||
|
||||
Most models support text + image input. Context windows range from 128K to 256K
|
||||
tokens.
|
||||
|
||||
## Environment note
|
||||
|
||||
If the Gateway runs as a daemon (launchd/systemd), make sure
|
||||
`VOLCANO_ENGINE_API_KEY` is available to that process (for example, in
|
||||
`~/.openclaw/.env` or via `env.shellEnv`).
|
||||
@@ -5,6 +5,8 @@ read_when:
|
||||
title: "Credits"
|
||||
---
|
||||
|
||||
# Credits and Acknowledgments
|
||||
|
||||
## The name
|
||||
|
||||
OpenClaw = CLAW + TARDIS, because every space lobster needs a time and space machine.
|
||||
|
||||
@@ -38,6 +38,11 @@ Scope intent:
|
||||
- `plugins.entries.moonshot.config.webSearch.apiKey`
|
||||
- `plugins.entries.perplexity.config.webSearch.apiKey`
|
||||
- `plugins.entries.firecrawl.config.webSearch.apiKey`
|
||||
- `tools.web.search.apiKey`
|
||||
- `tools.web.search.gemini.apiKey`
|
||||
- `tools.web.search.grok.apiKey`
|
||||
- `tools.web.search.kimi.apiKey`
|
||||
- `tools.web.search.perplexity.apiKey`
|
||||
- `gateway.auth.password`
|
||||
- `gateway.auth.token`
|
||||
- `gateway.remote.token`
|
||||
|
||||
@@ -447,6 +447,48 @@
|
||||
"secretShape": "secret_input",
|
||||
"optIn": true
|
||||
},
|
||||
{
|
||||
"id": "plugins.entries.brave.config.webSearch.apiKey",
|
||||
"configFile": "openclaw.json",
|
||||
"path": "plugins.entries.brave.config.webSearch.apiKey",
|
||||
"secretShape": "secret_input",
|
||||
"optIn": true
|
||||
},
|
||||
{
|
||||
"id": "plugins.entries.firecrawl.config.webSearch.apiKey",
|
||||
"configFile": "openclaw.json",
|
||||
"path": "plugins.entries.firecrawl.config.webSearch.apiKey",
|
||||
"secretShape": "secret_input",
|
||||
"optIn": true
|
||||
},
|
||||
{
|
||||
"id": "plugins.entries.google.config.webSearch.apiKey",
|
||||
"configFile": "openclaw.json",
|
||||
"path": "plugins.entries.google.config.webSearch.apiKey",
|
||||
"secretShape": "secret_input",
|
||||
"optIn": true
|
||||
},
|
||||
{
|
||||
"id": "plugins.entries.moonshot.config.webSearch.apiKey",
|
||||
"configFile": "openclaw.json",
|
||||
"path": "plugins.entries.moonshot.config.webSearch.apiKey",
|
||||
"secretShape": "secret_input",
|
||||
"optIn": true
|
||||
},
|
||||
{
|
||||
"id": "plugins.entries.perplexity.config.webSearch.apiKey",
|
||||
"configFile": "openclaw.json",
|
||||
"path": "plugins.entries.perplexity.config.webSearch.apiKey",
|
||||
"secretShape": "secret_input",
|
||||
"optIn": true
|
||||
},
|
||||
{
|
||||
"id": "plugins.entries.xai.config.webSearch.apiKey",
|
||||
"configFile": "openclaw.json",
|
||||
"path": "plugins.entries.xai.config.webSearch.apiKey",
|
||||
"secretShape": "secret_input",
|
||||
"optIn": true
|
||||
},
|
||||
{
|
||||
"id": "skills.entries.*.apiKey",
|
||||
"configFile": "openclaw.json",
|
||||
@@ -476,44 +518,37 @@
|
||||
"optIn": true
|
||||
},
|
||||
{
|
||||
"id": "plugins.entries.brave.config.webSearch.apiKey",
|
||||
"id": "tools.web.search.apiKey",
|
||||
"configFile": "openclaw.json",
|
||||
"path": "plugins.entries.brave.config.webSearch.apiKey",
|
||||
"path": "tools.web.search.apiKey",
|
||||
"secretShape": "secret_input",
|
||||
"optIn": true
|
||||
},
|
||||
{
|
||||
"id": "plugins.entries.google.config.webSearch.apiKey",
|
||||
"id": "tools.web.search.gemini.apiKey",
|
||||
"configFile": "openclaw.json",
|
||||
"path": "plugins.entries.google.config.webSearch.apiKey",
|
||||
"path": "tools.web.search.gemini.apiKey",
|
||||
"secretShape": "secret_input",
|
||||
"optIn": true
|
||||
},
|
||||
{
|
||||
"id": "plugins.entries.xai.config.webSearch.apiKey",
|
||||
"id": "tools.web.search.grok.apiKey",
|
||||
"configFile": "openclaw.json",
|
||||
"path": "plugins.entries.xai.config.webSearch.apiKey",
|
||||
"path": "tools.web.search.grok.apiKey",
|
||||
"secretShape": "secret_input",
|
||||
"optIn": true
|
||||
},
|
||||
{
|
||||
"id": "plugins.entries.moonshot.config.webSearch.apiKey",
|
||||
"id": "tools.web.search.kimi.apiKey",
|
||||
"configFile": "openclaw.json",
|
||||
"path": "plugins.entries.moonshot.config.webSearch.apiKey",
|
||||
"path": "tools.web.search.kimi.apiKey",
|
||||
"secretShape": "secret_input",
|
||||
"optIn": true
|
||||
},
|
||||
{
|
||||
"id": "plugins.entries.perplexity.config.webSearch.apiKey",
|
||||
"id": "tools.web.search.perplexity.apiKey",
|
||||
"configFile": "openclaw.json",
|
||||
"path": "plugins.entries.perplexity.config.webSearch.apiKey",
|
||||
"secretShape": "secret_input",
|
||||
"optIn": true
|
||||
},
|
||||
{
|
||||
"id": "plugins.entries.firecrawl.config.webSearch.apiKey",
|
||||
"configFile": "openclaw.json",
|
||||
"path": "plugins.entries.firecrawl.config.webSearch.apiKey",
|
||||
"path": "tools.web.search.perplexity.apiKey",
|
||||
"secretShape": "secret_input",
|
||||
"optIn": true
|
||||
}
|
||||
|
||||
@@ -5,8 +5,10 @@ read_when:
|
||||
- Bootstrapping a workspace manually
|
||||
---
|
||||
|
||||
# HEARTBEAT.md
|
||||
# HEARTBEAT.md Template
|
||||
|
||||
```markdown
|
||||
# Keep this file empty (or with only comments) to skip heartbeat API calls.
|
||||
|
||||
# Add tasks below when you want the agent to check something periodically.
|
||||
```
|
||||
|
||||
@@ -12,9 +12,10 @@ title: "Tests"
|
||||
- `pnpm test:force`: Kills any lingering gateway process holding the default control port, then runs the full Vitest suite with an isolated gateway port so server tests don’t collide with a running instance. Use this when a prior gateway run left port 18789 occupied.
|
||||
- `pnpm test:coverage`: Runs the unit suite with V8 coverage (via `vitest.unit.config.ts`). Global thresholds are 70% lines/branches/functions/statements. Coverage excludes integration-heavy entrypoints (CLI wiring, gateway/telegram bridges, webchat static server) to keep the target focused on unit-testable logic.
|
||||
- `pnpm test` on Node 22, 23, and 24 uses Vitest `vmForks` by default for faster startup. Node 25+ falls back to `forks` until re-validated. You can force behavior with `OPENCLAW_TEST_VM_FORKS=0|1`.
|
||||
- `pnpm test`: runs the fast core unit lane by default for quick local feedback.
|
||||
- `pnpm test`: runs the full wrapper. It keeps only a small behavioral override manifest in git, then uses a checked-in timing snapshot to peel the heaviest measured unit files into dedicated lanes.
|
||||
- `pnpm test:channels`: runs channel-heavy suites.
|
||||
- `pnpm test:extensions`: runs extension/plugin suites.
|
||||
- `pnpm test:perf:update-timings`: refreshes the checked-in slow-file timing snapshot used by `scripts/test-parallel.mjs`.
|
||||
- Gateway integration: opt-in via `OPENCLAW_TEST_INCLUDE_GATEWAY=1 pnpm test` or `pnpm test:gateway`.
|
||||
- `pnpm test:e2e`: Runs gateway end-to-end smoke tests (multi-instance WS/HTTP/node pairing). Defaults to `vmForks` + adaptive workers in `vitest.e2e.config.ts`; tune with `OPENCLAW_E2E_WORKERS=<n>` and set `OPENCLAW_E2E_VERBOSE=1` for verbose logs.
|
||||
- `pnpm test:live`: Runs provider live tests (minimax/zai). Requires API keys and `LIVE=1` (or provider-specific `*_LIVE_TEST=1`) to unskip.
|
||||
|
||||
@@ -5,6 +5,8 @@ read_when:
|
||||
title: "Docs directory"
|
||||
---
|
||||
|
||||
# Docs Directory
|
||||
|
||||
<Note>
|
||||
This page is a curated index. If you are new, start with [Getting Started](/start/getting-started).
|
||||
For a complete map of the docs, see [Docs hubs](/start/hubs).
|
||||
|
||||
@@ -162,6 +162,18 @@ Use these hubs to discover every page, including deep dives and reference docs t
|
||||
- [macOS skills](/platforms/mac/skills)
|
||||
- [macOS Peekaboo](/platforms/mac/peekaboo)
|
||||
|
||||
## Extensions + plugins
|
||||
|
||||
- [Plugins overview](/tools/plugin)
|
||||
- [Building extensions](/plugins/building-extensions)
|
||||
- [Plugin manifest](/plugins/manifest)
|
||||
- [Agent tools](/plugins/agent-tools)
|
||||
- [Plugin bundles](/plugins/bundles)
|
||||
- [Community plugins](/plugins/community)
|
||||
- [Capability cookbook](/tools/capability-cookbook)
|
||||
- [Voice call plugin](/plugins/voice-call)
|
||||
- [Zalo user plugin](/plugins/zalouser)
|
||||
|
||||
## Workspace + templates
|
||||
|
||||
- [Skills](/tools/skills)
|
||||
|
||||
@@ -111,6 +111,7 @@ All fields are optional unless noted:
|
||||
- `lang` (`string`): language override hint for before and after mode.
|
||||
- `title` (`string`): viewer title override.
|
||||
- `mode` (`"view" | "file" | "both"`): output mode. Defaults to plugin default `defaults.mode`.
|
||||
Deprecated alias: `"image"` behaves like `"file"` and is still accepted for backward compatibility.
|
||||
- `theme` (`"light" | "dark"`): viewer theme. Defaults to plugin default `defaults.theme`.
|
||||
- `layout` (`"unified" | "split"`): diff layout. Defaults to plugin default `defaults.layout`.
|
||||
- `expandUnchanged` (`boolean`): expand unchanged sections when full context is available. Per-call option only (not a plugin default key).
|
||||
@@ -150,9 +151,12 @@ Shared fields for modes that create a viewer:
|
||||
- `inputKind`
|
||||
- `fileCount`
|
||||
- `mode`
|
||||
- `context` (`agentId`, `sessionId`, `messageChannel`, `agentAccountId` when available)
|
||||
|
||||
File fields when PNG or PDF is rendered:
|
||||
|
||||
- `artifactId`
|
||||
- `expiresAt`
|
||||
- `filePath`
|
||||
- `path` (same value as `filePath`, for message tool compatibility)
|
||||
- `fileBytes`
|
||||
|
||||
@@ -1,40 +1,25 @@
|
||||
---
|
||||
summary: "Per-agent sandbox + tool restrictions, precedence, and examples"
|
||||
summary: “Per-agent sandbox + tool restrictions, precedence, and examples”
|
||||
title: Multi-Agent Sandbox & Tools
|
||||
read_when: "You want per-agent sandboxing or per-agent tool allow/deny policies in a multi-agent gateway."
|
||||
read_when: “You want per-agent sandboxing or per-agent tool allow/deny policies in a multi-agent gateway.”
|
||||
status: active
|
||||
---
|
||||
|
||||
# Multi-Agent Sandbox & Tools Configuration
|
||||
|
||||
## Overview
|
||||
Each agent in a multi-agent setup can override the global sandbox and tool
|
||||
policy. This page covers per-agent configuration, precedence rules, and
|
||||
examples.
|
||||
|
||||
Each agent in a multi-agent setup can now have its own:
|
||||
|
||||
- **Sandbox configuration** (`agents.list[].sandbox` overrides `agents.defaults.sandbox`)
|
||||
- **Tool restrictions** (`tools.allow` / `tools.deny`, plus `agents.list[].tools`)
|
||||
|
||||
This allows you to run multiple agents with different security profiles:
|
||||
|
||||
- Personal assistant with full access
|
||||
- Family/work agents with restricted tools
|
||||
- Public-facing agents in sandboxes
|
||||
|
||||
`setupCommand` belongs under `sandbox.docker` (global or per-agent) and runs once
|
||||
when the container is created.
|
||||
|
||||
Auth is per-agent: each agent reads from its own `agentDir` auth store at:
|
||||
|
||||
```
|
||||
~/.openclaw/agents/<agentId>/agent/auth-profiles.json
|
||||
```
|
||||
- **Sandbox backends and modes**: see [Sandboxing](/gateway/sandboxing).
|
||||
- **Debugging blocked tools**: see [Sandbox vs Tool Policy vs Elevated](/gateway/sandbox-vs-tool-policy-vs-elevated) and `openclaw sandbox explain`.
|
||||
- **Elevated exec**: see [Elevated Mode](/tools/elevated).
|
||||
|
||||
Auth is per-agent: each agent reads from its own `agentDir` auth store at
|
||||
`~/.openclaw/agents/<agentId>/agent/auth-profiles.json`.
|
||||
Credentials are **not** shared between agents. Never reuse `agentDir` across agents.
|
||||
If you want to share creds, copy `auth-profiles.json` into the other agent's `agentDir`.
|
||||
|
||||
For how sandboxing behaves at runtime, see [Sandboxing](/gateway/sandboxing).
|
||||
For debugging “why is this blocked?”, see [Sandbox vs Tool Policy vs Elevated](/gateway/sandbox-vs-tool-policy-vs-elevated) and `openclaw sandbox explain`.
|
||||
|
||||
---
|
||||
|
||||
## Configuration Examples
|
||||
@@ -222,30 +207,9 @@ If `agents.list[].tools.sandbox.tools` is set, it replaces `tools.sandbox.tools`
|
||||
If `agents.list[].tools.profile` is set, it overrides `tools.profile` for that agent.
|
||||
Provider tool keys accept either `provider` (e.g. `google-antigravity`) or `provider/model` (e.g. `openai/gpt-5.2`).
|
||||
|
||||
### Tool groups (shorthands)
|
||||
Tool policies support `group:*` shorthands that expand to multiple tools. See [Tool groups](/gateway/sandbox-vs-tool-policy-vs-elevated#tool-groups-shorthands) for the full list.
|
||||
|
||||
Tool policies (global, agent, sandbox) support `group:*` entries that expand to multiple concrete tools:
|
||||
|
||||
- `group:runtime`: `exec`, `bash`, `process`
|
||||
- `group:fs`: `read`, `write`, `edit`, `apply_patch`
|
||||
- `group:sessions`: `sessions_list`, `sessions_history`, `sessions_send`, `sessions_spawn`, `session_status`
|
||||
- `group:memory`: `memory_search`, `memory_get`
|
||||
- `group:ui`: `browser`, `canvas`
|
||||
- `group:automation`: `cron`, `gateway`
|
||||
- `group:messaging`: `message`
|
||||
- `group:nodes`: `nodes`
|
||||
- `group:openclaw`: all built-in OpenClaw tools (excludes provider plugins)
|
||||
|
||||
### Elevated Mode
|
||||
|
||||
`tools.elevated` is the global baseline (sender-based allowlist). `agents.list[].tools.elevated` can further restrict elevated for specific agents (both must allow).
|
||||
|
||||
Mitigation patterns:
|
||||
|
||||
- Deny `exec` for untrusted agents (`agents.list[].tools.deny: ["exec"]`)
|
||||
- Avoid allowlisting senders that route to restricted agents
|
||||
- Disable elevated globally (`tools.elevated.enabled: false`) if you only want sandboxed execution
|
||||
- Disable elevated per agent (`agents.list[].tools.elevated.enabled: false`) for sensitive profiles
|
||||
Per-agent elevated overrides (`agents.list[].tools.elevated`) can further restrict elevated exec for specific agents. See [Elevated Mode](/tools/elevated) for details.
|
||||
|
||||
---
|
||||
|
||||
@@ -390,8 +354,11 @@ After configuring multi-agent sandbox and tools:
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
## See also
|
||||
|
||||
- [Sandboxing](/gateway/sandboxing) -- full sandbox reference (modes, scopes, backends, images)
|
||||
- [Sandbox vs Tool Policy vs Elevated](/gateway/sandbox-vs-tool-policy-vs-elevated) -- debugging "why is this blocked?"
|
||||
- [Elevated Mode](/tools/elevated)
|
||||
- [Multi-Agent Routing](/concepts/multi-agent)
|
||||
- [Sandbox Configuration](/gateway/configuration#agentsdefaults-sandbox)
|
||||
- [Session Management](/concepts/session)
|
||||
|
||||
2440
docs/tools/plugin.md
2440
docs/tools/plugin.md
File diff suppressed because it is too large
Load Diff
@@ -2,8 +2,8 @@ import { spawn } from "node:child_process";
|
||||
import { chmod, mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { createWindowsCmdShimFixture } from "openclaw/plugin-sdk/testing";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { createWindowsCmdShimFixture } from "../../../shared/windows-cmd-shim-test-fixtures.js";
|
||||
import {
|
||||
resolveSpawnCommand,
|
||||
spawnAndCollect,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { AcpRuntime, OpenClawPluginServiceContext } from "openclaw/plugin-sdk/acpx";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { AcpRuntimeError } from "../../../src/acp/runtime/errors.js";
|
||||
import {
|
||||
@@ -6,6 +5,7 @@ import {
|
||||
getAcpRuntimeBackend,
|
||||
requireAcpRuntimeBackend,
|
||||
} from "../../../src/acp/runtime/registry.js";
|
||||
import type { AcpRuntime, OpenClawPluginServiceContext } from "../runtime-api.js";
|
||||
import { ACPX_BUNDLED_BIN, ACPX_PINNED_VERSION } from "./config.js";
|
||||
import { createAcpxRuntimeService } from "./service.js";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { definePluginEntry } from "openclaw/plugin-sdk/core";
|
||||
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
|
||||
import {
|
||||
createBedrockNoCacheWrapper,
|
||||
isAnthropicBedrockModel,
|
||||
|
||||
4
extensions/bluebubbles/runtime-api.ts
Normal file
4
extensions/bluebubbles/runtime-api.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export {
|
||||
resolveBlueBubblesGroupRequireMention,
|
||||
resolveBlueBubblesGroupToolPolicy,
|
||||
} from "./src/group-policy.js";
|
||||
@@ -1,4 +1,6 @@
|
||||
import { defineSetupPluginEntry } from "openclaw/plugin-sdk/core";
|
||||
import { bluebubblesPlugin } from "./src/channel.js";
|
||||
import { bluebubblesSetupPlugin } from "./src/channel.setup.js";
|
||||
|
||||
export default defineSetupPluginEntry(bluebubblesPlugin);
|
||||
export { bluebubblesSetupPlugin } from "./src/channel.setup.js";
|
||||
|
||||
export default defineSetupPluginEntry(bluebubblesSetupPlugin);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
|
||||
import { createAccountListHelpers, type OpenClawConfig } from "./runtime-api.js";
|
||||
import { createAccountListHelpers } from "openclaw/plugin-sdk/account-helpers";
|
||||
import { normalizeAccountId } from "openclaw/plugin-sdk/account-id";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
|
||||
import { hasConfiguredSecretInput, normalizeSecretInputString } from "./secret-input.js";
|
||||
import { normalizeBlueBubblesServerUrl, type BlueBubblesAccountConfig } from "./types.js";
|
||||
|
||||
|
||||
@@ -484,4 +484,94 @@ describe("sendBlueBubblesAttachment", () => {
|
||||
expect(bodyText).not.toContain('name="selectedMessageGuid"');
|
||||
expect(bodyText).not.toContain('name="partIndex"');
|
||||
});
|
||||
|
||||
it("auto-creates a new chat when sending to a phone number with no existing chat", async () => {
|
||||
// First call: resolveChatGuidForTarget queries chats, returns empty (no match)
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ data: [] }),
|
||||
});
|
||||
// Second call: createChatForHandle creates new chat
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
text: () =>
|
||||
Promise.resolve(
|
||||
JSON.stringify({
|
||||
data: { chatGuid: "iMessage;-;+15559876543", guid: "iMessage;-;+15559876543" },
|
||||
}),
|
||||
),
|
||||
});
|
||||
// Third call: actual attachment send
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
text: () => Promise.resolve(JSON.stringify({ data: { guid: "attach-msg-1" } })),
|
||||
});
|
||||
|
||||
const result = await sendBlueBubblesAttachment({
|
||||
to: "+15559876543",
|
||||
buffer: new Uint8Array([1, 2, 3]),
|
||||
filename: "photo.jpg",
|
||||
contentType: "image/jpeg",
|
||||
opts: { serverUrl: "http://localhost:1234", password: "test" },
|
||||
});
|
||||
|
||||
expect(result.messageId).toBe("attach-msg-1");
|
||||
// Verify chat creation was called
|
||||
const createCallBody = JSON.parse(mockFetch.mock.calls[1][1].body);
|
||||
expect(createCallBody.addresses).toEqual(["+15559876543"]);
|
||||
// Verify attachment was sent to the newly created chat
|
||||
const attachBody = mockFetch.mock.calls[2][1]?.body as Uint8Array;
|
||||
const attachText = decodeBody(attachBody);
|
||||
expect(attachText).toContain("iMessage;-;+15559876543");
|
||||
});
|
||||
|
||||
it("retries chatGuid resolution after creating a chat with no returned guid", async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ data: [] }),
|
||||
});
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
text: () => Promise.resolve(JSON.stringify({ data: {} })),
|
||||
});
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ data: [{ guid: "iMessage;-;+15557654321" }] }),
|
||||
});
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
text: () => Promise.resolve(JSON.stringify({ data: { guid: "attach-msg-2" } })),
|
||||
});
|
||||
|
||||
const result = await sendBlueBubblesAttachment({
|
||||
to: "+15557654321",
|
||||
buffer: new Uint8Array([4, 5, 6]),
|
||||
filename: "photo.jpg",
|
||||
contentType: "image/jpeg",
|
||||
opts: { serverUrl: "http://localhost:1234", password: "test" },
|
||||
});
|
||||
|
||||
expect(result.messageId).toBe("attach-msg-2");
|
||||
const createCallBody = JSON.parse(mockFetch.mock.calls[1][1].body);
|
||||
expect(createCallBody.addresses).toEqual(["+15557654321"]);
|
||||
const attachBody = mockFetch.mock.calls[3][1]?.body as Uint8Array;
|
||||
const attachText = decodeBody(attachBody);
|
||||
expect(attachText).toContain("iMessage;-;+15557654321");
|
||||
});
|
||||
|
||||
it("still throws for non-handle targets when chatGuid is not found", async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ data: [] }),
|
||||
});
|
||||
|
||||
await expect(
|
||||
sendBlueBubblesAttachment({
|
||||
to: "chat_id:999",
|
||||
buffer: new Uint8Array([1, 2, 3]),
|
||||
filename: "photo.jpg",
|
||||
opts: { serverUrl: "http://localhost:1234", password: "test" },
|
||||
}),
|
||||
).rejects.toThrow("chatGuid not found");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,7 +10,7 @@ import { resolveRequestUrl } from "./request-url.js";
|
||||
import type { OpenClawConfig } from "./runtime-api.js";
|
||||
import { getBlueBubblesRuntime, warnBlueBubbles } from "./runtime.js";
|
||||
import { extractBlueBubblesMessageId, resolveBlueBubblesSendTarget } from "./send-helpers.js";
|
||||
import { resolveChatGuidForTarget } from "./send.js";
|
||||
import { resolveChatGuidForTarget, createChatForHandle } from "./send.js";
|
||||
import {
|
||||
blueBubblesFetchWithTimeout,
|
||||
buildBlueBubblesApiUrl,
|
||||
@@ -180,16 +180,37 @@ export async function sendBlueBubblesAttachment(params: {
|
||||
}
|
||||
|
||||
const target = resolveBlueBubblesSendTarget(to);
|
||||
const chatGuid = await resolveChatGuidForTarget({
|
||||
let chatGuid = await resolveChatGuidForTarget({
|
||||
baseUrl,
|
||||
password,
|
||||
timeoutMs: opts.timeoutMs,
|
||||
target,
|
||||
});
|
||||
if (!chatGuid) {
|
||||
throw new Error(
|
||||
"BlueBubbles attachment send failed: chatGuid not found for target. Use a chat_guid target or ensure the chat exists.",
|
||||
);
|
||||
// For handle targets (phone numbers/emails), auto-create a new DM chat
|
||||
if (target.kind === "handle") {
|
||||
const created = await createChatForHandle({
|
||||
baseUrl,
|
||||
password,
|
||||
address: target.address,
|
||||
timeoutMs: opts.timeoutMs,
|
||||
});
|
||||
chatGuid = created.chatGuid;
|
||||
// If we still don't have a chatGuid, try resolving again (chat was created server-side)
|
||||
if (!chatGuid) {
|
||||
chatGuid = await resolveChatGuidForTarget({
|
||||
baseUrl,
|
||||
password,
|
||||
timeoutMs: opts.timeoutMs,
|
||||
target,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (!chatGuid) {
|
||||
throw new Error(
|
||||
"BlueBubbles attachment send failed: chatGuid not found for target. Use a chat_guid target or ensure the chat exists.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const url = buildBlueBubblesApiUrl({
|
||||
|
||||
76
extensions/bluebubbles/src/channel.setup.ts
Normal file
76
extensions/bluebubbles/src/channel.setup.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { formatNormalizedAllowFromEntries } from "openclaw/plugin-sdk/allow-from";
|
||||
import { createScopedChannelConfigAdapter } from "openclaw/plugin-sdk/channel-config-helpers";
|
||||
import { buildChannelConfigSchema } from "openclaw/plugin-sdk/channel-config-schema";
|
||||
import type { ChannelPlugin } from "openclaw/plugin-sdk/core";
|
||||
import {
|
||||
listBlueBubblesAccountIds,
|
||||
type ResolvedBlueBubblesAccount,
|
||||
resolveBlueBubblesAccount,
|
||||
resolveDefaultBlueBubblesAccountId,
|
||||
} from "./accounts.js";
|
||||
import { BlueBubblesConfigSchema } from "./config-schema.js";
|
||||
import { blueBubblesSetupAdapter } from "./setup-core.js";
|
||||
import { blueBubblesSetupWizard } from "./setup-surface.js";
|
||||
import { normalizeBlueBubblesHandle } from "./targets.js";
|
||||
|
||||
const meta = {
|
||||
id: "bluebubbles",
|
||||
label: "BlueBubbles",
|
||||
selectionLabel: "BlueBubbles (macOS app)",
|
||||
detailLabel: "BlueBubbles",
|
||||
docsPath: "/channels/bluebubbles",
|
||||
docsLabel: "bluebubbles",
|
||||
blurb: "iMessage via the BlueBubbles mac app + REST API.",
|
||||
systemImage: "bubble.left.and.text.bubble.right",
|
||||
aliases: ["bb"],
|
||||
order: 75,
|
||||
preferOver: ["imessage"],
|
||||
} as const;
|
||||
|
||||
const bluebubblesConfigAdapter = createScopedChannelConfigAdapter<ResolvedBlueBubblesAccount>({
|
||||
sectionKey: "bluebubbles",
|
||||
listAccountIds: listBlueBubblesAccountIds,
|
||||
resolveAccount: (cfg, accountId) => resolveBlueBubblesAccount({ cfg, accountId }),
|
||||
defaultAccountId: resolveDefaultBlueBubblesAccountId,
|
||||
clearBaseFields: ["serverUrl", "password", "name", "webhookPath"],
|
||||
resolveAllowFrom: (account: ResolvedBlueBubblesAccount) => account.config.allowFrom,
|
||||
formatAllowFrom: (allowFrom) =>
|
||||
formatNormalizedAllowFromEntries({
|
||||
allowFrom,
|
||||
normalizeEntry: (entry) => normalizeBlueBubblesHandle(entry.replace(/^bluebubbles:/i, "")),
|
||||
}),
|
||||
});
|
||||
|
||||
export const bluebubblesSetupPlugin: ChannelPlugin<ResolvedBlueBubblesAccount> = {
|
||||
id: "bluebubbles",
|
||||
meta: {
|
||||
...meta,
|
||||
aliases: [...meta.aliases],
|
||||
preferOver: [...meta.preferOver],
|
||||
},
|
||||
capabilities: {
|
||||
chatTypes: ["direct", "group"],
|
||||
media: true,
|
||||
reactions: true,
|
||||
edit: true,
|
||||
unsend: true,
|
||||
reply: true,
|
||||
effects: true,
|
||||
groupManagement: true,
|
||||
},
|
||||
reload: { configPrefixes: ["channels.bluebubbles"] },
|
||||
configSchema: buildChannelConfigSchema(BlueBubblesConfigSchema),
|
||||
setupWizard: blueBubblesSetupWizard,
|
||||
config: {
|
||||
...bluebubblesConfigAdapter,
|
||||
isConfigured: (account) => account.configured,
|
||||
describeAccount: (account) => ({
|
||||
accountId: account.accountId,
|
||||
name: account.name,
|
||||
enabled: account.enabled,
|
||||
configured: account.configured,
|
||||
baseUrl: account.baseUrl,
|
||||
}),
|
||||
},
|
||||
setup: blueBubblesSetupAdapter,
|
||||
};
|
||||
@@ -4,7 +4,15 @@ import {
|
||||
createScopedDmSecurityResolver,
|
||||
} from "openclaw/plugin-sdk/channel-config-helpers";
|
||||
import { createAccountStatusSink } from "openclaw/plugin-sdk/channel-lifecycle";
|
||||
import { collectOpenGroupPolicyRestrictSendersWarnings } from "openclaw/plugin-sdk/channel-policy";
|
||||
import {
|
||||
createOpenGroupPolicyRestrictSendersWarningCollector,
|
||||
projectWarningCollector,
|
||||
} from "openclaw/plugin-sdk/channel-policy";
|
||||
import {
|
||||
createAttachedChannelResultAdapter,
|
||||
createPairingPrefixStripper,
|
||||
createTextPairingAdapter,
|
||||
} from "openclaw/plugin-sdk/channel-runtime";
|
||||
import { createLazyRuntimeNamedExport } from "openclaw/plugin-sdk/lazy-runtime";
|
||||
import {
|
||||
listBlueBubblesAccountIds,
|
||||
@@ -68,6 +76,17 @@ const resolveBlueBubblesDmPolicy = createScopedDmSecurityResolver<ResolvedBlueBu
|
||||
normalizeEntry: (raw) => normalizeBlueBubblesHandle(raw.replace(/^bluebubbles:/i, "")),
|
||||
});
|
||||
|
||||
const collectBlueBubblesSecurityWarnings =
|
||||
createOpenGroupPolicyRestrictSendersWarningCollector<ResolvedBlueBubblesAccount>({
|
||||
resolveGroupPolicy: (account) => account.config.groupPolicy,
|
||||
defaultGroupPolicy: "allowlist",
|
||||
surface: "BlueBubbles groups",
|
||||
openScope: "any member",
|
||||
groupPolicyPath: "channels.bluebubbles.groupPolicy",
|
||||
groupAllowFromPath: "channels.bluebubbles.groupAllowFrom",
|
||||
mentionGated: false,
|
||||
});
|
||||
|
||||
const meta = {
|
||||
id: "bluebubbles",
|
||||
label: "BlueBubbles",
|
||||
@@ -123,17 +142,10 @@ export const bluebubblesPlugin: ChannelPlugin<ResolvedBlueBubblesAccount> = {
|
||||
actions: bluebubblesMessageActions,
|
||||
security: {
|
||||
resolveDmPolicy: resolveBlueBubblesDmPolicy,
|
||||
collectWarnings: ({ account }) => {
|
||||
const groupPolicy = account.config.groupPolicy ?? "allowlist";
|
||||
return collectOpenGroupPolicyRestrictSendersWarnings({
|
||||
groupPolicy,
|
||||
surface: "BlueBubbles groups",
|
||||
openScope: "any member",
|
||||
groupPolicyPath: "channels.bluebubbles.groupPolicy",
|
||||
groupAllowFromPath: "channels.bluebubbles.groupAllowFrom",
|
||||
mentionGated: false,
|
||||
});
|
||||
},
|
||||
collectWarnings: projectWarningCollector(
|
||||
({ account }: { account: ResolvedBlueBubblesAccount }) => account,
|
||||
collectBlueBubblesSecurityWarnings,
|
||||
),
|
||||
},
|
||||
messaging: {
|
||||
normalizeTarget: normalizeBlueBubblesMessagingTarget,
|
||||
@@ -226,17 +238,18 @@ export const bluebubblesPlugin: ChannelPlugin<ResolvedBlueBubblesAccount> = {
|
||||
},
|
||||
},
|
||||
setup: blueBubblesSetupAdapter,
|
||||
pairing: {
|
||||
pairing: createTextPairingAdapter({
|
||||
idLabel: "bluebubblesSenderId",
|
||||
normalizeAllowEntry: (entry) => normalizeBlueBubblesHandle(entry.replace(/^bluebubbles:/i, "")),
|
||||
notifyApproval: async ({ cfg, id }) => {
|
||||
message: PAIRING_APPROVED_MESSAGE,
|
||||
normalizeAllowEntry: createPairingPrefixStripper(/^bluebubbles:/i, normalizeBlueBubblesHandle),
|
||||
notify: async ({ cfg, id, message }) => {
|
||||
await (
|
||||
await loadBlueBubblesChannelRuntime()
|
||||
).sendMessageBlueBubbles(id, PAIRING_APPROVED_MESSAGE, {
|
||||
).sendMessageBlueBubbles(id, message, {
|
||||
cfg: cfg,
|
||||
});
|
||||
},
|
||||
},
|
||||
}),
|
||||
outbound: {
|
||||
deliveryMode: "direct",
|
||||
textChunkLimit: 4000,
|
||||
@@ -250,46 +263,44 @@ export const bluebubblesPlugin: ChannelPlugin<ResolvedBlueBubblesAccount> = {
|
||||
}
|
||||
return { ok: true, to: trimmed };
|
||||
},
|
||||
sendText: async ({ cfg, to, text, accountId, replyToId }) => {
|
||||
const runtime = await loadBlueBubblesChannelRuntime();
|
||||
const rawReplyToId = typeof replyToId === "string" ? replyToId.trim() : "";
|
||||
// Resolve short ID (e.g., "5") to full UUID
|
||||
const replyToMessageGuid = rawReplyToId
|
||||
? runtime.resolveBlueBubblesMessageId(rawReplyToId, { requireKnownShortId: true })
|
||||
: "";
|
||||
const result = await runtime.sendMessageBlueBubbles(to, text, {
|
||||
cfg: cfg,
|
||||
accountId: accountId ?? undefined,
|
||||
replyToMessageGuid: replyToMessageGuid || undefined,
|
||||
});
|
||||
return { channel: "bluebubbles", ...result };
|
||||
},
|
||||
sendMedia: async (ctx) => {
|
||||
const runtime = await loadBlueBubblesChannelRuntime();
|
||||
const { cfg, to, text, mediaUrl, accountId, replyToId } = ctx;
|
||||
const { mediaPath, mediaBuffer, contentType, filename, caption } = ctx as {
|
||||
mediaPath?: string;
|
||||
mediaBuffer?: Uint8Array;
|
||||
contentType?: string;
|
||||
filename?: string;
|
||||
caption?: string;
|
||||
};
|
||||
const resolvedCaption = caption ?? text;
|
||||
const result = await runtime.sendBlueBubblesMedia({
|
||||
cfg: cfg,
|
||||
to,
|
||||
mediaUrl,
|
||||
mediaPath,
|
||||
mediaBuffer,
|
||||
contentType,
|
||||
filename,
|
||||
caption: resolvedCaption ?? undefined,
|
||||
replyToId: replyToId ?? null,
|
||||
accountId: accountId ?? undefined,
|
||||
});
|
||||
|
||||
return { channel: "bluebubbles", ...result };
|
||||
},
|
||||
...createAttachedChannelResultAdapter({
|
||||
channel: "bluebubbles",
|
||||
sendText: async ({ cfg, to, text, accountId, replyToId }) => {
|
||||
const runtime = await loadBlueBubblesChannelRuntime();
|
||||
const rawReplyToId = typeof replyToId === "string" ? replyToId.trim() : "";
|
||||
const replyToMessageGuid = rawReplyToId
|
||||
? runtime.resolveBlueBubblesMessageId(rawReplyToId, { requireKnownShortId: true })
|
||||
: "";
|
||||
return await runtime.sendMessageBlueBubbles(to, text, {
|
||||
cfg: cfg,
|
||||
accountId: accountId ?? undefined,
|
||||
replyToMessageGuid: replyToMessageGuid || undefined,
|
||||
});
|
||||
},
|
||||
sendMedia: async (ctx) => {
|
||||
const runtime = await loadBlueBubblesChannelRuntime();
|
||||
const { cfg, to, text, mediaUrl, accountId, replyToId } = ctx;
|
||||
const { mediaPath, mediaBuffer, contentType, filename, caption } = ctx as {
|
||||
mediaPath?: string;
|
||||
mediaBuffer?: Uint8Array;
|
||||
contentType?: string;
|
||||
filename?: string;
|
||||
caption?: string;
|
||||
};
|
||||
return await runtime.sendBlueBubblesMedia({
|
||||
cfg: cfg,
|
||||
to,
|
||||
mediaUrl,
|
||||
mediaPath,
|
||||
mediaBuffer,
|
||||
contentType,
|
||||
filename,
|
||||
caption: caption ?? text ?? undefined,
|
||||
replyToId: replyToId ?? null,
|
||||
accountId: accountId ?? undefined,
|
||||
});
|
||||
},
|
||||
}),
|
||||
},
|
||||
status: {
|
||||
defaultRuntime: {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { DEFAULT_ACCOUNT_ID, type OpenClawConfig } from "./runtime-api.js";
|
||||
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/account-id";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
|
||||
|
||||
type BlueBubblesConfigPatch = {
|
||||
serverUrl?: string;
|
||||
|
||||
@@ -3,9 +3,10 @@ import {
|
||||
buildCatchallMultiAccountChannelSchema,
|
||||
DmPolicySchema,
|
||||
GroupPolicySchema,
|
||||
MarkdownConfigSchema,
|
||||
ToolPolicySchema,
|
||||
} from "openclaw/plugin-sdk/channel-config-schema";
|
||||
import { z } from "zod";
|
||||
import { MarkdownConfigSchema, ToolPolicySchema } from "./runtime-api.js";
|
||||
import { buildSecretInputSchema, hasConfiguredSecretInput } from "./secret-input.js";
|
||||
|
||||
const bluebubblesActionSchema = z
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
resolveChannelGroupToolsPolicy,
|
||||
type GroupToolPolicyConfig,
|
||||
} from "openclaw/plugin-sdk/channel-policy";
|
||||
import type { OpenClawConfig } from "./runtime-api.js";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
|
||||
type BlueBubblesGroupContext = {
|
||||
cfg: OpenClawConfig;
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
import {
|
||||
resolveOutboundMediaUrls,
|
||||
resolveTextChunksWithFallback,
|
||||
sendMediaWithLeadingCaption,
|
||||
} from "openclaw/plugin-sdk/reply-payload";
|
||||
import { downloadBlueBubblesAttachment } from "./attachments.js";
|
||||
import { markBlueBubblesChatRead, sendBlueBubblesTyping } from "./chat.js";
|
||||
import { fetchBlueBubblesHistory } from "./history.js";
|
||||
@@ -33,10 +38,9 @@ import { normalizeBlueBubblesReactionInput, sendBlueBubblesReaction } from "./re
|
||||
import type { OpenClawConfig } from "./runtime-api.js";
|
||||
import {
|
||||
DM_GROUP_ACCESS_REASON,
|
||||
createScopedPairingAccess,
|
||||
createReplyPrefixOptions,
|
||||
createChannelPairingController,
|
||||
createChannelReplyPipeline,
|
||||
evictOldHistoryKeys,
|
||||
issuePairingChallenge,
|
||||
logAckFailure,
|
||||
logInboundDrop,
|
||||
logTypingFailure,
|
||||
@@ -447,7 +451,7 @@ export async function processMessage(
|
||||
target: WebhookTarget,
|
||||
): Promise<void> {
|
||||
const { account, config, runtime, core, statusSink } = target;
|
||||
const pairing = createScopedPairingAccess({
|
||||
const pairing = createChannelPairingController({
|
||||
core,
|
||||
channel: "bluebubbles",
|
||||
accountId: account.accountId,
|
||||
@@ -649,12 +653,10 @@ export async function processMessage(
|
||||
}
|
||||
|
||||
if (accessDecision.decision === "pairing") {
|
||||
await issuePairingChallenge({
|
||||
channel: "bluebubbles",
|
||||
await pairing.issueChallenge({
|
||||
senderId: message.senderId,
|
||||
senderIdLine: `Your BlueBubbles sender id: ${message.senderId}`,
|
||||
meta: { name: message.senderName },
|
||||
upsertPairingRequest: pairing.upsertPairingRequest,
|
||||
onCreated: () => {
|
||||
runtime.log?.(`[bluebubbles] pairing request sender=${message.senderId} created=true`);
|
||||
logVerbose(core, runtime, `bluebubbles pairing request sender=${message.senderId}`);
|
||||
@@ -1223,17 +1225,47 @@ export async function processMessage(
|
||||
}, typingRestartDelayMs);
|
||||
};
|
||||
try {
|
||||
const { onModelSelected, ...prefixOptions } = createReplyPrefixOptions({
|
||||
const { onModelSelected, typingCallbacks, ...replyPipeline } = createChannelReplyPipeline({
|
||||
cfg: config,
|
||||
agentId: route.agentId,
|
||||
channel: "bluebubbles",
|
||||
accountId: account.accountId,
|
||||
typingCallbacks: {
|
||||
onReplyStart: async () => {
|
||||
if (!chatGuidForActions) {
|
||||
return;
|
||||
}
|
||||
if (!baseUrl || !password) {
|
||||
return;
|
||||
}
|
||||
streamingActive = true;
|
||||
clearTypingRestartTimer();
|
||||
try {
|
||||
await sendBlueBubblesTyping(chatGuidForActions, true, {
|
||||
cfg: config,
|
||||
accountId: account.accountId,
|
||||
});
|
||||
} catch (err) {
|
||||
runtime.error?.(`[bluebubbles] typing start failed: ${String(err)}`);
|
||||
}
|
||||
},
|
||||
onIdle: () => {
|
||||
if (!chatGuidForActions) {
|
||||
return;
|
||||
}
|
||||
if (!baseUrl || !password) {
|
||||
return;
|
||||
}
|
||||
// Intentionally no-op for block streaming. We stop typing in finally
|
||||
// after the run completes to avoid flicker between paragraph blocks.
|
||||
},
|
||||
},
|
||||
});
|
||||
await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
||||
ctx: ctxPayload,
|
||||
cfg: config,
|
||||
dispatcherOptions: {
|
||||
...prefixOptions,
|
||||
...replyPipeline,
|
||||
deliver: async (payload, info) => {
|
||||
const rawReplyToId =
|
||||
privateApiEnabled && typeof payload.replyToId === "string"
|
||||
@@ -1243,11 +1275,7 @@ export async function processMessage(
|
||||
const replyToMessageGuid = rawReplyToId
|
||||
? resolveBlueBubblesMessageId(rawReplyToId, { requireKnownShortId: true })
|
||||
: "";
|
||||
const mediaList = payload.mediaUrls?.length
|
||||
? payload.mediaUrls
|
||||
: payload.mediaUrl
|
||||
? [payload.mediaUrl]
|
||||
: [];
|
||||
const mediaList = resolveOutboundMediaUrls(payload);
|
||||
if (mediaList.length > 0) {
|
||||
const tableMode = core.channel.text.resolveMarkdownTableMode({
|
||||
cfg: config,
|
||||
@@ -1257,43 +1285,44 @@ export async function processMessage(
|
||||
const text = sanitizeReplyDirectiveText(
|
||||
core.channel.text.convertMarkdownTables(payload.text ?? "", tableMode),
|
||||
);
|
||||
let first = true;
|
||||
for (const mediaUrl of mediaList) {
|
||||
const caption = first ? text : undefined;
|
||||
first = false;
|
||||
const cachedBody = (caption ?? "").trim() || "<media:attachment>";
|
||||
const pendingId = rememberPendingOutboundMessageId({
|
||||
accountId: account.accountId,
|
||||
sessionKey: route.sessionKey,
|
||||
outboundTarget,
|
||||
chatGuid: chatGuidForActions ?? chatGuid,
|
||||
chatIdentifier,
|
||||
chatId,
|
||||
snippet: cachedBody,
|
||||
});
|
||||
let result: Awaited<ReturnType<typeof sendBlueBubblesMedia>>;
|
||||
try {
|
||||
result = await sendBlueBubblesMedia({
|
||||
cfg: config,
|
||||
to: outboundTarget,
|
||||
mediaUrl,
|
||||
caption: caption ?? undefined,
|
||||
replyToId: replyToMessageGuid || null,
|
||||
await sendMediaWithLeadingCaption({
|
||||
mediaUrls: mediaList,
|
||||
caption: text,
|
||||
send: async ({ mediaUrl, caption }) => {
|
||||
const cachedBody = (caption ?? "").trim() || "<media:attachment>";
|
||||
const pendingId = rememberPendingOutboundMessageId({
|
||||
accountId: account.accountId,
|
||||
sessionKey: route.sessionKey,
|
||||
outboundTarget,
|
||||
chatGuid: chatGuidForActions ?? chatGuid,
|
||||
chatIdentifier,
|
||||
chatId,
|
||||
snippet: cachedBody,
|
||||
});
|
||||
} catch (err) {
|
||||
forgetPendingOutboundMessageId(pendingId);
|
||||
throw err;
|
||||
}
|
||||
if (maybeEnqueueOutboundMessageId(result.messageId, cachedBody)) {
|
||||
forgetPendingOutboundMessageId(pendingId);
|
||||
}
|
||||
sentMessage = true;
|
||||
statusSink?.({ lastOutboundAt: Date.now() });
|
||||
if (info.kind === "block") {
|
||||
restartTypingSoon();
|
||||
}
|
||||
}
|
||||
let result: Awaited<ReturnType<typeof sendBlueBubblesMedia>>;
|
||||
try {
|
||||
result = await sendBlueBubblesMedia({
|
||||
cfg: config,
|
||||
to: outboundTarget,
|
||||
mediaUrl,
|
||||
caption: caption ?? undefined,
|
||||
replyToId: replyToMessageGuid || null,
|
||||
accountId: account.accountId,
|
||||
});
|
||||
} catch (err) {
|
||||
forgetPendingOutboundMessageId(pendingId);
|
||||
throw err;
|
||||
}
|
||||
if (maybeEnqueueOutboundMessageId(result.messageId, cachedBody)) {
|
||||
forgetPendingOutboundMessageId(pendingId);
|
||||
}
|
||||
sentMessage = true;
|
||||
statusSink?.({ lastOutboundAt: Date.now() });
|
||||
if (info.kind === "block") {
|
||||
restartTypingSoon();
|
||||
}
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1312,11 +1341,14 @@ export async function processMessage(
|
||||
);
|
||||
const chunks =
|
||||
chunkMode === "newline"
|
||||
? core.channel.text.chunkTextWithMode(text, textLimit, chunkMode)
|
||||
: core.channel.text.chunkMarkdownText(text, textLimit);
|
||||
if (!chunks.length && text) {
|
||||
chunks.push(text);
|
||||
}
|
||||
? resolveTextChunksWithFallback(
|
||||
text,
|
||||
core.channel.text.chunkTextWithMode(text, textLimit, chunkMode),
|
||||
)
|
||||
: resolveTextChunksWithFallback(
|
||||
text,
|
||||
core.channel.text.chunkMarkdownText(text, textLimit),
|
||||
);
|
||||
if (!chunks.length) {
|
||||
return;
|
||||
}
|
||||
@@ -1351,34 +1383,8 @@ export async function processMessage(
|
||||
}
|
||||
}
|
||||
},
|
||||
onReplyStart: async () => {
|
||||
if (!chatGuidForActions) {
|
||||
return;
|
||||
}
|
||||
if (!baseUrl || !password) {
|
||||
return;
|
||||
}
|
||||
streamingActive = true;
|
||||
clearTypingRestartTimer();
|
||||
try {
|
||||
await sendBlueBubblesTyping(chatGuidForActions, true, {
|
||||
cfg: config,
|
||||
accountId: account.accountId,
|
||||
});
|
||||
} catch (err) {
|
||||
runtime.error?.(`[bluebubbles] typing start failed: ${String(err)}`);
|
||||
}
|
||||
},
|
||||
onIdle: async () => {
|
||||
if (!chatGuidForActions) {
|
||||
return;
|
||||
}
|
||||
if (!baseUrl || !password) {
|
||||
return;
|
||||
}
|
||||
// Intentionally no-op for block streaming. We stop typing in finally
|
||||
// after the run completes to avoid flicker between paragraph blocks.
|
||||
},
|
||||
onReplyStart: typingCallbacks?.onReplyStart,
|
||||
onIdle: typingCallbacks?.onIdle,
|
||||
onError: (err, info) => {
|
||||
runtime.error?.(`BlueBubbles ${info.kind} reply failed: ${String(err)}`);
|
||||
},
|
||||
@@ -1442,7 +1448,7 @@ export async function processReaction(
|
||||
target: WebhookTarget,
|
||||
): Promise<void> {
|
||||
const { account, config, runtime, core } = target;
|
||||
const pairing = createScopedPairingAccess({
|
||||
const pairing = createChannelPairingController({
|
||||
core,
|
||||
channel: "bluebubbles",
|
||||
accountId: account.accountId,
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
|
||||
import type { ResolvedBlueBubblesAccount } from "./accounts.js";
|
||||
import { normalizeWebhookPath, type OpenClawConfig } from "./runtime-api.js";
|
||||
import { getBlueBubblesRuntime } from "./runtime.js";
|
||||
import type { BlueBubblesAccountConfig } from "./types.js";
|
||||
|
||||
export { normalizeWebhookPath };
|
||||
export {
|
||||
DEFAULT_WEBHOOK_PATH,
|
||||
normalizeWebhookPath,
|
||||
resolveWebhookPathFromConfig,
|
||||
} from "./webhook-shared.js";
|
||||
|
||||
export type BlueBubblesRuntimeEnv = {
|
||||
log?: (message: string) => void;
|
||||
@@ -29,13 +32,3 @@ export type WebhookTarget = {
|
||||
path: string;
|
||||
statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void;
|
||||
};
|
||||
|
||||
export const DEFAULT_WEBHOOK_PATH = "/bluebubbles-webhook";
|
||||
|
||||
export function resolveWebhookPathFromConfig(config?: BlueBubblesAccountConfig): string {
|
||||
const raw = config?.webhookPath?.trim();
|
||||
if (raw) {
|
||||
return normalizeWebhookPath(raw);
|
||||
}
|
||||
return DEFAULT_WEBHOOK_PATH;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,6 @@
|
||||
import {
|
||||
buildSecretInputSchema,
|
||||
hasConfiguredSecretInput,
|
||||
normalizeResolvedSecretInputString,
|
||||
normalizeSecretInputString,
|
||||
} from "./runtime-api.js";
|
||||
|
||||
export {
|
||||
buildSecretInputSchema,
|
||||
hasConfiguredSecretInput,
|
||||
normalizeResolvedSecretInputString,
|
||||
normalizeSecretInputString,
|
||||
};
|
||||
} from "openclaw/plugin-sdk/secret-input";
|
||||
|
||||
@@ -3,7 +3,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import "./test-mocks.js";
|
||||
import { getCachedBlueBubblesPrivateApiStatus } from "./probe.js";
|
||||
import { clearBlueBubblesRuntime, setBlueBubblesRuntime } from "./runtime.js";
|
||||
import { sendMessageBlueBubbles, resolveChatGuidForTarget } from "./send.js";
|
||||
import { sendMessageBlueBubbles, resolveChatGuidForTarget, createChatForHandle } from "./send.js";
|
||||
import {
|
||||
BLUE_BUBBLES_PRIVATE_API_STATUS,
|
||||
installBlueBubblesFetchTestHooks,
|
||||
@@ -781,4 +781,109 @@ describe("send", () => {
|
||||
expect(body.tempGuid.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("createChatForHandle", () => {
|
||||
it("creates a new chat and returns chatGuid from response", async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
text: () =>
|
||||
Promise.resolve(
|
||||
JSON.stringify({
|
||||
data: { guid: "iMessage;-;+15559876543", chatGuid: "iMessage;-;+15559876543" },
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
const result = await createChatForHandle({
|
||||
baseUrl: "http://localhost:1234",
|
||||
password: "test",
|
||||
address: "+15559876543",
|
||||
message: "Hello!",
|
||||
});
|
||||
|
||||
expect(result.chatGuid).toBe("iMessage;-;+15559876543");
|
||||
expect(result.messageId).toBeDefined();
|
||||
const body = JSON.parse(mockFetch.mock.calls[0][1].body);
|
||||
expect(body.addresses).toEqual(["+15559876543"]);
|
||||
expect(body.message).toBe("Hello!");
|
||||
});
|
||||
|
||||
it("creates a new chat without a message when message is omitted", async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
text: () =>
|
||||
Promise.resolve(
|
||||
JSON.stringify({
|
||||
data: { guid: "iMessage;-;+15559876543" },
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
const result = await createChatForHandle({
|
||||
baseUrl: "http://localhost:1234",
|
||||
password: "test",
|
||||
address: "+15559876543",
|
||||
});
|
||||
|
||||
expect(result.chatGuid).toBe("iMessage;-;+15559876543");
|
||||
const body = JSON.parse(mockFetch.mock.calls[0][1].body);
|
||||
expect(body.message).toBe("");
|
||||
});
|
||||
|
||||
it.each([
|
||||
["data.chatGuid", { data: { chatGuid: "shape-chat-guid" } }, "shape-chat-guid"],
|
||||
["data.guid", { data: { guid: "shape-guid" } }, "shape-guid"],
|
||||
[
|
||||
"data.chats[0].guid",
|
||||
{ data: { chats: [{ guid: "shape-array-guid" }] } },
|
||||
"shape-array-guid",
|
||||
],
|
||||
["data.chat.guid", { data: { chat: { guid: "shape-object-guid" } } }, "shape-object-guid"],
|
||||
])("extracts chatGuid from %s", async (_label, responseBody, expectedChatGuid) => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
text: () => Promise.resolve(JSON.stringify(responseBody)),
|
||||
});
|
||||
|
||||
const result = await createChatForHandle({
|
||||
baseUrl: "http://localhost:1234",
|
||||
password: "test",
|
||||
address: "+15559876543",
|
||||
});
|
||||
|
||||
expect(result.chatGuid).toBe(expectedChatGuid);
|
||||
});
|
||||
|
||||
it("throws when Private API is not enabled", async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 403,
|
||||
text: () => Promise.resolve("Private API not enabled"),
|
||||
});
|
||||
|
||||
await expect(
|
||||
createChatForHandle({
|
||||
baseUrl: "http://localhost:1234",
|
||||
password: "test",
|
||||
address: "+15559876543",
|
||||
}),
|
||||
).rejects.toThrow("Private API must be enabled");
|
||||
});
|
||||
|
||||
it("returns null chatGuid when response has no chat data", async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
text: () => Promise.resolve(JSON.stringify({ data: {} })),
|
||||
});
|
||||
|
||||
const result = await createChatForHandle({
|
||||
baseUrl: "http://localhost:1234",
|
||||
password: "test",
|
||||
address: "+15559876543",
|
||||
message: "Hello",
|
||||
});
|
||||
|
||||
expect(result.chatGuid).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -312,16 +312,20 @@ export async function resolveChatGuidForTarget(params: {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new chat (DM) and optionally sends an initial message.
|
||||
* Creates a new DM chat for the given address and returns the chat GUID.
|
||||
* Requires Private API to be enabled in BlueBubbles.
|
||||
*
|
||||
* If a `message` is provided it is sent as the initial message in the new chat;
|
||||
* otherwise an empty-string message body is used (BlueBubbles still creates the
|
||||
* chat but will not deliver a visible bubble).
|
||||
*/
|
||||
async function createNewChatWithMessage(params: {
|
||||
export async function createChatForHandle(params: {
|
||||
baseUrl: string;
|
||||
password: string;
|
||||
address: string;
|
||||
message: string;
|
||||
message?: string;
|
||||
timeoutMs?: number;
|
||||
}): Promise<BlueBubblesSendResult> {
|
||||
}): Promise<{ chatGuid: string | null; messageId: string }> {
|
||||
const url = buildBlueBubblesApiUrl({
|
||||
baseUrl: params.baseUrl,
|
||||
path: "/api/v1/chat/new",
|
||||
@@ -329,7 +333,7 @@ async function createNewChatWithMessage(params: {
|
||||
});
|
||||
const payload = {
|
||||
addresses: [params.address],
|
||||
message: params.message,
|
||||
message: params.message ?? "",
|
||||
tempGuid: `temp-${crypto.randomUUID()}`,
|
||||
};
|
||||
const res = await blueBubblesFetchWithTimeout(
|
||||
@@ -343,7 +347,6 @@ async function createNewChatWithMessage(params: {
|
||||
);
|
||||
if (!res.ok) {
|
||||
const errorText = await res.text();
|
||||
// Check for Private API not enabled error
|
||||
if (
|
||||
res.status === 400 ||
|
||||
res.status === 403 ||
|
||||
@@ -355,7 +358,64 @@ async function createNewChatWithMessage(params: {
|
||||
}
|
||||
throw new Error(`BlueBubbles create chat failed (${res.status}): ${errorText || "unknown"}`);
|
||||
}
|
||||
return parseBlueBubblesMessageResponse(res);
|
||||
const body = await res.text();
|
||||
let messageId = "ok";
|
||||
let chatGuid: string | null = null;
|
||||
if (body) {
|
||||
try {
|
||||
const parsed = JSON.parse(body) as Record<string, unknown>;
|
||||
messageId = extractBlueBubblesMessageId(parsed);
|
||||
// Extract chatGuid from the response data
|
||||
const data = parsed.data as Record<string, unknown> | undefined;
|
||||
if (data) {
|
||||
chatGuid =
|
||||
(typeof data.chatGuid === "string" && data.chatGuid) ||
|
||||
(typeof data.guid === "string" && data.guid) ||
|
||||
null;
|
||||
// Also try nested chats array (some BB versions nest it)
|
||||
if (!chatGuid) {
|
||||
const chats = data.chats ?? data.chat;
|
||||
if (Array.isArray(chats) && chats.length > 0) {
|
||||
const first = chats[0] as Record<string, unknown> | undefined;
|
||||
chatGuid =
|
||||
(typeof first?.guid === "string" && first.guid) ||
|
||||
(typeof first?.chatGuid === "string" && first.chatGuid) ||
|
||||
null;
|
||||
} else if (chats && typeof chats === "object" && !Array.isArray(chats)) {
|
||||
const chatObj = chats as Record<string, unknown>;
|
||||
chatGuid =
|
||||
(typeof chatObj.guid === "string" && chatObj.guid) ||
|
||||
(typeof chatObj.chatGuid === "string" && chatObj.chatGuid) ||
|
||||
null;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// ignore parse errors
|
||||
}
|
||||
}
|
||||
return { chatGuid, messageId };
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new chat (DM) and sends an initial message.
|
||||
* Requires Private API to be enabled in BlueBubbles.
|
||||
*/
|
||||
async function createNewChatWithMessage(params: {
|
||||
baseUrl: string;
|
||||
password: string;
|
||||
address: string;
|
||||
message: string;
|
||||
timeoutMs?: number;
|
||||
}): Promise<BlueBubblesSendResult> {
|
||||
const result = await createChatForHandle({
|
||||
baseUrl: params.baseUrl,
|
||||
password: params.password,
|
||||
address: params.address,
|
||||
message: params.message,
|
||||
timeoutMs: params.timeoutMs,
|
||||
});
|
||||
return { messageId: result.messageId };
|
||||
}
|
||||
|
||||
export async function sendMessageBlueBubbles(
|
||||
|
||||
@@ -3,7 +3,7 @@ import { buildChannelSetupWizardAdapterFromSetupWizard } from "../../../src/chan
|
||||
import { DEFAULT_ACCOUNT_ID } from "../../../src/routing/session-key.js";
|
||||
import type { WizardPrompter } from "../../../src/wizard/prompts.js";
|
||||
import { resolveBlueBubblesAccount } from "./accounts.js";
|
||||
import { DEFAULT_WEBHOOK_PATH } from "./monitor-shared.js";
|
||||
import { DEFAULT_WEBHOOK_PATH } from "./webhook-shared.js";
|
||||
|
||||
async function createBlueBubblesConfigureAdapter() {
|
||||
const { blueBubblesSetupAdapter, blueBubblesSetupWizard } = await import("./setup-surface.js");
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
resolveDefaultBlueBubblesAccountId,
|
||||
} from "./accounts.js";
|
||||
import { applyBlueBubblesConnectionConfig } from "./config-apply.js";
|
||||
import { DEFAULT_WEBHOOK_PATH } from "./monitor-shared.js";
|
||||
import { hasConfiguredSecretInput, normalizeSecretInputString } from "./secret-input.js";
|
||||
import {
|
||||
blueBubblesSetupAdapter,
|
||||
@@ -23,6 +22,7 @@ import {
|
||||
} from "./setup-core.js";
|
||||
import { parseBlueBubblesAllowTarget } from "./targets.js";
|
||||
import { normalizeBlueBubblesServerUrl } from "./types.js";
|
||||
import { DEFAULT_WEBHOOK_PATH } from "./webhook-shared.js";
|
||||
|
||||
const channel = "bluebubbles" as const;
|
||||
const CONFIGURE_CUSTOM_WEBHOOK_FLAG = "__bluebubblesConfigureCustomWebhookPath";
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { isAllowedParsedChatSender } from "openclaw/plugin-sdk/allow-from";
|
||||
import {
|
||||
isAllowedParsedChatSender,
|
||||
parseChatAllowTargetPrefixes,
|
||||
parseChatTargetPrefixesOrThrow,
|
||||
type ParsedChatTarget,
|
||||
resolveServicePrefixedAllowTarget,
|
||||
resolveServicePrefixedTarget,
|
||||
} from "./runtime-api.js";
|
||||
} from "openclaw/plugin-sdk/imessage-core";
|
||||
|
||||
export type BlueBubblesService = "imessage" | "sms" | "auto";
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { DmPolicy, GroupPolicy } from "./runtime-api.js";
|
||||
import type { DmPolicy, GroupPolicy } from "openclaw/plugin-sdk/setup";
|
||||
|
||||
export type { DmPolicy, GroupPolicy } from "./runtime-api.js";
|
||||
export type { DmPolicy, GroupPolicy } from "openclaw/plugin-sdk/setup";
|
||||
|
||||
export type BlueBubblesGroupConfig = {
|
||||
/** If true, only respond in this group when mentioned. */
|
||||
|
||||
14
extensions/bluebubbles/src/webhook-shared.ts
Normal file
14
extensions/bluebubbles/src/webhook-shared.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { normalizeWebhookPath } from "openclaw/plugin-sdk/webhook-path";
|
||||
import type { BlueBubblesAccountConfig } from "./types.js";
|
||||
|
||||
export { normalizeWebhookPath };
|
||||
|
||||
export const DEFAULT_WEBHOOK_PATH = "/bluebubbles-webhook";
|
||||
|
||||
export function resolveWebhookPathFromConfig(config?: BlueBubblesAccountConfig): string {
|
||||
const raw = config?.webhookPath?.trim();
|
||||
if (raw) {
|
||||
return normalizeWebhookPath(raw);
|
||||
}
|
||||
return DEFAULT_WEBHOOK_PATH;
|
||||
}
|
||||
@@ -11,13 +11,13 @@ import {
|
||||
readNumberParam,
|
||||
readProviderEnvValue,
|
||||
readStringParam,
|
||||
resolveProviderWebSearchPluginConfig,
|
||||
resolveSearchCacheTtlMs,
|
||||
resolveSearchCount,
|
||||
resolveSearchTimeoutSeconds,
|
||||
resolveSiteName,
|
||||
resolveProviderWebSearchPluginConfig,
|
||||
setTopLevelCredentialValue,
|
||||
setProviderWebSearchPluginConfigValue,
|
||||
type OpenClawConfig,
|
||||
type SearchConfigRecord,
|
||||
type WebSearchProviderPlugin,
|
||||
type WebSearchProviderToolDefinition,
|
||||
@@ -92,7 +92,6 @@ const BRAVE_SEARCH_LANG_ALIASES: Record<string, string> = {
|
||||
const BRAVE_UI_LANG_LOCALE = /^([a-z]{2})-([a-z]{2})$/i;
|
||||
|
||||
type BraveConfig = {
|
||||
apiKey?: unknown;
|
||||
mode?: string;
|
||||
};
|
||||
|
||||
@@ -115,41 +114,18 @@ type BraveLlmContextResponse = {
|
||||
sources?: { url?: string; hostname?: string; date?: string }[];
|
||||
};
|
||||
|
||||
function resolveBraveConfig(
|
||||
config?: OpenClawConfig,
|
||||
searchConfig?: SearchConfigRecord,
|
||||
): BraveConfig {
|
||||
const pluginConfig = resolveProviderWebSearchPluginConfig(config, "brave");
|
||||
if (pluginConfig) {
|
||||
return pluginConfig as BraveConfig;
|
||||
}
|
||||
const scoped = (searchConfig as Record<string, unknown> | undefined)?.brave;
|
||||
return scoped && typeof scoped === "object" && !Array.isArray(scoped)
|
||||
? ({
|
||||
...(scoped as BraveConfig),
|
||||
apiKey: (searchConfig as Record<string, unknown> | undefined)?.apiKey,
|
||||
} as BraveConfig)
|
||||
: ({ apiKey: (searchConfig as Record<string, unknown> | undefined)?.apiKey } as BraveConfig);
|
||||
function resolveBraveConfig(searchConfig?: SearchConfigRecord): BraveConfig {
|
||||
const brave = searchConfig?.brave;
|
||||
return brave && typeof brave === "object" && !Array.isArray(brave) ? (brave as BraveConfig) : {};
|
||||
}
|
||||
|
||||
function resolveBraveMode(brave?: BraveConfig): "web" | "llm-context" {
|
||||
return brave?.mode === "llm-context" ? "llm-context" : "web";
|
||||
}
|
||||
|
||||
function resolveBraveApiKey(
|
||||
config?: OpenClawConfig,
|
||||
searchConfig?: SearchConfigRecord,
|
||||
): string | undefined {
|
||||
const braveConfig = resolveBraveConfig(config, searchConfig);
|
||||
function resolveBraveApiKey(searchConfig?: SearchConfigRecord): string | undefined {
|
||||
return (
|
||||
readConfiguredSecretString(
|
||||
braveConfig.apiKey,
|
||||
"plugins.entries.brave.config.webSearch.apiKey",
|
||||
) ??
|
||||
readConfiguredSecretString(
|
||||
(searchConfig as Record<string, unknown> | undefined)?.apiKey,
|
||||
"tools.web.search.apiKey",
|
||||
) ??
|
||||
readConfiguredSecretString(searchConfig?.apiKey, "tools.web.search.apiKey") ??
|
||||
readProviderEnvValue(["BRAVE_API_KEY"])
|
||||
);
|
||||
}
|
||||
@@ -410,10 +386,9 @@ function missingBraveKeyPayload() {
|
||||
}
|
||||
|
||||
function createBraveToolDefinition(
|
||||
config?: OpenClawConfig,
|
||||
searchConfig?: SearchConfigRecord,
|
||||
): WebSearchProviderToolDefinition {
|
||||
const braveConfig = resolveBraveConfig(config, searchConfig);
|
||||
const braveConfig = resolveBraveConfig(searchConfig);
|
||||
const braveMode = resolveBraveMode(braveConfig);
|
||||
|
||||
return {
|
||||
@@ -423,7 +398,7 @@ function createBraveToolDefinition(
|
||||
: "Search the web using Brave Search API. Supports region-specific and localized search via country and language parameters. Returns titles, URLs, and snippets for fast research.",
|
||||
parameters: createBraveSchema(),
|
||||
execute: async (args) => {
|
||||
const apiKey = resolveBraveApiKey(config, searchConfig);
|
||||
const apiKey = resolveBraveApiKey(searchConfig);
|
||||
if (!apiKey) {
|
||||
return missingBraveKeyPayload();
|
||||
}
|
||||
@@ -624,16 +599,30 @@ export function createBraveWebSearchProvider(): WebSearchProviderPlugin {
|
||||
credentialPath: "plugins.entries.brave.config.webSearch.apiKey",
|
||||
inactiveSecretPaths: ["plugins.entries.brave.config.webSearch.apiKey"],
|
||||
getCredentialValue: (searchConfig) => searchConfig?.apiKey,
|
||||
setCredentialValue: (searchConfigTarget, value) => {
|
||||
searchConfigTarget.apiKey = value;
|
||||
},
|
||||
setCredentialValue: setTopLevelCredentialValue,
|
||||
getConfiguredCredentialValue: (config) =>
|
||||
resolveProviderWebSearchPluginConfig(config, "brave")?.apiKey,
|
||||
setConfiguredCredentialValue: (configTarget, value) => {
|
||||
setProviderWebSearchPluginConfigValue(configTarget, "brave", "apiKey", value);
|
||||
},
|
||||
createTool: (ctx) =>
|
||||
createBraveToolDefinition(ctx.config, ctx.searchConfig as SearchConfigRecord | undefined),
|
||||
createBraveToolDefinition(
|
||||
(() => {
|
||||
const searchConfig = ctx.searchConfig as SearchConfigRecord | undefined;
|
||||
const pluginConfig = resolveProviderWebSearchPluginConfig(ctx.config, "brave");
|
||||
if (!pluginConfig) {
|
||||
return searchConfig;
|
||||
}
|
||||
return {
|
||||
...(searchConfig ?? {}),
|
||||
...(pluginConfig.apiKey === undefined ? {} : { apiKey: pluginConfig.apiKey }),
|
||||
brave: {
|
||||
...resolveBraveConfig(searchConfig),
|
||||
...pluginConfig,
|
||||
},
|
||||
} as SearchConfigRecord;
|
||||
})(),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
} from "openclaw/plugin-sdk/provider-models";
|
||||
import {
|
||||
applyAgentDefaultModelPrimary,
|
||||
applyProviderConfigWithModelCatalog,
|
||||
applyProviderConfigWithModelCatalogPreset,
|
||||
type OpenClawConfig,
|
||||
} from "openclaw/plugin-sdk/provider-onboard";
|
||||
|
||||
@@ -17,24 +17,20 @@ export { CHUTES_DEFAULT_MODEL_REF };
|
||||
* Registers all catalog models and sets provider aliases (chutes-fast, etc.).
|
||||
*/
|
||||
export function applyChutesProviderConfig(cfg: OpenClawConfig): OpenClawConfig {
|
||||
const models = { ...cfg.agents?.defaults?.models };
|
||||
for (const m of CHUTES_MODEL_CATALOG) {
|
||||
models[`chutes/${m.id}`] = {
|
||||
...models[`chutes/${m.id}`],
|
||||
};
|
||||
}
|
||||
|
||||
models["chutes-fast"] = { alias: "chutes/zai-org/GLM-4.7-FP8" };
|
||||
models["chutes-vision"] = { alias: "chutes/chutesai/Mistral-Small-3.2-24B-Instruct-2506" };
|
||||
models["chutes-pro"] = { alias: "chutes/deepseek-ai/DeepSeek-V3.2-TEE" };
|
||||
|
||||
const chutesModels = CHUTES_MODEL_CATALOG.map(buildChutesModelDefinition);
|
||||
return applyProviderConfigWithModelCatalog(cfg, {
|
||||
agentModels: models,
|
||||
return applyProviderConfigWithModelCatalogPreset(cfg, {
|
||||
providerId: "chutes",
|
||||
api: "openai-completions",
|
||||
baseUrl: CHUTES_BASE_URL,
|
||||
catalogModels: chutesModels,
|
||||
catalogModels: CHUTES_MODEL_CATALOG.map(buildChutesModelDefinition),
|
||||
aliases: [
|
||||
...CHUTES_MODEL_CATALOG.map((model) => `chutes/${model.id}`),
|
||||
{ modelRef: "chutes-fast", alias: "chutes/zai-org/GLM-4.7-FP8" },
|
||||
{
|
||||
modelRef: "chutes-vision",
|
||||
alias: "chutes/chutesai/Mistral-Small-3.2-24B-Instruct-2506",
|
||||
},
|
||||
{ modelRef: "chutes-pro", alias: "chutes/deepseek-ai/DeepSeek-V3.2-TEE" },
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,8 @@ The tool can return:
|
||||
- `details.viewerUrl`: a gateway URL that can be opened in the canvas
|
||||
- `details.filePath`: a local rendered artifact path when file rendering is requested
|
||||
- `details.fileFormat`: the rendered file format (`png` or `pdf`)
|
||||
- `details.artifactId` and `details.expiresAt`: artifact identity and TTL metadata
|
||||
- `details.context`: available routing metadata such as `agentId`, `sessionId`, `messageChannel`, and `agentAccountId`
|
||||
|
||||
When the plugin is enabled, it also ships a companion skill from `skills/` and prepends stable tool-usage guidance into system-prompt space via `before_prompt_build`. The hook uses `prependSystemContext`, so the guidance stays out of user-prompt space while still being available every turn.
|
||||
|
||||
@@ -49,6 +51,7 @@ Patch:
|
||||
Useful options:
|
||||
|
||||
- `mode`: `view`, `file`, or `both`
|
||||
Deprecated alias: `image` behaves like `file` and is still accepted for backward compatibility.
|
||||
- `layout`: `unified` or `split`
|
||||
- `theme`: `light` or `dark` (default: `dark`)
|
||||
- `fileFormat`: `png` or `pdf` (default: `png`)
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { IncomingMessage } from "node:http";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { createMockServerResponse } from "../../test/helpers/extensions/mock-http-response.js";
|
||||
import { createTestPluginApi } from "../../test/helpers/extensions/plugin-api.js";
|
||||
import type { OpenClawPluginApi } from "./api.js";
|
||||
import type { OpenClawPluginApi, OpenClawPluginToolContext } from "./api.js";
|
||||
import plugin from "./index.js";
|
||||
|
||||
describe("diffs plugin registration", () => {
|
||||
@@ -48,7 +48,9 @@ describe("diffs plugin registration", () => {
|
||||
};
|
||||
type RegisteredHttpRouteParams = Parameters<OpenClawPluginApi["registerHttpRoute"]>[0];
|
||||
|
||||
let registeredTool: RegisteredTool | undefined;
|
||||
let registeredToolFactory:
|
||||
| ((ctx: OpenClawPluginToolContext) => RegisteredTool | RegisteredTool[] | null | undefined)
|
||||
| undefined;
|
||||
let registeredHttpRouteHandler: RegisteredHttpRouteParams["handler"] | undefined;
|
||||
|
||||
const api = createTestPluginApi({
|
||||
@@ -75,7 +77,7 @@ describe("diffs plugin registration", () => {
|
||||
},
|
||||
runtime: {} as never,
|
||||
registerTool(tool: Parameters<OpenClawPluginApi["registerTool"]>[0]) {
|
||||
registeredTool = typeof tool === "function" ? undefined : tool;
|
||||
registeredToolFactory = typeof tool === "function" ? tool : () => tool;
|
||||
},
|
||||
registerHttpRoute(params: RegisteredHttpRouteParams) {
|
||||
registeredHttpRouteHandler = params.handler;
|
||||
@@ -84,6 +86,12 @@ describe("diffs plugin registration", () => {
|
||||
|
||||
plugin.register?.(api as unknown as OpenClawPluginApi);
|
||||
|
||||
const registeredTool = registeredToolFactory?.({
|
||||
agentId: "main",
|
||||
sessionId: "session-123",
|
||||
messageChannel: "discord",
|
||||
agentAccountId: "default",
|
||||
}) as RegisteredTool | undefined;
|
||||
const result = await registeredTool?.execute?.("tool-1", {
|
||||
before: "one\n",
|
||||
after: "two\n",
|
||||
@@ -108,6 +116,14 @@ describe("diffs plugin registration", () => {
|
||||
expect(String(res.body)).toContain('"disableLineNumbers":true');
|
||||
expect(String(res.body)).toContain('"diffIndicators":"classic"');
|
||||
expect(String(res.body)).toContain("--diffs-line-height: 30px;");
|
||||
expect((result as { details?: Record<string, unknown> } | undefined)?.details?.context).toEqual(
|
||||
{
|
||||
agentId: "main",
|
||||
sessionId: "session-123",
|
||||
messageChannel: "discord",
|
||||
agentAccountId: "default",
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import path from "node:path";
|
||||
import type { OpenClawPluginApi } from "./api.js";
|
||||
import { resolvePreferredOpenClawTmpDir } from "./api.js";
|
||||
import {
|
||||
definePluginEntry,
|
||||
resolvePreferredOpenClawTmpDir,
|
||||
type OpenClawPluginApi,
|
||||
} from "./api.js";
|
||||
import {
|
||||
diffsPluginConfigSchema,
|
||||
resolveDiffsPluginDefaults,
|
||||
@@ -11,7 +14,7 @@ import { DIFFS_AGENT_GUIDANCE } from "./src/prompt-guidance.js";
|
||||
import { DiffArtifactStore } from "./src/store.js";
|
||||
import { createDiffsTool } from "./src/tool.js";
|
||||
|
||||
const plugin = {
|
||||
export default definePluginEntry({
|
||||
id: "diffs",
|
||||
name: "Diffs",
|
||||
description: "Read-only diff viewer and PNG/PDF renderer for agents.",
|
||||
@@ -24,7 +27,9 @@ const plugin = {
|
||||
logger: api.logger,
|
||||
});
|
||||
|
||||
api.registerTool(createDiffsTool({ api, store, defaults }));
|
||||
api.registerTool((ctx) => createDiffsTool({ api, store, defaults, context: ctx }), {
|
||||
name: "diffs",
|
||||
});
|
||||
api.registerHttpRoute({
|
||||
path: "/plugins/diffs",
|
||||
auth: "plugin",
|
||||
@@ -39,6 +44,4 @@ const plugin = {
|
||||
prependSystemContext: DIFFS_AGENT_GUIDANCE,
|
||||
}));
|
||||
},
|
||||
};
|
||||
|
||||
export default plugin;
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user