Compare commits

..

1 Commits

Author SHA1 Message Date
Vincent Koc
a60b552a2b browser: include cdp range in sandbox resolved config 2026-03-01 23:30:15 -08:00
45 changed files with 267 additions and 501 deletions

View File

@@ -40,7 +40,6 @@ Docs: https://docs.openclaw.ai
### Fixes
- Pairing/AllowFrom account fallback: handle omitted `accountId` values in `readChannelAllowFromStore` and `readChannelAllowFromStoreSync` as `default`, while preserving legacy unscoped allowFrom merges for default-account flows. Thanks @Sid-Qin and @vincentkoc.
- Agents/Subagent announce cleanup: keep completion-message runs pending while descendants settle, add a 30 minute hard-expiry backstop to avoid indefinite pending state, and keep retry bookkeeping resumable across deferred wakes. (#23970) Thanks @tyler6204.
- BlueBubbles/Message metadata: harden send response ID extraction, include sender identity in DM context, and normalize inbound `message_id` selection to avoid duplicate ID metadata. (#23970) Thanks @tyler6204.
- Gateway/Control UI method guard: allow POST requests to non-UI routes to fall through when no base path is configured, and add POST regression coverage for fallthrough and base-path 405 behavior. (#23970) Thanks @tyler6204.

View File

@@ -439,4 +439,4 @@ Planned features:
- [Multi-Agent Configuration](/tools/multi-agent-sandbox-tools)
- [Routing Configuration](/channels/channel-routing)
- [Session Management](/concepts/session)
- [Session Management](/concepts/sessions)

View File

@@ -13,28 +13,28 @@ Text is supported everywhere; media and reactions vary by channel.
## Supported channels
- [BlueBubbles](/channels/bluebubbles) — **Recommended for iMessage**; uses the BlueBubbles macOS server REST API with full feature support (edit, unsend, effects, reactions, group management — edit currently broken on macOS 26 Tahoe).
- [WhatsApp](/channels/whatsapp) — Most popular; uses Baileys and requires QR pairing.
- [Telegram](/channels/telegram) — Bot API via grammY; supports groups.
- [Discord](/channels/discord) — Discord Bot API + Gateway; supports servers, channels, and DMs.
- [IRC](/channels/irc) — Classic IRC servers; channels + DMs with pairing/allowlist controls.
- [Slack](/channels/slack) — Bolt SDK; workspace apps.
- [Feishu](/channels/feishu) — Feishu/Lark bot via WebSocket (plugin, installed separately).
- [Google Chat](/channels/googlechat) — Google Chat API app via HTTP webhook.
- [iMessage (legacy)](/channels/imessage) — Legacy macOS integration via imsg CLI (deprecated, use BlueBubbles for new setups).
- [IRC](/channels/irc) — Classic IRC servers; channels + DMs with pairing/allowlist controls.
- [LINE](/channels/line) — LINE Messaging API bot (plugin, installed separately).
- [Matrix](/channels/matrix) — Matrix protocol (plugin, installed separately).
- [Mattermost](/channels/mattermost) — Bot API + WebSocket; channels, groups, DMs (plugin, installed separately).
- [Microsoft Teams](/channels/msteams) — Bot Framework; enterprise support (plugin, installed separately).
- [Nextcloud Talk](/channels/nextcloud-talk) — Self-hosted chat via Nextcloud Talk (plugin, installed separately).
- [Nostr](/channels/nostr) — Decentralized DMs via NIP-04 (plugin, installed separately).
- [Signal](/channels/signal) — signal-cli; privacy-focused.
- [BlueBubbles](/channels/bluebubbles) — **Recommended for iMessage**; uses the BlueBubbles macOS server REST API with full feature support (edit, unsend, effects, reactions, group management — edit currently broken on macOS 26 Tahoe).
- [iMessage (legacy)](/channels/imessage) — Legacy macOS integration via imsg CLI (deprecated, use BlueBubbles for new setups).
- [Microsoft Teams](/channels/msteams) — Bot Framework; enterprise support (plugin, installed separately).
- [Synology Chat](/channels/synology-chat) — Synology NAS Chat via outgoing+incoming webhooks (plugin, installed separately).
- [Slack](/channels/slack) — Bolt SDK; workspace apps.
- [Telegram](/channels/telegram) — Bot API via grammY; supports groups.
- [LINE](/channels/line) — LINE Messaging API bot (plugin, installed separately).
- [Nextcloud Talk](/channels/nextcloud-talk) — Self-hosted chat via Nextcloud Talk (plugin, installed separately).
- [Matrix](/channels/matrix) — Matrix protocol (plugin, installed separately).
- [Nostr](/channels/nostr) — Decentralized DMs via NIP-04 (plugin, installed separately).
- [Tlon](/channels/tlon) — Urbit-based messenger (plugin, installed separately).
- [Twitch](/channels/twitch) — Twitch chat via IRC connection (plugin, installed separately).
- [WebChat](/web/webchat) — Gateway WebChat UI over WebSocket.
- [WhatsApp](/channels/whatsapp) — Most popular; uses Baileys and requires QR pairing.
- [Zalo](/channels/zalo) — Zalo Bot API; Vietnam's popular messenger (plugin, installed separately).
- [Zalo Personal](/channels/zalouser) — Zalo personal account via QR login (plugin, installed separately).
- [WebChat](/web/webchat) — Gateway WebChat UI over WebSocket.
## Notes

10
docs/concepts/sessions.md Normal file
View File

@@ -0,0 +1,10 @@
---
summary: "Alias for session management docs"
read_when:
- You looked for docs/concepts/sessions.md; canonical doc lives in docs/concepts/session.md
title: "Sessions"
---
# Sessions
Canonical session management docs live in [Session management](/concepts/session).

View File

@@ -597,7 +597,7 @@
},
{
"source": "/sessions",
"destination": "/concepts/session"
"destination": "/concepts/sessions"
},
{
"source": "/setup",
@@ -832,6 +832,7 @@
"group": "First steps",
"pages": [
"start/getting-started",
"start/quickstart",
"start/onboarding-overview",
"start/wizard",
"start/onboarding"
@@ -898,25 +899,25 @@
{
"group": "Messaging platforms",
"pages": [
"channels/bluebubbles",
"channels/whatsapp",
"channels/telegram",
"channels/discord",
"channels/irc",
"channels/slack",
"channels/feishu",
"channels/googlechat",
"channels/mattermost",
"channels/signal",
"channels/imessage",
"channels/irc",
"channels/bluebubbles",
"channels/msteams",
"channels/synology-chat",
"channels/line",
"channels/matrix",
"channels/mattermost",
"channels/msteams",
"channels/nextcloud-talk",
"channels/nostr",
"channels/signal",
"channels/synology-chat",
"channels/slack",
"channels/telegram",
"channels/tlon",
"channels/twitch",
"channels/whatsapp",
"channels/zalo",
"channels/zalouser"
]
@@ -959,6 +960,7 @@
"group": "Sessions and memory",
"pages": [
"concepts/session",
"concepts/sessions",
"concepts/session-pruning",
"concepts/session-tool",
"concepts/memory",
@@ -990,20 +992,20 @@
{
"group": "Built-in tools",
"pages": [
"tools/apply-patch",
"brave-search",
"perplexity",
"tools/lobster",
"tools/llm-task",
"tools/diffs",
"tools/elevated",
"tools/exec",
"tools/exec-approvals",
"tools/firecrawl",
"tools/llm-task",
"tools/lobster",
"tools/loop-detection",
"tools/reactions",
"tools/web",
"tools/apply-patch",
"tools/elevated",
"tools/thinking",
"tools/web"
"tools/reactions"
]
},
{
@@ -1095,7 +1097,8 @@
"group": "Providers",
"pages": [
"providers/anthropic",
"providers/bedrock",
"providers/openai",
"providers/openrouter",
"providers/cloudflare-ai-gateway",
"providers/claude-max-api-proxy",
"providers/deepgram",
@@ -1103,24 +1106,23 @@
"providers/huggingface",
"providers/kilocode",
"providers/litellm",
"providers/glm",
"providers/minimax",
"providers/bedrock",
"providers/vercel-ai-gateway",
"providers/moonshot",
"providers/mistral",
"providers/minimax",
"providers/nvidia",
"providers/ollama",
"providers/openai",
"providers/opencode",
"providers/openrouter",
"providers/qianfan",
"providers/qwen",
"providers/synthetic",
"providers/together",
"providers/vercel-ai-gateway",
"providers/venice",
"providers/vllm",
"providers/xiaomi",
"providers/zai"
"providers/glm",
"providers/zai",
"providers/synthetic",
"providers/qianfan"
]
}
]
@@ -1430,6 +1432,7 @@
"group": "第一步",
"pages": [
"zh-CN/start/getting-started",
"zh-CN/start/quickstart",
"zh-CN/start/wizard",
"zh-CN/start/onboarding"
]
@@ -1494,24 +1497,24 @@
{
"group": "消息平台",
"pages": [
"zh-CN/channels/bluebubbles",
"zh-CN/channels/discord",
"zh-CN/channels/feishu",
"zh-CN/channels/whatsapp",
"zh-CN/channels/telegram",
"zh-CN/channels/grammy",
"zh-CN/channels/discord",
"zh-CN/channels/slack",
"zh-CN/channels/feishu",
"zh-CN/channels/googlechat",
"zh-CN/channels/mattermost",
"zh-CN/channels/signal",
"zh-CN/channels/imessage",
"zh-CN/channels/bluebubbles",
"zh-CN/channels/nextcloud-talk",
"zh-CN/channels/msteams",
"zh-CN/channels/line",
"zh-CN/channels/matrix",
"zh-CN/channels/mattermost",
"zh-CN/channels/msteams",
"zh-CN/channels/nextcloud-talk",
"zh-CN/channels/nostr",
"zh-CN/channels/signal",
"zh-CN/channels/slack",
"zh-CN/channels/telegram",
"zh-CN/channels/tlon",
"zh-CN/channels/twitch",
"zh-CN/channels/whatsapp",
"zh-CN/channels/zalo",
"zh-CN/channels/zalouser"
]
@@ -1554,6 +1557,7 @@
"group": "会话与记忆",
"pages": [
"zh-CN/concepts/session",
"zh-CN/concepts/sessions",
"zh-CN/concepts/session-pruning",
"zh-CN/concepts/session-tool",
"zh-CN/concepts/memory",
@@ -1585,19 +1589,18 @@
{
"group": "内置工具",
"pages": [
"zh-CN/tools/apply-patch",
"zh-CN/brave-search",
"zh-CN/perplexity",
"zh-CN/tools/diffs",
"zh-CN/tools/elevated",
"zh-CN/tools/lobster",
"zh-CN/tools/llm-task",
"zh-CN/tools/exec",
"zh-CN/tools/exec-approvals",
"zh-CN/tools/firecrawl",
"zh-CN/tools/llm-task",
"zh-CN/tools/lobster",
"zh-CN/tools/reactions",
"zh-CN/tools/web",
"zh-CN/tools/apply-patch",
"zh-CN/tools/elevated",
"zh-CN/tools/thinking",
"zh-CN/tools/web"
"zh-CN/tools/reactions"
]
},
{
@@ -1687,24 +1690,24 @@
"group": "提供商",
"pages": [
"zh-CN/providers/anthropic",
"zh-CN/providers/openai",
"zh-CN/providers/openrouter",
"zh-CN/providers/bedrock",
"zh-CN/providers/vercel-ai-gateway",
"zh-CN/providers/claude-max-api-proxy",
"zh-CN/providers/deepgram",
"zh-CN/providers/github-copilot",
"zh-CN/providers/glm",
"zh-CN/providers/moonshot",
"zh-CN/providers/minimax",
"zh-CN/providers/opencode",
"zh-CN/providers/ollama",
"zh-CN/providers/openai",
"zh-CN/providers/openrouter",
"zh-CN/providers/qianfan",
"zh-CN/providers/opencode",
"zh-CN/providers/qwen",
"zh-CN/providers/synthetic",
"zh-CN/providers/venice",
"zh-CN/providers/vercel-ai-gateway",
"zh-CN/providers/xiaomi",
"zh-CN/providers/zai"
"zh-CN/providers/glm",
"zh-CN/providers/zai",
"zh-CN/providers/synthetic",
"zh-CN/providers/qianfan"
]
}
]

View File

@@ -35,29 +35,28 @@ See [Venice AI](/providers/venice).
## Provider docs
- [Amazon Bedrock](/providers/bedrock)
- [Anthropic (API + Claude Code CLI)](/providers/anthropic)
- [Cloudflare AI Gateway](/providers/cloudflare-ai-gateway)
- [GLM models](/providers/glm)
- [Hugging Face (Inference)](/providers/huggingface)
- [Kilocode](/providers/kilocode)
- [LiteLLM (unified gateway)](/providers/litellm)
- [MiniMax](/providers/minimax)
- [Mistral](/providers/mistral)
- [Moonshot AI (Kimi + Kimi Coding)](/providers/moonshot)
- [NVIDIA](/providers/nvidia)
- [Ollama (local models)](/providers/ollama)
- [OpenAI (API + Codex)](/providers/openai)
- [OpenCode Zen](/providers/opencode)
- [OpenRouter](/providers/openrouter)
- [Qianfan](/providers/qianfan)
- [Anthropic (API + Claude Code CLI)](/providers/anthropic)
- [Qwen (OAuth)](/providers/qwen)
- [Together AI](/providers/together)
- [OpenRouter](/providers/openrouter)
- [LiteLLM (unified gateway)](/providers/litellm)
- [Vercel AI Gateway](/providers/vercel-ai-gateway)
- [Venice (Venice AI, privacy-focused)](/providers/venice)
- [vLLM (local models)](/providers/vllm)
- [Xiaomi](/providers/xiaomi)
- [Together AI](/providers/together)
- [Cloudflare AI Gateway](/providers/cloudflare-ai-gateway)
- [Moonshot AI (Kimi + Kimi Coding)](/providers/moonshot)
- [Mistral](/providers/mistral)
- [OpenCode Zen](/providers/opencode)
- [Amazon Bedrock](/providers/bedrock)
- [Z.AI](/providers/zai)
- [Xiaomi](/providers/xiaomi)
- [GLM models](/providers/glm)
- [MiniMax](/providers/minimax)
- [Venice (Venice AI, privacy-focused)](/providers/venice)
- [Hugging Face (Inference)](/providers/huggingface)
- [Ollama (local models)](/providers/ollama)
- [vLLM (local models)](/providers/vllm)
- [Qianfan](/providers/qianfan)
- [NVIDIA](/providers/nvidia)
## Transcription providers

View File

@@ -50,6 +50,7 @@ Use these hubs to discover every page, including deep dives and reference docs t
- [Multi-agent routing](/concepts/multi-agent)
- [Compaction](/concepts/compaction)
- [Sessions](/concepts/session)
- [Sessions (alias)](/concepts/sessions)
- [Session pruning](/concepts/session-pruning)
- [Session tools](/concepts/session-tool)
- [Queue](/concepts/queue)

View File

@@ -446,4 +446,4 @@ interface OpenClawConfig {
- [多智能体配置](/tools/multi-agent-sandbox-tools)
- [路由配置](/channels/channel-routing)
- [会话管理](/concepts/session)
- [会话管理](/concepts/sessions)

View File

@@ -20,26 +20,26 @@ OpenClaw 可以在你已经使用的任何聊天应用上与你交流。每个
## 支持的渠道
- [BlueBubbles](/channels/bluebubbles) — **推荐用于 iMessage**;使用 BlueBubbles macOS 服务器 REST API功能完整编辑、撤回、特效、回应、群组管理——编辑功能在 macOS 26 Tahoe 上目前不可用)
- [WhatsApp](/channels/whatsapp) — 最受欢迎;使用 Baileys需要二维码配对
- [Telegram](/channels/telegram) — 通过 grammY 使用 Bot API支持群组。
- [Discord](/channels/discord) — Discord Bot API + Gateway支持服务器、频道和私信。
- [Slack](/channels/slack) — Bolt SDK工作区应用。
- [飞书](/channels/feishu) — 飞书Lark机器人插件需单独安装
- [Google Chat](/channels/googlechat) — 通过 HTTP webhook 的 Google Chat API 应用。
- [iMessage旧版](/channels/imessage) — 通过 imsg CLI 的旧版 macOS 集成(已弃用,新设置请使用 BlueBubbles
- [LINE](/channels/line) — LINE Messaging API 机器人(插件,需单独安装)。
- [Matrix](/channels/matrix) — Matrix 协议(插件,需单独安装)。
- [Mattermost](/channels/mattermost) — Bot API + WebSocket频道、群组、私信插件需单独安装
- [Microsoft Teams](/channels/msteams) — Bot Framework企业支持插件需单独安装
- [Nextcloud Talk](/channels/nextcloud-talk) — 通过 Nextcloud Talk 的自托管聊天(插件,需单独安装)。
- [Nostr](/channels/nostr) — 通过 NIP-04 的去中心化私信(插件,需单独安装)。
- [Signal](/channels/signal) — signal-cli注重隐私。
- [Slack](/channels/slack) — Bolt SDK工作区应用
- [Telegram](/channels/telegram) — 通过 grammY 使用 Bot API支持群组
- [BlueBubbles](/channels/bluebubbles) — **推荐用于 iMessage**;使用 BlueBubbles macOS 服务器 REST API功能完整编辑、撤回、特效、回应、群组管理——编辑功能在 macOS 26 Tahoe 上目前不可用)
- [iMessage旧版](/channels/imessage) — 通过 imsg CLI 的旧版 macOS 集成(已弃用,新设置请使用 BlueBubbles
- [Microsoft Teams](/channels/msteams) — Bot Framework企业支持插件需单独安装
- [LINE](/channels/line) — LINE Messaging API 机器人(插件,需单独安装)。
- [Nextcloud Talk](/channels/nextcloud-talk) — 通过 Nextcloud Talk 的自托管聊天(插件,需单独安装)。
- [Matrix](/channels/matrix) — Matrix 协议(插件,需单独安装)。
- [Nostr](/channels/nostr) — 通过 NIP-04 的去中心化私信(插件,需单独安装)。
- [Tlon](/channels/tlon) — 基于 Urbit 的消息应用(插件,需单独安装)。
- [Twitch](/channels/twitch) — 通过 IRC 连接的 Twitch 聊天(插件,需单独安装)。
- [WebChat](/web/webchat) — 基于 WebSocket 的 Gateway 网关 WebChat 界面。
- [WhatsApp](/channels/whatsapp) — 最受欢迎;使用 Baileys需要二维码配对。
- [Zalo](/channels/zalo) — Zalo Bot API越南流行的消息应用插件需单独安装
- [Zalo Personal](/channels/zalouser) — 通过二维码登录的 Zalo 个人账号(插件,需单独安装)。
- [WebChat](/web/webchat) — 基于 WebSocket 的 Gateway 网关 WebChat 界面。
## 注意事项

View File

@@ -0,0 +1,17 @@
---
read_when:
- 你查找了 docs/sessions.md规范文档位于 docs/session.md
summary: 会话管理文档的别名
title: 会话
x-i18n:
generated_at: "2026-02-01T20:23:55Z"
model: claude-opus-4-5
provider: pi
source_hash: 7f1e39c3c07b9bb5cdcda361399cf1ce1226ebae3a797d8f93e734aa6a4d00e2
source_path: concepts/sessions.md
workflow: 14
---
# 会话
规范的会话管理文档位于[会话管理](/concepts/session)。

View File

@@ -41,19 +41,20 @@ Venice 是我们推荐的 Venice AI 设置,用于隐私优先的推理,并
## 提供商文档
- [Amazon Bedrock](/providers/bedrock)
- [OpenAIAPI + Codex](/providers/openai)
- [AnthropicAPI + Claude Code CLI](/providers/anthropic)
- [QwenOAuth](/providers/qwen)
- [OpenRouter](/providers/openrouter)
- [Vercel AI Gateway](/providers/vercel-ai-gateway)
- [Moonshot AIKimi + Kimi Coding](/providers/moonshot)
- [OpenCode Zen](/providers/opencode)
- [Amazon Bedrock](/providers/bedrock)
- [Z.AI](/providers/zai)
- [Xiaomi](/providers/xiaomi)
- [GLM 模型](/providers/glm)
- [MiniMax](/providers/minimax)
- [Moonshot AIKimi + Kimi Coding](/providers/moonshot)
- [Ollama本地模型](/providers/ollama)
- [OpenAIAPI + Codex](/providers/openai)
- [OpenCode Zen](/providers/opencode)
- [OpenRouter](/providers/openrouter)
- [QwenOAuth](/providers/qwen)
- [VeniceVenice AI注重隐私](/providers/venice)
- [Xiaomi](/providers/xiaomi)
- [Z.AI](/providers/zai)
- [Ollama本地模型](/providers/ollama)
## 转录提供商

View File

@@ -53,6 +53,7 @@ x-i18n:
- [多智能体路由](/concepts/multi-agent)
- [压缩](/concepts/compaction)
- [会话](/concepts/session)
- [会话(别名)](/concepts/sessions)
- [会话修剪](/concepts/session-pruning)
- [会话工具](/concepts/session-tool)
- [队列](/concepts/queue)

View File

@@ -44,19 +44,19 @@ function createMergeConfigProvider() {
return {
baseUrl: "https://config.example/v1",
apiKey: "CONFIG_KEY",
api: "openai-responses" as const,
api: "openai-responses",
models: [
{
id: "config-model",
name: "Config model",
input: ["text"] as Array<"text" | "image">,
input: ["text"],
reasoning: false,
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 8192,
maxTokens: 2048,
},
],
};
} as const;
}
async function runCustomProviderMergeTest(seedProvider: {

View File

@@ -11,6 +11,7 @@ function applyThinkingDefault(thinking: ThinkingLevel) {
harness.setSessionsSpawnConfigOverride({
session: { mainKey: "main", scope: "per-sender" },
agents: { defaults: { subagents: { thinking } } },
routing: { sessions: { mainKey: MAIN_SESSION_KEY } },
});
}

View File

@@ -15,6 +15,7 @@ function configureDefaultsWithoutTimeout() {
setSessionsSpawnConfigOverride({
session: { mainKey: "main", scope: "per-sender" },
agents: { defaults: { subagents: { maxConcurrent: 8 } } },
routing: { sessions: { mainKey: MAIN_SESSION_KEY } },
});
}

View File

@@ -9,6 +9,7 @@ function applySubagentTimeoutDefault(seconds: number) {
sessionsHarness.setSessionsSpawnConfigOverride({
session: { mainKey: "main", scope: "per-sender" },
agents: { defaults: { subagents: { runTimeoutSeconds: seconds } } },
routing: { sessions: { mainKey: MAIN_SESSION_KEY } },
});
}

View File

@@ -1,4 +1,4 @@
import { vi, type Mock } from "vitest";
import { vi } from "vitest";
type SessionsSpawnTestConfig = ReturnType<(typeof import("../config/config.js"))["loadConfig"]>;
type CreateSessionsSpawnTool =
@@ -16,6 +16,10 @@ type SessionsSpawnGatewayMockOptions = {
agentWaitResult?: { status: "ok" | "timeout"; startedAt: number; endedAt: number };
};
// Avoid exporting vitest mock types (TS2742 under pnpm + d.ts emit).
// oxlint-disable-next-line typescript/no-explicit-any
type AnyMock = any;
const hoisted = vi.hoisted(() => {
const callGatewayMock = vi.fn();
const defaultConfigOverride = {
@@ -28,12 +32,12 @@ const hoisted = vi.hoisted(() => {
return { callGatewayMock, defaultConfigOverride, state };
});
export function getCallGatewayMock(): Mock {
export function getCallGatewayMock(): AnyMock {
return hoisted.callGatewayMock;
}
export function getGatewayRequests(): Array<GatewayRequest> {
return getCallGatewayMock().mock.calls.map((call: unknown[]) => call[0] as GatewayRequest);
return getCallGatewayMock().mock.calls.map((call: [unknown]) => call[0] as GatewayRequest);
}
export function getGatewayMethods(): Array<string | undefined> {

View File

@@ -6,7 +6,6 @@ import {
DEFAULT_OPENCLAW_BROWSER_COLOR,
DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME,
} from "../../browser/constants.js";
import { deriveDefaultBrowserCdpPortRange } from "../../config/port-defaults.js";
import { defaultRuntime } from "../../runtime.js";
import { BROWSER_BRIDGES } from "./browser-bridges.js";
import { computeSandboxBrowserConfigHash } from "./config-hash.js";
@@ -71,16 +70,15 @@ function buildSandboxBrowserResolvedConfig(params: {
evaluateEnabled: boolean;
}): ResolvedBrowserConfig {
const cdpHost = "127.0.0.1";
const cdpPortRange = deriveDefaultBrowserCdpPortRange(params.controlPort);
return {
enabled: true,
evaluateEnabled: params.evaluateEnabled,
controlPort: params.controlPort,
cdpPortRangeStart: params.cdpPort,
cdpPortRangeEnd: params.cdpPort,
cdpProtocol: "http",
cdpHost,
cdpIsLoopback: true,
cdpPortRangeStart: cdpPortRange.start,
cdpPortRangeEnd: cdpPortRange.end,
remoteCdpTimeoutMs: 1500,
remoteCdpHandshakeTimeoutMs: 3000,
color: DEFAULT_OPENCLAW_BROWSER_COLOR,

View File

@@ -31,19 +31,6 @@ describe("resolveMainSessionAlias", () => {
scope: "per-sender",
});
});
it("uses session.mainKey over any legacy routing sessions key", () => {
const cfg = {
session: { mainKey: " work ", scope: "per-sender" },
routing: { sessions: { mainKey: "legacy-main" } },
} as OpenClawConfig;
expect(resolveMainSessionAlias(cfg)).toEqual({
mainKey: "work",
alias: "work",
scope: "per-sender",
});
});
});
describe("session key display/internal mapping", () => {

View File

@@ -11,8 +11,6 @@ function buildResolvedConfig(): ResolvedBrowserConfig {
enabled: true,
evaluateEnabled: false,
controlPort: 0,
cdpPortRangeStart: 18800,
cdpPortRangeEnd: 18899,
cdpProtocol: "http",
cdpHost: "127.0.0.1",
cdpIsLoopback: true,

View File

@@ -55,22 +55,6 @@ describe("browser config", () => {
});
});
it("supports overriding the local CDP auto-allocation range start", () => {
const resolved = resolveBrowserConfig({
cdpPortRangeStart: 19000,
});
const openclaw = resolveProfile(resolved, "openclaw");
expect(resolved.cdpPortRangeStart).toBe(19000);
expect(openclaw?.cdpPort).toBe(19000);
expect(openclaw?.cdpUrl).toBe("http://127.0.0.1:19000");
});
it("rejects cdpPortRangeStart values that overflow the CDP range window", () => {
expect(() => resolveBrowserConfig({ cdpPortRangeStart: 65535 })).toThrow(
/cdpPortRangeStart .* too high/i,
);
});
it("normalizes hex colors", () => {
const resolved = resolveBrowserConfig({
color: "ff4500",

View File

@@ -20,8 +20,6 @@ export type ResolvedBrowserConfig = {
enabled: boolean;
evaluateEnabled: boolean;
controlPort: number;
cdpPortRangeStart: number;
cdpPortRangeEnd: number;
cdpProtocol: "http" | "https";
cdpHost: string;
cdpIsLoopback: boolean;
@@ -65,27 +63,6 @@ function normalizeTimeoutMs(raw: number | undefined, fallback: number) {
return value < 0 ? fallback : value;
}
function resolveCdpPortRangeStart(
rawStart: number | undefined,
fallbackStart: number,
rangeSpan: number,
) {
const start =
typeof rawStart === "number" && Number.isFinite(rawStart)
? Math.floor(rawStart)
: fallbackStart;
if (start < 1 || start > 65535) {
throw new Error(`browser.cdpPortRangeStart must be between 1 and 65535, got: ${start}`);
}
const maxStart = 65535 - rangeSpan;
if (start > maxStart) {
throw new Error(
`browser.cdpPortRangeStart (${start}) is too high for a ${rangeSpan + 1}-port range; max is ${maxStart}.`,
);
}
return start;
}
function normalizeStringList(raw: string[] | undefined): string[] | undefined {
if (!Array.isArray(raw) || raw.length === 0) {
return undefined;
@@ -216,13 +193,6 @@ export function resolveBrowserConfig(
);
const derivedCdpRange = deriveDefaultBrowserCdpPortRange(controlPort);
const cdpRangeSpan = derivedCdpRange.end - derivedCdpRange.start;
const cdpPortRangeStart = resolveCdpPortRangeStart(
cfg?.cdpPortRangeStart,
derivedCdpRange.start,
cdpRangeSpan,
);
const cdpPortRangeEnd = cdpPortRangeStart + cdpRangeSpan;
const rawCdpUrl = (cfg?.cdpUrl ?? "").trim();
let cdpInfo:
@@ -258,7 +228,7 @@ export function resolveBrowserConfig(
// Use legacy cdpUrl port for backward compatibility when no profiles configured
const legacyCdpPort = rawCdpUrl ? cdpInfo.port : undefined;
const profiles = ensureDefaultChromeExtensionProfile(
ensureDefaultProfile(cfg?.profiles, defaultColor, legacyCdpPort, cdpPortRangeStart),
ensureDefaultProfile(cfg?.profiles, defaultColor, legacyCdpPort, derivedCdpRange.start),
controlPort,
);
const cdpProtocol = cdpInfo.parsed.protocol === "https:" ? "https" : "http";
@@ -284,8 +254,6 @@ export function resolveBrowserConfig(
enabled,
evaluateEnabled,
controlPort,
cdpPortRangeStart,
cdpPortRangeEnd,
cdpProtocol,
cdpHost: cdpInfo.parsed.hostname,
cdpIsLoopback: isLoopbackHost(cdpInfo.parsed.hostname),

View File

@@ -61,46 +61,6 @@ describe("BrowserProfilesService", () => {
expect(writeConfigFile).toHaveBeenCalled();
});
it("falls back to derived CDP range when resolved CDP range is missing", async () => {
const base = resolveBrowserConfig({});
const baseWithoutRange = { ...base } as {
[key: string]: unknown;
cdpPortRangeStart?: unknown;
cdpPortRangeEnd?: unknown;
};
delete baseWithoutRange.cdpPortRangeStart;
delete baseWithoutRange.cdpPortRangeEnd;
const resolved = {
...baseWithoutRange,
controlPort: 30000,
} as BrowserServerState["resolved"];
const { ctx, state } = createCtx(resolved);
vi.mocked(loadConfig).mockReturnValue({ browser: { profiles: {} } });
const service = createBrowserProfilesService(ctx);
const result = await service.createProfile({ name: "work" });
expect(result.cdpPort).toBe(30009);
expect(state.resolved.profiles.work?.cdpPort).toBe(30009);
expect(writeConfigFile).toHaveBeenCalled();
});
it("allocates from configured cdpPortRangeStart for new local profiles", async () => {
const resolved = resolveBrowserConfig({ cdpPortRangeStart: 19000 });
const { ctx, state } = createCtx(resolved);
vi.mocked(loadConfig).mockReturnValue({ browser: { cdpPortRangeStart: 19000, profiles: {} } });
const service = createBrowserProfilesService(ctx);
const result = await service.createProfile({ name: "work" });
expect(result.cdpPort).toBe(19001);
expect(result.isRemote).toBe(false);
expect(state.resolved.profiles.work?.cdpPort).toBe(19001);
expect(writeConfigFile).toHaveBeenCalled();
});
it("accepts per-profile cdpUrl for remote Chrome", async () => {
const resolved = resolveBrowserConfig({});
const { ctx } = createCtx(resolved);

View File

@@ -40,30 +40,6 @@ export type DeleteProfileResult = {
const HEX_COLOR_RE = /^#[0-9A-Fa-f]{6}$/;
const cdpPortRange = (resolved: {
controlPort: number;
cdpPortRangeStart?: number;
cdpPortRangeEnd?: number;
}): { start: number; end: number } => {
const start = resolved.cdpPortRangeStart;
const end = resolved.cdpPortRangeEnd;
if (
typeof start === "number" &&
Number.isFinite(start) &&
Number.isInteger(start) &&
typeof end === "number" &&
Number.isFinite(end) &&
Number.isInteger(end) &&
start > 0 &&
end >= start &&
end <= 65535
) {
return { start, end };
}
return deriveDefaultBrowserCdpPortRange(resolved.controlPort);
};
export function createBrowserProfilesService(ctx: BrowserRouteContext) {
const listProfiles = async (): Promise<ProfileStatus[]> => {
return await ctx.listProfiles();
@@ -104,7 +80,7 @@ export function createBrowserProfilesService(ctx: BrowserRouteContext) {
};
} else {
const usedPorts = getUsedPorts(resolvedProfiles);
const range = cdpPortRange(state.resolved);
const range = deriveDefaultBrowserCdpPortRange(state.resolved.controlPort);
const cdpPort = allocateCdpPort(usedPorts, range);
if (cdpPort === null) {
throw new Error("no available CDP ports in range");

View File

@@ -12,8 +12,6 @@ function makeBrowserState(): BrowserServerState {
resolved: {
enabled: true,
controlPort: 18791,
cdpPortRangeStart: 18800,
cdpPortRangeEnd: 18899,
cdpProtocol: "http",
cdpHost: "127.0.0.1",
cdpIsLoopback: true,

View File

@@ -24,8 +24,6 @@ function makeState(
resolved: {
enabled: true,
controlPort: 18791,
cdpPortRangeStart: 18800,
cdpPortRangeEnd: 18899,
cdpProtocol: profile === "remote" ? "https" : "http",
cdpHost: profile === "remote" ? "browserless.example" : "127.0.0.1",
cdpIsLoopback: profile !== "remote",

View File

@@ -1,10 +1,6 @@
import { describe, expect, it, vi } from "vitest";
import { createTypingCallbacks } from "./typing.js";
type TypingCallbackOverrides = Partial<Parameters<typeof createTypingCallbacks>[0]>;
type TypingHarnessStart = ReturnType<typeof vi.fn<() => Promise<void>>>;
type TypingHarnessError = ReturnType<typeof vi.fn<(err: unknown) => void>>;
const flushMicrotasks = async () => {
await Promise.resolve();
await Promise.resolve();
@@ -19,30 +15,16 @@ async function withFakeTimers(run: () => Promise<void>) {
}
}
function createTypingHarness(overrides: TypingCallbackOverrides = {}) {
const start: TypingHarnessStart = vi.fn<() => Promise<void>>(async () => {});
const stop: TypingHarnessStart = vi.fn<() => Promise<void>>(async () => {});
const onStartError: TypingHarnessError = vi.fn<(err: unknown) => void>();
const onStopError: TypingHarnessError = vi.fn<(err: unknown) => void>();
if (overrides.start) {
start.mockImplementation(overrides.start);
}
if (overrides.stop) {
stop.mockImplementation(overrides.stop);
}
if (overrides.onStartError) {
onStartError.mockImplementation(overrides.onStartError);
}
if (overrides.onStopError) {
onStopError.mockImplementation(overrides.onStopError);
}
function createTypingHarness(overrides: Partial<Parameters<typeof createTypingCallbacks>[0]> = {}) {
const start = overrides.start ?? vi.fn().mockResolvedValue(undefined);
const stop = overrides.stop ?? vi.fn().mockResolvedValue(undefined);
const onStartError = overrides.onStartError ?? vi.fn();
const onStopError = overrides.onStopError ?? vi.fn();
const callbacks = createTypingCallbacks({
start,
stop,
onStartError,
onStopError,
...(onStopError ? { onStopError } : {}),
...(overrides.maxConsecutiveFailures !== undefined
? { maxConsecutiveFailures: overrides.maxConsecutiveFailures }
: {}),

View File

@@ -227,7 +227,7 @@ function createTelegramOutboundPlugin() {
};
to: string;
text: string;
accountId?: string | null;
accountId?: string;
mediaUrl?: string;
},
mediaUrl?: string,

View File

@@ -10,20 +10,6 @@ import type { ChannelChoice } from "./onboard-types.js";
import { getChannelOnboardingAdapter } from "./onboarding/registry.js";
import type { ChannelOnboardingAdapter } from "./onboarding/types.js";
type ChannelOnboardingAdapterPatch = Partial<
Pick<
ChannelOnboardingAdapter,
"configure" | "configureInteractive" | "configureWhenConfigured" | "getStatus"
>
>;
type PatchedOnboardingAdapterFields = {
configure?: ChannelOnboardingAdapter["configure"];
configureInteractive?: ChannelOnboardingAdapter["configureInteractive"];
configureWhenConfigured?: ChannelOnboardingAdapter["configureWhenConfigured"];
getStatus?: ChannelOnboardingAdapter["getStatus"];
};
export function setDefaultChannelPluginRegistryForTests(): void {
const channels = [
{ pluginId: "discord", plugin: discordPlugin, source: "test" },
@@ -36,46 +22,23 @@ export function setDefaultChannelPluginRegistryForTests(): void {
setActivePluginRegistry(createTestRegistry(channels));
}
export function patchChannelOnboardingAdapter(
export function patchChannelOnboardingAdapter<K extends keyof ChannelOnboardingAdapter>(
channel: ChannelChoice,
patch: ChannelOnboardingAdapterPatch,
patch: Pick<ChannelOnboardingAdapter, K>,
): () => void {
const adapter = getChannelOnboardingAdapter(channel);
if (!adapter) {
throw new Error(`missing onboarding adapter for ${channel}`);
}
const previous: PatchedOnboardingAdapterFields = {};
if (Object.prototype.hasOwnProperty.call(patch, "getStatus")) {
previous.getStatus = adapter.getStatus;
adapter.getStatus = patch.getStatus ?? adapter.getStatus;
const keys = Object.keys(patch) as K[];
const previous = {} as Pick<ChannelOnboardingAdapter, K>;
for (const key of keys) {
previous[key] = adapter[key];
adapter[key] = patch[key];
}
if (Object.prototype.hasOwnProperty.call(patch, "configure")) {
previous.configure = adapter.configure;
adapter.configure = patch.configure ?? adapter.configure;
}
if (Object.prototype.hasOwnProperty.call(patch, "configureInteractive")) {
previous.configureInteractive = adapter.configureInteractive;
adapter.configureInteractive = patch.configureInteractive;
}
if (Object.prototype.hasOwnProperty.call(patch, "configureWhenConfigured")) {
previous.configureWhenConfigured = adapter.configureWhenConfigured;
adapter.configureWhenConfigured = patch.configureWhenConfigured;
}
return () => {
if (Object.prototype.hasOwnProperty.call(patch, "getStatus")) {
adapter.getStatus = previous.getStatus!;
}
if (Object.prototype.hasOwnProperty.call(patch, "configure")) {
adapter.configure = previous.configure!;
}
if (Object.prototype.hasOwnProperty.call(patch, "configureInteractive")) {
adapter.configureInteractive = previous.configureInteractive;
}
if (Object.prototype.hasOwnProperty.call(patch, "configureWhenConfigured")) {
adapter.configureWhenConfigured = previous.configureWhenConfigured;
for (const key of keys) {
adapter[key] = previous[key];
}
};
}

View File

@@ -84,14 +84,12 @@ function createTelegramCfg(botToken: string, enabled?: boolean): OpenClawConfig
function patchTelegramAdapter(overrides: Parameters<typeof patchChannelOnboardingAdapter>[1]) {
return patchChannelOnboardingAdapter("telegram", {
getStatus: vi.fn(async ({ cfg }: { cfg: OpenClawConfig }) => ({
channel: "telegram",
configured: Boolean(cfg.channels?.telegram?.botToken),
statusLines: [],
})),
...overrides,
getStatus:
overrides.getStatus ??
vi.fn(async ({ cfg }: { cfg: OpenClawConfig }) => ({
channel: "telegram",
configured: Boolean(cfg.channels?.telegram?.botToken),
statusLines: [],
})),
});
}

View File

@@ -1,6 +1,5 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { CONTEXT_WINDOW_HARD_MIN_TOKENS } from "../agents/context-window-guard.js";
import type { OpenClawConfig } from "../config/config.js";
import { defaultRuntime } from "../runtime.js";
import {
applyCustomApiConfig,
@@ -79,13 +78,13 @@ function expectOpenAiCompatResult(params: {
function buildCustomProviderConfig(contextWindow?: number) {
if (contextWindow === undefined) {
return {} as OpenClawConfig;
return {};
}
return {
models: {
providers: {
custom: {
api: "openai-completions" as const,
api: "openai-completions",
baseUrl: "https://llm.example.com/v1",
models: [
{
@@ -101,7 +100,7 @@ function buildCustomProviderConfig(contextWindow?: number) {
},
},
},
} as OpenClawConfig;
};
}
function applyCustomModelConfigWithContextWindow(contextWindow?: number) {

View File

@@ -220,8 +220,6 @@ export const FIELD_HELP: Record<string, string> = {
"Disables Chromium sandbox isolation flags for environments where sandboxing fails at runtime. Keep this off whenever possible because process isolation protections are reduced.",
"browser.attachOnly":
"Restricts browser mode to attach-only behavior without starting local browser processes. Use this when all browser sessions are externally managed by a remote CDP provider.",
"browser.cdpPortRangeStart":
"Starting local CDP port used for auto-allocated browser profile ports. Increase this when host-level port defaults conflict with other local services.",
"browser.defaultProfile":
"Default browser profile name selected when callers do not explicitly choose a profile. Use a stable low-privilege profile as the default to reduce accidental cross-context state use.",
"browser.profiles":

View File

@@ -105,7 +105,6 @@ export const FIELD_LABELS: Record<string, string> = {
"browser.headless": "Browser Headless Mode",
"browser.noSandbox": "Browser No-Sandbox Mode",
"browser.attachOnly": "Browser Attach-only Mode",
"browser.cdpPortRangeStart": "Browser CDP Port Range Start",
"browser.defaultProfile": "Browser Default Profile",
"browser.profiles": "Browser Profiles",
"browser.profiles.*.cdpPort": "Browser Profile CDP Port",

View File

@@ -48,8 +48,6 @@ export type BrowserConfig = {
noSandbox?: boolean;
/** If true: never launch; only attach to an existing browser. Default: false */
attachOnly?: boolean;
/** Starting local CDP port for auto-assigned browser profiles. Default derives from gateway port. */
cdpPortRangeStart?: number;
/** Default profile to use when profile param is omitted. Default: "chrome" */
defaultProfile?: string;
/** Named browser profiles with explicit CDP ports or URLs. */

View File

@@ -250,7 +250,6 @@ export const OpenClawSchema = z
headless: z.boolean().optional(),
noSandbox: z.boolean().optional(),
attachOnly: z.boolean().optional(),
cdpPortRangeStart: z.number().int().min(1).max(65535).optional(),
defaultProfile: z.string().optional(),
snapshotDefaults: BrowserSnapshotDefaultsSchema,
ssrfPolicy: z

View File

@@ -81,7 +81,7 @@ describe("runCronIsolatedAgentTurn — cron model override (#21057)", () => {
// Hold onto the cron session *object* — the code may reassign its
// `sessionEntry` property (e.g. during skills snapshot refresh), so
// checking a stale reference would give a false negative.
let cronSession: ReturnType<typeof makeCronSession>;
let cronSession: { sessionEntry: ReturnType<typeof makeFreshSessionEntry>; [k: string]: unknown };
beforeEach(() => {
previousFastTestEnv = clearFastTestEnv();
@@ -103,7 +103,7 @@ describe("runCronIsolatedAgentTurn — cron model override (#21057)", () => {
cronSession = makeCronSession({
sessionEntry: makeFreshSessionEntry(),
});
}) as { sessionEntry: ReturnType<typeof makeFreshSessionEntry>; [k: string]: unknown };
resolveCronSessionMock.mockReturnValue(cronSession);
});

View File

@@ -1,12 +1,10 @@
import { vi, type Mock } from "vitest";
import { vi } from "vitest";
type CronSessionEntry = {
sessionId: string;
updatedAt: number;
systemSent: boolean;
skillsSnapshot: unknown;
model?: string;
modelProvider?: string;
[key: string]: unknown;
};
@@ -19,27 +17,23 @@ type CronSession = {
[key: string]: unknown;
};
function createMock(): Mock {
return vi.fn();
}
export const buildWorkspaceSkillSnapshotMock = createMock();
export const resolveAgentConfigMock = createMock();
export const resolveAgentModelFallbacksOverrideMock = createMock();
export const resolveAgentSkillsFilterMock = createMock();
export const getModelRefStatusMock = createMock();
export const isCliProviderMock = createMock();
export const resolveAllowedModelRefMock = createMock();
export const resolveConfiguredModelRefMock = createMock();
export const resolveHooksGmailModelMock = createMock();
export const resolveThinkingDefaultMock = createMock();
export const runWithModelFallbackMock = createMock();
export const runEmbeddedPiAgentMock = createMock();
export const runCliAgentMock = createMock();
export const getCliSessionIdMock = createMock();
export const updateSessionStoreMock = createMock();
export const resolveCronSessionMock = createMock();
export const logWarnMock = createMock();
export const buildWorkspaceSkillSnapshotMock = vi.fn();
export const resolveAgentConfigMock = vi.fn();
export const resolveAgentModelFallbacksOverrideMock = vi.fn();
export const resolveAgentSkillsFilterMock = vi.fn();
export const getModelRefStatusMock = vi.fn();
export const isCliProviderMock = vi.fn();
export const resolveAllowedModelRefMock = vi.fn();
export const resolveConfiguredModelRefMock = vi.fn();
export const resolveHooksGmailModelMock = vi.fn();
export const resolveThinkingDefaultMock = vi.fn();
export const runWithModelFallbackMock = vi.fn();
export const runEmbeddedPiAgentMock = vi.fn();
export const runCliAgentMock = vi.fn();
export const getCliSessionIdMock = vi.fn();
export const updateSessionStoreMock = vi.fn();
export const resolveCronSessionMock = vi.fn();
export const logWarnMock = vi.fn();
vi.mock("../../agents/agent-scope.js", () => ({
resolveAgentConfig: resolveAgentConfigMock,

View File

@@ -1,17 +1,10 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { describe, expect, it, type Mock, vi } from "vitest";
import { describe, expect, it, vi } from "vitest";
import { saveExecApprovals } from "../infra/exec-approvals.js";
import type { ExecHostResponse } from "../infra/exec-host.js";
import { handleSystemRunInvoke, formatSystemRunAllowlistMissMessage } from "./invoke-system-run.js";
import type { HandleSystemRunInvokeOptions } from "./invoke-system-run.js";
type MockedRunCommand = Mock<HandleSystemRunInvokeOptions["runCommand"]>;
type MockedRunViaMacAppExecHost = Mock<HandleSystemRunInvokeOptions["runViaMacAppExecHost"]>;
type MockedSendInvokeResult = Mock<HandleSystemRunInvokeOptions["sendInvokeResult"]>;
type MockedSendExecFinishedEvent = Mock<HandleSystemRunInvokeOptions["sendExecFinishedEvent"]>;
type MockedSendNodeEvent = Mock<HandleSystemRunInvokeOptions["sendNodeEvent"]>;
describe("formatSystemRunAllowlistMissMessage", () => {
it("returns legacy allowlist miss message by default", () => {
@@ -41,7 +34,7 @@ describe("handleSystemRunInvoke mac app exec host routing", () => {
}
function expectInvokeOk(
sendInvokeResult: MockedSendInvokeResult,
sendInvokeResult: ReturnType<typeof vi.fn>,
params?: { payloadContains?: string },
) {
expect(sendInvokeResult).toHaveBeenCalledWith(
@@ -55,7 +48,7 @@ describe("handleSystemRunInvoke mac app exec host routing", () => {
}
function expectInvokeErrorMessage(
sendInvokeResult: MockedSendInvokeResult,
sendInvokeResult: ReturnType<typeof vi.fn>,
params: { message: string; exact?: boolean },
) {
expect(sendInvokeResult).toHaveBeenCalledWith(
@@ -69,8 +62,8 @@ describe("handleSystemRunInvoke mac app exec host routing", () => {
}
function expectApprovalRequiredDenied(params: {
sendNodeEvent: MockedSendNodeEvent;
sendInvokeResult: MockedSendInvokeResult;
sendNodeEvent: ReturnType<typeof vi.fn>;
sendInvokeResult: ReturnType<typeof vi.fn>;
}) {
expect(params.sendNodeEvent).toHaveBeenCalledWith(
expect.anything(),
@@ -132,7 +125,7 @@ describe("handleSystemRunInvoke mac app exec host routing", () => {
}
function expectCommandPinnedToCanonicalPath(params: {
runCommand: MockedRunCommand;
runCommand: ReturnType<typeof vi.fn>;
expected: string;
commandTail: string[];
cwd?: string;
@@ -153,50 +146,23 @@ describe("handleSystemRunInvoke mac app exec host routing", () => {
security?: "full" | "allowlist";
ask?: "off" | "on-miss" | "always";
approved?: boolean;
runCommand?: HandleSystemRunInvokeOptions["runCommand"];
runViaMacAppExecHost?: HandleSystemRunInvokeOptions["runViaMacAppExecHost"];
sendInvokeResult?: HandleSystemRunInvokeOptions["sendInvokeResult"];
sendExecFinishedEvent?: HandleSystemRunInvokeOptions["sendExecFinishedEvent"];
sendNodeEvent?: HandleSystemRunInvokeOptions["sendNodeEvent"];
runCommand?: ReturnType<typeof vi.fn>;
runViaMacAppExecHost?: ReturnType<typeof vi.fn>;
sendInvokeResult?: ReturnType<typeof vi.fn>;
sendExecFinishedEvent?: ReturnType<typeof vi.fn>;
sendNodeEvent?: ReturnType<typeof vi.fn>;
skillBinsCurrent?: () => Promise<Array<{ name: string; resolvedPath: string }>>;
}): Promise<{
runCommand: MockedRunCommand;
runViaMacAppExecHost: MockedRunViaMacAppExecHost;
sendInvokeResult: MockedSendInvokeResult;
sendNodeEvent: MockedSendNodeEvent;
sendExecFinishedEvent: MockedSendExecFinishedEvent;
}> {
const runCommand: MockedRunCommand = vi.fn<HandleSystemRunInvokeOptions["runCommand"]>(
async () => createLocalRunResult(),
);
const runViaMacAppExecHost: MockedRunViaMacAppExecHost = vi.fn<
HandleSystemRunInvokeOptions["runViaMacAppExecHost"]
>(async () => params.runViaResponse ?? null);
const sendInvokeResult: MockedSendInvokeResult = vi.fn<
HandleSystemRunInvokeOptions["sendInvokeResult"]
>(async () => {});
const sendNodeEvent: MockedSendNodeEvent = vi.fn<HandleSystemRunInvokeOptions["sendNodeEvent"]>(
async () => {},
);
const sendExecFinishedEvent: MockedSendExecFinishedEvent = vi.fn<
HandleSystemRunInvokeOptions["sendExecFinishedEvent"]
>(async () => {});
if (params.runCommand !== undefined) {
runCommand.mockImplementation(params.runCommand);
}
if (params.runViaMacAppExecHost !== undefined) {
runViaMacAppExecHost.mockImplementation(params.runViaMacAppExecHost);
}
if (params.sendInvokeResult !== undefined) {
sendInvokeResult.mockImplementation(params.sendInvokeResult);
}
if (params.sendNodeEvent !== undefined) {
sendNodeEvent.mockImplementation(params.sendNodeEvent);
}
if (params.sendExecFinishedEvent !== undefined) {
sendExecFinishedEvent.mockImplementation(params.sendExecFinishedEvent);
}
}) {
const runCommand =
params.runCommand ??
vi.fn(async (_command: string[], _cwd?: string, _env?: Record<string, string>) =>
createLocalRunResult(),
);
const runViaMacAppExecHost =
params.runViaMacAppExecHost ?? vi.fn(async () => params.runViaResponse ?? null);
const sendInvokeResult = params.sendInvokeResult ?? vi.fn(async () => {});
const sendExecFinishedEvent = params.sendExecFinishedEvent ?? vi.fn(async () => {});
const sendNodeEvent = params.sendNodeEvent ?? vi.fn(async () => {});
await handleSystemRunInvoke({
client: {} as never,
@@ -224,13 +190,7 @@ describe("handleSystemRunInvoke mac app exec host routing", () => {
preferMacAppExecHost: params.preferMacAppExecHost,
});
return {
runCommand,
runViaMacAppExecHost,
sendInvokeResult,
sendNodeEvent,
sendExecFinishedEvent,
};
return { runCommand, runViaMacAppExecHost, sendInvokeResult, sendExecFinishedEvent };
}
it("uses local execution by default when mac app exec host preference is disabled", async () => {

View File

@@ -395,21 +395,4 @@ describe("pairing store", () => {
expect(scoped).toEqual(["1002", "1001"]);
});
});
it("uses default-account allowFrom when account id is omitted", async () => {
await withTempStateDir(async (stateDir) => {
await writeAllowFromFixture({ stateDir, channel: "telegram", allowFrom: ["1001"] });
await writeAllowFromFixture({
stateDir,
channel: "telegram",
accountId: DEFAULT_ACCOUNT_ID,
allowFrom: ["1002"],
});
const asyncScoped = await readChannelAllowFromStore("telegram", process.env);
const syncScoped = readChannelAllowFromStoreSync("telegram", process.env);
expect(asyncScoped).toEqual(["1002", "1001"]);
expect(syncScoped).toEqual(["1002", "1001"]);
});
});
});

View File

@@ -225,10 +225,6 @@ function shouldIncludeLegacyAllowFromEntries(normalizedAccountId: string): boole
return !normalizedAccountId || normalizedAccountId === DEFAULT_ACCOUNT_ID;
}
function resolveAllowFromAccountId(accountId?: string): string {
return normalizePairingAccountId(accountId) || DEFAULT_ACCOUNT_ID;
}
function normalizeId(value: string | number): string {
return String(value).trim();
}
@@ -399,9 +395,10 @@ export async function readLegacyChannelAllowFromStore(
export async function readChannelAllowFromStore(
channel: PairingChannel,
env: NodeJS.ProcessEnv = process.env,
accountId?: string,
accountId: string,
): Promise<string[]> {
const resolvedAccountId = resolveAllowFromAccountId(accountId);
const normalizedAccountId = accountId.trim().toLowerCase();
const resolvedAccountId = normalizedAccountId || DEFAULT_ACCOUNT_ID;
if (!shouldIncludeLegacyAllowFromEntries(resolvedAccountId)) {
return await readNonDefaultAccountAllowFrom({
@@ -430,9 +427,10 @@ export function readLegacyChannelAllowFromStoreSync(
export function readChannelAllowFromStoreSync(
channel: PairingChannel,
env: NodeJS.ProcessEnv = process.env,
accountId?: string,
accountId: string,
): string[] {
const resolvedAccountId = resolveAllowFromAccountId(accountId);
const normalizedAccountId = accountId.trim().toLowerCase();
const resolvedAccountId = normalizedAccountId || DEFAULT_ACCOUNT_ID;
if (!shouldIncludeLegacyAllowFromEntries(resolvedAccountId)) {
return readNonDefaultAccountAllowFromSync({

View File

@@ -3,7 +3,6 @@ import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../config/config.js";
import type { SecretProviderConfig } from "../config/types.secrets.js";
import { resolveSecretRefString, resolveSecretRefValue } from "./resolve.js";
async function writeSecureFile(filePath: string, content: string, mode = 0o600): Promise<void> {
@@ -29,7 +28,7 @@ describe("secret ref resolver", () => {
function createProviderConfig(
providerId: string,
provider: SecretProviderConfig,
provider: Record<string, unknown>,
): OpenClawConfig {
return {
secrets: {
@@ -43,7 +42,7 @@ describe("secret ref resolver", () => {
async function resolveWithProvider(params: {
ref: Parameters<typeof resolveSecretRefString>[0];
providerId: string;
provider: SecretProviderConfig;
provider: Record<string, unknown>;
}) {
return await resolveSecretRefString(params.ref, {
config: createProviderConfig(params.providerId, params.provider),
@@ -53,17 +52,17 @@ describe("secret ref resolver", () => {
function createExecProvider(
command: string,
overrides?: Record<string, unknown>,
): SecretProviderConfig {
): Record<string, unknown> {
return {
source: "exec",
command,
passEnv: ["PATH"],
...overrides,
} as SecretProviderConfig;
};
}
async function expectExecResolveRejects(
provider: SecretProviderConfig,
provider: Record<string, unknown>,
message: string,
): Promise<void> {
await expect(

View File

@@ -72,7 +72,7 @@ async function runMemberCase(args: MemberCaseArgs = {}): Promise<void> {
}
describe("registerSlackMemberEvents", () => {
const cases: Array<{ name: string; args: MemberCaseArgs; calls: number }> = [
it.each([
{
name: "enqueues DM member events when dmPolicy is open",
args: { overrides: { dmPolicy: "open" } },
@@ -112,8 +112,7 @@ describe("registerSlackMemberEvents", () => {
},
calls: 0,
},
];
it.each(cases)("$name", async ({ args, calls }) => {
])("$name", async ({ args, calls }) => {
await runMemberCase(args);
expect(memberMocks.enqueue).toHaveBeenCalledTimes(calls);
});

View File

@@ -87,7 +87,7 @@ async function runMessageCase(input: MessageCase = {}): Promise<void> {
}
describe("registerSlackMessageEvents", () => {
const cases: Array<{ name: string; input: MessageCase; calls: number }> = [
it.each([
{
name: "enqueues message_changed system events when dmPolicy is open",
input: { overrides: { dmPolicy: "open" }, event: makeChangedEvent() },
@@ -130,8 +130,7 @@ describe("registerSlackMessageEvents", () => {
},
calls: 0,
},
];
it.each(cases)("$name", async ({ input, calls }) => {
])("$name", async ({ input, calls }) => {
await runMessageCase(input);
expect(messageQueueMock).toHaveBeenCalledTimes(calls);
});

View File

@@ -75,36 +75,32 @@ async function runPinCase(input: PinCase = {}): Promise<void> {
}
describe("registerSlackPinEvents", () => {
const cases: Array<{ name: string; args: PinCase; expectedCalls: number }> = [
{
name: "enqueues DM pin system events when dmPolicy is open",
args: { overrides: { dmPolicy: "open" } },
expectedCalls: 1,
},
{
name: "blocks DM pin system events when dmPolicy is disabled",
args: { overrides: { dmPolicy: "disabled" } },
expectedCalls: 0,
},
{
name: "blocks DM pin system events for unauthorized senders in allowlist mode",
args: {
it.each([
["enqueues DM pin system events when dmPolicy is open", { overrides: { dmPolicy: "open" } }, 1],
[
"blocks DM pin system events when dmPolicy is disabled",
{ overrides: { dmPolicy: "disabled" } },
0,
],
[
"blocks DM pin system events for unauthorized senders in allowlist mode",
{
overrides: { dmPolicy: "allowlist", allowFrom: ["U2"] },
event: makePinEvent({ user: "U1" }),
},
expectedCalls: 0,
},
{
name: "allows DM pin system events for authorized senders in allowlist mode",
args: {
0,
],
[
"allows DM pin system events for authorized senders in allowlist mode",
{
overrides: { dmPolicy: "allowlist", allowFrom: ["U1"] },
event: makePinEvent({ user: "U1" }),
},
expectedCalls: 1,
},
{
name: "blocks channel pin events for users outside channel users allowlist",
args: {
1,
],
[
"blocks channel pin events for users outside channel users allowlist",
{
overrides: {
dmPolicy: "open",
channelType: "channel",
@@ -112,10 +108,9 @@ describe("registerSlackPinEvents", () => {
},
event: makePinEvent({ channel: "C1", user: "U_ATTACKER" }),
},
expectedCalls: 0,
},
];
it.each(cases)("$name", async ({ args, expectedCalls }) => {
0,
],
])("%s", async (_name, args: PinCase, expectedCalls: number) => {
await runPinCase(args);
expect(pinEnqueueMock).toHaveBeenCalledTimes(expectedCalls);
});

View File

@@ -78,20 +78,20 @@ async function executeReactionCase(input: ReactionRunInput = {}) {
}
describe("registerSlackReactionEvents", () => {
const cases: Array<{ name: string; input: ReactionRunInput; expectedCalls: number }> = [
it.each([
{
name: "enqueues DM reaction system events when dmPolicy is open",
input: { overrides: { dmPolicy: "open" } },
args: { overrides: { dmPolicy: "open" } },
expectedCalls: 1,
},
{
name: "blocks DM reaction system events when dmPolicy is disabled",
input: { overrides: { dmPolicy: "disabled" } },
args: { overrides: { dmPolicy: "disabled" } },
expectedCalls: 0,
},
{
name: "blocks DM reaction system events for unauthorized senders in allowlist mode",
input: {
args: {
overrides: { dmPolicy: "allowlist", allowFrom: ["U2"] },
event: buildReactionEvent({ user: "U1" }),
},
@@ -99,7 +99,7 @@ describe("registerSlackReactionEvents", () => {
},
{
name: "allows DM reaction system events for authorized senders in allowlist mode",
input: {
args: {
overrides: { dmPolicy: "allowlist", allowFrom: ["U1"] },
event: buildReactionEvent({ user: "U1" }),
},
@@ -107,8 +107,8 @@ describe("registerSlackReactionEvents", () => {
},
{
name: "enqueues channel reaction events regardless of dmPolicy",
input: {
handler: "removed",
args: {
handler: "removed" as const,
overrides: { dmPolicy: "disabled", channelType: "channel" },
event: {
...buildReactionEvent({ channel: "C1" }),
@@ -119,7 +119,7 @@ describe("registerSlackReactionEvents", () => {
},
{
name: "blocks channel reaction events for users outside channel users allowlist",
input: {
args: {
overrides: {
dmPolicy: "open",
channelType: "channel",
@@ -129,10 +129,8 @@ describe("registerSlackReactionEvents", () => {
},
expectedCalls: 0,
},
];
it.each(cases)("$name", async ({ input, expectedCalls }) => {
await executeReactionCase(input);
])("$name", async ({ args, expectedCalls }) => {
await executeReactionCase(args);
expect(reactionQueueMock).toHaveBeenCalledTimes(expectedCalls);
});