fix(plugins): find installed external web-search providers on fresh installs (#114327)

* 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
This commit is contained in:
Peter Steinberger
2026-07-27 01:32:04 -04:00
committed by GitHub
parent 4bc1fd314b
commit fa28e9be8d
3 changed files with 52 additions and 16 deletions
@@ -455,6 +455,46 @@ describe("web-provider-runtime-shared", () => {
expect(mocks.loadOpenClawPlugins).not.toHaveBeenCalled();
});
it("does not treat an active registry missing declared candidates as authoritative", () => {
// Regression: an active registry with SOME web providers used to win even when a
// manifest-declared candidate (npm-installed Brave with BRAVE_API_KEY set) was
// absent from it, so env-var auto-detect could never see the installed provider.
const activeRegistry = { source: "active" };
const scopedRegistry = { source: "scoped" };
const mapRegistryProviders = vi.fn(({ registry }) =>
registry === scopedRegistry ? ["brave", "grok"] : ["grok"],
);
mocks.getLoadedRuntimePluginRegistry.mockImplementation((args: unknown) => {
const requiredPluginIds = (args as { requiredPluginIds?: readonly string[] })
?.requiredPluginIds;
// Simulate active-registry coverage: brave never loaded at startup.
if (requiredPluginIds?.includes("brave")) {
return undefined;
}
return activeRegistry as never;
});
mocks.loadOpenClawPlugins.mockReturnValue(scopedRegistry as never);
const result = resolveRuntimeWebProviders(
{
config: {},
env: { BRAVE_API_KEY: "key" } as never,
},
{
resolveBundledResolutionConfig: () => ({
config: {},
activationSourceConfig: {},
autoEnabledReasons: {},
}),
resolveCandidatePluginIds: () => ["brave", "xai"],
mapRegistryProviders,
},
);
expect(result).toEqual(["brave", "grok"]);
expect(mocks.loadOpenClawPlugins).toHaveBeenCalledTimes(1);
});
it("falls back when the direct runtime registry has no web providers", () => {
const activeRegistry = { source: "active" };
const fallbackRegistry = { source: "fallback" };
+6 -16
View File
@@ -270,21 +270,11 @@ export function resolveRuntimeWebProviders<TEntry>(
params: Omit<ResolvePluginWebProvidersParams, "activate" | "cache" | "mode">,
deps: ResolveWebProviderRuntimeDeps<TEntry>,
): TEntry[] {
const runtimeRegistry = getLoadedRuntimePluginRegistry({
env: params.env,
workspaceDir: params.workspaceDir,
requiredPluginIds: params.onlyPluginIds,
});
const hasExplicitEmptyScope =
params.onlyPluginIds !== undefined && params.onlyPluginIds.length === 0;
const runtimeProviders = resolveRuntimeRegistryWebProviders({
hasExplicitEmptyScope,
mapRegistryProviders: deps.mapRegistryProviders,
onlyPluginIds: params.onlyPluginIds,
registry: runtimeRegistry,
});
if (runtimeProviders?.shouldReturn) {
return runtimeProviders.providers;
}
// 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);
}