mirror of
https://github.com/moltbot/moltbot.git
synced 2026-04-26 16:06:16 +00:00
fix(matrix): tighten account scoping and default detection
This commit is contained in:
@@ -658,7 +658,7 @@ See [Pairing](/channels/pairing) for the shared DM pairing flow and storage layo
|
||||
Top-level `channels.matrix` values act as defaults for named accounts unless an account overrides them.
|
||||
You can scope inherited room entries to one Matrix account with `groups.<room>.account` (or legacy `rooms.<room>.account`).
|
||||
Entries without `account` stay shared across all Matrix accounts, and entries with `account: "default"` still work when the default account is configured directly on top-level `channels.matrix.*`.
|
||||
Partial shared auth defaults do not create a separate implicit default account by themselves. OpenClaw only treats Matrix accounts with a usable homeserver plus access-token or user-ID-based auth shape as selectable for implicit routing.
|
||||
Partial shared auth defaults do not create a separate implicit default account by themselves. OpenClaw only synthesizes the top-level `default` account when that default has fresh auth (`homeserver` plus `accessToken`, or `homeserver` plus `userId` and `password`); named accounts can still stay discoverable from `homeserver` plus `userId` when cached credentials satisfy auth later.
|
||||
Set `defaultAccount` when you want OpenClaw to prefer one named Matrix account for implicit routing, probing, and CLI operations.
|
||||
If you configure multiple named accounts, set `defaultAccount` or pass `--account <id>` for CLI commands that rely on implicit account selection.
|
||||
Pass `--account <id>` to `openclaw matrix verify ...` and `openclaw matrix devices ...` when you want to override that implicit selection for one command.
|
||||
|
||||
@@ -67,6 +67,45 @@ describe("Matrix account selection topology", () => {
|
||||
expect(requiresExplicitMatrixDefaultAccount(cfg, env)).toBe(false);
|
||||
});
|
||||
|
||||
it("does not materialize a top-level default account from homeserver plus userId alone", () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
matrix: {
|
||||
homeserver: "https://matrix.example.org",
|
||||
userId: "@default:example.org",
|
||||
accounts: {
|
||||
ops: {
|
||||
homeserver: "https://matrix.example.org",
|
||||
accessToken: "ops-token",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as CoreConfig;
|
||||
|
||||
expect(resolveConfiguredMatrixAccountIds(cfg, {} as NodeJS.ProcessEnv)).toEqual(["ops"]);
|
||||
expect(resolveMatrixDefaultOrOnlyAccountId(cfg, {} as NodeJS.ProcessEnv)).toBe("ops");
|
||||
expect(requiresExplicitMatrixDefaultAccount(cfg, {} as NodeJS.ProcessEnv)).toBe(false);
|
||||
});
|
||||
|
||||
it("does not materialize a default env account from global homeserver plus userId alone", () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
matrix: {},
|
||||
},
|
||||
} as CoreConfig;
|
||||
const env = {
|
||||
MATRIX_HOMESERVER: "https://matrix.example.org",
|
||||
MATRIX_USER_ID: "@default:example.org",
|
||||
MATRIX_OPS_HOMESERVER: "https://matrix.example.org",
|
||||
MATRIX_OPS_ACCESS_TOKEN: "ops-token",
|
||||
} as NodeJS.ProcessEnv;
|
||||
|
||||
expect(resolveConfiguredMatrixAccountIds(cfg, env)).toEqual(["ops"]);
|
||||
expect(resolveMatrixDefaultOrOnlyAccountId(cfg, env)).toBe("ops");
|
||||
expect(requiresExplicitMatrixDefaultAccount(cfg, env)).toBe(false);
|
||||
});
|
||||
|
||||
it("counts env-backed named accounts when shared homeserver comes from channel config", () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
|
||||
@@ -89,26 +89,50 @@ function hasUsableResolvedMatrixAuth(values: {
|
||||
return Boolean(values.homeserver && (values.accessToken || values.userId));
|
||||
}
|
||||
|
||||
function hasReadyEffectiveMatrixAccountSource(params: {
|
||||
function hasFreshResolvedMatrixAuth(values: {
|
||||
homeserver: string;
|
||||
userId: string;
|
||||
accessToken: string;
|
||||
password: string;
|
||||
}): boolean {
|
||||
return Boolean(values.homeserver && (values.accessToken || (values.userId && values.password)));
|
||||
}
|
||||
|
||||
function resolveEffectiveMatrixAccountSources(params: {
|
||||
channel: Record<string, unknown> | null;
|
||||
accountId: string;
|
||||
env: NodeJS.ProcessEnv;
|
||||
}): boolean {
|
||||
}): ReturnType<typeof resolveMatrixAccountStringValues> {
|
||||
const normalizedAccountId = normalizeAccountId(params.accountId);
|
||||
const resolved = resolveMatrixAccountStringValues({
|
||||
return resolveMatrixAccountStringValues({
|
||||
accountId: normalizedAccountId,
|
||||
scopedEnv: resolveScopedMatrixEnvStringSources(normalizedAccountId, params.env),
|
||||
channel: resolveMatrixChannelStringSources(params.channel),
|
||||
globalEnv: resolveGlobalMatrixEnvStringSources(params.env),
|
||||
});
|
||||
return hasUsableResolvedMatrixAuth(resolved);
|
||||
}
|
||||
|
||||
function hasUsableEffectiveMatrixAccountSource(params: {
|
||||
channel: Record<string, unknown> | null;
|
||||
accountId: string;
|
||||
env: NodeJS.ProcessEnv;
|
||||
}): boolean {
|
||||
return hasUsableResolvedMatrixAuth(resolveEffectiveMatrixAccountSources(params));
|
||||
}
|
||||
|
||||
function hasFreshEffectiveMatrixAccountSource(params: {
|
||||
channel: Record<string, unknown> | null;
|
||||
accountId: string;
|
||||
env: NodeJS.ProcessEnv;
|
||||
}): boolean {
|
||||
return hasFreshResolvedMatrixAuth(resolveEffectiveMatrixAccountSources(params));
|
||||
}
|
||||
|
||||
function hasConfiguredDefaultMatrixAccountSource(params: {
|
||||
channel: Record<string, unknown> | null;
|
||||
env: NodeJS.ProcessEnv;
|
||||
}): boolean {
|
||||
return hasReadyEffectiveMatrixAccountSource({
|
||||
return hasFreshEffectiveMatrixAccountSource({
|
||||
channel: params.channel,
|
||||
accountId: DEFAULT_ACCOUNT_ID,
|
||||
env: params.env,
|
||||
@@ -149,7 +173,9 @@ export function resolveConfiguredMatrixAccountIds(
|
||||
configuredAccountIds.push(DEFAULT_ACCOUNT_ID);
|
||||
}
|
||||
const readyEnvAccountIds = listMatrixEnvAccountIds(env).filter((accountId) =>
|
||||
hasReadyEffectiveMatrixAccountSource({ channel, accountId, env }),
|
||||
normalizeAccountId(accountId) === DEFAULT_ACCOUNT_ID
|
||||
? hasConfiguredDefaultMatrixAccountSource({ channel, env })
|
||||
: hasUsableEffectiveMatrixAccountSource({ channel, accountId, env }),
|
||||
);
|
||||
return listCombinedAccountIds({
|
||||
configuredAccountIds,
|
||||
|
||||
@@ -686,6 +686,66 @@ describe("resolveMatrixAccount", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps scoped groups bound to their account even when only one account is active", () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
matrix: {
|
||||
groups: {
|
||||
"!default-room:example.org": {
|
||||
allow: true,
|
||||
account: "default",
|
||||
},
|
||||
"!shared-room:example.org": {
|
||||
allow: true,
|
||||
},
|
||||
},
|
||||
accounts: {
|
||||
ops: {
|
||||
homeserver: "https://matrix.example.org",
|
||||
accessToken: "ops-token",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as CoreConfig;
|
||||
|
||||
expect(resolveMatrixAccount({ cfg, accountId: "ops" }).config.groups).toEqual({
|
||||
"!shared-room:example.org": {
|
||||
allow: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps scoped legacy rooms bound to their account even when only one account is active", () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
matrix: {
|
||||
rooms: {
|
||||
"!default-room:example.org": {
|
||||
allow: true,
|
||||
account: "default",
|
||||
},
|
||||
"!shared-room:example.org": {
|
||||
allow: true,
|
||||
},
|
||||
},
|
||||
accounts: {
|
||||
ops: {
|
||||
homeserver: "https://matrix.example.org",
|
||||
accessToken: "ops-token",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as CoreConfig;
|
||||
|
||||
expect(resolveMatrixAccount({ cfg, accountId: "ops" }).config.rooms).toEqual({
|
||||
"!shared-room:example.org": {
|
||||
allow: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("lets an account clear inherited groups with an explicit empty map", () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
|
||||
@@ -15,15 +15,11 @@ type MatrixRoomEntries = Record<string, NonNullable<MatrixConfig["groups"]>[stri
|
||||
function selectInheritedMatrixRoomEntries(params: {
|
||||
entries: MatrixRoomEntries | undefined;
|
||||
accountId: string;
|
||||
isMultiAccount: boolean;
|
||||
}): MatrixRoomEntries | undefined {
|
||||
const entries = params.entries;
|
||||
if (!entries) {
|
||||
return undefined;
|
||||
}
|
||||
if (!params.isMultiAccount) {
|
||||
return entries;
|
||||
}
|
||||
const selected = Object.fromEntries(
|
||||
Object.entries(entries).filter(([, value]) => {
|
||||
const scopedAccount =
|
||||
@@ -199,12 +195,10 @@ export function resolveMatrixAccountConfig(params: {
|
||||
nestedObjectKeys: ["dm", "actions"],
|
||||
});
|
||||
const accountConfig = findMatrixAccountConfig(params.cfg, accountId);
|
||||
const isMultiAccount = resolveConfiguredMatrixAccountIds(params.cfg, env).length > 1;
|
||||
const groups = mergeMatrixRoomEntries(
|
||||
selectInheritedMatrixRoomEntries({
|
||||
entries: base.groups,
|
||||
accountId,
|
||||
isMultiAccount,
|
||||
}),
|
||||
accountConfig?.groups,
|
||||
Boolean(accountConfig && Object.hasOwn(accountConfig, "groups")),
|
||||
@@ -213,7 +207,6 @@ export function resolveMatrixAccountConfig(params: {
|
||||
selectInheritedMatrixRoomEntries({
|
||||
entries: base.rooms,
|
||||
accountId,
|
||||
isMultiAccount,
|
||||
}),
|
||||
accountConfig?.rooms,
|
||||
Boolean(accountConfig && Object.hasOwn(accountConfig, "rooms")),
|
||||
|
||||
@@ -451,6 +451,43 @@ describe("resolveMatrixConfig", () => {
|
||||
expect(resolveMatrixAuthContext({ cfg, env }).accountId).toBe("ops");
|
||||
});
|
||||
|
||||
it("does not materialize a default account from top-level homeserver plus userId alone", () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
matrix: {
|
||||
homeserver: "https://matrix.example.org",
|
||||
userId: "@default:example.org",
|
||||
accounts: {
|
||||
ops: {
|
||||
homeserver: "https://matrix.example.org",
|
||||
accessToken: "ops-token",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as CoreConfig;
|
||||
|
||||
expect(resolveImplicitMatrixAccountId(cfg, {} as NodeJS.ProcessEnv)).toBe("ops");
|
||||
expect(resolveMatrixAuthContext({ cfg, env: {} as NodeJS.ProcessEnv }).accountId).toBe("ops");
|
||||
});
|
||||
|
||||
it("does not materialize a default env account from global homeserver plus userId alone", () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
matrix: {},
|
||||
},
|
||||
} as CoreConfig;
|
||||
const env = {
|
||||
MATRIX_HOMESERVER: "https://matrix.example.org",
|
||||
MATRIX_USER_ID: "@default:example.org",
|
||||
MATRIX_OPS_HOMESERVER: "https://matrix.example.org",
|
||||
MATRIX_OPS_ACCESS_TOKEN: "ops-token",
|
||||
} as NodeJS.ProcessEnv;
|
||||
|
||||
expect(resolveImplicitMatrixAccountId(cfg, env)).toBe("ops");
|
||||
expect(resolveMatrixAuthContext({ cfg, env }).accountId).toBe("ops");
|
||||
});
|
||||
|
||||
it("keeps implicit selection for env-backed accounts that can use cached credentials", () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
|
||||
Reference in New Issue
Block a user