Matrix: prompt invite auto-join during onboarding (#62168)

Merged via squash.

Prepared head SHA: aec7a2249a
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
This commit is contained in:
Gustavo Madeira Santana
2026-04-06 23:22:01 -04:00
committed by GitHub
parent 8d2ccd851c
commit 9fd47a5aed
8 changed files with 487 additions and 95 deletions

View File

@@ -103,6 +103,44 @@ describe("updateMatrixAccountConfig", () => {
expect(updated.channels?.["matrix"]?.accounts?.default?.proxy).toBeUndefined();
});
it("stores and clears Matrix invite auto-join settings", () => {
const cfg = {
channels: {
matrix: {
accounts: {
default: {
autoJoin: "allowlist",
autoJoinAllowlist: ["#ops:example.org"],
},
},
},
},
} as CoreConfig;
const allowlistUpdated = updateMatrixAccountConfig(cfg, "default", {
autoJoin: "allowlist",
autoJoinAllowlist: ["!ops-room:example.org", "#ops:example.org"],
});
expect(allowlistUpdated.channels?.matrix?.accounts?.default).toMatchObject({
autoJoin: "allowlist",
autoJoinAllowlist: ["!ops-room:example.org", "#ops:example.org"],
});
const offUpdated = updateMatrixAccountConfig(cfg, "default", {
autoJoin: "off",
autoJoinAllowlist: null,
});
expect(offUpdated.channels?.matrix?.accounts?.default?.autoJoin).toBe("off");
expect(offUpdated.channels?.matrix?.accounts?.default?.autoJoinAllowlist).toBeUndefined();
const alwaysUpdated = updateMatrixAccountConfig(cfg, "default", {
autoJoin: "always",
autoJoinAllowlist: null,
});
expect(alwaysUpdated.channels?.matrix?.accounts?.default?.autoJoin).toBe("always");
expect(alwaysUpdated.channels?.matrix?.accounts?.default?.autoJoinAllowlist).toBeUndefined();
});
it("normalizes account id and defaults account enabled=true", () => {
const updated = updateMatrixAccountConfig({} as CoreConfig, "Main Bot", {
name: "Main Bot",

View File

@@ -30,6 +30,8 @@ export type MatrixAccountPatch = {
encryption?: boolean | null;
initialSyncLimit?: number | null;
allowBots?: MatrixConfig["allowBots"] | null;
autoJoin?: MatrixConfig["autoJoin"] | null;
autoJoinAllowlist?: MatrixConfig["autoJoinAllowlist"] | null;
dm?: MatrixConfig["dm"] | null;
groupPolicy?: MatrixConfig["groupPolicy"] | null;
groupAllowFrom?: MatrixConfig["groupAllowFrom"] | null;
@@ -203,6 +205,14 @@ export function updateMatrixAccountConfig(
nextAccount.allowBots = patch.allowBots;
}
}
if (patch.autoJoin !== undefined) {
if (patch.autoJoin === null) {
delete nextAccount.autoJoin;
} else {
nextAccount.autoJoin = patch.autoJoin;
}
}
applyNullableArrayField(nextAccount, "autoJoinAllowlist", patch.autoJoinAllowlist);
if (patch.dm !== undefined) {
if (patch.dm === null) {
delete nextAccount.dm;
@@ -245,16 +255,20 @@ export function updateMatrixAccountConfig(
);
if (shouldStoreMatrixAccountAtTopLevel(cfg, normalizedAccountId)) {
const { accounts: _ignoredAccounts, defaultAccount, ...baseMatrix } = matrix;
const { accounts: _ignoredAccounts, defaultAccount } = matrix;
const {
accounts: _ignoredNextAccounts,
defaultAccount: _ignoredNextDefaultAccount,
...topLevelAccount
} = nextAccount;
return {
...cfg,
channels: {
...cfg.channels,
matrix: {
...baseMatrix,
...(defaultAccount ? { defaultAccount } : {}),
enabled: true,
...nextAccount,
...topLevelAccount,
},
},
};

View File

@@ -117,13 +117,18 @@ export async function runMatrixAddAccountAllowlistConfigure(params: {
cfg: CoreConfig;
allowFromInput: string;
roomsAllowlistInput: string;
autoJoinPolicy?: "always" | "allowlist" | "off";
autoJoinAllowlistInput?: string;
deviceName?: string;
notes?: string[];
}) {
const prompter = createMatrixWizardPrompter({
notes: params.notes,
select: {
"Matrix already configured. What do you want to do?": "add-account",
"Matrix auth method": "token",
"Matrix rooms access": "allowlist",
"Matrix invite auto-join": params.autoJoinPolicy ?? "allowlist",
},
text: {
"Matrix account name": "ops",
@@ -132,10 +137,13 @@ export async function runMatrixAddAccountAllowlistConfigure(params: {
"Matrix device name (optional)": params.deviceName ?? "",
"Matrix allowFrom (full @user:server; display name only if unique)": params.allowFromInput,
"Matrix rooms allowlist (comma-separated)": params.roomsAllowlistInput,
"Matrix invite auto-join allowlist (comma-separated)":
params.autoJoinAllowlistInput ?? "#ops-invites:example.org",
},
confirm: {
"Enable end-to-end encryption (E2EE)?": false,
"Configure Matrix rooms access?": true,
"Configure Matrix invite auto-join?": true,
},
onConfirm: async () => false,
});

View File

@@ -85,6 +85,72 @@ describe("matrix onboarding", () => {
).toBe(true);
});
it("routes env-shortcut add-account flow through Matrix invite auto-join setup", async () => {
installMatrixTestRuntime();
process.env.MATRIX_HOMESERVER = "https://matrix.env.example.org";
process.env.MATRIX_USER_ID = "@env:example.org";
process.env.MATRIX_PASSWORD = "env-password"; // pragma: allowlist secret
process.env.MATRIX_ACCESS_TOKEN = "";
process.env.MATRIX_OPS_HOMESERVER = "https://matrix.ops.env.example.org";
process.env.MATRIX_OPS_ACCESS_TOKEN = "ops-env-token";
const notes: string[] = [];
const prompter = createMatrixWizardPrompter({
notes,
select: {
"Matrix already configured. What do you want to do?": "add-account",
"Matrix rooms access": "allowlist",
"Matrix invite auto-join": "allowlist",
},
text: {
"Matrix account name": "ops",
"Matrix rooms allowlist (comma-separated)": "!ops-room:example.org",
"Matrix invite auto-join allowlist (comma-separated)": "#ops-invites:example.org",
},
confirm: {
"Configure Matrix rooms access?": true,
"Configure Matrix invite auto-join?": true,
},
onConfirm: (message) => message.startsWith("Matrix env vars detected"),
});
const result = await runMatrixInteractiveConfigure({
cfg: {
channels: {
matrix: {
accounts: {
default: {
homeserver: "https://matrix.main.example.org",
accessToken: "main-token",
},
},
},
},
} as CoreConfig,
prompter,
shouldPromptAccountIds: true,
configured: true,
});
expect(result).not.toBe("skip");
if (result === "skip") {
return;
}
expect(result.accountId).toBe("ops");
expect(result.cfg.channels?.matrix?.accounts?.ops).toMatchObject({
enabled: true,
groupPolicy: "allowlist",
groups: {
"!ops-room:example.org": { enabled: true },
},
autoJoin: "allowlist",
autoJoinAllowlist: ["#ops-invites:example.org"],
});
expect(notes.join("\n")).toContain("WARNING: Matrix invite auto-join defaults to off.");
});
it("promotes legacy top-level Matrix config before adding a named account", async () => {
installMatrixTestRuntime();
@@ -289,6 +355,7 @@ describe("matrix onboarding", () => {
"Matrix credentials already configured. Keep them?": true,
"Enable end-to-end encryption (E2EE)?": false,
"Configure Matrix rooms access?": false,
"Configure Matrix invite auto-join?": false,
},
});
@@ -353,6 +420,7 @@ describe("matrix onboarding", () => {
it("writes allowlists and room access to the selected Matrix account", async () => {
installMatrixTestRuntime();
const notes: string[] = [];
const result = await runMatrixAddAccountAllowlistConfigure({
cfg: {
@@ -369,7 +437,9 @@ describe("matrix onboarding", () => {
} as CoreConfig,
allowFromInput: "@alice:example.org",
roomsAllowlistInput: "!ops-room:example.org",
autoJoinAllowlistInput: "#ops-invites:example.org",
deviceName: "Ops Gateway",
notes,
});
expect(result).not.toBe("skip");
@@ -387,12 +457,124 @@ describe("matrix onboarding", () => {
allowFrom: ["@alice:example.org"],
},
groupPolicy: "allowlist",
autoJoin: "allowlist",
autoJoinAllowlist: ["#ops-invites:example.org"],
groups: {
"!ops-room:example.org": { enabled: true },
},
});
expect(result.cfg.channels?.["matrix"]?.dm).toBeUndefined();
expect(result.cfg.channels?.["matrix"]?.groups).toBeUndefined();
expect(notes.join("\n")).toContain("WARNING: Matrix invite auto-join defaults to off.");
});
it("clears Matrix invite auto-join allowlists when switching auto-join off", async () => {
installMatrixTestRuntime();
const notes: string[] = [];
const prompter = createMatrixWizardPrompter({
notes,
select: {
"Matrix already configured. What do you want to do?": "update",
"Matrix invite auto-join": "off",
},
text: {
"Matrix homeserver URL": "https://matrix.example.org",
"Matrix device name (optional)": "OpenClaw Gateway",
},
confirm: {
"Matrix credentials already configured. Keep them?": true,
"Enable end-to-end encryption (E2EE)?": false,
"Configure Matrix rooms access?": false,
"Configure Matrix invite auto-join?": true,
"Update Matrix invite auto-join?": true,
},
});
const result = await runMatrixInteractiveConfigure({
cfg: {
channels: {
matrix: {
homeserver: "https://matrix.example.org",
accessToken: "matrix-token",
autoJoin: "allowlist",
autoJoinAllowlist: ["#ops:example.org"],
},
},
} as CoreConfig,
prompter,
configured: true,
});
expect(result).not.toBe("skip");
if (result === "skip") {
return;
}
expect(result.cfg.channels?.matrix?.autoJoin).toBe("off");
expect(result.cfg.channels?.matrix?.autoJoinAllowlist).toBeUndefined();
expect(notes.join("\n")).toContain("Matrix invite auto-join remains off.");
expect(notes.join("\n")).toContain(
"Agents will not join invited rooms or fresh DM-style invites until you change autoJoin.",
);
});
it("re-prompts Matrix invite auto-join allowlists until entries are stable invite targets", async () => {
installMatrixTestRuntime();
const notes: string[] = [];
let inviteAllowlistPrompts = 0;
const prompter = createMatrixWizardPrompter({
notes,
select: {
"Matrix already configured. What do you want to do?": "update",
"Matrix invite auto-join": "allowlist",
},
text: {
"Matrix homeserver URL": "https://matrix.example.org",
"Matrix device name (optional)": "OpenClaw Gateway",
},
confirm: {
"Matrix credentials already configured. Keep them?": true,
"Enable end-to-end encryption (E2EE)?": false,
"Configure Matrix rooms access?": false,
"Configure Matrix invite auto-join?": true,
"Update Matrix invite auto-join?": true,
},
onText: async (message) => {
if (message === "Matrix invite auto-join allowlist (comma-separated)") {
inviteAllowlistPrompts += 1;
return inviteAllowlistPrompts === 1 ? "Project Room" : "#ops:example.org";
}
throw new Error(`unexpected text prompt: ${message}`);
},
});
const result = await runMatrixInteractiveConfigure({
cfg: {
channels: {
matrix: {
homeserver: "https://matrix.example.org",
accessToken: "matrix-token",
},
},
} as CoreConfig,
prompter,
configured: true,
});
expect(result).not.toBe("skip");
if (result === "skip") {
return;
}
expect(inviteAllowlistPrompts).toBe(2);
expect(result.cfg.channels?.matrix?.autoJoin).toBe("allowlist");
expect(result.cfg.channels?.matrix?.autoJoinAllowlist).toEqual(["#ops:example.org"]);
expect(notes.join("\n")).toContain(
"Use only stable Matrix invite targets for auto-join: !roomId:server, #alias:server, or *.",
);
expect(notes.join("\n")).toContain("Invalid: Project Room");
});
it("reports account-scoped DM config keys for named accounts", () => {

View File

@@ -30,13 +30,40 @@ import {
normalizeAccountId,
promptAccountId,
promptChannelAccessConfig,
splitSetupEntries,
type RuntimeEnv,
type WizardPrompter,
} from "./runtime-api.js";
import { moveSingleMatrixAccountConfigToNamedAccount } from "./setup-config.js";
import type { CoreConfig } from "./types.js";
import type { CoreConfig, MatrixConfig } from "./types.js";
const channel = "matrix" as const;
type MatrixInviteAutoJoinPolicy = NonNullable<MatrixConfig["autoJoin"]>;
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(
@@ -95,12 +122,6 @@ async function promptMatrixAllowFrom(params: {
const account = resolveMatrixAccount({ cfg, accountId });
const canResolve = Boolean(account.configured);
const parseInput = (raw: string) =>
raw
.split(/[\n,;]+/g)
.map((entry) => entry.trim())
.filter(Boolean);
const isFullUserId = (value: string) => value.startsWith("@") && value.includes(":");
while (true) {
@@ -110,7 +131,7 @@ async function promptMatrixAllowFrom(params: {
initialValue: existingAllowFrom[0] ? String(existingAllowFrom[0]) : undefined,
validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
});
const parts = parseInput(String(entry));
const parts = splitSetupEntries(String(entry));
const resolvedIds: string[] = [];
const pending: string[] = [];
const unresolved: string[] = [];
@@ -187,6 +208,200 @@ function setMatrixGroupRooms(cfg: CoreConfig, roomKeys: string[], accountId?: st
});
}
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<CoreConfig> {
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<CoreConfig> {
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) => (match.name ?? "").toLowerCase() === trimmed.toLowerCase(),
);
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,
@@ -289,13 +504,12 @@ async function runMatrixConfigure(params: {
});
if (useEnv) {
next = updateMatrixAccountConfig(next, accountId, { enabled: true });
if (params.forceAllowFrom) {
next = await promptMatrixAllowFrom({
cfg: next,
prompter: params.prompter,
accountId,
});
}
next = await configureMatrixAccessPrompts({
cfg: next,
prompter: params.prompter,
forceAllowFrom: params.forceAllowFrom,
accountId,
});
return { cfg: next, accountId };
}
}
@@ -421,84 +635,12 @@ async function runMatrixConfigure(params: {
encryption: enableEncryption,
});
if (params.forceAllowFrom) {
next = await promptMatrixAllowFrom({
cfg: next,
prompter: params.prompter,
accountId,
});
}
const existingAccountConfig = resolveMatrixAccountConfig({ cfg: next, accountId });
const existingGroups = existingAccountConfig.groups ?? existingAccountConfig.rooms;
const accessConfig = await promptChannelAccessConfig({
next = await configureMatrixAccessPrompts({
cfg: next,
prompter: params.prompter,
label: "Matrix rooms",
currentPolicy: existingAccountConfig.groupPolicy ?? "allowlist",
currentEntries: Object.keys(existingGroups ?? {}),
placeholder: "!roomId:server, #alias:server, Project Room",
updatePrompt: Boolean(existingGroups),
forceAllowFrom: params.forceAllowFrom,
accountId,
});
if (accessConfig) {
if (accessConfig.policy !== "allowlist") {
next = setMatrixGroupPolicy(next, accessConfig.policy, 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,
query: trimmed,
limit: 10,
});
const exact = matches.find(
(match) => (match.name ?? "").toLowerCase() === trimmed.toLowerCase(),
);
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", accountId);
next = setMatrixGroupRooms(next, roomKeys, accountId);
}
}
return { cfg: next, accountId };
}

View File

@@ -57,6 +57,7 @@ export {
moveSingleAccountChannelSectionToDefaultAccount,
promptAccountId,
promptChannelAccessConfig,
splitSetupEntries,
} from "openclaw/plugin-sdk/setup";
export type { RuntimeEnv } from "openclaw/plugin-sdk/runtime";
export {