Merge branch 'main' into fix/token-usage-input-output-breakdown
This commit is contained in:
commit
26e9c84e7a
32
.github/workflows/ci.yml
vendored
32
.github/workflows/ci.yml
vendored
@ -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
1
.gitignore
vendored
@ -9,6 +9,7 @@ pnpm-lock.yaml
|
||||
bun.lock
|
||||
bun.lockb
|
||||
coverage
|
||||
__openclaw_vitest__/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.tsbuildinfo
|
||||
|
||||
20
CHANGELOG.md
20
CHANGELOG.md
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"))
|
||||
}
|
||||
}
|
||||
@ -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"])
|
||||
}
|
||||
}
|
||||
@ -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()
|
||||
|
||||
@ -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"])
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
},
|
||||
{
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -47,6 +47,10 @@
|
||||
"source": "Quick Start",
|
||||
"target": "快速开始"
|
||||
},
|
||||
{
|
||||
"source": "Capability Cookbook",
|
||||
"target": "能力扩展手册"
|
||||
},
|
||||
{
|
||||
"source": "Setup Wizard Reference",
|
||||
"target": "设置向导参考"
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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.
|
||||
|
||||
250
docs/concepts/context-engine.md
Normal file
250
docs/concepts/context-engine.md
Normal 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).
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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 can’t 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`)
|
||||
|
||||
|
||||
@ -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"
|
||||
]
|
||||
|
||||
@ -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" },
|
||||
|
||||
@ -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))
|
||||
|
||||
|
||||
@ -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",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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.
|
||||
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
|
||||
|
||||
112
docs/tools/capability-cookbook.md
Normal file
112
docs/tools/capability-cookbook.md
Normal 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.
|
||||
@ -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.
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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`.
|
||||
|
||||
|
||||
@ -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).
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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", () => {
|
||||
|
||||
@ -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;
|
||||
});
|
||||
|
||||
@ -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;
|
||||
});
|
||||
|
||||
@ -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,
|
||||
};
|
||||
|
||||
@ -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,
|
||||
});
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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,
|
||||
};
|
||||
|
||||
@ -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";
|
||||
|
||||
|
||||
@ -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,
|
||||
};
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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";
|
||||
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 };
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
});
|
||||
|
||||
@ -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;
|
||||
});
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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;
|
||||
});
|
||||
|
||||
@ -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 };
|
||||
|
||||
|
||||
@ -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;
|
||||
});
|
||||
|
||||
@ -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
|
||||
? [
|
||||
"I’ll 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
|
||||
? [
|
||||
"I’ll 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
|
||||
? [
|
||||
"I’ll 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
|
||||
? [
|
||||
"I’ll 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),
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@ -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;
|
||||
});
|
||||
|
||||
@ -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", () => {
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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,
|
||||
});
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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";
|
||||
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -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";
|
||||
|
||||
|
||||
@ -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";
|
||||
|
||||
|
||||
@ -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 };
|
||||
|
||||
@ -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,
|
||||
}),
|
||||
};
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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", () => {
|
||||
|
||||
@ -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. */
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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}$/;
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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">;
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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"),
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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:"];
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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";
|
||||
|
||||
|
||||
@ -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 |
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { buildUntrustedChannelMetadata } from "../../../../src/security/channel-metadata.js";
|
||||
import { buildUntrustedChannelMetadata } from "openclaw/plugin-sdk/security-runtime";
|
||||
import {
|
||||
resolveDiscordOwnerAllowFrom,
|
||||
type DiscordChannelConfigResolved,
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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
Loading…
x
Reference in New Issue
Block a user