fix: tighten Matrix invite auto-join onboarding

This commit is contained in:
Gustavo Madeira Santana
2026-04-06 20:46:58 -04:00
parent 5812627bb2
commit 145a7fe9ef
3 changed files with 121 additions and 14 deletions

View File

@@ -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 `*`.
</Warning>
Allowlist example:

View File

@@ -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", () => {

View File

@@ -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: {