merge main into PR 77502

This commit is contained in:
Patrick Erichsen
2026-05-04 15:13:35 -07:00
59 changed files with 1095 additions and 489 deletions

View File

@@ -11,7 +11,9 @@ Docs: https://docs.openclaw.ai
### Changes
- Plugins/migration: emit catalog-backed install hints when `plugins.entries` or `plugins.allow` references an official external plugin that is not installed, so upgraded configs point operators to `openclaw plugins install <spec>` instead of telling them to remove valid plugin config. (#77483) Thanks @hclsys.
- Dependencies: refresh runtime and provider packages including Pi 0.73.0, ACPX adapters, OpenAI, Anthropic, Slack, and TypeScript native preview, while keeping the Bedrock runtime installer override pinned below the Windows ARM Node 24 npm resolver failure.
- Plugins/active-memory: skip session-store channel entries that contain `:` when resolving the recall subagent's channel, so QQ c2c agent IDs (e.g. `c2c:10D4F7C2…`) and other scoped conversation IDs do not reach bundled-plugin `dirName` validation and crash the recall run. The same guard already applied to explicit `channelId` params (#76704); this extends it to store-derived channels. (#77396) Thanks @hclsys.
- Secrets/external channel contracts: also look in `<rootDir>/dist/` when resolving the `secret-contract-api` sidecar, so npm-published externalized channel plugins (e.g. `@openclaw/discord` since 2026.5.2) whose compiled artifacts live under `dist/` actually contribute their channel SecretRef contracts to the runtime snapshot. Without this, env-backed `channels.discord.token` SecretRefs silently failed to resolve at gateway start on 2026.5.3, leaving the channel `not configured` even though #76449 had landed the generic external-contract loader. Thanks @mogglemoss.
- Models/auth: add `openclaw models auth list [--provider <id>] [--json]` so users can inspect saved per-agent auth profiles without dumping secrets or hitting the old “too many arguments” path. Thanks @vincentkoc.
- Control UI/header: show the active agent name in dashboard breadcrumbs without adding the current session key, keeping non-chat views oriented without crowding the topbar.
- Control UI/cron: make the New Job sidebar collapsible so the jobs list can reclaim space while keeping the form one click away. Thanks @BunsDev.
@@ -54,6 +56,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Agents/OpenAI: default direct OpenAI Responses models to the SSE transport instead of WebSocket auto-selection, preventing pi runtime chat turns from hanging on servers where the WebSocket path stalls while the OpenAI HTTP stream works. Thanks @vincentkoc.
- Docker: prune package-excluded plugin dist directories from runtime images unless the build explicitly opts that plugin in, so official external plugins such as Feishu stay install-on-demand instead of shipping partial metadata without compiled runtime output. Fixes #77424. Thanks @vincentkoc.
- CLI/update: disable and skip plugins that fail package-update plugin sync, so a broken npm/ClawHub/git/marketplace plugin cannot turn a successful OpenClaw package update into a failed update result. Thanks @vincentkoc.
- CLI/update: use an absolute POSIX npm script shell during package-manager updates, so restricted PATH environments can still run dependency lifecycle scripts while updating from `--tag main`. Fixes #77530. Thanks @PeterTremonti.
- Diagnostics: grant the internal diagnostics event bus to official installed diagnostics exporter plugins, so npm-installed `@openclaw/diagnostics-prometheus` can emit metrics without broadening the capability to arbitrary global plugins. Fixes #76628. Thanks @RayWoo.
@@ -65,6 +68,7 @@ Docs: https://docs.openclaw.ai
- Doctor/config: restore legacy group chat config migrations for `routing.allowFrom`, `routing.groupChat.*`, and `channels.telegram.requireMention` so upgrades keep WhatsApp, Telegram, and iMessage group mention gates and history settings instead of leaving configs invalid or silently blocked. Thanks @scoootscooob.
- CLI/update: make package-update follow-up processes write completion results and exit explicitly, so Windows packaged upgrades do not hang after the new package finishes post-core plugin work. Thanks @vincentkoc.
- Release validation: skip Slack live QA unless Slack credentials are explicitly configured, so release gates can keep proving non-Slack surfaces while Slack is still local and credential-gated. Thanks @vincentkoc.
- Plugins/update: treat OpenClaw CalVer correction versions like `2026.5.3-1` as satisfying base plugin API ranges, so correction builds can install plugins that require the base runtime API. Fixes #77293. (#77450) Thanks @p3nchan.
- fix(gateway): clamp unbound websocket auth scopes [AI]. (#77413) Thanks @pgondhi987.
- Gate zalouser startup name matching [AI]. (#77411) Thanks @pgondhi987.
- Active Memory: send a bounded latest-message search query to the recall worker so channel/runtime metadata does not become the memory search string. Fixes #65309. Thanks @joeykrug, @westley3601, @pimenov, and @tasi333.
@@ -73,8 +77,10 @@ Docs: https://docs.openclaw.ai
- fix(qqbot): keep private commands off framework surface [AI]. (#77212) Thanks @pgondhi987.
- Claude CLI: honor non-off `/think` levels by passing Claude Code's session-scoped `--effort` flag through the CLI backend seam, so chat bridges no longer show an inert thinking control. Fixes #77303. Thanks @Petr1t.
- Agents/subagents: refresh deferred final-delivery payloads when same-session completion output changes, so retried parent notifications use the final child summary instead of stale progress text. Thanks @vincentkoc.
- active-memory: skip the memory sub-agent gracefully instead of logging a confusing allowlist error when no memory plugin (`memory-core` or `memory-lancedb`) is loaded, so active-memory with no memory backend no longer produces misleading "No callable tools remain" warnings in the gateway log. Fixes #77506. Thanks @hclsys.
- Memory/wiki: preserve representation from both corpora in `corpus=all` searches while backfilling unused result capacity, so memory hits are not starved by numerically higher wiki integer scores. Fixes #77337. Thanks @hclsys.
- Telegram: clean up tool-only draft previews after assistant message boundaries so transient `Surfacing...` tool-status bubbles do not linger when no matching final preview arrives. Thanks @BunsDev.
- Telegram: let explicit forum-topic `requireMention` settings override persisted `/activate` and `/deactivate` state, so per-topic mention gates work consistently. Fixes #49864. Thanks @Panniantong.
- Cron: surface failed isolated-run diagnostics in `cron show`, status, and run history when requested tools are unavailable, so blocked cron runs report the actual tool-policy failure instead of a misleading green result. Fixes #75763. Thanks @RyanSandoval.
- TUI/escape abort: track the in-flight runId after `chat.send` resolves so pressing Esc during the gap before the first gateway event aborts the run instead of repeatedly printing `no active run`. Fixes #1296. Thanks @Lukavyi and @romneyda.
- TUI/render: stop the long-token sanitizer from injecting literal spaces inside inline code spans, fenced code blocks, table borders, and bare hyphenated/dotted identifiers, so copied package names, entity IDs, and shell line-continuations stay byte-for-byte intact while narrow-terminal protection still chunks unidentifiable long prose tokens. Fixes #48432, #39505. Thanks @DocOellerson, @xeusoc, @CCcassiusdjs, @akramcodez, @brokemac79, @romneyda.

View File

@@ -124,6 +124,7 @@ RUN printf 'packages:\n - .\n - ui\n' > /tmp/pnpm-workspace.runtime.yaml && \
cp /tmp/pnpm-workspace.runtime.yaml pnpm-workspace.yaml && \
CI=true NPM_CONFIG_FROZEN_LOCKFILE=false pnpm prune --prod && \
node scripts/postinstall-bundled-plugins.mjs && \
OPENCLAW_EXTENSIONS="$OPENCLAW_EXTENSIONS" node scripts/prune-docker-plugin-dist.mjs && \
find dist -type f \( -name '*.d.ts' -o -name '*.d.mts' -o -name '*.d.cts' -o -name '*.map' \) -delete && \
node scripts/check-package-dist-imports.mjs /app

View File

@@ -8,8 +8,8 @@
},
"type": "module",
"dependencies": {
"@agentclientprotocol/claude-agent-acp": "0.31.4",
"@zed-industries/codex-acp": "0.12.0",
"@agentclientprotocol/claude-agent-acp": "0.32.0",
"@zed-industries/codex-acp": "0.13.0",
"acpx": "0.6.1"
},
"devDependencies": {

View File

@@ -211,8 +211,8 @@ ${ACPX_CMD} codex sessions close oc-codex-<conversationId>
Defaults are:
- `openclaw -> openclaw acp`
- `claude -> npx -y @agentclientprotocol/claude-agent-acp@^0.31.0`
- `codex -> bundled @zed-industries/codex-acp@0.12.0 through OpenClaw's isolated CODEX_HOME wrapper`
- `claude -> bundled @agentclientprotocol/claude-agent-acp@0.32.0`
- `codex -> bundled @zed-industries/codex-acp@0.13.0 through OpenClaw's isolated CODEX_HOME wrapper`
- `copilot -> copilot --acp --stdio`
- `cursor -> cursor-agent acp`
- `droid -> droid exec --output-format acp`

View File

@@ -163,7 +163,7 @@ describe("prepareAcpxCodexAuthConfig", () => {
});
const wrapper = await fs.readFile(generated.wrapperPath, "utf8");
expect(wrapper).toContain('"@zed-industries/codex-acp@^0.12.0"');
expect(wrapper).toContain('"@zed-industries/codex-acp@0.13.0"');
expect(wrapper).toContain('"--", "codex-acp"');
expect(wrapper).not.toContain("@zed-industries/codex-acp@^0.11.1");
});
@@ -184,7 +184,7 @@ describe("prepareAcpxCodexAuthConfig", () => {
});
const wrapper = await fs.readFile(generated.wrapperPath, "utf8");
expect(wrapper).toContain('"@agentclientprotocol/claude-agent-acp@0.31.4"');
expect(wrapper).toContain('"@agentclientprotocol/claude-agent-acp@0.32.0"');
expect(wrapper).toContain('"--", "claude-agent-acp"');
expect(wrapper).not.toContain("@agentclientprotocol/claude-agent-acp@^0.31.0");
expect(wrapper).not.toContain("@agentclientprotocol/claude-agent-acp@0.31.0");

View File

@@ -4,10 +4,8 @@ import path from "node:path";
import type { ResolvedAcpxPluginConfig } from "./config.js";
const CODEX_ACP_PACKAGE = "@zed-industries/codex-acp";
const CODEX_ACP_PACKAGE_RANGE = "^0.12.0";
const CODEX_ACP_BIN = "codex-acp";
const CLAUDE_ACP_PACKAGE = "@agentclientprotocol/claude-agent-acp";
const CLAUDE_ACP_PACKAGE_VERSION = "0.31.4";
const CLAUDE_ACP_BIN = "claude-agent-acp";
const RUN_CONFIGURED_COMMAND_SENTINEL = "--openclaw-run-configured";
const requireFromHere = createRequire(import.meta.url);
@@ -15,8 +13,22 @@ const requireFromHere = createRequire(import.meta.url);
type PackageManifest = {
name?: unknown;
bin?: unknown;
dependencies?: Record<string, unknown>;
};
const selfManifest = requireFromHere("../package.json") as PackageManifest;
function readManifestDependencyVersion(packageName: string): string {
const version = selfManifest.dependencies?.[packageName];
if (typeof version !== "string" || version.trim() === "") {
throw new Error(`Missing ${packageName} dependency version in @openclaw/acpx manifest`);
}
return version;
}
const CODEX_ACP_PACKAGE_VERSION = readManifestDependencyVersion(CODEX_ACP_PACKAGE);
const CLAUDE_ACP_PACKAGE_VERSION = readManifestDependencyVersion(CLAUDE_ACP_PACKAGE);
function quoteCommandPart(value: string): string {
return JSON.stringify(value);
}
@@ -205,7 +217,7 @@ child.on("exit", (code, signal) => {
function buildCodexAcpWrapperScript(installedBinPath?: string): string {
return buildAdapterWrapperScript({
displayName: "Codex",
packageSpec: `${CODEX_ACP_PACKAGE}@${CODEX_ACP_PACKAGE_RANGE}`,
packageSpec: `${CODEX_ACP_PACKAGE}@${CODEX_ACP_PACKAGE_VERSION}`,
binName: CODEX_ACP_BIN,
installedBinPath,
envSetup: `const codexHome = fileURLToPath(new URL("./codex-home/", import.meta.url));

View File

@@ -13,8 +13,8 @@ describe("acpx package manifest", () => {
) as AcpxPackageManifest;
expect(packageJson.dependencies?.acpx).toBeDefined();
expect(packageJson.dependencies?.["@zed-industries/codex-acp"]).toBe("0.12.0");
expect(packageJson.dependencies?.["@agentclientprotocol/claude-agent-acp"]).toBe("0.31.4");
expect(packageJson.dependencies?.["@zed-industries/codex-acp"]).toBe("0.13.0");
expect(packageJson.dependencies?.["@agentclientprotocol/claude-agent-acp"]).toBe("0.32.0");
expect(packageJson.devDependencies?.["@agentclientprotocol/claude-agent-acp"]).toBeUndefined();
});
});

View File

@@ -9,7 +9,7 @@ type TestSessionStore = {
const DOCUMENTED_OPENCLAW_BRIDGE_COMMAND =
"env OPENCLAW_HIDE_BANNER=1 OPENCLAW_SUPPRESS_NOTES=1 openclaw acp --url ws://127.0.0.1:18789 --token-file ~/.openclaw/gateway.token --session agent:main:main";
const CODEX_ACP_COMMAND = "npx @zed-industries/codex-acp@^0.12.0";
const CODEX_ACP_COMMAND = "npx @zed-industries/codex-acp@0.13.0";
const CODEX_ACP_WRAPPER_COMMAND = `node "/tmp/openclaw/acpx/codex-acp-wrapper.mjs"`;
function makeRuntime(
@@ -226,7 +226,7 @@ describe("AcpxRuntime fresh reset wrapper", () => {
reasoningEffort: "medium",
}),
).toBe(
"npx @zed-industries/codex-acp@^0.12.0 -c model=gpt-5.4 -c model_reasoning_effort=medium",
"npx @zed-industries/codex-acp@0.13.0 -c model=gpt-5.4 -c model_reasoning_effort=medium",
);
expect(__testing.isCodexAcpCommand("openclaw acp")).toBe(false);
});

View File

@@ -125,6 +125,23 @@ describe("active-memory plugin", () => {
"utf8",
);
};
const makeMemoryToolAllowlistError = (
reason: string,
sources = "runtime toolsAllow: memory_recall, memory_search, memory_get",
) =>
new Error(
`No callable tools remain after resolving explicit tool allowlist ` +
`(${sources}); ${reason}. ` +
`Fix the allowlist or enable the plugin that registers the requested tool.`,
);
const hasDebugLine = (needle: string) =>
vi
.mocked(api.logger.debug)
.mock.calls.some((call: unknown[]) => String(call[0]).includes(needle));
const hasWarnLine = (needle: string) =>
vi
.mocked(api.logger.warn)
.mock.calls.some((call: unknown[]) => String(call[0]).includes(needle));
beforeEach(async () => {
vi.clearAllMocks();
@@ -1646,6 +1663,133 @@ describe("active-memory plugin", () => {
expect(result).toBeUndefined();
});
it("skips the recall subagent when no registered memory tools match", async () => {
const sessionKey = "agent:main:missing-memory-tools";
hoisted.sessionStore[sessionKey] = {
sessionId: "s-missing-memory-tools",
updatedAt: 0,
};
const error = makeMemoryToolAllowlistError("no registered tools matched");
expect(__testing.isMissingRegisteredMemoryToolsError(error)).toBe(true);
runEmbeddedPiAgent.mockRejectedValueOnce(error);
const result = await hooks.before_prompt_build(
{ prompt: "what wings should i order? missing memory tools", messages: [] },
{ agentId: "main", trigger: "user", sessionKey, messageProvider: "webchat" },
);
expect(result).toBeUndefined();
expect(hasDebugLine("no memory tools registered")).toBe(true);
expect(hasWarnLine("No callable tools remain")).toBe(false);
const lines = getActiveMemoryLines(sessionKey);
expect(lines).toEqual([expect.stringContaining("🧩 Active Memory: status=empty")]);
expect(lines.join("\n")).not.toContain("status=unavailable");
});
it("skips missing memory tools when the allowlist error includes inherited sources", async () => {
const sessionKey = "agent:main:missing-memory-tools-with-policy-source";
hoisted.sessionStore[sessionKey] = {
sessionId: "s-missing-memory-tools-with-policy-source",
updatedAt: 0,
};
const error = makeMemoryToolAllowlistError(
"no registered tools matched",
"tools.allow: *, lobster; runtime toolsAllow: memory_recall, memory_search, memory_get",
);
expect(__testing.isMissingRegisteredMemoryToolsError(error)).toBe(true);
runEmbeddedPiAgent.mockRejectedValueOnce(error);
const result = await hooks.before_prompt_build(
{ prompt: "what wings should i order? missing memory tools with policy", messages: [] },
{ agentId: "main", trigger: "user", sessionKey, messageProvider: "webchat" },
);
expect(result).toBeUndefined();
expect(hasDebugLine("no memory tools registered")).toBe(true);
expect(hasWarnLine("No callable tools remain")).toBe(false);
expect(getActiveMemoryLines(sessionKey)).toEqual([
expect.stringContaining("🧩 Active Memory: status=empty"),
]);
});
it("keeps memory-tool allowlist errors visible when upstream policy can filter memory tools", async () => {
const sessionKey = "agent:main:memory-tools-filtered-by-policy";
hoisted.sessionStore[sessionKey] = {
sessionId: "s-memory-tools-filtered-by-policy",
updatedAt: 0,
};
const error = makeMemoryToolAllowlistError(
"no registered tools matched",
"tools.allow: read, exec; runtime toolsAllow: memory_recall, memory_search, memory_get",
);
expect(__testing.isMissingRegisteredMemoryToolsError(error)).toBe(false);
runEmbeddedPiAgent.mockRejectedValueOnce(error);
const result = await hooks.before_prompt_build(
{ prompt: "what wings should i order? memory tools filtered by policy", messages: [] },
{ agentId: "main", trigger: "user", sessionKey, messageProvider: "webchat" },
);
expect(result).toBeUndefined();
expect(hasDebugLine("no memory tools registered")).toBe(false);
expect(hasWarnLine("No callable tools remain")).toBe(true);
expect(getActiveMemoryLines(sessionKey)).toEqual([
expect.stringContaining("🧩 Active Memory: status=unavailable"),
]);
});
it.each([
["disabled tools", "tools are disabled for this run"],
["models without tool support", "the selected model does not support tools"],
])("keeps allowlist errors for %s visible", async (_label, reason) => {
const sessionKey = `agent:main:${reason.replace(/\W+/g, "-")}`;
hoisted.sessionStore[sessionKey] = {
sessionId: `s-${reason.replace(/\W+/g, "-")}`,
updatedAt: 0,
};
const error = makeMemoryToolAllowlistError(reason);
expect(__testing.isMissingRegisteredMemoryToolsError(error)).toBe(false);
runEmbeddedPiAgent.mockRejectedValueOnce(error);
const result = await hooks.before_prompt_build(
{ prompt: `what wings should i order? ${reason}`, messages: [] },
{ agentId: "main", trigger: "user", sessionKey, messageProvider: "webchat" },
);
expect(result).toBeUndefined();
expect(hasDebugLine("no memory tools registered")).toBe(false);
expect(hasWarnLine(reason)).toBe(true);
expect(getActiveMemoryLines(sessionKey)).toEqual([
expect.stringContaining("🧩 Active Memory: status=unavailable"),
]);
});
it("does not skip missing memory-tool allowlist errors after abort", async () => {
const sessionKey = "agent:main:missing-memory-tools-after-abort";
hoisted.sessionStore[sessionKey] = {
sessionId: "s-missing-memory-tools-after-abort",
updatedAt: 0,
};
runEmbeddedPiAgent.mockImplementationOnce(async (params: { abortSignal?: AbortSignal }) => {
Object.defineProperty(params.abortSignal as AbortSignal, "aborted", {
configurable: true,
value: true,
});
throw makeMemoryToolAllowlistError("no registered tools matched");
});
const result = await hooks.before_prompt_build(
{ prompt: "what wings should i order? missing memory tools after abort", messages: [] },
{ agentId: "main", trigger: "user", sessionKey, messageProvider: "webchat" },
);
expect(result).toBeUndefined();
expect(hasDebugLine("no memory tools registered")).toBe(false);
expect(getActiveMemoryLines(sessionKey)).toEqual([
expect.stringContaining("🧩 Active Memory: status=timeout"),
]);
});
it("returns partial transcript text on timeout when the subagent has already written assistant output", async () => {
__testing.setMinimumTimeoutMsForTests(1);
__testing.setSetupGraceTimeoutMsForTests(0);

View File

@@ -41,6 +41,7 @@ const DEFAULT_QMD_SEARCH_MODE = "search" as const;
const DEFAULT_TRANSCRIPT_DIR = "active-memory";
const DEFAULT_CIRCUIT_BREAKER_MAX_TIMEOUTS = 3;
const DEFAULT_CIRCUIT_BREAKER_COOLDOWN_MS = 60_000;
const ACTIVE_MEMORY_TOOL_ALLOWLIST = ["memory_recall", "memory_search", "memory_get"] as const;
const TOGGLE_STATE_FILE = "session-toggles.json";
const DEFAULT_PARTIAL_TRANSCRIPT_MAX_CHARS = 32_000;
const DEFAULT_TRANSCRIPT_READ_MAX_LINES = 2_000;
@@ -494,6 +495,38 @@ function normalizeOptionalString(value: unknown): string | undefined {
return typeof value === "string" && value.trim() ? value.trim() : undefined;
}
function isMissingRegisteredMemoryToolsError(error: unknown): boolean {
if (!(error instanceof Error)) {
return false;
}
const message = error.message.trim();
const prefix = "No callable tools remain after resolving explicit tool allowlist (";
const suffix =
"); no registered tools matched. Fix the allowlist or enable the plugin that registers the requested tool.";
if (!message.startsWith(prefix) || !message.endsWith(suffix)) {
return false;
}
const sources = message.slice(prefix.length, -suffix.length);
const runtimeSource = `runtime toolsAllow: ${ACTIVE_MEMORY_TOOL_ALLOWLIST.join(", ")}`;
const sourceParts = sources
.split(";")
.map((source) => source.trim())
.filter(Boolean);
if (!sourceParts.includes(runtimeSource)) {
return false;
}
return sourceParts.every((source) => {
if (source === runtimeSource) {
return true;
}
const entries = source
.slice(source.indexOf(":") + 1)
.split(",")
.map((entry) => entry.trim());
return entries.includes("*");
});
}
function resolveRecallRunChannelContext(params: {
api: OpenClawPluginApi;
agentId: string;
@@ -2394,7 +2427,7 @@ async function runRecallSubagent(params: {
timeoutMs: embeddedTimeoutMs,
runId: subagentSessionId,
trigger: "manual",
toolsAllow: ["memory_recall", "memory_search", "memory_get"],
toolsAllow: [...ACTIVE_MEMORY_TOOL_ALLOWLIST],
disableMessageTool: true,
allowGatewaySubagentBinding: true,
bootstrapContextMode: "lightweight",
@@ -2437,6 +2470,12 @@ async function runRecallSubagent(params: {
const searchDebug = partialReply ? await readActiveMemorySearchDebug(sessionFile) : undefined;
attachPartialTimeoutData(error, partialReply, searchDebug);
}
if (!params.abortSignal?.aborted && isMissingRegisteredMemoryToolsError(error)) {
params.api.logger.debug?.(
`active-memory: no memory tools registered (memory-core or memory-lancedb required); skipping sub-agent`,
);
return { rawReply: "NONE" };
}
throw error;
} finally {
if (tempDir) {
@@ -2959,6 +2998,7 @@ const testing = {
buildPromptPrefix,
getCachedResult,
isCircuitBreakerOpen,
isMissingRegisteredMemoryToolsError,
normalizePluginConfig,
readActiveMemorySearchDebug,
readPartialAssistantText,

View File

@@ -5,9 +5,9 @@
"description": "OpenClaw Amazon Bedrock Mantle (OpenAI-compatible) provider plugin",
"type": "module",
"dependencies": {
"@anthropic-ai/sdk": "0.92.0",
"@anthropic-ai/sdk": "0.93.0",
"@aws/bedrock-token-generator": "^1.1.0",
"@mariozechner/pi-ai": "0.71.1"
"@mariozechner/pi-ai": "0.73.0"
},
"devDependencies": {
"@openclaw/plugin-sdk": "workspace:*"

View File

@@ -5,8 +5,8 @@
"description": "OpenClaw Amazon Bedrock provider plugin",
"type": "module",
"dependencies": {
"@aws-sdk/client-bedrock": "3.1041.0",
"@aws-sdk/client-bedrock-runtime": "3.1041.0",
"@aws-sdk/client-bedrock": "3.1042.0",
"@aws-sdk/client-bedrock-runtime": "3.1042.0",
"@aws-sdk/credential-provider-node": "3.972.39"
},
"devDependencies": {

View File

@@ -6,8 +6,8 @@
"type": "module",
"dependencies": {
"@anthropic-ai/vertex-sdk": "^0.16.0",
"@mariozechner/pi-agent-core": "0.71.1",
"@mariozechner/pi-ai": "0.71.1"
"@mariozechner/pi-agent-core": "0.73.0",
"@mariozechner/pi-ai": "0.73.0"
},
"devDependencies": {
"@openclaw/plugin-sdk": "workspace:*"

View File

@@ -5,7 +5,7 @@
"description": "OpenClaw Anthropic provider plugin",
"type": "module",
"dependencies": {
"@mariozechner/pi-ai": "0.71.1"
"@mariozechner/pi-ai": "0.73.0"
},
"devDependencies": {
"@openclaw/plugin-sdk": "workspace:*"

View File

@@ -4,7 +4,7 @@
"description": "OpenClaw Bonjour/mDNS gateway discovery",
"type": "module",
"dependencies": {
"@homebridge/ciao": "^1.3.7"
"@homebridge/ciao": "^1.3.8"
},
"devDependencies": {
"@openclaw/plugin-sdk": "workspace:*"

View File

@@ -14,7 +14,7 @@
},
"devDependencies": {
"@openclaw/plugin-sdk": "workspace:*",
"undici": "8.1.0"
"undici": "8.2.0"
},
"openclaw": {
"extensions": [

View File

@@ -8,11 +8,11 @@
},
"type": "module",
"dependencies": {
"@mariozechner/pi-coding-agent": "0.71.1",
"@mariozechner/pi-coding-agent": "0.73.0",
"@openai/codex": "0.128.0",
"ajv": "^8.20.0",
"ws": "^8.20.0",
"zod": "^4.4.1"
"zod": "^4.4.3"
},
"devDependencies": {
"@openclaw/plugin-sdk": "workspace:*"

View File

@@ -13,7 +13,7 @@
"https-proxy-agent": "^9.0.0",
"opusscript": "^0.1.1",
"typebox": "1.1.37",
"undici": "8.1.0",
"undici": "8.2.0",
"ws": "^8.20.0"
},
"devDependencies": {

View File

@@ -5,7 +5,7 @@
"description": "OpenClaw Fireworks provider plugin",
"type": "module",
"dependencies": {
"@mariozechner/pi-ai": "0.71.1"
"@mariozechner/pi-ai": "0.73.0"
},
"devDependencies": {
"@openclaw/plugin-sdk": "workspace:*"

View File

@@ -8,7 +8,7 @@
"@clack/prompts": "^1.3.0"
},
"devDependencies": {
"@mariozechner/pi-ai": "0.71.1",
"@mariozechner/pi-ai": "0.73.0",
"@openclaw/plugin-sdk": "workspace:*"
},
"openclaw": {

View File

@@ -6,7 +6,7 @@
"type": "module",
"dependencies": {
"@google/genai": "^1.51.0",
"@mariozechner/pi-ai": "0.71.1"
"@mariozechner/pi-ai": "0.73.0"
},
"devDependencies": {
"@openclaw/plugin-sdk": "workspace:*"

View File

@@ -10,7 +10,7 @@
"dependencies": {
"gaxios": "7.1.4",
"google-auth-library": "10.6.2",
"zod": "^4.4.1"
"zod": "^4.4.3"
},
"devDependencies": {
"@openclaw/plugin-sdk": "workspace:*",

View File

@@ -5,7 +5,7 @@
"description": "OpenClaw Kimi provider plugin",
"type": "module",
"dependencies": {
"@mariozechner/pi-ai": "0.71.1"
"@mariozechner/pi-ai": "0.73.0"
},
"devDependencies": {
"@openclaw/plugin-sdk": "workspace:*"

View File

@@ -5,7 +5,7 @@
"description": "OpenClaw LM Studio provider plugin",
"type": "module",
"dependencies": {
"@mariozechner/pi-ai": "0.71.1"
"@mariozechner/pi-ai": "0.73.0"
},
"openclaw": {
"extensions": [

View File

@@ -10,7 +10,7 @@
"dependencies": {
"@lancedb/lancedb": "^0.27.2",
"apache-arrow": "18.1.0",
"openai": "^6.35.0",
"openai": "^6.36.0",
"typebox": "1.1.37"
},
"devDependencies": {

View File

@@ -6,7 +6,7 @@
"type": "module",
"dependencies": {
"typebox": "1.1.37",
"yaml": "^2.8.3"
"yaml": "^2.8.4"
},
"devDependencies": {
"@openclaw/plugin-sdk": "workspace:*",

View File

@@ -5,7 +5,7 @@
"description": "Hermes to OpenClaw migration provider",
"type": "module",
"dependencies": {
"yaml": "^2.8.3"
"yaml": "^2.8.4"
},
"devDependencies": {
"@openclaw/plugin-sdk": "workspace:*",

View File

@@ -8,7 +8,7 @@
},
"type": "module",
"dependencies": {
"zod": "^4.4.1"
"zod": "^4.4.3"
},
"devDependencies": {
"@openclaw/plugin-sdk": "workspace:*",

View File

@@ -9,7 +9,7 @@
"type": "module",
"dependencies": {
"nostr-tools": "^2.23.3",
"zod": "^4.4.1"
"zod": "^4.4.3"
},
"devDependencies": {
"@openclaw/plugin-sdk": "workspace:*",

View File

@@ -5,7 +5,7 @@
"description": "OpenClaw Ollama provider plugin",
"type": "module",
"dependencies": {
"@mariozechner/pi-ai": "0.71.1",
"@mariozechner/pi-ai": "0.73.0",
"typebox": "1.1.37"
},
"devDependencies": {

View File

@@ -60,7 +60,7 @@ function providerWizardByKey() {
describe("OpenAI plugin manifest", () => {
it("keeps runtime dependencies in the package manifest", () => {
expect(packageJson.dependencies?.["@mariozechner/pi-ai"]).toBe("0.71.1");
expect(packageJson.dependencies?.["@mariozechner/pi-ai"]).toBe("0.73.0");
expect(packageJson.dependencies?.ws).toBe("^8.20.0");
});

View File

@@ -5,7 +5,7 @@
"description": "OpenClaw OpenAI provider plugins",
"type": "module",
"dependencies": {
"@mariozechner/pi-ai": "0.71.1",
"@mariozechner/pi-ai": "0.73.0",
"ws": "^8.20.0"
},
"devDependencies": {

View File

@@ -5,11 +5,11 @@
"description": "OpenClaw QA lab plugin with private debugger UI and scenario runner",
"type": "module",
"dependencies": {
"@copilotkit/aimock": "1.16.4",
"@copilotkit/aimock": "1.17.0",
"@modelcontextprotocol/sdk": "1.29.0",
"playwright-core": "1.59.1",
"yaml": "^2.8.3",
"zod": "^4.4.1"
"yaml": "^2.8.4",
"zod": "^4.4.3"
},
"devDependencies": {
"@openclaw/discord": "workspace:*",

View File

@@ -5,7 +5,7 @@
"description": "OpenClaw Matrix QA runner plugin",
"type": "module",
"dependencies": {
"undici": "8.1.0"
"undici": "8.2.0"
},
"devDependencies": {
"@openclaw/matrix": "workspace:*",

View File

@@ -13,7 +13,7 @@
"mpg123-decoder": "^1.0.3",
"silk-wasm": "^3.7.1",
"ws": "^8.20.0",
"zod": "^4.4.1"
"zod": "^4.4.3"
},
"devDependencies": {
"@openclaw/plugin-sdk": "workspace:*",

View File

@@ -6,8 +6,8 @@
"type": "module",
"dependencies": {
"@slack/bolt": "^4.7.2",
"@slack/types": "^2.20.1",
"@slack/web-api": "^7.15.1",
"@slack/types": "^2.21.0",
"@slack/web-api": "^7.15.2",
"https-proxy-agent": "^9.0.0"
},
"devDependencies": {

View File

@@ -8,7 +8,7 @@
},
"type": "module",
"dependencies": {
"zod": "^4.4.1"
"zod": "^4.4.3"
},
"devDependencies": {
"@openclaw/plugin-sdk": "workspace:*"

View File

@@ -9,7 +9,7 @@
"@grammyjs/transformer-throttler": "^1.2.1",
"grammy": "^1.42.0",
"typebox": "1.1.37",
"undici": "8.1.0"
"undici": "8.2.0"
},
"devDependencies": {
"@openclaw/plugin-sdk": "workspace:*"

View File

@@ -0,0 +1,112 @@
import { getRuntimeConfig } from "openclaw/plugin-sdk/runtime-config-snapshot";
import { beforeEach, describe, expect, it, vi } from "vitest";
const { defaultRouteConfig } = vi.hoisted(() => ({
defaultRouteConfig: {
agents: {
list: [{ id: "main", default: true }],
},
channels: { telegram: {} },
messages: { groupChat: { mentionPatterns: [] } },
},
}));
vi.mock("openclaw/plugin-sdk/runtime-config-snapshot", async () => {
const actual = await vi.importActual<
typeof import("openclaw/plugin-sdk/runtime-config-snapshot")
>("openclaw/plugin-sdk/runtime-config-snapshot");
return {
...actual,
getRuntimeConfig: vi.fn(() => defaultRouteConfig),
};
});
const { buildTelegramMessageContextForTest } =
await import("./bot-message-context.test-harness.js");
describe("buildTelegramMessageContext requireMention precedence", () => {
function buildForumMessage(threadId = 99) {
return {
message_id: 1,
chat: {
id: -1001234567890,
type: "supergroup" as const,
title: "Forum",
is_forum: true,
},
date: 1_700_000_000,
text: "hello everyone",
message_thread_id: threadId,
from: { id: 42, first_name: "Alice" },
};
}
beforeEach(() => {
vi.mocked(getRuntimeConfig).mockReturnValue(defaultRouteConfig as never);
});
it("lets explicit topic requireMention=false override group requireMention=true", async () => {
const ctx = await buildTelegramMessageContextForTest({
message: buildForumMessage(),
resolveGroupActivation: () => undefined,
resolveGroupRequireMention: () => true,
resolveTelegramGroupConfig: () => ({
groupConfig: { requireMention: true },
topicConfig: { requireMention: false },
}),
});
expect(ctx).not.toBeNull();
});
it("lets explicit topic requireMention=false override mention activation", async () => {
const resolveGroupActivation = vi.fn(() => true);
const ctx = await buildTelegramMessageContextForTest({
message: buildForumMessage(),
resolveGroupActivation,
resolveGroupRequireMention: () => true,
resolveTelegramGroupConfig: () => ({
groupConfig: { requireMention: true },
topicConfig: { requireMention: false },
}),
});
expect(ctx).not.toBeNull();
expect(resolveGroupActivation).toHaveBeenCalledWith(
expect.objectContaining({
chatId: -1001234567890,
messageThreadId: 99,
sessionKey: "agent:main:telegram:group:-1001234567890:topic:99",
}),
);
});
it("lets explicit topic requireMention=true override always activation", async () => {
const ctx = await buildTelegramMessageContextForTest({
message: buildForumMessage(),
resolveGroupActivation: () => false,
resolveGroupRequireMention: () => false,
resolveTelegramGroupConfig: () => ({
groupConfig: { requireMention: false },
topicConfig: { requireMention: true },
}),
});
expect(ctx).toBeNull();
});
it("keeps activation fallback when no topic requireMention is configured", async () => {
const ctx = await buildTelegramMessageContextForTest({
message: buildForumMessage(),
resolveGroupActivation: () => false,
resolveGroupRequireMention: () => true,
resolveTelegramGroupConfig: () => ({
groupConfig: { requireMention: true },
topicConfig: { agentId: "main" },
}),
});
expect(ctx).not.toBeNull();
});
});

View File

@@ -411,8 +411,8 @@ export const buildTelegramMessageContext = async ({
});
const baseRequireMention = resolveGroupRequireMention(chatId);
const requireMention = firstDefined(
activationOverride,
topicConfig?.requireMention,
activationOverride,
telegramGroupConfig?.requireMention,
baseRequireMention,
);

View File

@@ -8,8 +8,8 @@
},
"type": "module",
"dependencies": {
"@aws-sdk/client-s3": "3.1041.0",
"@aws-sdk/s3-request-presigner": "3.1041.0",
"@aws-sdk/client-s3": "3.1042.0",
"@aws-sdk/s3-request-presigner": "3.1042.0",
"@tloncorp/tlon-skill": "0.3.5",
"@urbit/aura": "^3.0.0"
},

View File

@@ -5,7 +5,7 @@
"description": "OpenClaw webhook bridge plugin",
"type": "module",
"dependencies": {
"zod": "^4.4.1"
"zod": "^4.4.3"
},
"devDependencies": {
"@openclaw/plugin-sdk": "workspace:*"

View File

@@ -12,7 +12,7 @@
"https-proxy-agent": "^9.0.0",
"jimp": "^1.6.1",
"typebox": "1.1.37",
"undici": "8.1.0"
"undici": "8.2.0"
},
"devDependencies": {
"@openclaw/plugin-sdk": "workspace:*",

View File

@@ -5,7 +5,7 @@
"description": "OpenClaw xAI plugin",
"type": "module",
"dependencies": {
"@mariozechner/pi-ai": "0.71.1",
"@mariozechner/pi-ai": "0.73.0",
"typebox": "1.1.37"
},
"devDependencies": {

View File

@@ -8,7 +8,7 @@
},
"type": "module",
"dependencies": {
"undici": "8.1.0"
"undici": "8.2.0"
},
"devDependencies": {
"@openclaw/plugin-sdk": "workspace:*",

View File

@@ -1663,27 +1663,27 @@
},
"dependencies": {
"@agentclientprotocol/sdk": "0.21.0",
"@anthropic-ai/sdk": "0.92.0",
"@anthropic-ai/sdk": "0.93.0",
"@anthropic-ai/vertex-sdk": "^0.16.0",
"@aws-sdk/client-bedrock": "3.1041.0",
"@aws-sdk/client-bedrock-runtime": "3.1041.0",
"@aws-sdk/client-bedrock": "3.1042.0",
"@aws-sdk/client-bedrock-runtime": "3.1042.0",
"@aws-sdk/credential-provider-node": "3.972.39",
"@aws/bedrock-token-generator": "^1.1.0",
"@clack/prompts": "^1.3.0",
"@google/genai": "^1.51.0",
"@grammyjs/runner": "^2.0.3",
"@grammyjs/transformer-throttler": "^1.2.1",
"@homebridge/ciao": "^1.3.7",
"@homebridge/ciao": "^1.3.8",
"@lydell/node-pty": "1.2.0-beta.12",
"@mariozechner/pi-agent-core": "0.71.1",
"@mariozechner/pi-ai": "0.71.1",
"@mariozechner/pi-coding-agent": "0.71.1",
"@mariozechner/pi-tui": "0.71.1",
"@mariozechner/pi-agent-core": "0.73.0",
"@mariozechner/pi-ai": "0.73.0",
"@mariozechner/pi-coding-agent": "0.73.0",
"@mariozechner/pi-tui": "0.73.0",
"@modelcontextprotocol/sdk": "1.29.0",
"@mozilla/readability": "^0.6.0",
"@slack/bolt": "^4.7.2",
"@slack/types": "^2.20.1",
"@slack/web-api": "^7.15.1",
"@slack/types": "^2.21.0",
"@slack/web-api": "^7.15.2",
"ajv": "^8.20.0",
"chalk": "^5.6.2",
"chokidar": "^5.0.0",
@@ -1695,7 +1695,7 @@
"global-agent": "^4.1.3",
"grammy": "^1.42.0",
"https-proxy-agent": "^9.0.0",
"ipaddr.js": "^2.3.0",
"ipaddr.js": "^2.4.0",
"jiti": "^2.6.1",
"json5": "^2.2.3",
"jszip": "^3.10.1",
@@ -1703,7 +1703,7 @@
"markdown-it": "14.1.1",
"minimatch": "10.2.5",
"node-edge-tts": "^1.2.10",
"openai": "^6.35.0",
"openai": "^6.36.0",
"openshell": "0.1.0",
"pdfjs-dist": "^5.7.284",
"playwright-core": "1.59.1",
@@ -1714,15 +1714,15 @@
"tree-sitter-bash": "^0.25.1",
"tslog": "^4.10.2",
"typebox": "1.1.37",
"undici": "8.1.0",
"undici": "8.2.0",
"web-push": "^3.6.7",
"web-tree-sitter": "^0.26.8",
"ws": "^8.20.0",
"yaml": "^2.8.3",
"zod": "^4.4.1"
"yaml": "^2.8.4",
"zod": "^4.4.3"
},
"devDependencies": {
"@copilotkit/aimock": "1.16.4",
"@copilotkit/aimock": "1.17.0",
"@grammyjs/types": "^3.26.0",
"@lit-labs/signals": "^0.2.0",
"@lit/context": "^1.1.6",
@@ -1731,7 +1731,7 @@
"@types/markdown-it": "^14.1.2",
"@types/node": "25.6.0",
"@types/ws": "^8.18.1",
"@typescript/native-preview": "7.0.0-dev.20260501.1",
"@typescript/native-preview": "7.0.0-dev.20260504.1",
"@vitest/coverage-v8": "^4.1.5",
"jscpd": "4.0.9",
"jsdom": "^29.1.1",
@@ -1761,7 +1761,7 @@
"packageManager": "pnpm@10.33.2+sha512.a90faf6feeab71ad6c6e57f94e0fe1a12f5dcc22cd754db40ae9593eb6a3e0b6b12e3540218bb37ae083404b1f2ce6db2a4121e979829b4aff94b99f49da1cf8",
"pnpm": {
"overrides": {
"@anthropic-ai/sdk": "0.92.0",
"@anthropic-ai/sdk": "0.93.0",
"hono": "4.12.14",
"@hono/node-server": "1.19.14",
"@aws-sdk/client-bedrock-runtime": "3.1024.0",
@@ -1818,7 +1818,7 @@
},
"patchedDependencies": {
"@whiskeysockets/baileys@7.0.0-rc.9": "patches/@whiskeysockets__baileys@7.0.0-rc.9.patch",
"@agentclientprotocol/claude-agent-acp@0.31.4": "patches/@agentclientprotocol__claude-agent-acp@0.31.4.patch"
"@agentclientprotocol/claude-agent-acp@0.32.0": "patches/@agentclientprotocol__claude-agent-acp@0.32.0.patch"
}
}
}

View File

@@ -1,8 +1,8 @@
diff --git a/dist/acp-agent.js b/dist/acp-agent.js
index 0a8f5e3c57ed05189cba546bd65fc18143744d09..a8522d86a5a2f1bbcdd446d222cb9b7b5acb79ca 100644
index e1d9aa9f0815f57ea2fd299a7f2b8ef0917ca191..875fdfb25fbfa905ca80728355d25a17e6d89148 100644
--- a/dist/acp-agent.js
+++ b/dist/acp-agent.js
@@ -421,6 +421,7 @@ export class ClaudeAcpAgent {
@@ -436,6 +436,7 @@ export class ClaudeAcpAgent {
session.promptRunning = true;
let handedOff = false;
let stopReason = "end_turn";
@@ -10,7 +10,7 @@ index 0a8f5e3c57ed05189cba546bd65fc18143744d09..a8522d86a5a2f1bbcdd446d222cb9b7b
try {
while (true) {
const { value: message, done } = await session.query.next();
@@ -428,6 +429,9 @@ export class ClaudeAcpAgent {
@@ -443,6 +444,9 @@ export class ClaudeAcpAgent {
if (session.cancelled) {
return { stopReason: "cancelled" };
}
@@ -20,7 +20,7 @@ index 0a8f5e3c57ed05189cba546bd65fc18143744d09..a8522d86a5a2f1bbcdd446d222cb9b7b
break;
}
if (session.emitRawSDKMessages &&
@@ -496,7 +500,7 @@ export class ClaudeAcpAgent {
@@ -499,7 +503,7 @@ export class ClaudeAcpAgent {
break;
}
case "session_state_changed": {
@@ -29,7 +29,7 @@ index 0a8f5e3c57ed05189cba546bd65fc18143744d09..a8522d86a5a2f1bbcdd446d222cb9b7b
return { stopReason, usage: sessionUsage(session) };
}
break;
@@ -601,6 +605,7 @@ export class ClaudeAcpAgent {
@@ -621,6 +625,7 @@ export class ClaudeAcpAgent {
unreachable(message, this.logger);
break;
}

811
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,6 @@
export function parseDockerPluginKeepList(value: unknown): Set<string>;
export function pruneDockerPluginDist(params?: {
cwd?: string;
repoRoot?: string;
env?: NodeJS.ProcessEnv;
}): string[];

View File

@@ -0,0 +1,52 @@
import fs from "node:fs";
import path from "node:path";
import { pathToFileURL } from "node:url";
import { collectRootPackageExcludedExtensionDirs } from "./lib/bundled-plugin-build-entries.mjs";
import { removePathIfExists } from "./runtime-postbuild-shared.mjs";
function parsePluginList(value) {
if (typeof value !== "string") {
return new Set();
}
return new Set(
value
.split(/[\s,]+/u)
.map((entry) => entry.trim())
.filter(Boolean),
);
}
export function parseDockerPluginKeepList(value) {
return parsePluginList(value);
}
export function pruneDockerPluginDist(params = {}) {
const repoRoot = params.cwd ?? params.repoRoot ?? process.cwd();
const env = params.env ?? process.env;
const keepPluginIds = parseDockerPluginKeepList(env.OPENCLAW_EXTENSIONS);
const excludedPluginIds = collectRootPackageExcludedExtensionDirs({ cwd: repoRoot });
const removed = [];
for (const pluginId of [...excludedPluginIds].toSorted((left, right) =>
left.localeCompare(right),
)) {
if (keepPluginIds.has(pluginId)) {
continue;
}
for (const root of ["dist", "dist-runtime"]) {
const pluginDistDir = path.join(repoRoot, root, "extensions", pluginId);
if (!fs.existsSync(pluginDistDir)) {
continue;
}
removePathIfExists(pluginDistDir);
removed.push(path.relative(repoRoot, pluginDistDir).replaceAll("\\", "/"));
}
}
return removed;
}
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
pruneDockerPluginDist();
}

View File

@@ -8,6 +8,7 @@ import type { MessagingToolSend } from "./pi-embedded-messaging.types.js";
import {
handleToolExecutionEnd,
handleToolExecutionStart,
handleToolExecutionUpdate,
} from "./pi-embedded-subscribe.handlers.tools.js";
import type {
ToolCallSummary,
@@ -713,6 +714,47 @@ describe("handleToolExecutionEnd exec approval prompts", () => {
});
describe("handleToolExecutionEnd derived tool events", () => {
it("emits command output deltas for exec update results", async () => {
const { ctx, onAgentEvent } = createTestContext();
await handleToolExecutionStart(
ctx as never,
{
type: "tool_execution_start",
toolName: "exec",
toolCallId: "tool-exec-update-output",
args: { command: "npm test" },
} as never,
);
handleToolExecutionUpdate(
ctx as never,
{
type: "tool_execution_update",
toolName: "exec",
toolCallId: "tool-exec-update-output",
partialResult: {
details: {
status: "running",
aggregated: "RUN src/example.test.ts",
},
},
} as never,
);
expect(onAgentEvent).toHaveBeenCalledWith(
expect.objectContaining({
stream: "command_output",
data: expect.objectContaining({
itemId: "command:tool-exec-update-output",
phase: "delta",
output: "RUN src/example.test.ts",
status: "running",
}),
}),
);
});
it("emits command output events for exec results", async () => {
const { ctx, onAgentEvent } = createTestContext();

View File

@@ -772,7 +772,11 @@ export function handleToolExecutionUpdate(
},
});
if (isExecToolName(toolName)) {
const output = extractToolResultText(sanitized);
const execDetails = readExecToolDetails(sanitized);
const output =
execDetails && "aggregated" in execDetails
? execDetails.aggregated
: extractToolResultText(sanitized);
const commandData: AgentItemEventData = {
itemId: buildCommandItemId(toolCallId),
phase: "update",

View File

@@ -111,6 +111,9 @@ describe("Dockerfile", () => {
expect(dockerfile).toContain("pnpm-workspace.runtime.yaml");
expect(dockerfile).toContain(" - ui\\n");
expect(dockerfile).toContain("CI=true NPM_CONFIG_FROZEN_LOCKFILE=false pnpm prune --prod");
expect(dockerfile).toContain(
'OPENCLAW_EXTENSIONS="$OPENCLAW_EXTENSIONS" node scripts/prune-docker-plugin-dist.mjs',
);
expect(dockerfile).toContain("prune must not rediscover unrelated workspaces");
expect(dockerfile).not.toContain(
`npm install --prefix "${BUNDLED_PLUGIN_ROOT_DIR}/$ext" --omit=dev --silent`,

View File

@@ -100,6 +100,12 @@ describe("clawhub helpers", () => {
expect(satisfiesPluginApiRange("invalid", "^1.2.0")).toBe(false);
});
it("treats OpenClaw CalVer correction versions as stable plugin API hosts", () => {
expect(satisfiesPluginApiRange("2026.5.3-1", ">=2026.5.3")).toBe(true);
expect(satisfiesPluginApiRange("2026.5.3-2", ">=2026.5.3")).toBe(true);
expect(satisfiesPluginApiRange("2026.5.3-beta.1", ">=2026.5.3")).toBe(false);
});
it("accepts legacy bare major.minor plugin api ranges as lower bounds", () => {
expect(satisfiesPluginApiRange("2026.5.2", "2026.4")).toBe(true);
expect(satisfiesPluginApiRange("2026.4.0", "2026.4")).toBe(true);

View File

@@ -542,6 +542,13 @@ function satisfiesSemverRange(version: string, range: string): boolean {
return tokens.every((token) => satisfiesComparator(version, token));
}
const OPENCLAW_CALVER_STABLE_CORRECTION_PATTERN = /^[vV]?(\d{4}\.\d{1,2}\.\d{1,2})-\d+$/;
function normalizeCalVerCorrectionForPluginApi(pluginApiVersion: string): string {
const match = OPENCLAW_CALVER_STABLE_CORRECTION_PATTERN.exec(pluginApiVersion.trim());
return match?.[1] ?? pluginApiVersion;
}
function buildUrl(params: Pick<ClawHubRequestParams, "baseUrl" | "path" | "search">): URL {
const url = new URL(params.path, `${normalizeBaseUrl(params.baseUrl)}/`);
for (const [key, value] of Object.entries(params.search ?? {})) {
@@ -1046,7 +1053,10 @@ export function satisfiesPluginApiRange(
if (!pluginApiRange) {
return true;
}
return satisfiesSemverRange(pluginApiVersion, pluginApiRange);
return satisfiesSemverRange(
normalizeCalVerCorrectionForPluginApi(pluginApiVersion),
pluginApiRange,
);
}
export function satisfiesGatewayMinimum(

View File

@@ -852,6 +852,36 @@ describe("installPluginFromClawHub", () => {
expect(archiveCleanupMock).toHaveBeenCalledTimes(1);
});
it("installs when a CalVer correction runtime satisfies the base plugin API range", async () => {
resolveCompatibilityHostVersionMock.mockReturnValueOnce("2026.5.3-1");
fetchClawHubPackageVersionMock.mockResolvedValueOnce({
version: {
version: "2026.5.3",
createdAt: 0,
changelog: "",
sha256hash: "a9eac48c6129bc44b6f93c9a9f48f6c700d191b7279a1e1915f28df6f59bb1af",
compatibility: {
pluginApiRange: ">=2026.5.3",
minGatewayVersion: "2026.3.0",
},
},
});
const result = await installPluginFromClawHub({
spec: "clawhub:demo",
baseUrl: "https://clawhub.ai",
});
expectSuccessfulClawHubInstall(result);
expect(downloadClawHubPackageArchiveMock).toHaveBeenCalledTimes(1);
expect(installPluginFromArchiveMock).toHaveBeenCalledWith(
expect.objectContaining({
archivePath: "/tmp/clawhub-demo/archive.zip",
}),
);
expect(archiveCleanupMock).toHaveBeenCalledTimes(1);
});
it("does not let a wildcard plugin API range hide an invalid runtime version", async () => {
resolveCompatibilityHostVersionMock.mockReturnValueOnce("invalid");
fetchClawHubPackageVersionMock.mockResolvedValueOnce({

View File

@@ -0,0 +1,56 @@
import fs from "node:fs";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import {
parseDockerPluginKeepList,
pruneDockerPluginDist,
} from "../../scripts/prune-docker-plugin-dist.mjs";
import { cleanupTempDirs, makeTempRepoRoot, writeJsonFile } from "../../test/helpers/temp-repo.js";
const tempDirs: string[] = [];
function makeRepoRoot(prefix: string): string {
return makeTempRepoRoot(tempDirs, prefix);
}
function writeDistPluginFile(repoRoot: string, root: "dist" | "dist-runtime", pluginId: string) {
const pluginDir = path.join(repoRoot, root, "extensions", pluginId);
fs.mkdirSync(pluginDir, { recursive: true });
fs.writeFileSync(path.join(pluginDir, "openclaw.plugin.json"), "{}\n", "utf8");
}
afterEach(() => {
cleanupTempDirs(tempDirs);
});
describe("pruneDockerPluginDist", () => {
it("parses space and comma separated Docker plugin keep lists", () => {
expect([...parseDockerPluginKeepList("diagnostics-otel feishu,discord")]).toEqual([
"diagnostics-otel",
"feishu",
"discord",
]);
});
it("removes package-excluded plugin dist unless Docker explicitly opts it in", () => {
const repoRoot = makeRepoRoot("openclaw-docker-plugin-dist-");
writeJsonFile(path.join(repoRoot, "package.json"), {
files: ["dist/**", "!dist/extensions/diagnostics-otel/**", "!dist/extensions/feishu/**"],
});
writeDistPluginFile(repoRoot, "dist", "diagnostics-otel");
writeDistPluginFile(repoRoot, "dist", "feishu");
writeDistPluginFile(repoRoot, "dist-runtime", "feishu");
writeDistPluginFile(repoRoot, "dist", "telegram");
const removed = pruneDockerPluginDist({
repoRoot,
env: { OPENCLAW_EXTENSIONS: "diagnostics-otel" } as NodeJS.ProcessEnv,
});
expect(removed).toEqual(["dist/extensions/feishu", "dist-runtime/extensions/feishu"]);
expect(fs.existsSync(path.join(repoRoot, "dist", "extensions", "diagnostics-otel"))).toBe(true);
expect(fs.existsSync(path.join(repoRoot, "dist", "extensions", "feishu"))).toBe(false);
expect(fs.existsSync(path.join(repoRoot, "dist-runtime", "extensions", "feishu"))).toBe(false);
expect(fs.existsSync(path.join(repoRoot, "dist", "extensions", "telegram"))).toBe(true);
});
});

View File

@@ -98,6 +98,66 @@ describe("external channel secret contract api", () => {
expect(api?.collectRuntimeConfigAssignments).toBeTypeOf("function");
});
it("loads dist/ secret-contract-api sidecars for compiled npm-published external channel plugins", () => {
const rootDir = makeTrackedTempDir("openclaw-channel-secret-contract-dist", tempDirs);
fs.mkdirSync(path.join(rootDir, "dist"), { recursive: true });
fs.writeFileSync(
path.join(rootDir, "dist", "secret-contract-api.cjs"),
`
module.exports = {
secretTargetRegistryEntries: [
{
id: "channels.discord.token",
targetType: "channels.discord.token",
configFile: "openclaw.json",
pathPattern: "channels.discord.token",
secretShape: "secret_input",
expectedResolvedValue: "string",
includeInPlan: true,
includeInConfigure: true,
includeInAudit: true
}
],
collectRuntimeConfigAssignments(params) {
params.context.assignments.push({
path: "channels.discord.token",
ref: { source: "env", provider: "default", id: "DISCORD_BOT_TOKEN" },
expected: "string",
apply() {}
});
}
};
`,
"utf8",
);
const record = {
id: "discord",
origin: "global",
channels: ["discord"],
channelConfigs: {},
rootDir,
};
loadPluginMetadataSnapshotMock.mockReturnValue({
plugins: [record],
});
const api = loadChannelSecretContractApi({
channelId: "discord",
config: { channels: { discord: {} } },
env: {},
loadablePluginOrigins: new Map([["discord", "global"]]),
});
expect(api?.secretTargetRegistryEntries).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: "channels.discord.token",
}),
]),
);
expect(api?.collectRuntimeConfigAssignments).toBeTypeOf("function");
});
it("skips external channel records outside the loadable plugin origin set", () => {
const record = writeExternalChannelPlugin({ pluginId: "discord", channelId: "discord" });
loadPluginMetadataSnapshotMock.mockReturnValue({

View File

@@ -87,16 +87,21 @@ function orderedContractApiExtensions(): readonly string[] {
}
function resolvePluginContractApiPath(rootDir: string): string | null {
for (const extension of orderedContractApiExtensions()) {
const candidate = path.join(rootDir, `secret-contract-api${extension}`);
if (fs.existsSync(candidate)) {
return candidate;
}
}
for (const extension of orderedContractApiExtensions()) {
const candidate = path.join(rootDir, `contract-api${extension}`);
if (fs.existsSync(candidate)) {
return candidate;
// Compiled npm-published plugins place their public artifacts under <rootDir>/dist/
// (per package.json `openclaw.runtimeExtensions`), while flat-layout plugins keep
// them at <rootDir>/. Search both, preferring dist/ when running from built openclaw
// artifacts and rootDir/ when running from source.
const searchDirs = RUNNING_FROM_BUILT_ARTIFACT
? [path.join(rootDir, "dist"), rootDir]
: [rootDir, path.join(rootDir, "dist")];
for (const basename of ["secret-contract-api", "contract-api"]) {
for (const dir of searchDirs) {
for (const extension of orderedContractApiExtensions()) {
const candidate = path.join(dir, `${basename}${extension}`);
if (fs.existsSync(candidate)) {
return candidate;
}
}
}
}
return null;