perf: narrow plugin SDK import surfaces

This commit is contained in:
Peter Steinberger
2026-04-17 16:04:44 +01:00
parent af954a81d1
commit 418056f7a0
28 changed files with 414 additions and 190 deletions

View File

@@ -1,2 +1,2 @@
9683f324fae8f455f2b64d7e152a77009941e4c7558521bca2510d8bcf573af9 plugin-sdk-api-baseline.json
097bf226e4e857e9296d0851852a2963c6263d176c4c470452d9a8efd36988e5 plugin-sdk-api-baseline.jsonl
e3df4c13b4dcdc07809775c56eed15c3ab924db191a08fb5a7b48d6f73001966 plugin-sdk-api-baseline.json
2bb30ad45d5b382e92fd6b8a240a47f7679c59f9b524e54420879fadc28264b8 plugin-sdk-api-baseline.jsonl

View File

@@ -185,7 +185,9 @@ Keep inbound mention handling split in two layers:
- plugin-owned evidence gathering
- shared policy evaluation
Use `openclaw/plugin-sdk/channel-inbound` for the shared layer.
Use `openclaw/plugin-sdk/channel-mention-gating` for mention-policy decisions.
Use `openclaw/plugin-sdk/channel-inbound` only when you need the broader inbound
helper barrel.
Good fit for plugin-local logic:
@@ -255,6 +257,11 @@ bundled channel plugins that already depend on runtime injection:
- `implicitMentionKindWhen`
- `resolveInboundMentionDecision`
If you only need `implicitMentionKindWhen` and
`resolveInboundMentionDecision`, import from
`openclaw/plugin-sdk/channel-mention-gating` to avoid loading unrelated inbound
runtime helpers.
The older `resolveMentionGating*` helpers remain on
`openclaw/plugin-sdk/channel-inbound` as compatibility exports only. New code
should use `resolveInboundMentionDecision({ facts, policy })`.

View File

@@ -88,6 +88,7 @@ explicitly promotes one as public.
| `plugin-sdk/channel-config-helpers` | `createHybridChannelConfigAdapter` |
| `plugin-sdk/channel-config-schema` | Channel config schema types |
| `plugin-sdk/telegram-command-config` | Telegram custom-command normalization/validation helpers with bundled-contract fallback |
| `plugin-sdk/command-gating` | Narrow command authorization gate helpers |
| `plugin-sdk/channel-policy` | `resolveChannelGroupRequireMention` |
| `plugin-sdk/channel-lifecycle` | `createAccountStatusSink` |
| `plugin-sdk/inbound-envelope` | Shared inbound route + envelope builder helpers |
@@ -95,6 +96,7 @@ explicitly promotes one as public.
| `plugin-sdk/messaging-targets` | Target parsing/matching helpers |
| `plugin-sdk/outbound-media` | Shared outbound media loading helpers |
| `plugin-sdk/outbound-runtime` | Outbound identity/send delegate helpers |
| `plugin-sdk/poll-runtime` | Narrow poll normalization helpers |
| `plugin-sdk/thread-bindings-runtime` | Thread-binding lifecycle and adapter helpers |
| `plugin-sdk/agent-media-payload` | Legacy agent media payload builder |
| `plugin-sdk/conversation-runtime` | Conversation/thread binding, pairing, and configured-binding helpers |
@@ -108,7 +110,10 @@ explicitly promotes one as public.
| `plugin-sdk/group-access` | Shared group-access decision helpers |
| `plugin-sdk/direct-dm` | Shared direct-DM auth/guard helpers |
| `plugin-sdk/interactive-runtime` | Interactive reply payload normalization/reduction helpers |
| `plugin-sdk/channel-inbound` | Inbound debounce, mention matching, mention-policy helpers, and envelope helpers |
| `plugin-sdk/channel-inbound` | Compatibility barrel for inbound debounce, mention matching, mention-policy helpers, and envelope helpers |
| `plugin-sdk/channel-mention-gating` | Narrow mention-policy helpers without the broader inbound runtime surface |
| `plugin-sdk/channel-location` | Channel location context and formatting helpers |
| `plugin-sdk/channel-logging` | Channel logging helpers for inbound drops and typing/ack failures |
| `plugin-sdk/channel-send-result` | Reply result types |
| `plugin-sdk/channel-actions` | `createMessageToolButtonsSchema`, `createMessageToolCardSchema` |
| `plugin-sdk/channel-targets` | Target parsing/matching helpers |
@@ -166,6 +171,7 @@ explicitly promotes one as public.
| `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/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/secret-input` | Secret input parsing helpers |
| `plugin-sdk/webhook-ingress` | Webhook request/target helpers |
@@ -187,6 +193,7 @@ explicitly promotes one as public.
| `plugin-sdk/gateway-runtime` | Gateway client and channel-status patch helpers |
| `plugin-sdk/config-runtime` | Config load/write 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 |
| `plugin-sdk/approval-runtime` | Exec/plugin approval helpers, approval-capability builders, auth/profile helpers, native routing/runtime helpers |
| `plugin-sdk/reply-runtime` | Shared inbound/reply runtime helpers, chunking, dispatch, heartbeat, reply planner |
| `plugin-sdk/reply-dispatch-runtime` | Narrow reply dispatch/finalize helpers |
@@ -211,6 +218,7 @@ explicitly promotes one as public.
| `plugin-sdk/file-lock` | Re-entrant file-lock helpers |
| `plugin-sdk/persistent-dedupe` | Disk-backed dedupe cache helpers |
| `plugin-sdk/acp-runtime` | ACP runtime/session and reply-dispatch helpers |
| `plugin-sdk/acp-binding-resolve-runtime` | Read-only ACP binding resolution without lifecycle startup imports |
| `plugin-sdk/agent-config-primitives` | Narrow agent runtime config-schema primitives |
| `plugin-sdk/boolean-param` | Loose boolean param reader |
| `plugin-sdk/dangerous-name-runtime` | Dangerous-name matching resolution helpers |
@@ -226,6 +234,12 @@ explicitly promotes one as public.
| `plugin-sdk/diagnostic-runtime` | Diagnostic flag and event helpers |
| `plugin-sdk/error-runtime` | Error graph, formatting, shared error classification helpers, `isApprovalNotFoundError` |
| `plugin-sdk/fetch-runtime` | Wrapped fetch, proxy, and pinned lookup helpers |
| `plugin-sdk/runtime-fetch` | Dispatcher-aware runtime fetch without proxy/guarded-fetch imports |
| `plugin-sdk/response-limit-runtime` | Bounded response-body reader without the broad media runtime surface |
| `plugin-sdk/session-binding-runtime` | Current conversation binding state without configured binding routing or pairing stores |
| `plugin-sdk/session-store-runtime` | Session-store read helpers without broad config writes/maintenance imports |
| `plugin-sdk/context-visibility-runtime` | Context visibility resolution and supplemental context filtering without broad config/security imports |
| `plugin-sdk/string-coerce-runtime` | Narrow primitive record/string coercion and normalization helpers without markdown/logging imports |
| `plugin-sdk/host-runtime` | Hostname and SCP host normalization helpers |
| `plugin-sdk/retry-runtime` | Retry config and retry runner helpers |
| `plugin-sdk/agent-runtime` | Agent dir/identity/workspace helpers |

View File

@@ -228,6 +228,10 @@
"types": "./dist/plugin-sdk/outbound-runtime.d.ts",
"default": "./dist/plugin-sdk/outbound-runtime.js"
},
"./plugin-sdk/poll-runtime": {
"types": "./dist/plugin-sdk/poll-runtime.d.ts",
"default": "./dist/plugin-sdk/poll-runtime.js"
},
"./plugin-sdk/infra-runtime": {
"types": "./dist/plugin-sdk/infra-runtime.d.ts",
"default": "./dist/plugin-sdk/infra-runtime.js"
@@ -320,6 +324,10 @@
"types": "./dist/plugin-sdk/secret-ref-runtime.d.ts",
"default": "./dist/plugin-sdk/secret-ref-runtime.js"
},
"./plugin-sdk/secret-file-runtime": {
"types": "./dist/plugin-sdk/secret-file-runtime.d.ts",
"default": "./dist/plugin-sdk/secret-file-runtime.js"
},
"./plugin-sdk/security-runtime": {
"types": "./dist/plugin-sdk/security-runtime.d.ts",
"default": "./dist/plugin-sdk/security-runtime.js"
@@ -372,6 +380,10 @@
"types": "./dist/plugin-sdk/acp-binding-runtime.d.ts",
"default": "./dist/plugin-sdk/acp-binding-runtime.js"
},
"./plugin-sdk/acp-binding-resolve-runtime": {
"types": "./dist/plugin-sdk/acp-binding-resolve-runtime.d.ts",
"default": "./dist/plugin-sdk/acp-binding-resolve-runtime.js"
},
"./plugin-sdk/lazy-runtime": {
"types": "./dist/plugin-sdk/lazy-runtime.d.ts",
"default": "./dist/plugin-sdk/lazy-runtime.js"
@@ -484,6 +496,10 @@
"types": "./dist/plugin-sdk/command-auth-native.d.ts",
"default": "./dist/plugin-sdk/command-auth-native.js"
},
"./plugin-sdk/command-gating": {
"types": "./dist/plugin-sdk/command-gating.d.ts",
"default": "./dist/plugin-sdk/command-gating.js"
},
"./plugin-sdk/command-status": {
"types": "./dist/plugin-sdk/command-status.d.ts",
"default": "./dist/plugin-sdk/command-status.js"
@@ -512,6 +528,14 @@
"types": "./dist/plugin-sdk/direct-dm.d.ts",
"default": "./dist/plugin-sdk/direct-dm.js"
},
"./plugin-sdk/direct-dm-access": {
"types": "./dist/plugin-sdk/direct-dm-access.d.ts",
"default": "./dist/plugin-sdk/direct-dm-access.js"
},
"./plugin-sdk/direct-dm-guard-policy": {
"types": "./dist/plugin-sdk/direct-dm-guard-policy.d.ts",
"default": "./dist/plugin-sdk/direct-dm-guard-policy.js"
},
"./plugin-sdk/device-bootstrap": {
"types": "./dist/plugin-sdk/device-bootstrap.d.ts",
"default": "./dist/plugin-sdk/device-bootstrap.js"
@@ -584,6 +608,18 @@
"types": "./dist/plugin-sdk/channel-inbound-roots.d.ts",
"default": "./dist/plugin-sdk/channel-inbound-roots.js"
},
"./plugin-sdk/channel-logging": {
"types": "./dist/plugin-sdk/channel-logging.d.ts",
"default": "./dist/plugin-sdk/channel-logging.js"
},
"./plugin-sdk/channel-location": {
"types": "./dist/plugin-sdk/channel-location.d.ts",
"default": "./dist/plugin-sdk/channel-location.js"
},
"./plugin-sdk/channel-mention-gating": {
"types": "./dist/plugin-sdk/channel-mention-gating.d.ts",
"default": "./dist/plugin-sdk/channel-mention-gating.js"
},
"./plugin-sdk/channel-lifecycle": {
"types": "./dist/plugin-sdk/channel-lifecycle.d.ts",
"default": "./dist/plugin-sdk/channel-lifecycle.js"
@@ -604,6 +640,10 @@
"types": "./dist/plugin-sdk/channel-targets.d.ts",
"default": "./dist/plugin-sdk/channel-targets.js"
},
"./plugin-sdk/context-visibility-runtime": {
"types": "./dist/plugin-sdk/context-visibility-runtime.d.ts",
"default": "./dist/plugin-sdk/context-visibility-runtime.js"
},
"./plugin-sdk/feishu": {
"types": "./dist/plugin-sdk/feishu.d.ts",
"default": "./dist/plugin-sdk/feishu.js"
@@ -624,6 +664,30 @@
"types": "./dist/plugin-sdk/fetch-runtime.d.ts",
"default": "./dist/plugin-sdk/fetch-runtime.js"
},
"./plugin-sdk/runtime-fetch": {
"types": "./dist/plugin-sdk/runtime-fetch.d.ts",
"default": "./dist/plugin-sdk/runtime-fetch.js"
},
"./plugin-sdk/response-limit-runtime": {
"types": "./dist/plugin-sdk/response-limit-runtime.d.ts",
"default": "./dist/plugin-sdk/response-limit-runtime.js"
},
"./plugin-sdk/session-binding-runtime": {
"types": "./dist/plugin-sdk/session-binding-runtime.d.ts",
"default": "./dist/plugin-sdk/session-binding-runtime.js"
},
"./plugin-sdk/session-store-runtime": {
"types": "./dist/plugin-sdk/session-store-runtime.d.ts",
"default": "./dist/plugin-sdk/session-store-runtime.js"
},
"./plugin-sdk/ssrf-dispatcher": {
"types": "./dist/plugin-sdk/ssrf-dispatcher.d.ts",
"default": "./dist/plugin-sdk/ssrf-dispatcher.js"
},
"./plugin-sdk/string-coerce-runtime": {
"types": "./dist/plugin-sdk/string-coerce-runtime.d.ts",
"default": "./dist/plugin-sdk/string-coerce-runtime.js"
},
"./plugin-sdk/group-access": {
"types": "./dist/plugin-sdk/group-access.d.ts",
"default": "./dist/plugin-sdk/group-access.js"
@@ -1032,10 +1096,6 @@
"types": "./dist/plugin-sdk/speech.d.ts",
"default": "./dist/plugin-sdk/speech.js"
},
"./plugin-sdk/session-store-runtime": {
"types": "./dist/plugin-sdk/session-store-runtime.d.ts",
"default": "./dist/plugin-sdk/session-store-runtime.js"
},
"./plugin-sdk/string-normalization-runtime": {
"types": "./dist/plugin-sdk/string-normalization-runtime.d.ts",
"default": "./dist/plugin-sdk/string-normalization-runtime.js"
@@ -1052,6 +1112,10 @@
"types": "./dist/plugin-sdk/telegram-command-config.d.ts",
"default": "./dist/plugin-sdk/telegram-command-config.js"
},
"./plugin-sdk/text-autolink-runtime": {
"types": "./dist/plugin-sdk/text-autolink-runtime.d.ts",
"default": "./dist/plugin-sdk/text-autolink-runtime.js"
},
"./plugin-sdk/thread-ownership": {
"types": "./dist/plugin-sdk/thread-ownership.d.ts",
"default": "./dist/plugin-sdk/thread-ownership.js"

View File

@@ -43,6 +43,7 @@
"interactive-runtime",
"outbound-media",
"outbound-runtime",
"poll-runtime",
"infra-runtime",
"runtime-config-snapshot",
"runtime-group-policy",
@@ -66,6 +67,7 @@
"channel-secret-runtime",
"channel-secret-tts-runtime",
"secret-ref-runtime",
"secret-file-runtime",
"security-runtime",
"gateway-runtime",
"github-copilot-login",
@@ -79,6 +81,7 @@
"windows-spawn",
"acp-runtime",
"acp-binding-runtime",
"acp-binding-resolve-runtime",
"lazy-runtime",
"testing",
"temp-path",
@@ -107,6 +110,7 @@
"dangerous-name-runtime",
"command-auth",
"command-auth-native",
"command-gating",
"command-status",
"command-status-runtime",
"command-detection",
@@ -114,6 +118,8 @@
"collection-runtime",
"compat",
"direct-dm",
"direct-dm-access",
"direct-dm-guard-policy",
"device-bootstrap",
"diagnostic-runtime",
"diagnostics-otel",
@@ -132,16 +138,26 @@
"channel-feedback",
"channel-inbound",
"channel-inbound-roots",
"channel-logging",
"channel-location",
"channel-mention-gating",
"channel-lifecycle",
"channel-pairing",
"channel-policy",
"channel-send-result",
"channel-targets",
"context-visibility-runtime",
"feishu",
"feishu-conversation",
"feishu-setup",
"file-lock",
"fetch-runtime",
"runtime-fetch",
"response-limit-runtime",
"session-binding-runtime",
"session-store-runtime",
"ssrf-dispatcher",
"string-coerce-runtime",
"group-access",
"global-singleton",
"directory-runtime",
@@ -244,11 +260,11 @@
"channel-status",
"status-helpers",
"speech",
"session-store-runtime",
"string-normalization-runtime",
"state-paths",
"target-resolver-runtime",
"telegram-command-config",
"text-autolink-runtime",
"thread-ownership",
"tlon",
"tool-payload",

View File

@@ -1,4 +1 @@
[
"qa-lab",
"qa-runtime"
]
["qa-lab", "qa-runtime"]

View File

@@ -0,0 +1,2 @@
// Read-only ACP binding resolution without the lifecycle runtime import graph.
export { resolveConfiguredAcpBindingRecord } from "../acp/persistent-bindings.resolve.js";

View File

@@ -0,0 +1,2 @@
export type { LocationSource, NormalizedLocation } from "../channels/location.js";
export { formatLocationText, toLocationContext } from "../channels/location.js";

View File

@@ -0,0 +1,2 @@
export type { LogFn } from "../channels/logging.js";
export { logAckFailure, logInboundDrop, logTypingFailure } from "../channels/logging.js";

View File

@@ -0,0 +1,21 @@
export type {
InboundImplicitMentionKind,
InboundMentionDecision,
InboundMentionFacts,
InboundMentionPolicy,
MentionGateParams,
MentionGateResult,
MentionGateWithBypassParams,
MentionGateWithBypassResult,
ResolveInboundMentionDecisionFlatParams,
ResolveInboundMentionDecisionNestedParams,
ResolveInboundMentionDecisionParams,
} from "../channels/mention-gating.js";
export {
implicitMentionKindWhen,
resolveInboundMentionDecision,
// @deprecated Prefer `resolveInboundMentionDecision({ facts, policy })`.
resolveMentionGating,
// @deprecated Prefer `resolveInboundMentionDecision({ facts, policy })`.
resolveMentionGatingWithBypass,
} from "../channels/mention-gating.js";

View File

@@ -16,6 +16,7 @@ export {
} from "../channels/plugins/setup-helpers.js";
export { buildChannelConfigSchema } from "../channels/plugins/config-schema.js";
export {
clearAccountEntryFields,
deleteAccountFromConfigSection,
setAccountEnabledInConfigSection,
} from "../channels/plugins/config-helpers.js";

View File

@@ -0,0 +1,9 @@
export type {
CommandAuthorizer,
CommandGatingModeWhenAccessGroupsOff,
} from "../channels/command-gating.js";
export {
resolveCommandAuthorizedFromAuthorizers,
resolveControlCommandGate,
resolveDualTextControlCommandGate,
} from "../channels/command-gating.js";

View File

@@ -0,0 +1,14 @@
// Narrow context visibility helpers without broad config-runtime imports.
export {
resolveChannelContextVisibilityMode,
resolveDefaultContextVisibility,
} from "../config/context-visibility.js";
export {
evaluateSupplementalContextVisibility,
filterSupplementalContextItems,
shouldIncludeSupplementalContext,
type ContextVisibilityDecision,
type ContextVisibilityDecisionReason,
type ContextVisibilityKind,
} from "../security/context-visibility.js";

View File

@@ -0,0 +1,140 @@
import type { ChannelId } from "../channels/plugins/types.public.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import {
readStoreAllowFromForDmPolicy,
resolveDmGroupAccessWithLists,
type DmGroupAccessReasonCode,
} from "../security/dm-policy-shared.js";
export type DirectDmCommandAuthorizationRuntime = {
shouldComputeCommandAuthorized: (rawBody: string, cfg: OpenClawConfig) => boolean;
resolveCommandAuthorizedFromAuthorizers: (params: {
useAccessGroups: boolean;
authorizers: Array<{ configured: boolean; allowed: boolean }>;
modeWhenAccessGroupsOff?: "allow" | "deny" | "configured";
}) => boolean;
};
export type ResolvedInboundDirectDmAccess = {
access: {
decision: "allow" | "block" | "pairing";
reasonCode: DmGroupAccessReasonCode;
reason: string;
effectiveAllowFrom: string[];
};
shouldComputeAuth: boolean;
senderAllowedForCommands: boolean;
commandAuthorized: boolean | undefined;
};
/** Resolve direct-DM policy, effective allowlists, and optional command auth in one place. */
export async function resolveInboundDirectDmAccessWithRuntime(params: {
cfg: OpenClawConfig;
channel: ChannelId;
accountId: string;
dmPolicy?: string | null;
allowFrom?: Array<string | number> | null;
senderId: string;
rawBody: string;
isSenderAllowed: (senderId: string, allowFrom: string[]) => boolean;
runtime: DirectDmCommandAuthorizationRuntime;
modeWhenAccessGroupsOff?: "allow" | "deny" | "configured";
readStoreAllowFrom?: (provider: ChannelId, accountId: string) => Promise<string[]>;
}): Promise<ResolvedInboundDirectDmAccess> {
const dmPolicy = params.dmPolicy ?? "pairing";
const storeAllowFrom =
dmPolicy === "pairing"
? await readStoreAllowFromForDmPolicy({
provider: params.channel,
accountId: params.accountId,
dmPolicy,
readStore: params.readStoreAllowFrom,
})
: [];
const access = resolveDmGroupAccessWithLists({
isGroup: false,
dmPolicy,
allowFrom: params.allowFrom,
storeAllowFrom,
groupAllowFromFallbackToAllowFrom: false,
isSenderAllowed: (allowEntries) => params.isSenderAllowed(params.senderId, allowEntries),
});
const shouldComputeAuth = params.runtime.shouldComputeCommandAuthorized(
params.rawBody,
params.cfg,
);
const senderAllowedForCommands = params.isSenderAllowed(
params.senderId,
access.effectiveAllowFrom,
);
const commandAuthorized = shouldComputeAuth
? dmPolicy === "open"
? true
: params.runtime.resolveCommandAuthorizedFromAuthorizers({
useAccessGroups: params.cfg.commands?.useAccessGroups !== false,
authorizers: [
{
configured: access.effectiveAllowFrom.length > 0,
allowed: senderAllowedForCommands,
},
],
modeWhenAccessGroupsOff: params.modeWhenAccessGroupsOff,
})
: undefined;
return {
access: {
decision: access.decision,
reasonCode: access.reasonCode,
reason: access.reason,
effectiveAllowFrom: access.effectiveAllowFrom,
},
shouldComputeAuth,
senderAllowedForCommands,
commandAuthorized,
};
}
/** Convert resolved DM policy into a pre-crypto allow/block/pairing callback. */
export function createPreCryptoDirectDmAuthorizer(params: {
resolveAccess: (
senderId: string,
) => Promise<Pick<ResolvedInboundDirectDmAccess, "access"> | ResolvedInboundDirectDmAccess>;
issuePairingChallenge?: (params: {
senderId: string;
reply: (text: string) => Promise<void>;
}) => Promise<void>;
onBlocked?: (params: {
senderId: string;
reason: string;
reasonCode: DmGroupAccessReasonCode;
}) => void;
}) {
return async (input: {
senderId: string;
reply: (text: string) => Promise<void>;
}): Promise<"allow" | "block" | "pairing"> => {
const resolved = await params.resolveAccess(input.senderId);
const access = "access" in resolved ? resolved.access : resolved;
if (access.decision === "allow") {
return "allow";
}
if (access.decision === "pairing") {
if (params.issuePairingChallenge) {
await params.issuePairingChallenge({
senderId: input.senderId,
reply: input.reply,
});
}
return "pairing";
}
params.onBlocked?.({
senderId: input.senderId,
reason: access.reason,
reasonCode: access.reasonCode,
});
return "block";
};
}

View File

@@ -0,0 +1,36 @@
export type DirectDmPreCryptoGuardPolicy = {
allowedKinds: readonly number[];
maxFutureSkewSec: number;
maxCiphertextBytes: number;
maxPlaintextBytes: number;
rateLimit: {
windowMs: number;
maxPerSenderPerWindow: number;
maxGlobalPerWindow: number;
maxTrackedSenderKeys: number;
};
};
export type DirectDmPreCryptoGuardPolicyOverrides = Partial<
Omit<DirectDmPreCryptoGuardPolicy, "rateLimit">
> & {
rateLimit?: Partial<DirectDmPreCryptoGuardPolicy["rateLimit"]>;
};
/** Shared policy object for DM-style pre-crypto guardrails. */
export function createDirectDmPreCryptoGuardPolicy(
overrides: DirectDmPreCryptoGuardPolicyOverrides = {},
): DirectDmPreCryptoGuardPolicy {
return {
allowedKinds: overrides.allowedKinds ?? [4],
maxFutureSkewSec: overrides.maxFutureSkewSec ?? 120,
maxCiphertextBytes: overrides.maxCiphertextBytes ?? 16 * 1024,
maxPlaintextBytes: overrides.maxPlaintextBytes ?? 8 * 1024,
rateLimit: {
windowMs: overrides.rateLimit?.windowMs ?? 60_000,
maxPerSenderPerWindow: overrides.rateLimit?.maxPerSenderPerWindow ?? 20,
maxGlobalPerWindow: overrides.rateLimit?.maxGlobalPerWindow ?? 200,
maxTrackedSenderKeys: overrides.rateLimit?.maxTrackedSenderKeys ?? 4096,
},
};
}

View File

@@ -1,185 +1,20 @@
import type { DispatchReplyWithBufferedBlockDispatcher } from "../auto-reply/reply/provider-dispatcher.types.js";
import type { FinalizedMsgContext } from "../auto-reply/templating.js";
import type { ChannelId } from "../channels/plugins/types.public.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import {
readStoreAllowFromForDmPolicy,
resolveDmGroupAccessWithLists,
type DmGroupAccessReasonCode,
} from "../security/dm-policy-shared.js";
import { resolveInboundRouteEnvelopeBuilderWithRuntime } from "./inbound-envelope.js";
import { recordInboundSessionAndDispatchReply } from "./inbound-reply-dispatch.js";
import type { OutboundReplyPayload } from "./reply-payload.js";
export type DirectDmCommandAuthorizationRuntime = {
shouldComputeCommandAuthorized: (rawBody: string, cfg: OpenClawConfig) => boolean;
resolveCommandAuthorizedFromAuthorizers: (params: {
useAccessGroups: boolean;
authorizers: Array<{ configured: boolean; allowed: boolean }>;
modeWhenAccessGroupsOff?: "allow" | "deny" | "configured";
}) => boolean;
};
export type ResolvedInboundDirectDmAccess = {
access: {
decision: "allow" | "block" | "pairing";
reasonCode: DmGroupAccessReasonCode;
reason: string;
effectiveAllowFrom: string[];
};
shouldComputeAuth: boolean;
senderAllowedForCommands: boolean;
commandAuthorized: boolean | undefined;
};
/** Resolve direct-DM policy, effective allowlists, and optional command auth in one place. */
export async function resolveInboundDirectDmAccessWithRuntime(params: {
cfg: OpenClawConfig;
channel: ChannelId;
accountId: string;
dmPolicy?: string | null;
allowFrom?: Array<string | number> | null;
senderId: string;
rawBody: string;
isSenderAllowed: (senderId: string, allowFrom: string[]) => boolean;
runtime: DirectDmCommandAuthorizationRuntime;
modeWhenAccessGroupsOff?: "allow" | "deny" | "configured";
readStoreAllowFrom?: (provider: ChannelId, accountId: string) => Promise<string[]>;
}): Promise<ResolvedInboundDirectDmAccess> {
const dmPolicy = params.dmPolicy ?? "pairing";
const storeAllowFrom =
dmPolicy === "pairing"
? await readStoreAllowFromForDmPolicy({
provider: params.channel,
accountId: params.accountId,
dmPolicy,
readStore: params.readStoreAllowFrom,
})
: [];
const access = resolveDmGroupAccessWithLists({
isGroup: false,
dmPolicy,
allowFrom: params.allowFrom,
storeAllowFrom,
groupAllowFromFallbackToAllowFrom: false,
isSenderAllowed: (allowEntries) => params.isSenderAllowed(params.senderId, allowEntries),
});
const shouldComputeAuth = params.runtime.shouldComputeCommandAuthorized(
params.rawBody,
params.cfg,
);
const senderAllowedForCommands = params.isSenderAllowed(
params.senderId,
access.effectiveAllowFrom,
);
const commandAuthorized = shouldComputeAuth
? dmPolicy === "open"
? true
: params.runtime.resolveCommandAuthorizedFromAuthorizers({
useAccessGroups: params.cfg.commands?.useAccessGroups !== false,
authorizers: [
{
configured: access.effectiveAllowFrom.length > 0,
allowed: senderAllowedForCommands,
},
],
modeWhenAccessGroupsOff: params.modeWhenAccessGroupsOff,
})
: undefined;
return {
access: {
decision: access.decision,
reasonCode: access.reasonCode,
reason: access.reason,
effectiveAllowFrom: access.effectiveAllowFrom,
},
shouldComputeAuth,
senderAllowedForCommands,
commandAuthorized,
};
}
/** Convert resolved DM policy into a pre-crypto allow/block/pairing callback. */
export function createPreCryptoDirectDmAuthorizer(params: {
resolveAccess: (
senderId: string,
) => Promise<Pick<ResolvedInboundDirectDmAccess, "access"> | ResolvedInboundDirectDmAccess>;
issuePairingChallenge?: (params: {
senderId: string;
reply: (text: string) => Promise<void>;
}) => Promise<void>;
onBlocked?: (params: {
senderId: string;
reason: string;
reasonCode: DmGroupAccessReasonCode;
}) => void;
}) {
return async (input: {
senderId: string;
reply: (text: string) => Promise<void>;
}): Promise<"allow" | "block" | "pairing"> => {
const resolved = await params.resolveAccess(input.senderId);
const access = "access" in resolved ? resolved.access : resolved;
if (access.decision === "allow") {
return "allow";
}
if (access.decision === "pairing") {
if (params.issuePairingChallenge) {
await params.issuePairingChallenge({
senderId: input.senderId,
reply: input.reply,
});
}
return "pairing";
}
params.onBlocked?.({
senderId: input.senderId,
reason: access.reason,
reasonCode: access.reasonCode,
});
return "block";
};
}
export type DirectDmPreCryptoGuardPolicy = {
allowedKinds: readonly number[];
maxFutureSkewSec: number;
maxCiphertextBytes: number;
maxPlaintextBytes: number;
rateLimit: {
windowMs: number;
maxPerSenderPerWindow: number;
maxGlobalPerWindow: number;
maxTrackedSenderKeys: number;
};
};
export type DirectDmPreCryptoGuardPolicyOverrides = Partial<
Omit<DirectDmPreCryptoGuardPolicy, "rateLimit">
> & {
rateLimit?: Partial<DirectDmPreCryptoGuardPolicy["rateLimit"]>;
};
/** Shared policy object for DM-style pre-crypto guardrails. */
export function createDirectDmPreCryptoGuardPolicy(
overrides: DirectDmPreCryptoGuardPolicyOverrides = {},
): DirectDmPreCryptoGuardPolicy {
return {
allowedKinds: overrides.allowedKinds ?? [4],
maxFutureSkewSec: overrides.maxFutureSkewSec ?? 120,
maxCiphertextBytes: overrides.maxCiphertextBytes ?? 16 * 1024,
maxPlaintextBytes: overrides.maxPlaintextBytes ?? 8 * 1024,
rateLimit: {
windowMs: overrides.rateLimit?.windowMs ?? 60_000,
maxPerSenderPerWindow: overrides.rateLimit?.maxPerSenderPerWindow ?? 20,
maxGlobalPerWindow: overrides.rateLimit?.maxGlobalPerWindow ?? 200,
maxTrackedSenderKeys: overrides.rateLimit?.maxTrackedSenderKeys ?? 4096,
},
};
}
export {
createPreCryptoDirectDmAuthorizer,
resolveInboundDirectDmAccessWithRuntime,
type DirectDmCommandAuthorizationRuntime,
type ResolvedInboundDirectDmAccess,
} from "./direct-dm-access.js";
export {
createDirectDmPreCryptoGuardPolicy,
type DirectDmPreCryptoGuardPolicy,
type DirectDmPreCryptoGuardPolicyOverrides,
} from "./direct-dm-guard-policy.js";
type DirectDmRoutePeer = {
kind: "direct";

View File

@@ -0,0 +1,6 @@
export type { NormalizedPollInput, PollInput } from "../polls.js";
export {
normalizePollDurationHours,
normalizePollInput,
resolvePollMaxSelections,
} from "../polls.js";

View File

@@ -0,0 +1,3 @@
// Narrow response-size reader for plugins that download bounded HTTP bodies.
export { readResponseWithLimit } from "../media/read-response-with-limit.js";

View File

@@ -0,0 +1,7 @@
// Narrow runtime fetch helpers for plugins that need dispatcher-aware fetch
// without importing the broad infra-runtime compatibility barrel.
export {
fetchWithRuntimeDispatcher,
type DispatcherAwareRequestInit,
} from "../infra/net/runtime-fetch.js";

View File

@@ -0,0 +1,7 @@
export {
DEFAULT_SECRET_FILE_MAX_BYTES,
loadSecretFileSync,
readSecretFileSync,
tryReadSecretFileSync,
} from "../infra/secret-file.js";
export type { SecretFileReadOptions, SecretFileReadResult } from "../infra/secret-file.js";

View File

@@ -2,6 +2,7 @@ import { z } from "zod";
import {
hasConfiguredSecretInput,
isSecretRef,
coerceSecretRef,
resolveSecretInputString,
normalizeResolvedSecretInputString,
normalizeSecretInputString,
@@ -16,6 +17,7 @@ export type {
} from "../config/types.secrets.js";
export {
buildSecretInputSchema,
coerceSecretRef,
hasConfiguredSecretInput,
isSecretRef,
resolveSecretInputString,

View File

@@ -0,0 +1,7 @@
// Narrow session-binding runtime surface for channels that only need current
// conversation binding state, not configured binding routing or pairing stores.
export {
getSessionBindingService,
type SessionBindingRecord,
type SessionBindingService,
} from "../infra/outbound/session-binding-service.js";

View File

@@ -1 +1,4 @@
export { readSessionUpdatedAt, resolveStorePath } from "../config/sessions.js";
// Narrow session-store read helpers for channel hot paths.
export { loadSessionStore } from "../config/sessions/store-load.js";
export { resolveSessionStoreEntry } from "../config/sessions/store-entry.js";

View File

@@ -0,0 +1,9 @@
// Narrow SSRF dispatcher helpers for plugins that pin DNS resolution before fetch.
export {
closeDispatcher,
createPinnedDispatcher,
resolvePinnedHostnameWithPolicy,
type PinnedDispatcherPolicy,
type SsrFPolicy,
} from "../infra/net/ssrf.js";

View File

@@ -1,3 +1,4 @@
// Public state/config path helpers for plugins that persist small caches.
export { resolveOAuthDir, resolveStateDir, STATE_DIR } from "../config/paths.js";
export { resolveRequiredHomeDir } from "../infra/home-dir.js";

View File

@@ -0,0 +1,15 @@
// Narrow primitive coercion helpers for plugins that do not need the full text-runtime barrel.
export {
hasNonEmptyString,
localeLowercasePreservingWhitespace,
lowercasePreservingWhitespace,
normalizeLowercaseStringOrEmpty,
normalizeNullableString,
normalizeOptionalLowercaseString,
normalizeOptionalString,
normalizeOptionalStringifiedId,
normalizeStringifiedOptionalString,
readStringValue,
} from "../shared/string-coerce.js";
export { isRecord } from "../utils.js";

View File

@@ -0,0 +1,2 @@
// Narrow text helper for renderers that only need file-ref autolink detection.
export { isAutoLinkedFileRef } from "../shared/text/auto-linked-file-ref.js";

View File

@@ -41,4 +41,6 @@ export {
} from "./webhook-targets.js";
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 { normalizePluginHttpPath } from "../plugins/http-path.js";