docs: document plugin security channel helpers

This commit is contained in:
Peter Steinberger
2026-06-03 21:01:26 -04:00
parent 4fbc318e30
commit 9b6cd2ea75
14 changed files with 61 additions and 0 deletions

View File

@@ -1,10 +1,12 @@
import { listChannelCatalogEntries } from "./channel-catalog-registry.js";
import type { PluginPackageChannel } from "./manifest.js";
/** Lists channel metadata contributed by bundled package manifests. */
export function listBundledPackageChannelMetadata(): readonly PluginPackageChannel[] {
return listChannelCatalogEntries({ origin: "bundled" }).map((entry) => entry.channel);
}
/** Finds bundled package channel metadata by id or alias. */
export function findBundledPackageChannelMetadata(
channelId: string,
): PluginPackageChannel | undefined {

View File

@@ -37,6 +37,7 @@ type BundledPluginPathPair = {
built: string;
};
/** Metadata collected from a bundled plugin package and manifest. */
export type BundledPluginMetadata = {
dirName: string;
idHint: string;
@@ -168,6 +169,7 @@ function collectBundledPluginMetadata(
return entries;
}
/** Lists bundled plugin metadata from source or built package layouts. */
export function listBundledPluginMetadata(params?: {
rootDir?: string;
scanDir?: string;
@@ -190,6 +192,7 @@ export function listBundledPluginMetadata(params?: {
return metadata;
}
/** Finds bundled plugin metadata by manifest id. */
export function findBundledPluginMetadataById(
pluginId: string,
params?: {
@@ -202,6 +205,7 @@ export function findBundledPluginMetadataById(
return listBundledPluginMetadata(params).find((entry) => entry.manifest.id === pluginId);
}
/** Resolves the source directory for a bundled plugin in the current workspace. */
export function resolveBundledPluginWorkspaceSourcePath(params: {
rootDir: string;
scanDir?: string;
@@ -296,6 +300,7 @@ function listBundledPluginEntrySearchPaths(
return uniqueStrings(paths);
}
/** Resolves a generated runtime path for a bundled plugin entry. */
export function resolveBundledPluginGeneratedPath(
rootDir: string,
entry: BundledPluginPathPair | undefined,
@@ -345,6 +350,7 @@ function resolveBundledPluginEntryCandidate(baseDir: string, entryPath: string):
return candidate;
}
/** Resolves the repo entry path for a bundled plugin, preferring source unless requested. */
export function resolveBundledPluginRepoEntryPath(params: {
rootDir: string;
pluginId: string;

View File

@@ -28,11 +28,13 @@ import { loadPluginManifestRegistryForPluginRegistry } from "./plugin-registry-c
const IGNORED_CHANNEL_CONFIG_KEYS = new Set(["defaults", "modelByChannel"]);
/** Source classes that can make a channel appear configured for read-only scopes. */
export type ConfiguredChannelPresenceSource =
| "explicit-config"
| Exclude<ChannelPresenceSignalSource, "config">
| "manifest-env";
/** Reasons a configured channel signal is not effective. */
export type ConfiguredChannelBlockedReason =
| "plugins-disabled"
| "blocked-by-denylist"
@@ -44,6 +46,7 @@ export type ConfiguredChannelBlockedReason =
| "no-channel-owner"
| "not-activated";
/** Policy evaluation row for one configured channel signal. */
export type ConfiguredChannelPresencePolicyEntry = {
channelId: string;
sources: ConfiguredChannelPresenceSource[];
@@ -70,6 +73,7 @@ function hasNonEmptyEnvValue(env: NodeJS.ProcessEnv, key: string): boolean {
return typeof value === "string" && value.trim().length > 0;
}
/** True when config contains meaningful enabled channel settings. */
export function hasExplicitChannelConfig(params: {
config: OpenClawConfig;
channelId: string;
@@ -89,6 +93,7 @@ export function hasExplicitChannelConfig(params: {
return enabled === true || hasMeaningfulChannelConfig(entry);
}
/** Lists explicitly configured channel ids, excluding global channel config keys. */
export function listExplicitConfiguredChannelIdsForConfig(config: OpenClawConfig): string[] {
const channels = config.channels;
if (!channels || typeof channels !== "object" || Array.isArray(channels)) {
@@ -182,6 +187,7 @@ function isChannelPluginEligibleForScopedOwnership(params: {
rootConfig: OpenClawConfig;
channelId?: string;
}): boolean {
// Explicit config can activate bundled channel owners even under restrictive allowlists.
const allowRestrictiveAllowlistBypass =
params.channelId !== undefined &&
isBundledManifestOwner(params.plugin) &&
@@ -221,6 +227,7 @@ function evaluateEffectiveChannelPlugin(params: {
config: OpenClawConfig;
activationSource: ReturnType<typeof createPluginActivationSource>;
}): { effective: boolean; pluginId: string; blockedReason?: ConfiguredChannelBlockedReason } {
// Bundled channels with explicit config are effective before default enablement checks.
const explicitBundledChannelConfig =
isBundledManifestOwner(params.plugin) &&
hasExplicitChannelConfig({
@@ -319,6 +326,7 @@ function loadInstalledChannelManifestRecords(params: {
}).plugins;
}
/** Resolves effective configured-channel policy rows from config, auth state, env, and manifests. */
export function resolveConfiguredChannelPresencePolicy(params: {
config: OpenClawConfig;
activationSourceConfig?: OpenClawConfig;
@@ -406,6 +414,7 @@ export function resolveConfiguredChannelPresencePolicy(params: {
return entries;
}
/** Lists effective channel ids available to read-only scoped discovery. */
export function listConfiguredChannelIdsForReadOnlyScope(
params: Parameters<typeof resolveConfiguredChannelPresencePolicy>[0],
): string[] {
@@ -414,12 +423,14 @@ export function listConfiguredChannelIdsForReadOnlyScope(
.map((entry) => entry.channelId);
}
/** True when read-only scoped discovery has any effective configured channel. */
export function hasConfiguredChannelsForReadOnlyScope(
params: Parameters<typeof resolveConfiguredChannelPresencePolicy>[0],
): boolean {
return listConfiguredChannelIdsForReadOnlyScope(params).length > 0;
}
/** Lists channel ids that should be announced as configured for operators. */
export function listConfiguredAnnounceChannelIdsForConfig(params: {
config: OpenClawConfig;
activationSourceConfig?: OpenClawConfig;
@@ -498,6 +509,7 @@ function resolveScopedChannelOwnerPluginIds(params: {
.toSorted((left, right) => left.localeCompare(right));
}
/** Resolves plugin ids discoverable for scoped channel activation. */
export function resolveDiscoverableScopedChannelPluginIds(params: {
config: OpenClawConfig;
activationSourceConfig?: OpenClawConfig;
@@ -509,6 +521,7 @@ export function resolveDiscoverableScopedChannelPluginIds(params: {
return resolveScopedChannelOwnerPluginIds(params);
}
/** Resolves plugin ids that own currently configured channels. */
export function resolveConfiguredChannelPluginIds(params: {
config: OpenClawConfig;
activationSourceConfig?: OpenClawConfig;

View File

@@ -50,6 +50,7 @@ function collectMissingChannelMetaFields(meta?: Partial<ChannelMeta> | null): st
return missing;
}
/** Validates and normalizes a channel plugin registration before runtime catalog insertion. */
export function normalizeRegisteredChannelPlugin(params: {
pluginId: string;
source: string;

View File

@@ -1,5 +1,6 @@
import type { AgentToolResult } from "../agents/runtime/index.js";
/** Tool-result event emitted to Codex app-server plugin extensions. */
export type CodexAppServerToolResultEvent = {
threadId: string;
turnId: string;
@@ -9,6 +10,7 @@ export type CodexAppServerToolResultEvent = {
result: AgentToolResult<unknown>;
};
/** Session context passed with Codex app-server extension events. */
export type CodexAppServerExtensionContext = {
agentId?: string;
sessionId?: string;
@@ -16,10 +18,12 @@ export type CodexAppServerExtensionContext = {
runId?: string;
};
/** Optional replacement result returned by a Codex app-server extension handler. */
export type CodexAppServerToolResultHandlerResult = {
result: AgentToolResult<unknown>;
};
/** Runtime event surface exposed to Codex app-server extension factories. */
export type CodexAppServerExtensionRuntime = {
on: (
event: "tool_result",
@@ -33,6 +37,7 @@ export type CodexAppServerExtensionRuntime = {
) => void;
};
/** Factory signature for Codex app-server plugin extensions. */
export type CodexAppServerExtensionFactory = (
runtime: CodexAppServerExtensionRuntime,
) => Promise<void> | void;

View File

@@ -1,5 +1,6 @@
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
/** Normalizes plugin HTTP paths to leading-slash form with optional fallback. */
export function normalizePluginHttpPath(
path?: string | null,
fallback?: string | null,

View File

@@ -11,6 +11,7 @@ type InstallScanLogger = {
warn?: (message: string) => void;
};
/** Result returned by plugin/skill install security policy checks. */
export type InstallSecurityScanResult = {
blocked?: {
code?: "security_scan_blocked" | "security_scan_failed";
@@ -18,8 +19,10 @@ export type InstallSecurityScanResult = {
};
};
/** Plugin install request kinds that share install policy without skill install semantics. */
export type PluginInstallRequestKind = Exclude<InstallPolicyRequestKind, "skill-install">;
/** Skill install metadata shape passed into shared install policy evaluation. */
export type SkillInstallSpecMetadata = {
id?: string;
kind: "brew" | "node" | "go" | "uv" | "download";
@@ -36,16 +39,19 @@ export type SkillInstallSpecMetadata = {
targetDir?: string;
};
/** Package executable metadata used to scope dependency and entrypoint scans. */
export type PackageExecutableScanMetadata = {
runtimeExtensions?: readonly string[];
runtimeSetupEntry?: string;
setupEntry?: string;
};
/** Lazily loads install scanning so normal plugin startup avoids policy/runtime imports. */
async function loadInstallSecurityScanRuntime() {
return await import("./install-security-scan.runtime.js");
}
/** Scans an unpacked bundle source before plugin install/update. */
export async function scanBundleInstallSource(
params: InstallSafetyOverrides & {
config?: OpenClawConfig;
@@ -63,6 +69,7 @@ export async function scanBundleInstallSource(
return await scanBundleInstallSourceRuntime(params);
}
/** Scans a package source directory and executable metadata before install/update. */
export async function scanPackageInstallSource(
params: InstallSafetyOverrides & {
config?: OpenClawConfig;
@@ -84,6 +91,7 @@ export async function scanPackageInstallSource(
return await scanPackageInstallSourceRuntime(params);
}
/** Scans the installed package dependency tree after npm resolution. */
export async function scanInstalledPackageDependencyTree(params: {
additionalPackageDirs?: string[];
allowManagedNpmRootPackagePeerSymlinks?: boolean;
@@ -103,6 +111,7 @@ export async function scanInstalledPackageDependencyTree(params: {
return await scanInstalledPackageDependencyTreeRuntime(params);
}
/** Scans one file-based plugin install source. */
export async function scanFileInstallSource(
params: InstallSafetyOverrides & {
config?: OpenClawConfig;
@@ -118,6 +127,7 @@ export async function scanFileInstallSource(
return await scanFileInstallSourceRuntime(params);
}
/** Runs npm install policy checks before package install side effects. */
export async function preflightPluginNpmInstallPolicy(params: {
config?: OpenClawConfig;
logger: InstallScanLogger;
@@ -133,6 +143,7 @@ export async function preflightPluginNpmInstallPolicy(params: {
return await preflightPluginNpmInstallPolicyRuntime(params);
}
/** Runs git install policy checks before plugin install side effects. */
export async function preflightPluginGitInstallPolicy(params: {
config?: OpenClawConfig;
logger: InstallScanLogger;
@@ -146,6 +157,7 @@ export async function preflightPluginGitInstallPolicy(params: {
return await preflightPluginGitInstallPolicyRuntime(params);
}
/** Evaluates shared install policy for skill-managed dependency installs. */
export async function evaluateSkillInstallPolicy(params: {
config?: OpenClawConfig;
installId: string;

View File

@@ -13,11 +13,13 @@ import {
} from "./interactive-state.js";
import type { PluginInteractiveHandlerRegistration } from "./types.js";
/** Registration result for plugin interactive namespace handlers. */
export type InteractiveRegistrationResult = {
ok: boolean;
error?: string;
};
/** Resolves a channel payload to a registered plugin interactive namespace handler. */
export function resolvePluginInteractiveNamespaceMatch(
channel: string,
data: string,
@@ -29,6 +31,7 @@ export function resolvePluginInteractiveNamespaceMatch(
});
}
/** Registers one plugin interactive namespace for a channel. */
export function registerPluginInteractiveHandler(
pluginId: string,
registration: PluginInteractiveHandlerRegistration,
@@ -59,14 +62,17 @@ export function registerPluginInteractiveHandler(
return { ok: true };
}
/** Clears all active plugin interactive handlers. */
export function clearPluginInteractiveHandlers(): void {
clearPluginInteractiveHandlersState();
}
/** Clears stored plugin interactive handler registrations. */
export function clearPluginInteractiveHandlerRegistrations(): void {
clearPluginInteractiveHandlerRegistrationsState();
}
/** Clears active interactive handlers owned by one plugin. */
export function clearPluginInteractiveHandlersForPlugin(pluginId: string): void {
const interactiveHandlers = getPluginInteractiveHandlersState();
for (const [key, value] of interactiveHandlers.entries()) {
@@ -76,10 +82,12 @@ export function clearPluginInteractiveHandlersForPlugin(pluginId: string): void
}
}
/** Lists active plugin interactive handlers. */
export function listPluginInteractiveHandlers(): RegisteredInteractiveHandler[] {
return Array.from(getPluginInteractiveHandlersState().values());
}
/** Restores active plugin interactive handlers from a saved registry snapshot. */
export function restorePluginInteractiveHandlers(
registrations: readonly RegisteredInteractiveHandler[],
): void {

View File

@@ -1,3 +1,4 @@
/** Plugin-local re-export of shared path safety helpers for plugin install/runtime code. */
export {
isNotFoundPathError,
hasNodeErrorCode,

View File

@@ -12,24 +12,28 @@ type RunProviderModelSelectedHook =
type ResolvePluginProviders = typeof import("./providers.runtime.js").resolvePluginProviders;
type ResolvePluginSetupProvider = typeof import("./setup-registry.js").resolvePluginSetupProvider;
/** Runtime wrapper for provider plugin wizard choice resolution. */
export function resolveProviderPluginChoice(
...args: Parameters<ResolveProviderPluginChoice>
): ReturnType<ResolveProviderPluginChoice> {
return resolveProviderPluginChoiceImpl(...args);
}
/** Runtime wrapper for provider model-selected hook dispatch. */
export function runProviderModelSelectedHook(
...args: Parameters<RunProviderModelSelectedHook>
): ReturnType<RunProviderModelSelectedHook> {
return runProviderModelSelectedHookImpl(...args);
}
/** Runtime wrapper for registered model provider discovery. */
export function resolvePluginProviders(
...args: Parameters<ResolvePluginProviders>
): ReturnType<ResolvePluginProviders> {
return resolvePluginProvidersImpl(...args);
}
/** Runtime wrapper for plugin setup-provider discovery. */
export function resolvePluginSetupProvider(
...args: Parameters<ResolvePluginSetupProvider>
): ReturnType<ResolvePluginSetupProvider> {

View File

@@ -1,6 +1,7 @@
import type { WizardPrompter } from "../wizard/prompts.js";
import type { SecretInputMode } from "./provider-auth-types.js";
/** Prompt copy overrides for provider secret input mode selection. */
export type SecretInputModePromptCopy = {
modeMessage?: string;
plaintextLabel?: string;
@@ -9,6 +10,7 @@ export type SecretInputModePromptCopy = {
refHint?: string;
};
/** Resolves provider secret input mode from explicit option or wizard selection. */
export async function resolveSecretInputModeForEnvSelection(params: {
prompter: Pick<WizardPrompter, "select">;
explicitMode?: SecretInputMode;

View File

@@ -1 +1,2 @@
/** Provider secret input modes: inline plaintext or external secret reference. */
export type SecretInputMode = "plaintext" | "ref"; // pragma: allowlist secret

View File

@@ -12,6 +12,7 @@ const CORE_BUILT_IN_MODEL_APIS = new Set([
"openai-responses",
]);
/** Returns the plugin API id that owns a provider config when it is not core built-in. */
export function resolveProviderConfigApiOwnerHint(params: {
provider: string;
config?: OpenClawConfig;

View File

@@ -12,6 +12,7 @@ export const PUBLIC_SURFACE_SOURCE_EXTENSIONS = [
".cjs",
] as const;
/** Normalizes a bundled public artifact subpath and rejects traversal/absolute paths. */
export function normalizeBundledPluginArtifactSubpath(artifactBasename: string): string {
if (
path.posix.isAbsolute(artifactBasename) ||
@@ -39,6 +40,7 @@ export function normalizeBundledPluginArtifactSubpath(artifactBasename: string):
return normalized;
}
/** Normalizes a bundled plugin directory name and rejects path-like values. */
export function normalizeBundledPluginDirName(dirName: string): string {
const normalized = dirName.trim();
if (
@@ -54,6 +56,7 @@ export function normalizeBundledPluginDirName(dirName: string): string {
return normalized;
}
/** Resolves a source-tree public surface artifact path for bundled plugin development. */
export function resolveBundledPluginSourcePublicSurfacePath(params: {
sourceRoot: string;
dirName: string;
@@ -153,6 +156,7 @@ function resolvePublicSurfaceFromBundledDir(params: {
);
}
/** Resolves a bundled plugin public surface artifact across source, dist, and package layouts. */
export function resolveBundledPluginPublicSurfacePath(params: {
rootDir: string;
dirName: string;