From 9fd47a5aed783c20d0bf29a652aa1ad4a642bbff Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Mon, 6 Apr 2026 23:22:01 -0400 Subject: [PATCH] Matrix: prompt invite auto-join during onboarding (#62168) Merged via squash. Prepared head SHA: aec7a2249a5e3b491a2901b40f932ef43a9587de Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com> Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com> Reviewed-by: @gumadeiras --- CHANGELOG.md | 1 + docs/channels/matrix.md | 8 +- .../matrix/src/matrix/config-update.test.ts | 38 ++ extensions/matrix/src/matrix/config-update.ts | 20 +- .../matrix/src/onboarding.test-harness.ts | 8 + extensions/matrix/src/onboarding.test.ts | 182 ++++++++++ extensions/matrix/src/onboarding.ts | 324 +++++++++++++----- extensions/matrix/src/runtime-api.ts | 1 + 8 files changed, 487 insertions(+), 95 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c622295ffc1..02c67667ecf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,6 +52,7 @@ Docs: https://docs.openclaw.ai - Hooks/wake: queue direct and mapped wake-hook payloads as untrusted system events so external wake content no longer enters the main session as trusted input. (#62003) - Slack/thread mentions: add `channels.slack.thread.requireExplicitMention` so Slack channels that already require mentions can also require explicit `@bot` mentions inside bot-participated threads. (#58276) Thanks @praktika-engineer. - UI/light mode: target both root and nested WebKit scrollbar thumbs in the light theme so page-level and container scrollbars stay visible on light backgrounds. (#61753) Thanks @chziyue. +- Matrix/onboarding: add an invite auto-join setup step with explicit off warnings and strict stable-target validation so new Matrix accounts stop silently ignoring invited rooms and fresh DM-style invites unless operators opt in. (#62168) Thanks @gumadeiras. ## 2026.4.5 diff --git a/docs/channels/matrix.md b/docs/channels/matrix.md index 40d5eb12ec3..dd5dfd2720a 100644 --- a/docs/channels/matrix.md +++ b/docs/channels/matrix.md @@ -61,13 +61,17 @@ What the Matrix wizard actually asks for: - optional device name - 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: -- If Matrix auth env vars already exist for the selected account, and that account does not already have auth saved in config, the wizard offers an env shortcut and only writes `enabled: true` for that account. +- If Matrix auth env vars already exist for the selected account, and that account does not already have auth saved in config, the wizard offers an env shortcut so setup can keep auth in env vars instead of copying secrets into config. - When you add another Matrix account interactively, the entered account name is normalized into the account ID used in config and env vars. For example, `Ops Bot` becomes `ops-bot`. - 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"`. @@ -77,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/matrix/config-update.test.ts b/extensions/matrix/src/matrix/config-update.test.ts index 463c2e7a46b..8fb23fced25 100644 --- a/extensions/matrix/src/matrix/config-update.test.ts +++ b/extensions/matrix/src/matrix/config-update.test.ts @@ -103,6 +103,44 @@ describe("updateMatrixAccountConfig", () => { expect(updated.channels?.["matrix"]?.accounts?.default?.proxy).toBeUndefined(); }); + it("stores and clears Matrix invite auto-join settings", () => { + const cfg = { + channels: { + matrix: { + accounts: { + default: { + autoJoin: "allowlist", + autoJoinAllowlist: ["#ops:example.org"], + }, + }, + }, + }, + } as CoreConfig; + + const allowlistUpdated = updateMatrixAccountConfig(cfg, "default", { + autoJoin: "allowlist", + autoJoinAllowlist: ["!ops-room:example.org", "#ops:example.org"], + }); + expect(allowlistUpdated.channels?.matrix?.accounts?.default).toMatchObject({ + autoJoin: "allowlist", + autoJoinAllowlist: ["!ops-room:example.org", "#ops:example.org"], + }); + + const offUpdated = updateMatrixAccountConfig(cfg, "default", { + autoJoin: "off", + autoJoinAllowlist: null, + }); + expect(offUpdated.channels?.matrix?.accounts?.default?.autoJoin).toBe("off"); + expect(offUpdated.channels?.matrix?.accounts?.default?.autoJoinAllowlist).toBeUndefined(); + + const alwaysUpdated = updateMatrixAccountConfig(cfg, "default", { + autoJoin: "always", + autoJoinAllowlist: null, + }); + expect(alwaysUpdated.channels?.matrix?.accounts?.default?.autoJoin).toBe("always"); + expect(alwaysUpdated.channels?.matrix?.accounts?.default?.autoJoinAllowlist).toBeUndefined(); + }); + it("normalizes account id and defaults account enabled=true", () => { const updated = updateMatrixAccountConfig({} as CoreConfig, "Main Bot", { name: "Main Bot", diff --git a/extensions/matrix/src/matrix/config-update.ts b/extensions/matrix/src/matrix/config-update.ts index 797461e0d81..b089ab4e1b9 100644 --- a/extensions/matrix/src/matrix/config-update.ts +++ b/extensions/matrix/src/matrix/config-update.ts @@ -30,6 +30,8 @@ export type MatrixAccountPatch = { encryption?: boolean | null; initialSyncLimit?: number | null; allowBots?: MatrixConfig["allowBots"] | null; + autoJoin?: MatrixConfig["autoJoin"] | null; + autoJoinAllowlist?: MatrixConfig["autoJoinAllowlist"] | null; dm?: MatrixConfig["dm"] | null; groupPolicy?: MatrixConfig["groupPolicy"] | null; groupAllowFrom?: MatrixConfig["groupAllowFrom"] | null; @@ -203,6 +205,14 @@ export function updateMatrixAccountConfig( nextAccount.allowBots = patch.allowBots; } } + if (patch.autoJoin !== undefined) { + if (patch.autoJoin === null) { + delete nextAccount.autoJoin; + } else { + nextAccount.autoJoin = patch.autoJoin; + } + } + applyNullableArrayField(nextAccount, "autoJoinAllowlist", patch.autoJoinAllowlist); if (patch.dm !== undefined) { if (patch.dm === null) { delete nextAccount.dm; @@ -245,16 +255,20 @@ export function updateMatrixAccountConfig( ); if (shouldStoreMatrixAccountAtTopLevel(cfg, normalizedAccountId)) { - const { accounts: _ignoredAccounts, defaultAccount, ...baseMatrix } = matrix; + const { accounts: _ignoredAccounts, defaultAccount } = matrix; + const { + accounts: _ignoredNextAccounts, + defaultAccount: _ignoredNextDefaultAccount, + ...topLevelAccount + } = nextAccount; return { ...cfg, channels: { ...cfg.channels, matrix: { - ...baseMatrix, ...(defaultAccount ? { defaultAccount } : {}), enabled: true, - ...nextAccount, + ...topLevelAccount, }, }, }; diff --git a/extensions/matrix/src/onboarding.test-harness.ts b/extensions/matrix/src/onboarding.test-harness.ts index 4fab92ad3f4..bc689d9c68f 100644 --- a/extensions/matrix/src/onboarding.test-harness.ts +++ b/extensions/matrix/src/onboarding.test-harness.ts @@ -117,13 +117,18 @@ export async function runMatrixAddAccountAllowlistConfigure(params: { cfg: CoreConfig; allowFromInput: string; roomsAllowlistInput: string; + autoJoinPolicy?: "always" | "allowlist" | "off"; + autoJoinAllowlistInput?: string; deviceName?: string; + notes?: string[]; }) { const prompter = createMatrixWizardPrompter({ + notes: params.notes, select: { "Matrix already configured. What do you want to do?": "add-account", "Matrix auth method": "token", "Matrix rooms access": "allowlist", + "Matrix invite auto-join": params.autoJoinPolicy ?? "allowlist", }, text: { "Matrix account name": "ops", @@ -132,10 +137,13 @@ export async function runMatrixAddAccountAllowlistConfigure(params: { "Matrix device name (optional)": params.deviceName ?? "", "Matrix allowFrom (full @user:server; display name only if unique)": params.allowFromInput, "Matrix rooms allowlist (comma-separated)": params.roomsAllowlistInput, + "Matrix invite auto-join allowlist (comma-separated)": + params.autoJoinAllowlistInput ?? "#ops-invites:example.org", }, confirm: { "Enable end-to-end encryption (E2EE)?": false, "Configure Matrix rooms access?": true, + "Configure Matrix invite auto-join?": true, }, onConfirm: async () => false, }); diff --git a/extensions/matrix/src/onboarding.test.ts b/extensions/matrix/src/onboarding.test.ts index ce50090a7a0..aaa0c4f8e31 100644 --- a/extensions/matrix/src/onboarding.test.ts +++ b/extensions/matrix/src/onboarding.test.ts @@ -85,6 +85,72 @@ describe("matrix onboarding", () => { ).toBe(true); }); + it("routes env-shortcut add-account flow through Matrix invite auto-join setup", async () => { + installMatrixTestRuntime(); + + process.env.MATRIX_HOMESERVER = "https://matrix.env.example.org"; + process.env.MATRIX_USER_ID = "@env:example.org"; + process.env.MATRIX_PASSWORD = "env-password"; // pragma: allowlist secret + process.env.MATRIX_ACCESS_TOKEN = ""; + process.env.MATRIX_OPS_HOMESERVER = "https://matrix.ops.env.example.org"; + process.env.MATRIX_OPS_ACCESS_TOKEN = "ops-env-token"; + + const notes: string[] = []; + const prompter = createMatrixWizardPrompter({ + notes, + select: { + "Matrix already configured. What do you want to do?": "add-account", + "Matrix rooms access": "allowlist", + "Matrix invite auto-join": "allowlist", + }, + text: { + "Matrix account name": "ops", + "Matrix rooms allowlist (comma-separated)": "!ops-room:example.org", + "Matrix invite auto-join allowlist (comma-separated)": "#ops-invites:example.org", + }, + confirm: { + "Configure Matrix rooms access?": true, + "Configure Matrix invite auto-join?": true, + }, + onConfirm: (message) => message.startsWith("Matrix env vars detected"), + }); + + const result = await runMatrixInteractiveConfigure({ + cfg: { + channels: { + matrix: { + accounts: { + default: { + homeserver: "https://matrix.main.example.org", + accessToken: "main-token", + }, + }, + }, + }, + } as CoreConfig, + prompter, + shouldPromptAccountIds: true, + configured: true, + }); + + expect(result).not.toBe("skip"); + if (result === "skip") { + return; + } + + expect(result.accountId).toBe("ops"); + expect(result.cfg.channels?.matrix?.accounts?.ops).toMatchObject({ + enabled: true, + groupPolicy: "allowlist", + groups: { + "!ops-room:example.org": { enabled: true }, + }, + autoJoin: "allowlist", + autoJoinAllowlist: ["#ops-invites:example.org"], + }); + expect(notes.join("\n")).toContain("WARNING: Matrix invite auto-join defaults to off."); + }); + it("promotes legacy top-level Matrix config before adding a named account", async () => { installMatrixTestRuntime(); @@ -289,6 +355,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?": false, }, }); @@ -353,6 +420,7 @@ describe("matrix onboarding", () => { it("writes allowlists and room access to the selected Matrix account", async () => { installMatrixTestRuntime(); + const notes: string[] = []; const result = await runMatrixAddAccountAllowlistConfigure({ cfg: { @@ -369,7 +437,9 @@ describe("matrix onboarding", () => { } as CoreConfig, allowFromInput: "@alice:example.org", roomsAllowlistInput: "!ops-room:example.org", + autoJoinAllowlistInput: "#ops-invites:example.org", deviceName: "Ops Gateway", + notes, }); expect(result).not.toBe("skip"); @@ -387,12 +457,124 @@ describe("matrix onboarding", () => { allowFrom: ["@alice:example.org"], }, groupPolicy: "allowlist", + autoJoin: "allowlist", + autoJoinAllowlist: ["#ops-invites:example.org"], groups: { "!ops-room:example.org": { enabled: true }, }, }); expect(result.cfg.channels?.["matrix"]?.dm).toBeUndefined(); expect(result.cfg.channels?.["matrix"]?.groups).toBeUndefined(); + expect(notes.join("\n")).toContain("WARNING: Matrix invite auto-join defaults to off."); + }); + + 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", + }, + 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, + }, + }); + + const result = await runMatrixInteractiveConfigure({ + cfg: { + channels: { + matrix: { + homeserver: "https://matrix.example.org", + accessToken: "matrix-token", + autoJoin: "allowlist", + autoJoinAllowlist: ["#ops:example.org"], + }, + }, + } as CoreConfig, + prompter, + configured: true, + }); + + expect(result).not.toBe("skip"); + if (result === "skip") { + return; + } + + 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 e57d83dcd3e..4905ddab6e9 100644 --- a/extensions/matrix/src/onboarding.ts +++ b/extensions/matrix/src/onboarding.ts @@ -30,13 +30,40 @@ import { normalizeAccountId, promptAccountId, promptChannelAccessConfig, + splitSetupEntries, type RuntimeEnv, type WizardPrompter, } from "./runtime-api.js"; import { moveSingleMatrixAccountConfigToNamedAccount } from "./setup-config.js"; -import type { CoreConfig } from "./types.js"; +import type { CoreConfig, MatrixConfig } from "./types.js"; const channel = "matrix" as const; +type MatrixInviteAutoJoinPolicy = NonNullable; + +const matrixInviteAutoJoinOptions: Array<{ + value: MatrixInviteAutoJoinPolicy; + label: string; +}> = [ + { value: "allowlist", label: "Allowlist (recommended)" }, + { value: "always", label: "Always (join every invite)" }, + { value: "off", label: "Off (do not auto-join invites)" }, +]; + +function isMatrixInviteAutoJoinPolicy(value: string): value is MatrixInviteAutoJoinPolicy { + 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( @@ -95,12 +122,6 @@ async function promptMatrixAllowFrom(params: { const account = resolveMatrixAccount({ cfg, accountId }); const canResolve = Boolean(account.configured); - const parseInput = (raw: string) => - raw - .split(/[\n,;]+/g) - .map((entry) => entry.trim()) - .filter(Boolean); - const isFullUserId = (value: string) => value.startsWith("@") && value.includes(":"); while (true) { @@ -110,7 +131,7 @@ async function promptMatrixAllowFrom(params: { initialValue: existingAllowFrom[0] ? String(existingAllowFrom[0]) : undefined, validate: (value) => (String(value ?? "").trim() ? undefined : "Required"), }); - const parts = parseInput(String(entry)); + const parts = splitSetupEntries(String(entry)); const resolvedIds: string[] = []; const pending: string[] = []; const unresolved: string[] = []; @@ -187,6 +208,200 @@ function setMatrixGroupRooms(cfg: CoreConfig, roomKeys: string[], accountId?: st }); } +function setMatrixAutoJoin( + cfg: CoreConfig, + autoJoin: MatrixInviteAutoJoinPolicy, + autoJoinAllowlist: string[], + accountId?: string, +) { + return updateMatrixAccountConfig(cfg, resolveMatrixOnboardingAccountId(cfg, accountId), { + autoJoin, + autoJoinAllowlist: autoJoin === "allowlist" ? autoJoinAllowlist : null, + }); +} + +async function configureMatrixInviteAutoJoin(params: { + cfg: CoreConfig; + prompter: WizardPrompter; + accountId?: string; +}): Promise { + const accountId = resolveMatrixOnboardingAccountId(params.cfg, params.accountId); + const existingConfig = resolveMatrixAccountConfig({ cfg: params.cfg, accountId }); + const currentPolicy = existingConfig.autoJoin ?? "off"; + const currentAllowlist = (existingConfig.autoJoinAllowlist ?? []).map((entry) => String(entry)); + const hasExistingConfig = existingConfig.autoJoin !== undefined || currentAllowlist.length > 0; + + await params.prompter.note( + [ + "WARNING: Matrix invite auto-join defaults to off.", + "OpenClaw agents will not join invited rooms or fresh DM-style invites unless you set autoJoin.", + 'Choose "allowlist" to restrict joins or "always" to join every invite.', + ].join("\n"), + "Matrix invite auto-join", + ); + + const wants = await params.prompter.confirm({ + message: hasExistingConfig + ? "Update Matrix invite auto-join?" + : "Configure Matrix invite auto-join?", + initialValue: hasExistingConfig ? currentPolicy !== "off" : true, + }); + if (!wants) { + return params.cfg; + } + + const selectedPolicy = await params.prompter.select({ + message: "Matrix invite auto-join", + options: matrixInviteAutoJoinOptions, + initialValue: currentPolicy, + }); + if (!isMatrixInviteAutoJoinPolicy(selectedPolicy)) { + throw new Error(`Unsupported Matrix invite auto-join policy: ${String(selectedPolicy)}`); + } + const policy = selectedPolicy; + + 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); + } + + 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: { + cfg: CoreConfig; + prompter: WizardPrompter; + forceAllowFrom: boolean; + accountId: string; +}): Promise { + let next = params.cfg; + + if (params.forceAllowFrom) { + next = await promptMatrixAllowFrom({ + cfg: next, + prompter: params.prompter, + accountId: params.accountId, + }); + } + + const existingAccountConfig = resolveMatrixAccountConfig({ + cfg: next, + accountId: params.accountId, + }); + const existingGroups = existingAccountConfig.groups ?? existingAccountConfig.rooms; + const accessConfig = await promptChannelAccessConfig({ + prompter: params.prompter, + label: "Matrix rooms", + currentPolicy: existingAccountConfig.groupPolicy ?? "allowlist", + currentEntries: Object.keys(existingGroups ?? {}), + placeholder: "!roomId:server, #alias:server, Project Room", + updatePrompt: Boolean(existingGroups), + }); + if (accessConfig) { + if (accessConfig.policy !== "allowlist") { + next = setMatrixGroupPolicy(next, accessConfig.policy, params.accountId); + } else { + let roomKeys = accessConfig.entries; + if (accessConfig.entries.length > 0) { + try { + const resolvedIds: string[] = []; + const unresolved: string[] = []; + for (const entry of accessConfig.entries) { + const trimmed = entry.trim(); + if (!trimmed) { + continue; + } + const cleaned = trimmed.replace(/^(room|channel):/i, "").trim(); + if (cleaned.startsWith("!") && cleaned.includes(":")) { + resolvedIds.push(cleaned); + continue; + } + const matches = await listMatrixDirectoryGroupsLive({ + cfg: next, + accountId: params.accountId, + query: trimmed, + limit: 10, + }); + const exact = matches.find( + (match) => (match.name ?? "").toLowerCase() === trimmed.toLowerCase(), + ); + const best = exact ?? matches[0]; + if (best?.id) { + resolvedIds.push(best.id); + } else { + unresolved.push(entry); + } + } + roomKeys = [...resolvedIds, ...unresolved.map((entry) => entry.trim()).filter(Boolean)]; + if (resolvedIds.length > 0 || unresolved.length > 0) { + await params.prompter.note( + [ + resolvedIds.length > 0 ? `Resolved: ${resolvedIds.join(", ")}` : undefined, + unresolved.length > 0 + ? `Unresolved (kept as typed): ${unresolved.join(", ")}` + : undefined, + ] + .filter(Boolean) + .join("\n"), + "Matrix rooms", + ); + } + } catch (err) { + await params.prompter.note( + `Room lookup failed; keeping entries as typed. ${String(err)}`, + "Matrix rooms", + ); + } + } + next = setMatrixGroupPolicy(next, "allowlist", params.accountId); + next = setMatrixGroupRooms(next, roomKeys, params.accountId); + } + } + + return await configureMatrixInviteAutoJoin({ + cfg: next, + prompter: params.prompter, + accountId: params.accountId, + }); +} + const dmPolicy: ChannelSetupDmPolicy = { label: "Matrix", channel, @@ -289,13 +504,12 @@ async function runMatrixConfigure(params: { }); if (useEnv) { next = updateMatrixAccountConfig(next, accountId, { enabled: true }); - if (params.forceAllowFrom) { - next = await promptMatrixAllowFrom({ - cfg: next, - prompter: params.prompter, - accountId, - }); - } + next = await configureMatrixAccessPrompts({ + cfg: next, + prompter: params.prompter, + forceAllowFrom: params.forceAllowFrom, + accountId, + }); return { cfg: next, accountId }; } } @@ -421,84 +635,12 @@ async function runMatrixConfigure(params: { encryption: enableEncryption, }); - if (params.forceAllowFrom) { - next = await promptMatrixAllowFrom({ - cfg: next, - prompter: params.prompter, - accountId, - }); - } - - const existingAccountConfig = resolveMatrixAccountConfig({ cfg: next, accountId }); - const existingGroups = existingAccountConfig.groups ?? existingAccountConfig.rooms; - const accessConfig = await promptChannelAccessConfig({ + next = await configureMatrixAccessPrompts({ + cfg: next, prompter: params.prompter, - label: "Matrix rooms", - currentPolicy: existingAccountConfig.groupPolicy ?? "allowlist", - currentEntries: Object.keys(existingGroups ?? {}), - placeholder: "!roomId:server, #alias:server, Project Room", - updatePrompt: Boolean(existingGroups), + forceAllowFrom: params.forceAllowFrom, + accountId, }); - if (accessConfig) { - if (accessConfig.policy !== "allowlist") { - next = setMatrixGroupPolicy(next, accessConfig.policy, accountId); - } else { - let roomKeys = accessConfig.entries; - if (accessConfig.entries.length > 0) { - try { - const resolvedIds: string[] = []; - const unresolved: string[] = []; - for (const entry of accessConfig.entries) { - const trimmed = entry.trim(); - if (!trimmed) { - continue; - } - const cleaned = trimmed.replace(/^(room|channel):/i, "").trim(); - if (cleaned.startsWith("!") && cleaned.includes(":")) { - resolvedIds.push(cleaned); - continue; - } - const matches = await listMatrixDirectoryGroupsLive({ - cfg: next, - accountId, - query: trimmed, - limit: 10, - }); - const exact = matches.find( - (match) => (match.name ?? "").toLowerCase() === trimmed.toLowerCase(), - ); - const best = exact ?? matches[0]; - if (best?.id) { - resolvedIds.push(best.id); - } else { - unresolved.push(entry); - } - } - roomKeys = [...resolvedIds, ...unresolved.map((entry) => entry.trim()).filter(Boolean)]; - if (resolvedIds.length > 0 || unresolved.length > 0) { - await params.prompter.note( - [ - resolvedIds.length > 0 ? `Resolved: ${resolvedIds.join(", ")}` : undefined, - unresolved.length > 0 - ? `Unresolved (kept as typed): ${unresolved.join(", ")}` - : undefined, - ] - .filter(Boolean) - .join("\n"), - "Matrix rooms", - ); - } - } catch (err) { - await params.prompter.note( - `Room lookup failed; keeping entries as typed. ${String(err)}`, - "Matrix rooms", - ); - } - } - next = setMatrixGroupPolicy(next, "allowlist", accountId); - next = setMatrixGroupRooms(next, roomKeys, accountId); - } - } return { cfg: next, accountId }; } diff --git a/extensions/matrix/src/runtime-api.ts b/extensions/matrix/src/runtime-api.ts index 9d2b0be676a..55e12329cd8 100644 --- a/extensions/matrix/src/runtime-api.ts +++ b/extensions/matrix/src/runtime-api.ts @@ -57,6 +57,7 @@ export { moveSingleAccountChannelSectionToDefaultAccount, promptAccountId, promptChannelAccessConfig, + splitSetupEntries, } from "openclaw/plugin-sdk/setup"; export type { RuntimeEnv } from "openclaw/plugin-sdk/runtime"; export {