mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-02 11:11:35 +00:00
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:
committed by
GitHub
parent
4bc1fd314b
commit
fa28e9be8d
@@ -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.
|
||||
|
||||
|
||||
@@ -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" };
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user