import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/account-id"; import { type ChannelSetupDmPolicy, type ChannelSetupWizardAdapter, } from "openclaw/plugin-sdk/setup"; import { isPrivateNetworkOptInEnabled } from "openclaw/plugin-sdk/ssrf-runtime"; import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime"; import { requiresExplicitMatrixDefaultAccount } from "./account-selection.js"; import { listMatrixDirectoryGroupsLive } from "./directory-live.js"; import { listMatrixAccountIds, resolveDefaultMatrixAccountId, resolveMatrixAccount, resolveMatrixAccountConfig, } from "./matrix/accounts.js"; import { resolveValidatedMatrixHomeserverUrl, validateMatrixHomeserverUrl, } from "./matrix/client.js"; import { resolveMatrixEnvAuthReadiness } from "./matrix/client/env-auth.js"; import { resolveMatrixConfigFieldPath, updateMatrixAccountConfig } from "./matrix/config-update.js"; import { ensureMatrixSdkInstalled, isMatrixSdkAvailable } from "./matrix/deps.js"; import { resolveMatrixTargets } from "./resolve-targets.js"; import type { DmPolicy } from "./runtime-api.js"; import { addWildcardAllowFrom, formatDocsLink, hasConfiguredSecretInput, isPrivateOrLoopbackHost, mergeAllowFromEntries, normalizeAccountId, promptAccountId, promptChannelAccessConfig, splitSetupEntries, type RuntimeEnv, type WizardPrompter, } from "./runtime-api.js"; import { moveSingleMatrixAccountConfigToNamedAccount } from "./setup-config.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( accountId?.trim() || resolveDefaultMatrixAccountId(cfg) || DEFAULT_ACCOUNT_ID, ); } function setMatrixDmPolicy(cfg: CoreConfig, policy: DmPolicy, accountId?: string) { const resolvedAccountId = resolveMatrixOnboardingAccountId(cfg, accountId); const existing = resolveMatrixAccountConfig({ cfg, accountId: resolvedAccountId, }); const allowFrom = policy === "open" ? addWildcardAllowFrom(existing.dm?.allowFrom) : undefined; return updateMatrixAccountConfig(cfg, resolvedAccountId, { dm: { ...existing.dm, policy, ...(allowFrom ? { allowFrom } : {}), }, }); } async function noteMatrixAuthHelp(prompter: WizardPrompter): Promise { await prompter.note( [ "Matrix requires a homeserver URL.", "Use an access token (recommended) or password login to an existing account.", "With access token: user ID is fetched automatically.", "Env vars supported: MATRIX_HOMESERVER, MATRIX_USER_ID, MATRIX_ACCESS_TOKEN, MATRIX_PASSWORD, MATRIX_DEVICE_ID, MATRIX_DEVICE_NAME.", "Per-account env vars: MATRIX__HOMESERVER, MATRIX__USER_ID, MATRIX__ACCESS_TOKEN, MATRIX__PASSWORD, MATRIX__DEVICE_ID, MATRIX__DEVICE_NAME.", `Docs: ${formatDocsLink("/channels/matrix", "channels/matrix")}`, ].join("\n"), "Matrix setup", ); } function requiresMatrixPrivateNetworkOptIn(homeserver: string): boolean { try { const parsed = new URL(homeserver); return parsed.protocol === "http:" && !isPrivateOrLoopbackHost(parsed.hostname); } catch { return false; } } async function promptMatrixAllowFrom(params: { cfg: CoreConfig; prompter: WizardPrompter; accountId?: string; }): Promise { const { cfg, prompter } = params; const accountId = resolveMatrixOnboardingAccountId(cfg, params.accountId); const existingConfig = resolveMatrixAccountConfig({ cfg, accountId }); const existingAllowFrom = existingConfig.dm?.allowFrom ?? []; const account = resolveMatrixAccount({ cfg, accountId }); const canResolve = Boolean(account.configured); const isFullUserId = (value: string) => value.startsWith("@") && value.includes(":"); while (true) { const entry = await prompter.text({ message: "Matrix allowFrom (full @user:server; display name only if unique)", placeholder: "@user:server", initialValue: existingAllowFrom[0] ? String(existingAllowFrom[0]) : undefined, validate: (value) => (String(value ?? "").trim() ? undefined : "Required"), }); const parts = splitSetupEntries(String(entry)); const resolvedIds: string[] = []; const pending: string[] = []; const unresolved: string[] = []; const unresolvedNotes: string[] = []; for (const part of parts) { if (isFullUserId(part)) { resolvedIds.push(part); continue; } if (!canResolve) { unresolved.push(part); continue; } pending.push(part); } if (pending.length > 0) { const results = await resolveMatrixTargets({ cfg, accountId, inputs: pending, kind: "user", }).catch(() => []); for (const result of results) { if (result?.resolved && result.id) { resolvedIds.push(result.id); continue; } if (result?.input) { unresolved.push(result.input); if (result.note) { unresolvedNotes.push(`${result.input}: ${result.note}`); } } } } if (unresolved.length > 0) { const details = unresolvedNotes.length > 0 ? unresolvedNotes : unresolved; await prompter.note( `Could not resolve:\n${details.join("\n")}\nUse full @user:server IDs.`, "Matrix allowlist", ); continue; } const unique = mergeAllowFromEntries(existingAllowFrom, resolvedIds); return updateMatrixAccountConfig(cfg, accountId, { dm: { ...existingConfig.dm, policy: "allowlist", allowFrom: unique, }, }); } } function setMatrixGroupPolicy( cfg: CoreConfig, groupPolicy: "open" | "allowlist" | "disabled", accountId?: string, ) { return updateMatrixAccountConfig(cfg, resolveMatrixOnboardingAccountId(cfg, accountId), { groupPolicy, }); } function setMatrixGroupRooms(cfg: CoreConfig, roomKeys: string[], accountId?: string) { const groups = Object.fromEntries(roomKeys.map((key) => [key, { enabled: true }])); return updateMatrixAccountConfig(cfg, resolveMatrixOnboardingAccountId(cfg, accountId), { groups, rooms: null, }); } 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) => normalizeLowercaseStringOrEmpty(match.name) === normalizeLowercaseStringOrEmpty(trimmed), ); 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, policyKey: "channels.matrix.dm.policy", allowFromKey: "channels.matrix.dm.allowFrom", resolveConfigKeys: (cfg, accountId) => { const effectiveAccountId = resolveMatrixOnboardingAccountId(cfg as CoreConfig, accountId); return { policyKey: resolveMatrixConfigFieldPath(cfg as CoreConfig, effectiveAccountId, "dm.policy"), allowFromKey: resolveMatrixConfigFieldPath( cfg as CoreConfig, effectiveAccountId, "dm.allowFrom", ), }; }, getCurrent: (cfg, accountId) => resolveMatrixAccountConfig({ cfg: cfg as CoreConfig, accountId: resolveMatrixOnboardingAccountId(cfg as CoreConfig, accountId), }).dm?.policy ?? "pairing", setPolicy: (cfg, policy, accountId) => setMatrixDmPolicy(cfg as CoreConfig, policy, accountId), promptAllowFrom: promptMatrixAllowFrom, }; type MatrixConfigureIntent = "update" | "add-account"; async function runMatrixConfigure(params: { cfg: CoreConfig; runtime: RuntimeEnv; prompter: WizardPrompter; forceAllowFrom: boolean; accountOverrides?: Partial>; shouldPromptAccountIds?: boolean; intent: MatrixConfigureIntent; }): Promise<{ cfg: CoreConfig; accountId: string }> { let next = params.cfg; await ensureMatrixSdkInstalled({ runtime: params.runtime, confirm: async (message) => await params.prompter.confirm({ message, initialValue: true, }), }); const defaultAccountId = resolveDefaultMatrixAccountId(next); let accountId = defaultAccountId || DEFAULT_ACCOUNT_ID; if (params.intent === "add-account") { const enteredName = String( await params.prompter.text({ message: "Matrix account name", validate: (value) => (value?.trim() ? undefined : "Required"), }), ).trim(); accountId = normalizeAccountId(enteredName); if (enteredName !== accountId) { await params.prompter.note(`Account id will be "${accountId}".`, "Matrix account"); } if (accountId !== DEFAULT_ACCOUNT_ID) { next = moveSingleMatrixAccountConfigToNamedAccount(next); } next = updateMatrixAccountConfig(next, accountId, { name: enteredName, enabled: true }); } else { const override = params.accountOverrides?.[channel]?.trim(); if (override) { accountId = normalizeAccountId(override); } else if (params.shouldPromptAccountIds) { accountId = await promptAccountId({ cfg: next, prompter: params.prompter, label: "Matrix", currentId: accountId, listAccountIds: (inputCfg) => listMatrixAccountIds(inputCfg as CoreConfig), defaultAccountId, }); } } const existing = resolveMatrixAccountConfig({ cfg: next, accountId }); const account = resolveMatrixAccount({ cfg: next, accountId }); if (!account.configured) { await noteMatrixAuthHelp(params.prompter); } const envReadiness = resolveMatrixEnvAuthReadiness(accountId, process.env); const envReady = envReadiness.ready; const envHomeserver = envReadiness.homeserver; const envUserId = envReadiness.userId; if ( envReady && !existing.homeserver && !existing.userId && !existing.accessToken && !existing.password ) { const useEnv = await params.prompter.confirm({ message: `Matrix env vars detected (${envReadiness.sourceHint}). Use env values?`, initialValue: true, }); if (useEnv) { next = updateMatrixAccountConfig(next, accountId, { enabled: true }); next = await configureMatrixAccessPrompts({ cfg: next, prompter: params.prompter, forceAllowFrom: params.forceAllowFrom, accountId, }); return { cfg: next, accountId }; } } const homeserver = String( await params.prompter.text({ message: "Matrix homeserver URL", initialValue: existing.homeserver ?? envHomeserver, validate: (value) => { try { validateMatrixHomeserverUrl(String(value ?? ""), { allowPrivateNetwork: true, }); return undefined; } catch (error) { return error instanceof Error ? error.message : "Invalid Matrix homeserver URL"; } }, }), ).trim(); const requiresAllowPrivateNetwork = requiresMatrixPrivateNetworkOptIn(homeserver); const shouldPromptAllowPrivateNetwork = requiresAllowPrivateNetwork || isPrivateNetworkOptInEnabled(existing); const allowPrivateNetwork = shouldPromptAllowPrivateNetwork ? await params.prompter.confirm({ message: "Allow private/internal Matrix homeserver traffic for this account?", initialValue: isPrivateNetworkOptInEnabled(existing) || requiresAllowPrivateNetwork, }) : false; if (requiresAllowPrivateNetwork && !allowPrivateNetwork) { throw new Error("Matrix homeserver requires explicit private-network opt-in"); } await resolveValidatedMatrixHomeserverUrl(homeserver, { dangerouslyAllowPrivateNetwork: allowPrivateNetwork, }); let accessToken = existing.accessToken; let password = existing.password; let userId = existing.userId ?? ""; if (hasConfiguredSecretInput(accessToken) || hasConfiguredSecretInput(password)) { const keep = await params.prompter.confirm({ message: "Matrix credentials already configured. Keep them?", initialValue: true, }); if (!keep) { accessToken = undefined; password = undefined; userId = ""; } } if (!hasConfiguredSecretInput(accessToken) && !hasConfiguredSecretInput(password)) { const authMode = await params.prompter.select({ message: "Matrix auth method", options: [ { value: "token", label: "Access token (user ID fetched automatically)" }, { value: "password", label: "Password (requires user ID)" }, ], }); if (authMode === "token") { accessToken = String( await params.prompter.text({ message: "Matrix access token", validate: (value) => (value?.trim() ? undefined : "Required"), }), ).trim(); password = undefined; userId = ""; } else { userId = String( await params.prompter.text({ message: "Matrix user ID", initialValue: existing.userId ?? envUserId, validate: (value) => { const raw = String(value ?? "").trim(); if (!raw) { return "Required"; } if (!raw.startsWith("@")) { return "Matrix user IDs should start with @"; } if (!raw.includes(":")) { return "Matrix user IDs should include a server (:server)"; } return undefined; }, }), ).trim(); password = String( await params.prompter.text({ message: "Matrix password", validate: (value) => (value?.trim() ? undefined : "Required"), }), ).trim(); accessToken = undefined; } } const deviceName = String( await params.prompter.text({ message: "Matrix device name (optional)", initialValue: existing.deviceName ?? "OpenClaw Gateway", }), ).trim(); const enableEncryption = await params.prompter.confirm({ message: "Enable end-to-end encryption (E2EE)?", initialValue: existing.encryption ?? false, }); next = updateMatrixAccountConfig(next, accountId, { enabled: true, homeserver, ...(shouldPromptAllowPrivateNetwork ? { allowPrivateNetwork: allowPrivateNetwork ? true : null } : {}), userId: userId || null, accessToken: accessToken ?? null, password: password ?? null, deviceName: deviceName || null, encryption: enableEncryption, }); next = await configureMatrixAccessPrompts({ cfg: next, prompter: params.prompter, forceAllowFrom: params.forceAllowFrom, accountId, }); return { cfg: next, accountId }; } export const matrixOnboardingAdapter: ChannelSetupWizardAdapter = { channel, getStatus: async ({ cfg, accountOverrides }) => { const resolvedCfg = cfg as CoreConfig; const sdkReady = isMatrixSdkAvailable(); if (!accountOverrides[channel] && requiresExplicitMatrixDefaultAccount(resolvedCfg)) { return { channel, configured: false, statusLines: ['Matrix: set "channels.matrix.defaultAccount" to select a named account'], selectionHint: !sdkReady ? "install Matrix deps" : "set defaultAccount", }; } const account = resolveMatrixAccount({ cfg: resolvedCfg, accountId: resolveMatrixOnboardingAccountId(resolvedCfg, accountOverrides[channel]), }); const configured = account.configured; return { channel, configured, statusLines: [ `Matrix: ${configured ? "configured" : "needs homeserver + access token or password"}`, ], selectionHint: !sdkReady ? "install Matrix deps" : configured ? "configured" : "needs auth", }; }, configure: async ({ cfg, runtime, prompter, forceAllowFrom, accountOverrides, shouldPromptAccountIds, }) => await runMatrixConfigure({ cfg: cfg as CoreConfig, runtime, prompter, forceAllowFrom, accountOverrides, shouldPromptAccountIds, intent: "update", }), configureInteractive: async ({ cfg, runtime, prompter, forceAllowFrom, accountOverrides, shouldPromptAccountIds, configured, }) => { if (!configured) { return await runMatrixConfigure({ cfg: cfg as CoreConfig, runtime, prompter, forceAllowFrom, accountOverrides, shouldPromptAccountIds, intent: "update", }); } const action = await prompter.select({ message: "Matrix already configured. What do you want to do?", options: [ { value: "update", label: "Modify settings" }, { value: "add-account", label: "Add account" }, { value: "skip", label: "Skip (leave as-is)" }, ], initialValue: "update", }); if (action === "skip") { return "skip"; } return await runMatrixConfigure({ cfg: cfg as CoreConfig, runtime, prompter, forceAllowFrom, accountOverrides, shouldPromptAccountIds, intent: action === "add-account" ? "add-account" : "update", }); }, afterConfigWritten: async ({ previousCfg, cfg, accountId, runtime }) => { const { runMatrixSetupBootstrapAfterConfigWrite } = await import("./setup-bootstrap.js"); await runMatrixSetupBootstrapAfterConfigWrite({ previousCfg: previousCfg as CoreConfig, cfg: cfg as CoreConfig, accountId, runtime, }); }, dmPolicy, disable: (cfg) => ({ ...(cfg as CoreConfig), channels: { ...(cfg as CoreConfig).channels, matrix: { ...(cfg as CoreConfig).channels?.["matrix"], enabled: false }, }, }), };