From 145a7fe9efaa6c629ecae5a533d458b326a7d271 Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Mon, 6 Apr 2026 20:46:58 -0400 Subject: [PATCH] fix: tighten Matrix invite auto-join onboarding --- docs/channels/matrix.md | 4 ++ extensions/matrix/src/onboarding.test.ts | 65 +++++++++++++++++++++++ extensions/matrix/src/onboarding.ts | 66 +++++++++++++++++++----- 3 files changed, 121 insertions(+), 14 deletions(-) diff --git a/docs/channels/matrix.md b/docs/channels/matrix.md index 72609f2a8ec..dd5dfd2720a 100644 --- a/docs/channels/matrix.md +++ b/docs/channels/matrix.md @@ -62,6 +62,7 @@ What the Matrix wizard actually asks for: - whether to enable E2EE - whether to configure Matrix room access now - whether to configure Matrix invite auto-join now +- when invite auto-join is enabled, whether it should be `allowlist`, `always`, or `off` Wizard behavior that matters: @@ -70,6 +71,7 @@ Wizard behavior that matters: - DM allowlist prompts accept full `@user:server` values immediately. Display names only work when live directory lookup finds one exact match; otherwise the wizard asks you to retry with a full Matrix ID. - Room allowlist prompts accept room IDs and aliases directly. They can also resolve joined-room names live, but unresolved names are only kept as typed during setup and are ignored later by runtime allowlist resolution. Prefer `!room:server` or `#alias:server`. - The wizard now shows an explicit warning before the invite auto-join step because `channels.matrix.autoJoin` defaults to `off`; agents will not join invited rooms or fresh DM-style invites unless you set it. +- In invite auto-join allowlist mode, use only stable invite targets: `!roomId:server`, `#alias:server`, or `*`. Plain room names are rejected. - Runtime room/session identity uses the stable Matrix room ID. Room-declared aliases are only used as lookup inputs, not as the long-term session key or stable group identity. - To resolve room names before saving them, use `openclaw channels resolve --channel matrix "Project Room"`. @@ -79,6 +81,8 @@ Wizard behavior that matters: If you leave it unset, the bot will not join invited rooms or fresh DM-style invites, so it will not appear in new groups or invited DMs unless you join manually first. Set `autoJoin: "allowlist"` together with `autoJoinAllowlist` to restrict which invites it accepts, or set `autoJoin: "always"` if you want it to join every invite. + +In `allowlist` mode, `autoJoinAllowlist` only accepts `!roomId:server`, `#alias:server`, or `*`. Allowlist example: diff --git a/extensions/matrix/src/onboarding.test.ts b/extensions/matrix/src/onboarding.test.ts index 99939f3c4b0..aaa0c4f8e31 100644 --- a/extensions/matrix/src/onboarding.test.ts +++ b/extensions/matrix/src/onboarding.test.ts @@ -470,8 +470,10 @@ describe("matrix onboarding", () => { it("clears Matrix invite auto-join allowlists when switching auto-join off", async () => { installMatrixTestRuntime(); + const notes: string[] = []; const prompter = createMatrixWizardPrompter({ + notes, select: { "Matrix already configured. What do you want to do?": "update", "Matrix invite auto-join": "off", @@ -484,6 +486,7 @@ describe("matrix onboarding", () => { "Matrix credentials already configured. Keep them?": true, "Enable end-to-end encryption (E2EE)?": false, "Configure Matrix rooms access?": false, + "Configure Matrix invite auto-join?": true, "Update Matrix invite auto-join?": true, }, }); @@ -510,6 +513,68 @@ describe("matrix onboarding", () => { expect(result.cfg.channels?.matrix?.autoJoin).toBe("off"); expect(result.cfg.channels?.matrix?.autoJoinAllowlist).toBeUndefined(); + expect(notes.join("\n")).toContain("Matrix invite auto-join remains off."); + expect(notes.join("\n")).toContain( + "Agents will not join invited rooms or fresh DM-style invites until you change autoJoin.", + ); + }); + + it("re-prompts Matrix invite auto-join allowlists until entries are stable invite targets", async () => { + installMatrixTestRuntime(); + const notes: string[] = []; + let inviteAllowlistPrompts = 0; + + const prompter = createMatrixWizardPrompter({ + notes, + select: { + "Matrix already configured. What do you want to do?": "update", + "Matrix invite auto-join": "allowlist", + }, + text: { + "Matrix homeserver URL": "https://matrix.example.org", + "Matrix device name (optional)": "OpenClaw Gateway", + }, + confirm: { + "Matrix credentials already configured. Keep them?": true, + "Enable end-to-end encryption (E2EE)?": false, + "Configure Matrix rooms access?": false, + "Configure Matrix invite auto-join?": true, + "Update Matrix invite auto-join?": true, + }, + onText: async (message) => { + if (message === "Matrix invite auto-join allowlist (comma-separated)") { + inviteAllowlistPrompts += 1; + return inviteAllowlistPrompts === 1 ? "Project Room" : "#ops:example.org"; + } + throw new Error(`unexpected text prompt: ${message}`); + }, + }); + + const result = await runMatrixInteractiveConfigure({ + cfg: { + channels: { + matrix: { + homeserver: "https://matrix.example.org", + accessToken: "matrix-token", + }, + }, + } as CoreConfig, + prompter, + configured: true, + }); + + expect(result).not.toBe("skip"); + if (result === "skip") { + return; + } + + expect(inviteAllowlistPrompts).toBe(2); + expect(result.cfg.channels?.matrix?.autoJoin).toBe("allowlist"); + expect(result.cfg.channels?.matrix?.autoJoinAllowlist).toEqual(["#ops:example.org"]); + expect(notes.join("\n")).toContain( + "Use only stable Matrix invite targets for auto-join: !roomId:server, #alias:server, or *.", + ); + expect(notes.join("\n")).toContain("Invalid: Project Room"); }); it("reports account-scoped DM config keys for named accounts", () => { diff --git a/extensions/matrix/src/onboarding.ts b/extensions/matrix/src/onboarding.ts index 2753920eb88..3d57ac751ca 100644 --- a/extensions/matrix/src/onboarding.ts +++ b/extensions/matrix/src/onboarding.ts @@ -53,6 +53,18 @@ function isMatrixInviteAutoJoinPolicy(value: string): value is MatrixInviteAutoJ return value === "allowlist" || value === "always" || value === "off"; } +function isMatrixInviteAutoJoinTarget(entry: string): boolean { + return ( + entry === "*" || + (entry.startsWith("!") && entry.includes(":")) || + (entry.startsWith("#") && entry.includes(":")) + ); +} + +function normalizeMatrixInviteAutoJoinTargets(entries: string[]): string[] { + return [...new Set(entries.map((entry) => entry.trim()).filter(Boolean))]; +} + function resolveMatrixOnboardingAccountId(cfg: CoreConfig, accountId?: string): string { return normalizeAccountId( accountId?.trim() || resolveDefaultMatrixAccountId(cfg) || DEFAULT_ACCOUNT_ID, @@ -248,23 +260,49 @@ async function configureMatrixInviteAutoJoin(params: { } const policy = selectedPolicy; - if (policy !== "allowlist") { + if (policy === "off") { + await params.prompter.note( + [ + "Matrix invite auto-join remains off.", + "Agents will not join invited rooms or fresh DM-style invites until you change autoJoin.", + ].join("\n"), + "Matrix invite auto-join", + ); return setMatrixAutoJoin(params.cfg, policy, [], accountId); } - const rawAllowlist = String( - await params.prompter.text({ - message: "Matrix invite auto-join allowlist (comma-separated)", - placeholder: "!roomId:server, #alias:server, *", - initialValue: currentAllowlist[0] ? currentAllowlist.join(", ") : undefined, - validate: (value) => { - const entries = splitSetupEntries(String(value ?? "")); - return entries.length > 0 ? undefined : "Required"; - }, - }), - ); - const allowlist = splitSetupEntries(rawAllowlist); - return setMatrixAutoJoin(params.cfg, "allowlist", allowlist, accountId); + if (policy === "always") { + return setMatrixAutoJoin(params.cfg, policy, [], accountId); + } + + while (true) { + const rawAllowlist = String( + await params.prompter.text({ + message: "Matrix invite auto-join allowlist (comma-separated)", + placeholder: "!roomId:server, #alias:server, *", + initialValue: currentAllowlist[0] ? currentAllowlist.join(", ") : undefined, + validate: (value) => { + const entries = splitSetupEntries(String(value ?? "")); + return entries.length > 0 ? undefined : "Required"; + }, + }), + ); + const allowlist = normalizeMatrixInviteAutoJoinTargets(splitSetupEntries(rawAllowlist)); + const invalidEntries = allowlist.filter((entry) => !isMatrixInviteAutoJoinTarget(entry)); + if (allowlist.length === 0 || invalidEntries.length > 0) { + await params.prompter.note( + [ + "Use only stable Matrix invite targets for auto-join: !roomId:server, #alias:server, or *.", + invalidEntries.length > 0 ? `Invalid: ${invalidEntries.join(", ")}` : undefined, + ] + .filter(Boolean) + .join("\n"), + "Matrix invite auto-join", + ); + continue; + } + return setMatrixAutoJoin(params.cfg, "allowlist", allowlist, accountId); + } } async function configureMatrixAccessPrompts(params: {