Merge branch 'main' into fix/token-usage-input-output-breakdown

This commit is contained in:
jiarung 2026-03-17 18:54:40 +08:00 committed by GitHub
commit 26e9c84e7a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1283 changed files with 28366 additions and 18812 deletions

View File

@ -273,7 +273,9 @@ jobs:
use-sticky-disk: "false"
- name: Run changed extension tests
run: pnpm test:extension ${{ matrix.extension }}
env:
EXTENSION_ID: ${{ matrix.extension }}
run: pnpm test:extension "$EXTENSION_ID"
# Types, lint, and format check.
check:
@ -458,30 +460,30 @@ jobs:
run: pre-commit run --all-files detect-private-key
- name: Audit changed GitHub workflows with zizmor
env:
BASE_SHA: ${{ github.event_name == 'push' && github.event.before || github.event.pull_request.base.sha }}
run: |
set -euo pipefail
BASE="$(
python - <<'PY'
import json
import os
if [ -z "${BASE_SHA:-}" ] || [ "${BASE_SHA}" = "0000000000000000000000000000000000000000" ]; then
echo "No usable base SHA detected; skipping zizmor."
exit 0
fi
with open(os.environ["GITHUB_EVENT_PATH"], "r", encoding="utf-8") as fh:
event = json.load(fh)
if ! git cat-file -e "${BASE_SHA}^{commit}" 2>/dev/null; then
echo "Base SHA ${BASE_SHA} is unavailable; skipping zizmor."
exit 0
fi
if os.environ["GITHUB_EVENT_NAME"] == "push":
print(event["before"])
else:
print(event["pull_request"]["base"]["sha"])
PY
)"
mapfile -t workflow_files < <(git diff --name-only "$BASE" HEAD -- '.github/workflows/*.yml' '.github/workflows/*.yaml')
mapfile -t workflow_files < <(
git diff --name-only "${BASE_SHA}" HEAD -- '.github/workflows/*.yml' '.github/workflows/*.yaml'
)
if [ "${#workflow_files[@]}" -eq 0 ]; then
echo "No workflow changes detected; skipping zizmor."
exit 0
fi
printf 'Auditing workflow files:\n%s\n' "${workflow_files[@]}"
pre-commit run zizmor --files "${workflow_files[@]}"
- name: Audit production dependencies

1
.gitignore vendored
View File

@ -9,6 +9,7 @@ pnpm-lock.yaml
bun.lock
bun.lockb
coverage
__openclaw_vitest__/
__pycache__/
*.pyc
.tsbuildinfo

View File

@ -7,6 +7,7 @@ Docs: https://docs.openclaw.ai
### Changes
- Commands/btw: add `/btw` side questions for quick tool-less answers about the current session without changing future session context, with dismissible in-session TUI answers and explicit BTW replies on external channels. (#45444) Thanks @ngutman.
- Gateway/docs: clarify that empty URL input allowlists are treated as unset, document `allowUrl: false` as the deny-all switch, and add regression coverage for the normalization path.
- Sandbox/runtime: add pluggable sandbox backends, ship an OpenShell backend with `mirror` and `remote` workspace modes, and make sandbox list/recreate/prune backend-aware instead of Docker-only.
- Sandbox/SSH: add a core SSH sandbox backend with secret-backed key, certificate, and known_hosts inputs, move shared remote exec/filesystem tooling into core, and keep OpenShell focused on sandbox lifecycle plus optional `mirror` mode.
- Web tools/Firecrawl: add Firecrawl as an `onboard`/configure search provider via a bundled plugin, expose explicit `firecrawl_search` and `firecrawl_scrape` tools, and align core `web_fetch` fallback behavior with Firecrawl base-URL/env fallback plus guarded endpoint fetches.
@ -18,6 +19,7 @@ Docs: https://docs.openclaw.ai
- Android/mobile: add a system-aware dark theme across onboarding and post-onboarding screens so the app follows the device theme through setup, chat, and voice flows. (#46249) Thanks @sibbl.
- Feishu/ACP: add current-conversation ACP and subagent session binding for supported DMs and topic conversations, including completion delivery back to the originating Feishu conversation. (#46819) Thanks @Takhoffman.
- Plugins/marketplaces: add Claude marketplace registry resolution, `plugin@marketplace` installs, marketplace listing, and update support, plus Docker E2E coverage for local and official marketplace flows. (#48058) Thanks @vincentkoc.
- Commands/plugins: add owner-gated `/plugins` and `/plugin` chat commands for plugin list/show and enable/disable flows, alongside explicit `commands.plugins` config gating. Thanks @vincentkoc.
- Feishu/cards: add structured interactive approval and quick-action launcher cards, preserve callback user and conversation context through routing, and keep legacy card-action fallback behavior so common actions can run without typing raw commands. (#47873) Thanks @Takhoffman.
- Feishu/streaming: add `onReasoningStream` and `onReasoningEnd` support to streaming cards, so `/reasoning stream` renders thinking tokens as markdown blockquotes in the same card — matching the Telegram channel's reasoning lane behavior. (#46029) Thanks @day253.
- 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.
@ -30,11 +32,15 @@ Docs: https://docs.openclaw.ai
- secrets: harden read-only SecretRef command paths and diagnostics. (#47794) Thanks @joshavant.
- Browser/existing-session: support `browser.profiles.<name>.userDataDir` so Chrome DevTools MCP can attach to Brave, Edge, and other Chromium-based browsers through their own user data directories. (#48170) Thanks @velvet-shark.
- Skills/prompt budget: preserve all registered skills via a compact catalog fallback before dropping entries when the full prompt format exceeds `maxSkillsPromptChars`. (#47553) Thanks @snese.
- Plugins/bundles: make enabled bundle MCP servers expose runnable tools in embedded Pi, and default relative bundle MCP launches to the bundle root so marketplace bundles like Context7 work through Pi instead of stopping at config import.
- Scope message SecretRef resolution and harden doctor/status paths. (#48728) Thanks @joshavant.
- Plugins/testing: add a public `openclaw/plugin-sdk/testing` seam for plugin-author test helpers, and move bundled-extension-only test bridges out of `extensions/` into private repo test helpers.
### Breaking
- Browser/Chrome MCP: remove the legacy Chrome extension relay path, bundled extension assets, `driver: "extension"`, and `browser.relayBindHost`. Run `openclaw doctor --fix` to migrate host-local browser config to `existing-session` / `user`; Docker, headless, sandbox, and remote browser flows still use raw CDP. (#47893) Thanks @vincentkoc.
- Plugins/runtime: remove the public `openclaw/extension-api` surface with no compatibility shim. Bundled plugins must use injected runtime for host-side operations (for example `api.runtime.agent.runEmbeddedPiAgent`) and any remaining direct imports must come from narrow `openclaw/plugin-sdk/*` subpaths instead of the monolithic SDK root.
- Tools/image generation: standardize the stock image create/edit path on the core `image_generate` tool. The old `nano-banana-pro` docs/examples are gone; if you previously copied that sample-skill config, switch to `agents.defaults.imageGenerationModel` for built-in image generation or install a separate third-party skill explicitly.
### Fixes
@ -70,6 +76,7 @@ Docs: https://docs.openclaw.ai
- Feishu/actions: expand the runtime action surface with message read/edit, explicit thread replies, pinning, and operator-facing chat/member inspection so Feishu can operate more of the workspace directly. (#47968) Thanks @Takhoffman.
- Feishu/topic threads: fetch full thread context, including prior bot replies, when starting a topic-thread session so follow-up turns in Feishu topics keep the right conversation state. (#45254) Thanks @Coobiw.
- Feishu/media: keep native image, file, audio, and video/media handling aligned across outbound sends, inbound downloads, thread replies, directory/action aliases, and capability docs so unsupported areas are explicit instead of implied. (#47968) Thanks @Takhoffman.
- Feishu/webhooks: harden signed webhook verification to use constant-time signature comparison and keep malformed short signatures fail-closed in webhook E2E coverage.
- WhatsApp/reconnect: restore the append recency filter in the extension inbox monitor and handle protobuf `Long` timestamps correctly, so fresh post-reconnect append messages are processed while stale history sync stays suppressed. (#42588) Thanks @MonkeyLeeT.
- WhatsApp/login: wait for pending creds writes before reopening after Baileys `515` pairing restarts in both QR login and `channels login` flows, and keep the restart coverage pinned to the real wrapped error shape plus per-account creds queues. (#27910) Thanks @asyncjason.
- Telegram/message send: forward `--force-document` through the `sendPayload` path as well as `sendMedia`, so Telegram payload sends with `channelData` keep uploading images as documents instead of silently falling back to compressed photo sends. (#47119) Thanks @thepagent.
@ -108,6 +115,15 @@ Docs: https://docs.openclaw.ai
- Control UI/overview: keep the language dropdown aligned with the persisted locale during dashboard startup so refreshing the page does not fall back to English before locale hydration completes. (#48019) Thanks @git-jxj.
- Agents/compaction: rerun transcript repair after `session.compact()` so orphaned `tool_result` blocks cannot survive compaction and break later Anthropic requests. (#16095) thanks @claw-sylphx.
- Agents/compaction: trigger overflow recovery from the tool-result guard once post-compaction context still exceeds the safe threshold, so long tool loops compact before the next model call hard-fails. (#29371) thanks @keshav55.
- macOS/exec approvals: harden exec-host request HMAC verification to use a timing-safe compare and keep malformed or truncated signatures fail-closed in focused IPC auth coverage.
- Gateway/exec approvals: surface requested env override keys in gateway-host approval prompts so operators can review surviving env context without inheriting noisy base host env.
### Fixes
- Agents/bootstrap warnings: move bootstrap truncation warnings out of the system prompt and into the per-turn prompt body so prompt-cache reuse stays stable when truncation warnings appear or disappear. (#48753) Thanks @scoootscooob and @obviyus.
- Telegram/DM topic session keys: route named-account DM topics through the same per-account base session key across inbound messages, native commands, and session-state lookups so `/status` and thread recovery stop creating phantom `agent:main:main:thread:...` sessions. (#48204) Thanks @vincentkoc.
- macOS/node service startup: use `openclaw node start/stop --json` from the Mac app instead of the removed `openclaw service node ...` command shape, so current CLI installs expose the full node exec surface again. (#46843) Fixes #43171. Thanks @Br1an67.
- macOS/launch at login: stop emitting `KeepAlive` for the desktop app launch agent so OpenClaw no longer relaunches immediately after a manual quit while launch at login remains enabled. (#40213) Thanks @stablegenius49.
## 2026.3.13
@ -282,6 +298,8 @@ Docs: https://docs.openclaw.ai
- Discord/gateway startup: treat plain-text and transient `/gateway/bot` metadata fetch failures as transient startup errors so Discord gateway boot no longer crashes on unhandled rejections. (#44397) Thanks @jalehman.
- Agents/Ollama overflow: rewrite Ollama `prompt too long` API payloads through the normal context-overflow sanitizer so embedded sessions keep the friendly overflow copy and auto-compaction trigger. (#34019) thanks @lishuaigit.
- Control UI/auth: restore one-time legacy `?token=` imports for shared Control UI links while keeping `#token=` preferred, and carry pending query tokens through gateway URL confirmation so compatibility links still authenticate after confirmation. (#43979) Thanks @stim64045-spec.
## 2026.3.11
### Security
@ -1211,7 +1229,7 @@ Docs: https://docs.openclaw.ai
- Signal/Sync message null-handling: treat `syncMessage` presence (including `null`) as sync envelope traffic so replayed sentTranscript payloads cannot bypass loop guards after daemon restart. Landed from contributor PR #31138 by @Sid-Qin. Thanks @Sid-Qin.
- Infra/fs-safe: sanitize directory-read failures so raw `EISDIR` text never leaks to messaging surfaces, with regression tests for both root-scoped and direct safe reads. Landed from contributor PR #31205 by @polooooo. Thanks @polooooo.
## Unreleased
## 2026.2.27
### Changes

View File

@ -93,6 +93,7 @@ Welcome to the lobster tank! 🦞
- `pnpm test:extension <extension-name>`
- `pnpm test:extension --list` to see valid extension ids
- If you changed shared plugin or channel surfaces, run `pnpm test:contracts`
- For targeted shared-surface work, use `pnpm test:contracts:channels` or `pnpm test:contracts:plugins`
- If you changed broader runtime behavior, still run the relevant wider lanes (`pnpm test:extensions`, `pnpm test:channels`, or `pnpm test`) before asking for review
- If you have access to Codex, run `codex review --base origin/main` locally before opening or updating your PR. Treat this as the current highest standard of AI review, even if GitHub Codex review also runs.
- Ensure CI checks pass

View File

@ -81,22 +81,23 @@ final class CanvasSchemeHandler: NSObject, WKURLSchemeHandler {
return self.html("Not Found", title: "Canvas: 404")
}
// Directory traversal guard: served files must live under the session root.
let standardizedRoot = sessionRoot.standardizedFileURL
let standardizedFile = fileURL.standardizedFileURL
guard standardizedFile.path.hasPrefix(standardizedRoot.path) else {
// Resolve symlinks before enforcing the session-root boundary so links inside
// the canvas tree cannot escape to arbitrary host files.
let resolvedRoot = sessionRoot.resolvingSymlinksInPath().standardizedFileURL
let resolvedFile = fileURL.resolvingSymlinksInPath().standardizedFileURL
guard self.isFileURL(resolvedFile, withinDirectory: resolvedRoot) else {
return self.html("Forbidden", title: "Canvas: 403")
}
do {
let data = try Data(contentsOf: standardizedFile)
let mime = CanvasScheme.mimeType(forExtension: standardizedFile.pathExtension)
let servedPath = standardizedFile.path
let data = try Data(contentsOf: resolvedFile)
let mime = CanvasScheme.mimeType(forExtension: resolvedFile.pathExtension)
let servedPath = resolvedFile.path
canvasLogger.debug(
"served \(session, privacy: .public)/\(path, privacy: .public) -> \(servedPath, privacy: .public)")
return CanvasResponse(mime: mime, data: data)
} catch {
let failedPath = standardizedFile.path
let failedPath = resolvedFile.path
let errorText = error.localizedDescription
canvasLogger
.error(
@ -145,6 +146,11 @@ final class CanvasSchemeHandler: NSObject, WKURLSchemeHandler {
return nil
}
private func isFileURL(_ fileURL: URL, withinDirectory rootURL: URL) -> Bool {
let rootPath = rootURL.path.hasSuffix("/") ? rootURL.path : rootURL.path + "/"
return fileURL.path == rootURL.path || fileURL.path.hasPrefix(rootPath)
}
private func html(_ body: String, title: String = "Canvas") -> CanvasResponse {
let html = """
<!doctype html>

View File

@ -89,6 +89,20 @@ private func readLineFromHandle(_ handle: FileHandle, maxBytes: Int) throws -> S
return String(data: lineData, encoding: .utf8)
}
func timingSafeHexStringEquals(_ lhs: String, _ rhs: String) -> Bool {
let lhsBytes = Array(lhs.utf8)
let rhsBytes = Array(rhs.utf8)
guard lhsBytes.count == rhsBytes.count else {
return false
}
var diff: UInt8 = 0
for index in lhsBytes.indices {
diff |= lhsBytes[index] ^ rhsBytes[index]
}
return diff == 0
}
enum ExecApprovalsSocketClient {
private struct TimeoutError: LocalizedError {
var message: String
@ -854,7 +868,7 @@ private final class ExecApprovalsSocketServer: @unchecked Sendable {
error: ExecHostError(code: "INVALID_REQUEST", message: "expired request", reason: "ttl"))
}
let expected = self.hmacHex(nonce: request.nonce, ts: request.ts, requestJson: request.requestJson)
if expected != request.hmac {
if !timingSafeHexStringEquals(expected, request.hmac) {
return ExecHostResponse(
type: "exec-res",
id: request.id,

View File

@ -26,7 +26,12 @@ enum LaunchAgentManager {
}
private static func writePlist(bundlePath: String) {
let plist = """
let plist = self.plistContents(bundlePath: bundlePath)
try? plist.write(to: self.plistURL, atomically: true, encoding: .utf8)
}
static func plistContents(bundlePath: String) -> String {
"""
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
@ -41,8 +46,6 @@ enum LaunchAgentManager {
<string>\(FileManager().homeDirectoryForCurrentUser.path)</string>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
@ -55,7 +58,6 @@ enum LaunchAgentManager {
</dict>
</plist>
"""
try? plist.write(to: self.plistURL, atomically: true, encoding: .utf8)
}
@discardableResult

View File

@ -6,7 +6,7 @@ enum NodeServiceManager {
static func start() async -> String? {
let result = await self.runServiceCommandResult(
["node", "start"],
["start"],
timeout: 20,
quiet: false)
if let error = self.errorMessage(from: result, treatNotLoadedAsError: true) {
@ -18,7 +18,7 @@ enum NodeServiceManager {
static func stop() async -> String? {
let result = await self.runServiceCommandResult(
["node", "stop"],
["stop"],
timeout: 15,
quiet: false)
if let error = self.errorMessage(from: result, treatNotLoadedAsError: false) {
@ -30,6 +30,14 @@ enum NodeServiceManager {
}
extension NodeServiceManager {
private static func serviceCommand(_ args: [String]) -> [String] {
CommandResolver.openclawCommand(
subcommand: "node",
extraArgs: self.withJsonFlag(args),
// Service management must always run locally, even if remote mode is configured.
configRoot: ["gateway": ["mode": "local"]])
}
private struct CommandResult {
let success: Bool
let payload: Data?
@ -52,11 +60,7 @@ extension NodeServiceManager {
timeout: Double,
quiet: Bool) async -> CommandResult
{
let command = CommandResolver.openclawCommand(
subcommand: "service",
extraArgs: self.withJsonFlag(args),
// Service management must always run locally, even if remote mode is configured.
configRoot: ["gateway": ["mode": "local"]])
let command = self.serviceCommand(args)
var env = ProcessInfo.processInfo.environment
env["PATH"] = CommandResolver.preferredPaths().joined(separator: ":")
let response = await ShellExecutor.runDetailed(command: command, cwd: nil, env: env, timeout: timeout)
@ -136,3 +140,11 @@ extension NodeServiceManager {
TextSummarySupport.summarizeLastLine(text)
}
}
#if DEBUG
extension NodeServiceManager {
static func _testServiceCommand(_ args: [String]) -> [String] {
self.serviceCommand(args)
}
}
#endif

View File

@ -0,0 +1,21 @@
import Testing
@testable import OpenClaw
struct ExecApprovalsSocketAuthTests {
@Test
func `timing safe hex compare matches equal strings`() {
#expect(timingSafeHexStringEquals(String(repeating: "a", count: 64), String(repeating: "a", count: 64)))
}
@Test
func `timing safe hex compare rejects mismatched strings`() {
let expected = String(repeating: "a", count: 63) + "b"
let provided = String(repeating: "a", count: 63) + "c"
#expect(!timingSafeHexStringEquals(expected, provided))
}
@Test
func `timing safe hex compare rejects different length strings`() {
#expect(!timingSafeHexStringEquals(String(repeating: "a", count: 64), "deadbeef"))
}
}

View File

@ -0,0 +1,19 @@
import Foundation
import Testing
@testable import OpenClaw
struct LaunchAgentManagerTests {
@Test func `launch at login plist does not keep app alive after manual quit`() throws {
let plist = LaunchAgentManager.plistContents(bundlePath: "/Applications/OpenClaw.app")
let data = try #require(plist.data(using: .utf8))
let object = try #require(
PropertyListSerialization.propertyList(from: data, format: nil) as? [String: Any]
)
#expect(object["RunAtLoad"] as? Bool == true)
#expect(object["KeepAlive"] == nil)
let args = try #require(object["ProgramArguments"] as? [String])
#expect(args == ["/Applications/OpenClaw.app/Contents/MacOS/OpenClaw"])
}
}

View File

@ -216,6 +216,32 @@ struct LowCoverageHelperTests {
#expect(handler._testTextEncodingName(for: "application/octet-stream") == nil)
}
@Test @MainActor func `canvas scheme handler blocks symlink escapes`() throws {
let root = FileManager().temporaryDirectory
.appendingPathComponent("canvas-\(UUID().uuidString)", isDirectory: true)
defer { try? FileManager().removeItem(at: root) }
try FileManager().createDirectory(at: root, withIntermediateDirectories: true)
let session = root.appendingPathComponent("main", isDirectory: true)
try FileManager().createDirectory(at: session, withIntermediateDirectories: true)
let outside = root.deletingLastPathComponent().appendingPathComponent("canvas-secret-\(UUID().uuidString).txt")
defer { try? FileManager().removeItem(at: outside) }
try "top-secret".write(to: outside, atomically: true, encoding: .utf8)
let symlink = session.appendingPathComponent("index.html")
try FileManager().createSymbolicLink(at: symlink, withDestinationURL: outside)
let handler = CanvasSchemeHandler(root: root)
let url = try #require(CanvasScheme.makeURL(session: "main", path: "index.html"))
let response = handler._testResponse(for: url)
let body = String(data: response.data, encoding: .utf8) ?? ""
#expect(response.mime == "text/html")
#expect(body.contains("Forbidden"))
#expect(!body.contains("top-secret"))
}
@Test @MainActor func `menu context card injector inserts and finds index`() {
let injector = MenuContextCardInjector()
let menu = NSMenu()

View File

@ -0,0 +1,19 @@
import Foundation
import Testing
@testable import OpenClaw
@Suite(.serialized) struct NodeServiceManagerTests {
@Test func `builds node service commands with current CLI shape`() throws {
let tmp = try makeTempDirForTests()
CommandResolver.setProjectRoot(tmp.path)
let openclawPath = tmp.appendingPathComponent("node_modules/.bin/openclaw")
try makeExecutableForTests(at: openclawPath)
let start = NodeServiceManager._testServiceCommand(["start"])
#expect(start == [openclawPath.path, "node", "start", "--json"])
let stop = NodeServiceManager._testServiceCommand(["stop"])
#expect(stop == [openclawPath.path, "node", "stop", "--json"])
}
}

View File

@ -1754,6 +1754,58 @@
"help": "Delay style for block replies (\"off\", \"natural\", \"custom\").",
"hasChildren": false
},
{
"path": "agents.defaults.imageGenerationModel",
"kind": "core",
"type": [
"object",
"string"
],
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [],
"hasChildren": true
},
{
"path": "agents.defaults.imageGenerationModel.fallbacks",
"kind": "core",
"type": "array",
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [
"media",
"reliability"
],
"label": "Image Generation Model Fallbacks",
"help": "Ordered fallback image-generation models (provider/model).",
"hasChildren": true
},
{
"path": "agents.defaults.imageGenerationModel.fallbacks.*",
"kind": "core",
"type": "string",
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [],
"hasChildren": false
},
{
"path": "agents.defaults.imageGenerationModel.primary",
"kind": "core",
"type": "string",
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [
"media"
],
"label": "Image Generation Model",
"help": "Optional image-generation model (provider/model) used by the shared image generation capability.",
"hasChildren": false
},
{
"path": "agents.defaults.imageMaxDimensionPx",
"kind": "core",
@ -11733,6 +11785,116 @@
"tags": [],
"hasChildren": false
},
{
"path": "channels.discord.accounts.*.voice.tts.microsoft",
"kind": "channel",
"type": "object",
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [],
"hasChildren": true
},
{
"path": "channels.discord.accounts.*.voice.tts.microsoft.enabled",
"kind": "channel",
"type": "boolean",
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [],
"hasChildren": false
},
{
"path": "channels.discord.accounts.*.voice.tts.microsoft.lang",
"kind": "channel",
"type": "string",
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [],
"hasChildren": false
},
{
"path": "channels.discord.accounts.*.voice.tts.microsoft.outputFormat",
"kind": "channel",
"type": "string",
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [],
"hasChildren": false
},
{
"path": "channels.discord.accounts.*.voice.tts.microsoft.pitch",
"kind": "channel",
"type": "string",
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [],
"hasChildren": false
},
{
"path": "channels.discord.accounts.*.voice.tts.microsoft.proxy",
"kind": "channel",
"type": "string",
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [],
"hasChildren": false
},
{
"path": "channels.discord.accounts.*.voice.tts.microsoft.rate",
"kind": "channel",
"type": "string",
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [],
"hasChildren": false
},
{
"path": "channels.discord.accounts.*.voice.tts.microsoft.saveSubtitles",
"kind": "channel",
"type": "boolean",
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [],
"hasChildren": false
},
{
"path": "channels.discord.accounts.*.voice.tts.microsoft.timeoutMs",
"kind": "channel",
"type": "integer",
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [],
"hasChildren": false
},
{
"path": "channels.discord.accounts.*.voice.tts.microsoft.voice",
"kind": "channel",
"type": "string",
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [],
"hasChildren": false
},
{
"path": "channels.discord.accounts.*.voice.tts.microsoft.volume",
"kind": "channel",
"type": "string",
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [],
"hasChildren": false
},
{
"path": "channels.discord.accounts.*.voice.tts.mode",
"kind": "channel",
@ -11961,11 +12123,6 @@
"kind": "channel",
"type": "string",
"required": false,
"enumValues": [
"elevenlabs",
"openai",
"edge"
],
"deprecated": false,
"sensitive": false,
"tags": [],
@ -14698,6 +14855,116 @@
"tags": [],
"hasChildren": false
},
{
"path": "channels.discord.voice.tts.microsoft",
"kind": "channel",
"type": "object",
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [],
"hasChildren": true
},
{
"path": "channels.discord.voice.tts.microsoft.enabled",
"kind": "channel",
"type": "boolean",
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [],
"hasChildren": false
},
{
"path": "channels.discord.voice.tts.microsoft.lang",
"kind": "channel",
"type": "string",
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [],
"hasChildren": false
},
{
"path": "channels.discord.voice.tts.microsoft.outputFormat",
"kind": "channel",
"type": "string",
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [],
"hasChildren": false
},
{
"path": "channels.discord.voice.tts.microsoft.pitch",
"kind": "channel",
"type": "string",
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [],
"hasChildren": false
},
{
"path": "channels.discord.voice.tts.microsoft.proxy",
"kind": "channel",
"type": "string",
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [],
"hasChildren": false
},
{
"path": "channels.discord.voice.tts.microsoft.rate",
"kind": "channel",
"type": "string",
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [],
"hasChildren": false
},
{
"path": "channels.discord.voice.tts.microsoft.saveSubtitles",
"kind": "channel",
"type": "boolean",
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [],
"hasChildren": false
},
{
"path": "channels.discord.voice.tts.microsoft.timeoutMs",
"kind": "channel",
"type": "integer",
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [],
"hasChildren": false
},
{
"path": "channels.discord.voice.tts.microsoft.voice",
"kind": "channel",
"type": "string",
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [],
"hasChildren": false
},
{
"path": "channels.discord.voice.tts.microsoft.volume",
"kind": "channel",
"type": "string",
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [],
"hasChildren": false
},
{
"path": "channels.discord.voice.tts.mode",
"kind": "channel",
@ -14926,11 +15193,6 @@
"kind": "channel",
"type": "string",
"required": false,
"enumValues": [
"elevenlabs",
"openai",
"edge"
],
"deprecated": false,
"sensitive": false,
"tags": [],
@ -38002,6 +38264,20 @@
"help": "Allow /debug chat command for runtime-only overrides (default: false).",
"hasChildren": false
},
{
"path": "commands.mcp",
"kind": "core",
"type": "boolean",
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [
"advanced"
],
"label": "Allow /mcp",
"help": "Allow /mcp chat command to manage OpenClaw MCP server config under mcp.servers (default: false).",
"hasChildren": false
},
{
"path": "commands.native",
"kind": "core",
@ -38098,6 +38374,20 @@
"help": "Optional secret used to HMAC hash owner IDs when ownerDisplay=hash. Prefer env substitution.",
"hasChildren": false
},
{
"path": "commands.plugins",
"kind": "core",
"type": "boolean",
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [
"advanced"
],
"label": "Allow /plugins",
"help": "Allow /plugins chat command to list discovered plugins and toggle plugin enablement in config (default: false).",
"hasChildren": false
},
{
"path": "commands.restart",
"kind": "core",
@ -39636,7 +39926,7 @@
"network"
],
"label": "OpenAI Chat Completions Allow Image URLs",
"help": "Allow server-side URL fetches for `image_url` parts (default: false; data URIs remain supported).",
"help": "Allow server-side URL fetches for `image_url` parts (default: false; data URIs remain supported). Set this to `false` to disable URL fetching entirely.",
"hasChildren": false
},
{
@ -39701,7 +39991,7 @@
"network"
],
"label": "OpenAI Chat Completions Image URL Allowlist",
"help": "Optional hostname allowlist for `image_url` URL fetches; supports exact hosts and `*.example.com` wildcards.",
"help": "Optional hostname allowlist for `image_url` URL fetches; supports exact hosts and `*.example.com` wildcards. Empty or omitted lists mean no hostname allowlist restriction.",
"hasChildren": true
},
{
@ -42004,6 +42294,137 @@
"help": "Sensitive redaction mode: \"off\" disables built-in masking, while \"tools\" redacts sensitive tool/config payload fields. Keep \"tools\" in shared logs unless you have isolated secure log sinks.",
"hasChildren": false
},
{
"path": "mcp",
"kind": "core",
"type": "object",
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [
"advanced"
],
"label": "MCP",
"help": "Global MCP server definitions managed by OpenClaw. Embedded Pi and other runtime adapters can consume these servers without storing them inside Pi-owned project settings.",
"hasChildren": true
},
{
"path": "mcp.servers",
"kind": "core",
"type": "object",
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [
"advanced"
],
"label": "MCP Servers",
"help": "Named MCP server definitions. OpenClaw stores them in its own config and runtime adapters decide which transports are supported at execution time.",
"hasChildren": true
},
{
"path": "mcp.servers.*",
"kind": "core",
"type": "object",
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [],
"hasChildren": true
},
{
"path": "mcp.servers.*.*",
"kind": "core",
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [],
"hasChildren": false
},
{
"path": "mcp.servers.*.args",
"kind": "core",
"type": "array",
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [],
"hasChildren": true
},
{
"path": "mcp.servers.*.args.*",
"kind": "core",
"type": "string",
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [],
"hasChildren": false
},
{
"path": "mcp.servers.*.command",
"kind": "core",
"type": "string",
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [],
"hasChildren": false
},
{
"path": "mcp.servers.*.cwd",
"kind": "core",
"type": "string",
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [],
"hasChildren": false
},
{
"path": "mcp.servers.*.env",
"kind": "core",
"type": "object",
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [],
"hasChildren": true
},
{
"path": "mcp.servers.*.env.*",
"kind": "core",
"type": [
"boolean",
"number",
"string"
],
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [],
"hasChildren": false
},
{
"path": "mcp.servers.*.url",
"kind": "core",
"type": "string",
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [],
"hasChildren": false
},
{
"path": "mcp.servers.*.workingDirectory",
"kind": "core",
"type": "string",
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [],
"hasChildren": false
},
{
"path": "media",
"kind": "core",
@ -43560,6 +43981,116 @@
"tags": [],
"hasChildren": false
},
{
"path": "messages.tts.microsoft",
"kind": "core",
"type": "object",
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [],
"hasChildren": true
},
{
"path": "messages.tts.microsoft.enabled",
"kind": "core",
"type": "boolean",
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [],
"hasChildren": false
},
{
"path": "messages.tts.microsoft.lang",
"kind": "core",
"type": "string",
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [],
"hasChildren": false
},
{
"path": "messages.tts.microsoft.outputFormat",
"kind": "core",
"type": "string",
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [],
"hasChildren": false
},
{
"path": "messages.tts.microsoft.pitch",
"kind": "core",
"type": "string",
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [],
"hasChildren": false
},
{
"path": "messages.tts.microsoft.proxy",
"kind": "core",
"type": "string",
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [],
"hasChildren": false
},
{
"path": "messages.tts.microsoft.rate",
"kind": "core",
"type": "string",
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [],
"hasChildren": false
},
{
"path": "messages.tts.microsoft.saveSubtitles",
"kind": "core",
"type": "boolean",
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [],
"hasChildren": false
},
{
"path": "messages.tts.microsoft.timeoutMs",
"kind": "core",
"type": "integer",
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [],
"hasChildren": false
},
{
"path": "messages.tts.microsoft.voice",
"kind": "core",
"type": "string",
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [],
"hasChildren": false
},
{
"path": "messages.tts.microsoft.volume",
"kind": "core",
"type": "string",
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [],
"hasChildren": false
},
{
"path": "messages.tts.mode",
"kind": "core",
@ -43786,11 +44317,6 @@
"kind": "core",
"type": "string",
"required": false,
"enumValues": [
"elevenlabs",
"openai",
"edge"
],
"deprecated": false,
"sensitive": false,
"tags": [],
@ -46144,6 +46670,75 @@
"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.elevenlabs",
"kind": "plugin",
"type": "object",
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [
"advanced"
],
"label": "@openclaw/elevenlabs-speech",
"help": "OpenClaw ElevenLabs speech plugin (plugin: elevenlabs)",
"hasChildren": true
},
{
"path": "plugins.entries.elevenlabs.config",
"kind": "plugin",
"type": "object",
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [
"advanced"
],
"label": "@openclaw/elevenlabs-speech Config",
"help": "Plugin-defined config payload for elevenlabs.",
"hasChildren": false
},
{
"path": "plugins.entries.elevenlabs.enabled",
"kind": "plugin",
"type": "boolean",
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [
"advanced"
],
"label": "Enable @openclaw/elevenlabs-speech",
"hasChildren": false
},
{
"path": "plugins.entries.elevenlabs.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.elevenlabs.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.feishu",
"kind": "plugin",
@ -46766,7 +47361,7 @@
"hasChildren": false
},
{
"path": "plugins.entries.kimi-coding",
"path": "plugins.entries.kimi",
"kind": "plugin",
"type": "object",
"required": false,
@ -46775,12 +47370,12 @@
"tags": [
"advanced"
],
"label": "@openclaw/kimi-coding-provider",
"help": "OpenClaw Kimi Coding provider plugin (plugin: kimi-coding)",
"label": "@openclaw/kimi-provider",
"help": "OpenClaw Kimi provider plugin (plugin: kimi)",
"hasChildren": true
},
{
"path": "plugins.entries.kimi-coding.config",
"path": "plugins.entries.kimi.config",
"kind": "plugin",
"type": "object",
"required": false,
@ -46789,12 +47384,12 @@
"tags": [
"advanced"
],
"label": "@openclaw/kimi-coding-provider Config",
"help": "Plugin-defined config payload for kimi-coding.",
"label": "@openclaw/kimi-provider Config",
"help": "Plugin-defined config payload for kimi.",
"hasChildren": false
},
{
"path": "plugins.entries.kimi-coding.enabled",
"path": "plugins.entries.kimi.enabled",
"kind": "plugin",
"type": "boolean",
"required": false,
@ -46803,11 +47398,11 @@
"tags": [
"advanced"
],
"label": "Enable @openclaw/kimi-coding-provider",
"label": "Enable @openclaw/kimi-provider",
"hasChildren": false
},
{
"path": "plugins.entries.kimi-coding.hooks",
"path": "plugins.entries.kimi.hooks",
"kind": "plugin",
"type": "object",
"required": false,
@ -46821,7 +47416,7 @@
"hasChildren": true
},
{
"path": "plugins.entries.kimi-coding.hooks.allowPromptInjection",
"path": "plugins.entries.kimi.hooks.allowPromptInjection",
"kind": "plugin",
"type": "boolean",
"required": false,
@ -47516,6 +48111,75 @@
"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.microsoft",
"kind": "plugin",
"type": "object",
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [
"advanced"
],
"label": "@openclaw/microsoft-speech",
"help": "OpenClaw Microsoft speech plugin (plugin: microsoft)",
"hasChildren": true
},
{
"path": "plugins.entries.microsoft.config",
"kind": "plugin",
"type": "object",
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [
"advanced"
],
"label": "@openclaw/microsoft-speech Config",
"help": "Plugin-defined config payload for microsoft.",
"hasChildren": false
},
{
"path": "plugins.entries.microsoft.enabled",
"kind": "plugin",
"type": "boolean",
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [
"advanced"
],
"label": "Enable @openclaw/microsoft-speech",
"hasChildren": false
},
{
"path": "plugins.entries.microsoft.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.microsoft.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.minimax",
"kind": "plugin",
@ -51184,11 +51848,6 @@
"kind": "plugin",
"type": "string",
"required": false,
"enumValues": [
"openai",
"elevenlabs",
"edge"
],
"deprecated": false,
"sensitive": false,
"tags": [
@ -51196,7 +51855,7 @@
"media"
],
"label": "TTS Provider Override",
"help": "Deep-merges with messages.tts (Edge is ignored for calls).",
"help": "Deep-merges with messages.tts (Microsoft is ignored for calls).",
"hasChildren": false
},
{

View File

@ -1,4 +1,4 @@
{"generatedBy":"scripts/generate-config-doc-baseline.ts","recordType":"meta","totalPaths":5104}
{"generatedBy":"scripts/generate-config-doc-baseline.ts","recordType":"meta","totalPaths":5165}
{"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}
@ -150,6 +150,10 @@
{"recordType":"path","path":"agents.defaults.humanDelay.maxMs","kind":"core","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":["performance"],"label":"Human Delay Max (ms)","help":"Maximum delay in ms for custom humanDelay (default: 2500).","hasChildren":false}
{"recordType":"path","path":"agents.defaults.humanDelay.minMs","kind":"core","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Human Delay Min (ms)","help":"Minimum delay in ms for custom humanDelay (default: 800).","hasChildren":false}
{"recordType":"path","path":"agents.defaults.humanDelay.mode","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Human Delay Mode","help":"Delay style for block replies (\"off\", \"natural\", \"custom\").","hasChildren":false}
{"recordType":"path","path":"agents.defaults.imageGenerationModel","kind":"core","type":["object","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"agents.defaults.imageGenerationModel.fallbacks","kind":"core","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["media","reliability"],"label":"Image Generation Model Fallbacks","help":"Ordered fallback image-generation models (provider/model).","hasChildren":true}
{"recordType":"path","path":"agents.defaults.imageGenerationModel.fallbacks.*","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"agents.defaults.imageGenerationModel.primary","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":["media"],"label":"Image Generation Model","help":"Optional image-generation model (provider/model) used by the shared image generation capability.","hasChildren":false}
{"recordType":"path","path":"agents.defaults.imageMaxDimensionPx","kind":"core","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":["media","performance"],"label":"Image Max Dimension (px)","help":"Max image side length in pixels when sanitizing transcript/tool-result image payloads (default: 1200).","hasChildren":false}
{"recordType":"path","path":"agents.defaults.imageModel","kind":"core","type":["object","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"agents.defaults.imageModel.fallbacks","kind":"core","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["media","models","reliability"],"label":"Image Model Fallbacks","help":"Ordered fallback image models (provider/model).","hasChildren":true}
@ -1047,6 +1051,17 @@
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.elevenlabs.voiceSettings.useSpeakerBoost","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.maxTextLength","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.microsoft","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.microsoft.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.microsoft.lang","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.microsoft.outputFormat","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.microsoft.pitch","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.microsoft.proxy","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.microsoft.rate","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.microsoft.saveSubtitles","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.microsoft.timeoutMs","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.microsoft.voice","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.microsoft.volume","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.mode","kind":"channel","type":"string","required":false,"enumValues":["final","all"],"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.modelOverrides","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.modelOverrides.allowModelId","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
@ -1068,7 +1083,7 @@
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.openai.speed","kind":"channel","type":"number","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.openai.voice","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.prefsPath","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.provider","kind":"channel","type":"string","required":false,"enumValues":["elevenlabs","openai","edge"],"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.provider","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.summaryModel","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.timeoutMs","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.ackReaction","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
@ -1302,6 +1317,17 @@
{"recordType":"path","path":"channels.discord.voice.tts.elevenlabs.voiceSettings.useSpeakerBoost","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.voice.tts.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.voice.tts.maxTextLength","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.voice.tts.microsoft","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.discord.voice.tts.microsoft.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.voice.tts.microsoft.lang","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.voice.tts.microsoft.outputFormat","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.voice.tts.microsoft.pitch","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.voice.tts.microsoft.proxy","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.voice.tts.microsoft.rate","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.voice.tts.microsoft.saveSubtitles","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.voice.tts.microsoft.timeoutMs","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.voice.tts.microsoft.voice","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.voice.tts.microsoft.volume","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.voice.tts.mode","kind":"channel","type":"string","required":false,"enumValues":["final","all"],"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.voice.tts.modelOverrides","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.discord.voice.tts.modelOverrides.allowModelId","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
@ -1323,7 +1349,7 @@
{"recordType":"path","path":"channels.discord.voice.tts.openai.speed","kind":"channel","type":"number","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.voice.tts.openai.voice","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.voice.tts.prefsPath","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.discord.voice.tts.provider","kind":"channel","type":"string","required":false,"enumValues":["elevenlabs","openai","edge"],"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"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}
@ -3431,12 +3457,14 @@
{"recordType":"path","path":"commands.bashForegroundMs","kind":"core","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Bash Foreground Window (ms)","help":"How long bash waits before backgrounding (default: 2000; 0 backgrounds immediately).","hasChildren":false}
{"recordType":"path","path":"commands.config","kind":"core","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Allow /config","help":"Allow /config chat command to read/write config on disk (default: false).","hasChildren":false}
{"recordType":"path","path":"commands.debug","kind":"core","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Allow /debug","help":"Allow /debug chat command for runtime-only overrides (default: false).","hasChildren":false}
{"recordType":"path","path":"commands.mcp","kind":"core","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Allow /mcp","help":"Allow /mcp chat command to manage OpenClaw MCP server config under mcp.servers (default: false).","hasChildren":false}
{"recordType":"path","path":"commands.native","kind":"core","type":["boolean","string"],"required":true,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Native Commands","help":"Registers native slash/menu commands with channels that support command registration (Discord, Slack, Telegram). Keep enabled for discoverability unless you intentionally run text-only command workflows.","hasChildren":false}
{"recordType":"path","path":"commands.nativeSkills","kind":"core","type":["boolean","string"],"required":true,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Native Skill Commands","help":"Registers native skill commands so users can invoke skills directly from provider command menus where supported. Keep aligned with your skill policy so exposed commands match what operators expect.","hasChildren":false}
{"recordType":"path","path":"commands.ownerAllowFrom","kind":"core","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Command Owners","help":"Explicit owner allowlist for owner-only tools/commands. Use channel-native IDs (optionally prefixed like \"whatsapp:+15551234567\"). '*' is ignored.","hasChildren":true}
{"recordType":"path","path":"commands.ownerAllowFrom.*","kind":"core","type":["number","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"commands.ownerDisplay","kind":"core","type":"string","required":true,"enumValues":["raw","hash"],"defaultValue":"raw","deprecated":false,"sensitive":false,"tags":["access"],"label":"Owner ID Display","help":"Controls how owner IDs are rendered in the system prompt. Allowed values: raw, hash. Default: raw.","hasChildren":false}
{"recordType":"path","path":"commands.ownerDisplaySecret","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":true,"tags":["access","auth","security"],"label":"Owner ID Hash Secret","help":"Optional secret used to HMAC hash owner IDs when ownerDisplay=hash. Prefer env substitution.","hasChildren":false}
{"recordType":"path","path":"commands.plugins","kind":"core","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Allow /plugins","help":"Allow /plugins chat command to list discovered plugins and toggle plugin enablement in config (default: false).","hasChildren":false}
{"recordType":"path","path":"commands.restart","kind":"core","type":"boolean","required":true,"defaultValue":true,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Allow Restart","help":"Allow /restart and gateway restart tool actions (default: true).","hasChildren":false}
{"recordType":"path","path":"commands.text","kind":"core","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Text Commands","help":"Enables text-command parsing in chat input in addition to native command surfaces where available. Keep this enabled for compatibility across channels that do not support native command registration.","hasChildren":false}
{"recordType":"path","path":"commands.useAccessGroups","kind":"core","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Use Access Groups","help":"Enforce access-group allowlists/policies for commands.","hasChildren":false}
@ -3551,11 +3579,11 @@
{"recordType":"path","path":"gateway.http.endpoints.chatCompletions.images","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["media","network"],"label":"OpenAI Chat Completions Image Limits","help":"Image fetch/validation controls for OpenAI-compatible `image_url` parts.","hasChildren":true}
{"recordType":"path","path":"gateway.http.endpoints.chatCompletions.images.allowedMimes","kind":"core","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access","media","network"],"label":"OpenAI Chat Completions Image MIME Allowlist","help":"Allowed MIME types for `image_url` parts (case-insensitive list).","hasChildren":true}
{"recordType":"path","path":"gateway.http.endpoints.chatCompletions.images.allowedMimes.*","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"gateway.http.endpoints.chatCompletions.images.allowUrl","kind":"core","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access","media","network"],"label":"OpenAI Chat Completions Allow Image URLs","help":"Allow server-side URL fetches for `image_url` parts (default: false; data URIs remain supported).","hasChildren":false}
{"recordType":"path","path":"gateway.http.endpoints.chatCompletions.images.allowUrl","kind":"core","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access","media","network"],"label":"OpenAI Chat Completions Allow Image URLs","help":"Allow server-side URL fetches for `image_url` parts (default: false; data URIs remain supported). Set this to `false` to disable URL fetching entirely.","hasChildren":false}
{"recordType":"path","path":"gateway.http.endpoints.chatCompletions.images.maxBytes","kind":"core","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":["media","network","performance"],"label":"OpenAI Chat Completions Image Max Bytes","help":"Max bytes per fetched/decoded `image_url` image (default: 10MB).","hasChildren":false}
{"recordType":"path","path":"gateway.http.endpoints.chatCompletions.images.maxRedirects","kind":"core","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":["media","network","performance","storage"],"label":"OpenAI Chat Completions Image Max Redirects","help":"Max HTTP redirects allowed when fetching `image_url` URLs (default: 3).","hasChildren":false}
{"recordType":"path","path":"gateway.http.endpoints.chatCompletions.images.timeoutMs","kind":"core","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":["media","network","performance"],"label":"OpenAI Chat Completions Image Timeout (ms)","help":"Timeout in milliseconds for `image_url` URL fetches (default: 10000).","hasChildren":false}
{"recordType":"path","path":"gateway.http.endpoints.chatCompletions.images.urlAllowlist","kind":"core","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access","media","network"],"label":"OpenAI Chat Completions Image URL Allowlist","help":"Optional hostname allowlist for `image_url` URL fetches; supports exact hosts and `*.example.com` wildcards.","hasChildren":true}
{"recordType":"path","path":"gateway.http.endpoints.chatCompletions.images.urlAllowlist","kind":"core","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access","media","network"],"label":"OpenAI Chat Completions Image URL Allowlist","help":"Optional hostname allowlist for `image_url` URL fetches; supports exact hosts and `*.example.com` wildcards. Empty or omitted lists mean no hostname allowlist restriction.","hasChildren":true}
{"recordType":"path","path":"gateway.http.endpoints.chatCompletions.images.urlAllowlist.*","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"gateway.http.endpoints.chatCompletions.maxBodyBytes","kind":"core","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":["network","performance"],"label":"OpenAI Chat Completions Max Body Bytes","help":"Max request body size in bytes for `/v1/chat/completions` (default: 20MB).","hasChildren":false}
{"recordType":"path","path":"gateway.http.endpoints.chatCompletions.maxImageParts","kind":"core","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":["media","network","performance"],"label":"OpenAI Chat Completions Max Image Parts","help":"Max number of `image_url` parts accepted from the latest user message (default: 8).","hasChildren":false}
@ -3737,6 +3765,18 @@
{"recordType":"path","path":"logging.redactPatterns","kind":"core","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["observability","privacy"],"label":"Custom Redaction Patterns","help":"Additional custom redact regex patterns applied to log output before emission/storage. Use this to mask org-specific tokens and identifiers not covered by built-in redaction rules.","hasChildren":true}
{"recordType":"path","path":"logging.redactPatterns.*","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"logging.redactSensitive","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":["observability","privacy"],"label":"Sensitive Data Redaction Mode","help":"Sensitive redaction mode: \"off\" disables built-in masking, while \"tools\" redacts sensitive tool/config payload fields. Keep \"tools\" in shared logs unless you have isolated secure log sinks.","hasChildren":false}
{"recordType":"path","path":"mcp","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"MCP","help":"Global MCP server definitions managed by OpenClaw. Embedded Pi and other runtime adapters can consume these servers without storing them inside Pi-owned project settings.","hasChildren":true}
{"recordType":"path","path":"mcp.servers","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"MCP Servers","help":"Named MCP server definitions. OpenClaw stores them in its own config and runtime adapters decide which transports are supported at execution time.","hasChildren":true}
{"recordType":"path","path":"mcp.servers.*","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"mcp.servers.*.*","kind":"core","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"mcp.servers.*.args","kind":"core","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"mcp.servers.*.args.*","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"mcp.servers.*.command","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"mcp.servers.*.cwd","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"mcp.servers.*.env","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"mcp.servers.*.env.*","kind":"core","type":["boolean","number","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"mcp.servers.*.url","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"mcp.servers.*.workingDirectory","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"media","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Media","help":"Top-level media behavior shared across providers and tools that handle inbound files. Keep defaults unless you need stable filenames for external processing pipelines or longer-lived inbound media retention.","hasChildren":true}
{"recordType":"path","path":"media.preserveFilenames","kind":"core","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["storage"],"label":"Preserve Media Filenames","help":"When enabled, uploaded media keeps its original filename instead of a generated temp-safe name. Turn this on when downstream automations depend on stable names, and leave off to reduce accidental filename leakage.","hasChildren":false}
{"recordType":"path","path":"media.ttlHours","kind":"core","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Media Retention TTL (hours)","help":"Optional retention window in hours for persisted inbound media cleanup across the full media tree. Leave unset to preserve legacy behavior, or set values like 24 (1 day) or 168 (7 days) when you want automatic cleanup.","hasChildren":false}
@ -3867,6 +3907,17 @@
{"recordType":"path","path":"messages.tts.elevenlabs.voiceSettings.useSpeakerBoost","kind":"core","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"messages.tts.enabled","kind":"core","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"messages.tts.maxTextLength","kind":"core","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"messages.tts.microsoft","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"messages.tts.microsoft.enabled","kind":"core","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"messages.tts.microsoft.lang","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"messages.tts.microsoft.outputFormat","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"messages.tts.microsoft.pitch","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"messages.tts.microsoft.proxy","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"messages.tts.microsoft.rate","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"messages.tts.microsoft.saveSubtitles","kind":"core","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"messages.tts.microsoft.timeoutMs","kind":"core","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"messages.tts.microsoft.voice","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"messages.tts.microsoft.volume","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"messages.tts.mode","kind":"core","type":"string","required":false,"enumValues":["final","all"],"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"messages.tts.modelOverrides","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"messages.tts.modelOverrides.allowModelId","kind":"core","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
@ -3888,7 +3939,7 @@
{"recordType":"path","path":"messages.tts.openai.speed","kind":"core","type":"number","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"messages.tts.openai.voice","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"messages.tts.prefsPath","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"messages.tts.provider","kind":"core","type":"string","required":false,"enumValues":["elevenlabs","openai","edge"],"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"messages.tts.provider","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"messages.tts.summaryModel","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"messages.tts.timeoutMs","kind":"core","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"meta","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Metadata","help":"Metadata fields automatically maintained by OpenClaw to record write/version history for this config file. Keep these values system-managed and avoid manual edits unless debugging migration history.","hasChildren":true}
@ -4067,6 +4118,11 @@
{"recordType":"path","path":"plugins.entries.discord.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/discord","hasChildren":false}
{"recordType":"path","path":"plugins.entries.discord.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.discord.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.elevenlabs","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/elevenlabs-speech","help":"OpenClaw ElevenLabs speech plugin (plugin: elevenlabs)","hasChildren":true}
{"recordType":"path","path":"plugins.entries.elevenlabs.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/elevenlabs-speech Config","help":"Plugin-defined config payload for elevenlabs.","hasChildren":false}
{"recordType":"path","path":"plugins.entries.elevenlabs.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/elevenlabs-speech","hasChildren":false}
{"recordType":"path","path":"plugins.entries.elevenlabs.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.elevenlabs.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.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}
@ -4112,11 +4168,11 @@
{"recordType":"path","path":"plugins.entries.kilocode.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/kilocode-provider","hasChildren":false}
{"recordType":"path","path":"plugins.entries.kilocode.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.kilocode.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.kimi-coding","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/kimi-coding-provider","help":"OpenClaw Kimi Coding provider plugin (plugin: kimi-coding)","hasChildren":true}
{"recordType":"path","path":"plugins.entries.kimi-coding.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/kimi-coding-provider Config","help":"Plugin-defined config payload for kimi-coding.","hasChildren":false}
{"recordType":"path","path":"plugins.entries.kimi-coding.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/kimi-coding-provider","hasChildren":false}
{"recordType":"path","path":"plugins.entries.kimi-coding.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.kimi-coding.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.kimi","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/kimi-provider","help":"OpenClaw Kimi provider plugin (plugin: kimi)","hasChildren":true}
{"recordType":"path","path":"plugins.entries.kimi.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/kimi-provider Config","help":"Plugin-defined config payload for kimi.","hasChildren":false}
{"recordType":"path","path":"plugins.entries.kimi.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/kimi-provider","hasChildren":false}
{"recordType":"path","path":"plugins.entries.kimi.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.kimi.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.line","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/line","help":"OpenClaw LINE channel plugin (plugin: line)","hasChildren":true}
{"recordType":"path","path":"plugins.entries.line.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/line Config","help":"Plugin-defined config payload for line.","hasChildren":false}
{"recordType":"path","path":"plugins.entries.line.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/line","hasChildren":false}
@ -4168,6 +4224,11 @@
{"recordType":"path","path":"plugins.entries.memory-lancedb.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["storage"],"label":"Enable @openclaw/memory-lancedb","hasChildren":false}
{"recordType":"path","path":"plugins.entries.memory-lancedb.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.memory-lancedb.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.microsoft","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/microsoft-speech","help":"OpenClaw Microsoft speech plugin (plugin: microsoft)","hasChildren":true}
{"recordType":"path","path":"plugins.entries.microsoft.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/microsoft-speech Config","help":"Plugin-defined config payload for microsoft.","hasChildren":false}
{"recordType":"path","path":"plugins.entries.microsoft.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/microsoft-speech","hasChildren":false}
{"recordType":"path","path":"plugins.entries.microsoft.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.microsoft.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.minimax","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["performance"],"label":"@openclaw/minimax-provider","help":"OpenClaw MiniMax provider and OAuth plugin (plugin: minimax)","hasChildren":true}
{"recordType":"path","path":"plugins.entries.minimax.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["performance"],"label":"@openclaw/minimax-provider Config","help":"Plugin-defined config payload for minimax.","hasChildren":false}
{"recordType":"path","path":"plugins.entries.minimax.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["performance"],"label":"Enable @openclaw/minimax-provider","hasChildren":false}
@ -4449,7 +4510,7 @@
{"recordType":"path","path":"plugins.entries.voice-call.config.tts.openai.speed","kind":"plugin","type":"number","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"plugins.entries.voice-call.config.tts.openai.voice","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":["advanced","media"],"label":"OpenAI TTS Voice","hasChildren":false}
{"recordType":"path","path":"plugins.entries.voice-call.config.tts.prefsPath","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"plugins.entries.voice-call.config.tts.provider","kind":"plugin","type":"string","required":false,"enumValues":["openai","elevenlabs","edge"],"deprecated":false,"sensitive":false,"tags":["advanced","media"],"label":"TTS Provider Override","help":"Deep-merges with messages.tts (Edge is ignored for calls).","hasChildren":false}
{"recordType":"path","path":"plugins.entries.voice-call.config.tts.provider","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":["advanced","media"],"label":"TTS Provider Override","help":"Deep-merges with messages.tts (Microsoft is ignored for calls).","hasChildren":false}
{"recordType":"path","path":"plugins.entries.voice-call.config.tts.summaryModel","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"plugins.entries.voice-call.config.tts.timeoutMs","kind":"plugin","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"plugins.entries.voice-call.config.tunnel","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}

View File

@ -47,6 +47,10 @@
"source": "Quick Start",
"target": "快速开始"
},
{
"source": "Capability Cookbook",
"target": "能力扩展手册"
},
{
"source": "Setup Wizard Reference",
"target": "设置向导参考"

View File

@ -168,7 +168,7 @@ openclaw pairing approve discord <CODE>
<Note>
Token resolution is account-aware. Config token values win over env fallback. `DISCORD_BOT_TOKEN` is only used for the default account.
For advanced outbound calls (message tool/channel actions), an explicit per-call `token` is used for that call. Account policy/retry settings still come from the selected account in the active runtime snapshot.
For advanced outbound calls (message tool/channel actions), an explicit per-call `token` is used for that call. This applies to send and read/probe-style actions (for example read/search/fetch/thread/pins/permissions). Account policy/retry settings still come from the selected account in the active runtime snapshot.
</Note>
## Recommended: Set up a guild workspace

View File

@ -32,6 +32,8 @@ Notes:
- Doctor includes a memory-search readiness check and can recommend `openclaw configure --section model` when embedding credentials are missing.
- If sandbox mode is enabled but Docker is unavailable, doctor reports a high-signal warning with remediation (`install Docker` or `openclaw config set agents.defaults.sandbox.mode off`).
- If `gateway.auth.token`/`gateway.auth.password` are SecretRef-managed and unavailable in the current command path, doctor reports a read-only warning and does not write plaintext fallback credentials.
- If channel SecretRef inspection fails in a fix path, doctor continues and reports a warning instead of exiting early.
- Telegram `allowFrom` username auto-resolution (`doctor --fix`) requires a resolvable Telegram token in the current command path. If token inspection is unavailable, doctor reports a warning and skips auto-resolution for that pass.
## macOS: `launchctl` env overrides

View File

@ -50,6 +50,16 @@ Name lookup:
- `--dry-run`
- `--verbose`
## SecretRef behavior
- `openclaw message` resolves supported channel SecretRefs before running the selected action.
- Resolution is scoped to the active action target when possible:
- channel-scoped when `--channel` is set (or inferred from prefixed targets like `discord:...`)
- account-scoped when `--account` is set (channel globals + selected account surfaces)
- when `--account` is omitted, OpenClaw does not force a `default` account SecretRef scope
- Unresolved SecretRefs on unrelated channels do not block a targeted message action.
- If the selected channel/account SecretRef is unresolved, the command fails closed for that action.
## Actions
### Core

View File

@ -27,3 +27,4 @@ Notes:
- Read-only status surfaces (`status`, `status --json`, `status --all`) resolve supported SecretRefs for their targeted config paths when possible.
- If a supported channel SecretRef is configured but unavailable in the current command path, status stays read-only and reports degraded output instead of crashing. Human output shows warnings such as “configured token unavailable in this command path”, and JSON output includes `secretDiagnostics`.
- When command-local SecretRef resolution succeeds, status prefers the resolved snapshot and clears transient “secret unavailable” channel markers from the final output.
- `status --all` includes a Secrets overview row and a diagnosis section that summarizes secret diagnostics (truncated for readability) without stopping report generation.

View File

@ -97,6 +97,17 @@ compaction and can run alongside it.
See [OpenAI provider](/providers/openai) for model params and overrides.
## Custom context engines
Compaction behavior is owned by the active
[context engine](/concepts/context-engine). The legacy engine uses the built-in
summarization described above. Plugin engines (selected via
`plugins.slots.contextEngine`) can implement any compaction strategy — DAG
summaries, vector retrieval, incremental condensation, etc.
When a plugin engine sets `ownsCompaction: true`, OpenClaw delegates all
compaction decisions to the engine and does not run built-in auto-compaction.
## Tips
- Use `/compact` when sessions feel stale or context is bloated.

View File

@ -0,0 +1,250 @@
---
summary: "Context engine: pluggable context assembly, compaction, and subagent lifecycle"
read_when:
- You want to understand how OpenClaw assembles model context
- You are switching between the legacy engine and a plugin engine
- You are building a context engine plugin
title: "Context Engine"
---
# Context Engine
A **context engine** controls how OpenClaw builds model context for each run.
It decides which messages to include, how to summarize older history, and how
to manage context across subagent boundaries.
OpenClaw ships with a built-in `legacy` engine. Plugins can register
alternative engines that replace the entire context pipeline.
## Quick start
Check which engine is active:
```bash
openclaw doctor
# or inspect config directly:
cat ~/.openclaw/openclaw.json | jq '.plugins.slots.contextEngine'
```
### Installing a context engine plugin
Context engine plugins are installed like any other OpenClaw plugin. Install
first, then select the engine in the slot:
```bash
# Install from npm
openclaw plugins install @martian-engineering/lossless-claw
# Or install from a local path (for development)
openclaw plugins install -l ./my-context-engine
```
Then enable the plugin and select it as the active engine in your config:
```json5
// openclaw.json
{
plugins: {
slots: {
contextEngine: "lossless-claw", // must match the plugin's registered engine id
},
entries: {
"lossless-claw": {
enabled: true,
// Plugin-specific config goes here (see the plugin's docs)
},
},
},
}
```
Restart the gateway after installing and configuring.
To switch back to the built-in engine, set `contextEngine` to `"legacy"` (or
remove the key entirely — `"legacy"` is the default).
## How it works
Every time OpenClaw runs a model prompt, the context engine participates at
four lifecycle points:
1. **Ingest** — called when a new message is added to the session. The engine
can store or index the message in its own data store.
2. **Assemble** — called before each model run. The engine returns an ordered
set of messages (and an optional `systemPromptAddition`) that fit within
the token budget.
3. **Compact** — called when the context window is full, or when the user runs
`/compact`. The engine summarizes older history to free space.
4. **After turn** — called after a run completes. The engine can persist state,
trigger background compaction, or update indexes.
### Subagent lifecycle (optional)
OpenClaw currently calls one subagent lifecycle hook:
- **onSubagentEnded** — clean up when a subagent session completes or is swept.
The `prepareSubagentSpawn` hook is part of the interface for future use, but
the runtime does not invoke it yet.
### System prompt addition
The `assemble` method can return a `systemPromptAddition` string. OpenClaw
prepends this to the system prompt for the run. This lets engines inject
dynamic recall guidance, retrieval instructions, or context-aware hints
without requiring static workspace files.
## The legacy engine
The built-in `legacy` engine preserves OpenClaw's original behavior:
- **Ingest**: no-op (the session manager handles message persistence directly).
- **Assemble**: pass-through (the existing sanitize → validate → limit pipeline
in the runtime handles context assembly).
- **Compact**: delegates to the built-in summarization compaction, which creates
a single summary of older messages and keeps recent messages intact.
- **After turn**: no-op.
The legacy engine does not register tools or provide a `systemPromptAddition`.
When no `plugins.slots.contextEngine` is set (or it's set to `"legacy"`), this
engine is used automatically.
## Plugin engines
A plugin can register a context engine using the plugin API:
```ts
export default function register(api) {
api.registerContextEngine("my-engine", () => ({
info: {
id: "my-engine",
name: "My Context Engine",
ownsCompaction: true,
},
async ingest({ sessionId, message, isHeartbeat }) {
// Store the message in your data store
return { ingested: true };
},
async assemble({ sessionId, messages, tokenBudget }) {
// Return messages that fit the budget
return {
messages: buildContext(messages, tokenBudget),
estimatedTokens: countTokens(messages),
systemPromptAddition: "Use lcm_grep to search history...",
};
},
async compact({ sessionId, force }) {
// Summarize older context
return { ok: true, compacted: true };
},
}));
}
```
Then enable it in config:
```json5
{
plugins: {
slots: {
contextEngine: "my-engine",
},
entries: {
"my-engine": {
enabled: true,
},
},
},
}
```
### The ContextEngine interface
Required members:
| Member | Kind | Purpose |
| ------------------ | -------- | -------------------------------------------------------- |
| `info` | Property | Engine id, name, version, and whether it owns compaction |
| `ingest(params)` | Method | Store a single message |
| `assemble(params)` | Method | Build context for a model run (returns `AssembleResult`) |
| `compact(params)` | Method | Summarize/reduce context |
`assemble` returns an `AssembleResult` with:
- `messages` — the ordered messages to send to the model.
- `estimatedTokens` (required, `number`) — the engine's estimate of total
tokens in the assembled context. OpenClaw uses this for compaction threshold
decisions and diagnostic reporting.
- `systemPromptAddition` (optional, `string`) — prepended to the system prompt.
Optional members:
| Member | Kind | Purpose |
| ------------------------------ | ------ | --------------------------------------------------------------------------------------------------------------- |
| `bootstrap(params)` | Method | Initialize engine state for a session. Called once when the engine first sees a session (e.g., import history). |
| `ingestBatch(params)` | Method | Ingest a completed turn as a batch. Called after a run completes, with all messages from that turn at once. |
| `afterTurn(params)` | Method | Post-run lifecycle work (persist state, trigger background compaction). |
| `prepareSubagentSpawn(params)` | Method | Set up shared state for a child session. |
| `onSubagentEnded(params)` | Method | Clean up after a subagent ends. |
| `dispose()` | Method | Release resources. Called during gateway shutdown or plugin reload — not per-session. |
### ownsCompaction
When `info.ownsCompaction` is `true`, the engine manages its own compaction
lifecycle. OpenClaw will not trigger the built-in auto-compaction; instead it
delegates entirely to the engine's `compact()` method. The engine may also
run compaction proactively in `afterTurn()`.
When `false` or unset, OpenClaw's built-in auto-compaction logic runs
alongside the engine.
## Configuration reference
```json5
{
plugins: {
slots: {
// Select the active context engine. Default: "legacy".
// Set to a plugin id to use a plugin engine.
contextEngine: "legacy",
},
},
}
```
The slot is exclusive at run time — only one registered context engine is
resolved for a given run or compaction operation. Other enabled
`kind: "context-engine"` plugins can still load and run their registration
code; `plugins.slots.contextEngine` only selects which registered engine id
OpenClaw resolves when it needs a context engine.
## Relationship to compaction and memory
- **Compaction** is one responsibility of the context engine. The legacy engine
delegates to OpenClaw's built-in summarization. Plugin engines can implement
any compaction strategy (DAG summaries, vector retrieval, etc.).
- **Memory plugins** (`plugins.slots.memory`) are separate from context engines.
Memory plugins provide search/retrieval; context engines control what the
model sees. They can work together — a context engine might use memory
plugin data during assembly.
- **Session pruning** (trimming old tool results in-memory) still runs
regardless of which context engine is active.
## Tips
- Use `openclaw doctor` to verify your engine is loading correctly.
- If switching engines, existing sessions continue with their current history.
The new engine takes over for future runs.
- Engine errors are logged and surfaced in diagnostics. If a plugin engine
fails to register or the selected engine id cannot be resolved, OpenClaw
does not fall back automatically; runs fail until you fix the plugin or
switch `plugins.slots.contextEngine` back to `"legacy"`.
- For development, use `openclaw plugins install -l ./my-engine` to link a
local plugin directory without copying.
See also: [Compaction](/concepts/compaction), [Context](/concepts/context),
[Plugins](/tools/plugin), [Plugin manifest](/plugins/manifest).

View File

@ -157,7 +157,8 @@ By default, OpenClaw uses the built-in `legacy` context engine for assembly and
compaction. If you install a plugin that provides `kind: "context-engine"` and
select it with `plugins.slots.contextEngine`, OpenClaw delegates context
assembly, `/compact`, and related subagent context lifecycle hooks to that
engine instead.
engine instead. See [Context Engine](/concepts/context-engine) for the full
pluggable interface, lifecycle hooks, and configuration.
## What `/context` actually reports

View File

@ -26,6 +26,7 @@ Related:
- `agents.defaults.models` is the allowlist/catalog of models OpenClaw can use (plus aliases).
- `agents.defaults.imageModel` is used **only when** the primary model cant accept images.
- `agents.defaults.imageGenerationModel` is used by the shared image-generation capability.
- Per-agent defaults can override `agents.defaults.model` via `agents.list[].model` plus bindings (see [/concepts/multi-agent](/concepts/multi-agent)).
## Quick model policy
@ -49,6 +50,7 @@ subscription** (OAuth) and **Anthropic** (API key or `claude setup-token`).
- `agents.defaults.model.primary` and `agents.defaults.model.fallbacks`
- `agents.defaults.imageModel.primary` and `agents.defaults.imageModel.fallbacks`
- `agents.defaults.imageGenerationModel.primary` and `agents.defaults.imageGenerationModel.fallbacks`
- `agents.defaults.models` (allowlist + aliases + provider params)
- `models.providers` (custom providers written into `models.json`)

View File

@ -59,6 +59,10 @@
"source": "/compaction",
"destination": "/concepts/compaction"
},
{
"source": "/context-engine",
"destination": "/concepts/context-engine"
},
{
"source": "/cron",
"destination": "/cron-jobs"
@ -952,6 +956,7 @@
"concepts/agent-loop",
"concepts/system-prompt",
"concepts/context",
"concepts/context-engine",
"concepts/agent-workspace",
"concepts/oauth"
]

View File

@ -434,7 +434,7 @@ Save to `~/.openclaw/openclaw.json` and you can DM the bot from that number.
nodeManager: "npm",
},
entries: {
"nano-banana-pro": {
"image-lab": {
enabled: true,
apiKey: "GEMINI_KEY_HERE",
env: { GEMINI_API_KEY: "GEMINI_KEY_HERE" },

View File

@ -875,6 +875,10 @@ Time format in system prompt. Default: `auto` (OS preference).
primary: "openrouter/qwen/qwen-2.5-vl-72b-instruct:free",
fallbacks: ["openrouter/google/gemini-2.0-flash-vision:free"],
},
imageGenerationModel: {
primary: "openai/gpt-image-1",
fallbacks: ["google/gemini-3.1-flash-image-preview"],
},
pdfModel: {
primary: "anthropic/claude-opus-4-6",
fallbacks: ["openai/gpt-5-mini"],
@ -899,6 +903,8 @@ Time format in system prompt. Default: `auto` (OS preference).
- `imageModel`: accepts either a string (`"provider/model"`) or an object (`{ primary, fallbacks }`).
- Used by the `image` tool path as its vision-model config.
- Also used as fallback routing when the selected/default model cannot accept image input.
- `imageGenerationModel`: accepts either a string (`"provider/model"`) or an object (`{ primary, fallbacks }`).
- Used by the shared image-generation capability and any future tool/plugin surface that generates images.
- `pdfModel`: accepts either a string (`"provider/model"`) or an object (`{ primary, fallbacks }`).
- Used by the `pdf` tool for model routing.
- If omitted, the PDF tool falls back to `imageModel`, then to best-effort provider defaults.
@ -2365,7 +2371,7 @@ See [Local Models](/gateway/local-models). TL;DR: run MiniMax M2.5 via LM Studio
nodeManager: "npm", // npm | pnpm | yarn
},
entries: {
"nano-banana-pro": {
"image-lab": {
apiKey: { source: "env", provider: "default", id: "GEMINI_API_KEY" }, // or plaintext string
env: { GEMINI_API_KEY: "GEMINI_KEY_HERE" },
},
@ -2606,6 +2612,8 @@ See [Plugins](/tools/plugin).
- `gateway.http.endpoints.responses.maxUrlParts`
- `gateway.http.endpoints.responses.files.urlAllowlist`
- `gateway.http.endpoints.responses.images.urlAllowlist`
Empty allowlists are treated as unset; use `gateway.http.endpoints.responses.files.allowUrl=false`
and/or `gateway.http.endpoints.responses.images.allowUrl=false` to disable URL fetching.
- Optional response hardening header:
- `gateway.http.securityHeaders.strictTransportSecurity` (set only for HTTPS origins you control; see [Trusted Proxy Auth](/gateway/trusted-proxy-auth#tls-termination-and-hsts))

View File

@ -597,11 +597,11 @@ Rules:
},
skills: {
entries: {
"nano-banana-pro": {
"image-lab": {
apiKey: {
source: "file",
provider: "filemain",
id: "/skills/entries/nano-banana-pro/apiKey",
id: "/skills/entries/image-lab/apiKey",
},
},
},

View File

@ -144,6 +144,8 @@ URL fetch defaults:
- Optional hostname allowlists are supported per input type (`files.urlAllowlist`, `images.urlAllowlist`).
- Exact host: `"cdn.example.com"`
- Wildcard subdomains: `"*.assets.example.com"` (does not match apex)
- Empty or omitted allowlists mean no hostname allowlist restriction.
- To disable URL-based fetches entirely, set `files.allowUrl: false` and/or `images.allowUrl: false`.
## File + image limits (config)

View File

@ -568,6 +568,8 @@ tool calls. Reduce the blast radius by:
- For OpenResponses URL inputs (`input_file` / `input_image`), set tight
`gateway.http.endpoints.responses.files.urlAllowlist` and
`gateway.http.endpoints.responses.images.urlAllowlist`, and keep `maxUrlParts` low.
Empty allowlists are treated as unset; use `files.allowUrl: false` / `images.allowUrl: false`
if you want to disable URL fetching entirely.
- Enabling sandboxing and strict tool allowlists for any agent that touches untrusted input.
- Keeping secrets out of prompts; pass them via env/config on the gateway host instead.

View File

@ -360,6 +360,30 @@ If you want to rely on env keys (e.g. exported in your `~/.profile`), run local
- Enable: `BYTEPLUS_API_KEY=... BYTEPLUS_LIVE_TEST=1 pnpm test:live src/agents/byteplus.live.test.ts`
- Optional model override: `BYTEPLUS_CODING_MODEL=ark-code-latest`
## Image generation live
- Test: `src/image-generation/runtime.live.test.ts`
- Command: `pnpm test:live src/image-generation/runtime.live.test.ts`
- Scope:
- Enumerates every registered image-generation provider plugin
- Loads missing provider env vars from your login shell (`~/.profile`) before probing
- Uses live/env API keys ahead of stored auth profiles by default, so stale test keys in `auth-profiles.json` do not mask real shell credentials
- Skips providers with no usable auth/profile/model
- Runs the stock image-generation variants through the shared runtime capability:
- `google:flash-generate`
- `google:pro-generate`
- `google:pro-edit`
- `openai:default-generate`
- Current bundled providers covered:
- `openai`
- `google`
- Optional narrowing:
- `OPENCLAW_LIVE_IMAGE_GENERATION_PROVIDERS="openai,google"`
- `OPENCLAW_LIVE_IMAGE_GENERATION_MODELS="openai/gpt-image-1,google/gemini-3.1-flash-image-preview"`
- `OPENCLAW_LIVE_IMAGE_GENERATION_CASES="google:flash-generate,google:pro-edit"`
- Optional auth behavior:
- `OPENCLAW_LIVE_REQUIRE_PROFILE_KEYS=1` to force profile-store auth and ignore env-only overrides
## Docker runners (optional “works in Linux” checks)
These run `pnpm test:live` inside the repo Docker image, mounting your local config dir and workspace (and sourcing `~/.profile` if mounted). They also bind-mount CLI auth homes like `~/.codex`, `~/.claude`, `~/.qwen`, and `~/.minimax` when present, then copy them into the container home before the run so external-CLI OAuth can refresh tokens without mutating the host auth store:

View File

@ -104,11 +104,15 @@ loader. Cursor command markdown works through the same path.
- `HOOK.md`
- `handler.ts` or `handler.js`
#### MCP for CLI backends
#### MCP for Pi
- enabled bundles can contribute MCP server config
- current runtime wiring is used by the `claude-cli` backend
- OpenClaw merges bundle MCP config into the backend `--mcp-config` file
- OpenClaw merges bundle MCP config into the effective embedded Pi settings as
`mcpServers`
- OpenClaw also exposes supported bundle MCP tools during embedded Pi agent
turns by launching supported stdio MCP servers as subprocesses
- project-local Pi settings still apply after bundle defaults, so workspace
settings can override bundle MCP entries when needed
#### Embedded Pi settings
@ -133,7 +137,6 @@ diagnostics/info output, but OpenClaw does not run them yet:
- Cursor `.cursor/agents`
- Cursor `.cursor/hooks.json`
- Cursor `.cursor/rules`
- Cursor `mcpServers` outside the current mapped runtime paths
- Codex inline/app metadata beyond capability reporting
## Capability reporting
@ -153,7 +156,8 @@ Current exceptions:
- Claude `commands` is considered supported because it maps to skills
- Claude `settings` is considered supported because it maps to embedded Pi settings
- Cursor `commands` is considered supported because it maps to skills
- bundle MCP is considered supported where OpenClaw actually imports it
- bundle MCP is considered supported because it maps into embedded Pi settings
and exposes supported stdio tools to embedded Pi
- Codex `hooks` is considered supported only for OpenClaw hook-pack layouts
## Format differences
@ -195,6 +199,8 @@ Claude-specific notes:
- `commands/` is treated like skill content
- `settings.json` is imported into embedded Pi settings
- `.mcp.json` and manifest `mcpServers` can expose supported stdio tools to
embedded Pi
- `hooks/hooks.json` is detected, but not executed as Claude automation
### Cursor
@ -246,7 +252,9 @@ Current behavior:
- bundle discovery reads files inside the plugin root with boundary checks
- skills and hook-pack paths must stay inside the plugin root
- bundle settings files are read with the same boundary checks
- OpenClaw does not execute arbitrary bundle runtime code in-process
- supported stdio bundle MCP servers may be launched as subprocesses for
embedded Pi tool calls
- OpenClaw does not load arbitrary bundle runtime modules in-process
This makes bundle support safer by default than native plugin modules, but you
should still treat third-party bundles as trusted content for the features they

View File

@ -0,0 +1,112 @@
---
summary: "Cookbook for adding a new shared capability to OpenClaw"
read_when:
- Adding a new core capability and plugin seam
- Deciding whether code belongs in core, a vendor plugin, or a feature plugin
- Wiring a new runtime helper for channels or tools
title: "Capability Cookbook"
---
# Capability Cookbook
Use this when OpenClaw needs a new domain such as image generation, video
generation, or some future vendor-backed feature area.
The rule:
- plugin = ownership boundary
- capability = shared core contract
That means you should not start by wiring a vendor directly into a channel or a
tool. Start by defining the capability.
## When to create a capability
Create a new capability when all of these are true:
1. more than one vendor could plausibly implement it
2. channels, tools, or feature plugins should consume it without caring about
the vendor
3. core needs to own fallback, policy, config, or delivery behavior
If the work is vendor-only and no shared contract exists yet, stop and define
the contract first.
## The standard sequence
1. Define the typed core contract.
2. Add plugin registration for that contract.
3. Add a shared runtime helper.
4. Wire one real vendor plugin as proof.
5. Move feature/channel consumers onto the runtime helper.
6. Add contract tests.
7. Document the operator-facing config and ownership model.
## What goes where
Core:
- request/response types
- provider registry + resolution
- fallback behavior
- config schema and labels/help
- runtime helper surface
Vendor plugin:
- vendor API calls
- vendor auth handling
- vendor-specific request normalization
- registration of the capability implementation
Feature/channel plugin:
- calls `api.runtime.*` or the matching `plugin-sdk/*-runtime` helper
- never calls a vendor implementation directly
## File checklist
For a new capability, expect to touch these areas:
- `src/<capability>/types.ts`
- `src/<capability>/...registry/runtime.ts`
- `src/plugins/types.ts`
- `src/plugins/registry.ts`
- `src/plugins/captured-registration.ts`
- `src/plugins/contracts/registry.ts`
- `src/plugins/runtime/types-core.ts`
- `src/plugins/runtime/index.ts`
- `src/plugin-sdk/<capability>.ts`
- `src/plugin-sdk/<capability>-runtime.ts`
- one or more `extensions/<vendor>/...`
- config/docs/tests
## Example: image generation
Image generation follows the standard shape:
1. core defines `ImageGenerationProvider`
2. core exposes `registerImageGenerationProvider(...)`
3. core exposes `runtime.imageGeneration.generate(...)`
4. the `openai` and `google` plugins register vendor-backed implementations
5. future vendors can register the same contract without changing channels/tools
The config key is separate from vision-analysis routing:
- `agents.defaults.imageModel` = analyze images
- `agents.defaults.imageGenerationModel` = generate images
Keep those separate so fallback and policy remain explicit.
## Review checklist
Before shipping a new capability, verify:
- no channel/tool imports vendor code directly
- the runtime helper is the shared path
- at least one contract test asserts bundled ownership
- config docs name the new model/config key
- plugin docs explain the ownership boundary
If a PR skips the capability layer and hardcodes vendor behavior into a
channel/tool, send it back and define the contract first.

View File

@ -400,6 +400,30 @@ Notes:
- Only available when `agents.defaults.imageModel` is configured (primary or fallbacks), or when an implicit image model can be inferred from your default model + configured auth (best-effort pairing).
- Uses the image model directly (independent of the main chat model).
### `image_generate`
Generate one or more images with the configured image-generation model.
Core parameters:
- `action` (optional: `generate` or `list`; default `generate`)
- `prompt` (required)
- `image` or `images` (optional reference image path/URL for edit mode)
- `model` (optional provider/model override)
- `size` (optional size hint)
- `resolution` (optional `1K|2K|4K` hint)
- `count` (optional, `1-4`, default `1`)
Notes:
- Only available when `agents.defaults.imageGenerationModel` is configured.
- Use `action: "list"` to inspect registered providers, default models, supported model ids, sizes, resolutions, and edit support.
- Returns local `MEDIA:<path>` lines so channels can deliver the generated files directly.
- Uses the image-generation model directly (independent of the main chat model).
- Google-backed flows support reference-image edits plus explicit `1K|2K|4K` resolution hints.
- When editing and `resolution` is omitted, OpenClaw infers a draft/final resolution from the input image size.
- This is the built-in replacement for the old sample `nano-banana-pro` skill workflow. Use `agents.defaults.imageGenerationModel`, not `skills.entries`, for stock image generation.
### `pdf`
Analyze one or more PDF documents.

View File

@ -113,11 +113,13 @@ That means:
Examples:
- the bundled `openai` plugin owns OpenAI model-provider behavior and OpenAI
speech + media-understanding behavior
speech + media-understanding + image-generation behavior
- the bundled `elevenlabs` plugin owns ElevenLabs speech behavior
- the bundled `microsoft` plugin owns Microsoft speech behavior
- the bundled `google`, `minimax`, `mistral`, `moonshot`, and `zai` plugins own
their media-understanding backends
- the bundled `google` plugin owns Google model-provider behavior plus Google
media-understanding + image-generation + web-search behavior
- the bundled `minimax`, `mistral`, `moonshot`, and `zai` plugins own their
media-understanding backends
- the `voice-call` plugin is a feature plugin: it owns call transport, tools,
CLI, routes, and runtime, but it consumes core TTS/STT capability instead of
inventing a second speech stack
@ -169,6 +171,76 @@ For example, TTS follows this shape:
That same pattern should be preferred for future capabilities.
### Multi-capability company plugin example
A company plugin should feel cohesive from the outside. If OpenClaw has shared
contracts for models, speech, media understanding, and web search, a vendor can
own all of its surfaces in one place:
```ts
import type { OpenClawPluginDefinition } from "openclaw/plugin-sdk";
import {
buildOpenAISpeechProvider,
createPluginBackedWebSearchProvider,
describeImageWithModel,
transcribeOpenAiCompatibleAudio,
} from "openclaw/plugin-sdk";
const plugin: OpenClawPluginDefinition = {
id: "exampleai",
name: "ExampleAI",
register(api) {
api.registerProvider({
id: "exampleai",
// auth/model catalog/runtime hooks
});
api.registerSpeechProvider(
buildOpenAISpeechProvider({
id: "exampleai",
// vendor speech config
}),
);
api.registerMediaUnderstandingProvider({
id: "exampleai",
capabilities: ["image", "audio", "video"],
async describeImage(req) {
return describeImageWithModel({
provider: "exampleai",
model: req.model,
input: req.input,
});
},
async transcribeAudio(req) {
return transcribeOpenAiCompatibleAudio({
provider: "exampleai",
model: req.model,
input: req.input,
});
},
});
api.registerWebSearchProvider(
createPluginBackedWebSearchProvider({
id: "exampleai-search",
// credential + fetch logic
}),
);
},
};
export default plugin;
```
What matters is not the exact helper names. The shape matters:
- one plugin owns the vendor surface
- core still owns the capability contracts
- channels and feature plugins consume `api.runtime.*` helpers, not vendor code
- contract tests can assert that the plugin registered the capabilities it
claims to own
### Capability example: video understanding
OpenClaw already treats image/audio/video understanding as one shared
@ -187,6 +259,9 @@ If OpenClaw adds a new domain later, such as video generation, use the same
sequence again: define the core capability first, then let vendor plugins
register implementations against it.
Need a concrete rollout checklist? See
[Capability Cookbook](/tools/capability-cookbook).
## Compatible bundles
OpenClaw also recognizes two compatible external bundle layouts:
@ -214,18 +289,23 @@ plugins:
OpenClaw skill loader
- supported now: Claude bundle `settings.json` defaults for embedded Pi agent
settings (with shell override keys sanitized)
- supported now: bundle MCP config, merged into embedded Pi agent settings as
`mcpServers`, with supported stdio bundle MCP tools exposed during embedded
Pi agent turns
- supported now: Cursor `.cursor/commands/*.md` roots, mapped into the normal
OpenClaw skill loader
- supported now: Codex bundle hook directories that use the OpenClaw hook-pack
layout (`HOOK.md` + `handler.ts`/`handler.js`)
- detected but not wired yet: other declared bundle capabilities such as
agents, Claude hook automation, Cursor rules/hooks/MCP metadata, MCP/app/LSP
agents, Claude hook automation, Cursor rules/hooks metadata, app/LSP
metadata, output styles
That means bundle install/discovery/list/info/enablement all work, and bundle
skills, Claude command-skills, Claude bundle settings defaults, and compatible
Codex hook directories load when the bundle is enabled, but bundle runtime code
is not executed in-process.
Codex hook directories load when the bundle is enabled. Supported bundle MCP
servers may also run as subprocesses for embedded Pi tool calls when they use
supported stdio transport, but bundle runtime modules are not loaded
in-process.
Bundle hook support is limited to the normal OpenClaw hook directory format
(`HOOK.md` plus `handler.ts`/`handler.js` under the declared hook roots).
@ -782,6 +862,32 @@ Notes:
- Returns `{ text: undefined }` when no transcription output is produced (for example skipped/unsupported input).
- `api.runtime.stt.transcribeAudioFile(...)` remains as a compatibility alias.
For web search, plugins can consume the shared runtime helper instead of
reaching into the agent tool wiring:
```ts
const providers = api.runtime.webSearch.listProviders({
config: api.config,
});
const result = await api.runtime.webSearch.search({
config: api.config,
args: {
query: "OpenClaw plugin runtime helpers",
count: 5,
},
});
```
Plugins can also register web-search providers via
`api.registerWebSearchProvider(...)`.
Notes:
- Keep provider selection, credential resolution, and shared request semantics in core.
- Use web-search providers for vendor-specific search transports.
- `api.runtime.webSearch.*` is the preferred shared surface for feature/channel plugins that need search behavior without depending on the agent tool wrapper.
## Gateway HTTP routes
Plugins can expose HTTP endpoints with `api.registerHttpRoute(...)`.
@ -819,8 +925,16 @@ Notes:
Use SDK subpaths instead of the monolithic `openclaw/plugin-sdk` import when
authoring plugins:
- `openclaw/plugin-sdk/core` for generic plugin APIs, provider auth types, and shared helpers such as routing/session utilities and logger-backed runtimes.
- `openclaw/plugin-sdk/compat` for bundled/internal plugin code that needs broader shared runtime helpers than `core`.
- `openclaw/plugin-sdk/core` for the smallest generic plugin-facing contract.
- Domain subpaths such as `openclaw/plugin-sdk/channel-config-helpers`,
`openclaw/plugin-sdk/channel-config-schema`,
`openclaw/plugin-sdk/channel-policy`,
`openclaw/plugin-sdk/reply-history`,
`openclaw/plugin-sdk/routing`,
`openclaw/plugin-sdk/runtime-store`, and
`openclaw/plugin-sdk/directory-runtime` for shared runtime/config helpers.
- `openclaw/plugin-sdk/compat` remains as a legacy migration surface for older
external plugins. Bundled plugins should not use it.
- `openclaw/plugin-sdk/telegram` for Telegram channel plugin types and shared channel-facing helpers. Built-in Telegram implementation internals stay private to the bundled extension.
- `openclaw/plugin-sdk/discord` for Discord channel plugin types and shared channel-facing helpers. Built-in Discord implementation internals stay private to the bundled extension.
- `openclaw/plugin-sdk/slack` for Slack channel plugin types and shared channel-facing helpers. Built-in Slack implementation internals stay private to the bundled extension.
@ -881,8 +995,8 @@ Compatibility note:
- `openclaw/plugin-sdk` remains supported for existing external plugins.
- New and migrated bundled plugins should use channel or extension-specific
subpaths; use `core` for generic surfaces and `compat` only when broader
shared helpers are required.
subpaths; use `core` plus explicit domain subpaths for generic surfaces, and
treat `compat` as migration-only.
## Read-only channel inspection
@ -1386,6 +1500,65 @@ Recommended sequence:
This is how OpenClaw stays opinionated without becoming hardcoded to one
provider's worldview.
### Capability checklist
When you add a new capability, the implementation should usually touch these
surfaces together:
- core contract types in `src/<capability>/types.ts`
- core runner/runtime helper in `src/<capability>/runtime.ts`
- plugin API registration surface in `src/plugins/types.ts`
- plugin registry wiring in `src/plugins/registry.ts`
- plugin runtime exposure in `src/plugins/runtime/*` when feature/channel
plugins need to consume it
- capture/test helpers in `src/test-utils/plugin-registration.ts`
- ownership/contract assertions in `src/plugins/contracts/registry.ts`
- operator/plugin docs in `docs/`
If one of those surfaces is missing, that is usually a sign the capability is
not fully integrated yet.
### Capability template
Minimal pattern:
```ts
// core contract
export type VideoGenerationProviderPlugin = {
id: string;
label: string;
generateVideo: (req: VideoGenerationRequest) => Promise<VideoGenerationResult>;
};
// plugin API
api.registerVideoGenerationProvider({
id: "openai",
label: "OpenAI",
async generateVideo(req) {
return await generateOpenAiVideo(req);
},
});
// shared runtime helper for feature/channel plugins
const clip = await api.runtime.videoGeneration.generateFile({
prompt: "Show the robot walking through the lab.",
cfg,
});
```
Contract test pattern:
```ts
expect(findVideoGenerationProviderIdsForPlugin("openai")).toEqual(["openai"]);
```
That keeps the rule simple:
- core owns the capability contract + orchestration
- vendor plugins own vendor implementations
- feature/channel plugins consume runtime helpers
- contract tests keep ownership explicit
Context engine plugins can also register a runtime-owned context manager:
```ts

View File

@ -24,7 +24,7 @@ All skills-related configuration lives under `skills` in `~/.openclaw/openclaw.j
nodeManager: "npm", // npm | pnpm | yarn | bun (Gateway runtime still Node; bun not recommended)
},
entries: {
"nano-banana-pro": {
"image-lab": {
enabled: true,
apiKey: { source: "env", provider: "default", id: "GEMINI_API_KEY" }, // or plaintext string
env: {
@ -38,6 +38,10 @@ All skills-related configuration lives under `skills` in `~/.openclaw/openclaw.j
}
```
For built-in image generation/editing, prefer `agents.defaults.imageGenerationModel`
plus the core `image_generate` tool. `skills.entries.*` is only for custom or
third-party skill workflows.
## Fields
- `allowBundled`: optional allowlist for **bundled** skills only. When set, only

View File

@ -81,8 +81,8 @@ that up as `<workspace>/skills` on the next session.
```markdown
---
name: nano-banana-pro
description: Generate or edit images via Gemini 3 Pro Image
name: image-lab
description: Generate or edit images via a provider-backed image workflow
---
```
@ -109,8 +109,8 @@ OpenClaw **filters skills at load time** using `metadata` (single-line JSON):
```markdown
---
name: nano-banana-pro
description: Generate or edit images via Gemini 3 Pro Image
name: image-lab
description: Generate or edit images via a provider-backed image workflow
metadata:
{
"openclaw":
@ -194,7 +194,7 @@ Bundled/managed skills can be toggled and supplied with env values:
{
skills: {
entries: {
"nano-banana-pro": {
"image-lab": {
enabled: true,
apiKey: { source: "env", provider: "default", id: "GEMINI_API_KEY" }, // or plaintext string
env: {
@ -214,6 +214,10 @@ Bundled/managed skills can be toggled and supplied with env values:
Note: if the skill name contains hyphens, quote the key (JSON5 allows quoted keys).
If you want stock image generation/editing inside OpenClaw itself, use the core
`image_generate` tool with `agents.defaults.imageGenerationModel` instead of a
bundled skill. Skill examples here are for custom or third-party workflows.
Config keys match the **skill name** by default. If a skill defines
`metadata.openclaw.skillKey`, use that key under `skills.entries`.

View File

@ -36,6 +36,8 @@ They run immediately, are stripped before the model sees the message, and the re
bash: false,
bashForegroundMs: 2000,
config: false,
mcp: false,
plugins: false,
debug: false,
restart: false,
allowFrom: {
@ -59,6 +61,8 @@ They run immediately, are stripped before the model sees the message, and the re
- `commands.bash` (default `false`) enables `! <cmd>` to run host shell commands (`/bash <cmd>` is an alias; requires `tools.elevated` allowlists).
- `commands.bashForegroundMs` (default `2000`) controls how long bash waits before switching to background mode (`0` backgrounds immediately).
- `commands.config` (default `false`) enables `/config` (reads/writes `openclaw.json`).
- `commands.mcp` (default `false`) enables `/mcp` (reads/writes OpenClaw-managed MCP config under `mcp.servers`).
- `commands.plugins` (default `false`) enables `/plugins` (plugin discovery/status plus enable/disable toggles).
- `commands.debug` (default `false`) enables `/debug` (runtime-only overrides).
- `commands.allowFrom` (optional) sets a per-provider allowlist for command authorization. When configured, it is the
only authorization source for commands and directives (channel allowlists/pairing and `commands.useAccessGroups`
@ -90,6 +94,8 @@ Text + native (when enabled):
- `/steer <id|#> <message>` (steer a running sub-agent immediately: in-run when possible, otherwise abort current work and restart on the steer message)
- `/tell <id|#> <message>` (alias for `/steer`)
- `/config show|get|set|unset` (persist config to disk, owner-only; requires `commands.config: true`)
- `/mcp show|get|set|unset` (manage OpenClaw MCP server config, owner-only; requires `commands.mcp: true`)
- `/plugins list|show|get|enable|disable` (inspect discovered plugins and toggle enablement, owner-only for writes; requires `commands.plugins: true`)
- `/debug show|set|unset|reset` (runtime overrides, owner-only; requires `commands.debug: true`)
- `/usage off|tokens|full|cost` (per-response usage footer or local cost summary)
- `/tts off|always|inbound|tagged|status|provider|limit|summary|audio` (control TTS; see [/tts](/tts))
@ -214,6 +220,44 @@ Notes:
- Config is validated before write; invalid changes are rejected.
- `/config` updates persist across restarts.
## MCP updates
`/mcp` writes OpenClaw-managed MCP server definitions under `mcp.servers`. Owner-only. Disabled by default; enable with `commands.mcp: true`.
Examples:
```text
/mcp show
/mcp show context7
/mcp set context7={"command":"uvx","args":["context7-mcp"]}
/mcp unset context7
```
Notes:
- `/mcp` stores config in OpenClaw config, not Pi-owned project settings.
- Runtime adapters decide which transports are actually executable.
## Plugin updates
`/plugins` lets operators inspect discovered plugins and toggle enablement in config. Read-only flows can use `/plugin` as an alias. Disabled by default; enable with `commands.plugins: true`.
Examples:
```text
/plugins
/plugins list
/plugin show context7
/plugins enable context7
/plugins disable context7
```
Notes:
- `/plugins list` and `/plugins show` use real plugin discovery against the current workspace plus on-disk config.
- `/plugins enable|disable` updates plugin config only; it does not install or uninstall plugins.
- After enable/disable changes, restart the gateway to apply them.
## Surface notes
- **Text commands** run in the normal chat session (DMs share `main`, groups have their own session).

View File

@ -242,7 +242,7 @@ http://localhost:5173/?gatewayUrl=wss://<gateway-host>:18789#token=<gateway-toke
Notes:
- `gatewayUrl` is stored in localStorage after load and removed from the URL.
- `token` is imported from the URL fragment, stored in sessionStorage for the current browser tab session and selected gateway URL, and stripped from the URL; it is not stored in localStorage.
- `token` is preferably imported from the URL fragment, stored in sessionStorage for the current browser tab session and selected gateway URL, and stripped from the URL; legacy `?token=` query params are also imported once for compatibility and then removed.
- `password` is kept in memory only.
- When `gatewayUrl` is set, the UI does not fall back to config or environment credentials.
Provide `token` (or `password`) explicitly. Missing explicit credentials is an error.

View File

@ -1,7 +1,7 @@
import fs from "node:fs";
import { chmod, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import path from "node:path";
import { resolvePreferredOpenClawTmpDir } from "../../../../src/infra/tmp-openclaw-dir.js";
import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/infra-runtime";
import type { ResolvedAcpxPluginConfig } from "../config.js";
import { ACPX_PINNED_VERSION } from "../config.js";
import { AcpxRuntime } from "../runtime.js";

View File

@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { registerSingleProviderPlugin } from "../../src/test-utils/plugin-registration.js";
import { registerSingleProviderPlugin } from "../../test/helpers/extensions/plugin-registration.js";
import amazonBedrockPlugin from "./index.js";
describe("amazon-bedrock provider plugin", () => {

View File

@ -1,14 +1,13 @@
import { emptyPluginConfigSchema, type OpenClawPluginApi } from "openclaw/plugin-sdk/core";
import { definePluginEntry } from "openclaw/plugin-sdk/core";
const PROVIDER_ID = "amazon-bedrock";
const CLAUDE_46_MODEL_RE = /claude-(?:opus|sonnet)-4(?:\.|-)6(?:$|[-.])/i;
const amazonBedrockPlugin = {
export default definePluginEntry({
id: PROVIDER_ID,
name: "Amazon Bedrock Provider",
description: "Bundled Amazon Bedrock provider policy plugin",
configSchema: emptyPluginConfigSchema(),
register(api: OpenClawPluginApi) {
register(api) {
api.registerProvider({
id: PROVIDER_ID,
label: "Amazon Bedrock",
@ -18,6 +17,4 @@ const amazonBedrockPlugin = {
CLAUDE_46_MODEL_RE.test(modelId.trim()) ? "adaptive" : undefined,
});
},
};
export default amazonBedrockPlugin;
});

View File

@ -1,31 +1,32 @@
import { formatCliCommand } from "openclaw/plugin-sdk/cli-runtime";
import { parseDurationMs } from "openclaw/plugin-sdk/cli-runtime";
import {
emptyPluginConfigSchema,
type OpenClawPluginApi,
definePluginEntry,
type ProviderAuthContext,
type ProviderResolveDynamicModelContext,
type ProviderRuntimeModel,
} from "openclaw/plugin-sdk/core";
import {
CLAUDE_CLI_PROFILE_ID,
applyAuthProfileConfig,
buildTokenProfileId,
createProviderApiKeyAuthMethod,
ensureApiKeyFromOptionEnvOrPrompt,
listProfilesForProvider,
upsertAuthProfile,
} from "../../src/agents/auth-profiles.js";
import { suggestOAuthProfileIdForLegacyDefault } from "../../src/agents/auth-profiles/repair.js";
import type { AuthProfileStore } from "../../src/agents/auth-profiles/types.js";
import { normalizeModelCompat } from "../../src/agents/model-compat.js";
import { formatCliCommand } from "../../src/cli/command-format.js";
import { parseDurationMs } from "../../src/cli/parse-duration.js";
import {
normalizeApiKeyInput,
suggestOAuthProfileIdForLegacyDefault,
type AuthProfileStore,
type ProviderAuthResult,
normalizeSecretInput,
normalizeSecretInputModeInput,
promptSecretRefForSetup,
resolveSecretInputModeForEnvSelection,
} from "../../src/commands/auth-choice.apply-helpers.js";
import { buildTokenProfileId, validateAnthropicSetupToken } from "../../src/commands/auth-token.js";
import { applyAuthProfileConfig } from "../../src/commands/onboard-auth.js";
import { fetchClaudeUsage } from "../../src/infra/provider-usage.fetch.js";
import { createProviderApiKeyAuthMethod } from "../../src/plugins/provider-api-key-auth.js";
import type { ProviderAuthResult } from "../../src/plugins/types.js";
import { normalizeSecretInput } from "../../src/utils/normalize-secret-input.js";
upsertAuthProfile,
validateAnthropicSetupToken,
validateApiKeyInput,
} from "openclaw/plugin-sdk/provider-auth";
import { normalizeModelCompat } from "openclaw/plugin-sdk/provider-models";
import { fetchClaudeUsage } from "openclaw/plugin-sdk/provider-usage";
import { anthropicMediaUnderstandingProvider } from "./media-understanding-provider.js";
const PROVIDER_ID = "anthropic";
@ -310,12 +311,11 @@ async function runAnthropicSetupTokenNonInteractive(ctx: {
});
}
const anthropicPlugin = {
export default definePluginEntry({
id: PROVIDER_ID,
name: "Anthropic Provider",
description: "Bundled Anthropic provider plugin",
configSchema: emptyPluginConfigSchema(),
register(api: OpenClawPluginApi) {
register(api) {
api.registerProvider({
id: PROVIDER_ID,
label: "Anthropic",
@ -397,6 +397,4 @@ const anthropicPlugin = {
});
api.registerMediaUnderstandingProvider(anthropicMediaUnderstandingProvider);
},
};
export default anthropicPlugin;
});

View File

@ -1,8 +1,12 @@
import { describeImageWithModel } from "../../src/media-understanding/providers/image.js";
import type { MediaUnderstandingProvider } from "../../src/media-understanding/types.js";
import {
describeImageWithModel,
describeImagesWithModel,
type MediaUnderstandingProvider,
} from "openclaw/plugin-sdk/media-understanding";
export const anthropicMediaUnderstandingProvider: MediaUnderstandingProvider = {
id: "anthropic",
capabilities: ["image"],
describeImage: describeImageWithModel,
describeImages: describeImagesWithModel,
};

View File

@ -1,17 +1,11 @@
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/bluebubbles";
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/bluebubbles";
import { defineChannelPluginEntry } from "openclaw/plugin-sdk/core";
import { bluebubblesPlugin } from "./src/channel.js";
import { setBlueBubblesRuntime } from "./src/runtime.js";
const plugin = {
export default defineChannelPluginEntry({
id: "bluebubbles",
name: "BlueBubbles",
description: "BlueBubbles channel plugin (macOS app)",
configSchema: emptyPluginConfigSchema(),
register(api: OpenClawPluginApi) {
setBlueBubblesRuntime(api.runtime);
api.registerChannel({ plugin: bluebubblesPlugin });
},
};
export default plugin;
plugin: bluebubblesPlugin,
setRuntime: setBlueBubblesRuntime,
});

View File

@ -1,5 +1,4 @@
import { defineSetupPluginEntry } from "openclaw/plugin-sdk/core";
import { bluebubblesPlugin } from "./src/channel.js";
export default {
plugin: bluebubblesPlugin,
};
export default defineSetupPluginEntry(bluebubblesPlugin);

View File

@ -1,13 +1,31 @@
export { sendBlueBubblesAttachment } from "./attachments.js";
export {
addBlueBubblesParticipant,
editBlueBubblesMessage,
leaveBlueBubblesChat,
removeBlueBubblesParticipant,
renameBlueBubblesChat,
setGroupIconBlueBubbles,
unsendBlueBubblesMessage,
import { sendBlueBubblesAttachment as sendBlueBubblesAttachmentImpl } from "./attachments.js";
import {
addBlueBubblesParticipant as addBlueBubblesParticipantImpl,
editBlueBubblesMessage as editBlueBubblesMessageImpl,
leaveBlueBubblesChat as leaveBlueBubblesChatImpl,
removeBlueBubblesParticipant as removeBlueBubblesParticipantImpl,
renameBlueBubblesChat as renameBlueBubblesChatImpl,
setGroupIconBlueBubbles as setGroupIconBlueBubblesImpl,
unsendBlueBubblesMessage as unsendBlueBubblesMessageImpl,
} from "./chat.js";
export { resolveBlueBubblesMessageId } from "./monitor.js";
export { sendBlueBubblesReaction } from "./reactions.js";
export { resolveChatGuidForTarget, sendMessageBlueBubbles } from "./send.js";
import { resolveBlueBubblesMessageId as resolveBlueBubblesMessageIdImpl } from "./monitor.js";
import { sendBlueBubblesReaction as sendBlueBubblesReactionImpl } from "./reactions.js";
import {
resolveChatGuidForTarget as resolveChatGuidForTargetImpl,
sendMessageBlueBubbles as sendMessageBlueBubblesImpl,
} from "./send.js";
export const blueBubblesActionsRuntime = {
sendBlueBubblesAttachment: sendBlueBubblesAttachmentImpl,
addBlueBubblesParticipant: addBlueBubblesParticipantImpl,
editBlueBubblesMessage: editBlueBubblesMessageImpl,
leaveBlueBubblesChat: leaveBlueBubblesChatImpl,
removeBlueBubblesParticipant: removeBlueBubblesParticipantImpl,
renameBlueBubblesChat: renameBlueBubblesChatImpl,
setGroupIconBlueBubbles: setGroupIconBlueBubblesImpl,
unsendBlueBubblesMessage: unsendBlueBubblesMessageImpl,
resolveBlueBubblesMessageId: resolveBlueBubblesMessageIdImpl,
sendBlueBubblesReaction: sendBlueBubblesReactionImpl,
resolveChatGuidForTarget: resolveChatGuidForTargetImpl,
sendMessageBlueBubbles: sendMessageBlueBubblesImpl,
};

View File

@ -11,18 +11,19 @@ import {
type ChannelMessageActionAdapter,
type ChannelMessageActionName,
} from "openclaw/plugin-sdk/bluebubbles";
import { createLazyRuntimeSurface } from "../../../src/shared/lazy-runtime.js";
import { resolveBlueBubblesAccount } from "./accounts.js";
import { getCachedBlueBubblesPrivateApiStatus, isMacOS26OrHigher } from "./probe.js";
import { normalizeSecretInputString } from "./secret-input.js";
import { normalizeBlueBubblesHandle, parseBlueBubblesTarget } from "./targets.js";
import type { BlueBubblesSendTarget } from "./types.js";
let actionsRuntimePromise: Promise<typeof import("./actions.runtime.js")> | null = null;
type BlueBubblesActionsRuntime = typeof import("./actions.runtime.js").blueBubblesActionsRuntime;
function loadBlueBubblesActionsRuntime() {
actionsRuntimePromise ??= import("./actions.runtime.js");
return actionsRuntimePromise;
}
const loadBlueBubblesActionsRuntime = createLazyRuntimeSurface(
() => import("./actions.runtime.js"),
({ blueBubblesActionsRuntime }) => blueBubblesActionsRuntime,
);
const providerId = "bluebubbles";

View File

@ -1,6 +1,19 @@
export { sendBlueBubblesMedia } from "./media-send.js";
export { resolveBlueBubblesMessageId } from "./monitor.js";
export { monitorBlueBubblesProvider, resolveWebhookPathFromConfig } from "./monitor.js";
export { type BlueBubblesProbe, probeBlueBubbles } from "./probe.js";
export { sendMessageBlueBubbles } from "./send.js";
export { blueBubblesSetupWizard } from "./setup-surface.js";
import { sendBlueBubblesMedia as sendBlueBubblesMediaImpl } from "./media-send.js";
import {
monitorBlueBubblesProvider as monitorBlueBubblesProviderImpl,
resolveBlueBubblesMessageId as resolveBlueBubblesMessageIdImpl,
resolveWebhookPathFromConfig as resolveWebhookPathFromConfigImpl,
} from "./monitor.js";
import { probeBlueBubbles as probeBlueBubblesImpl } from "./probe.js";
import { sendMessageBlueBubbles as sendMessageBlueBubblesImpl } from "./send.js";
export type { BlueBubblesProbe } from "./probe.js";
export const blueBubblesChannelRuntime = {
sendBlueBubblesMedia: sendBlueBubblesMediaImpl,
resolveBlueBubblesMessageId: resolveBlueBubblesMessageIdImpl,
monitorBlueBubblesProvider: monitorBlueBubblesProviderImpl,
resolveWebhookPathFromConfig: resolveWebhookPathFromConfigImpl,
probeBlueBubbles: probeBlueBubblesImpl,
sendMessageBlueBubbles: sendMessageBlueBubblesImpl,
};

View File

@ -1,3 +1,4 @@
import { formatNormalizedAllowFromEntries } from "openclaw/plugin-sdk/allow-from";
import type { ChannelAccountSnapshot, ChannelPlugin } from "openclaw/plugin-sdk/bluebubbles";
import {
buildChannelConfigSchema,
@ -11,13 +12,13 @@ import {
resolveBlueBubblesGroupToolPolicy,
setAccountEnabledInConfigSection,
} from "openclaw/plugin-sdk/bluebubbles";
import { mapAllowFromEntries } from "openclaw/plugin-sdk/channel-config-helpers";
import { createAccountStatusSink } from "openclaw/plugin-sdk/channel-lifecycle";
import {
buildAccountScopedDmSecurityPolicy,
collectOpenGroupPolicyRestrictSendersWarnings,
createAccountStatusSink,
formatNormalizedAllowFromEntries,
mapAllowFromEntries,
} from "openclaw/plugin-sdk/compat";
} from "openclaw/plugin-sdk/channel-policy";
import { createLazyRuntimeSurface } from "../../../src/shared/lazy-runtime.js";
import {
listBlueBubblesAccountIds,
type ResolvedBlueBubblesAccount,
@ -37,12 +38,12 @@ import {
parseBlueBubblesTarget,
} from "./targets.js";
let blueBubblesChannelRuntimePromise: Promise<typeof import("./channel.runtime.js")> | null = null;
type BlueBubblesChannelRuntime = typeof import("./channel.runtime.js").blueBubblesChannelRuntime;
function loadBlueBubblesChannelRuntime() {
blueBubblesChannelRuntimePromise ??= import("./channel.runtime.js");
return blueBubblesChannelRuntimePromise;
}
const loadBlueBubblesChannelRuntime = createLazyRuntimeSurface(
() => import("./channel.runtime.js"),
({ blueBubblesChannelRuntime }) => blueBubblesChannelRuntime,
);
const meta = {
id: "bluebubbles",

View File

@ -4,7 +4,7 @@ import {
buildCatchallMultiAccountChannelSchema,
DmPolicySchema,
GroupPolicySchema,
} from "openclaw/plugin-sdk/compat";
} from "openclaw/plugin-sdk/channel-config-schema";
import { z } from "zod";
import { buildSecretInputSchema, hasConfiguredSecretInput } from "./secret-input.js";

View File

@ -2,7 +2,7 @@ import { EventEmitter } from "node:events";
import type { IncomingMessage, ServerResponse } from "node:http";
import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk/bluebubbles";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { createPluginRuntimeMock } from "../../test-utils/plugin-runtime-mock.js";
import { createPluginRuntimeMock } from "../../../test/helpers/extensions/plugin-runtime-mock.js";
import type { ResolvedBlueBubblesAccount } from "./accounts.js";
import { fetchBlueBubblesHistory } from "./history.js";
import { resetBlueBubblesSelfChatCache } from "./monitor-self-chat-cache.js";

View File

@ -2,7 +2,7 @@ import { EventEmitter } from "node:events";
import type { IncomingMessage, ServerResponse } from "node:http";
import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk/bluebubbles";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { createPluginRuntimeMock } from "../../test-utils/plugin-runtime-mock.js";
import { createPluginRuntimeMock } from "../../../test/helpers/extensions/plugin-runtime-mock.js";
import type { ResolvedBlueBubblesAccount } from "./accounts.js";
import { fetchBlueBubblesHistory } from "./history.js";
import {

View File

@ -1,5 +1,5 @@
import type { PluginRuntime } from "openclaw/plugin-sdk/bluebubbles";
import { createPluginRuntimeStore } from "openclaw/plugin-sdk/compat";
import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
const runtimeStore = createPluginRuntimeStore<PluginRuntime>("BlueBubbles runtime not initialized");
type LegacyRuntimeLogShape = { log?: (message: string) => void };

View File

@ -1,12 +1,12 @@
import {
normalizeAccountId,
patchScopedAccountConfig,
prepareScopedSetupConfig,
} from "../../../src/channels/plugins/setup-helpers.js";
import { setTopLevelChannelDmPolicyWithAllowFrom } from "../../../src/channels/plugins/setup-wizard-helpers.js";
import type { ChannelSetupAdapter } from "../../../src/channels/plugins/types.adapters.js";
import type { OpenClawConfig } from "../../../src/config/config.js";
import type { DmPolicy } from "../../../src/config/types.js";
import { normalizeAccountId } from "../../../src/routing/session-key.js";
setTopLevelChannelDmPolicyWithAllowFrom,
type ChannelSetupAdapter,
type DmPolicy,
type OpenClawConfig,
} from "openclaw/plugin-sdk/setup";
import { applyBlueBubblesConnectionConfig } from "./config-apply.js";
const channel = "bluebubbles" as const;

View File

@ -1,17 +1,15 @@
import { definePluginEntry } from "openclaw/plugin-sdk/core";
import {
createPluginBackedWebSearchProvider,
getTopLevelCredentialValue,
setTopLevelCredentialValue,
} from "../../src/agents/tools/web-search-plugin-factory.js";
import { emptyPluginConfigSchema } from "../../src/plugins/config-schema.js";
import type { OpenClawPluginApi } from "../../src/plugins/types.js";
} from "openclaw/plugin-sdk/provider-web-search";
const bravePlugin = {
export default definePluginEntry({
id: "brave",
name: "Brave Plugin",
description: "Bundled Brave plugin",
configSchema: emptyPluginConfigSchema(),
register(api: OpenClawPluginApi) {
register(api) {
api.registerWebSearchProvider(
createPluginBackedWebSearchProvider({
id: "brave",
@ -27,6 +25,4 @@ const bravePlugin = {
}),
);
},
};
export default bravePlugin;
});

View File

@ -1,18 +1,16 @@
import { emptyPluginConfigSchema, type OpenClawPluginApi } from "openclaw/plugin-sdk/core";
import { ensureModelAllowlistEntry } from "../../src/commands/model-allowlist.js";
import { createProviderApiKeyAuthMethod } from "../../src/plugins/provider-api-key-auth.js";
import { buildPairedProviderApiKeyCatalog } from "../../src/plugins/provider-catalog.js";
import { definePluginEntry } from "openclaw/plugin-sdk/core";
import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth";
import { ensureModelAllowlistEntry } from "openclaw/plugin-sdk/provider-onboard";
import { buildBytePlusCodingProvider, buildBytePlusProvider } from "./provider-catalog.js";
const PROVIDER_ID = "byteplus";
const BYTEPLUS_DEFAULT_MODEL_REF = "byteplus-plan/ark-code-latest";
const byteplusPlugin = {
export default definePluginEntry({
id: PROVIDER_ID,
name: "BytePlus Provider",
description: "Bundled BytePlus provider plugin",
configSchema: emptyPluginConfigSchema(),
register(api: OpenClawPluginApi) {
register(api) {
api.registerProvider({
id: PROVIDER_ID,
label: "BytePlus",
@ -46,18 +44,19 @@ const byteplusPlugin = {
],
catalog: {
order: "paired",
run: (ctx) =>
buildPairedProviderApiKeyCatalog({
ctx,
providerId: PROVIDER_ID,
buildProviders: () => ({
byteplus: buildBytePlusProvider(),
"byteplus-plan": buildBytePlusCodingProvider(),
}),
}),
run: async (ctx) => {
const apiKey = ctx.resolveProviderApiKey(PROVIDER_ID).apiKey;
if (!apiKey) {
return null;
}
return {
providers: {
byteplus: { ...buildBytePlusProvider(), apiKey },
"byteplus-plan": { ...buildBytePlusCodingProvider(), apiKey },
},
};
},
},
});
},
};
export default byteplusPlugin;
});

View File

@ -4,8 +4,8 @@ import {
BYTEPLUS_CODING_BASE_URL,
BYTEPLUS_CODING_MODEL_CATALOG,
BYTEPLUS_MODEL_CATALOG,
} from "../../src/agents/byteplus-models.js";
import type { ModelProviderConfig } from "../../src/config/types.models.js";
type ModelProviderConfig,
} from "openclaw/plugin-sdk/provider-models";
export function buildBytePlusProvider(): ModelProviderConfig {
return {

View File

@ -1,21 +1,22 @@
import { emptyPluginConfigSchema, type OpenClawPluginApi } from "openclaw/plugin-sdk/core";
import { upsertAuthProfile } from "../../src/agents/auth-profiles.js";
import { ensureAuthProfileStore, listProfilesForProvider } from "../../src/agents/auth-profiles.js";
import { definePluginEntry } from "openclaw/plugin-sdk/core";
import {
applyAuthProfileConfig,
buildApiKeyCredential,
coerceSecretRef,
ensureApiKeyFromOptionEnvOrPrompt,
ensureAuthProfileStore,
listProfilesForProvider,
normalizeApiKeyInput,
normalizeOptionalSecretInput,
resolveNonEnvSecretRefApiKeyMarker,
type SecretInput,
upsertAuthProfile,
validateApiKeyInput,
} from "openclaw/plugin-sdk/provider-auth";
import {
buildCloudflareAiGatewayModelDefinition,
resolveCloudflareAiGatewayBaseUrl,
} from "../../src/agents/cloudflare-ai-gateway.js";
import { resolveNonEnvSecretRefApiKeyMarker } from "../../src/agents/model-auth-markers.js";
import {
normalizeApiKeyInput,
validateApiKeyInput,
} from "../../src/commands/auth-choice.api-key.js";
import { ensureApiKeyFromOptionEnvOrPrompt } from "../../src/commands/auth-choice.apply-helpers.js";
import { buildApiKeyCredential } from "../../src/commands/auth-credentials.js";
import { applyAuthProfileConfig } from "../../src/commands/onboard-auth.js";
import type { SecretInput } from "../../src/config/types.secrets.js";
import { coerceSecretRef } from "../../src/config/types.secrets.js";
import { normalizeOptionalSecretInput } from "../../src/utils/normalize-secret-input.js";
} from "openclaw/plugin-sdk/provider-models";
import {
applyCloudflareAiGatewayConfig,
buildCloudflareAiGatewayConfigPatch,
@ -83,12 +84,11 @@ async function resolveCloudflareGatewayMetadataInteractive(ctx: {
return { accountId, gatewayId };
}
const cloudflareAiGatewayPlugin = {
export default definePluginEntry({
id: PROVIDER_ID,
name: "Cloudflare AI Gateway Provider",
description: "Bundled Cloudflare AI Gateway provider plugin",
configSchema: emptyPluginConfigSchema(),
register(api: OpenClawPluginApi) {
register(api) {
api.registerProvider({
id: PROVIDER_ID,
label: "Cloudflare AI Gateway",
@ -251,6 +251,4 @@ const cloudflareAiGatewayPlugin = {
},
});
},
};
export default cloudflareAiGatewayPlugin;
});

View File

@ -2,12 +2,12 @@ import {
buildCloudflareAiGatewayModelDefinition,
CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_REF,
resolveCloudflareAiGatewayBaseUrl,
} from "../../src/agents/cloudflare-ai-gateway.js";
} from "openclaw/plugin-sdk/provider-models";
import {
applyAgentDefaultModelPrimary,
applyProviderConfigWithDefaultModel,
} from "../../src/commands/onboard-auth.config-shared.js";
import type { OpenClawConfig } from "../../src/config/config.js";
type OpenClawConfig,
} from "openclaw/plugin-sdk/provider-onboard";
export { CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_REF };

View File

@ -1,6 +1,5 @@
import {
emptyPluginConfigSchema,
type OpenClawPluginApi,
definePluginEntry,
type ProviderAuthContext,
type ProviderAuthResult,
} from "openclaw/plugin-sdk/copilot-proxy";
@ -71,12 +70,11 @@ function buildModelDefinition(modelId: string) {
};
}
const copilotProxyPlugin = {
export default definePluginEntry({
id: "copilot-proxy",
name: "Copilot Proxy",
description: "Local Copilot Proxy (VS Code LM) provider plugin",
configSchema: emptyPluginConfigSchema(),
register(api: OpenClawPluginApi) {
register(api) {
api.registerProvider({
id: "copilot-proxy",
label: "Copilot Proxy",
@ -157,6 +155,4 @@ const copilotProxyPlugin = {
},
});
},
};
export default copilotProxyPlugin;
});

View File

@ -1,12 +1,13 @@
import os from "node:os";
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/device-pair";
import {
approveDevicePairing,
definePluginEntry,
issueDeviceBootstrapToken,
listDevicePairing,
resolveGatewayBindUrl,
runPluginCommandWithTimeout,
resolveTailnetHostWithRunner,
type OpenClawPluginApi,
} from "openclaw/plugin-sdk/device-pair";
import qrcode from "qrcode-terminal";
import {
@ -325,226 +326,233 @@ function formatSetupInstructions(): string {
].join("\n");
}
export default function register(api: OpenClawPluginApi) {
registerPairingNotifierService(api);
export default definePluginEntry({
id: "device-pair",
name: "Device Pair",
description: "QR/bootstrap pairing helpers for OpenClaw devices",
register(api: OpenClawPluginApi) {
registerPairingNotifierService(api);
api.registerCommand({
name: "pair",
description: "Generate setup codes and approve device pairing requests.",
acceptsArgs: true,
handler: async (ctx) => {
const args = ctx.args?.trim() ?? "";
const tokens = args.split(/\s+/).filter(Boolean);
const action = tokens[0]?.toLowerCase() ?? "";
api.logger.info?.(
`device-pair: /pair invoked channel=${ctx.channel} sender=${ctx.senderId ?? "unknown"} action=${
action || "new"
}`,
);
api.registerCommand({
name: "pair",
description: "Generate setup codes and approve device pairing requests.",
acceptsArgs: true,
handler: async (ctx) => {
const args = ctx.args?.trim() ?? "";
const tokens = args.split(/\s+/).filter(Boolean);
const action = tokens[0]?.toLowerCase() ?? "";
api.logger.info?.(
`device-pair: /pair invoked channel=${ctx.channel} sender=${ctx.senderId ?? "unknown"} action=${
action || "new"
}`,
);
if (action === "status" || action === "pending") {
const list = await listDevicePairing();
return { text: formatPendingRequests(list.pending) };
}
if (action === "notify") {
const notifyAction = tokens[1]?.trim().toLowerCase() ?? "status";
return await handleNotifyCommand({
api,
ctx,
action: notifyAction,
});
}
if (action === "approve") {
const requested = tokens[1]?.trim();
const list = await listDevicePairing();
if (list.pending.length === 0) {
return { text: "No pending device pairing requests." };
if (action === "status" || action === "pending") {
const list = await listDevicePairing();
return { text: formatPendingRequests(list.pending) };
}
let pending: (typeof list.pending)[number] | undefined;
if (requested) {
if (requested.toLowerCase() === "latest") {
pending = [...list.pending].toSorted((a, b) => (b.ts ?? 0) - (a.ts ?? 0))[0];
} else {
pending = list.pending.find((entry) => entry.requestId === requested);
if (action === "notify") {
const notifyAction = tokens[1]?.trim().toLowerCase() ?? "status";
return await handleNotifyCommand({
api,
ctx,
action: notifyAction,
});
}
if (action === "approve") {
const requested = tokens[1]?.trim();
const list = await listDevicePairing();
if (list.pending.length === 0) {
return { text: "No pending device pairing requests." };
}
} else if (list.pending.length === 1) {
pending = list.pending[0];
} else {
let pending: (typeof list.pending)[number] | undefined;
if (requested) {
if (requested.toLowerCase() === "latest") {
pending = [...list.pending].toSorted((a, b) => (b.ts ?? 0) - (a.ts ?? 0))[0];
} else {
pending = list.pending.find((entry) => entry.requestId === requested);
}
} else if (list.pending.length === 1) {
pending = list.pending[0];
} else {
return {
text:
`${formatPendingRequests(list.pending)}\n\n` +
"Multiple pending requests found. Approve one explicitly:\n" +
"/pair approve <requestId>\n" +
"Or approve the most recent:\n" +
"/pair approve latest",
};
}
if (!pending) {
return { text: "Pairing request not found." };
}
const approved = await approveDevicePairing(pending.requestId);
if (!approved) {
return { text: "Pairing request not found." };
}
const label = approved.device.displayName?.trim() || approved.device.deviceId;
const platform = approved.device.platform?.trim();
const platformLabel = platform ? ` (${platform})` : "";
return { text: `✅ Paired ${label}${platformLabel}.` };
}
const authLabelResult = resolveAuthLabel(api.config);
if (authLabelResult.error) {
return { text: `Error: ${authLabelResult.error}` };
}
const urlResult = await resolveGatewayUrl(api);
if (!urlResult.url) {
return { text: `Error: ${urlResult.error ?? "Gateway URL unavailable."}` };
}
const payload: SetupPayload = {
url: urlResult.url,
bootstrapToken: (await issueDeviceBootstrapToken()).token,
};
if (action === "qr") {
const setupCode = encodeSetupCode(payload);
const qrAscii = await renderQrAscii(setupCode);
const authLabel = authLabelResult.label ?? "auth";
const channel = ctx.channel;
const target = ctx.senderId?.trim() || ctx.from?.trim() || ctx.to?.trim() || "";
let autoNotifyArmed = false;
if (channel === "telegram" && target) {
try {
autoNotifyArmed = await armPairNotifyOnce({ api, ctx });
} catch (err) {
api.logger.warn?.(
`device-pair: failed to arm one-shot pairing notify (${String(
(err as Error)?.message ?? err,
)})`,
);
}
}
if (channel === "telegram" && target) {
try {
const send = api.runtime?.channel?.telegram?.sendMessageTelegram;
if (send) {
await send(
target,
["Scan this QR code with the OpenClaw iOS app:", "", "```", qrAscii, "```"].join(
"\n",
),
{
...(ctx.messageThreadId != null
? { messageThreadId: ctx.messageThreadId }
: {}),
...(ctx.accountId ? { accountId: ctx.accountId } : {}),
},
);
return {
text: [
`Gateway: ${payload.url}`,
`Auth: ${authLabel}`,
"",
autoNotifyArmed
? "After scanning, wait here for the pairing request ping."
: "After scanning, come back here and run `/pair approve` to complete pairing.",
...(autoNotifyArmed
? [
"Ill auto-ping here when the pairing request arrives, then auto-disable.",
"If the ping does not arrive, run `/pair approve latest` manually.",
]
: []),
].join("\n"),
};
}
} catch (err) {
api.logger.warn?.(
`device-pair: telegram QR send failed, falling back (${String(
(err as Error)?.message ?? err,
)})`,
);
}
}
// Render based on channel capability
api.logger.info?.(`device-pair: QR fallback channel=${channel} target=${target}`);
const infoLines = [
`Gateway: ${payload.url}`,
`Auth: ${authLabel}`,
"",
autoNotifyArmed
? "After scanning, wait here for the pairing request ping."
: "After scanning, run `/pair approve` to complete pairing.",
...(autoNotifyArmed
? [
"Ill auto-ping here when the pairing request arrives, then auto-disable.",
"If the ping does not arrive, run `/pair approve latest` manually.",
]
: []),
];
// WebUI + CLI/TUI: ASCII QR
return {
text:
`${formatPendingRequests(list.pending)}\n\n` +
"Multiple pending requests found. Approve one explicitly:\n" +
"/pair approve <requestId>\n" +
"Or approve the most recent:\n" +
"/pair approve latest",
text: [
"Scan this QR code with the OpenClaw iOS app:",
"",
"```",
qrAscii,
"```",
"",
...infoLines,
].join("\n"),
};
}
if (!pending) {
return { text: "Pairing request not found." };
}
const approved = await approveDevicePairing(pending.requestId);
if (!approved) {
return { text: "Pairing request not found." };
}
const label = approved.device.displayName?.trim() || approved.device.deviceId;
const platform = approved.device.platform?.trim();
const platformLabel = platform ? ` (${platform})` : "";
return { text: `✅ Paired ${label}${platformLabel}.` };
}
const authLabelResult = resolveAuthLabel(api.config);
if (authLabelResult.error) {
return { text: `Error: ${authLabelResult.error}` };
}
const urlResult = await resolveGatewayUrl(api);
if (!urlResult.url) {
return { text: `Error: ${urlResult.error ?? "Gateway URL unavailable."}` };
}
const payload: SetupPayload = {
url: urlResult.url,
bootstrapToken: (await issueDeviceBootstrapToken()).token,
};
if (action === "qr") {
const setupCode = encodeSetupCode(payload);
const qrAscii = await renderQrAscii(setupCode);
const authLabel = authLabelResult.label ?? "auth";
const channel = ctx.channel;
const target = ctx.senderId?.trim() || ctx.from?.trim() || ctx.to?.trim() || "";
let autoNotifyArmed = false;
const authLabel = authLabelResult.label ?? "auth";
if (channel === "telegram" && target) {
try {
autoNotifyArmed = await armPairNotifyOnce({ api, ctx });
} catch (err) {
api.logger.warn?.(
`device-pair: failed to arm one-shot pairing notify (${String(
(err as Error)?.message ?? err,
)})`,
const runtimeKeys = Object.keys(api.runtime ?? {});
const channelKeys = Object.keys(api.runtime?.channel ?? {});
api.logger.debug?.(
`device-pair: runtime keys=${runtimeKeys.join(",") || "none"} channel keys=${
channelKeys.join(",") || "none"
}`,
);
}
}
if (channel === "telegram" && target) {
try {
const send = api.runtime?.channel?.telegram?.sendMessageTelegram;
if (send) {
await send(
target,
["Scan this QR code with the OpenClaw iOS app:", "", "```", qrAscii, "```"].join(
"\n",
),
{
...(ctx.messageThreadId != null ? { messageThreadId: ctx.messageThreadId } : {}),
...(ctx.accountId ? { accountId: ctx.accountId } : {}),
},
if (!send) {
throw new Error(
`telegram runtime unavailable (runtime keys: ${runtimeKeys.join(",")}; channel keys: ${channelKeys.join(
",",
)})`,
);
return {
text: [
`Gateway: ${payload.url}`,
`Auth: ${authLabel}`,
"",
autoNotifyArmed
? "After scanning, wait here for the pairing request ping."
: "After scanning, come back here and run `/pair approve` to complete pairing.",
...(autoNotifyArmed
? [
"Ill auto-ping here when the pairing request arrives, then auto-disable.",
"If the ping does not arrive, run `/pair approve latest` manually.",
]
: []),
].join("\n"),
};
}
await send(target, formatSetupInstructions(), {
...(ctx.messageThreadId != null ? { messageThreadId: ctx.messageThreadId } : {}),
...(ctx.accountId ? { accountId: ctx.accountId } : {}),
});
api.logger.info?.(
`device-pair: telegram split send ok target=${target} account=${ctx.accountId ?? "none"} thread=${
ctx.messageThreadId ?? "none"
}`,
);
return { text: encodeSetupCode(payload) };
} catch (err) {
api.logger.warn?.(
`device-pair: telegram QR send failed, falling back (${String(
`device-pair: telegram split send failed, falling back to single message (${String(
(err as Error)?.message ?? err,
)})`,
);
}
}
// Render based on channel capability
api.logger.info?.(`device-pair: QR fallback channel=${channel} target=${target}`);
const infoLines = [
`Gateway: ${payload.url}`,
`Auth: ${authLabel}`,
"",
autoNotifyArmed
? "After scanning, wait here for the pairing request ping."
: "After scanning, run `/pair approve` to complete pairing.",
...(autoNotifyArmed
? [
"Ill auto-ping here when the pairing request arrives, then auto-disable.",
"If the ping does not arrive, run `/pair approve latest` manually.",
]
: []),
];
// WebUI + CLI/TUI: ASCII QR
return {
text: [
"Scan this QR code with the OpenClaw iOS app:",
"",
"```",
qrAscii,
"```",
"",
...infoLines,
].join("\n"),
text: formatSetupReply(payload, authLabel),
};
}
const channel = ctx.channel;
const target = ctx.senderId?.trim() || ctx.from?.trim() || ctx.to?.trim() || "";
const authLabel = authLabelResult.label ?? "auth";
if (channel === "telegram" && target) {
try {
const runtimeKeys = Object.keys(api.runtime ?? {});
const channelKeys = Object.keys(api.runtime?.channel ?? {});
api.logger.debug?.(
`device-pair: runtime keys=${runtimeKeys.join(",") || "none"} channel keys=${
channelKeys.join(",") || "none"
}`,
);
const send = api.runtime?.channel?.telegram?.sendMessageTelegram;
if (!send) {
throw new Error(
`telegram runtime unavailable (runtime keys: ${runtimeKeys.join(",")}; channel keys: ${channelKeys.join(
",",
)})`,
);
}
await send(target, formatSetupInstructions(), {
...(ctx.messageThreadId != null ? { messageThreadId: ctx.messageThreadId } : {}),
...(ctx.accountId ? { accountId: ctx.accountId } : {}),
});
api.logger.info?.(
`device-pair: telegram split send ok target=${target} account=${ctx.accountId ?? "none"} thread=${
ctx.messageThreadId ?? "none"
}`,
);
return { text: encodeSetupCode(payload) };
} catch (err) {
api.logger.warn?.(
`device-pair: telegram split send failed, falling back to single message (${String(
(err as Error)?.message ?? err,
)})`,
);
}
}
return {
text: formatSetupReply(payload, authLabel),
};
},
});
}
},
});
},
});

View File

@ -1,15 +1,11 @@
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/diagnostics-otel";
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/diagnostics-otel";
import { definePluginEntry } from "openclaw/plugin-sdk/core";
import { createDiagnosticsOtelService } from "./src/service.js";
const plugin = {
export default definePluginEntry({
id: "diagnostics-otel",
name: "Diagnostics OpenTelemetry",
description: "Export diagnostics events to OpenTelemetry",
configSchema: emptyPluginConfigSchema(),
register(api: OpenClawPluginApi) {
register(api) {
api.registerService(createDiagnosticsOtelService());
},
};
export default plugin;
});

View File

@ -1,8 +1,8 @@
import type { IncomingMessage } from "node:http";
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/diffs";
import { describe, expect, it, vi } from "vitest";
import { createMockServerResponse } from "../../src/test-utils/mock-http-response.js";
import { createTestPluginApi } from "../test-utils/plugin-api.js";
import { createMockServerResponse } from "../../test/helpers/extensions/mock-http-response.js";
import { createTestPluginApi } from "../../test/helpers/extensions/plugin-api.js";
import plugin from "./index.js";
describe("diffs plugin registration", () => {

View File

@ -1,6 +1,6 @@
import type { IncomingMessage } from "node:http";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { createMockServerResponse } from "../../../src/test-utils/mock-http-response.js";
import { createMockServerResponse } from "../../../test/helpers/extensions/mock-http-response.js";
import { createDiffsHttpHandler } from "./http.js";
import { DiffArtifactStore } from "./store.js";
import { createDiffStoreHarness } from "./test-helpers.js";

View File

@ -2,7 +2,7 @@ import fs from "node:fs/promises";
import path from "node:path";
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/diffs";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { createTestPluginApi } from "../../test-utils/plugin-api.js";
import { createTestPluginApi } from "../../../test/helpers/extensions/plugin-api.js";
import type { DiffScreenshotter } from "./browser.js";
import { DEFAULT_DIFFS_TOOL_DEFAULTS } from "./config.js";
import { DiffArtifactStore } from "./store.js";

View File

@ -1,22 +1,13 @@
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/core";
import { defineChannelPluginEntry } from "openclaw/plugin-sdk/core";
import { discordPlugin } from "./src/channel.js";
import { setDiscordRuntime } from "./src/runtime.js";
import { registerDiscordSubagentHooks } from "./src/subagent-hooks.js";
const plugin = {
export default defineChannelPluginEntry({
id: "discord",
name: "Discord",
description: "Discord channel plugin",
configSchema: emptyPluginConfigSchema(),
register(api: OpenClawPluginApi) {
setDiscordRuntime(api.runtime);
api.registerChannel({ plugin: discordPlugin });
if (api.registrationMode !== "full") {
return;
}
registerDiscordSubagentHooks(api);
},
};
export default plugin;
plugin: discordPlugin,
setRuntime: setDiscordRuntime,
registerFull: registerDiscordSubagentHooks,
});

View File

@ -1,3 +1,4 @@
import { defineSetupPluginEntry } from "openclaw/plugin-sdk/core";
import { discordSetupPlugin } from "./src/channel.setup.js";
export default { plugin: discordSetupPlugin };
export default defineSetupPluginEntry(discordSetupPlugin);

View File

@ -1,13 +1,13 @@
import {
hasConfiguredSecretInput,
normalizeSecretInputString,
} from "../../../src/config/types.secrets.js";
import {
DEFAULT_ACCOUNT_ID,
normalizeAccountId,
type OpenClawConfig,
type DiscordAccountConfig,
} from "../../../src/plugin-sdk-internal/discord.js";
} from "openclaw/plugin-sdk/account-resolution";
import {
hasConfiguredSecretInput,
normalizeSecretInputString,
} from "openclaw/plugin-sdk/config-runtime";
import type { DiscordAccountConfig } from "openclaw/plugin-sdk/discord";
import {
mergeDiscordAccountConfig,
resolveDefaultDiscordAccountId,

View File

@ -3,12 +3,9 @@ import {
createAccountListHelpers,
normalizeAccountId,
resolveAccountEntry,
} from "../../../src/plugin-sdk-internal/accounts.js";
import type {
OpenClawConfig,
DiscordAccountConfig,
DiscordActionConfig,
} from "../../../src/plugin-sdk-internal/discord.js";
type OpenClawConfig,
} from "openclaw/plugin-sdk/account-resolution";
import type { DiscordAccountConfig, DiscordActionConfig } from "openclaw/plugin-sdk/discord";
import { resolveDiscordToken } from "./token.js";
export type ResolvedDiscordAccount = {

View File

@ -4,13 +4,13 @@ import {
readNumberParam,
readStringArrayParam,
readStringParam,
} from "../../../../src/agents/tools/common.js";
} from "openclaw/plugin-sdk/agent-runtime";
import {
isDiscordModerationAction,
readDiscordModerationCommand,
} from "../../../../src/agents/tools/discord-actions-moderation-shared.js";
import { handleDiscordAction } from "../../../../src/agents/tools/discord-actions.js";
import type { ChannelMessageActionContext } from "../../../../src/channels/plugins/types.js";
} from "openclaw/plugin-sdk/agent-runtime";
import { handleDiscordAction } from "openclaw/plugin-sdk/agent-runtime";
import type { ChannelMessageActionContext } from "openclaw/plugin-sdk/channel-runtime";
type Ctx = Pick<
ChannelMessageActionContext,

View File

@ -1,15 +1,15 @@
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
import { readBooleanParam } from "openclaw/plugin-sdk/boolean-param";
import {
readNumberParam,
readStringArrayParam,
readStringParam,
} from "../../../../src/agents/tools/common.js";
import { readDiscordParentIdParam } from "../../../../src/agents/tools/discord-actions-shared.js";
import { handleDiscordAction } from "../../../../src/agents/tools/discord-actions.js";
import { resolveReactionMessageId } from "../../../../src/channels/plugins/actions/reaction-message-id.js";
import type { ChannelMessageActionContext } from "../../../../src/channels/plugins/types.js";
import { normalizeInteractiveReply } from "../../../../src/interactive/payload.js";
} from "openclaw/plugin-sdk/agent-runtime";
import { readDiscordParentIdParam } from "openclaw/plugin-sdk/agent-runtime";
import { handleDiscordAction } from "openclaw/plugin-sdk/agent-runtime";
import { readBooleanParam } from "openclaw/plugin-sdk/boolean-param";
import { resolveReactionMessageId } from "openclaw/plugin-sdk/channel-runtime";
import type { ChannelMessageActionContext } from "openclaw/plugin-sdk/channel-runtime";
import { normalizeInteractiveReply } from "openclaw/plugin-sdk/channel-runtime";
import { buildDiscordInteractiveComponents } from "../shared-interactive.js";
import { resolveDiscordChannelId } from "../targets.js";
import { tryHandleDiscordMessageActionGuildAdmin } from "./handle-action.guild-admin.js";

View File

@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { withFetchPreconnect } from "../../../src/test-utils/fetch-mock.js";
import { withFetchPreconnect } from "../../../test/helpers/extensions/fetch-mock.js";
import { fetchDiscord } from "./api.js";
import { jsonResponse } from "./test-http-helpers.js";

View File

@ -1,5 +1,9 @@
import { resolveFetch } from "../../../src/infra/fetch.js";
import { resolveRetryConfig, retryAsync, type RetryConfig } from "../../../src/infra/retry.js";
import { resolveFetch } from "openclaw/plugin-sdk/infra-runtime";
import {
resolveRetryConfig,
retryAsync,
type RetryConfig,
} from "openclaw/plugin-sdk/infra-runtime";
const DISCORD_API_BASE = "https://discord.com/api/v10";
const DISCORD_API_RETRY_DEFAULTS = {

View File

@ -1,6 +1,9 @@
import type { OpenClawConfig } from "../../../src/config/config.js";
import type { DiscordGuildChannelConfig, DiscordGuildEntry } from "../../../src/config/types.js";
import { isRecord } from "../../../src/utils.js";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
import type {
DiscordGuildChannelConfig,
DiscordGuildEntry,
} from "openclaw/plugin-sdk/config-runtime";
import { isRecord } from "openclaw/plugin-sdk/text-runtime";
import { inspectDiscordAccount } from "./account-inspect.js";
import { fetchChannelPermissionsDiscord } from "./send.js";

View File

@ -1,12 +1,12 @@
import {
createUnionActionGate,
listTokenSourcedAccounts,
} from "../../../src/channels/plugins/actions/shared.js";
} from "openclaw/plugin-sdk/channel-runtime";
import type {
ChannelMessageActionAdapter,
ChannelMessageActionName,
} from "../../../src/channels/plugins/types.js";
import type { DiscordActionConfig } from "../../../src/config/types.discord.js";
} from "openclaw/plugin-sdk/channel-runtime";
import type { DiscordActionConfig } from "openclaw/plugin-sdk/config-runtime";
import { createDiscordActionGate, listEnabledDiscordAccounts } from "./accounts.js";
import { handleDiscordMessageAction } from "./actions/handle-action.js";

View File

@ -1 +1,5 @@
export { discordSetupWizard } from "./setup-surface.js";
import { discordSetupWizard as discordSetupWizardImpl } from "./setup-surface.js";
type DiscordSetupWizard = typeof import("./setup-surface.js").discordSetupWizard;
export const discordSetupWizard: DiscordSetupWizard = { ...discordSetupWizardImpl };

View File

@ -1,8 +1,10 @@
import type { ChannelPlugin } from "openclaw/plugin-sdk/discord";
import type { ResolvedDiscordAccount } from "./accounts.js";
import { type ChannelPlugin } from "openclaw/plugin-sdk/discord";
import { type ResolvedDiscordAccount } from "./accounts.js";
import { discordSetupAdapter } from "./setup-core.js";
import { createDiscordPluginBase } from "./shared.js";
export const discordSetupPlugin: ChannelPlugin<ResolvedDiscordAccount> = createDiscordPluginBase({
setup: discordSetupAdapter,
});
export const discordSetupPlugin: ChannelPlugin<ResolvedDiscordAccount> = {
...createDiscordPluginBase({
setup: discordSetupAdapter,
}),
};

View File

@ -1,19 +1,21 @@
import { Separator, TextDisplay } from "@buape/carbon";
import {
buildAccountScopedAllowlistConfigEditor,
buildAccountScopedDmSecurityPolicy,
collectOpenProviderGroupPolicyWarnings,
collectOpenGroupPolicyConfiguredRouteWarnings,
} from "openclaw/plugin-sdk/compat";
resolveLegacyDmAllowlistConfigPaths,
} from "openclaw/plugin-sdk/allowlist-config-edit";
import {
buildAgentSessionKey,
resolveThreadSessionKeys,
type RoutePeer,
} from "openclaw/plugin-sdk/core";
buildAccountScopedDmSecurityPolicy,
collectOpenGroupPolicyConfiguredRouteWarnings,
collectOpenProviderGroupPolicyWarnings,
} from "openclaw/plugin-sdk/channel-config-helpers";
import { resolveOutboundSendDep } from "openclaw/plugin-sdk/channel-runtime";
import { normalizeMessageChannel } from "openclaw/plugin-sdk/channel-runtime";
import { buildOutboundBaseSessionKey, normalizeOutboundThreadId } from "openclaw/plugin-sdk/core";
import {
buildComputedAccountStatusSnapshot,
buildTokenChannelStatusSummary,
DEFAULT_ACCOUNT_ID,
getChatChannelMeta,
listDiscordDirectoryGroupsFromConfig,
listDiscordDirectoryPeersFromConfig,
PAIRING_APPROVED_MESSAGE,
@ -25,9 +27,7 @@ import {
type ChannelPlugin,
type OpenClawConfig,
} from "openclaw/plugin-sdk/discord";
import { resolveOutboundSendDep } from "../../../src/infra/outbound/send-deps.js";
import { normalizeOutboundThreadId } from "../../../src/infra/outbound/thread-id.js";
import { normalizeMessageChannel } from "../../../src/utils/message-channel.js";
import { resolveThreadSessionKeys, type RoutePeer } from "openclaw/plugin-sdk/routing";
import {
listDiscordAccountIds,
resolveDiscordAccount,
@ -57,6 +57,7 @@ type DiscordSendFn = ReturnType<
typeof getDiscordRuntime
>["channel"]["discord"]["sendMessageDiscord"];
const meta = getChatChannelMeta("discord");
const REQUIRED_DISCORD_PERMISSIONS = ["ViewChannel", "SendMessages"] as const;
function formatDiscordIntents(intents?: {
@ -203,14 +204,7 @@ function buildDiscordBaseSessionKey(params: {
accountId?: string | null;
peer: RoutePeer;
}) {
return buildAgentSessionKey({
agentId: params.agentId,
channel: "discord",
accountId: params.accountId,
peer: params.peer,
dmScope: params.cfg.session?.dmScope ?? "main",
identityLinks: params.cfg.session?.identityLinks,
});
return buildOutboundBaseSessionKey({ ...params, channel: "discord" });
}
function resolveDiscordOutboundTargetKindHint(params: {
@ -303,14 +297,7 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount> = {
channelId: "discord",
normalize: ({ cfg, accountId, values }) =>
discordConfigAccessors.formatAllowFrom!({ cfg, accountId, allowFrom: values }),
resolvePaths: (scope) =>
scope === "dm"
? {
readPaths: [["allowFrom"], ["dm", "allowFrom"]],
writePath: ["allowFrom"],
cleanupPaths: [["dm", "allowFrom"]],
}
: null,
resolvePaths: resolveLegacyDmAllowlistConfigPaths,
}),
},
security: {

View File

@ -1,5 +1,8 @@
import { describe, expect, it } from "vitest";
import { countLines, hasBalancedFences } from "../../../src/test-utils/chunk-test-helpers.js";
import {
countLines,
hasBalancedFences,
} from "../../../test/helpers/extensions/chunk-test-helpers.js";
import { chunkDiscordText, chunkDiscordTextWithMode } from "./chunk.js";
describe("chunkDiscordText", () => {

View File

@ -1,4 +1,4 @@
import { chunkMarkdownTextWithMode, type ChunkMode } from "../../../src/auto-reply/chunk.js";
import { chunkMarkdownTextWithMode, type ChunkMode } from "openclaw/plugin-sdk/reply-runtime";
export type ChunkDiscordTextOpts = {
/** Max characters per Discord message. Default: 2000. */

View File

@ -1,8 +1,8 @@
import { RequestClient } from "@buape/carbon";
import { loadConfig } from "../../../src/config/config.js";
import { createDiscordRetryRunner, type RetryRunner } from "../../../src/infra/retry-policy.js";
import type { RetryConfig } from "../../../src/infra/retry.js";
import { normalizeAccountId } from "../../../src/routing/session-key.js";
import { loadConfig } from "openclaw/plugin-sdk/config-runtime";
import { createDiscordRetryRunner, type RetryRunner } from "openclaw/plugin-sdk/infra-runtime";
import type { RetryConfig } from "openclaw/plugin-sdk/infra-runtime";
import { normalizeAccountId } from "openclaw/plugin-sdk/routing";
import {
mergeDiscordAccountConfig,
resolveDiscordAccount,

View File

@ -1,4 +1,4 @@
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../../src/routing/account-id.js";
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/routing";
const DISCORD_DIRECTORY_CACHE_MAX_ENTRIES = 4000;
const DISCORD_DISCRIMINATOR_SUFFIX = /#\d{4}$/;

View File

@ -1,5 +1,5 @@
import type { DirectoryConfigParams } from "../../../src/channels/plugins/directory-config.js";
import type { ChannelDirectoryEntry } from "../../../src/channels/plugins/types.js";
import type { DirectoryConfigParams } from "openclaw/plugin-sdk/channel-runtime";
import type { ChannelDirectoryEntry } from "openclaw/plugin-sdk/channel-runtime";
import { resolveDiscordAccount } from "./accounts.js";
import { fetchDiscord } from "./api.js";
import { rememberDiscordDirectoryUser } from "./directory-cache.js";

View File

@ -1,7 +1,7 @@
import { resolveTextChunkLimit } from "../../../src/auto-reply/chunk.js";
import { type OpenClawConfig } from "../../../src/config/config.js";
import { resolveAccountEntry } from "../../../src/routing/account-lookup.js";
import { normalizeAccountId } from "../../../src/routing/session-key.js";
import { type OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
import { resolveTextChunkLimit } from "openclaw/plugin-sdk/reply-runtime";
import { resolveAccountEntry } from "openclaw/plugin-sdk/routing";
import { normalizeAccountId } from "openclaw/plugin-sdk/routing";
import { DISCORD_TEXT_CHUNK_LIMIT } from "./outbound-adapter.js";
const DEFAULT_DISCORD_DRAFT_STREAM_MIN = 200;

View File

@ -1,6 +1,6 @@
import type { RequestClient } from "@buape/carbon";
import { Routes } from "discord-api-types/v10";
import { createFinalizableDraftLifecycle } from "../../../src/channels/draft-stream-controls.js";
import { createFinalizableDraftLifecycle } from "openclaw/plugin-sdk/channel-runtime";
/** Discord messages cap at 2000 characters. */
const DISCORD_STREAM_MAX_CHARS = 2000;

View File

@ -1,6 +1,6 @@
import type { ReplyPayload } from "../../../src/auto-reply/types.js";
import type { OpenClawConfig } from "../../../src/config/config.js";
import { getExecApprovalReplyMetadata } from "../../../src/infra/exec-approval-reply.js";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
import { getExecApprovalReplyMetadata } from "openclaw/plugin-sdk/infra-runtime";
import type { ReplyPayload } from "openclaw/plugin-sdk/reply-runtime";
import { resolveDiscordAccount } from "./accounts.js";
export function isDiscordExecApprovalClientEnabled(params: {

View File

@ -1,6 +1,6 @@
import type { EventEmitter } from "node:events";
import { logVerbose } from "../../../src/globals.js";
import type { RuntimeEnv } from "../../../src/runtime.js";
import { logVerbose } from "openclaw/plugin-sdk/runtime-env";
import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env";
type GatewayEmitter = Pick<EventEmitter, "on" | "removeListener">;

View File

@ -1,6 +1,6 @@
import { ChannelType, type Guild } from "@buape/carbon";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { typedCases } from "../../../src/test-utils/typed-cases.js";
import { typedCases } from "../../../test/helpers/extensions/typed-cases.js";
import {
allowListMatches,
buildDiscordMediaPayload,

View File

@ -1,5 +1,5 @@
import type { MockFn } from "openclaw/plugin-sdk/testing";
import { vi } from "vitest";
import type { MockFn } from "../../../src/test-utils/vitest-mock-fn.js";
export const sendMock: MockFn = vi.fn();
export const reactMock: MockFn = vi.fn();
@ -15,8 +15,8 @@ vi.mock("./send.js", () => ({
},
}));
vi.mock("../../../src/auto-reply/dispatch.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../../../src/auto-reply/dispatch.js")>();
vi.mock("openclaw/plugin-sdk/reply-runtime", async (importOriginal) => {
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/reply-runtime")>();
return {
...actual,
dispatchInboundMessage: (...args: unknown[]) => dispatchMock(...args),
@ -36,10 +36,10 @@ function createPairingStoreMocks() {
};
}
vi.mock("../../../src/pairing/pairing-store.js", () => createPairingStoreMocks());
vi.mock("openclaw/plugin-sdk/conversation-runtime", () => createPairingStoreMocks());
vi.mock("../../../src/config/sessions.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../../../src/config/sessions.js")>();
vi.mock("openclaw/plugin-sdk/config-runtime", async (importOriginal) => {
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/config-runtime")>();
return {
...actual,
resolveStorePath: vi.fn(() => "/tmp/openclaw-sessions.json"),

View File

@ -18,41 +18,41 @@ import {
} from "@buape/carbon";
import type { APIStringSelectComponent } from "discord-api-types/v10";
import { ButtonStyle, ChannelType } from "discord-api-types/v10";
import { resolveHumanDelayConfig } from "../../../../src/agents/identity.js";
import { resolveChunkMode, resolveTextChunkLimit } from "../../../../src/auto-reply/chunk.js";
import {
formatInboundEnvelope,
resolveEnvelopeFormatOptions,
} from "../../../../src/auto-reply/envelope.js";
import { finalizeInboundContext } from "../../../../src/auto-reply/reply/inbound-context.js";
import { dispatchReplyWithBufferedBlockDispatcher } from "../../../../src/auto-reply/reply/provider-dispatcher.js";
import { createReplyReferencePlanner } from "../../../../src/auto-reply/reply/reply-reference.js";
import { resolveCommandAuthorizedFromAuthorizers } from "../../../../src/channels/command-gating.js";
import { createReplyPrefixOptions } from "../../../../src/channels/reply-prefix.js";
import { recordInboundSession } from "../../../../src/channels/session.js";
import type { OpenClawConfig } from "../../../../src/config/config.js";
import { isDangerousNameMatchingEnabled } from "../../../../src/config/dangerous-name-matching.js";
import { resolveMarkdownTableMode } from "../../../../src/config/markdown-tables.js";
import { readSessionUpdatedAt, resolveStorePath } from "../../../../src/config/sessions.js";
import type { DiscordAccountConfig } from "../../../../src/config/types.discord.js";
import { logVerbose } from "../../../../src/globals.js";
import { enqueueSystemEvent } from "../../../../src/infra/system-events.js";
import { logDebug, logError } from "../../../../src/logger.js";
import { getAgentScopedMediaLocalRoots } from "../../../../src/media/local-roots.js";
import { issuePairingChallenge } from "../../../../src/pairing/pairing-challenge.js";
import { upsertChannelPairingRequest } from "../../../../src/pairing/pairing-store.js";
import { resolveHumanDelayConfig } from "openclaw/plugin-sdk/agent-runtime";
import { resolveCommandAuthorizedFromAuthorizers } from "openclaw/plugin-sdk/channel-runtime";
import { createReplyPrefixOptions } from "openclaw/plugin-sdk/channel-runtime";
import { recordInboundSession } from "openclaw/plugin-sdk/channel-runtime";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
import { isDangerousNameMatchingEnabled } from "openclaw/plugin-sdk/config-runtime";
import { resolveMarkdownTableMode } from "openclaw/plugin-sdk/config-runtime";
import { readSessionUpdatedAt, resolveStorePath } from "openclaw/plugin-sdk/config-runtime";
import type { DiscordAccountConfig } from "openclaw/plugin-sdk/config-runtime";
import { issuePairingChallenge } from "openclaw/plugin-sdk/conversation-runtime";
import { upsertChannelPairingRequest } from "openclaw/plugin-sdk/conversation-runtime";
import {
buildPluginBindingResolvedText,
parsePluginBindingApprovalCustomId,
resolvePluginConversationBindingApproval,
} from "../../../../src/plugins/conversation-binding.js";
import { dispatchPluginInteractiveHandler } from "../../../../src/plugins/interactive.js";
import { resolveAgentRoute } from "../../../../src/routing/resolve-route.js";
import { createNonExitingRuntime, type RuntimeEnv } from "../../../../src/runtime.js";
} from "openclaw/plugin-sdk/conversation-runtime";
import { enqueueSystemEvent } from "openclaw/plugin-sdk/infra-runtime";
import { getAgentScopedMediaLocalRoots } from "openclaw/plugin-sdk/media-runtime";
import { dispatchPluginInteractiveHandler } from "openclaw/plugin-sdk/plugin-runtime";
import { resolveChunkMode, resolveTextChunkLimit } from "openclaw/plugin-sdk/reply-runtime";
import {
formatInboundEnvelope,
resolveEnvelopeFormatOptions,
} from "openclaw/plugin-sdk/reply-runtime";
import { finalizeInboundContext } from "openclaw/plugin-sdk/reply-runtime";
import { dispatchReplyWithBufferedBlockDispatcher } from "openclaw/plugin-sdk/reply-runtime";
import { createReplyReferencePlanner } from "openclaw/plugin-sdk/reply-runtime";
import { resolveAgentRoute } from "openclaw/plugin-sdk/routing";
import { logVerbose } from "openclaw/plugin-sdk/runtime-env";
import { createNonExitingRuntime, type RuntimeEnv } from "openclaw/plugin-sdk/runtime-env";
import {
readStoreAllowFromForDmPolicy,
resolvePinnedMainDmOwnerFromAllowlist,
} from "../../../../src/security/dm-policy-shared.js";
} from "openclaw/plugin-sdk/security-runtime";
import { logDebug, logError } from "openclaw/plugin-sdk/text-runtime";
import { resolveDiscordMaxLinesPerMessage } from "../accounts.js";
import { resolveDiscordComponentEntry, resolveDiscordModalEntry } from "../components-registry.js";
import {

View File

@ -1,12 +1,12 @@
import type { Guild, User } from "@buape/carbon";
import { evaluateGroupRouteAccessForPolicy } from "openclaw/plugin-sdk/group-access";
import type { AllowlistMatch } from "../../../../src/channels/allowlist-match.js";
import type { AllowlistMatch } from "openclaw/plugin-sdk/channel-runtime";
import {
buildChannelKeyCandidates,
resolveChannelEntryMatchWithFallback,
resolveChannelMatchConfig,
type ChannelMatchSource,
} from "../../../../src/channels/channel-config.js";
} from "openclaw/plugin-sdk/channel-runtime";
import { evaluateGroupRouteAccessForPolicy } from "openclaw/plugin-sdk/group-access";
import { formatDiscordUserTag } from "./format.js";
export type DiscordAllowList = {

View File

@ -6,12 +6,12 @@ import {
resolveProfilesUnavailableReason,
type AuthProfileFailureReason,
type AuthProfileStore,
} from "../../../../src/agents/auth-profiles.js";
} from "openclaw/plugin-sdk/agent-runtime";
import type {
DiscordAccountConfig,
DiscordAutoPresenceConfig,
} from "../../../../src/config/config.js";
import { warn } from "../../../../src/globals.js";
} from "openclaw/plugin-sdk/config-runtime";
import { warn } from "openclaw/plugin-sdk/runtime-env";
import { resolveDiscordPresenceUpdate } from "./presence.js";
const DEFAULT_CUSTOM_ACTIVITY_TYPE = 4;

View File

@ -1,4 +1,4 @@
import type { DiscordSlashCommandConfig } from "../../../../src/config/types.discord.js";
import type { DiscordSlashCommandConfig } from "openclaw/plugin-sdk/config-runtime";
export function resolveDiscordSlashCommandConfig(
raw?: DiscordSlashCommandConfig,

View File

@ -1,9 +1,9 @@
import { resolveCommandAuthorizedFromAuthorizers } from "../../../../src/channels/command-gating.js";
import { resolveCommandAuthorizedFromAuthorizers } from "openclaw/plugin-sdk/channel-runtime";
import {
readStoreAllowFromForDmPolicy,
resolveDmGroupAccessWithLists,
type DmGroupAccessDecision,
} from "../../../../src/security/dm-policy-shared.js";
} from "openclaw/plugin-sdk/security-runtime";
import { normalizeDiscordAllowList, resolveDiscordAllowListMatch } from "./allow-list.js";
const DISCORD_ALLOW_LIST_PREFIXES = ["discord:", "user:", "pk:"];

View File

@ -1,5 +1,5 @@
import { issuePairingChallenge } from "../../../../src/pairing/pairing-challenge.js";
import { upsertChannelPairingRequest } from "../../../../src/pairing/pairing-store.js";
import { issuePairingChallenge } from "openclaw/plugin-sdk/conversation-runtime";
import { upsertChannelPairingRequest } from "openclaw/plugin-sdk/conversation-runtime";
import type { DiscordDmCommandAccess } from "./dm-command-auth.js";
export async function handleDiscordDmCommandDecision(params: {

View File

@ -10,30 +10,24 @@ import {
type TopLevelComponents,
} from "@buape/carbon";
import { ButtonStyle, Routes } from "discord-api-types/v10";
import type { OpenClawConfig } from "../../../../src/config/config.js";
import { loadSessionStore, resolveStorePath } from "../../../../src/config/sessions.js";
import type { DiscordExecApprovalConfig } from "../../../../src/config/types.discord.js";
import { GatewayClient } from "../../../../src/gateway/client.js";
import { createOperatorApprovalsGatewayClient } from "../../../../src/gateway/operator-approvals-client.js";
import type { EventFrame } from "../../../../src/gateway/protocol/index.js";
import { resolveExecApprovalCommandDisplay } from "../../../../src/infra/exec-approval-command-display.js";
import { getExecApprovalApproverDmNoticeText } from "../../../../src/infra/exec-approval-reply.js";
import { normalizeMessageChannel } from "openclaw/plugin-sdk/channel-runtime";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
import { loadSessionStore, resolveStorePath } from "openclaw/plugin-sdk/config-runtime";
import type { DiscordExecApprovalConfig } from "openclaw/plugin-sdk/config-runtime";
import { GatewayClient } from "openclaw/plugin-sdk/gateway-runtime";
import { createOperatorApprovalsGatewayClient } from "openclaw/plugin-sdk/gateway-runtime";
import type { EventFrame } from "openclaw/plugin-sdk/gateway-runtime";
import { resolveExecApprovalCommandDisplay } from "openclaw/plugin-sdk/infra-runtime";
import { getExecApprovalApproverDmNoticeText } from "openclaw/plugin-sdk/infra-runtime";
import type {
ExecApprovalDecision,
ExecApprovalRequest,
ExecApprovalResolved,
} from "../../../../src/infra/exec-approvals.js";
import { logDebug, logError } from "../../../../src/logger.js";
import {
normalizeAccountId,
resolveAgentIdFromSessionKey,
} from "../../../../src/routing/session-key.js";
import type { RuntimeEnv } from "../../../../src/runtime.js";
import {
compileSafeRegex,
testRegexWithBoundedInput,
} from "../../../../src/security/safe-regex.js";
import { normalizeMessageChannel } from "../../../../src/utils/message-channel.js";
} from "openclaw/plugin-sdk/infra-runtime";
import { normalizeAccountId, resolveAgentIdFromSessionKey } from "openclaw/plugin-sdk/routing";
import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env";
import { compileSafeRegex, testRegexWithBoundedInput } from "openclaw/plugin-sdk/security-runtime";
import { logDebug, logError } from "openclaw/plugin-sdk/text-runtime";
import { createDiscordClient, stripUndefinedFields } from "../send.shared.js";
import { DiscordUiContainer } from "../ui.js";

View File

@ -1,11 +1,11 @@
import { GatewayIntents, GatewayPlugin } from "@buape/carbon/gateway";
import type { APIGatewayBotInfo } from "discord-api-types/v10";
import { HttpsProxyAgent } from "https-proxy-agent";
import type { DiscordAccountConfig } from "openclaw/plugin-sdk/config-runtime";
import { danger } from "openclaw/plugin-sdk/runtime-env";
import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env";
import { ProxyAgent, fetch as undiciFetch } from "undici";
import WebSocket from "ws";
import type { DiscordAccountConfig } from "../../../../src/config/types.js";
import { danger } from "../../../../src/globals.js";
import type { RuntimeEnv } from "../../../../src/runtime.js";
const DISCORD_GATEWAY_BOT_URL = "https://discord.com/api/v10/gateway/bot";
const DEFAULT_DISCORD_GATEWAY_URL = "wss://gateway.discord.gg/";
@ -20,7 +20,7 @@ type DiscordGatewayFetch = (
) => Promise<DiscordGatewayMetadataResponse>;
export function resolveDiscordGatewayIntents(
intentsConfig?: import("../../../../src/config/types.discord.js").DiscordIntentsConfig,
intentsConfig?: import("openclaw/plugin-sdk/config-runtime").DiscordIntentsConfig,
): number {
let intents =
GatewayIntents.Guilds |

View File

@ -1,4 +1,4 @@
import { buildUntrustedChannelMetadata } from "../../../../src/security/channel-metadata.js";
import { buildUntrustedChannelMetadata } from "openclaw/plugin-sdk/security-runtime";
import {
resolveDiscordOwnerAllowFrom,
type DiscordChannelConfigResolved,

View File

@ -1,7 +1,7 @@
import { createRunStateMachine } from "openclaw/plugin-sdk/channel-runtime";
import { formatDurationSeconds } from "openclaw/plugin-sdk/infra-runtime";
import { KeyedAsyncQueue } from "openclaw/plugin-sdk/keyed-async-queue";
import { createRunStateMachine } from "../../../../src/channels/run-state-machine.js";
import { danger } from "../../../../src/globals.js";
import { formatDurationSeconds } from "../../../../src/infra/format-time/format-duration.ts";
import { danger } from "openclaw/plugin-sdk/runtime-env";
import { materializeDiscordInboundJob, type DiscordInboundJob } from "./inbound-job.js";
import type { RuntimeEnv } from "./message-handler.preflight.types.js";
import { processDiscordMessage } from "./message-handler.process.js";

View File

@ -8,16 +8,16 @@ import {
ThreadUpdateListener,
type User,
} from "@buape/carbon";
import type { OpenClawConfig } from "../../../../src/config/config.js";
import { danger, logVerbose } from "../../../../src/globals.js";
import { formatDurationSeconds } from "../../../../src/infra/format-time/format-duration.ts";
import { enqueueSystemEvent } from "../../../../src/infra/system-events.js";
import { createSubsystemLogger } from "../../../../src/logging/subsystem.js";
import { resolveAgentRoute } from "../../../../src/routing/resolve-route.js";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
import { formatDurationSeconds } from "openclaw/plugin-sdk/infra-runtime";
import { enqueueSystemEvent } from "openclaw/plugin-sdk/infra-runtime";
import { resolveAgentRoute } from "openclaw/plugin-sdk/routing";
import { danger, logVerbose } from "openclaw/plugin-sdk/runtime-env";
import { createSubsystemLogger } from "openclaw/plugin-sdk/runtime-env";
import {
readStoreAllowFromForDmPolicy,
resolveDmGroupAccessWithLists,
} from "../../../../src/security/dm-policy-shared.js";
} from "openclaw/plugin-sdk/security-runtime";
import {
isDiscordGroupAllowedByPolicy,
normalizeDiscordAllowList,
@ -36,11 +36,9 @@ import { isThreadArchived } from "./thread-bindings.discord-api.js";
import { closeDiscordThreadSessions } from "./thread-session-close.js";
import { normalizeDiscordListenerTimeoutMs, runDiscordTaskWithTimeout } from "./timeouts.js";
type LoadedConfig = ReturnType<typeof import("../../../../src/config/config.js").loadConfig>;
type RuntimeEnv = import("../../../../src/runtime.js").RuntimeEnv;
type Logger = ReturnType<
typeof import("../../../../src/logging/subsystem.js").createSubsystemLogger
>;
type LoadedConfig = ReturnType<typeof import("openclaw/plugin-sdk/config-runtime").loadConfig>;
type RuntimeEnv = import("openclaw/plugin-sdk/runtime-env").RuntimeEnv;
type Logger = ReturnType<typeof import("openclaw/plugin-sdk/runtime-env").createSubsystemLogger>;
export type DiscordMessageEvent = Parameters<MessageCreateListener["handle"]>[0];

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