Address Windows review regressions

This commit is contained in:
Tak Hoffman 2026-03-19 01:36:14 -05:00
parent cc4464f2ce
commit 8d66245825
No known key found for this signature in database
9 changed files with 107 additions and 12 deletions

View File

@ -47,6 +47,9 @@ Docs: https://docs.openclaw.ai
### Fixes
- Hooks/Windows: preserve Windows-aware hook path handling across plugin-managed hook loading and bundle MCP config resolution, so path aliases and canonicalization differences no longer drop hook metadata or break bundled MCP launches.
- Outbound/channels: skip full configured-channel scans when explicit channel selection already determines the target, so explicit sends and broadcasts avoid slow unrelated plugin configuration checks.
- Tlon/install: fetch `@tloncorp/api` from the pinned HTTPS tarball artifact instead of a Git transport URL so installs no longer depend on GitHub SSH access.
- CLI/Ollama onboarding: keep the interactive model picker for explicit `openclaw onboard --auth-choice ollama` runs so setup still selects a default model without reintroducing pre-picker auto-pulls. (#49249) Thanks @BruceMacD.
- Plugins/bundler TDZ: fix `RESERVED_COMMANDS` temporal dead zone error that prevented device-pair, phone-control, and talk-voice plugins from registering when the bundler placed the commands module after call sites in the same output chunk. Thanks @BunsDev.
- Plugins/imports: fix stale googlechat runtime-api import paths and signal SDK circular re-exports broken by recent plugin-sdk refactors. Thanks @BunsDev.

View File

@ -222,11 +222,12 @@ async function resolveChannel(
params: Record<string, unknown>,
toolContext?: { currentChannelProvider?: string },
) {
const explicitChannel = readStringParam(params, "channel");
const selection = await resolveMessageChannelSelection({
cfg,
channel: readStringParam(params, "channel"),
channel: explicitChannel,
fallbackChannel: toolContext?.currentChannelProvider,
includeConfigured: false,
includeConfigured: !explicitChannel,
});
if (selection.source === "tool-context-fallback") {
params.channel = selection.channel;

View File

@ -1,7 +1,11 @@
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import type { ChannelOutboundAdapter, ChannelPlugin } from "../../channels/plugins/types.js";
import { setActivePluginRegistry } from "../../plugins/runtime.js";
import { createMSTeamsTestPlugin, createTestRegistry } from "../../test-utils/channel-plugins.js";
import {
createChannelTestPluginBase,
createMSTeamsTestPlugin,
createTestRegistry,
} from "../../test-utils/channel-plugins.js";
import { createIMessageTestPlugin } from "../../test-utils/imessage-test-plugin.js";
import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../../utils/message-channel.js";
@ -242,6 +246,78 @@ describe("sendPoll channel normalization", () => {
});
});
describe("implicit single-channel selection", () => {
it("keeps single configured channel fallback for sendMessage when channel is omitted", async () => {
const sendText = vi.fn(async () => ({ channel: "msteams", messageId: "m1" }));
setRegistry(
createTestRegistry([
{
pluginId: "msteams",
source: "test",
plugin: {
...createChannelTestPluginBase({
id: "msteams",
label: "Microsoft Teams",
docsPath: "/channels/msteams",
config: {
listAccountIds: () => ["default"],
resolveAccount: () => ({}),
isConfigured: () => true,
},
}),
outbound: {
...createMSTeamsOutbound(),
sendText,
},
},
},
]),
);
const result = await sendMessage({
cfg: {},
to: "conversation:19:abc@thread.tacv2",
content: "hi",
});
expect(result.channel).toBe("msteams");
expect(sendText).toHaveBeenCalled();
});
it("keeps single configured channel fallback for sendPoll when channel is omitted", async () => {
setRegistry(
createTestRegistry([
{
pluginId: "msteams",
source: "test",
plugin: {
...createChannelTestPluginBase({
id: "msteams",
label: "Microsoft Teams",
docsPath: "/channels/msteams",
config: {
listAccountIds: () => ["default"],
resolveAccount: () => ({}),
isConfigured: () => true,
},
}),
outbound: createMSTeamsOutbound({ includePoll: true }),
},
},
]),
);
const result = await sendPoll({
cfg: {},
to: "conversation:19:abc@thread.tacv2",
question: "Lunch?",
options: ["Pizza", "Sushi"],
});
expect(result.channel).toBe("msteams");
});
});
const setMattermostGatewayRegistry = () => {
setRegistry(
createTestRegistry([

View File

@ -132,11 +132,12 @@ async function resolveRequiredChannel(params: {
cfg: OpenClawConfig;
channel?: string;
}): Promise<string> {
const explicitChannel = typeof params.channel === "string" ? params.channel.trim() : "";
return (
await resolveMessageChannelSelection({
cfg: params.cfg,
channel: params.channel,
includeConfigured: false,
channel: explicitChannel || undefined,
includeConfigured: !explicitChannel,
})
).channel;
}

View File

@ -65,6 +65,7 @@ describe("isPathInside", () => {
it("accepts identical and nested paths but rejects escapes", () => {
expect(isPathInside("/workspace/root", "/workspace/root")).toBe(true);
expect(isPathInside("/workspace/root", "/workspace/root/nested/file.txt")).toBe(true);
expect(isPathInside("/workspace/root", "/workspace/root/..cache/file.txt")).toBe(true);
expect(isPathInside("/workspace/root", "/workspace/root/../escape.txt")).toBe(false);
});
@ -75,6 +76,9 @@ describe("isPathInside", () => {
expect(
isPathInside(String.raw`C:\workspace\root`, String.raw`C:\workspace\root\Nested\File.txt`),
).toBe(true);
expect(
isPathInside(String.raw`C:\workspace\root`, String.raw`C:\workspace\root\..cache\file.txt`),
).toBe(true);
expect(
isPathInside(String.raw`C:\workspace\root`, String.raw`C:\workspace\root\..\escape.txt`),
).toBe(false);

View File

@ -37,11 +37,20 @@ export function isPathInside(root: string, target: string): boolean {
const rootForCompare = normalizeWindowsPathForComparison(path.win32.resolve(root));
const targetForCompare = normalizeWindowsPathForComparison(path.win32.resolve(target));
const relative = path.win32.relative(rootForCompare, targetForCompare);
return relative === "" || (!relative.startsWith("..") && !path.win32.isAbsolute(relative));
return (
relative === "" ||
(relative !== ".." &&
!relative.startsWith(`..\\`) &&
!relative.startsWith("../") &&
!path.win32.isAbsolute(relative))
);
}
const resolvedRoot = path.resolve(root);
const resolvedTarget = path.resolve(target);
const relative = path.relative(resolvedRoot, resolvedTarget);
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
return (
relative === "" ||
(relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative))
);
}

View File

@ -327,7 +327,7 @@ export function loadEnabledBundleMcpConfig(params: {
const loaded = loadBundleMcpConfig({
pluginId: record.id,
rootDir: record.rootDir,
rootDir: record.format === "bundle" ? record.source : record.rootDir,
bundleFormat: record.bundleFormat,
});
merged = applyMergePatch(merged, loaded.config) as BundleMcpConfig;

View File

@ -299,7 +299,9 @@ describe("discoverOpenClawPlugins", () => {
expect(bundle?.format).toBe("bundle");
expect(bundle?.bundleFormat).toBe("codex");
expect(bundle?.source).toBe(bundleDir);
expect(normalizePathForAssertion(bundle?.rootDir)).toBe(normalizePathForAssertion(bundleDir));
expect(normalizePathForAssertion(bundle?.rootDir)).toBe(
normalizePathForAssertion(fs.realpathSync(bundleDir)),
);
});
it("auto-detects manifestless Claude bundles from the default layout", async () => {

View File

@ -377,8 +377,7 @@ function addCandidate(params: {
if (params.seen.has(resolved)) {
return;
}
const lexicalRoot = path.resolve(params.rootDir);
const resolvedRoot = safeRealpathSync(params.rootDir) ?? lexicalRoot;
const resolvedRoot = safeRealpathSync(params.rootDir) ?? path.resolve(params.rootDir);
if (
isUnsafePluginCandidate({
source: resolved,
@ -396,7 +395,7 @@ function addCandidate(params: {
idHint: params.idHint,
source: resolved,
setupSource: params.setupSource,
rootDir: lexicalRoot,
rootDir: resolvedRoot,
origin: params.origin,
format: params.format ?? "openclaw",
bundleFormat: params.bundleFormat,