mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-03 04:11:36 +00:00
* fix(plugins): stop treating a partial active registry as authoritative for web providers An active plugin registry with some web providers used to win even when a manifest-declared candidate (e.g. an npm-installed Brave plugin with BRAVE_API_KEY set) was absent from it, so env-var auto-detect could never see installed external search providers. Delegate to the coverage-checked resolvePluginWebProviders path, which reuses the active registry only when it covers every declared candidate. * feat(agents): require explicit overwrite before replacing pre-existing files in the write tool Blind writes to an existing path silently destroyed user content (observed as WildClawBench safety-task data loss with weaker models). The write tool now refuses to replace an existing differing file unless the call passes overwrite:true or this tool instance already wrote that path, keeping iterate-loops friction-free while making destructive replacement an explicit model decision. * docs(templates): make a concrete first-message task outrank the BOOTSTRAP.md birth sequence A fresh workspace's birth ritual hijacked substantive first messages: agents introduced themselves and asked for a name instead of doing the requested work (worst with weaker models, which follow the ritual literally). State task precedence explicitly at the top of the template. * fix(agents): lead the write overwrite guard error with the safe protocol Weak models retried immediately with overwrite:true when the flag came first in the message. Order the guidance read -> rename -> overwrite-as-last-resort so the destructive path requires an explicit judgment call. * refactor(agents): replace the write overwrite flag with a confirm-by-resend gate The overwrite:true escape hatch let weak models bulldoze reflexively and grew the tool schema. The first write to a differing pre-existing file now returns that file's content (head-clipped) and a resend of the identical write, issued after the warning, against byte-identical existing content confirms the replacement. Fingerprints hash raw bytes; oversized files fall back to size+mtime (named tradeoff); missing metadata fails closed. * revert(agents): restore plain overwrite semantics in the write tool The overwrite gate (flag, then confirm-by-resend) was overfit to one WildClawBench safety rubric: the guarded model still chose to overwrite and still scored zero, while every real overwrite in normal sessions paid a round-trip. Write means write; peers (Pi, Hermes) agree. * test(agents): drop the stale overwrite arg from the write output-contract test
281 lines
9.4 KiB
TypeScript
281 lines
9.4 KiB
TypeScript
// Shares web provider runtime helpers across plugin-owned providers.
|
|
import { withActivatedPluginIds } from "./activation-context.js";
|
|
import { getLoadedRuntimePluginRegistry } from "./active-runtime-registry.js";
|
|
import { isPluginRegistryLoadInFlight, loadOpenClawPlugins } from "./loader.js";
|
|
import type { PluginLoadOptions } from "./loader.js";
|
|
import type { PluginManifestRecord } from "./manifest-registry.js";
|
|
import { hasExplicitPluginIdScope, normalizePluginIdScope } from "./plugin-scope.js";
|
|
import type { PluginRegistry } from "./registry.js";
|
|
import { getActivePluginRegistryWorkspaceDir } from "./runtime.js";
|
|
import {
|
|
buildPluginRuntimeLoadOptionsFromValues,
|
|
createPluginRuntimeLoaderLogger,
|
|
} from "./runtime/load-context.js";
|
|
|
|
/** Shared options for resolving plugin-backed web providers. */
|
|
type ResolvePluginWebProvidersParams = {
|
|
config?: PluginLoadOptions["config"];
|
|
workspaceDir?: string;
|
|
env?: PluginLoadOptions["env"];
|
|
onlyPluginIds?: readonly string[];
|
|
activate?: boolean;
|
|
cache?: boolean;
|
|
mode?: "runtime" | "setup";
|
|
origin?: PluginManifestRecord["origin"];
|
|
sandboxed?: boolean;
|
|
};
|
|
|
|
type ResolveWebProviderRuntimeDeps<TEntry> = {
|
|
resolveBundledResolutionConfig: (params: {
|
|
config?: PluginLoadOptions["config"];
|
|
workspaceDir?: string;
|
|
env?: PluginLoadOptions["env"];
|
|
}) => {
|
|
config: PluginLoadOptions["config"];
|
|
activationSourceConfig?: PluginLoadOptions["config"];
|
|
autoEnabledReasons: Record<string, string[]>;
|
|
};
|
|
resolveCandidatePluginIds: (params: {
|
|
config?: PluginLoadOptions["config"];
|
|
workspaceDir?: string;
|
|
env?: PluginLoadOptions["env"];
|
|
onlyPluginIds?: readonly string[];
|
|
origin?: PluginManifestRecord["origin"];
|
|
sandboxed?: boolean;
|
|
}) => string[] | undefined;
|
|
mapRegistryProviders: (params: {
|
|
registry: PluginRegistry;
|
|
onlyPluginIds?: readonly string[];
|
|
}) => TEntry[];
|
|
resolveBundledPublicArtifactProviders?: (params: {
|
|
config?: PluginLoadOptions["config"];
|
|
workspaceDir?: string;
|
|
env?: PluginLoadOptions["env"];
|
|
onlyPluginIds?: readonly string[];
|
|
}) => TEntry[] | null;
|
|
resolveBundledRuntimeArtifactProviders?: (params: {
|
|
config?: PluginLoadOptions["config"];
|
|
workspaceDir?: string;
|
|
env?: PluginLoadOptions["env"];
|
|
onlyPluginIds: readonly string[];
|
|
}) => TEntry[] | null;
|
|
};
|
|
|
|
type WebProviderRuntimeContext = {
|
|
env: NonNullable<PluginLoadOptions["env"]>;
|
|
workspaceDir?: string;
|
|
config: PluginLoadOptions["config"];
|
|
activationSourceConfig?: PluginLoadOptions["config"];
|
|
autoEnabledReasons: Record<string, string[]>;
|
|
loadPluginIds?: string[];
|
|
onlyPluginIds?: string[];
|
|
};
|
|
|
|
type RuntimeRegistryWebProviderResolution<TEntry> = {
|
|
providers: TEntry[];
|
|
shouldReturn: boolean;
|
|
};
|
|
|
|
function resolveWebProviderRuntimeContext<TEntry>(
|
|
params: ResolvePluginWebProvidersParams,
|
|
deps: ResolveWebProviderRuntimeDeps<TEntry>,
|
|
): WebProviderRuntimeContext {
|
|
const env = params.env ?? process.env;
|
|
const workspaceDir = params.workspaceDir ?? getActivePluginRegistryWorkspaceDir();
|
|
const shouldFilterProviders =
|
|
params.config !== undefined ||
|
|
params.onlyPluginIds !== undefined ||
|
|
params.origin !== undefined ||
|
|
params.sandboxed === true;
|
|
const { config, activationSourceConfig, autoEnabledReasons } =
|
|
deps.resolveBundledResolutionConfig({
|
|
...params,
|
|
workspaceDir,
|
|
env,
|
|
});
|
|
const candidatePluginIds = normalizePluginIdScope(
|
|
deps.resolveCandidatePluginIds({
|
|
config,
|
|
workspaceDir,
|
|
env,
|
|
onlyPluginIds: params.onlyPluginIds,
|
|
origin: params.origin,
|
|
sandboxed: params.sandboxed,
|
|
}),
|
|
);
|
|
return {
|
|
activationSourceConfig,
|
|
autoEnabledReasons,
|
|
config,
|
|
env,
|
|
loadPluginIds: candidatePluginIds,
|
|
onlyPluginIds: shouldFilterProviders ? candidatePluginIds : undefined,
|
|
workspaceDir,
|
|
};
|
|
}
|
|
|
|
function resolveWebProviderLoadOptions(
|
|
context: WebProviderRuntimeContext,
|
|
params: ResolvePluginWebProvidersParams,
|
|
) {
|
|
return buildPluginRuntimeLoadOptionsFromValues(
|
|
{
|
|
env: context.env,
|
|
config: context.config,
|
|
activationSourceConfig: context.activationSourceConfig,
|
|
autoEnabledReasons: context.autoEnabledReasons,
|
|
workspaceDir: context.workspaceDir,
|
|
logger: createPluginRuntimeLoaderLogger(),
|
|
},
|
|
{
|
|
cache: params.cache ?? true,
|
|
activate: params.activate ?? false,
|
|
...(hasExplicitPluginIdScope(context.loadPluginIds)
|
|
? { onlyPluginIds: context.loadPluginIds }
|
|
: {}),
|
|
},
|
|
);
|
|
}
|
|
|
|
function resolveRuntimeRegistryWebProviders<TEntry>(params: {
|
|
hasExplicitEmptyScope: boolean;
|
|
mapRegistryProviders: ResolveWebProviderRuntimeDeps<TEntry>["mapRegistryProviders"];
|
|
onlyPluginIds?: readonly string[];
|
|
registry: PluginRegistry | undefined;
|
|
}): RuntimeRegistryWebProviderResolution<TEntry> | undefined {
|
|
if (!params.registry) {
|
|
return undefined;
|
|
}
|
|
const providers = params.mapRegistryProviders({
|
|
registry: params.registry,
|
|
onlyPluginIds: params.onlyPluginIds,
|
|
});
|
|
return {
|
|
providers,
|
|
shouldReturn: providers.length > 0 || params.hasExplicitEmptyScope,
|
|
};
|
|
}
|
|
|
|
/** Resolves plugin web providers from setup, active runtime, or a scoped load. */
|
|
export function resolvePluginWebProviders<TEntry>(
|
|
params: ResolvePluginWebProvidersParams,
|
|
deps: ResolveWebProviderRuntimeDeps<TEntry>,
|
|
): TEntry[] {
|
|
const env = params.env ?? process.env;
|
|
const workspaceDir = params.workspaceDir ?? getActivePluginRegistryWorkspaceDir();
|
|
if (params.mode === "setup") {
|
|
const pluginIds =
|
|
deps.resolveCandidatePluginIds({
|
|
config: params.config,
|
|
workspaceDir,
|
|
env,
|
|
onlyPluginIds: params.onlyPluginIds,
|
|
origin: params.origin,
|
|
sandboxed: params.sandboxed,
|
|
}) ?? [];
|
|
if (pluginIds.length === 0) {
|
|
return [];
|
|
}
|
|
if (params.activate !== true) {
|
|
const bundledArtifactProviders = deps.resolveBundledPublicArtifactProviders?.({
|
|
config: params.config,
|
|
workspaceDir,
|
|
env,
|
|
onlyPluginIds: pluginIds,
|
|
});
|
|
if (bundledArtifactProviders) {
|
|
return bundledArtifactProviders;
|
|
}
|
|
}
|
|
const registry = loadOpenClawPlugins(
|
|
buildPluginRuntimeLoadOptionsFromValues(
|
|
{
|
|
config: withActivatedPluginIds({
|
|
config: params.config,
|
|
pluginIds,
|
|
}),
|
|
activationSourceConfig: params.config,
|
|
autoEnabledReasons: {},
|
|
workspaceDir,
|
|
env,
|
|
logger: createPluginRuntimeLoaderLogger(),
|
|
},
|
|
{
|
|
onlyPluginIds: pluginIds,
|
|
cache: params.cache ?? true,
|
|
activate: params.activate ?? false,
|
|
},
|
|
),
|
|
);
|
|
return deps.mapRegistryProviders({ registry, onlyPluginIds: pluginIds });
|
|
}
|
|
|
|
const context = resolveWebProviderRuntimeContext(params, deps);
|
|
const loadOptions = resolveWebProviderLoadOptions(context, params);
|
|
const compatible = getLoadedRuntimePluginRegistry({
|
|
env: context.env,
|
|
loadOptions,
|
|
workspaceDir: context.workspaceDir,
|
|
requiredPluginIds: context.loadPluginIds,
|
|
});
|
|
const scopedPluginIds = context.onlyPluginIds;
|
|
const hasExplicitEmptyScope = scopedPluginIds !== undefined && scopedPluginIds.length === 0;
|
|
const compatibleProviders = resolveRuntimeRegistryWebProviders({
|
|
hasExplicitEmptyScope,
|
|
mapRegistryProviders: deps.mapRegistryProviders,
|
|
onlyPluginIds: context.onlyPluginIds,
|
|
registry: compatible,
|
|
});
|
|
if (compatibleProviders?.shouldReturn) {
|
|
return compatibleProviders.providers;
|
|
}
|
|
if (compatibleProviders) {
|
|
// The active gateway plugin registry may be otherwise compatible with this
|
|
// config while contributing zero web providers (for example when channels,
|
|
// memory, harnesses, and sidecars are loaded but Brave/web providers are
|
|
// not). Do not treat that empty active registry as authoritative: fall
|
|
// through to a scoped provider load below so first-class assistant tools
|
|
// still see the configured provider.
|
|
}
|
|
if (isPluginRegistryLoadInFlight(loadOptions)) {
|
|
return [];
|
|
}
|
|
if (hasExplicitEmptyScope) {
|
|
return [];
|
|
}
|
|
if (
|
|
params.activate !== true &&
|
|
context.loadPluginIds &&
|
|
deps.resolveBundledRuntimeArtifactProviders
|
|
) {
|
|
const bundledArtifactProviders = deps.resolveBundledRuntimeArtifactProviders({
|
|
config: context.config,
|
|
workspaceDir: context.workspaceDir,
|
|
env: context.env,
|
|
onlyPluginIds: context.loadPluginIds,
|
|
});
|
|
if (bundledArtifactProviders) {
|
|
return bundledArtifactProviders;
|
|
}
|
|
}
|
|
const registry = loadOpenClawPlugins(loadOptions);
|
|
return deps.mapRegistryProviders({
|
|
registry,
|
|
onlyPluginIds: context.onlyPluginIds,
|
|
});
|
|
}
|
|
|
|
/** Resolves web providers from the active runtime registry before falling back to plugin loading. */
|
|
export function resolveRuntimeWebProviders<TEntry>(
|
|
params: Omit<ResolvePluginWebProvidersParams, "activate" | "cache" | "mode">,
|
|
deps: ResolveWebProviderRuntimeDeps<TEntry>,
|
|
): TEntry[] {
|
|
// Do not treat the active registry's provider set as authoritative here: it can
|
|
// be non-empty while still missing manifest-declared candidates that never load
|
|
// at startup (for example an npm-installed Brave plugin with BRAVE_API_KEY set,
|
|
// whose manifest uses activation.onStartup=false). resolvePluginWebProviders
|
|
// reuses the active registry only when it covers every declared candidate, and
|
|
// otherwise runs the same scoped load the explicitly-configured path uses.
|
|
return resolvePluginWebProviders(params, deps);
|
|
}
|