Skip to content

Commit ea75cd8

Browse files
authored
Gate zalouser startup name matching [AI] (#77411)
* fix: gate zalouser startup name matching * addressing codex review * docs: add changelog entry for PR merge
1 parent 37c0520 commit ea75cd8

4 files changed

Lines changed: 79 additions & 8 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ Docs: https://docs.openclaw.ai
4646

4747
### Fixes
4848

49+
- Gate zalouser startup name matching [AI]. (#77411) Thanks @pgondhi987.
4950
- fix(device-pair): require pairing scope for pair command [AI]. (#76377) Thanks @pgondhi987.
5051
- fix(qqbot): keep private commands off framework surface [AI]. (#77212) Thanks @pgondhi987.
5152
- 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.

docs/channels/zalouser.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,9 @@ openclaw directory groups list --channel zalouser --query "work"
8181

8282
`channels.zalouser.dmPolicy` supports: `pairing | allowlist | open | disabled` (default: `pairing`).
8383

84-
`channels.zalouser.allowFrom` accepts user IDs or names. During setup, names are resolved to IDs using the plugin's in-process contact lookup.
84+
`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.
85+
86+
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.
8587

8688
Approve via:
8789

@@ -93,13 +95,13 @@ Approve via:
9395
- Default: `channels.zalouser.groupPolicy = "open"` (groups allowed). Use `channels.defaults.groupPolicy` to override the default when unset.
9496
- Restrict to an allowlist with:
9597
- `channels.zalouser.groupPolicy = "allowlist"`
96-
- `channels.zalouser.groups` (keys should be stable group IDs; names are resolved to IDs on startup when possible)
98+
- `channels.zalouser.groups` (keys should be stable group IDs; names are resolved to IDs on startup only when `channels.zalouser.dangerouslyAllowNameMatching: true` is enabled)
9799
- `channels.zalouser.groupAllowFrom` (controls which senders in allowed groups can trigger the bot)
98100
- Block all groups: `channels.zalouser.groupPolicy = "disabled"`.
99101
- The configure wizard can prompt for group allowlists.
100-
- On startup, OpenClaw resolves group/user names in allowlists to IDs and logs the mapping.
102+
- On startup, OpenClaw resolves group/user names in allowlists to IDs and logs the mapping only when `channels.zalouser.dangerouslyAllowNameMatching: true` is enabled.
101103
- Group allowlist matching is ID-only by default. Unresolved names are ignored for auth unless `channels.zalouser.dangerouslyAllowNameMatching: true` is enabled.
102-
- `channels.zalouser.dangerouslyAllowNameMatching: true` is a break-glass compatibility mode that re-enables mutable group-name matching.
104+
- `channels.zalouser.dangerouslyAllowNameMatching: true` is a break-glass compatibility mode that re-enables mutable startup name resolution and runtime group-name matching.
103105
- If `groupAllowFrom` is unset, runtime falls back to `allowFrom` for group sender checks.
104106
- Sender checks apply to both normal group messages and control commands (for example `/new`, `/reset`).
105107

@@ -181,7 +183,7 @@ Accounts map to `zalouser` profiles in OpenClaw state. Example:
181183

182184
**Allowlist/group name didn't resolve:**
183185

184-
- Use numeric IDs in `allowFrom`/`groupAllowFrom`/`groups`, or exact friend/group names.
186+
- 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`.
185187

186188
**Upgraded from old CLI-based setup:**
187189

extensions/zalouser/src/monitor.group-gating.test.ts

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { OpenClawConfig, PluginRuntime } from "../runtime-api.js";
33
import "./monitor.send-mocks.js";
44
import "./zalo-js.test-mocks.js";
55
import { resolveZalouserAccountSync } from "./accounts.js";
6-
import { __testing } from "./monitor.js";
6+
import { __testing, monitorZalouserProvider } from "./monitor.js";
77
import {
88
sendDeliveredZalouserMock,
99
sendMessageZalouserMock,
@@ -13,6 +13,11 @@ import {
1313
import { setZalouserRuntime } from "./runtime.js";
1414
import { createZalouserRuntimeEnv } from "./test-helpers.js";
1515
import type { ResolvedZalouserAccount, ZaloInboundMessage } from "./types.js";
16+
import {
17+
listZaloFriendsMock,
18+
listZaloGroupsMock,
19+
startZaloListenerMock,
20+
} from "./zalo-js.test-mocks.js";
1621

1722
function createAccount(): ResolvedZalouserAccount {
1823
return {
@@ -341,6 +346,12 @@ describe("zalouser monitor group mention gating", () => {
341346
sendTypingZalouserMock.mockClear();
342347
sendDeliveredZalouserMock.mockClear();
343348
sendSeenZalouserMock.mockClear();
349+
listZaloFriendsMock.mockReset();
350+
listZaloFriendsMock.mockResolvedValue([]);
351+
listZaloGroupsMock.mockReset();
352+
listZaloGroupsMock.mockResolvedValue([]);
353+
startZaloListenerMock.mockReset();
354+
startZaloListenerMock.mockResolvedValue({ stop: vi.fn() });
344355
});
345356

346357
async function processMessageWithDefaults(params: {
@@ -374,6 +385,23 @@ describe("zalouser monitor group mention gating", () => {
374385
expect(sendTypingZalouserMock).not.toHaveBeenCalled();
375386
}
376387

388+
async function startMonitorForStartupResolution(
389+
accountConfig: ResolvedZalouserAccount["config"],
390+
) {
391+
installRuntime({ commandAuthorized: false });
392+
const abortController = new AbortController();
393+
abortController.abort();
394+
await monitorZalouserProvider({
395+
account: {
396+
...createAccount(),
397+
config: accountConfig,
398+
},
399+
config: createConfig(),
400+
runtime: createRuntimeEnv(),
401+
abortSignal: abortController.signal,
402+
});
403+
}
404+
377405
async function expectGroupCommandAuthorizers(params: {
378406
accountConfig: ResolvedZalouserAccount["config"];
379407
expectedAuthorizers: Array<{ configured: boolean; allowed: boolean }>;
@@ -669,6 +697,45 @@ describe("zalouser monitor group mention gating", () => {
669697
expect(callArg?.ctx?.To).toBe("zalouser:group:g-attacker-001");
670698
});
671699

700+
it("does not resolve mutable allowlist or group names at startup by default", async () => {
701+
listZaloFriendsMock.mockResolvedValue([{ userId: "999", displayName: "Alice" }]);
702+
listZaloGroupsMock.mockResolvedValue([{ groupId: "g-other", name: "Trusted Team" }]);
703+
704+
await startMonitorForStartupResolution({
705+
...createAccount().config,
706+
dmPolicy: "allowlist",
707+
allowFrom: ["Alice"],
708+
groupPolicy: "allowlist",
709+
groupAllowFrom: ["Alice"],
710+
groups: {
711+
"Trusted Team": { enabled: true },
712+
},
713+
});
714+
715+
expect(listZaloFriendsMock).not.toHaveBeenCalled();
716+
expect(listZaloGroupsMock).not.toHaveBeenCalled();
717+
});
718+
719+
it("resolves mutable allowlist and group names at startup when enabled", async () => {
720+
listZaloFriendsMock.mockResolvedValue([{ userId: "123", displayName: "Alice" }]);
721+
listZaloGroupsMock.mockResolvedValue([{ groupId: "g-trusted", name: "Trusted Team" }]);
722+
723+
await startMonitorForStartupResolution({
724+
...createAccount().config,
725+
dangerouslyAllowNameMatching: true,
726+
dmPolicy: "allowlist",
727+
allowFrom: ["Alice"],
728+
groupPolicy: "allowlist",
729+
groupAllowFrom: ["Alice"],
730+
groups: {
731+
"Trusted Team": { enabled: true },
732+
},
733+
});
734+
735+
expect(listZaloFriendsMock).toHaveBeenCalledWith("default");
736+
expect(listZaloGroupsMock).toHaveBeenCalledWith("default");
737+
});
738+
672739
it("allows group control commands when sender is in groupAllowFrom", async () => {
673740
await expectGroupCommandAuthorizers({
674741
accountConfig: {

extensions/zalouser/src/monitor.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -828,8 +828,9 @@ export async function monitorZalouserProvider(
828828
const groupAllowFromEntries = (account.config.groupAllowFrom ?? [])
829829
.map((entry) => normalizeZalouserEntry(String(entry)))
830830
.filter((entry) => entry && entry !== "*");
831+
const allowNameMatching = isDangerousNameMatchingEnabled(account.config);
831832

832-
if (allowFromEntries.length > 0 || groupAllowFromEntries.length > 0) {
833+
if (allowNameMatching && (allowFromEntries.length > 0 || groupAllowFromEntries.length > 0)) {
833834
const friends = await listZaloFriends(profile);
834835
const byName = buildNameIndex(friends, (friend) => friend.displayName);
835836
if (allowFromEntries.length > 0) {
@@ -869,7 +870,7 @@ export async function monitorZalouserProvider(
869870

870871
const groupsConfig = account.config.groups ?? {};
871872
const groupKeys = Object.keys(groupsConfig).filter((key) => key !== "*");
872-
if (groupKeys.length > 0) {
873+
if (allowNameMatching && groupKeys.length > 0) {
873874
const groups = await listZaloGroups(profile);
874875
const byName = buildNameIndex(groups, (group) => group.name);
875876
const mapping: string[] = [];

0 commit comments

Comments
 (0)