diff --git a/docs/.generated/plugin-sdk-api-baseline.sha256 b/docs/.generated/plugin-sdk-api-baseline.sha256 index 65d8a2a28cc..ba9e97784f0 100644 --- a/docs/.generated/plugin-sdk-api-baseline.sha256 +++ b/docs/.generated/plugin-sdk-api-baseline.sha256 @@ -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 diff --git a/docs/plugins/sdk-channel-plugins.md b/docs/plugins/sdk-channel-plugins.md index 803f38f177a..b799f3e1bb6 100644 --- a/docs/plugins/sdk-channel-plugins.md +++ b/docs/plugins/sdk-channel-plugins.md @@ -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 })`. diff --git a/docs/plugins/sdk-overview.md b/docs/plugins/sdk-overview.md index c1c39f073c1..58296bdb335 100644 --- a/docs/plugins/sdk-overview.md +++ b/docs/plugins/sdk-overview.md @@ -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 | diff --git a/package.json b/package.json index 1aa971c2358..cba1a2943bf 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/scripts/lib/plugin-sdk-entrypoints.json b/scripts/lib/plugin-sdk-entrypoints.json index b296f5b5214..c00ac7223f1 100644 --- a/scripts/lib/plugin-sdk-entrypoints.json +++ b/scripts/lib/plugin-sdk-entrypoints.json @@ -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", diff --git a/scripts/lib/plugin-sdk-private-local-only-subpaths.json b/scripts/lib/plugin-sdk-private-local-only-subpaths.json index ba8dd822c2c..6b4a7af24a7 100644 --- a/scripts/lib/plugin-sdk-private-local-only-subpaths.json +++ b/scripts/lib/plugin-sdk-private-local-only-subpaths.json @@ -1,4 +1 @@ -[ - "qa-lab", - "qa-runtime" -] +["qa-lab", "qa-runtime"] diff --git a/src/plugin-sdk/acp-binding-resolve-runtime.ts b/src/plugin-sdk/acp-binding-resolve-runtime.ts new file mode 100644 index 00000000000..b4d422532b8 --- /dev/null +++ b/src/plugin-sdk/acp-binding-resolve-runtime.ts @@ -0,0 +1,2 @@ +// Read-only ACP binding resolution without the lifecycle runtime import graph. +export { resolveConfiguredAcpBindingRecord } from "../acp/persistent-bindings.resolve.js"; diff --git a/src/plugin-sdk/channel-location.ts b/src/plugin-sdk/channel-location.ts new file mode 100644 index 00000000000..54aa82b3154 --- /dev/null +++ b/src/plugin-sdk/channel-location.ts @@ -0,0 +1,2 @@ +export type { LocationSource, NormalizedLocation } from "../channels/location.js"; +export { formatLocationText, toLocationContext } from "../channels/location.js"; diff --git a/src/plugin-sdk/channel-logging.ts b/src/plugin-sdk/channel-logging.ts new file mode 100644 index 00000000000..7efa4c10236 --- /dev/null +++ b/src/plugin-sdk/channel-logging.ts @@ -0,0 +1,2 @@ +export type { LogFn } from "../channels/logging.js"; +export { logAckFailure, logInboundDrop, logTypingFailure } from "../channels/logging.js"; diff --git a/src/plugin-sdk/channel-mention-gating.ts b/src/plugin-sdk/channel-mention-gating.ts new file mode 100644 index 00000000000..077854be96e --- /dev/null +++ b/src/plugin-sdk/channel-mention-gating.ts @@ -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"; diff --git a/src/plugin-sdk/channel-plugin-common.ts b/src/plugin-sdk/channel-plugin-common.ts index d51d0c028ca..7d7777cf4ed 100644 --- a/src/plugin-sdk/channel-plugin-common.ts +++ b/src/plugin-sdk/channel-plugin-common.ts @@ -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"; diff --git a/src/plugin-sdk/command-gating.ts b/src/plugin-sdk/command-gating.ts new file mode 100644 index 00000000000..6b9e0bcfa09 --- /dev/null +++ b/src/plugin-sdk/command-gating.ts @@ -0,0 +1,9 @@ +export type { + CommandAuthorizer, + CommandGatingModeWhenAccessGroupsOff, +} from "../channels/command-gating.js"; +export { + resolveCommandAuthorizedFromAuthorizers, + resolveControlCommandGate, + resolveDualTextControlCommandGate, +} from "../channels/command-gating.js"; diff --git a/src/plugin-sdk/context-visibility-runtime.ts b/src/plugin-sdk/context-visibility-runtime.ts new file mode 100644 index 00000000000..3f91711f685 --- /dev/null +++ b/src/plugin-sdk/context-visibility-runtime.ts @@ -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"; diff --git a/src/plugin-sdk/direct-dm-access.ts b/src/plugin-sdk/direct-dm-access.ts new file mode 100644 index 00000000000..4ec6f656354 --- /dev/null +++ b/src/plugin-sdk/direct-dm-access.ts @@ -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 | null; + senderId: string; + rawBody: string; + isSenderAllowed: (senderId: string, allowFrom: string[]) => boolean; + runtime: DirectDmCommandAuthorizationRuntime; + modeWhenAccessGroupsOff?: "allow" | "deny" | "configured"; + readStoreAllowFrom?: (provider: ChannelId, accountId: string) => Promise; +}): Promise { + 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 | ResolvedInboundDirectDmAccess>; + issuePairingChallenge?: (params: { + senderId: string; + reply: (text: string) => Promise; + }) => Promise; + onBlocked?: (params: { + senderId: string; + reason: string; + reasonCode: DmGroupAccessReasonCode; + }) => void; +}) { + return async (input: { + senderId: string; + reply: (text: string) => Promise; + }): 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"; + }; +} diff --git a/src/plugin-sdk/direct-dm-guard-policy.ts b/src/plugin-sdk/direct-dm-guard-policy.ts new file mode 100644 index 00000000000..3f816b49697 --- /dev/null +++ b/src/plugin-sdk/direct-dm-guard-policy.ts @@ -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 +> & { + rateLimit?: Partial; +}; + +/** 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, + }, + }; +} diff --git a/src/plugin-sdk/direct-dm.ts b/src/plugin-sdk/direct-dm.ts index f080c49fde2..071a76b72ee 100644 --- a/src/plugin-sdk/direct-dm.ts +++ b/src/plugin-sdk/direct-dm.ts @@ -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 | null; - senderId: string; - rawBody: string; - isSenderAllowed: (senderId: string, allowFrom: string[]) => boolean; - runtime: DirectDmCommandAuthorizationRuntime; - modeWhenAccessGroupsOff?: "allow" | "deny" | "configured"; - readStoreAllowFrom?: (provider: ChannelId, accountId: string) => Promise; -}): Promise { - 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 | ResolvedInboundDirectDmAccess>; - issuePairingChallenge?: (params: { - senderId: string; - reply: (text: string) => Promise; - }) => Promise; - onBlocked?: (params: { - senderId: string; - reason: string; - reasonCode: DmGroupAccessReasonCode; - }) => void; -}) { - return async (input: { - senderId: string; - reply: (text: string) => Promise; - }): 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 -> & { - rateLimit?: Partial; -}; - -/** 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"; diff --git a/src/plugin-sdk/poll-runtime.ts b/src/plugin-sdk/poll-runtime.ts new file mode 100644 index 00000000000..0b39804599d --- /dev/null +++ b/src/plugin-sdk/poll-runtime.ts @@ -0,0 +1,6 @@ +export type { NormalizedPollInput, PollInput } from "../polls.js"; +export { + normalizePollDurationHours, + normalizePollInput, + resolvePollMaxSelections, +} from "../polls.js"; diff --git a/src/plugin-sdk/response-limit-runtime.ts b/src/plugin-sdk/response-limit-runtime.ts new file mode 100644 index 00000000000..303a8d132c6 --- /dev/null +++ b/src/plugin-sdk/response-limit-runtime.ts @@ -0,0 +1,3 @@ +// Narrow response-size reader for plugins that download bounded HTTP bodies. + +export { readResponseWithLimit } from "../media/read-response-with-limit.js"; diff --git a/src/plugin-sdk/runtime-fetch.ts b/src/plugin-sdk/runtime-fetch.ts new file mode 100644 index 00000000000..0d1dc85a811 --- /dev/null +++ b/src/plugin-sdk/runtime-fetch.ts @@ -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"; diff --git a/src/plugin-sdk/secret-file-runtime.ts b/src/plugin-sdk/secret-file-runtime.ts new file mode 100644 index 00000000000..07eee264cec --- /dev/null +++ b/src/plugin-sdk/secret-file-runtime.ts @@ -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"; diff --git a/src/plugin-sdk/secret-input.ts b/src/plugin-sdk/secret-input.ts index 47e48520c43..49c31046668 100644 --- a/src/plugin-sdk/secret-input.ts +++ b/src/plugin-sdk/secret-input.ts @@ -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, diff --git a/src/plugin-sdk/session-binding-runtime.ts b/src/plugin-sdk/session-binding-runtime.ts new file mode 100644 index 00000000000..125a219aa32 --- /dev/null +++ b/src/plugin-sdk/session-binding-runtime.ts @@ -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"; diff --git a/src/plugin-sdk/session-store-runtime.ts b/src/plugin-sdk/session-store-runtime.ts index 4cf8f89707d..54ec8ee7448 100644 --- a/src/plugin-sdk/session-store-runtime.ts +++ b/src/plugin-sdk/session-store-runtime.ts @@ -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"; diff --git a/src/plugin-sdk/ssrf-dispatcher.ts b/src/plugin-sdk/ssrf-dispatcher.ts new file mode 100644 index 00000000000..c5ee9af0be8 --- /dev/null +++ b/src/plugin-sdk/ssrf-dispatcher.ts @@ -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"; diff --git a/src/plugin-sdk/state-paths.ts b/src/plugin-sdk/state-paths.ts index aeae39fa1f1..a2337a042f0 100644 --- a/src/plugin-sdk/state-paths.ts +++ b/src/plugin-sdk/state-paths.ts @@ -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"; diff --git a/src/plugin-sdk/string-coerce-runtime.ts b/src/plugin-sdk/string-coerce-runtime.ts new file mode 100644 index 00000000000..d7bd89b5eae --- /dev/null +++ b/src/plugin-sdk/string-coerce-runtime.ts @@ -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"; diff --git a/src/plugin-sdk/text-autolink-runtime.ts b/src/plugin-sdk/text-autolink-runtime.ts new file mode 100644 index 00000000000..7b96feb4620 --- /dev/null +++ b/src/plugin-sdk/text-autolink-runtime.ts @@ -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"; diff --git a/src/plugin-sdk/webhook-ingress.ts b/src/plugin-sdk/webhook-ingress.ts index 58b8a325344..12b46afbcaf 100644 --- a/src/plugin-sdk/webhook-ingress.ts +++ b/src/plugin-sdk/webhook-ingress.ts @@ -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";