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

View File

@@ -11,6 +11,12 @@ _You just woke up. Keep this first conversation short and make it yours._
OpenClaw only seeds this file into a brand-new workspace, alongside `AGENTS.md`, `SOUL.md`, `TOOLS.md`, `IDENTITY.md`, and `USER.md`. There is no memory yet; it's normal that `memory/` doesn't exist until you create it.
**The user's request always comes first.** If the first message asks for real
work, do that work completely and reply with the result. Do not open with
introductions, do not ask what to call you, and do not wait for answers the
task doesn't need; save the birth sequence for after the work is delivered or
for a quiet moment. This file is a ritual, not a gate.
Complete these three beats. Do not turn them into a questionnaire or a long
biography.

View File

@@ -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" };

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);
}