Files
openclaw/extensions/irc/src/setup-core.ts
Peter Steinberger 99d662473c fix(channels): fail-fast headless channel setup with plugin-declared env contracts (#122530)
* fix(channels): validate headless channel setup

* docs(channels): document headless provisioning

* fix(channels): repair setup metadata typing

* chore(channels): regenerate official channel catalog for env metadata

* fix(slack): keep mode-conditional env contract plugin-owned

Static --use-env declaration keeps only the unconditional SLACK_BOT_TOKEN;
socket-vs-HTTP conditional requirements (app token, signing secret) stay in
Slack's own setup validation so HTTP mode no longer demands an irrelevant
SLACK_APP_TOKEN.

* chore(sdk): regenerate api baselines and catalog after rebase

* fix(slack): align manifest env declaration with runtime contract

* chore(sdk): regenerate api baselines after rebase

* chore(sdk): regenerate api baselines after rebase

* chore(sdk): regenerate api baselines after rebase
2026-08-12 17:12:15 +00:00

184 lines
5.8 KiB
TypeScript

// Irc plugin module implements setup core behavior.
import {
defineChannelSetupContract,
type ChannelSetupAdapter,
type ChannelSetupInput,
} from "openclaw/plugin-sdk/channel-setup";
import type { DmPolicy } from "openclaw/plugin-sdk/config-contracts";
import { parseStrictPositiveInteger } from "openclaw/plugin-sdk/number-runtime";
import { normalizeAccountId } from "openclaw/plugin-sdk/routing";
import {
applyAccountNameToChannelSection,
createSetupInputPresenceValidator,
createTopLevelChannelAllowFromSetter,
createTopLevelChannelDmPolicySetter,
patchScopedAccountConfig,
} from "openclaw/plugin-sdk/setup";
import type { CoreConfig, IrcAccountConfig, IrcNickServConfig } from "./types.js";
const channel = "irc" as const;
const setIrcTopLevelDmPolicy = createTopLevelChannelDmPolicySetter({
channel,
});
const setIrcTopLevelAllowFrom = createTopLevelChannelAllowFromSetter({
channel,
});
const validateIrcRequiredSetupInput = createSetupInputPresenceValidator({
whenNotUseEnv: [
{ someOf: ["host"], message: "IRC requires host." },
{ someOf: ["nick"], message: "IRC requires nick." },
],
});
type IrcSetupInput = ChannelSetupInput & {
host?: string;
port?: number | string;
tls?: boolean;
nick?: string;
username?: string;
realname?: string;
channels?: string[];
password?: string;
};
export function parsePort(raw: string, fallback: number): number {
const trimmed = raw.trim();
if (!trimmed) {
return fallback;
}
const parsed = parseStrictPositiveInteger(trimmed);
if (parsed === undefined || parsed > 65535) {
return fallback;
}
return parsed;
}
function validateIrcPortInput(input: ChannelSetupInput): string | null {
const raw = (input as IrcSetupInput).port;
if (raw === undefined || raw === null || raw === "") {
return null;
}
const parsed = parseStrictPositiveInteger(String(raw));
return parsed !== undefined && parsed <= 65535 ? null : "IRC port must be between 1 and 65535.";
}
export function updateIrcAccountConfig(
cfg: CoreConfig,
accountId: string,
patch: Partial<IrcAccountConfig>,
): CoreConfig {
return patchScopedAccountConfig({
cfg,
channelKey: channel,
accountId,
patch,
ensureChannelEnabled: false,
ensureAccountEnabled: false,
}) as CoreConfig;
}
export function setIrcDmPolicy(cfg: CoreConfig, dmPolicy: DmPolicy): CoreConfig {
return setIrcTopLevelDmPolicy(cfg, dmPolicy) as CoreConfig;
}
export function setIrcAllowFrom(cfg: CoreConfig, allowFrom: string[]): CoreConfig {
return setIrcTopLevelAllowFrom(cfg, allowFrom) as CoreConfig;
}
export function setIrcNickServ(
cfg: CoreConfig,
accountId: string,
nickserv?: IrcNickServConfig,
): CoreConfig {
return updateIrcAccountConfig(cfg, accountId, { nickserv });
}
export function setIrcGroupAccess(
cfg: CoreConfig,
accountId: string,
policy: "open" | "allowlist" | "disabled",
entries: string[],
normalizeGroupEntry: (raw: string) => string | null,
): CoreConfig {
if (policy !== "allowlist") {
return updateIrcAccountConfig(cfg, accountId, { enabled: true, groupPolicy: policy });
}
const normalizedEntries = [
...new Set(entries.flatMap((entry) => normalizeGroupEntry(entry) ?? [])),
];
const groups = Object.fromEntries(normalizedEntries.map((entry) => [entry, {}]));
return updateIrcAccountConfig(cfg, accountId, {
enabled: true,
groupPolicy: "allowlist",
groups,
});
}
export const ircSetupAdapter: ChannelSetupAdapter = {
singleAccountKeysToMove: ["password"],
resolveAccountId: ({ accountId }) => normalizeAccountId(accountId),
applyAccountName: ({ cfg, accountId, name }) =>
applyAccountNameToChannelSection({
cfg,
channelKey: channel,
accountId,
name,
}),
validateInput: (params) =>
validateIrcRequiredSetupInput(params) ?? validateIrcPortInput(params.input),
applyAccountConfig: ({ cfg, accountId, input }) => {
const setupInput = input as IrcSetupInput;
const namedConfig = applyAccountNameToChannelSection({
cfg,
channelKey: channel,
accountId,
name: setupInput.name,
});
const portInput =
typeof setupInput.port === "number" ? String(setupInput.port) : (setupInput.port ?? "");
const patch: Partial<IrcAccountConfig> = {
enabled: true,
host: setupInput.host?.trim(),
port: portInput ? parsePort(portInput, setupInput.tls === false ? 6667 : 6697) : undefined,
tls: setupInput.tls,
nick: setupInput.nick?.trim(),
username: setupInput.username?.trim(),
realname: setupInput.realname?.trim(),
password: setupInput.password?.trim(),
channels: setupInput.channels,
};
return patchScopedAccountConfig({
cfg: namedConfig,
channelKey: channel,
accountId,
patch,
}) as CoreConfig;
},
};
export const ircSetupContract = defineChannelSetupContract({
fields: {
host: { kind: "string", cli: { flags: "--host <host>", description: "IRC server host" } },
port: { kind: "string", cli: { flags: "--port <port>", description: "IRC server port" } },
tls: { kind: "boolean", cli: { flags: "--tls", description: "Use TLS for IRC" } },
nick: { kind: "string", cli: { flags: "--nick <nick>", description: "IRC nickname" } },
username: { kind: "string", cli: { flags: "--username <name>", description: "IRC username" } },
realname: { kind: "string", cli: { flags: "--realname <name>", description: "IRC real name" } },
channels: {
kind: "string-list",
cli: { flags: "--channels <names>", description: "IRC channels" },
},
password: {
kind: "string",
sensitive: true,
cli: { flags: "--password <password>", description: "IRC server password" },
},
useEnv: {
kind: "boolean",
cli: { flags: "--use-env", description: "Use IRC environment configuration" },
envVars: ["IRC_HOST", "IRC_NICK"],
},
},
legacyAdapter: ircSetupAdapter,
});