mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-20 22:40:58 +00:00
Core: harden matrix migration and runner wiring
This commit is contained in:
@@ -241,6 +241,27 @@ describe("resolveMatrixConfig", () => {
|
||||
expect(resolved.deviceId).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does not inherit the base userId for non-default accounts", () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
matrix: {
|
||||
homeserver: "https://base.example.org",
|
||||
userId: "@base:example.org",
|
||||
accessToken: "base-token",
|
||||
accounts: {
|
||||
ops: {
|
||||
homeserver: "https://ops.example.org",
|
||||
accessToken: "ops-token",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as CoreConfig;
|
||||
|
||||
const resolved = resolveMatrixConfigForAccount(cfg, "ops", {} as NodeJS.ProcessEnv);
|
||||
expect(resolved.userId).toBe("");
|
||||
});
|
||||
|
||||
it("rejects insecure public http Matrix homeservers", () => {
|
||||
expect(() => validateMatrixHomeserverUrl("http://matrix.example.org")).toThrow(
|
||||
"Matrix homeserver must use https:// unless it targets a private or loopback host",
|
||||
@@ -426,6 +447,38 @@ describe("resolveMatrixAuth", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("resolves token-only non-default account userId from whoami instead of inheriting the base user", async () => {
|
||||
const doRequestSpy = vi.spyOn(sdkModule.MatrixClient.prototype, "doRequest").mockResolvedValue({
|
||||
user_id: "@ops:example.org",
|
||||
device_id: "OPSDEVICE",
|
||||
});
|
||||
|
||||
const cfg = {
|
||||
channels: {
|
||||
matrix: {
|
||||
userId: "@base:example.org",
|
||||
homeserver: "https://matrix.example.org",
|
||||
accounts: {
|
||||
ops: {
|
||||
homeserver: "https://matrix.example.org",
|
||||
accessToken: "ops-token",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as CoreConfig;
|
||||
|
||||
const auth = await resolveMatrixAuth({
|
||||
cfg,
|
||||
env: {} as NodeJS.ProcessEnv,
|
||||
accountId: "ops",
|
||||
});
|
||||
|
||||
expect(doRequestSpy).toHaveBeenCalledWith("GET", "/_matrix/client/v3/account/whoami");
|
||||
expect(auth.userId).toBe("@ops:example.org");
|
||||
expect(auth.deviceId).toBe("OPSDEVICE");
|
||||
});
|
||||
|
||||
it("resolves missing whoami identity fields for token auth", async () => {
|
||||
const doRequestSpy = vi.spyOn(sdkModule.MatrixClient.prototype, "doRequest").mockResolvedValue({
|
||||
user_id: "@bot:example.org",
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
requiresExplicitMatrixDefaultAccount,
|
||||
resolveMatrixDefaultOrOnlyAccountId,
|
||||
} from "openclaw/plugin-sdk/matrix";
|
||||
import { getMatrixScopedEnvVarNames } from "../../../../../src/infra/matrix-env-vars.js";
|
||||
import { getMatrixRuntime } from "../../runtime.js";
|
||||
import type { CoreConfig } from "../../types.js";
|
||||
import {
|
||||
@@ -91,34 +92,7 @@ function resolveGlobalMatrixEnvConfig(env: NodeJS.ProcessEnv): MatrixEnvConfig {
|
||||
};
|
||||
}
|
||||
|
||||
function resolveMatrixEnvAccountToken(accountId: string): string {
|
||||
return Array.from(normalizeAccountId(accountId))
|
||||
.map((char) =>
|
||||
/[a-z0-9]/.test(char)
|
||||
? char.toUpperCase()
|
||||
: `_X${char.codePointAt(0)?.toString(16).toUpperCase() ?? "00"}_`,
|
||||
)
|
||||
.join("");
|
||||
}
|
||||
|
||||
export function getMatrixScopedEnvVarNames(accountId: string): {
|
||||
homeserver: string;
|
||||
userId: string;
|
||||
accessToken: string;
|
||||
password: string;
|
||||
deviceId: string;
|
||||
deviceName: string;
|
||||
} {
|
||||
const token = resolveMatrixEnvAccountToken(accountId);
|
||||
return {
|
||||
homeserver: `MATRIX_${token}_HOMESERVER`,
|
||||
userId: `MATRIX_${token}_USER_ID`,
|
||||
accessToken: `MATRIX_${token}_ACCESS_TOKEN`,
|
||||
password: `MATRIX_${token}_PASSWORD`, // pragma: allowlist secret
|
||||
deviceId: `MATRIX_${token}_DEVICE_ID`,
|
||||
deviceName: `MATRIX_${token}_DEVICE_NAME`,
|
||||
};
|
||||
}
|
||||
export { getMatrixScopedEnvVarNames } from "../../../../../src/infra/matrix-env-vars.js";
|
||||
|
||||
export function resolveScopedMatrixEnvConfig(
|
||||
accountId: string,
|
||||
@@ -274,13 +248,13 @@ export function resolveMatrixConfigForAccount(
|
||||
scopedEnvValue: scopedEnv.homeserver,
|
||||
globalEnvValue: globalEnv.homeserver,
|
||||
});
|
||||
const userId = resolveMatrixStringField({
|
||||
matrix,
|
||||
field: "userId",
|
||||
accountValue: accountField("userId"),
|
||||
scopedEnvValue: scopedEnv.userId,
|
||||
globalEnvValue: globalEnv.userId,
|
||||
});
|
||||
const userIdSource =
|
||||
accountField("userId") ||
|
||||
scopedEnv.userId ||
|
||||
(normalizedAccountId === DEFAULT_ACCOUNT_ID
|
||||
? readMatrixBaseConfigField(matrix, "userId") || globalEnv.userId || ""
|
||||
: "");
|
||||
const userId = userIdSource;
|
||||
const accessToken =
|
||||
resolveMatrixStringField({
|
||||
matrix,
|
||||
@@ -367,7 +341,7 @@ export function resolveMatrixAuthContext(params?: {
|
||||
!hasScopedMatrixEnvConfig(explicitAccountId, env)
|
||||
) {
|
||||
throw new Error(
|
||||
`Matrix account "${explicitAccountId}" is not configured. Add channels.matrix.accounts.${explicitAccountId} or define scoped MATRIX_${resolveMatrixEnvAccountToken(explicitAccountId)}_* variables.`,
|
||||
`Matrix account "${explicitAccountId}" is not configured. Add channels.matrix.accounts.${explicitAccountId} or define scoped ${getMatrixScopedEnvVarNames(explicitAccountId).accessToken.replace(/_ACCESS_TOKEN$/, "")}_* variables.`,
|
||||
);
|
||||
}
|
||||
const resolved = resolveMatrixConfigForAccount(cfg, effectiveAccountId, env);
|
||||
|
||||
19
src/agents/pi-embedded-runner/run/attempt.imports.test.ts
Normal file
19
src/agents/pi-embedded-runner/run/attempt.imports.test.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
const compactionFailuresImported = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("../compaction-failures.js", () => {
|
||||
compactionFailuresImported();
|
||||
return {};
|
||||
});
|
||||
|
||||
describe("run attempt module wiring", () => {
|
||||
it("loads the compaction failure bridge during runner init", async () => {
|
||||
vi.resetModules();
|
||||
compactionFailuresImported.mockClear();
|
||||
|
||||
await import("./attempt.js");
|
||||
|
||||
expect(compactionFailuresImported).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -142,6 +142,7 @@ import {
|
||||
selectCompactionTimeoutSnapshot,
|
||||
shouldFlagCompactionTimeout,
|
||||
} from "./compaction-timeout.js";
|
||||
import "../compaction-failures.js";
|
||||
import { pruneProcessedHistoryImages } from "./history-image-prune.js";
|
||||
import { detectAndLoadPromptImages } from "./images.js";
|
||||
import type { EmbeddedRunAttemptParams, EmbeddedRunAttemptResult } from "./types.js";
|
||||
|
||||
33
src/auto-reply/reply/session-target-resolution.test.ts
Normal file
33
src/auto-reply/reply/session-target-resolution.test.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
|
||||
const listAcpSessionEntriesMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("../../acp/runtime/session-meta.js", () => ({
|
||||
listAcpSessionEntries: listAcpSessionEntriesMock,
|
||||
}));
|
||||
|
||||
vi.mock("../../gateway/call.js", () => ({
|
||||
callGateway: vi.fn(async () => {
|
||||
throw new Error("gateway unavailable");
|
||||
}),
|
||||
}));
|
||||
|
||||
import { resolveSessionKeyByReference } from "./session-target-resolution.js";
|
||||
|
||||
describe("resolveSessionKeyByReference", () => {
|
||||
it("matches ACP fallback session references case-insensitively", async () => {
|
||||
listAcpSessionEntriesMock.mockResolvedValueOnce([
|
||||
{
|
||||
sessionKey: "user:alice:acp:982649c1-1234-4abc-8123-0123456789ab",
|
||||
},
|
||||
]);
|
||||
|
||||
const resolved = await resolveSessionKeyByReference({
|
||||
cfg: {} as OpenClawConfig,
|
||||
token: "acp:982649C1-1234-4ABC-8123-0123456789AB",
|
||||
});
|
||||
|
||||
expect(resolved).toBe("user:alice:acp:982649c1-1234-4abc-8123-0123456789ab");
|
||||
});
|
||||
});
|
||||
@@ -1,22 +1,23 @@
|
||||
import { listAcpSessionEntries } from "../../acp/runtime/session-meta.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { callGateway } from "../../gateway/call.js";
|
||||
import { SESSION_ID_RE } from "../../sessions/session-id.js";
|
||||
import { normalizeSessionId, SESSION_ID_RE } from "../../sessions/session-id.js";
|
||||
|
||||
function resolveAcpSessionKeySuffixToken(token: string): string | null {
|
||||
const trimmed = token.trim();
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
if (SESSION_ID_RE.test(trimmed)) {
|
||||
return trimmed;
|
||||
const normalizedSessionId = normalizeSessionId(trimmed);
|
||||
if (normalizedSessionId) {
|
||||
return normalizedSessionId;
|
||||
}
|
||||
const lower = trimmed.toLowerCase();
|
||||
if (!lower.startsWith("acp:")) {
|
||||
return null;
|
||||
}
|
||||
const suffix = trimmed.slice("acp:".length).trim();
|
||||
return SESSION_ID_RE.test(suffix) ? suffix : null;
|
||||
return normalizeSessionId(suffix);
|
||||
}
|
||||
|
||||
async function resolveSessionKeyViaGateway(token: string): Promise<string | null> {
|
||||
|
||||
30
src/infra/matrix-env-vars.ts
Normal file
30
src/infra/matrix-env-vars.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { normalizeAccountId } from "../routing/session-key.js";
|
||||
|
||||
export function resolveMatrixEnvAccountToken(accountId: string): string {
|
||||
return Array.from(normalizeAccountId(accountId))
|
||||
.map((char) =>
|
||||
/[a-z0-9]/.test(char)
|
||||
? char.toUpperCase()
|
||||
: `_X${char.codePointAt(0)?.toString(16).toUpperCase() ?? "00"}_`,
|
||||
)
|
||||
.join("");
|
||||
}
|
||||
|
||||
export function getMatrixScopedEnvVarNames(accountId: string): {
|
||||
homeserver: string;
|
||||
userId: string;
|
||||
accessToken: string;
|
||||
password: string;
|
||||
deviceId: string;
|
||||
deviceName: string;
|
||||
} {
|
||||
const token = resolveMatrixEnvAccountToken(accountId);
|
||||
return {
|
||||
homeserver: `MATRIX_${token}_HOMESERVER`,
|
||||
userId: `MATRIX_${token}_USER_ID`,
|
||||
accessToken: `MATRIX_${token}_ACCESS_TOKEN`,
|
||||
password: `MATRIX_${token}_PASSWORD`,
|
||||
deviceId: `MATRIX_${token}_DEVICE_ID`,
|
||||
deviceName: `MATRIX_${token}_DEVICE_NAME`,
|
||||
};
|
||||
}
|
||||
@@ -136,4 +136,34 @@ describe("resolveMatrixMigrationAccountTarget", () => {
|
||||
expect(target).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it("uses the same scoped env token encoding as runtime account auth", async () => {
|
||||
await withTempHome(async () => {
|
||||
const cfg: OpenClawConfig = {
|
||||
channels: {
|
||||
matrix: {
|
||||
accounts: {
|
||||
"ops-prod": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const env = {
|
||||
MATRIX_OPS_X2D_PROD_HOMESERVER: "https://matrix.example.org",
|
||||
MATRIX_OPS_X2D_PROD_USER_ID: "@ops-prod:example.org",
|
||||
MATRIX_OPS_X2D_PROD_ACCESS_TOKEN: "tok-ops-prod",
|
||||
} as NodeJS.ProcessEnv;
|
||||
|
||||
const target = resolveMatrixMigrationAccountTarget({
|
||||
cfg,
|
||||
env,
|
||||
accountId: "ops-prod",
|
||||
});
|
||||
|
||||
expect(target).not.toBeNull();
|
||||
expect(target?.homeserver).toBe("https://matrix.example.org");
|
||||
expect(target?.userId).toBe("@ops-prod:example.org");
|
||||
expect(target?.accessToken).toBe("tok-ops-prod");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
resolveMatrixChannelConfig,
|
||||
resolveMatrixDefaultOrOnlyAccountId,
|
||||
} from "./matrix-account-selection.js";
|
||||
import { getMatrixScopedEnvVarNames } from "./matrix-env-vars.js";
|
||||
import {
|
||||
resolveMatrixAccountStorageRoot,
|
||||
resolveMatrixCredentialsPath,
|
||||
@@ -41,13 +42,6 @@ function clean(value: unknown): string {
|
||||
return typeof value === "string" ? value.trim() : "";
|
||||
}
|
||||
|
||||
function resolveMatrixEnvAccountToken(accountId: string): string {
|
||||
return normalizeAccountId(accountId)
|
||||
.replace(/[^a-z0-9]+/gi, "_")
|
||||
.replace(/^_+|_+$/g, "")
|
||||
.toUpperCase();
|
||||
}
|
||||
|
||||
function resolveScopedMatrixEnvConfig(
|
||||
accountId: string,
|
||||
env: NodeJS.ProcessEnv,
|
||||
@@ -56,11 +50,11 @@ function resolveScopedMatrixEnvConfig(
|
||||
userId: string;
|
||||
accessToken: string;
|
||||
} {
|
||||
const token = resolveMatrixEnvAccountToken(accountId);
|
||||
const keys = getMatrixScopedEnvVarNames(accountId);
|
||||
return {
|
||||
homeserver: clean(env[`MATRIX_${token}_HOMESERVER`]),
|
||||
userId: clean(env[`MATRIX_${token}_USER_ID`]),
|
||||
accessToken: clean(env[`MATRIX_${token}_ACCESS_TOKEN`]),
|
||||
homeserver: clean(env[keys.homeserver]),
|
||||
userId: clean(env[keys.userId]),
|
||||
accessToken: clean(env[keys.accessToken]),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -3,3 +3,8 @@ export const SESSION_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[
|
||||
export function looksLikeSessionId(value: string): boolean {
|
||||
return SESSION_ID_RE.test(value.trim());
|
||||
}
|
||||
|
||||
export function normalizeSessionId(value: string): string | null {
|
||||
const trimmed = value.trim();
|
||||
return SESSION_ID_RE.test(trimmed) ? trimmed.toLowerCase() : null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user