docs: document agent tool policy helpers

This commit is contained in:
Peter Steinberger
2026-06-03 22:19:38 -04:00
parent fc5cb461c9
commit 2ff2ed4099
7 changed files with 44 additions and 0 deletions
+4
View File
@@ -17,6 +17,7 @@ import {
loadAuthProfileStoreForSecretsRuntime,
} from "./auth-profiles/store.js";
/** Options for discovering credentials without prompting for secret material. */
export type DiscoverAuthStorageOptions = {
externalCli?: ExternalCliAuthDiscovery;
readOnly?: boolean;
@@ -25,6 +26,7 @@ export type DiscoverAuthStorageOptions = {
syntheticAuthProviderRefs?: Iterable<string>;
} & AgentDiscoveryAuthLookupOptions;
/** Resolves agent credentials from auth profiles, env, and synthetic auth hooks. */
export function resolveAgentCredentialsForDiscovery(
agentDir: string,
options?: DiscoverAuthStorageOptions,
@@ -62,6 +64,8 @@ export function resolveAgentCredentialsForDiscovery(
if (credentials[provider]) {
continue;
}
// Synthetic auth is a plugin/runtime fallback. Only fill empty providers so
// persisted profiles and env-backed credentials remain authoritative.
const resolved = resolveProviderSyntheticAuthWithPlugin({
provider,
context: {
@@ -1,6 +1,7 @@
import type { DeliveryContext } from "../utils/delivery-context.types.js";
import type { AnyAgentTool } from "./tools/common.js";
/** Applies delivery-context defaults to plugin tools before final tool policy. */
export function applyPluginToolDeliveryDefaults(params: {
tools: AnyAgentTool[];
deliveryContext?: DeliveryContext;
+3
View File
@@ -5,6 +5,7 @@ import { ensureCustomApiRegistered } from "./custom-api-registry.js";
import { createTransportAwareStreamFnForModel } from "./provider-transport-stream.js";
import type { StreamFn } from "./runtime/index.js";
/** Resolves and registers the stream function for a provider-backed model. */
export function registerProviderStreamForModel<TApi extends Api>(params: {
model: Model<TApi>;
cfg?: OpenClawConfig;
@@ -38,6 +39,8 @@ export function registerProviderStreamForModel<TApi extends Api>(params: {
if (!streamFn) {
return undefined;
}
// Register custom APIs only after a concrete stream exists, so later callers
// can route by model.api without reloading provider runtime hooks.
ensureCustomApiRegistered(params.model.api, streamFn);
return streamFn;
}
@@ -13,6 +13,7 @@ import type { SubagentRunRecord } from "./subagent-registry.types.js";
const log = createSubsystemLogger("agents/subagent-registry-completion");
/** Compares subagent run outcomes, treating missing timing as compatible. */
export function runOutcomesEqual(
a: SubagentRunOutcome | undefined,
b: SubagentRunOutcome | undefined,
@@ -37,6 +38,7 @@ export function runOutcomesEqual(
return a.startedAt === b.startedAt && a.endedAt === b.endedAt && a.elapsedMs === b.elapsedMs;
}
/** Returns true when an outcome carries timing fields. */
export function runOutcomeHasTiming(outcome: SubagentRunOutcome | undefined): boolean {
return (
Number.isFinite(outcome?.startedAt) ||
@@ -45,6 +47,7 @@ export function runOutcomeHasTiming(outcome: SubagentRunOutcome | undefined): bo
);
}
/** Returns true when a run outcome update should replace current state. */
export function shouldUpdateRunOutcome(
current: SubagentRunOutcome | undefined,
next: SubagentRunOutcome | undefined,
@@ -54,6 +57,7 @@ export function shouldUpdateRunOutcome(
);
}
/** Maps registry run outcome to lifecycle event outcome. */
export function resolveLifecycleOutcomeFromRunOutcome(
outcome: SubagentRunOutcome | undefined,
): SubagentLifecycleEndedOutcome {
@@ -66,6 +70,7 @@ export function resolveLifecycleOutcomeFromRunOutcome(
return SUBAGENT_ENDED_OUTCOME_OK;
}
/** Emits the subagent_ended hook once per completed run. */
export async function emitSubagentEndedHookOnce(params: {
entry: SubagentRunRecord;
reason: SubagentLifecycleEndedReason;
@@ -87,6 +92,8 @@ export async function emitSubagentEndedHookOnce(params: {
return false;
}
// In-flight guard prevents concurrent completion paths from double-emitting
// the hook before endedHookEmittedAt is persisted.
params.inFlightRunIds.add(runId);
try {
const hookRunner = getGlobalHookRunner();
@@ -4,6 +4,9 @@ import { subagentRuns } from "./subagent-registry-memory.js";
import { getSubagentRunsSnapshotForRead } from "./subagent-registry-state.js";
import type { SubagentRunRecord } from "./subagent-registry.types.js";
// Session maintenance must preserve child sessions that are active, awaiting a
// completion announce, or suspended for later delivery. Completed cleanup rows
// no longer need to pin their session keys.
function isCleanupCompleteForMaintenance(entry: SubagentRunRecord): boolean {
return typeof entry.cleanupCompletedAt === "number";
}
@@ -32,6 +35,7 @@ function shouldPreserveForMaintenance(entry: SubagentRunRecord): boolean {
);
}
/** Lists child session keys protected from session-store maintenance pruning. */
export function listSessionMaintenanceProtectedSubagentSessionKeys(): string[] {
const keys = new Set<string>();
for (const entry of getSubagentRunsSnapshotForRead(subagentRuns).values()) {
+10
View File
@@ -10,8 +10,10 @@ import {
UPDATE_PLAN_TOOL_DISPLAY_SUMMARY,
} from "./tool-description-presets.js";
/** Built-in tool profile ids exposed in config and UI. */
export type ToolProfileId = "minimal" | "coding" | "messaging" | "full";
/** Allow/deny policy generated from a built-in tool profile. */
type ToolProfilePolicy = {
allow?: string[];
deny?: string[];
@@ -36,6 +38,8 @@ type CoreToolDefinition = {
includeInOpenClawGroup?: boolean;
};
// The core tool catalog drives profile defaults, UI grouping, and group:openclaw
// expansion. Keep ids normalized because policy matching is string-based.
const CORE_TOOL_SECTION_ORDER: Array<{ id: string; label: string }> = [
{ id: "fs", label: "Files" },
{ id: "runtime", label: "Runtime" },
@@ -385,8 +389,10 @@ function buildCoreToolGroupMap() {
};
}
/** Built-in core tool groups keyed by group id. */
export const CORE_TOOL_GROUPS = buildCoreToolGroupMap();
/** Profile options shown in model/tool configuration UIs. */
export const PROFILE_OPTIONS = [
{ id: "minimal", label: "Minimal" },
{ id: "coding", label: "Coding" },
@@ -394,6 +400,7 @@ export const PROFILE_OPTIONS = [
{ id: "full", label: "Full" },
] as const;
/** Resolves the allow/deny policy for a built-in tool profile. */
export function resolveCoreToolProfilePolicy(profile?: string): ToolProfilePolicy | undefined {
if (!profile) {
return undefined;
@@ -411,6 +418,7 @@ export function resolveCoreToolProfilePolicy(profile?: string): ToolProfilePolic
};
}
/** Lists core tools grouped into UI sections. */
export function listCoreToolSections(): CoreToolSection[] {
return CORE_TOOL_SECTION_ORDER.map((section) => ({
id: section.id,
@@ -423,6 +431,7 @@ export function listCoreToolSections(): CoreToolSection[] {
})).filter((section) => section.tools.length > 0);
}
/** Lists built-in profile ids that include a core tool. */
export function resolveCoreToolProfiles(toolId: string): ToolProfileId[] {
const tool = CORE_TOOL_BY_ID.get(toolId);
if (!tool) {
@@ -431,6 +440,7 @@ export function resolveCoreToolProfiles(toolId: string): ToolProfileId[] {
return [...tool.profiles];
}
/** Returns true when a tool id is a known core tool. */
export function isKnownCoreToolId(toolId: string): boolean {
return CORE_TOOL_BY_ID.has(toolId);
}
+15
View File
@@ -12,25 +12,30 @@ export {
} from "./tool-policy-shared.js";
export type { ToolProfileId } from "./tool-policy-shared.js";
/** Tool allow/deny policy shape accepted by agent and sandbox config. */
export type ToolPolicyLike = {
allow?: string[];
deny?: string[];
[IMPLICIT_ALLOW_ALL_FROM_ALSO_ALLOW]?: true;
};
/** Plugin-owned tool group expansion state. */
export type PluginToolGroups = {
all: string[];
byPlugin: Map<string, string[]>;
};
/** Analysis of an allowlist after matching core and plugin tool ids. */
export type AllowlistResolution = {
policy: ToolPolicyLike | undefined;
unknownAllowlist: string[];
pluginOnlyAllowlist: boolean;
};
/** Synthetic allowlist entry that means "use default plugin tools". */
export const DEFAULT_PLUGIN_TOOLS_ALLOWLIST_ENTRY = "__openclaw_default_plugin_tools__";
/** Returns true when an allow policy is narrower than all/default plugin tools. */
export function hasRestrictiveAllowPolicy(policy?: { allow?: string[] }): boolean {
return (
Array.isArray(policy?.allow) &&
@@ -45,6 +50,7 @@ export function hasRestrictiveAllowPolicy(policy?: { allow?: string[] }): boolea
);
}
/** Replaces an allowlist with the normalized names of an effective tool array. */
export function replaceWithEffectiveToolAllowlist(
target: string[],
tools: Array<{ name: string }>,
@@ -61,6 +67,7 @@ export function replaceWithEffectiveToolAllowlist(
}
}
/** Collects explicit allow entries from layered policies. */
export function collectExplicitAllowlist(policies: Array<ToolPolicyLike | undefined>): string[] {
const entries: string[] = [];
for (const policy of policies) {
@@ -73,6 +80,8 @@ export function collectExplicitAllowlist(policies: Array<ToolPolicyLike | undefi
}
const trimmed = value.trim();
if (trimmed === "*" && policy[IMPLICIT_ALLOW_ALL_FROM_ALSO_ALLOW] === true) {
// alsoAllow implicitly injects "*" for sandbox compatibility; do not
// report that implicit wildcard as an explicit operator allow entry.
continue;
}
if (trimmed) {
@@ -86,6 +95,7 @@ export function collectExplicitAllowlist(policies: Array<ToolPolicyLike | undefi
return uniqueStrings(entries);
}
/** Collects explicit deny entries from layered policies. */
export function collectExplicitDenylist(policies: Array<ToolPolicyLike | undefined>): string[] {
const entries: string[] = [];
for (const policy of policies) {
@@ -105,6 +115,7 @@ export function collectExplicitDenylist(policies: Array<ToolPolicyLike | undefin
return entries;
}
/** Builds plugin tool groups from tool metadata. */
export function buildPluginToolGroups<T extends { name: string }>(params: {
tools: T[];
toolMeta: (tool: T) => { pluginId: string } | undefined;
@@ -129,6 +140,7 @@ export function buildPluginToolGroups<T extends { name: string }>(params: {
return { all, byPlugin };
}
/** Expands group:plugins and plugin-id entries into concrete plugin tool names. */
export function expandPluginGroups(
list: string[] | undefined,
groups: PluginToolGroups,
@@ -157,6 +169,7 @@ export function expandPluginGroups(
return uniqueStrings(expanded);
}
/** Expands plugin groups in a policy while preserving undefined policies. */
export function expandPolicyWithPluginGroups(
policy: ToolPolicyLike | undefined,
groups: PluginToolGroups,
@@ -170,6 +183,7 @@ export function expandPolicyWithPluginGroups(
};
}
/** Classifies allowlists as core, plugin-only, or unknown for diagnostics. */
export function analyzeAllowlistByToolType(
policy: ToolPolicyLike | undefined,
groups: PluginToolGroups,
@@ -210,6 +224,7 @@ export function analyzeAllowlistByToolType(
};
}
/** Merges alsoAllow entries into an existing allow policy. */
export function mergeAlsoAllowPolicy<TPolicy extends { allow?: string[] }>(
policy: TPolicy | undefined,
alsoAllow?: string[],