fix(web-search): name a credential for a provider, fill Brave's open date bound, and thread abort - #886
Merged
sroussey merged 1 commit intoSep 2, 2026
Conversation
Coverage Report
File Coverage
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
…date bound, and thread abort
Four things a review found on the branch.
A search credential was resolved onto the request BEFORE routing chose a
provider, so `{ provider: "auto", credential_key: "tavily-api-key" }` sent the
Tavily secret to whichever vendor `route()` returned first — Brave, in
registration order, on its `X-Subscription-Token` header. `credential_keys` now
maps provider name to store key: the key sent is the one named for the provider
that runs, routing prefers a provider a key is named for, and the ambiguous
combination of a bare `credential_key` with `"auto"` is refused. The graph
credential scan learns to read a format annotated on a map's `additionalProperties`
value schema, so a run using the map still unlocks the store.
Brave declared `dateFilter: true` but sent no `freshness` for an end-only range,
running the search unfiltered while reporting the bound as honored. The open end
is filled, the way this branch already fills Gemini's `timeRangeFilter`.
The four grounded providers checked `context.signal` and then issued the SDK
request without it, so an abort cancelled nothing and the run was still billed
for the turn in flight. Each now passes the signal through the option its SDK
takes.
`packages/web-search` is a new package and shipped `build-types: tsgo`, which no
longer exists, plus a dead `lint` script.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DdZebeeuQjgQTUszAjJDLb
sroussey
force-pushed
the
claude/websearch-task-providers-8aik77
branch
from
September 2, 2026 15:29
a9bef17 to
1c07c86
Compare
sroussey
force-pushed
the
claude/eloquent-gauss-zd517e-websearch-hardening
branch
from
September 2, 2026 15:29
d071aa4 to
ad6b411
Compare
sroussey
merged commit Sep 2, 2026
d075b1a
into
claude/websearch-task-providers-8aik77
12 checks passed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Targets
claude/websearch-task-providers-8aik77, the head branch of #850, so these fixes fold into that PR rather than landing onmainseparately.Three findings from a review of #850.
1.
provider: "auto"sent the caller's key to whichever vendor routing pickedWebSearchTask.execute()copiedcredential_keyontobaseRequest.credentialKeybefore a provider was chosen.route()selects purely onunhonorableOptions(...)and returns the first registered provider in insertion order, and the built-ins register brave → tavily → searxng — so{ query, provider: "auto", credential_key: "tavily-api-key" }routed to Brave, which put the resolved Tavily secret on itsX-Subscription-Tokenheader toapi.search.brave.com. Nothing warned, and the caller could not know which vendor received the secret.Routing is now credential-aware rather than just refusing the combination:
credential_keysinput maps provider name → credential-store key. The key handed to a provider is the one named for that provider, so a key issued for one vendor cannot reach another.route()takes the set of named providers and considers them first — naming a key states which vendors the caller holds a key for, so"auto"no longer picks a keyless provider while a usable one sits behind it. The preference yields to the capability check, never overrides it.credential_keycombined with"auto"is refused with aTaskConfigurationErrornamingcredential_keys. It remains valid with a pinned provider, where the vendor is settled before the run.One supporting change in
@workglow/task-graph:scanGraphForCredentials/scanGraphForFormatnow also read a credential format annotated on a map'sadditionalPropertiesvalue schema. Without it the new port is invisible to the scan that unlocks the credential store, and every keyed"auto"run would go out unauthenticated — the same silent failure in a different place.Tests:
WebSearchCredential.test.tsdrives a realFetchUrlTaskand asserts a Tavily-named credential never reaches Brave (and never appears on any Brave call), that a routed provider with no named key gets none, and thatauto+credential_keythrows before any fetch.WebSearchTask.test.tscovers per-provider targeting, the routing preference and its subordination to capabilities.GraphFormatScanner.test.tscovers the map-valued port, including empty maps and empty values.2. Brave declared
dateFilter: truebut silently dropped an end-only rangefreshnessParamreturnedundefinedwhen onlyrange.endwas set, so nofreshnessparameter was sent and the request ran unfiltered — whiledateFilter: truemeantunhonorableOptionsfound no gap, a pinnedbraverequest passed, and"auto"would route end-only date requests here. The caller got undated results reported as a successful date-bounded search, which is exactly what the capability record exists to prevent.The open end is now filled, mirroring what this branch already does for Gemini's
timeRangeFilter(OPEN_INTERVAL_START):freshnesstakes a closed interval and accepts an arbitrary start date, so an end-only range becomes1970-01-01to<end>. An entirely empty range still sends nothing. The test that locked in the silent drop now asserts the filled range.3. No abort signal reached any grounded provider's SDK call
All four grounded providers called
context.signal.throwIfAborted()and then issued the SDK request with no signal. A Ctrl-C or graph abort mid-turn cancelled nothing: the request stayed in flight and the run was billed for tokens nobody would read — worst for Anthropic, which runs up toMAX_PAUSE_RESUMES + 1requests atmax_tokens: 16000. Every other Anthropic run-fn in the repo threads it, so this was a deviation rather than a choice.Option names verified against the installed SDKs:
@anthropic-ai/sdk0.123.0messages.create(params, { signal })openai7.8.0responses.create(params, { signal })openai7.8.0chat.completions.create(params, { signal })@google/genai2.20.0config.abortSignalEach provider gains a test asserting the signal reaches the SDK; the Anthropic one checks every request of a resumed paused turn, not just the first.
Verification
Re-run after the rebase, on
ad6b411e6:The new tests were also run against the pre-fix sources to confirm they are not vacuous: 9 of them fail there (7 credential/routing, 1 abort signal, 1 Brave date range).
🤖 Generated with Claude Code
https://claude.ai/code/session_01DdZebeeuQjgQTUszAjJDLb
Generated by Claude Code