Gate zalouser startup name matching [AI] (#77411)

* fix: gate zalouser startup name matching

* addressing codex review

* docs: add changelog entry for PR merge
This commit is contained in:
Pavan Kumar Gondhi
2026-05-04 22:47:19 +05:30
committed by GitHub
parent 37c0520a0b
commit ea75cd8971
4 changed files with 79 additions and 8 deletions

View File

@@ -46,6 +46,7 @@ Docs: https://docs.openclaw.ai
### Fixes ### Fixes
- Gate zalouser startup name matching [AI]. (#77411) Thanks @pgondhi987.
- fix(device-pair): require pairing scope for pair command [AI]. (#76377) 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. - 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. - 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.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: 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. - Default: `channels.zalouser.groupPolicy = "open"` (groups allowed). Use `channels.defaults.groupPolicy` to override the default when unset.
- Restrict to an allowlist with: - Restrict to an allowlist with:
- `channels.zalouser.groupPolicy = "allowlist"` - `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) - `channels.zalouser.groupAllowFrom` (controls which senders in allowed groups can trigger the bot)
- Block all groups: `channels.zalouser.groupPolicy = "disabled"`. - Block all groups: `channels.zalouser.groupPolicy = "disabled"`.
- The configure wizard can prompt for group allowlists. - 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. - 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. - 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`). - 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:** **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:** **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 "./monitor.send-mocks.js";
import "./zalo-js.test-mocks.js"; import "./zalo-js.test-mocks.js";
import { resolveZalouserAccountSync } from "./accounts.js"; import { resolveZalouserAccountSync } from "./accounts.js";
import { __testing } from "./monitor.js"; import { __testing, monitorZalouserProvider } from "./monitor.js";
import { import {
sendDeliveredZalouserMock, sendDeliveredZalouserMock,
sendMessageZalouserMock, sendMessageZalouserMock,
@@ -13,6 +13,11 @@ import {
import { setZalouserRuntime } from "./runtime.js"; import { setZalouserRuntime } from "./runtime.js";
import { createZalouserRuntimeEnv } from "./test-helpers.js"; import { createZalouserRuntimeEnv } from "./test-helpers.js";
import type { ResolvedZalouserAccount, ZaloInboundMessage } from "./types.js"; import type { ResolvedZalouserAccount, ZaloInboundMessage } from "./types.js";
import {
listZaloFriendsMock,
listZaloGroupsMock,
startZaloListenerMock,
} from "./zalo-js.test-mocks.js";
function createAccount(): ResolvedZalouserAccount { function createAccount(): ResolvedZalouserAccount {
return { return {
@@ -341,6 +346,12 @@ describe("zalouser monitor group mention gating", () => {
sendTypingZalouserMock.mockClear(); sendTypingZalouserMock.mockClear();
sendDeliveredZalouserMock.mockClear(); sendDeliveredZalouserMock.mockClear();
sendSeenZalouserMock.mockClear(); sendSeenZalouserMock.mockClear();
listZaloFriendsMock.mockReset();
listZaloFriendsMock.mockResolvedValue([]);
listZaloGroupsMock.mockReset();
listZaloGroupsMock.mockResolvedValue([]);
startZaloListenerMock.mockReset();
startZaloListenerMock.mockResolvedValue({ stop: vi.fn() });
}); });
async function processMessageWithDefaults(params: { async function processMessageWithDefaults(params: {
@@ -374,6 +385,23 @@ describe("zalouser monitor group mention gating", () => {
expect(sendTypingZalouserMock).not.toHaveBeenCalled(); 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: { async function expectGroupCommandAuthorizers(params: {
accountConfig: ResolvedZalouserAccount["config"]; accountConfig: ResolvedZalouserAccount["config"];
expectedAuthorizers: Array<{ configured: boolean; allowed: boolean }>; 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"); 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 () => { it("allows group control commands when sender is in groupAllowFrom", async () => {
await expectGroupCommandAuthorizers({ await expectGroupCommandAuthorizers({
accountConfig: { accountConfig: {

View File

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