mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-01 22:10:20 +00:00
refactor: tighten plugin sdk channel seams
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
/** Resolve an account by id, then fall back to the default account when the primary lacks credentials. */
|
||||
export function resolveAccountWithDefaultFallback<TAccount>(params: {
|
||||
accountId?: string | null;
|
||||
normalizeAccountId: (accountId?: string | null) => string;
|
||||
@@ -23,6 +24,7 @@ export function resolveAccountWithDefaultFallback<TAccount>(params: {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
/** List normalized configured account ids from a raw channel account record map. */
|
||||
export function listConfiguredAccountIds(params: {
|
||||
accounts: Record<string, unknown> | undefined;
|
||||
normalizeAccountId: (accountId: string) => string;
|
||||
|
||||
@@ -7,6 +7,7 @@ export type AgentMediaPayload = {
|
||||
MediaTypes?: string[];
|
||||
};
|
||||
|
||||
/** Convert outbound media descriptors into the legacy agent payload field layout. */
|
||||
export function buildAgentMediaPayload(
|
||||
mediaList: Array<{ path: string; contentType?: string | null }>,
|
||||
): AgentMediaPayload {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/** Lowercase and optionally strip prefixes from allowlist entries before sender comparisons. */
|
||||
export function formatAllowFromLowercase(params: {
|
||||
allowFrom: Array<string | number>;
|
||||
stripPrefixRe?: RegExp;
|
||||
@@ -9,6 +10,7 @@ export function formatAllowFromLowercase(params: {
|
||||
.map((entry) => entry.toLowerCase());
|
||||
}
|
||||
|
||||
/** Normalize allowlist entries through a channel-provided parser or canonicalizer. */
|
||||
export function formatNormalizedAllowFromEntries(params: {
|
||||
allowFrom: Array<string | number>;
|
||||
normalizeEntry: (entry: string) => string | undefined | null;
|
||||
@@ -20,6 +22,7 @@ export function formatNormalizedAllowFromEntries(params: {
|
||||
.filter((entry): entry is string => Boolean(entry));
|
||||
}
|
||||
|
||||
/** Check whether a sender id matches a simple normalized allowlist with wildcard support. */
|
||||
export function isNormalizedSenderAllowed(params: {
|
||||
senderId: string | number;
|
||||
allowFrom: Array<string | number>;
|
||||
@@ -45,6 +48,7 @@ type ParsedChatAllowTarget =
|
||||
| { kind: "chat_identifier"; chatIdentifier: string }
|
||||
| { kind: "handle"; handle: string };
|
||||
|
||||
/** Match chat-aware allowlist entries against sender, chat id, guid, or identifier fields. */
|
||||
export function isAllowedParsedChatSender<TParsed extends ParsedChatAllowTarget>(params: {
|
||||
allowFrom: Array<string | number>;
|
||||
sender: string;
|
||||
|
||||
@@ -183,6 +183,7 @@ function applyAccountScopedAllowlistConfigEdit(params: {
|
||||
};
|
||||
}
|
||||
|
||||
/** Build the default account-scoped allowlist editor used by channel plugins with config-backed lists. */
|
||||
export function buildAccountScopedAllowlistConfigEditor(params: {
|
||||
channelId: ChannelId;
|
||||
normalize: (params: {
|
||||
|
||||
@@ -6,6 +6,7 @@ export type BasicAllowlistResolutionEntry = {
|
||||
note?: string;
|
||||
};
|
||||
|
||||
/** Clone allowlist resolution entries into a plain serializable shape for UI and docs output. */
|
||||
export function mapBasicAllowlistResolutionEntries(
|
||||
entries: BasicAllowlistResolutionEntry[],
|
||||
): BasicAllowlistResolutionEntry[] {
|
||||
@@ -18,6 +19,7 @@ export function mapBasicAllowlistResolutionEntries(
|
||||
}));
|
||||
}
|
||||
|
||||
/** Map allowlist inputs sequentially so resolver side effects stay ordered and predictable. */
|
||||
export async function mapAllowlistResolutionInputs<T>(params: {
|
||||
inputs: string[];
|
||||
mapInput: (input: string) => Promise<T> | T;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/** Read loose boolean params from tool input that may arrive as booleans or "true"/"false" strings. */
|
||||
export function readBooleanParam(
|
||||
params: Record<string, unknown>,
|
||||
key: string,
|
||||
|
||||
@@ -10,16 +10,19 @@ import type { OpenClawConfig } from "../config/config.js";
|
||||
import { normalizeAccountId } from "../routing/session-key.js";
|
||||
import { normalizeStringEntries } from "../shared/string-normalization.js";
|
||||
|
||||
/** Coerce mixed allowlist config values into plain strings without trimming or deduping. */
|
||||
export function mapAllowFromEntries(
|
||||
allowFrom: Array<string | number> | null | undefined,
|
||||
): string[] {
|
||||
return (allowFrom ?? []).map((entry) => String(entry));
|
||||
}
|
||||
|
||||
/** Normalize user-facing allowlist entries the same way config and doctor flows expect. */
|
||||
export function formatTrimmedAllowFromEntries(allowFrom: Array<string | number>): string[] {
|
||||
return normalizeStringEntries(allowFrom);
|
||||
}
|
||||
|
||||
/** Collapse nullable config scalars into a trimmed optional string. */
|
||||
export function resolveOptionalConfigString(
|
||||
value: string | number | null | undefined,
|
||||
): string | undefined {
|
||||
@@ -30,6 +33,7 @@ export function resolveOptionalConfigString(
|
||||
return normalized || undefined;
|
||||
}
|
||||
|
||||
/** Build the shared allowlist/default target adapter surface for account-scoped channel configs. */
|
||||
export function createScopedAccountConfigAccessors<ResolvedAccount>(params: {
|
||||
resolveAccount: (params: { cfg: OpenClawConfig; accountId?: string | null }) => ResolvedAccount;
|
||||
resolveAllowFrom: (account: ResolvedAccount) => Array<string | number> | null | undefined;
|
||||
@@ -59,6 +63,7 @@ export function createScopedAccountConfigAccessors<ResolvedAccount>(params: {
|
||||
};
|
||||
}
|
||||
|
||||
/** Build the common CRUD/config helpers for channels that store multiple named accounts. */
|
||||
export function createScopedChannelConfigBase<
|
||||
ResolvedAccount,
|
||||
Config extends OpenClawConfig = OpenClawConfig,
|
||||
@@ -104,6 +109,7 @@ export function createScopedChannelConfigBase<
|
||||
};
|
||||
}
|
||||
|
||||
/** Convert account-specific DM security fields into the shared runtime policy resolver shape. */
|
||||
export function createScopedDmSecurityResolver<
|
||||
ResolvedAccount extends { accountId?: string | null },
|
||||
>(params: {
|
||||
@@ -143,6 +149,7 @@ export function createScopedDmSecurityResolver<
|
||||
});
|
||||
}
|
||||
|
||||
/** Read the effective WhatsApp allowlist through the active plugin contract. */
|
||||
export function resolveWhatsAppConfigAllowFrom(params: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
@@ -153,10 +160,12 @@ export function resolveWhatsAppConfigAllowFrom(params: {
|
||||
: [];
|
||||
}
|
||||
|
||||
/** Format WhatsApp allowlist entries with the same normalization used by the channel plugin. */
|
||||
export function formatWhatsAppConfigAllowFromEntries(allowFrom: Array<string | number>): string[] {
|
||||
return normalizeWhatsAppAllowFromEntries(allowFrom);
|
||||
}
|
||||
|
||||
/** Resolve the effective WhatsApp default recipient after account and root config fallback. */
|
||||
export function resolveWhatsAppConfigDefaultTo(params: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
@@ -167,6 +176,7 @@ export function resolveWhatsAppConfigDefaultTo(params: {
|
||||
return (account?.defaultTo ?? root?.defaultTo)?.trim() || undefined;
|
||||
}
|
||||
|
||||
/** Read iMessage allowlist entries from the active plugin's resolved account view. */
|
||||
export function resolveIMessageConfigAllowFrom(params: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
@@ -178,6 +188,7 @@ export function resolveIMessageConfigAllowFrom(params: {
|
||||
return mapAllowFromEntries(account.config.allowFrom);
|
||||
}
|
||||
|
||||
/** Resolve the effective iMessage default recipient from the plugin-resolved account config. */
|
||||
export function resolveIMessageConfigDefaultTo(params: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
|
||||
@@ -11,6 +11,7 @@ type PassiveAccountLifecycleParams<Handle> = {
|
||||
onStop?: () => void | Promise<void>;
|
||||
};
|
||||
|
||||
/** Bind a fixed account id into a status writer so lifecycle code can emit partial snapshots. */
|
||||
export function createAccountStatusSink(params: {
|
||||
accountId: string;
|
||||
setStatus: (next: ChannelAccountSnapshot) => void;
|
||||
|
||||
@@ -4,6 +4,7 @@ export type ChannelSendRawResult = {
|
||||
error?: string | null;
|
||||
};
|
||||
|
||||
/** Normalize raw channel send results into the shape shared outbound callers expect. */
|
||||
export function buildChannelSendResult(channel: string, result: ChannelSendRawResult) {
|
||||
return {
|
||||
channel,
|
||||
|
||||
@@ -33,6 +33,7 @@ export type ResolveSenderCommandAuthorizationWithRuntimeParams = Omit<
|
||||
runtime: CommandAuthorizationRuntime;
|
||||
};
|
||||
|
||||
/** Fast-path DM command authorization when only policy and sender allowlist state matter. */
|
||||
export function resolveDirectDmAuthorizationOutcome(params: {
|
||||
isGroup: boolean;
|
||||
dmPolicy: string;
|
||||
@@ -50,6 +51,7 @@ export function resolveDirectDmAuthorizationOutcome(params: {
|
||||
return "allowed";
|
||||
}
|
||||
|
||||
/** Runtime-backed wrapper around sender command authorization for grouped helper surfaces. */
|
||||
export async function resolveSenderCommandAuthorizationWithRuntime(
|
||||
params: ResolveSenderCommandAuthorizationWithRuntimeParams,
|
||||
): ReturnType<typeof resolveSenderCommandAuthorization> {
|
||||
@@ -60,6 +62,7 @@ export async function resolveSenderCommandAuthorizationWithRuntime(
|
||||
});
|
||||
}
|
||||
|
||||
/** Compute effective allowlists and command authorization for one inbound sender. */
|
||||
export async function resolveSenderCommandAuthorization(
|
||||
params: ResolveSenderCommandAuthorizationParams,
|
||||
): Promise<{
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
|
||||
/** Resolve the config path prefix for a channel account, falling back to the root channel section. */
|
||||
export function resolveChannelAccountConfigBasePath(params: {
|
||||
cfg: OpenClawConfig;
|
||||
channelKey: string;
|
||||
|
||||
@@ -11,6 +11,7 @@ type DiscordSendMediaOptionInput = DiscordSendOptionInput & {
|
||||
mediaLocalRoots?: readonly string[];
|
||||
};
|
||||
|
||||
/** Build the common Discord send options from SDK-level reply payload fields. */
|
||||
export function buildDiscordSendOptions(input: DiscordSendOptionInput) {
|
||||
return {
|
||||
verbose: false,
|
||||
@@ -20,6 +21,7 @@ export function buildDiscordSendOptions(input: DiscordSendOptionInput) {
|
||||
};
|
||||
}
|
||||
|
||||
/** Extend the base Discord send options with media-specific fields. */
|
||||
export function buildDiscordSendMediaOptions(input: DiscordSendMediaOptionInput) {
|
||||
return {
|
||||
...buildDiscordSendOptions(input),
|
||||
@@ -28,6 +30,7 @@ export function buildDiscordSendMediaOptions(input: DiscordSendMediaOptionInput)
|
||||
};
|
||||
}
|
||||
|
||||
/** Stamp raw Discord send results with the channel id expected by shared outbound flows. */
|
||||
export function tagDiscordChannelResult(result: DiscordSendResult) {
|
||||
return { channel: "discord" as const, ...result };
|
||||
}
|
||||
|
||||
@@ -23,9 +23,15 @@ export {
|
||||
looksLikeDiscordTargetId,
|
||||
normalizeDiscordMessagingTarget,
|
||||
normalizeDiscordOutboundTarget,
|
||||
} from "../channels/plugins/normalize/discord.js";
|
||||
} from "../../extensions/discord/src/normalize.js";
|
||||
export { collectDiscordAuditChannelIds } from "../../extensions/discord/src/audit.js";
|
||||
export { collectDiscordStatusIssues } from "../channels/plugins/status-issues/discord.js";
|
||||
export { collectDiscordStatusIssues } from "../../extensions/discord/src/status-issues.js";
|
||||
export {
|
||||
DISCORD_DEFAULT_INBOUND_WORKER_TIMEOUT_MS,
|
||||
DISCORD_DEFAULT_LISTENER_TIMEOUT_MS,
|
||||
} from "../../extensions/discord/src/monitor/timeouts.js";
|
||||
export { normalizeExplicitDiscordSessionKey } from "../../extensions/discord/src/session-key-normalization.js";
|
||||
export type { DiscordPluralKitConfig } from "../../extensions/discord/src/pluralkit.js";
|
||||
|
||||
export {
|
||||
resolveDefaultGroupPolicy,
|
||||
|
||||
@@ -4,18 +4,21 @@ export const pluginSdkEntrypoints = [...pluginSdkEntryList];
|
||||
|
||||
export const pluginSdkSubpaths = pluginSdkEntrypoints.filter((entry) => entry !== "index");
|
||||
|
||||
/** Map every SDK entrypoint name to its source file path inside the repo. */
|
||||
export function buildPluginSdkEntrySources() {
|
||||
return Object.fromEntries(
|
||||
pluginSdkEntrypoints.map((entry) => [entry, `src/plugin-sdk/${entry}.ts`]),
|
||||
);
|
||||
}
|
||||
|
||||
/** List the public package specifiers that should resolve to plugin SDK entrypoints. */
|
||||
export function buildPluginSdkSpecifiers() {
|
||||
return pluginSdkEntrypoints.map((entry) =>
|
||||
entry === "index" ? "openclaw/plugin-sdk" : `openclaw/plugin-sdk/${entry}`,
|
||||
);
|
||||
}
|
||||
|
||||
/** Build the package.json exports map for all plugin SDK subpaths. */
|
||||
export function buildPluginSdkPackageExports() {
|
||||
return Object.fromEntries(
|
||||
pluginSdkEntrypoints.map((entry) => [
|
||||
@@ -28,6 +31,7 @@ export function buildPluginSdkPackageExports() {
|
||||
);
|
||||
}
|
||||
|
||||
/** List the dist artifacts expected for every generated plugin SDK entrypoint. */
|
||||
export function listPluginSdkDistArtifacts() {
|
||||
return pluginSdkEntrypoints.flatMap((entry) => [
|
||||
`dist/plugin-sdk/${entry}.js`,
|
||||
|
||||
@@ -76,6 +76,10 @@ export {
|
||||
createDefaultChannelRuntimeState,
|
||||
} from "./status-helpers.js";
|
||||
export { withTempDownloadPath } from "./temp-path.js";
|
||||
export {
|
||||
buildFeishuConversationId,
|
||||
parseFeishuConversationId,
|
||||
} from "../../extensions/feishu/src/conversation-id.js";
|
||||
export {
|
||||
createFixedWindowRateLimiter,
|
||||
createWebhookAnomalyTracker,
|
||||
|
||||
@@ -6,6 +6,7 @@ function isAuthFailureStatus(status: number): boolean {
|
||||
return status === 401 || status === 403;
|
||||
}
|
||||
|
||||
/** Retry a fetch with bearer tokens from the provided scopes when the unauthenticated attempt fails. */
|
||||
export async function fetchWithBearerAuthScopeFallback(params: {
|
||||
url: string;
|
||||
scopes: readonly string[];
|
||||
|
||||
@@ -100,6 +100,7 @@ async function releaseHeldLock(normalizedFile: string): Promise<void> {
|
||||
await fs.rm(current.lockPath, { force: true }).catch(() => undefined);
|
||||
}
|
||||
|
||||
/** Acquire a re-entrant process-local file lock backed by a `.lock` sidecar file. */
|
||||
export async function acquireFileLock(
|
||||
filePath: string,
|
||||
options: FileLockOptions,
|
||||
@@ -147,6 +148,7 @@ export async function acquireFileLock(
|
||||
throw new Error(`file lock timeout for ${normalizedFile}`);
|
||||
}
|
||||
|
||||
/** Run an async callback while holding a file lock, always releasing the lock afterward. */
|
||||
export async function withFileLock<T>(
|
||||
filePath: string,
|
||||
options: FileLockOptions,
|
||||
|
||||
@@ -40,6 +40,7 @@ export type MatchedGroupAccessDecision = {
|
||||
reason: MatchedGroupAccessReason;
|
||||
};
|
||||
|
||||
/** Downgrade sender-scoped group policy to open mode when no allowlist is configured. */
|
||||
export function resolveSenderScopedGroupPolicy(params: {
|
||||
groupPolicy: GroupPolicy;
|
||||
groupAllowFrom: string[];
|
||||
@@ -50,6 +51,7 @@ export function resolveSenderScopedGroupPolicy(params: {
|
||||
return params.groupAllowFrom.length > 0 ? "allowlist" : "open";
|
||||
}
|
||||
|
||||
/** Evaluate route-level group access after policy, route match, and enablement checks. */
|
||||
export function evaluateGroupRouteAccessForPolicy(params: {
|
||||
groupPolicy: GroupPolicy;
|
||||
routeAllowlistConfigured: boolean;
|
||||
@@ -96,6 +98,7 @@ export function evaluateGroupRouteAccessForPolicy(params: {
|
||||
};
|
||||
}
|
||||
|
||||
/** Evaluate generic allowlist match state for channels that compare derived group identifiers. */
|
||||
export function evaluateMatchedGroupAccessForPolicy(params: {
|
||||
groupPolicy: GroupPolicy;
|
||||
allowlistConfigured: boolean;
|
||||
@@ -142,6 +145,7 @@ export function evaluateMatchedGroupAccessForPolicy(params: {
|
||||
};
|
||||
}
|
||||
|
||||
/** Evaluate sender access for an already-resolved group policy and allowlist. */
|
||||
export function evaluateSenderGroupAccessForPolicy(params: {
|
||||
groupPolicy: GroupPolicy;
|
||||
providerMissingFallbackApplied?: boolean;
|
||||
@@ -184,6 +188,7 @@ export function evaluateSenderGroupAccessForPolicy(params: {
|
||||
};
|
||||
}
|
||||
|
||||
/** Resolve provider fallback policy first, then evaluate sender access against that result. */
|
||||
export function evaluateSenderGroupAccess(params: {
|
||||
providerConfigPresent: boolean;
|
||||
configuredGroupPolicy?: GroupPolicy;
|
||||
|
||||
@@ -24,6 +24,7 @@ type InboundRouteResolveParams<TConfig, TPeer extends RoutePeerLike> = {
|
||||
peer: TPeer;
|
||||
};
|
||||
|
||||
/** Create an envelope formatter bound to one resolved route and session store. */
|
||||
export function createInboundEnvelopeBuilder<TConfig, TEnvelope>(params: {
|
||||
cfg: TConfig;
|
||||
route: RouteLike;
|
||||
@@ -54,6 +55,7 @@ export function createInboundEnvelopeBuilder<TConfig, TEnvelope>(params: {
|
||||
};
|
||||
}
|
||||
|
||||
/** Resolve a route first, then return both the route and a formatter for future inbound messages. */
|
||||
export function resolveInboundRouteEnvelopeBuilder<
|
||||
TConfig,
|
||||
TEnvelope,
|
||||
@@ -111,6 +113,7 @@ type InboundRouteEnvelopeRuntime<
|
||||
};
|
||||
};
|
||||
|
||||
/** Runtime-driven variant of inbound envelope resolution for plugins that already expose grouped helpers. */
|
||||
export function resolveInboundRouteEnvelopeBuilderWithRuntime<
|
||||
TConfig,
|
||||
TEnvelope,
|
||||
|
||||
@@ -20,6 +20,7 @@ type DispatchReplyWithBufferedBlockDispatcherFn =
|
||||
|
||||
type ReplyDispatchFromConfigOptions = Omit<GetReplyOptions, "onToolResult" | "onBlockReply">;
|
||||
|
||||
/** Run `dispatchReplyFromConfig` with a dispatcher that always gets its settled callback. */
|
||||
export async function dispatchReplyFromConfigWithSettledDispatcher(params: {
|
||||
cfg: OpenClawConfig;
|
||||
ctxPayload: FinalizedMsgContext;
|
||||
@@ -40,6 +41,7 @@ export async function dispatchReplyFromConfigWithSettledDispatcher(params: {
|
||||
});
|
||||
}
|
||||
|
||||
/** Assemble the common inbound reply dispatch dependencies for a resolved route. */
|
||||
export function buildInboundReplyDispatchBase(params: {
|
||||
cfg: OpenClawConfig;
|
||||
channel: string;
|
||||
@@ -80,6 +82,7 @@ type RecordInboundSessionAndDispatchReplyParams = Parameters<
|
||||
typeof recordInboundSessionAndDispatchReply
|
||||
>[0];
|
||||
|
||||
/** Resolve the shared dispatch base and immediately record + dispatch one inbound reply turn. */
|
||||
export async function dispatchInboundReplyWithBase(
|
||||
params: BuildInboundReplyDispatchBaseParams &
|
||||
Pick<
|
||||
@@ -97,6 +100,7 @@ export async function dispatchInboundReplyWithBase(
|
||||
});
|
||||
}
|
||||
|
||||
/** Record the inbound session first, then dispatch the reply using normalized outbound delivery. */
|
||||
export async function recordInboundSessionAndDispatchReply(params: {
|
||||
cfg: OpenClawConfig;
|
||||
channel: string;
|
||||
|
||||
@@ -2,6 +2,7 @@ import fs from "node:fs";
|
||||
import { writeJsonAtomic } from "../infra/json-files.js";
|
||||
import { safeParseJson } from "../utils.js";
|
||||
|
||||
/** Read JSON from disk and fall back cleanly when the file is missing or invalid. */
|
||||
export async function readJsonFileWithFallback<T>(
|
||||
filePath: string,
|
||||
fallback: T,
|
||||
@@ -22,6 +23,7 @@ export async function readJsonFileWithFallback<T>(
|
||||
}
|
||||
}
|
||||
|
||||
/** Write JSON with secure file permissions and atomic replacement semantics. */
|
||||
export async function writeJsonFileAtomically(filePath: string, value: unknown): Promise<void> {
|
||||
await writeJsonAtomic(filePath, value, {
|
||||
mode: 0o600,
|
||||
|
||||
@@ -3,6 +3,7 @@ export type KeyedAsyncQueueHooks = {
|
||||
onSettle?: () => void;
|
||||
};
|
||||
|
||||
/** Serialize async work per key while allowing unrelated keys to run concurrently. */
|
||||
export function enqueueKeyedTask<T>(params: {
|
||||
tails: Map<string, Promise<void>>;
|
||||
key: string;
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { createHash, randomBytes } from "node:crypto";
|
||||
|
||||
/** Encode a flat object as application/x-www-form-urlencoded form data. */
|
||||
export function toFormUrlEncoded(data: Record<string, string>): string {
|
||||
return Object.entries(data)
|
||||
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
|
||||
.join("&");
|
||||
}
|
||||
|
||||
/** Generate a PKCE verifier/challenge pair suitable for OAuth authorization flows. */
|
||||
export function generatePkceVerifierChallenge(): { verifier: string; challenge: string } {
|
||||
const verifier = randomBytes(32).toString("base64url");
|
||||
const challenge = createHash("sha256").update(verifier).digest("base64url");
|
||||
|
||||
@@ -5,6 +5,7 @@ export type OutboundMediaLoadOptions = {
|
||||
mediaLocalRoots?: readonly string[];
|
||||
};
|
||||
|
||||
/** Load outbound media from a remote URL or approved local path using the shared web-media policy. */
|
||||
export async function loadOutboundMediaFromUrl(
|
||||
mediaUrl: string,
|
||||
options: OutboundMediaLoadOptions = {},
|
||||
|
||||
@@ -8,6 +8,7 @@ type ScopedUpsertInput = Omit<
|
||||
"channel" | "accountId"
|
||||
>;
|
||||
|
||||
/** Scope pairing store operations to one channel/account pair for plugin-facing helpers. */
|
||||
export function createScopedPairingAccess(params: {
|
||||
core: PluginRuntime;
|
||||
channel: ChannelId;
|
||||
|
||||
@@ -91,6 +91,7 @@ function pruneData(
|
||||
});
|
||||
}
|
||||
|
||||
/** Create a dedupe helper that combines in-memory fast checks with a lock-protected disk store. */
|
||||
export function createPersistentDedupe(options: PersistentDedupeOptions): PersistentDedupe {
|
||||
const ttlMs = Math.max(0, Math.floor(options.ttlMs));
|
||||
const memoryMaxSize = Math.max(0, Math.floor(options.memoryMaxSize));
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { AuthProfileCredential } from "../agents/auth-profiles/types.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import type { ProviderAuthResult } from "../plugins/types.js";
|
||||
|
||||
/** Build the standard auth result payload for OAuth-style provider login flows. */
|
||||
export function buildOauthProviderAuthResult(params: {
|
||||
providerId: string;
|
||||
defaultModel: string;
|
||||
|
||||
@@ -5,6 +5,7 @@ export type OutboundReplyPayload = {
|
||||
replyToId?: string;
|
||||
};
|
||||
|
||||
/** Extract the supported outbound reply fields from loose tool or agent payload objects. */
|
||||
export function normalizeOutboundReplyPayload(
|
||||
payload: Record<string, unknown>,
|
||||
): OutboundReplyPayload {
|
||||
@@ -24,6 +25,7 @@ export function normalizeOutboundReplyPayload(
|
||||
};
|
||||
}
|
||||
|
||||
/** Wrap a deliverer so callers can hand it arbitrary payloads while channels receive normalized data. */
|
||||
export function createNormalizedOutboundDeliverer(
|
||||
handler: (payload: OutboundReplyPayload) => Promise<void>,
|
||||
): (payload: unknown) => Promise<void> {
|
||||
@@ -36,6 +38,7 @@ export function createNormalizedOutboundDeliverer(
|
||||
};
|
||||
}
|
||||
|
||||
/** Prefer multi-attachment payloads, then fall back to the legacy single-media field. */
|
||||
export function resolveOutboundMediaUrls(payload: {
|
||||
mediaUrls?: string[];
|
||||
mediaUrl?: string;
|
||||
@@ -49,6 +52,7 @@ export function resolveOutboundMediaUrls(payload: {
|
||||
return [];
|
||||
}
|
||||
|
||||
/** Send media-first payloads intact, or chunk text-only payloads through the caller's transport hooks. */
|
||||
export async function sendPayloadWithChunkedTextAndMedia<
|
||||
TContext extends { payload: object },
|
||||
TResult,
|
||||
@@ -90,6 +94,7 @@ export async function sendPayloadWithChunkedTextAndMedia<
|
||||
return lastResult!;
|
||||
}
|
||||
|
||||
/** Detect numeric-looking target ids for channels that distinguish ids from handles. */
|
||||
export function isNumericTargetId(raw: string): boolean {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) {
|
||||
@@ -98,6 +103,7 @@ export function isNumericTargetId(raw: string): boolean {
|
||||
return /^\d{3,}$/.test(trimmed);
|
||||
}
|
||||
|
||||
/** Append attachment links to plain text when the channel cannot send media inline. */
|
||||
export function formatTextWithAttachmentLinks(
|
||||
text: string | undefined,
|
||||
mediaUrls: string[],
|
||||
@@ -118,6 +124,7 @@ export function formatTextWithAttachmentLinks(
|
||||
return `${trimmedText}\n\n${mediaBlock}`;
|
||||
}
|
||||
|
||||
/** Send a caption with only the first media item, mirroring caption-limited channel transports. */
|
||||
export async function sendMediaWithLeadingCaption(params: {
|
||||
mediaUrls: string[];
|
||||
caption: string;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/** Extract a string URL from the common request-like inputs accepted by fetch helpers. */
|
||||
export function resolveRequestUrl(input: RequestInfo | URL): string {
|
||||
if (typeof input === "string") {
|
||||
return input;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/** Format a short note that separates successfully resolved targets from unresolved passthrough values. */
|
||||
export function formatResolvedUnresolvedNote(params: {
|
||||
resolved: string[];
|
||||
unresolved: string[];
|
||||
|
||||
@@ -13,6 +13,7 @@ export type PluginCommandRunOptions = {
|
||||
env?: NodeJS.ProcessEnv;
|
||||
};
|
||||
|
||||
/** Run a plugin-managed command with timeout handling and normalized stdout/stderr results. */
|
||||
export async function runPluginCommandWithTimeout(
|
||||
options: PluginCommandRunOptions,
|
||||
): Promise<PluginCommandRunResult> {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/** Create a tiny mutable runtime slot with strict access when the runtime has not been initialized. */
|
||||
export function createPluginRuntimeStore<T>(errorMessage: string): {
|
||||
setRuntime: (next: T) => void;
|
||||
clearRuntime: () => void;
|
||||
|
||||
@@ -6,6 +6,7 @@ type LoggerLike = {
|
||||
error: (message: string) => void;
|
||||
};
|
||||
|
||||
/** Adapt a simple logger into the RuntimeEnv contract used by shared plugin SDK helpers. */
|
||||
export function createLoggerBackedRuntime(params: {
|
||||
logger: LoggerLike;
|
||||
exitError?: (code: number) => Error;
|
||||
@@ -23,6 +24,7 @@ export function createLoggerBackedRuntime(params: {
|
||||
};
|
||||
}
|
||||
|
||||
/** Reuse an existing runtime when present, otherwise synthesize one from the provided logger. */
|
||||
export function resolveRuntimeEnv(params: {
|
||||
runtime?: RuntimeEnv;
|
||||
logger: LoggerLike;
|
||||
@@ -31,6 +33,7 @@ export function resolveRuntimeEnv(params: {
|
||||
return params.runtime ?? createLoggerBackedRuntime(params);
|
||||
}
|
||||
|
||||
/** Resolve a runtime that treats exit requests as unsupported errors instead of process termination. */
|
||||
export function resolveRuntimeEnvWithUnavailableExit(params: {
|
||||
runtime?: RuntimeEnv;
|
||||
logger: LoggerLike;
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
SECRET_PROVIDER_ALIAS_PATTERN,
|
||||
} from "../secrets/ref-contract.js";
|
||||
|
||||
/** Build the shared zod schema for secret inputs accepted by plugin auth/config surfaces. */
|
||||
export function buildSecretInputSchema() {
|
||||
const providerSchema = z
|
||||
.string()
|
||||
|
||||
@@ -15,6 +15,7 @@ function readSlackBlocksParam(actionParams: Record<string, unknown>) {
|
||||
return parseSlackBlocksInput(actionParams.blocks) as Record<string, unknown>[] | undefined;
|
||||
}
|
||||
|
||||
/** Translate generic channel action requests into Slack-specific tool invocations and payload shapes. */
|
||||
export async function handleSlackMessageAction(params: {
|
||||
providerId: string;
|
||||
ctx: ChannelMessageActionContext;
|
||||
|
||||
@@ -24,6 +24,7 @@ function isHostnameAllowedBySuffixAllowlist(
|
||||
return allowlist.some((entry) => normalized === entry || normalized.endsWith(`.${entry}`));
|
||||
}
|
||||
|
||||
/** Normalize suffix-style host allowlists into lowercase canonical entries with wildcard collapse. */
|
||||
export function normalizeHostnameSuffixAllowlist(
|
||||
input?: readonly string[],
|
||||
defaults?: readonly string[],
|
||||
@@ -39,6 +40,7 @@ export function normalizeHostnameSuffixAllowlist(
|
||||
return Array.from(new Set(normalized));
|
||||
}
|
||||
|
||||
/** Check whether a URL is HTTPS and its hostname matches the normalized suffix allowlist. */
|
||||
export function isHttpsUrlAllowedByHostnameSuffixAllowlist(
|
||||
url: string,
|
||||
allowlist: readonly string[],
|
||||
|
||||
@@ -9,6 +9,7 @@ type RuntimeLifecycleSnapshot = {
|
||||
lastOutboundAt?: number | null;
|
||||
};
|
||||
|
||||
/** Create the baseline runtime snapshot shape used by channel/account status stores. */
|
||||
export function createDefaultChannelRuntimeState<T extends Record<string, unknown>>(
|
||||
accountId: string,
|
||||
extra?: T,
|
||||
@@ -29,6 +30,7 @@ export function createDefaultChannelRuntimeState<T extends Record<string, unknow
|
||||
};
|
||||
}
|
||||
|
||||
/** Normalize a channel-level status summary so missing lifecycle fields become explicit nulls. */
|
||||
export function buildBaseChannelStatusSummary(snapshot: {
|
||||
configured?: boolean | null;
|
||||
running?: boolean | null;
|
||||
@@ -45,6 +47,7 @@ export function buildBaseChannelStatusSummary(snapshot: {
|
||||
};
|
||||
}
|
||||
|
||||
/** Extend the base summary with probe fields while preserving stable null defaults. */
|
||||
export function buildProbeChannelStatusSummary<TExtra extends Record<string, unknown>>(
|
||||
snapshot: {
|
||||
configured?: boolean | null;
|
||||
@@ -65,6 +68,7 @@ export function buildProbeChannelStatusSummary<TExtra extends Record<string, unk
|
||||
};
|
||||
}
|
||||
|
||||
/** Build the standard per-account status payload from config metadata plus runtime state. */
|
||||
export function buildBaseAccountStatusSnapshot(params: {
|
||||
account: {
|
||||
accountId: string;
|
||||
@@ -87,6 +91,7 @@ export function buildBaseAccountStatusSnapshot(params: {
|
||||
};
|
||||
}
|
||||
|
||||
/** Convenience wrapper when the caller already has flattened account fields instead of an account object. */
|
||||
export function buildComputedAccountStatusSnapshot(params: {
|
||||
accountId: string;
|
||||
name?: string;
|
||||
@@ -108,6 +113,7 @@ export function buildComputedAccountStatusSnapshot(params: {
|
||||
});
|
||||
}
|
||||
|
||||
/** Normalize runtime-only account state into the shared status snapshot fields. */
|
||||
export function buildRuntimeAccountStatusSnapshot(params: {
|
||||
runtime?: RuntimeLifecycleSnapshot | null;
|
||||
probe?: unknown;
|
||||
@@ -122,6 +128,7 @@ export function buildRuntimeAccountStatusSnapshot(params: {
|
||||
};
|
||||
}
|
||||
|
||||
/** Build token-based channel status summaries with optional mode reporting. */
|
||||
export function buildTokenChannelStatusSummary(
|
||||
snapshot: {
|
||||
configured?: boolean | null;
|
||||
@@ -151,6 +158,7 @@ export function buildTokenChannelStatusSummary(
|
||||
};
|
||||
}
|
||||
|
||||
/** Convert account runtime errors into the generic channel status issue format. */
|
||||
export function collectStatusIssuesFromLastError(
|
||||
channel: string,
|
||||
accounts: Array<{ accountId: string; lastError?: unknown }>,
|
||||
|
||||
@@ -48,7 +48,7 @@ export {
|
||||
export {
|
||||
looksLikeTelegramTargetId,
|
||||
normalizeTelegramMessagingTarget,
|
||||
} from "../channels/plugins/normalize/telegram.js";
|
||||
} from "../../extensions/telegram/src/normalize.js";
|
||||
export {
|
||||
parseTelegramReplyToMessageId,
|
||||
parseTelegramThreadId,
|
||||
@@ -58,7 +58,7 @@ export {
|
||||
normalizeTelegramAllowFromEntry,
|
||||
} from "../../extensions/telegram/src/allow-from.js";
|
||||
export { fetchTelegramChatId } from "../../extensions/telegram/src/api-fetch.js";
|
||||
export { collectTelegramStatusIssues } from "../channels/plugins/status-issues/telegram.js";
|
||||
export { collectTelegramStatusIssues } from "../../extensions/telegram/src/status-issues.js";
|
||||
export { sendTelegramPayloadMessages } from "../../extensions/telegram/src/outbound-adapter.js";
|
||||
export {
|
||||
buildBrowseProvidersButton,
|
||||
@@ -72,6 +72,7 @@ export {
|
||||
isTelegramExecApprovalApprover,
|
||||
isTelegramExecApprovalClientEnabled,
|
||||
} from "../../extensions/telegram/src/exec-approvals.js";
|
||||
export type { StickerMetadata } from "../../extensions/telegram/src/bot/types.js";
|
||||
|
||||
export {
|
||||
resolveAllowlistProviderRuntimeGroupPolicy,
|
||||
|
||||
@@ -40,6 +40,7 @@ function isNodeErrorWithCode(err: unknown, code: string): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
/** Build a unique temp file path with sanitized prefix/extension parts. */
|
||||
export function buildRandomTempFilePath(params: {
|
||||
prefix: string;
|
||||
extension?: string;
|
||||
@@ -58,6 +59,7 @@ export function buildRandomTempFilePath(params: {
|
||||
return path.join(resolveTempRoot(params.tmpDir), `${prefix}-${now}-${uuid}${extension}`);
|
||||
}
|
||||
|
||||
/** Create a temporary download directory, run the callback, then clean it up best-effort. */
|
||||
export async function withTempDownloadPath<T>(
|
||||
params: {
|
||||
prefix: string;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { chunkTextByBreakResolver } from "../shared/text-chunking.js";
|
||||
|
||||
/** Chunk outbound text while preferring newline boundaries over spaces. */
|
||||
export function chunkTextForOutbound(text: string, limit: number): string[] {
|
||||
return chunkTextByBreakResolver(text, limit, (window) => {
|
||||
const lastNewline = window.lastIndexOf("\n");
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/** Extract the canonical send target fields from tool arguments when the action matches. */
|
||||
export function extractToolSend(
|
||||
args: Record<string, unknown>,
|
||||
expectedAction = "sendMessage",
|
||||
|
||||
@@ -48,6 +48,7 @@ export type WebhookAnomalyTracker = {
|
||||
clear: () => void;
|
||||
};
|
||||
|
||||
/** Create a simple fixed-window rate limiter for in-memory webhook protection. */
|
||||
export function createFixedWindowRateLimiter(options: {
|
||||
windowMs: number;
|
||||
maxRequests: number;
|
||||
@@ -104,6 +105,7 @@ export function createFixedWindowRateLimiter(options: {
|
||||
};
|
||||
}
|
||||
|
||||
/** Count keyed events in memory with optional TTL pruning and bounded cardinality. */
|
||||
export function createBoundedCounter(options: {
|
||||
maxTrackedKeys: number;
|
||||
ttlMs?: number;
|
||||
@@ -161,6 +163,7 @@ export function createBoundedCounter(options: {
|
||||
};
|
||||
}
|
||||
|
||||
/** Track repeated webhook failures and emit sampled logs for suspicious request patterns. */
|
||||
export function createWebhookAnomalyTracker(options?: {
|
||||
maxTrackedKeys?: number;
|
||||
ttlMs?: number;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/** Normalize webhook paths into the canonical registry form used by route lookup. */
|
||||
export function normalizeWebhookPath(raw: string): string {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) {
|
||||
@@ -10,6 +11,7 @@ export function normalizeWebhookPath(raw: string): string {
|
||||
return withSlash;
|
||||
}
|
||||
|
||||
/** Resolve the effective webhook path from explicit path, URL, or default fallback. */
|
||||
export function resolveWebhookPath(params: {
|
||||
webhookPath?: string;
|
||||
webhookUrl?: string;
|
||||
|
||||
@@ -81,6 +81,7 @@ function respondWebhookBodyReadError(params: {
|
||||
return { ok: false };
|
||||
}
|
||||
|
||||
/** Create an in-memory limiter that caps concurrent webhook handlers per key. */
|
||||
export function createWebhookInFlightLimiter(options?: {
|
||||
maxInFlightPerKey?: number;
|
||||
maxTrackedKeys?: number;
|
||||
@@ -127,6 +128,7 @@ export function createWebhookInFlightLimiter(options?: {
|
||||
};
|
||||
}
|
||||
|
||||
/** Detect JSON content types, including structured syntax suffixes like `application/ld+json`. */
|
||||
export function isJsonContentType(value: string | string[] | undefined): boolean {
|
||||
const first = Array.isArray(value) ? value[0] : value;
|
||||
if (!first) {
|
||||
@@ -136,6 +138,7 @@ export function isJsonContentType(value: string | string[] | undefined): boolean
|
||||
return mediaType === "application/json" || Boolean(mediaType?.endsWith("+json"));
|
||||
}
|
||||
|
||||
/** Apply method, rate-limit, and content-type guards before a webhook handler reads the body. */
|
||||
export function applyBasicWebhookRequestGuards(params: {
|
||||
req: IncomingMessage;
|
||||
res: ServerResponse;
|
||||
@@ -176,6 +179,7 @@ export function applyBasicWebhookRequestGuards(params: {
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Start the shared webhook request lifecycle and return a release hook for in-flight tracking. */
|
||||
export function beginWebhookRequestPipelineOrReject(params: {
|
||||
req: IncomingMessage;
|
||||
res: ServerResponse;
|
||||
@@ -226,6 +230,7 @@ export function beginWebhookRequestPipelineOrReject(params: {
|
||||
};
|
||||
}
|
||||
|
||||
/** Read a webhook request body with bounded size/time limits and translate failures into responses. */
|
||||
export async function readWebhookBodyOrReject(params: {
|
||||
req: IncomingMessage;
|
||||
res: ServerResponse;
|
||||
@@ -260,6 +265,7 @@ export async function readWebhookBodyOrReject(params: {
|
||||
}
|
||||
}
|
||||
|
||||
/** Read and parse a JSON webhook body, rejecting malformed or oversized payloads consistently. */
|
||||
export async function readJsonWebhookBodyOrReject(params: {
|
||||
req: IncomingMessage;
|
||||
res: ServerResponse;
|
||||
|
||||
@@ -24,6 +24,7 @@ export type RegisterWebhookPluginRouteOptions = Omit<
|
||||
"path" | "fallbackPath"
|
||||
>;
|
||||
|
||||
/** Register a webhook target and lazily install the matching plugin HTTP route on first use. */
|
||||
export function registerWebhookTargetWithPluginRoute<T extends { path: string }>(params: {
|
||||
targetsByPath: Map<string, T[]>;
|
||||
target: T;
|
||||
@@ -54,6 +55,7 @@ function getPathTeardownMap<T>(targetsByPath: Map<string, T[]>): Map<string, ()
|
||||
return created;
|
||||
}
|
||||
|
||||
/** Add a normalized target to a path bucket and clean up route state when the last target leaves. */
|
||||
export function registerWebhookTarget<T extends { path: string }>(
|
||||
targetsByPath: Map<string, T[]>,
|
||||
target: T,
|
||||
@@ -99,6 +101,7 @@ export function registerWebhookTarget<T extends { path: string }>(
|
||||
return { target: normalizedTarget, unregister };
|
||||
}
|
||||
|
||||
/** Resolve all registered webhook targets for the incoming request path. */
|
||||
export function resolveWebhookTargets<T>(
|
||||
req: IncomingMessage,
|
||||
targetsByPath: Map<string, T[]>,
|
||||
@@ -112,6 +115,7 @@ export function resolveWebhookTargets<T>(
|
||||
return { path, targets };
|
||||
}
|
||||
|
||||
/** Run common webhook guards, then dispatch only when the request path resolves to live targets. */
|
||||
export async function withResolvedWebhookRequestPipeline<T>(params: {
|
||||
req: IncomingMessage;
|
||||
res: ServerResponse;
|
||||
@@ -183,6 +187,7 @@ function finalizeMatchedWebhookTarget<T>(matched: T | undefined): WebhookTargetM
|
||||
return { kind: "single", target: matched };
|
||||
}
|
||||
|
||||
/** Match exactly one synchronous target or report whether resolution was empty or ambiguous. */
|
||||
export function resolveSingleWebhookTarget<T>(
|
||||
targets: readonly T[],
|
||||
isMatch: (target: T) => boolean,
|
||||
@@ -201,6 +206,7 @@ export function resolveSingleWebhookTarget<T>(
|
||||
return finalizeMatchedWebhookTarget(matched);
|
||||
}
|
||||
|
||||
/** Async variant of single-target resolution for auth checks that need I/O. */
|
||||
export async function resolveSingleWebhookTargetAsync<T>(
|
||||
targets: readonly T[],
|
||||
isMatch: (target: T) => Promise<boolean>,
|
||||
@@ -219,6 +225,7 @@ export async function resolveSingleWebhookTargetAsync<T>(
|
||||
return finalizeMatchedWebhookTarget(matched);
|
||||
}
|
||||
|
||||
/** Resolve an authorized target and send the standard unauthorized or ambiguous response on failure. */
|
||||
export async function resolveWebhookTargetWithAuthOrReject<T>(params: {
|
||||
targets: readonly T[];
|
||||
res: ServerResponse;
|
||||
@@ -234,6 +241,7 @@ export async function resolveWebhookTargetWithAuthOrReject<T>(params: {
|
||||
return resolveWebhookTargetMatchOrReject(params, match);
|
||||
}
|
||||
|
||||
/** Synchronous variant of webhook auth resolution for cheap in-memory match checks. */
|
||||
export function resolveWebhookTargetWithAuthOrRejectSync<T>(params: {
|
||||
targets: readonly T[];
|
||||
res: ServerResponse;
|
||||
@@ -270,6 +278,7 @@ function resolveWebhookTargetMatchOrReject<T>(
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Reject non-POST webhook requests with the conventional Allow header. */
|
||||
export function rejectNonPostWebhookRequest(req: IncomingMessage, res: ServerResponse): boolean {
|
||||
if (req.method === "POST") {
|
||||
return false;
|
||||
|
||||
@@ -26,6 +26,12 @@ export {
|
||||
listWhatsAppDirectoryGroupsFromConfig,
|
||||
listWhatsAppDirectoryPeersFromConfig,
|
||||
} from "../channels/plugins/directory-config.js";
|
||||
export {
|
||||
hasAnyWhatsAppAuth,
|
||||
listEnabledWhatsAppAccounts,
|
||||
resolveWhatsAppAccount,
|
||||
} from "../../extensions/whatsapp/src/accounts.js";
|
||||
export { normalizeWhatsAppAllowFromEntries } from "../channels/plugins/normalize/whatsapp.js";
|
||||
export {
|
||||
collectAllowlistProviderGroupPolicyWarnings,
|
||||
collectOpenGroupPolicyRouteAllowlistWarnings,
|
||||
|
||||
@@ -52,6 +52,7 @@ function isFilePath(candidate: string): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
/** Resolve a Windows command name through PATH and PATHEXT so wrapper inspection sees the real file. */
|
||||
export function resolveWindowsExecutablePath(command: string, env: NodeJS.ProcessEnv): string {
|
||||
if (command.includes("/") || command.includes("\\") || path.isAbsolute(command)) {
|
||||
return command;
|
||||
@@ -188,6 +189,7 @@ function resolveEntrypointFromPackageJson(
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Resolve the safest direct spawn candidate for Windows wrappers, scripts, and binaries. */
|
||||
export function resolveWindowsSpawnProgramCandidate(
|
||||
params: ResolveWindowsSpawnProgramCandidateParams,
|
||||
): WindowsSpawnProgramCandidate {
|
||||
@@ -250,6 +252,7 @@ export function resolveWindowsSpawnProgramCandidate(
|
||||
};
|
||||
}
|
||||
|
||||
/** Apply shell-fallback policy when Windows wrapper resolution could not find a direct entrypoint. */
|
||||
export function applyWindowsSpawnProgramPolicy(params: {
|
||||
candidate: WindowsSpawnProgramCandidate;
|
||||
allowShellFallback?: boolean;
|
||||
@@ -275,6 +278,7 @@ export function applyWindowsSpawnProgramPolicy(params: {
|
||||
);
|
||||
}
|
||||
|
||||
/** Resolve the final Windows spawn program after candidate discovery and fallback policy. */
|
||||
export function resolveWindowsSpawnProgram(
|
||||
params: ResolveWindowsSpawnProgramParams,
|
||||
): WindowsSpawnProgram {
|
||||
@@ -285,6 +289,7 @@ export function resolveWindowsSpawnProgram(
|
||||
});
|
||||
}
|
||||
|
||||
/** Combine a resolved Windows spawn program with call-site argv for actual process launch. */
|
||||
export function materializeWindowsSpawnProgram(
|
||||
program: WindowsSpawnProgram,
|
||||
argv: string[],
|
||||
|
||||
Reference in New Issue
Block a user