refactor: move shared helpers off reserved sdk seams

This commit is contained in:
Peter Steinberger
2026-04-27 13:07:02 +01:00
parent e91f9a3f67
commit 0141471dd5
30 changed files with 89 additions and 34 deletions

View File

@@ -1,2 +1,2 @@
74344f185b3149695443bf8815c9dd784daf9c0b8118ecc54129dc57899e9564 plugin-sdk-api-baseline.json
7b84c2f1e5743dac9c764fdee6d3b23e64553516c409f4a24f009a36c40d64e8 plugin-sdk-api-baseline.jsonl
491267e919c6bf426f673a9066e703811c7779a32de87edd0ce493147fd4438e plugin-sdk-api-baseline.json
590d21aeb520f34b5bf23abb7b17602b204f170547c772d60b604bb34a3940bb plugin-sdk-api-baseline.jsonl

View File

@@ -138,12 +138,12 @@ For the plugin authoring guide, see [Plugin SDK overview](/plugins/sdk-overview)
| `plugin-sdk/allow-from` | `formatAllowFromLowercase` |
| `plugin-sdk/channel-secret-runtime` | Narrow secret-contract collection helpers for channel/plugin secret surfaces |
| `plugin-sdk/secret-ref-runtime` | Narrow `coerceSecretRef` and SecretRef typing helpers for secret-contract/config parsing |
| `plugin-sdk/security-runtime` | Shared trust, DM gating, external-content, and secret-collection helpers |
| `plugin-sdk/security-runtime` | Shared trust, DM gating, external-content, constant-time secret comparison, and secret-collection helpers |
| `plugin-sdk/ssrf-policy` | Host allowlist and private-network SSRF policy helpers |
| `plugin-sdk/ssrf-dispatcher` | Narrow pinned-dispatcher helpers without the broad infra runtime surface |
| `plugin-sdk/ssrf-runtime` | Pinned-dispatcher, SSRF-guarded fetch, and SSRF policy helpers |
| `plugin-sdk/ssrf-runtime` | Pinned-dispatcher, SSRF-guarded fetch, SSRF error, and SSRF policy helpers |
| `plugin-sdk/secret-input` | Secret input parsing helpers |
| `plugin-sdk/webhook-ingress` | Webhook request/target helpers |
| `plugin-sdk/webhook-ingress` | Webhook request/target helpers and raw websocket/body coercion |
| `plugin-sdk/webhook-request-guards` | Request body size/timeout helpers |
</Accordion>
@@ -160,7 +160,7 @@ For the plugin authoring guide, see [Plugin SDK overview](/plugins/sdk-overview)
| `plugin-sdk/lazy-runtime` | Lazy runtime import/binding helpers such as `createLazyRuntimeModule`, `createLazyRuntimeMethod`, and `createLazyRuntimeSurface` |
| `plugin-sdk/process-runtime` | Process exec helpers |
| `plugin-sdk/cli-runtime` | CLI formatting, wait, version, argument-invocation, and lazy command-group helpers |
| `plugin-sdk/gateway-runtime` | Gateway client and channel-status patch helpers |
| `plugin-sdk/gateway-runtime` | Gateway client, gateway CLI RPC, gateway protocol errors, and channel-status patch helpers |
| `plugin-sdk/config-runtime` | Config load/write helpers and plugin-config lookup helpers |
| `plugin-sdk/telegram-command-config` | Telegram command-name/description normalization and duplicate/conflict checks, even when the bundled Telegram contract surface is unavailable |
| `plugin-sdk/text-autolink-runtime` | File-reference autolink detection without the broad text-runtime barrel |

View File

@@ -1,6 +1,6 @@
import type { IncomingMessage, ServerResponse } from "node:http";
import { safeEqualSecret } from "openclaw/plugin-sdk/browser-security-runtime";
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
import { safeEqualSecret } from "openclaw/plugin-sdk/security-runtime";
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
import { resolveBlueBubblesEffectiveAllowPrivateNetwork } from "./accounts.js";
import { createBlueBubblesDebounceRegistry } from "./monitor-debounce.js";

View File

@@ -1,5 +1,5 @@
export type { RuntimeEnv } from "../runtime-api.js";
export { safeEqualSecret } from "openclaw/plugin-sdk/browser-security-runtime";
export { safeEqualSecret } from "openclaw/plugin-sdk/security-runtime";
export { applyBasicWebhookRequestGuards } from "openclaw/plugin-sdk/webhook-ingress";
export {
installRequestBodyLimitGuard,

View File

@@ -1,10 +1,10 @@
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
import {
callGatewayFromCli,
ErrorCodes,
errorShape,
} from "openclaw/plugin-sdk/browser-node-runtime";
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
import type { GatewayRequestHandlerOptions } from "openclaw/plugin-sdk/gateway-runtime";
type GatewayRequestHandlerOptions,
} from "openclaw/plugin-sdk/gateway-runtime";
import { definePluginEntry, type OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry";
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
import { Type } from "typebox";

View File

@@ -1,5 +1,5 @@
import { callGatewayFromCli } from "openclaw/plugin-sdk/browser-node-runtime";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
import { callGatewayFromCli } from "openclaw/plugin-sdk/gateway-runtime";
import type { PluginRuntime } from "openclaw/plugin-sdk/plugin-runtime";
import type { RuntimeLogger } from "openclaw/plugin-sdk/plugin-runtime";
import type { GoogleMeetConfig } from "../config.js";

View File

@@ -1,6 +1,5 @@
import type { Command } from "commander";
import { normalizeAccountId } from "openclaw/plugin-sdk/account-id";
import { formatZonedTimestamp } from "openclaw/plugin-sdk/matrix-runtime-shared";
import type { ChannelSetupInput } from "openclaw/plugin-sdk/setup";
import { resolveMatrixAccount, resolveMatrixAccountConfig } from "./matrix/accounts.js";
import { listMatrixOwnDevices, pruneMatrixStaleGatewayDevices } from "./matrix/actions/devices.js";
@@ -30,6 +29,7 @@ import { isOpenClawManagedMatrixDevice } from "./matrix/device-health.js";
import type { MatrixDirectRoomCandidate } from "./matrix/direct-management.js";
import { formatMatrixErrorMessage } from "./matrix/errors.js";
import { applyMatrixProfileUpdate, type MatrixProfileUpdateResult } from "./profile-update.js";
import { formatZonedTimestamp } from "./runtime-api.js";
import { getMatrixRuntime } from "./runtime.js";
import { matrixSetupAdapter } from "./setup-core.js";
import type { CoreConfig } from "./types.js";

View File

@@ -61,7 +61,7 @@ export { evaluateSenderGroupAccessForPolicy } from "openclaw/plugin-sdk/group-ac
export { createChannelReplyPipeline } from "openclaw/plugin-sdk/channel-reply-pipeline";
export { logTypingFailure } from "openclaw/plugin-sdk/channel-feedback";
export { loadOutboundMediaFromUrl } from "openclaw/plugin-sdk/outbound-media";
export { rawDataToString } from "openclaw/plugin-sdk/browser-node-runtime";
export { rawDataToString } from "openclaw/plugin-sdk/webhook-ingress";
export { chunkTextForOutbound } from "openclaw/plugin-sdk/text-chunking";
export {
DEFAULT_GROUP_HISTORY_LIMIT,

View File

@@ -1,6 +1,6 @@
import { createHmac } from "node:crypto";
import type { IncomingMessage, ServerResponse } from "node:http";
import { safeEqualSecret } from "openclaw/plugin-sdk/browser-security-runtime";
import { safeEqualSecret } from "openclaw/plugin-sdk/security-runtime";
import {
normalizeOptionalString,
normalizeStringifiedOptionalString,

View File

@@ -1,4 +1,3 @@
import { rawDataToString } from "openclaw/plugin-sdk/browser-node-runtime";
import { formatInboundFromLabel as formatInboundFromLabelShared } from "openclaw/plugin-sdk/channel-inbound";
import { createDedupeCache, type OpenClawConfig } from "openclaw/plugin-sdk/core";
import { resolveThreadSessionKeys as resolveThreadSessionKeysShared } from "openclaw/plugin-sdk/routing";
@@ -6,6 +5,7 @@ import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalString,
} from "openclaw/plugin-sdk/text-runtime";
import { rawDataToString } from "openclaw/plugin-sdk/webhook-ingress";
export { createDedupeCache, rawDataToString };

View File

@@ -6,7 +6,7 @@
*/
import type { IncomingMessage, ServerResponse } from "node:http";
import { safeEqualSecret } from "openclaw/plugin-sdk/browser-security-runtime";
import { safeEqualSecret } from "openclaw/plugin-sdk/security-runtime";
import { isPrivateNetworkOptInEnabled } from "openclaw/plugin-sdk/ssrf-runtime";
import type { ResolvedMattermostAccount } from "../mattermost/accounts.js";
import { getMattermostRuntime } from "../runtime.js";

View File

@@ -10,7 +10,7 @@ const gatewayRpcMock = vi.hoisted(() => {
};
});
vi.mock("openclaw/plugin-sdk/browser-node-runtime", () => ({
vi.mock("openclaw/plugin-sdk/gateway-runtime", () => ({
callGatewayFromCli: gatewayRpcMock.callGatewayFromCli,
}));

View File

@@ -1,5 +1,5 @@
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
import { callGatewayFromCli } from "openclaw/plugin-sdk/browser-node-runtime";
import { callGatewayFromCli } from "openclaw/plugin-sdk/gateway-runtime";
import { formatQaGatewayLogsForError } from "./gateway-log-redaction.js";
type QaGatewayRpcRequestOptions = {

View File

@@ -1,7 +1,7 @@
export type { Command } from "commander";
export type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
export { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
export { callGatewayFromCli } from "openclaw/plugin-sdk/browser-node-runtime";
export { callGatewayFromCli } from "openclaw/plugin-sdk/gateway-runtime";
export type { PluginRuntime } from "openclaw/plugin-sdk/runtime-store";
export { defaultQaRuntimeModelForMode } from "./model-selection.runtime.js";
export {

View File

@@ -2,10 +2,8 @@ import { randomUUID } from "node:crypto";
import fs from "node:fs/promises";
import path from "node:path";
import { setTimeout as sleep } from "node:timers/promises";
import {
formatMemoryDreamingDay,
resolveSessionTranscriptsDirForAgent,
} from "openclaw/plugin-sdk/memory-core";
import { resolveSessionTranscriptsDirForAgent } from "openclaw/plugin-sdk/memory-host-core";
import { formatMemoryDreamingDay } from "openclaw/plugin-sdk/memory-host-status";
import { buildAgentSessionKey } from "openclaw/plugin-sdk/routing";
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
import {

View File

@@ -2,7 +2,7 @@
* Security module: token validation, rate limiting, input sanitization, user allowlist.
*/
import { safeEqualSecret } from "openclaw/plugin-sdk/browser-security-runtime";
import { safeEqualSecret } from "openclaw/plugin-sdk/security-runtime";
import {
createFixedWindowRateLimiter,
type FixedWindowRateLimiter,

View File

@@ -2,11 +2,11 @@ import { createServer } from "node:http";
import type { IncomingMessage } from "node:http";
import net from "node:net";
import * as grammy from "grammy";
import { safeEqualSecret } from "openclaw/plugin-sdk/browser-security-runtime";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
import { isDiagnosticsEnabled } from "openclaw/plugin-sdk/diagnostic-runtime";
import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env";
import { defaultRuntime } from "openclaw/plugin-sdk/runtime-env";
import { safeEqualSecret } from "openclaw/plugin-sdk/security-runtime";
import { formatErrorMessage } from "openclaw/plugin-sdk/ssrf-runtime";
import {
logWebhookError,

View File

@@ -14,4 +14,4 @@ export {
type LookupFn,
type SsrFPolicy,
} from "openclaw/plugin-sdk/ssrf-runtime";
export { SsrFBlockedError } from "openclaw/plugin-sdk/browser-security-runtime";
export { SsrFBlockedError } from "openclaw/plugin-sdk/ssrf-runtime";

View File

@@ -1,4 +1,4 @@
import { SsrFBlockedError } from "openclaw/plugin-sdk/browser-security-runtime";
import { SsrFBlockedError } from "openclaw/plugin-sdk/ssrf-runtime";
import type { LookupFn } from "openclaw/plugin-sdk/ssrf-runtime";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { authenticate } from "./auth.js";

View File

@@ -1,5 +1,5 @@
import crypto from "node:crypto";
import { safeEqualSecret } from "openclaw/plugin-sdk/browser-security-runtime";
import { safeEqualSecret } from "openclaw/plugin-sdk/security-runtime";
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
import type { TwilioConfig } from "../config.js";
import { getHeader } from "../http-headers.js";

View File

@@ -1,6 +1,6 @@
import crypto from "node:crypto";
import { safeEqualSecret } from "openclaw/plugin-sdk/browser-security-runtime";
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
import { safeEqualSecret } from "openclaw/plugin-sdk/security-runtime";
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
import { getHeader } from "./http-headers.js";
import type { WebhookContext } from "./types.js";

View File

@@ -1,5 +1,5 @@
import type { IncomingMessage, ServerResponse } from "node:http";
import { safeEqualSecret } from "openclaw/plugin-sdk/browser-security-runtime";
import { safeEqualSecret } from "openclaw/plugin-sdk/security-runtime";
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
import { z } from "zod";
import type { PluginRuntime } from "../api.js";

View File

@@ -1,6 +1,6 @@
import type { IncomingMessage, ServerResponse } from "node:http";
import { safeEqualSecret } from "openclaw/plugin-sdk/browser-security-runtime";
import { createClaimableDedupe } from "openclaw/plugin-sdk/persistent-dedupe";
import { safeEqualSecret } from "openclaw/plugin-sdk/security-runtime";
import type { ResolvedZaloAccount } from "./accounts.js";
import type { ZaloFetch, ZaloUpdate } from "./api.js";
import type { ZaloRuntimeEnv } from "./monitor.types.js";

View File

@@ -58,4 +58,4 @@ export {
sendPayloadWithChunkedTextAndMedia,
type OutboundReplyPayload,
} from "openclaw/plugin-sdk/reply-payload";
export { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/browser-security-runtime";
export { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/temp-path";

View File

@@ -1,6 +1,6 @@
import fsp from "node:fs/promises";
import path from "node:path";
import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/browser-security-runtime";
import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/temp-path";
export async function writeQrDataUrlToTempFile(
qrDataUrl: string,

View File

@@ -1,10 +1,13 @@
// Public gateway/client helpers for plugins that talk to the host gateway surface.
export * from "../gateway/channel-status-patches.js";
export { addGatewayClientOptions, callGatewayFromCli } from "../cli/gateway-rpc.js";
export type { GatewayRpcOpts } from "../cli/gateway-rpc.js";
export { GatewayClient } from "../gateway/client.js";
export {
createOperatorApprovalsGatewayClient,
withOperatorApprovalsGatewayClient,
} from "../gateway/operator-approvals-client.js";
export { ErrorCodes, errorShape } from "../gateway/protocol/index.js";
export type { EventFrame } from "../gateway/protocol/index.js";
export type { GatewayRequestHandlerOptions } from "../gateway/server-methods/types.js";

View File

@@ -9,3 +9,4 @@ export * from "../security/context-visibility.js";
export * from "../security/dm-policy-shared.js";
export * from "../security/external-content.js";
export * from "../security/safe-regex.js";
export { safeEqualSecret } from "../security/secret-equal.js";

View File

@@ -4,6 +4,7 @@
export {
closeDispatcher,
createPinnedDispatcher,
SsrFBlockedError,
isBlockedHostnameOrIp,
resolvePinnedHostname,
resolvePinnedHostnameWithPolicy,

View File

@@ -43,5 +43,6 @@ export { normalizeWebhookPath, resolveWebhookPath } from "./webhook-path.js";
export { resolveRequestClientIp } from "../gateway/net.js";
export { createAuthRateLimiter } from "../gateway/auth-rate-limit.js";
export type { AuthRateLimiter, RateLimitConfig } from "../gateway/auth-rate-limit.js";
export { rawDataToString } from "../infra/ws.js";
export { normalizePluginHttpPath } from "../plugins/http-path.js";
export { DEFAULT_WEBHOOK_MAX_BODY_BYTES } from "../infra/http-body.js";

View File

@@ -86,6 +86,12 @@ function collectPluginOwnedSdkEntrypoints(): string[] {
.toSorted();
}
function resolvePluginOwnerFromEntrypoint(entrypoint: string): string | undefined {
return collectBundledPluginIds().find(
(pluginId) => entrypoint === pluginId || entrypoint.startsWith(`${pluginId}-`),
);
}
function collectClassificationOverlaps(classifications: Record<string, readonly string[]>) {
const seen = new Map<string, string[]>();
for (const [classification, entrypoints] of Object.entries(classifications)) {
@@ -224,6 +230,47 @@ function collectExtensionCoreImportLeaks(): Array<{ file: string; specifier: str
return leaks;
}
function collectCrossOwnerReservedSdkImports(): Array<{
file: string;
specifier: string;
owner?: string;
}> {
const leaks: Array<{ file: string; specifier: string; owner?: string }> = [];
const reserved = new Set<string>(reservedBundledPluginSdkEntrypoints);
const importPattern =
/\b(?:import|export)\b[\s\S]*?\bfrom\s*["']openclaw\/plugin-sdk\/([a-z0-9][a-z0-9-]*)["']/g;
for (const file of collectExtensionFiles(resolve(REPO_ROOT, "extensions"))) {
const repoRelativePath = relative(REPO_ROOT, file).replaceAll("\\", "/");
if (
/(?:^|\/)(?:__tests__|tests|test-support)(?:\/|$)/.test(repoRelativePath) ||
/(?:^|\/)test-support\.[cm]?tsx?$/.test(repoRelativePath) ||
/\.test-support\.[cm]?tsx?$/.test(repoRelativePath) ||
/\.test\.[cm]?tsx?$/.test(repoRelativePath)
) {
continue;
}
const pluginId = repoRelativePath.split("/")[1];
const source = readFileSync(file, "utf8");
for (const match of source.matchAll(importPattern)) {
const subpath = match[1];
if (!subpath || !reserved.has(subpath)) {
continue;
}
const owner = resolvePluginOwnerFromEntrypoint(subpath);
if (owner === pluginId) {
continue;
}
leaks.push({
file: repoRelativePath,
specifier: `openclaw/plugin-sdk/${subpath}`,
owner,
});
}
}
return leaks;
}
describe("plugin-sdk package contract guardrails", () => {
it("keeps plugin-sdk entrypoint metadata unique", () => {
const counts = new Map<string, number>();
@@ -350,6 +397,10 @@ describe("plugin-sdk package contract guardrails", () => {
expect(collectExtensionCoreImportLeaks()).toEqual([]);
});
it("keeps reserved SDK compatibility subpaths inside their owning bundled plugins", () => {
expect(collectCrossOwnerReservedSdkImports()).toEqual([]);
});
it("keeps generic core poll helpers free of plugin owner names", () => {
expect(collectGenericCoreOwnerNameLeaks()).toEqual([]);
});