diff --git a/extensions/matrix/src/matrix/accounts.test.ts b/extensions/matrix/src/matrix/accounts.test.ts index 6bd2a8fd414..d4e3960212f 100644 --- a/extensions/matrix/src/matrix/accounts.test.ts +++ b/extensions/matrix/src/matrix/accounts.test.ts @@ -92,6 +92,59 @@ describe("resolveMatrixAccount", () => { expect(account.configured).toBe(true); }); + it("treats accounts.default SecretRef access-token config as configured", () => { + const cfg: CoreConfig = { + channels: { + matrix: { + accounts: { + default: { + homeserver: "https://matrix.example.org", + accessToken: { source: "file", provider: "matrix-file", id: "value" }, + }, + }, + }, + }, + secrets: { + providers: { + "matrix-file": { + source: "file", + path: "/tmp/matrix-token", + }, + }, + }, + }; + + const account = resolveMatrixAccount({ cfg }); + expect(account.configured).toBe(true); + }); + + it("treats accounts.default SecretRef password config as configured", () => { + const cfg: CoreConfig = { + channels: { + matrix: { + accounts: { + default: { + homeserver: "https://matrix.example.org", + userId: "@bot:example.org", + password: { source: "file", provider: "matrix-file", id: "value" }, + }, + }, + }, + }, + secrets: { + providers: { + "matrix-file": { + source: "file", + path: "/tmp/matrix-password", + }, + }, + }, + }; + + const account = resolveMatrixAccount({ cfg }); + expect(account.configured).toBe(true); + }); + it("requires userId + password when no access token is set", () => { const cfg: CoreConfig = { channels: { diff --git a/extensions/matrix/src/matrix/accounts.ts b/extensions/matrix/src/matrix/accounts.ts index b90535654bb..3e2b0368989 100644 --- a/extensions/matrix/src/matrix/accounts.ts +++ b/extensions/matrix/src/matrix/accounts.ts @@ -98,21 +98,21 @@ export function resolveMatrixAccount(params: { const env = params.env ?? process.env; const accountId = normalizeAccountId(params.accountId); const matrixBase = resolveMatrixBaseConfig(params.cfg); - const explicitAccountConfig = - accountId === DEFAULT_ACCOUNT_ID - ? matrixBase - : (findMatrixAccountConfig(params.cfg, accountId) ?? {}); const base = resolveMatrixAccountConfig({ cfg: params.cfg, accountId }); + const explicitAuthConfig = + accountId === DEFAULT_ACCOUNT_ID + ? base + : (findMatrixAccountConfig(params.cfg, accountId) ?? {}); const enabled = base.enabled !== false && matrixBase.enabled !== false; const resolved = resolveMatrixConfigForAccount(params.cfg, accountId, env); const hasHomeserver = Boolean(resolved.homeserver); const hasUserId = Boolean(resolved.userId); const hasAccessToken = - Boolean(resolved.accessToken) || hasConfiguredSecretInput(explicitAccountConfig.accessToken); + Boolean(resolved.accessToken) || hasConfiguredSecretInput(explicitAuthConfig.accessToken); const hasPassword = Boolean(resolved.password); const hasPasswordAuth = - hasUserId && (hasPassword || hasConfiguredSecretInput(explicitAccountConfig.password)); + hasUserId && (hasPassword || hasConfiguredSecretInput(explicitAuthConfig.password)); const stored = loadMatrixCredentials(env, accountId); const hasStored = stored && resolved.homeserver diff --git a/src/secrets/runtime-config-collectors-channels.ts b/src/secrets/runtime-config-collectors-channels.ts index 3d9a1292a1e..46d946cb9d4 100644 --- a/src/secrets/runtime-config-collectors-channels.ts +++ b/src/secrets/runtime-config-collectors-channels.ts @@ -688,13 +688,21 @@ function collectMatrixAssignments(params: { normalizeSecretStringValue( params.context.env[getMatrixScopedEnvVarNames(accountId).accessToken], ).length > 0; + const inheritedDefaultAccountAccessTokenConfigured = + accountId === "default" && (baseAccessTokenConfigured || envAccessTokenConfigured); collectSecretInputAssignment({ value: account.password, path: `channels.matrix.accounts.${accountId}.password`, expected: "string", defaults: params.defaults, context: params.context, - active: enabled && !(accountAccessTokenConfigured || scopedEnvAccessTokenConfigured), + active: + enabled && + !( + accountAccessTokenConfigured || + scopedEnvAccessTokenConfigured || + inheritedDefaultAccountAccessTokenConfigured + ), inactiveReason: "Matrix account is disabled or this account has an accessToken configured.", apply: (value) => { account.password = value; diff --git a/src/secrets/runtime.test.ts b/src/secrets/runtime.test.ts index 1964e5b57c5..b5b9b816c1b 100644 --- a/src/secrets/runtime.test.ts +++ b/src/secrets/runtime.test.ts @@ -396,6 +396,95 @@ describe("secrets runtime snapshot", () => { ); }); + it.each([ + { + name: "top-level Matrix accessToken config", + config: { + channels: { + matrix: { + accessToken: "default-token", + accounts: { + default: { + password: { + source: "env", + provider: "default", + id: "MATRIX_DEFAULT_PASSWORD", + }, + }, + }, + }, + }, + }, + env: {}, + }, + { + name: "top-level Matrix accessToken SecretRef config", + config: { + channels: { + matrix: { + accessToken: { + source: "env", + provider: "default", + id: "MATRIX_ACCESS_TOKEN_REF", + }, + accounts: { + default: { + password: { + source: "env", + provider: "default", + id: "MATRIX_DEFAULT_PASSWORD", + }, + }, + }, + }, + }, + }, + env: { + MATRIX_ACCESS_TOKEN_REF: "default-token", + }, + }, + { + name: "MATRIX_ACCESS_TOKEN env auth", + config: { + channels: { + matrix: { + accounts: { + default: { + password: { + source: "env", + provider: "default", + id: "MATRIX_DEFAULT_PASSWORD", + }, + }, + }, + }, + }, + }, + env: { + MATRIX_ACCESS_TOKEN: "default-token", + }, + }, + ])("ignores default-account Matrix password refs shadowed by $name", async ({ config, env }) => { + const snapshot = await prepareSecretsRuntimeSnapshot({ + config: asConfig(config), + env, + agentDirs: ["/tmp/openclaw-agent-main"], + loadAuthStore: () => ({ version: 1, profiles: {} }), + }); + + expect(snapshot.config.channels?.matrix?.accounts?.default?.password).toEqual({ + source: "env", + provider: "default", + id: "MATRIX_DEFAULT_PASSWORD", + }); + expect(snapshot.warnings).toContainEqual( + expect.objectContaining({ + code: "SECRETS_REF_IGNORED_INACTIVE_SURFACE", + path: "channels.matrix.accounts.default.password", + }), + ); + }); + it("resolves sandbox ssh secret refs for active ssh backends", async () => { const snapshot = await prepareSecretsRuntimeSnapshot({ config: asConfig({