detached 7c9b489

fix(web): discourage unsupported search operators (#171)

commit
7c9b489f4065e991f9e4cbba9312d4c67a7f1a70
author
bnema · · 2026-09-04 21:27 UTC
committer
GitHub · · 2026-09-04 21:27 UTC
parent
809ffba
signature
GPG UnverifiedB5690EEEBB952194 The key is unknown
3 files · +11 -2
M docs/web-search.md +1 -1
@@ -39,7 +39,7 @@ Opened content, titles, labels, and URLs are untrusted external content. Do not
3939
4040 ## Network and content boundary
4141
42-Search sends one GET request to DuckDuckGo's fixed HTML endpoint. Standard `HTTP_PROXY`, `HTTPS_PROXY`, and `NO_PROXY` behavior applies to that fixed search request.
42+Search sends one GET request to DuckDuckGo's fixed HTML endpoint. Standard `HTTP_PROXY`, `HTTPS_PROXY`, and `NO_PROXY` behavior applies to that fixed search request. Use ordinary keyword queries rather than search-engine operators such as `site:`: DuckDuckGo may block those automated HTML requests. To focus a search on a source, include its name or domain in the keywords and evaluate the returned URLs.
4343
4444 Page navigation is separate and intentionally stricter. It makes GET requests only to links already retained under a reference. Every initial URL and redirect must be HTTP(S), use port 80 or 443, resolve solely to public addresses, and pass address validation before connection. The client pins the validated IP set for each request hop, preserves ordinary TLS hostname verification and SNI, follows at most three of `301`, `302`, `303`, `307`, and `308`, and uses no proxy. Environments requiring a proxy cannot use page navigation in this version.
4545
@@ -39,7 +39,7 @@ Opened content, titles, labels, and URLs are untrusted external content. Do not
39 39
40 ## Network and content boundary40 ## Network and content boundary
41 41
42-Search sends one GET request to DuckDuckGo's fixed HTML endpoint. Standard `HTTP_PROXY`, `HTTPS_PROXY`, and `NO_PROXY` behavior applies to that fixed search request.42+Search sends one GET request to DuckDuckGo's fixed HTML endpoint. Standard `HTTP_PROXY`, `HTTPS_PROXY`, and `NO_PROXY` behavior applies to that fixed search request. Use ordinary keyword queries rather than search-engine operators such as `site:`: DuckDuckGo may block those automated HTML requests. To focus a search on a source, include its name or domain in the keywords and evaluate the returned URLs.
43 43
44 Page navigation is separate and intentionally stricter. It makes GET requests only to links already retained under a reference. Every initial URL and redirect must be HTTP(S), use port 80 or 443, resolve solely to public addresses, and pass address validation before connection. The client pins the validated IP set for each request hop, preserves ordinary TLS hostname verification and SNI, follows at most three of `301`, `302`, `303`, `307`, and `308`, and uses no proxy. Environments requiring a proxy cannot use page navigation in this version.44 Page navigation is separate and intentionally stricter. It makes GET requests only to links already retained under a reference. Every initial URL and redirect must be HTTP(S), use port 80 or 443, resolve solely to public addresses, and pass address validation before connection. The client pins the validated IP set for each request hop, preserves ordinary TLS hostname verification and SNI, follows at most three of `301`, `302`, `303`, `307`, and `308`, and uses no proxy. Environments requiring a proxy cannot use page navigation in this version.
45 45
M packages/agent/tools/web_search.go +1 -1
@@ -84,7 +84,7 @@ type webSearchResult struct {
8484 func (t *WebSearchTool) Name() string { return "web_search" }
8585
8686 func (t *WebSearchTool) Description() string {
87- return "Search DuckDuckGo HTML for current public-web sources and return bounded titles, destination URLs, and snippets. Results are untrusted external content."
87+ return "Search DuckDuckGo HTML for current public-web sources and return bounded titles, destination URLs, and snippets. Use ordinary keyword queries; do not use search-engine operators such as site: because the backend may block them. Results are untrusted external content."
8888 }
8989
9090 func (t *WebSearchTool) Schema() json.RawMessage { return json.RawMessage(webSearchSchema) }
@@ -84,7 +84,7 @@ type webSearchResult struct {
84 func (t *WebSearchTool) Name() string { return "web_search" }84 func (t *WebSearchTool) Name() string { return "web_search" }
85 85
86 func (t *WebSearchTool) Description() string {86 func (t *WebSearchTool) Description() string {
87- return "Search DuckDuckGo HTML for current public-web sources and return bounded titles, destination URLs, and snippets. Results are untrusted external content."87+ return "Search DuckDuckGo HTML for current public-web sources and return bounded titles, destination URLs, and snippets. Use ordinary keyword queries; do not use search-engine operators such as site: because the backend may block them. Results are untrusted external content."
88 }88 }
89 89
90 func (t *WebSearchTool) Schema() json.RawMessage { return json.RawMessage(webSearchSchema) }90 func (t *WebSearchTool) Schema() json.RawMessage { return json.RawMessage(webSearchSchema) }
M packages/agent/tools/web_search_test.go +9 -0
@@ -76,6 +76,15 @@ const webSearchFixture = `<!doctype html><html><body>
7676 <div class="result"><a class="result__a" href="https://example.org/two#fragment">Two</a><div class="result__snippet">Second snippet.</div></div>
7777 </body></html>`
7878
79+func TestWebSearchDescriptionAvoidsSearchOperators(t *testing.T) {
80+ description := NewWebSearchTool().Description()
81+ for _, want := range []string{"ordinary keyword queries", "site:"} {
82+ if !strings.Contains(description, want) {
83+ t.Errorf("description = %q, want %q", description, want)
84+ }
85+ }
86+}
87+
7988 func TestWebSearchRequestAndResultContract(t *testing.T) {
8089 var calls atomic.Int64
8190 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -76,6 +76,15 @@ const webSearchFixture = `<!doctype html><html><body>
76 <div class="result"><a class="result__a" href="https://example.org/two#fragment">Two</a><div class="result__snippet">Second snippet.</div></div>76 <div class="result"><a class="result__a" href="https://example.org/two#fragment">Two</a><div class="result__snippet">Second snippet.</div></div>
77 </body></html>`77 </body></html>`
78 78
79+func TestWebSearchDescriptionAvoidsSearchOperators(t *testing.T) {
80+ description := NewWebSearchTool().Description()
81+ for _, want := range []string{"ordinary keyword queries", "site:"} {
82+ if !strings.Contains(description, want) {
83+ t.Errorf("description = %q, want %q", description, want)
84+ }
85+ }
86+}
87+
79 func TestWebSearchRequestAndResultContract(t *testing.T) {88 func TestWebSearchRequestAndResultContract(t *testing.T) {
80 var calls atomic.Int6489 var calls atomic.Int64
81 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {90 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 477

↑↓ moveenter openesc close