docs: document agent runtime tool policies

This commit is contained in:
Peter Steinberger
2026-06-03 22:03:28 -04:00
parent d26cef4249
commit d4867ec20d
5 changed files with 36 additions and 0 deletions
@@ -1,5 +1,8 @@
import { normalizeOptionalLowercaseString } from "@openclaw/normalization-core/string-coerce";
// Message providers can narrow the tool surface when a channel cannot safely
// render or execute a tool class. The policy is name-based because channel
// delivery happens after tools are already assembled.
const TOOL_DENY_BY_MESSAGE_PROVIDER: Readonly<Record<string, readonly string[]>> = {
"discord-voice": ["tts"],
voice: ["tts"],
@@ -9,6 +12,7 @@ const TOOL_ALLOW_BY_MESSAGE_PROVIDER: Readonly<Record<string, readonly string[]>
node: ["canvas", "image", "pdf", "tts", "web_fetch", "web_search"],
};
/** Filters tool names by the active message-provider allow/deny policy. */
export function filterToolNamesByMessageProvider(
toolNames: readonly string[],
messageProvider?: string,
@@ -30,6 +34,7 @@ export function filterToolNamesByMessageProvider(
return toolNames.filter((toolName) => !deniedSet.has(toolName));
}
/** Applies message-provider filtering while preserving duplicate tool entries. */
export function filterToolsByMessageProvider<TTool extends { name: string }>(
tools: readonly TTool[],
messageProvider?: string,
@@ -43,6 +48,8 @@ export function filterToolsByMessageProvider<TTool extends { name: string }>(
remainingCounts.set(toolName, (remainingCounts.get(toolName) ?? 0) + 1);
}
return tools.filter((tool) => {
// Counted matching preserves the original order and duplicate instances
// after name-level policy filtering.
const remaining = remainingCounts.get(tool.name) ?? 0;
if (remaining <= 0) {
return false;
+6
View File
@@ -5,10 +5,14 @@ import {
import { normalizeUniqueSingleOrTrimmedStringList } from "@openclaw/normalization-core/string-normalization";
import type { OpenClawConfig } from "../config/types.openclaw.js";
// Scope refs feed provider discovery and model catalog lookups. Keep the
// ordering deterministic so prompt/cache inputs do not drift across runs.
function dedupeCatalogScopeRefs(values: Array<string | undefined>): string[] {
return normalizeUniqueSingleOrTrimmedStringList(values);
}
// Accept provider/model refs in addition to separate provider fields so aliases
// and user-entered model refs discover the owning provider catalog.
function providerFromModelRef(value: string | undefined): string | undefined {
const trimmed = value?.trim();
if (!trimmed) {
@@ -33,6 +37,7 @@ function providerConfigDeclaresModel(
);
}
/** Resolves provider/model refs used to scope model catalog discovery. */
export function resolveModelCatalogScope(params: {
cfg?: OpenClawConfig;
provider: string;
@@ -50,6 +55,7 @@ export function resolveModelCatalogScope(params: {
};
}
/** Extracts provider ids from resolved catalog scope refs for discovery calls. */
export function resolveProviderDiscoveryProviderIdsForCatalogScope(params: {
providerRefs?: readonly string[];
modelRefs?: readonly string[];
+7
View File
@@ -9,10 +9,14 @@ import {
import type { AgentRuntimeAuthPlan } from "./types.js";
const CODEX_HARNESS_AUTH_PROVIDER = "openai";
// Empty metadata disables plugin alias lookups without changing the downstream
// resolver contract, matching the "plugins disabled" runtime-plan state.
const EMPTY_PROVIDER_AUTH_ALIAS_METADATA = {
plugins: [],
} satisfies NonNullable<ProviderAuthAliasLookupParams["metadataSnapshot"]>;
// Harness runtimes that authenticate through a different provider must be
// resolved before session auth profiles can be forwarded.
function resolveHarnessAuthProvider(params: {
harnessId?: string;
harnessRuntime?: string;
@@ -22,6 +26,7 @@ function resolveHarnessAuthProvider(params: {
return harnessId === "codex" || runtime === "codex" ? CODEX_HARNESS_AUTH_PROVIDER : undefined;
}
/** Builds the auth forwarding plan for one resolved agent runtime. */
export function buildAgentRuntimeAuthPlan(params: {
provider: string;
authProfileProvider?: string;
@@ -64,6 +69,8 @@ export function buildAgentRuntimeAuthPlan(params: {
!harnessProviderForAuth && providerForAuth === authProfileProviderForAuth;
const canForwardProfile = providerCanForwardProfile || harnessCanForwardProfile;
// Forward only when the selected provider/harness resolves to the same auth
// owner as the stored session profile; otherwise the runtime must choose auth.
return {
providerForAuth,
authProfileProviderForAuth,
+9
View File
@@ -15,6 +15,8 @@ import {
} from "../tool-schema-projection.js";
import type { AgentRuntimePlan } from "./types.js";
// Shared by normalization and diagnostics so the same provider/model/runtime
// context reaches both runtime-plan hooks and provider fallback code.
type AgentRuntimeToolPolicyParams<TSchemaType extends TSchema = TSchema, TResult = unknown> = {
runtimePlan?: AgentRuntimePlan;
tools: AgentTool<TSchemaType, TResult>[];
@@ -33,6 +35,7 @@ type AgentRuntimeToolPolicyParams<TSchemaType extends TSchema = TSchema, TResult
) => void;
};
/** Builds the provider/runtime context passed into runtime-plan tool hooks. */
function runtimePlanToolContext(params: {
workspaceDir?: string;
modelApi?: string | null;
@@ -45,6 +48,8 @@ function runtimePlanToolContext(params: {
};
}
// Normalizers may return cloned tool definitions. Copy plugin/channel metadata
// so downstream inventory, delivery, and attribution still know the owner.
function copyRuntimeToolMetadata(source: AgentTool, target: AgentTool): void {
if (source === target) {
return;
@@ -53,6 +58,8 @@ function copyRuntimeToolMetadata(source: AgentTool, target: AgentTool): void {
copyChannelAgentToolMeta(source as never, target as never);
}
// Duplicate names cannot be matched by map lookup alone, so same-index matches
// take precedence and unique-name fallback covers cloned arrays.
function preserveRuntimeToolMetadata<TSchemaType extends TSchema = TSchema, TResult = unknown>(
sourceTools: AgentTool<TSchemaType, TResult>[],
normalizedTools: AgentTool<TSchemaType, TResult>[],
@@ -81,6 +88,7 @@ function preserveRuntimeToolMetadata<TSchemaType extends TSchema = TSchema, TRes
return normalizedTools;
}
/** Normalizes tool schemas through a runtime plan or provider fallback policy. */
export function normalizeAgentRuntimeTools<
TSchemaType extends TSchema = TSchema,
TResult = unknown,
@@ -115,6 +123,7 @@ export function normalizeAgentRuntimeTools<
return preserveRuntimeToolMetadata(normalizableTools, normalizedTools);
}
/** Emits runtime-plan or provider fallback diagnostics for normalized tools. */
export function logAgentRuntimeToolDiagnostics(params: AgentRuntimeToolPolicyParams): void {
const planContext = runtimePlanToolContext(params);
if (params.runtimePlan) {
@@ -20,6 +20,8 @@ import type { AnyAgentTool } from "./tools/common.js";
const BUNDLE_MCP_PLUGIN_ID = "bundle-mcp";
// MCP tools often expose low-signal labels identical to their names. Prefer the
// display resolver in that case so inventory output stays readable.
function resolveMcpToolLabel(tool: AnyAgentTool): string {
const rawLabel = normalizeOptionalString(tool.label) ?? "";
if (
@@ -42,6 +44,8 @@ function summarizeToolDescription(tool: AnyAgentTool): string {
});
}
// Runtime schema diagnostics become operator-facing notices on the effective
// inventory screen instead of silently hiding quarantined MCP tools.
function buildMcpUnsupportedToolSchemaNotice(
diagnostic: RuntimeToolSchemaDiagnostic,
): EffectiveToolInventoryNotice {
@@ -52,6 +56,8 @@ function buildMcpUnsupportedToolSchemaNotice(
};
}
// Duplicate labels are ambiguous in inventory UIs; add the plugin/id only where
// needed so unique entries keep their concise display names.
function disambiguateLabels(entries: EffectiveToolInventoryEntry[]): EffectiveToolInventoryEntry[] {
const counts = new Map<string, number>();
for (const entry of entries) {
@@ -85,6 +91,7 @@ function buildMcpToolInventoryEntries(
);
}
/** Builds the runtime-compatible MCP tool inventory and quarantine notices. */
export function buildRuntimeCompatibleMcpToolInventory(params: {
tools: readonly AnyAgentTool[];
cfg: OpenClawConfig;