refactor(channels): trim deprecated setup-input fields and legacy promotion keys to published readers (#112587)

* refactor(channels): trim deprecated setup-input fields and legacy promotion keys to published readers

* ci: retrigger after dropped ready event

* fix(channels): update setup-input guardrails and dead code for the trimmed tier
This commit is contained in:
Peter Steinberger
2026-07-22 01:15:58 -07:00
committed by GitHub
parent a95c059f11
commit b40a8da70e
16 changed files with 142 additions and 213 deletions
+18 -3
View File
@@ -96,13 +96,28 @@ permanently. Channel-specific fields remain typed in a deprecated compatibility
tier so existing external plugins still compile while plugin authors move those
fields into plugin-local setup input types.
OpenClaw does not ship major releases. A registry sweep on 2026-07-22 inspected
426 published out-of-tree channel plugins and removed 21 fields with no readers.
The 22 retained fields each have a known published reader. Each further field is
deleted as soon as no published plugin reads it; the retained set shrinks as
plugin authors migrate to plugin-local setup input types.
The same sweep removed 23 legacy undeclared-adapter promotion keys with no
published dependents. Six common keys and the setup-only `rooms` key remain.
That set also shrinks as published plugins declare `singleAccountKeysToMove`.
The shared type has no index signature. Plugin-owned keys can still be present
on runtime input objects; declare them in a plugin-local intersection or narrow
them through the owning plugin's setup schema.
| `code` | `owner` | `replacement` | `removeAfter` | Removal condition |
| --------------------------------------- | --------- | ------------------------------------------------------------------------------------------------ | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `plugin-sdk-channel-setup-input-fields` | `channel` | Intersect `ChannelSetupInput` with a plugin-local type that declares the owning channel's fields | `next-plugin-sdk-major` | Remove at the first Plugin SDK major after the bundled-plugin migration in [#112238](https://github.com/openclaw/openclaw/issues/112238) |
| `code` | `owner` | `replacement` | Removal condition |
| --------------------------------------- | --------- | ------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------- |
| `plugin-sdk-channel-setup-input-fields` | `channel` | Intersect `ChannelSetupInput` with a plugin-local type that declares the owning channel's fields | Delete a field when the published-plugin registry sweep has no reader |
The legacy undeclared-adapter promotion tier follows the same reader-driven
policy. Declare `singleAccountKeysToMove`, including an empty array when the
plugin needs no extra promotion keys, so the shared fallback can be retired one
key at a time.
This is a source/type compatibility record only. It has no runtime adapter or
compatibility-registry entry because runtime setup input objects and setup
+6 -4
View File
@@ -387,9 +387,11 @@ export const acmeSetupAdapter: ChannelSetupAdapter = {
Channel-specific fields that were previously declared directly on
`ChannelSetupInput` remain temporarily typed for external source compatibility.
They are deprecated and will be removed at the next Plugin SDK major after
[#112238](https://github.com/openclaw/openclaw/issues/112238). New and bundled
plugins must not rely on that tier; declare the fields they own locally.
They are deprecated. A 2026-07-22 registry sweep of 426 published out-of-tree
channel plugins removed 21 fields with no readers and retained 22 with known
readers. Each retained field is deleted as soon as no published plugin reads it;
no version boundary is required. New and bundled plugins must not rely on this
tier; declare the fields they own locally.
### Channel-owned single-account promotion
@@ -401,7 +403,7 @@ Every channel plugin can extend or narrow that promotion through its setup adapt
- `namedAccountPromotionKeys`: when named accounts already exist, only these keys move into the promoted account; shared policy/delivery keys stay at the channel root
- `resolveSingleAccountPromotionTarget(...)`: choose which existing account receives promoted values
The presence of `singleAccountKeysToMove` marks the promotion contract complete. Declare the field even when it is an empty array to opt out of legacy key promotion. Adapters that omit the field retain the pre-declaration promotion tiers for compatibility with already-published plugins; this compatibility tier is scheduled for removal at the next Plugin SDK major after the migration documented in [#112238](https://github.com/openclaw/openclaw/issues/112238).
The presence of `singleAccountKeysToMove` marks the promotion contract complete. Declare the field even when it is an empty array to opt out of legacy key promotion. Adapters that omit the field retain a reader-backed pre-declaration promotion tier for already-published plugins. The 2026-07-22 registry sweep removed 23 keys with no published dependents and retained six common keys plus the setup-only `rooms` key. Each retained key is deleted as soon as its published readers migrate to declarations; no version boundary is required.
Declare `openclaw.setupFeatures.configPromotion: true` in the plugin package manifest when doctor must load these declarations from the lightweight bundled setup artifact. The setup-only plugin surface and the full channel plugin must expose the same declarations.
+17 -14
View File
@@ -1,5 +1,6 @@
// ClickClack tests cover non-interactive setup validation and config writes.
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/account-id";
import type { ChannelSetupInput } from "openclaw/plugin-sdk/channel-setup";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import { createNonExitingRuntimeEnv } from "openclaw/plugin-sdk/plugin-test-runtime";
import { beforeEach, describe, expect, it, vi } from "vitest";
@@ -20,6 +21,13 @@ import {
normalizeClickClackBaseUrl,
} from "./setup-core.js";
type ClickClackSetupInput = ChannelSetupInput & {
baseUrl?: string;
code?: string;
workspace?: string;
agentActivity?: boolean;
};
// Structural stand-in for the internal claim error: the setup formatter
// duck-types on a numeric `status`, so tests need only that shape.
function makeClaimError(status: number, detail: string): Error {
@@ -29,7 +37,7 @@ function makeClaimError(status: number, detail: string): Error {
function validate(params: {
cfg?: OpenClawConfig;
accountId?: string;
input: Parameters<NonNullable<typeof clickClackSetupAdapter.validateInput>>[0]["input"];
input: ClickClackSetupInput;
}) {
return clickClackSetupAdapter.validateInput?.({
cfg: params.cfg ?? {},
@@ -38,12 +46,7 @@ function validate(params: {
});
}
async function prepare(
input: Parameters<
NonNullable<typeof clickClackSetupAdapter.prepareAccountConfigInput>
>[0]["input"],
cfg: OpenClawConfig = {},
) {
async function prepare(input: ClickClackSetupInput, cfg: OpenClawConfig = {}) {
return await clickClackSetupAdapter.prepareAccountConfigInput?.({
cfg,
accountId: DEFAULT_ACCOUNT_ID,
@@ -331,7 +334,7 @@ describe("ClickClack setup adapter", () => {
defaultTo: " channel:general ",
allowFrom: ["*"],
agentActivity: true,
},
} as ClickClackSetupInput,
}),
).toEqual({
channels: {
@@ -432,7 +435,7 @@ describe("ClickClack setup adapter", () => {
token: "ccb_default",
baseUrl: "https://clickclack.example/",
workspace: " default ",
},
} as ClickClackSetupInput,
}),
).toEqual({
channels: {
@@ -455,7 +458,7 @@ describe("ClickClack setup adapter", () => {
tokenFile: "/run/secrets/clickclack",
baseUrl: "https://work.clickclack.example/",
workspace: "wsp_work",
},
} as ClickClackSetupInput,
}),
).toEqual({
channels: {
@@ -485,7 +488,7 @@ describe("ClickClack setup adapter", () => {
useEnv: true,
baseUrl: "https://clickclack.example/",
workspace: "default",
},
} as ClickClackSetupInput,
}),
).toEqual({
channels: {
@@ -522,7 +525,7 @@ describe("ClickClack setup adapter", () => {
token: "ccb_new",
baseUrl: "https://clickclack.example",
workspace: "default",
},
} as ClickClackSetupInput,
});
expect(withToken.channels?.clickclack).toMatchObject({ token: "ccb_new" });
expect(withToken.channels?.clickclack).not.toHaveProperty("tokenFile");
@@ -541,7 +544,7 @@ describe("ClickClack setup adapter", () => {
tokenFile: "/run/secrets/new-token",
baseUrl: "https://clickclack.example",
workspace: "default",
},
} as ClickClackSetupInput,
});
expect(withFile.channels?.clickclack).toMatchObject({
tokenFile: "/run/secrets/new-token",
@@ -584,7 +587,7 @@ describe("ClickClack setup adapter", () => {
token: "ccb_work",
baseUrl: "https://clickclack.example",
workspace: "work",
},
} as ClickClackSetupInput,
});
expect(namedWithToken.channels?.clickclack).not.toHaveProperty("tokenFile");
expect(namedWithToken.channels?.clickclack?.accounts).toMatchObject({
@@ -5,6 +5,7 @@ import type { RuntimeEnv } from "../runtime-api.js";
import { matrixPlugin } from "./channel.js";
import { resolveMatrixAccount } from "./matrix/accounts.js";
import { resolveMatrixConfigForAccount } from "./matrix/client/config.js";
import type { MatrixSetupInput } from "./setup-config.js";
import { installMatrixTestRuntime } from "./test-runtime.js";
import type { CoreConfig } from "./types.js";
@@ -360,7 +361,7 @@ describe("matrix directory", () => {
homeserver: "https://matrix.example.org",
userId: "@ops:example.org",
accessToken: "ops-token",
},
} as MatrixSetupInput,
}) as CoreConfig;
expect(updated.channels?.["matrix"]?.accessToken).toBeUndefined();
@@ -405,7 +406,7 @@ describe("matrix directory", () => {
homeserver: "https://matrix.example.org",
userId: "@bot:example.org",
accessToken: "bot-token",
},
} as MatrixSetupInput,
}) as CoreConfig;
const matrixConfig = updated.channels?.["matrix"];
@@ -580,7 +581,7 @@ describe("matrix directory", () => {
homeserver: "https://matrix.example.org",
userId: "@bot:example.org",
password: "new-password", // pragma: allowlist secret
},
} as MatrixSetupInput,
}) as CoreConfig;
expect(updated.channels?.["matrix"]?.accounts?.default?.password).toBe("new-password");
@@ -608,7 +609,7 @@ describe("matrix directory", () => {
input: {
homeserver: "https://matrix.example.org",
accessToken: "new-token",
},
} as MatrixSetupInput,
}) as CoreConfig;
expect(updated.channels?.["matrix"]?.accounts?.default?.accessToken).toBe("new-token");
+9 -8
View File
@@ -1,6 +1,7 @@
// Matrix tests cover setup core plugin behavior.
import type { ChannelSetupWizardAdapter } from "openclaw/plugin-sdk/setup";
import { describe, expect, it, vi } from "vitest";
import type { MatrixSetupInput } from "./setup-config.js";
import { createMatrixSetupWizardProxy, matrixSetupAdapter } from "./setup-core.js";
import type { CoreConfig } from "./types.js";
@@ -12,7 +13,7 @@ function applyOpsAccountConfig(cfg: CoreConfig): CoreConfig {
name: "Ops",
homeserver: "https://matrix.example.org",
accessToken: "ops-token",
},
} as MatrixSetupInput,
}) as CoreConfig;
}
@@ -201,7 +202,7 @@ describe("matrixSetupAdapter", () => {
homeserver: "https://matrix.example.org",
userId: "@ops:example.org",
accessToken: "ops-token",
},
} as MatrixSetupInput,
}) as CoreConfig;
expect(next.channels?.matrix?.homeserver).toBeUndefined();
@@ -346,7 +347,7 @@ describe("matrixSetupAdapter", () => {
name: "Ops",
useEnv: true,
avatarUrl: " mxc://example.org/ops-avatar ",
},
} as MatrixSetupInput,
}) as CoreConfig;
expectFields(next.channels?.matrix?.accounts?.ops, {
@@ -366,7 +367,7 @@ describe("matrixSetupAdapter", () => {
homeserver: "https://matrix.example.org",
accessToken: "ops-token",
proxy: "http://127.0.0.1:7890",
},
} as MatrixSetupInput,
}) as CoreConfig;
expectFields(next.channels?.matrix?.accounts?.ops, {
@@ -385,7 +386,7 @@ describe("matrixSetupAdapter", () => {
homeserver: "https://matrix.example.org",
accessToken: "ops-token",
avatarUrl: " mxc://example.org/ops-avatar ",
},
} as MatrixSetupInput,
}) as CoreConfig;
expectFields(next.channels?.matrix?.accounts?.ops, {
@@ -404,7 +405,7 @@ describe("matrixSetupAdapter", () => {
homeserver: "https://matrix.example.org",
accessToken: "ops-token",
avatarUrl: "file:///tmp/avatar.png",
},
} as MatrixSetupInput,
});
expect(validationError).toBe("Matrix avatar URL must be an mxc:// URI or an http(s) URL.");
@@ -418,7 +419,7 @@ describe("matrixSetupAdapter", () => {
homeserver: "http://matrix.internal:8008",
accessToken: "ops-token",
dangerouslyAllowPrivateNetwork: true,
},
} as MatrixSetupInput,
}) as CoreConfig;
expectFields(next.channels?.matrix?.accounts?.ops, {
@@ -458,7 +459,7 @@ describe("matrixSetupAdapter", () => {
homeserver: "https://matrix.example.org",
userId: "@ops:example.org",
accessToken: "ops-token",
},
} as MatrixSetupInput,
}) as CoreConfig;
expect(next.channels?.matrix?.streaming).toEqual({ block: { enabled: true } });
+4 -1
View File
@@ -1,9 +1,12 @@
import type { ChannelSetupInput } from "openclaw/plugin-sdk/channel-setup";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import { afterEach, describe, expect, it } from "vitest";
import { listRaftAccountIds, resolveRaftAccount } from "./accounts.js";
import { raftChannelConfigSchema } from "./config-schema.js";
import { raftSetupPlugin } from "./setup.js";
type RaftSetupInput = ChannelSetupInput & { profile?: string };
const originalProfile = process.env.RAFT_PROFILE;
const runtimeConfigSchema = raftChannelConfigSchema.runtime;
if (!runtimeConfigSchema) {
@@ -64,7 +67,7 @@ describe("Raft account resolution", () => {
accountId: "support",
input: {
profile: "support-profile",
},
} as RaftSetupInput,
});
expect(next.channels?.raft).toEqual({
@@ -24,39 +24,14 @@ import {
} from "./setup-promotion-helpers.js";
const legacyCommonKeys = [
"appToken",
"account",
"signalNumber",
"authDir",
"cliPath",
"dbPath",
"httpUrl",
"httpHost",
"httpPort",
"webhookSecret",
"service",
"region",
"homeserver",
"userId",
"accessToken",
"appToken",
"httpUrl",
"password",
"deviceName",
"url",
"code",
] as const;
const legacySetupOnlyKeys = [
"deviceId",
"avatarUrl",
"initialSyncLimit",
"encryption",
"allowlistOnly",
"threadReplies",
"startupVerification",
"startupVerificationCooldownHours",
"autoJoin",
"autoJoinAllowlist",
"rooms",
"userId",
"webhookSecret",
] as const;
const legacySetupOnlyKeys = ["rooms"] as const;
function valuesFor(keys: readonly string[]): Record<string, string> {
return Object.fromEntries(keys.map((key) => [key, `value:${key}`]));
@@ -101,7 +76,7 @@ describe("setup promotion helpers", () => {
expect(resolveBundledSurfaceMock).not.toHaveBeenCalled();
});
it("restores the exact former common tier when no declarations resolve", () => {
it("retains the published-reader common tier when no declarations resolve", () => {
expect(
resolveSingleAccountKeysToMove({
channelKey: "demo",
@@ -113,7 +88,7 @@ describe("setup promotion helpers", () => {
).toEqual(legacyCommonKeys);
});
it("adds the exact former setup-only tier on direct setup paths", () => {
it("adds the published-reader setup-only tier on direct setup paths", () => {
expect(
resolveSingleAccountKeysToMove({
channelKey: "demo",
@@ -31,44 +31,14 @@ type SingleAccountPromotionParams = {
resolveBundledSurface?: (channelKey: string) => ChannelSetupPromotionSurface | null;
};
// Shipped Plugin SDK compatibility: out-of-tree setup adapters published before
// promotion declarations existed still inherit these former core tiers. Remove at
// the next SDK major after #112238 / PR 3 makes declarations mandatory.
// Published undeclared adapters still depend on these keys: Chatu, GroupMe, OneBot,
// and WhatsApp Cloud use accessToken; Claworld uses appToken; OneBot uses httpUrl;
// MQTT and TrueConf use password; Rocket.Chat uses rooms and userId; WorkClaw and
// TIMBot use userId; Vama, Pinto, and Roam use webhookSecret (2026-07-22 sweep).
// Delete each key as soon as no published plugin reads it; no version boundary is needed.
const LEGACY_UNDECLARED_ADAPTER_PROMOTION_KEYS = {
common: [
"appToken",
"account",
"signalNumber",
"authDir",
"cliPath",
"dbPath",
"httpUrl",
"httpHost",
"httpPort",
"webhookSecret",
"service",
"region",
"homeserver",
"userId",
"accessToken",
"password",
"deviceName",
"url",
"code",
],
setupOnly: [
"deviceId",
"avatarUrl",
"initialSyncLimit",
"encryption",
"allowlistOnly",
"threadReplies",
"startupVerification",
"startupVerificationCooldownHours",
"autoJoin",
"autoJoinAllowlist",
"rooms",
],
common: ["accessToken", "appToken", "httpUrl", "password", "userId", "webhookSecret"],
setupOnly: ["rooms"],
} as const;
const legacyUndeclaredAdapterCommonPromotionKeys = new Set<string>(
+6 -9
View File
@@ -9,12 +9,7 @@ import type { RuntimeEnv } from "../../runtime.js";
import type { WizardPrompter } from "../../wizard/prompts.js";
import type { ChannelAccessPolicy } from "./setup-group-access.js";
import type { ChannelConfigAdapter, ChannelSetupAdapter } from "./types.adapters.js";
import type {
ChannelCapabilities,
ChannelId,
ChannelMeta,
ChannelSetupInput,
} from "./types.core.js";
import type { ChannelCapabilities, ChannelId, ChannelMeta } from "./types.core.js";
export type ChannelSetupPlugin = {
id: ChannelId;
@@ -88,7 +83,8 @@ type ChannelSetupWizardEnvShortcut = {
/** Declarative secret/input step for a channel account credential. */
export type ChannelSetupWizardCredential = {
inputKey: keyof ChannelSetupInput;
/** Plugin-owned key written into the runtime setup input. */
inputKey: string;
providerHint: string;
credentialLabel: string;
preferredEnvVar?: string;
@@ -124,7 +120,8 @@ export type ChannelSetupWizardCredential = {
/** Declarative non-secret text step that can depend on resolved credentials. */
export type ChannelSetupWizardTextInput = {
inputKey: keyof ChannelSetupInput;
/** Plugin-owned key written into the runtime setup input. */
inputKey: string;
message: string;
placeholder?: string;
required?: boolean;
@@ -179,7 +176,7 @@ export type ChannelSetupWizardAllowFromEntry = {
type ChannelSetupWizardAllowFrom = {
helpTitle?: string;
helpLines?: string[];
credentialInputKey?: keyof ChannelSetupInput;
credentialInputKey?: string;
message: string;
placeholder: string;
invalidWithoutCredentialNote: string;
+26 -67
View File
@@ -104,96 +104,55 @@ type ChannelSetupEnvelope = {
allowFrom?: string[];
};
/**
* Compatibility fields with known published readers in the 2026-07-22 registry sweep.
* Each field is deleted as soon as no published plugin reads it; no version boundary is needed.
*/
type DeprecatedChannelSetupFields = {
/** @deprecated Channel-owned setup field; declare it in the owning plugin's setup input type. Removed at the next Plugin SDK major (#112238). */
/** @deprecated Declare this field in the owning plugin's setup input type. */
privateKey?: string;
/** @deprecated Channel-owned setup field; declare it in the owning plugin's setup input type. Removed at the next Plugin SDK major (#112238). */
/** @deprecated Declare this field in the owning plugin's setup input type. */
secret?: string;
/** @deprecated Channel-owned setup field; declare it in the owning plugin's setup input type. Removed at the next Plugin SDK major (#112238). */
secretFile?: string;
/** @deprecated Channel-owned setup field; declare it in the owning plugin's setup input type. Removed at the next Plugin SDK major (#112238). */
/** @deprecated Declare this field in the owning plugin's setup input type. */
botToken?: string;
/** @deprecated Channel-owned setup field; declare it in the owning plugin's setup input type. Removed at the next Plugin SDK major (#112238). */
/** @deprecated Declare this field in the owning plugin's setup input type. */
appToken?: string;
/** @deprecated Channel-owned setup field; declare it in the owning plugin's setup input type. Removed at the next Plugin SDK major (#112238). */
userToken?: string;
/** @deprecated Channel-owned setup field; declare it in the owning plugin's setup input type. Removed at the next Plugin SDK major (#112238). */
/** @deprecated Declare this field in the owning plugin's setup input type. */
signingSecret?: string;
/** @deprecated Channel-owned setup field; declare it in the owning plugin's setup input type. Removed at the next Plugin SDK major (#112238). */
identity?: "bot" | "user";
/** @deprecated Channel-owned setup field; declare it in the owning plugin's setup input type. Removed at the next Plugin SDK major (#112238). */
/** @deprecated Declare this field in the owning plugin's setup input type. */
mode?: "socket" | "http" | "relay";
/** @deprecated Channel-owned setup field; declare it in the owning plugin's setup input type. Removed at the next Plugin SDK major (#112238). */
signalNumber?: string;
/** @deprecated Channel-owned setup field; declare it in the owning plugin's setup input type. Removed at the next Plugin SDK major (#112238). */
/** @deprecated Declare this field in the owning plugin's setup input type. */
cliPath?: string;
/** @deprecated Channel-owned setup field; declare it in the owning plugin's setup input type. Removed at the next Plugin SDK major (#112238). */
dbPath?: string;
/** @deprecated Channel-owned setup field; declare it in the owning plugin's setup input type. Removed at the next Plugin SDK major (#112238). */
service?: "imessage" | "sms" | "auto";
/** @deprecated Channel-owned setup field; declare it in the owning plugin's setup input type. Removed at the next Plugin SDK major (#112238). */
region?: string;
/** @deprecated Channel-owned setup field; declare it in the owning plugin's setup input type. Removed at the next Plugin SDK major (#112238). */
/** @deprecated Declare this field in the owning plugin's setup input type. */
authDir?: string;
/** @deprecated Channel-owned setup field; declare it in the owning plugin's setup input type. Removed at the next Plugin SDK major (#112238). */
/** @deprecated Declare this field in the owning plugin's setup input type. */
httpUrl?: string;
/** @deprecated Channel-owned setup field; declare it in the owning plugin's setup input type. Removed at the next Plugin SDK major (#112238). */
httpHost?: string;
/** @deprecated Channel-owned setup field; declare it in the owning plugin's setup input type. Removed at the next Plugin SDK major (#112238). */
/** @deprecated Declare this field in the owning plugin's setup input type. */
httpPort?: string;
/** @deprecated Channel-owned setup field; declare it in the owning plugin's setup input type. Removed at the next Plugin SDK major (#112238). */
/** @deprecated Declare this field in the owning plugin's setup input type. */
webhookPath?: string;
/** @deprecated Channel-owned setup field; declare it in the owning plugin's setup input type. Removed at the next Plugin SDK major (#112238). */
/** @deprecated Declare this field in the owning plugin's setup input type. */
webhookUrl?: string;
/** @deprecated Channel-owned setup field; declare it in the owning plugin's setup input type. Removed at the next Plugin SDK major (#112238). */
audienceType?: string;
/** @deprecated Channel-owned setup field; declare it in the owning plugin's setup input type. Removed at the next Plugin SDK major (#112238). */
audience?: string;
/** @deprecated Channel-owned setup field; declare it in the owning plugin's setup input type. Removed at the next Plugin SDK major (#112238). */
homeserver?: string;
/** @deprecated Channel-owned setup field; declare it in the owning plugin's setup input type. Removed at the next Plugin SDK major (#112238). */
dangerouslyAllowPrivateNetwork?: boolean;
/**
* Compatibility alias for `dangerouslyAllowPrivateNetwork`.
* @deprecated Channel-owned setup field; declare it in the owning plugin's setup input type. Removed at the next Plugin SDK major (#112238).
*/
allowPrivateNetwork?: boolean;
/** @deprecated Channel-owned setup field; declare it in the owning plugin's setup input type. Removed at the next Plugin SDK major (#112238). */
proxy?: string;
/** @deprecated Channel-owned setup field; declare it in the owning plugin's setup input type. Removed at the next Plugin SDK major (#112238). */
/** @deprecated Declare this field in the owning plugin's setup input type. */
userId?: string;
/** @deprecated Channel-owned setup field; declare it in the owning plugin's setup input type. Removed at the next Plugin SDK major (#112238). */
/** @deprecated Declare this field in the owning plugin's setup input type. */
accessToken?: string;
/** @deprecated Channel-owned setup field; declare it in the owning plugin's setup input type. Removed at the next Plugin SDK major (#112238). */
/** @deprecated Declare this field in the owning plugin's setup input type. */
password?: string;
/** @deprecated Channel-owned setup field; declare it in the owning plugin's setup input type. Removed at the next Plugin SDK major (#112238). */
/** @deprecated Declare this field in the owning plugin's setup input type. */
deviceName?: string;
/** @deprecated Channel-owned setup field; declare it in the owning plugin's setup input type. Removed at the next Plugin SDK major (#112238). */
avatarUrl?: string;
/** @deprecated Channel-owned setup field; declare it in the owning plugin's setup input type. Removed at the next Plugin SDK major (#112238). */
initialSyncLimit?: number;
/** @deprecated Channel-owned setup field; declare it in the owning plugin's setup input type. Removed at the next Plugin SDK major (#112238). */
profile?: string;
/** @deprecated Channel-owned setup field; declare it in the owning plugin's setup input type. Removed at the next Plugin SDK major (#112238). */
ship?: string;
/** @deprecated Channel-owned setup field; declare it in the owning plugin's setup input type. Removed at the next Plugin SDK major (#112238). */
/** @deprecated Declare this field in the owning plugin's setup input type. */
url?: string;
/** @deprecated Channel-owned setup field; declare it in the owning plugin's setup input type. Removed at the next Plugin SDK major (#112238). */
/** @deprecated Declare this field in the owning plugin's setup input type. */
baseUrl?: string;
/** @deprecated Channel-owned setup field; declare it in the owning plugin's setup input type. Removed at the next Plugin SDK major (#112238). */
relayUrls?: string;
/** @deprecated Channel-owned setup field; declare it in the owning plugin's setup input type. Removed at the next Plugin SDK major (#112238). */
/** @deprecated Declare this field in the owning plugin's setup input type. */
code?: string;
/** @deprecated Channel-owned setup field; declare it in the owning plugin's setup input type. Removed at the next Plugin SDK major (#112238). */
/** @deprecated Declare this field in the owning plugin's setup input type. */
groupChannels?: string[];
/** @deprecated Channel-owned setup field; declare it in the owning plugin's setup input type. Removed at the next Plugin SDK major (#112238). */
/** @deprecated Declare this field in the owning plugin's setup input type. */
dmAllowlist?: string[];
/** @deprecated Channel-owned setup field; declare it in the owning plugin's setup input type. Removed at the next Plugin SDK major (#112238). */
/** @deprecated Declare this field in the owning plugin's setup input type. */
autoDiscoverChannels?: boolean;
/** @deprecated Channel-owned setup field; declare it in the owning plugin's setup input type. Removed at the next Plugin SDK major (#112238). */
workspace?: string;
/** @deprecated Channel-owned setup field; declare it in the owning plugin's setup input type. Removed at the next Plugin SDK major (#112238). */
agentActivity?: boolean;
};
/** Generic setup envelope used by CLI, onboarding, and channel-owned setup adapters. */
+10 -5
View File
@@ -2,6 +2,7 @@
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { getBundledChannelSetupPlugin } from "../channels/plugins/bundled.js";
import type { ChannelPluginCatalogEntry } from "../channels/plugins/catalog.js";
import type { ChannelSetupInput } from "../channels/plugins/types.core.js";
import type { ChannelPlugin } from "../channels/plugins/types.public.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import type { PluginInstallRecord } from "../config/types.plugins.js";
@@ -379,6 +380,10 @@ type ResolveAccountIdParams = Parameters<
type PrepareAccountConfigInputParams = Parameters<
NonNullable<NonNullable<ChannelPlugin["setup"]>["prepareAccountConfigInput"]>
>[0];
type SignalSetupInput = ChannelSetupInput & { signalNumber?: string };
type NextcloudTalkSetupInput = ChannelSetupInput & { secretFile?: string };
type MatrixSetupInput = ChannelSetupInput & { initialSyncLimit?: number };
type PreparedChatSetupInput = ChannelSetupInput & { workspace?: string };
function createSignalPlugin(
afterAccountConfigWritten: SignalAfterAccountConfigWritten,
@@ -397,7 +402,7 @@ function createSignalPlugin(
enabled: true,
accounts: {
[accountId]: {
account: input.signalNumber,
account: (input as SignalSetupInput).signalNumber,
},
},
},
@@ -619,7 +624,7 @@ describe("channelsAddCommand", () => {
enabled: true,
baseUrl: input.baseUrl,
botSecret: input.secret,
botSecretFile: input.secretFile,
botSecretFile: (input as NextcloudTalkSetupInput).secretFile,
},
},
}));
@@ -638,7 +643,7 @@ describe("channelsAddCommand", () => {
...input,
baseUrl: input.baseUrl ?? input.url,
secret: input.secret ?? input.token ?? input.password,
secretFile: input.secretFile ?? input.tokenFile,
secretFile: (input as NextcloudTalkSetupInput).secretFile ?? input.tokenFile,
}),
applyAccountConfig,
},
@@ -777,7 +782,7 @@ describe("channelsAddCommand", () => {
"prepared-chat": {
enabled: true,
token: input.token,
workspace: input.workspace,
workspace: (input as PreparedChatSetupInput).workspace,
},
},
};
@@ -1076,7 +1081,7 @@ describe("channelsAddCommand", () => {
...cfg.channels,
matrix: {
enabled: true,
initialSyncLimit: input.initialSyncLimit,
initialSyncLimit: (input as MatrixSetupInput).initialSyncLimit,
},
},
}));
@@ -1,7 +1,7 @@
// Channels account tests cover non-default Telegram account setup, status, removal, and binding behavior.
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { createPatchedAccountSetupAdapter } from "../channels/plugins/setup-helpers.js";
import type { ChannelStatusIssue } from "../channels/plugins/types.core.js";
import type { ChannelSetupInput, ChannelStatusIssue } from "../channels/plugins/types.core.js";
import type { ChannelPlugin } from "../channels/plugins/types.public.js";
import { createScopedChannelConfigAdapter } from "../plugin-sdk/channel-config-helpers.js";
import { setActivePluginRegistry } from "../plugins/runtime.js";
@@ -36,6 +36,8 @@ type ChannelSectionConfig = {
accounts?: Record<string, Record<string, unknown>>;
};
type SignalSetupInput = ChannelSetupInput & { signalNumber?: string };
function formatChannelStatusJoined(channelAccounts: Record<string, unknown>) {
return formatGatewayChannelsStatusLines({
channelLabels: {
@@ -125,7 +127,7 @@ function createScopedCommandTestPlugin(params: {
token: input.token,
botToken: input.botToken,
appToken: input.appToken,
signalNumber: input.signalNumber,
signalNumber: (input as SignalSetupInput).signalNumber,
}),
}),
...(params.singleAccountKeysToMove
+4 -17
View File
@@ -575,25 +575,12 @@ vi.mock("../channels/plugins/setup-promotion-helpers.js", () => {
"defaultTo",
]);
const legacyCommonSingleAccountKeys = new Set([
"appToken",
"account",
"signalNumber",
"authDir",
"cliPath",
"dbPath",
"httpUrl",
"httpHost",
"httpPort",
"webhookSecret",
"service",
"region",
"homeserver",
"userId",
"accessToken",
"appToken",
"httpUrl",
"password",
"deviceName",
"url",
"code",
"userId",
"webhookSecret",
]);
const declaredSingleAccountKeys: Record<string, readonly string[]> = {
discord: [],
+2 -3
View File
@@ -20,12 +20,11 @@ describe("ChannelSetupInput", () => {
expectTypeOf<ChannelSetupInput["botToken"]>().toEqualTypeOf<string | undefined>();
expectTypeOf<ChannelSetupInput["appToken"]>().toEqualTypeOf<string | undefined>();
expectTypeOf<ChannelSetupInput["signalNumber"]>().toEqualTypeOf<string | undefined>();
expectTypeOf<ChannelSetupInput["homeserver"]>().toEqualTypeOf<string | undefined>();
expectTypeOf<ChannelSetupInput["mode"]>().toEqualTypeOf<
"socket" | "http" | "relay" | undefined
>();
expectTypeOf<ChannelSetupInput["identity"]>().toEqualTypeOf<"bot" | "user" | undefined>();
expectTypeOf<ChannelSetupInput["privateKey"]>().toEqualTypeOf<string | undefined>();
expectTypeOf<ChannelSetupInput["password"]>().toEqualTypeOf<string | undefined>();
const assignable: ChannelSetupInput = {} as ThirdPartySetupInput;
expectTypeOf(assignable).toEqualTypeOf<ChannelSetupInput>();
@@ -111,11 +111,11 @@ export function installChannelActionsContractSuite(params: {
}
}
type ChannelSetupContractCase<ResolvedAccount> = {
type ChannelSetupContractCase<ResolvedAccount, SetupInput extends ChannelSetupInput> = {
name: string;
cfg: OpenClawConfig;
accountId?: string;
input: ChannelSetupInput;
input: SetupInput;
expectedAccountId?: string;
expectedValidation?: string | null;
beforeTest?: () => void;
@@ -123,9 +123,12 @@ type ChannelSetupContractCase<ResolvedAccount> = {
assertResolvedAccount?: (account: ResolvedAccount, cfg: OpenClawConfig) => void;
};
export function installChannelSetupContractSuite<ResolvedAccount>(params: {
export function installChannelSetupContractSuite<
ResolvedAccount,
SetupInput extends ChannelSetupInput = ChannelSetupInput,
>(params: {
plugin: Pick<ChannelPlugin<ResolvedAccount>, "id" | "config" | "setup">;
cases: readonly ChannelSetupContractCase<ResolvedAccount>[];
cases: readonly ChannelSetupContractCase<ResolvedAccount, SetupInput>[];
}) {
it("exposes the base setup contract", () => {
expect(params.plugin.setup).toBeDefined();
@@ -149,11 +149,18 @@ describe("config footprint guardrails", () => {
expect(source).not.toContain("nativeStreaming:");
});
it("keeps shared setup input canonical-first", () => {
it("keeps Matrix setup input canonical-first after plugin ownership", () => {
const source = readSource("extensions/matrix/src/setup-config.ts");
const canonicalIndex = source.indexOf("dangerouslyAllowPrivateNetwork?: boolean;");
const aliasIndex = source.indexOf("allowPrivateNetwork?: boolean;");
expect(canonicalIndex).toBeGreaterThanOrEqual(0);
expect(aliasIndex).toBeGreaterThan(canonicalIndex);
});
it("keeps retired config aliases out of the shared setup input", () => {
const source = readSource("src/channels/plugins/types.core.ts");
expect(source).toContain("dangerouslyAllowPrivateNetwork?: boolean;");
expect(source).toContain("allowPrivateNetwork?: boolean;");
expect(source).not.toContain("streamMode?:");
expect(source).not.toContain("groupMentionsOnly?:");
expect(source).not.toContain("perSession?:");