Merge branch 'main' into fix/dashboard-tokenized-url-fallback-72081

This commit is contained in:
Val Alexander
2026-05-04 12:28:41 -05:00
committed by GitHub
4 changed files with 79 additions and 8 deletions

View File

@@ -46,6 +46,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Gate zalouser startup name matching [AI]. (#77411) Thanks @pgondhi987.
- fix(device-pair): require pairing scope for pair command [AI]. (#76377) Thanks @pgondhi987.
- fix(qqbot): keep private commands off framework surface [AI]. (#77212) Thanks @pgondhi987.
- 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.

View File

@@ -81,7 +81,9 @@ openclaw directory groups list --channel zalouser --query "work"
`channels.zalouser.dmPolicy` supports: `pairing | allowlist | open | disabled` (default: `pairing`).
`channels.zalouser.allowFrom` accepts user IDs or names. During setup, names are resolved to IDs using the plugin's in-process contact lookup.
`channels.zalouser.allowFrom` should use stable Zalo user IDs. During interactive setup, entered names can be resolved to IDs using the plugin's in-process contact lookup.
If a raw name remains in config, startup resolves it only when `channels.zalouser.dangerouslyAllowNameMatching: true` is enabled. Without that opt-in, runtime sender checks are ID-only and raw names are ignored for authorization.
Approve via:
@@ -93,13 +95,13 @@ Approve via:
- Default: `channels.zalouser.groupPolicy = "open"` (groups allowed). Use `channels.defaults.groupPolicy` to override the default when unset.
- Restrict to an allowlist with:
- `channels.zalouser.groupPolicy = "allowlist"`
- `channels.zalouser.groups` (keys should be stable group IDs; names are resolved to IDs on startup when possible)
- `channels.zalouser.groups` (keys should be stable group IDs; names are resolved to IDs on startup only when `channels.zalouser.dangerouslyAllowNameMatching: true` is enabled)
- `channels.zalouser.groupAllowFrom` (controls which senders in allowed groups can trigger the bot)
- Block all groups: `channels.zalouser.groupPolicy = "disabled"`.
- The configure wizard can prompt for group allowlists.
- On startup, OpenClaw resolves group/user names in allowlists to IDs and logs the mapping.
- On startup, OpenClaw resolves group/user names in allowlists to IDs and logs the mapping only when `channels.zalouser.dangerouslyAllowNameMatching: true` is enabled.
- Group allowlist matching is ID-only by default. Unresolved names are ignored for auth unless `channels.zalouser.dangerouslyAllowNameMatching: true` is enabled.
- `channels.zalouser.dangerouslyAllowNameMatching: true` is a break-glass compatibility mode that re-enables mutable group-name matching.
- `channels.zalouser.dangerouslyAllowNameMatching: true` is a break-glass compatibility mode that re-enables mutable startup name resolution and runtime group-name matching.
- If `groupAllowFrom` is unset, runtime falls back to `allowFrom` for group sender checks.
- Sender checks apply to both normal group messages and control commands (for example `/new`, `/reset`).
@@ -181,7 +183,7 @@ Accounts map to `zalouser` profiles in OpenClaw state. Example:
**Allowlist/group name didn't resolve:**
- Use numeric IDs in `allowFrom`/`groupAllowFrom`/`groups`, or exact friend/group names.
- Use numeric IDs in `allowFrom`/`groupAllowFrom` and stable group IDs in `groups`. If you intentionally need exact friend/group names, enable `channels.zalouser.dangerouslyAllowNameMatching: true`.
**Upgraded from old CLI-based setup:**

View File

@@ -3,7 +3,7 @@ import type { OpenClawConfig, PluginRuntime } from "../runtime-api.js";
import "./monitor.send-mocks.js";
import "./zalo-js.test-mocks.js";
import { resolveZalouserAccountSync } from "./accounts.js";
import { __testing } from "./monitor.js";
import { __testing, monitorZalouserProvider } from "./monitor.js";
import {
sendDeliveredZalouserMock,
sendMessageZalouserMock,
@@ -13,6 +13,11 @@ import {
import { setZalouserRuntime } from "./runtime.js";
import { createZalouserRuntimeEnv } from "./test-helpers.js";
import type { ResolvedZalouserAccount, ZaloInboundMessage } from "./types.js";
import {
listZaloFriendsMock,
listZaloGroupsMock,
startZaloListenerMock,
} from "./zalo-js.test-mocks.js";
function createAccount(): ResolvedZalouserAccount {
return {
@@ -341,6 +346,12 @@ describe("zalouser monitor group mention gating", () => {
sendTypingZalouserMock.mockClear();
sendDeliveredZalouserMock.mockClear();
sendSeenZalouserMock.mockClear();
listZaloFriendsMock.mockReset();
listZaloFriendsMock.mockResolvedValue([]);
listZaloGroupsMock.mockReset();
listZaloGroupsMock.mockResolvedValue([]);
startZaloListenerMock.mockReset();
startZaloListenerMock.mockResolvedValue({ stop: vi.fn() });
});
async function processMessageWithDefaults(params: {
@@ -374,6 +385,23 @@ describe("zalouser monitor group mention gating", () => {
expect(sendTypingZalouserMock).not.toHaveBeenCalled();
}
async function startMonitorForStartupResolution(
accountConfig: ResolvedZalouserAccount["config"],
) {
installRuntime({ commandAuthorized: false });
const abortController = new AbortController();
abortController.abort();
await monitorZalouserProvider({
account: {
...createAccount(),
config: accountConfig,
},
config: createConfig(),
runtime: createRuntimeEnv(),
abortSignal: abortController.signal,
});
}
async function expectGroupCommandAuthorizers(params: {
accountConfig: ResolvedZalouserAccount["config"];
expectedAuthorizers: Array<{ configured: boolean; allowed: boolean }>;
@@ -669,6 +697,45 @@ describe("zalouser monitor group mention gating", () => {
expect(callArg?.ctx?.To).toBe("zalouser:group:g-attacker-001");
});
it("does not resolve mutable allowlist or group names at startup by default", async () => {
listZaloFriendsMock.mockResolvedValue([{ userId: "999", displayName: "Alice" }]);
listZaloGroupsMock.mockResolvedValue([{ groupId: "g-other", name: "Trusted Team" }]);
await startMonitorForStartupResolution({
...createAccount().config,
dmPolicy: "allowlist",
allowFrom: ["Alice"],
groupPolicy: "allowlist",
groupAllowFrom: ["Alice"],
groups: {
"Trusted Team": { enabled: true },
},
});
expect(listZaloFriendsMock).not.toHaveBeenCalled();
expect(listZaloGroupsMock).not.toHaveBeenCalled();
});
it("resolves mutable allowlist and group names at startup when enabled", async () => {
listZaloFriendsMock.mockResolvedValue([{ userId: "123", displayName: "Alice" }]);
listZaloGroupsMock.mockResolvedValue([{ groupId: "g-trusted", name: "Trusted Team" }]);
await startMonitorForStartupResolution({
...createAccount().config,
dangerouslyAllowNameMatching: true,
dmPolicy: "allowlist",
allowFrom: ["Alice"],
groupPolicy: "allowlist",
groupAllowFrom: ["Alice"],
groups: {
"Trusted Team": { enabled: true },
},
});
expect(listZaloFriendsMock).toHaveBeenCalledWith("default");
expect(listZaloGroupsMock).toHaveBeenCalledWith("default");
});
it("allows group control commands when sender is in groupAllowFrom", async () => {
await expectGroupCommandAuthorizers({
accountConfig: {

View File

@@ -828,8 +828,9 @@ export async function monitorZalouserProvider(
const groupAllowFromEntries = (account.config.groupAllowFrom ?? [])
.map((entry) => normalizeZalouserEntry(String(entry)))
.filter((entry) => entry && entry !== "*");
const allowNameMatching = isDangerousNameMatchingEnabled(account.config);
if (allowFromEntries.length > 0 || groupAllowFromEntries.length > 0) {
if (allowNameMatching && (allowFromEntries.length > 0 || groupAllowFromEntries.length > 0)) {
const friends = await listZaloFriends(profile);
const byName = buildNameIndex(friends, (friend) => friend.displayName);
if (allowFromEntries.length > 0) {
@@ -869,7 +870,7 @@ export async function monitorZalouserProvider(
const groupsConfig = account.config.groups ?? {};
const groupKeys = Object.keys(groupsConfig).filter((key) => key !== "*");
if (groupKeys.length > 0) {
if (allowNameMatching && groupKeys.length > 0) {
const groups = await listZaloGroups(profile);
const byName = buildNameIndex(groups, (group) => group.name);
const mapping: string[] = [];