* feat(tools): add Serply as a web_search provider
Serply is a SERP API returning Google web results on a tenant-supplied
key. It joins the existing chain the same way Exa, Tavily and Brave do:
keyed from config_secrets, skipped silently when no key is stored, and
selectable through the provider parameter for cross-engine corroboration.
Freshness maps onto Google's tbs recency filter, which Serply forwards
upstream. Only the pd/pw/pm/py shortcuts are sent: upstream ignores the
equivalent cdr date range and answers with unfiltered results, so a
range leaves tbs unset rather than naming a filter the results do not
honour.
The settings form gained a fifth sortable provider, so the locked
DuckDuckGo card now derives its position number instead of hardcoding
it, and a stored provider_order is reconciled against the full provider
list rather than against Parallel alone.
* test(tools): use a locally named round trip stub in the serply test
The serply cancellation test borrowed parallelRoundTripFunc from
web_search_parallel_test.go. Define serplyRoundTripFunc alongside the
test that uses it so the file does not depend on another provider's
test helper.
Currently the web_search tool resolves a tenant-configured provider
chain (e.g. tavily → exa → brave) and stops at the first success.
That's the right default for cost/latency, but it makes cross-engine
corroboration impossible at the agent level: the caller has no way to
ask "search the same query on Exa specifically" once Tavily has
already returned a hit.
Add an optional `provider` argument. When set, the chain is narrowed
to the named provider (case-insensitive); other providers are not
queried. The first-success-wins fallback is preserved when the arg
is omitted.
Use case (concrete): a research agent verifying source freshness
calls web_search twice for the same query — once with provider="tavily"
and once with provider="exa" — and compares the URLs returned. URLs
that appear in both engines are high-confidence original primary
sources; URLs only in one signal a republish or low-circulation outlet
worth verifying further.
Other changes:
- Cache key now includes the requested provider so per-engine
results don't collide. Without this, the second call would just
replay the first engine's cached result, defeating the purpose.
- Unknown provider returns a clear error listing what IS configured
for the tenant — better DX than a silent no-op.
Tests: 5 new cases covering narrowing, case-insensitivity, unknown
provider, default first-success behaviour preservation, and cache
isolation across providers.
Refactor WebSearchTool constructor to accept ConfigSecretsStore and
MessageBus instead of hardcoded WebSearchConfig. Implement runtime
per-tenant provider chain resolution via tenantChainCache.
Constructor now:
- Subscribes to cache invalidation events on config_secrets changes
- Returns nil-safe tool instance (no early return on empty providers)
- Passes secrets store and bus for dynamic resolution per Execute() call
Add resolve unit tests; delete legacy config_test.go (functionality
subsumed by cache tests and integration tests).
Refactor web_search Execute to resolve its provider chain per-request
via BuiltinToolSettingsFromCtx instead of iterating the singleton's
hardcoded provider list. Tenant admins can now reorder or disable
providers via builtin_tool_tenant_configs.settings using the shape
{"provider_order":[...], "brave":{"enabled":false}, ...}. Unknown
provider names in provider_order are logged + skipped — no injection
path via this settings blob. Malformed JSON falls back to defaults
(fail-open so a bad admin paste never crashes the tool). Secrets stay
in config_secrets — tenant cannot inject API keys through this path.
11 unit tests cover reorder, filter, disable, unknown-skip, malformed,
global-layer, and tenant-vs-global precedence.
Note: This is the MVP tenant adoption half of the plan's Phase 7. The
Exa + Tavily feature port from PR 825 and coordination with its author
is deferred to a separate follow-up (the port is net-new feature work
orthogonal to the overlay adoption).
- Update go.mod and Dockerfile to Go 1.26
- Apply `go fix ./...` stdlib modernizations across 170+ files
- Add `go fix` to post-implementation checklist in CLAUDE.md
- Fix go fix misapplied rewrite in loop_history.go
- Scan web_fetch/web_search tool results for prompt injection patterns via inputGuard
- Strip hidden HTML elements (display:none, aria-hidden, sr-only classes) during conversion
- Scope web tool caches per channel to prevent cross-channel cache poisoning
- Enforce domain blocklist and allowlist checks on HTTP redirect targets
- Add untrusted content reminder to external content wrapper
- Log redirect source URL in fetch results for transparency
Co-authored-by: Nam Nguyen Ngoc <namnn.0911@gmail.com>
Multi-agent AI gateway with WebSocket RPC, HTTP API, and messaging channel integrations.
Go port of OpenClaw with multi-tenant PostgreSQL, per-user isolation, security hardening,
and production observability.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>