diff --git a/extensions/matrix/src/matrix/client/config.ts b/extensions/matrix/src/matrix/client/config.ts index f39e6995eb7..6f2f548c820 100644 --- a/extensions/matrix/src/matrix/client/config.ts +++ b/extensions/matrix/src/matrix/client/config.ts @@ -11,6 +11,7 @@ import { listNormalizedMatrixAccountIds, resolveMatrixBaseConfig, } from "../account-config.js"; +import { resolveMatrixConfigFieldPath } from "../config-update.js"; import { MatrixClient } from "../sdk.js"; import { ensureMatrixSdkLoggingConfigured } from "./logging.js"; import type { MatrixAuth, MatrixResolvedConfig } from "./types.js"; @@ -28,6 +29,54 @@ type MatrixEnvConfig = { deviceName?: string; }; +type MatrixConfigStringField = + | "homeserver" + | "userId" + | "accessToken" + | "password" + | "deviceId" + | "deviceName"; + +function resolveMatrixBaseConfigFieldPath(field: MatrixConfigStringField): string { + return `channels.matrix.${field}`; +} + +function readMatrixBaseConfigField( + matrix: ReturnType, + field: MatrixConfigStringField, +): string { + return clean(matrix[field], resolveMatrixBaseConfigFieldPath(field)); +} + +function readMatrixAccountConfigField( + cfg: CoreConfig, + accountId: string, + account: Partial>, + field: MatrixConfigStringField, +): string { + return clean(account[field], resolveMatrixConfigFieldPath(cfg, accountId, field)); +} + +function resolveMatrixStringField(params: { + matrix: ReturnType; + field: MatrixConfigStringField; + accountValue?: string; + scopedEnvValue?: string; + globalEnvValue?: string; +}): string { + return ( + params.accountValue || + params.scopedEnvValue || + readMatrixBaseConfigField(params.matrix, params.field) || + params.globalEnvValue || + "" + ); +} + +function clampMatrixInitialSyncLimit(value: unknown): number | undefined { + return typeof value === "number" ? Math.max(0, Math.floor(value)) : undefined; +} + function resolveGlobalMatrixEnvConfig(env: NodeJS.ProcessEnv): MatrixEnvConfig { return { homeserver: clean(env.MATRIX_HOMESERVER, "MATRIX_HOMESERVER"), @@ -100,36 +149,47 @@ export function resolveMatrixConfig( const matrix = resolveMatrixBaseConfig(cfg); const defaultScopedEnv = resolveScopedMatrixEnvConfig(DEFAULT_ACCOUNT_ID, env); const globalEnv = resolveGlobalMatrixEnvConfig(env); - const homeserver = - clean(matrix.homeserver, "channels.matrix.homeserver") || - defaultScopedEnv.homeserver || - globalEnv.homeserver; - const userId = - clean(matrix.userId, "channels.matrix.userId") || defaultScopedEnv.userId || globalEnv.userId; + const homeserver = resolveMatrixStringField({ + matrix, + field: "homeserver", + scopedEnvValue: defaultScopedEnv.homeserver, + globalEnvValue: globalEnv.homeserver, + }); + const userId = resolveMatrixStringField({ + matrix, + field: "userId", + scopedEnvValue: defaultScopedEnv.userId, + globalEnvValue: globalEnv.userId, + }); const accessToken = - clean(matrix.accessToken, "channels.matrix.accessToken") || - defaultScopedEnv.accessToken || - globalEnv.accessToken || - undefined; + resolveMatrixStringField({ + matrix, + field: "accessToken", + scopedEnvValue: defaultScopedEnv.accessToken, + globalEnvValue: globalEnv.accessToken, + }) || undefined; const password = - clean(matrix.password, "channels.matrix.password") || - defaultScopedEnv.password || - globalEnv.password || - undefined; + resolveMatrixStringField({ + matrix, + field: "password", + scopedEnvValue: defaultScopedEnv.password, + globalEnvValue: globalEnv.password, + }) || undefined; const deviceId = - clean(matrix.deviceId, "channels.matrix.deviceId") || - defaultScopedEnv.deviceId || - globalEnv.deviceId || - undefined; + resolveMatrixStringField({ + matrix, + field: "deviceId", + scopedEnvValue: defaultScopedEnv.deviceId, + globalEnvValue: globalEnv.deviceId, + }) || undefined; const deviceName = - clean(matrix.deviceName, "channels.matrix.deviceName") || - defaultScopedEnv.deviceName || - globalEnv.deviceName || - undefined; - const initialSyncLimit = - typeof matrix.initialSyncLimit === "number" - ? Math.max(0, Math.floor(matrix.initialSyncLimit)) - : undefined; + resolveMatrixStringField({ + matrix, + field: "deviceName", + scopedEnvValue: defaultScopedEnv.deviceName, + globalEnvValue: globalEnv.deviceName, + }) || undefined; + const initialSyncLimit = clampMatrixInitialSyncLimit(matrix.initialSyncLimit); const encryption = matrix.encryption ?? false; return { homeserver, @@ -153,76 +213,58 @@ export function resolveMatrixConfigForAccount( const normalizedAccountId = normalizeAccountId(accountId); const scopedEnv = resolveScopedMatrixEnvConfig(normalizedAccountId, env); const globalEnv = resolveGlobalMatrixEnvConfig(env); - - const accountHomeserver = clean( - account.homeserver, - `channels.matrix.accounts.${normalizedAccountId}.homeserver`, - ); - const accountUserId = clean( - account.userId, - `channels.matrix.accounts.${normalizedAccountId}.userId`, - ); - const accountAccessToken = clean( - account.accessToken, - `channels.matrix.accounts.${normalizedAccountId}.accessToken`, - ); - const accountPassword = clean( - account.password, - `channels.matrix.accounts.${normalizedAccountId}.password`, - ); - const accountDeviceId = clean( - account.deviceId, - `channels.matrix.accounts.${normalizedAccountId}.deviceId`, - ); - const accountDeviceName = clean( - account.deviceName, - `channels.matrix.accounts.${normalizedAccountId}.deviceName`, - ); - - const homeserver = - accountHomeserver || - scopedEnv.homeserver || - clean(matrix.homeserver, "channels.matrix.homeserver") || - globalEnv.homeserver; - const userId = - accountUserId || - scopedEnv.userId || - clean(matrix.userId, "channels.matrix.userId") || - globalEnv.userId; + const accountField = (field: MatrixConfigStringField) => + readMatrixAccountConfigField(cfg, normalizedAccountId, account, field); + const homeserver = resolveMatrixStringField({ + matrix, + field: "homeserver", + accountValue: accountField("homeserver"), + scopedEnvValue: scopedEnv.homeserver, + globalEnvValue: globalEnv.homeserver, + }); + const userId = resolveMatrixStringField({ + matrix, + field: "userId", + accountValue: accountField("userId"), + scopedEnvValue: scopedEnv.userId, + globalEnvValue: globalEnv.userId, + }); const accessToken = - accountAccessToken || - scopedEnv.accessToken || - clean(matrix.accessToken, "channels.matrix.accessToken") || - globalEnv.accessToken || - undefined; + resolveMatrixStringField({ + matrix, + field: "accessToken", + accountValue: accountField("accessToken"), + scopedEnvValue: scopedEnv.accessToken, + globalEnvValue: globalEnv.accessToken, + }) || undefined; const password = - accountPassword || - scopedEnv.password || - clean(matrix.password, "channels.matrix.password") || - globalEnv.password || - undefined; + resolveMatrixStringField({ + matrix, + field: "password", + accountValue: accountField("password"), + scopedEnvValue: scopedEnv.password, + globalEnvValue: globalEnv.password, + }) || undefined; const deviceId = - accountDeviceId || - scopedEnv.deviceId || - clean(matrix.deviceId, "channels.matrix.deviceId") || - globalEnv.deviceId || - undefined; + resolveMatrixStringField({ + matrix, + field: "deviceId", + accountValue: accountField("deviceId"), + scopedEnvValue: scopedEnv.deviceId, + globalEnvValue: globalEnv.deviceId, + }) || undefined; const deviceName = - accountDeviceName || - scopedEnv.deviceName || - clean(matrix.deviceName, "channels.matrix.deviceName") || - globalEnv.deviceName || - undefined; + resolveMatrixStringField({ + matrix, + field: "deviceName", + accountValue: accountField("deviceName"), + scopedEnvValue: scopedEnv.deviceName, + globalEnvValue: globalEnv.deviceName, + }) || undefined; - const accountInitialSyncLimit = - typeof account.initialSyncLimit === "number" - ? Math.max(0, Math.floor(account.initialSyncLimit)) - : undefined; + const accountInitialSyncLimit = clampMatrixInitialSyncLimit(account.initialSyncLimit); const initialSyncLimit = - accountInitialSyncLimit ?? - (typeof matrix.initialSyncLimit === "number" - ? Math.max(0, Math.floor(matrix.initialSyncLimit)) - : undefined); + accountInitialSyncLimit ?? clampMatrixInitialSyncLimit(matrix.initialSyncLimit); const encryption = typeof account.encryption === "boolean" ? account.encryption : (matrix.encryption ?? false); diff --git a/extensions/matrix/src/matrix/config-update.test.ts b/extensions/matrix/src/matrix/config-update.test.ts index 0852e703413..92a7ac344f1 100644 --- a/extensions/matrix/src/matrix/config-update.test.ts +++ b/extensions/matrix/src/matrix/config-update.test.ts @@ -1,8 +1,28 @@ import { describe, expect, it } from "vitest"; import type { CoreConfig } from "../types.js"; -import { updateMatrixAccountConfig } from "./config-update.js"; +import { resolveMatrixConfigFieldPath, updateMatrixAccountConfig } from "./config-update.js"; describe("updateMatrixAccountConfig", () => { + it("resolves account-aware Matrix config field paths", () => { + expect(resolveMatrixConfigFieldPath({} as CoreConfig, "default", "dm.policy")).toBe( + "channels.matrix.dm.policy", + ); + + const cfg = { + channels: { + matrix: { + accounts: { + ops: {}, + }, + }, + }, + } as CoreConfig; + + expect(resolveMatrixConfigFieldPath(cfg, "ops", ".dm.allowFrom")).toBe( + "channels.matrix.accounts.ops.dm.allowFrom", + ); + }); + it("supports explicit null clears and boolean false values", () => { const cfg = { channels: { diff --git a/extensions/matrix/src/matrix/config-update.ts b/extensions/matrix/src/matrix/config-update.ts index 06bd6f87fe4..dc6d194ea6f 100644 --- a/extensions/matrix/src/matrix/config-update.ts +++ b/extensions/matrix/src/matrix/config-update.ts @@ -93,6 +93,18 @@ export function resolveMatrixConfigPath(cfg: CoreConfig, accountId: string): str return `channels.matrix.accounts.${normalizedAccountId}`; } +export function resolveMatrixConfigFieldPath( + cfg: CoreConfig, + accountId: string, + fieldPath: string, +): string { + const suffix = fieldPath.trim().replace(/^\.+/, ""); + if (!suffix) { + return resolveMatrixConfigPath(cfg, accountId); + } + return `${resolveMatrixConfigPath(cfg, accountId)}.${suffix}`; +} + export function updateMatrixAccountConfig( cfg: CoreConfig, accountId: string, diff --git a/extensions/matrix/src/matrix/encryption-guidance.ts b/extensions/matrix/src/matrix/encryption-guidance.ts index ce6132cefd8..7e6f7b9a3b1 100644 --- a/extensions/matrix/src/matrix/encryption-guidance.ts +++ b/extensions/matrix/src/matrix/encryption-guidance.ts @@ -1,7 +1,7 @@ import { normalizeOptionalAccountId } from "openclaw/plugin-sdk/account-id"; import type { CoreConfig } from "../types.js"; import { resolveDefaultMatrixAccountId } from "./accounts.js"; -import { resolveMatrixConfigPath } from "./config-update.js"; +import { resolveMatrixConfigFieldPath } from "./config-update.js"; export function resolveMatrixEncryptionConfigPath( cfg: CoreConfig, @@ -9,7 +9,7 @@ export function resolveMatrixEncryptionConfigPath( ): string { const effectiveAccountId = normalizeOptionalAccountId(accountId) ?? resolveDefaultMatrixAccountId(cfg); - return `${resolveMatrixConfigPath(cfg, effectiveAccountId)}.encryption`; + return resolveMatrixConfigFieldPath(cfg, effectiveAccountId, "encryption"); } export function formatMatrixEncryptionUnavailableError( diff --git a/extensions/matrix/src/onboarding.ts b/extensions/matrix/src/onboarding.ts index c962095a5ae..f3a0c9b11c8 100644 --- a/extensions/matrix/src/onboarding.ts +++ b/extensions/matrix/src/onboarding.ts @@ -24,7 +24,11 @@ import { hasReadyMatrixEnvAuth, resolveScopedMatrixEnvConfig, } from "./matrix/client.js"; -import { resolveMatrixConfigPath, updateMatrixAccountConfig } from "./matrix/config-update.js"; +import { + resolveMatrixConfigFieldPath, + resolveMatrixConfigPath, + updateMatrixAccountConfig, +} from "./matrix/config-update.js"; import { ensureMatrixSdkInstalled, isMatrixSdkAvailable } from "./matrix/deps.js"; import { resolveMatrixTargets } from "./resolve-targets.js"; import type { CoreConfig } from "./types.js"; @@ -176,13 +180,14 @@ const dmPolicy: ChannelOnboardingDmPolicy = { policyKey: "channels.matrix.dm.policy", allowFromKey: "channels.matrix.dm.allowFrom", resolveConfigKeys: (cfg, accountId) => { - const basePath = resolveMatrixConfigPath( - cfg as CoreConfig, - resolveMatrixOnboardingAccountId(cfg as CoreConfig, accountId), - ); + const effectiveAccountId = resolveMatrixOnboardingAccountId(cfg as CoreConfig, accountId); return { - policyKey: `${basePath}.dm.policy`, - allowFromKey: `${basePath}.dm.allowFrom`, + policyKey: resolveMatrixConfigFieldPath(cfg as CoreConfig, effectiveAccountId, "dm.policy"), + allowFromKey: resolveMatrixConfigFieldPath( + cfg as CoreConfig, + effectiveAccountId, + "dm.allowFrom", + ), }; }, getCurrent: (cfg, accountId) =>