Repository navigation
Releases
v0.3.1 — Visibility & Observability
Added
web_searchtoolview card on the Web: theweb_searchrow in a conversation takes over the host's built-in web card and adds the provenance receipt line, tone-coded per-source verification badges (alive,verified,dead 404, …), the truncation notice, and a per-result drill-down (click a source to see its serving backend, freshness, and verification state). When the web seam is not pinned to this provider the row degrades gracefully (no receipt line claimed, no badges invented, no backend claimed) instead of mislabeling someone else's results.- In-flight search indicator: while a search is running the toolview row shows a sweep animation plus a client-ticked elapsed label; neither the phase nor the serving backend is claimed until the result settles.
- Session Health tab on the Web: the settings card's Health tab shows session telemetry served by the host's
GET /web-search-ext/health— uptime, per-backend success/failure counts, last-call timing, active 429 cooldowns, and session search/fetch counts. The payload is counters and closed codes only (no credentials, URLs, or query text), and the tab shows an explicit unavailable state with a retry when the route is not mounted. - First-install connectivity probe: the Health tab's Connectivity section probes Exa (keyed REST or anonymous MCP) and Firecrawl (keyed or keyless) — automatically once when the tab first opens with no stored result, and at will via "Test now". The host runs the probe on demand at
POST /web-search-ext/probeand never at apply time, so installs, host restarts, and CI make zero vendor calls; the payload carries plan literals and closed codes only (no vendor messages, URLs, or keys). /search-engineslash command: switch the preferred backend, see live status (key source, last-call outcome, active 429 cooldown), and run the connectivity test — straight from the composer's/menu, in the host's popup shell. If/search-engineis already taken the command registers under/web-search-engineinstead, and the settings card says which name materialized (or that neither was available) — the fallback is surfaced, never silent.- Verification-tier selector in the settings card: the card now exposes
verifyLevel(off/liveness/content) as a labeled select. A saved tier applies from the nextweb_search/web_fetchonward — no restart; the host resolves the settings document live per call. An unrecognized tier written directly into the profile patch shows the schema default instead of a dead option, and saving writes the schema-valid value back. - Context budget:
numResultsis now a hard cap onweb_searchoutput — a caller's largermaxResultsis clamped before any backend runs, vendor over-delivery is clamped before verification (so L0/L1 never probes a URL that will not be returned), and the receipt reports the cap with the structuredtruncatedflag.
Fixed
web_fetchavailable on live hosts: 0.3.0 pinned this plugin as the web seam's fetch provider, but no composition path registered the model-facingweb_fetchtool (pre-0.2 preset layers shippedfetch: false, anddsh webdisables the profile-layertool-webrow), so live hosts returnedunknown tool "web_fetch". The plugin now registers the stockweb_fetchtool at apply time when the preset layer left it unregistered — same schema, prompt section, and presentation as the harness stock tool, dedup-guarded, with execution routed through the SSRF-guardedctx.web.fetch.- L1 byte cap: when a fetched chunk crossed
contentCheckBytes, the entire chunk was dropped whilelengthstill counted the dropped bytes, socontentCheckMinBytescould gate on bytes that were never kept. The crossing chunk's fitting prefix is now retained (decoded-character cap, matching the existing slice model). - Client entry no longer fails to apply on live hosts: the
web_searchtoolview registration collided with the host's built-inweb-toolviewentry — same keyed slot, same key, same default priority 0 — which the slot registry hard-rejects, so the whole client entry failed to load ("Failed to load plugins") and the settings card, Health tab, and/search-enginecommand all died with it. The registration now shadows the host entry at an explicit priority (the registry renders the lowest-priority entry per key), and the committed bundle's registration is asserted intest/toolview.test.mjsbecause no CI path runs the browser-side registry.
Changed
- DeepSeek Harness 0.2.x compatibility: settings now ride the plugin's own volatile Cordis config (
ctx.configFormson the client,.volatile()fields on the host) instead of the removedinstallSettingsSection/settingsNamespacehelpers; client credentials calls use the typedremote.credentialsnamespace; the card registers intosettings.plugins.tab;@deepseek-ai/dsh-client-runtimeleft the client inject list (replaced bydsh-client-modules); removed…Outline14/…Outline12icon imports (IconChevronDown,IconLoading,IconGlobe,IconInspect) became the renamed…OutlineRegularicons. Peer ranges andengines.dshnow target>=0.2.1-rc.1 <0.3.0-0, and@deepseek-ai/schemasteryis^3.18.5(first release with.volatile()). - Attribution user-agent bumped to
dsh-web-search-ext/0.3.1.
Upgrade note
This release targets DeepSeek Harness 0.2.1-rc.1 or newer (>=0.2.1-rc.1 <0.3.0-0). The 0.1.x plugin APIs were removed by the harness; installs into older harness lines are refused by the plugin manager.
Install/upgrade: dsh plugin --profile web add @fno2010/dsh-web-search-ext@0.3.1, then restart dsh web.
0.3.0 — verifiable, current search
0.3.0 — verifiable, current search
Added
- Keyless
web_fetch: a new fetch provider is registered as the web seam'sfetchProvider, soweb_fetchworks without any API key. It scrapes through Firecrawl (POST {base}/scrape; keyless requests allowed by default) and fails over to Exa's anonymous hosted MCPweb_fetch_exawhen Firecrawl is unavailable or rate-limited. Output is capped byfetchMaxChars(default 50 000) withtruncatedset when the cap bites. The bundle patch now pinsweb.fetchProvider: web-search-extalongsideweb.searchProvider. - Result verification (L0 liveness, on by default): every returned source is probed locally (HEAD first, GET on 405/501; redirects are followed manually — at most 3 hops, each hop re-validated against the SSRF guard before being followed) before the result is returned; each snippet gains a status marker —
[alive],[dead 404],[blocked],[timeout],[unreachable],[skipped]— and no result is ever dropped. Verification is purely local HTTP: no vendor quota, no keys. - L1 content check (experimental, opt-in via
verifyLevel: "content"): fetches the firstcontentCheckBytesof each page (body read bounded bycontentCheckTimeoutMs) and checks that the snippet's leading words still appear; markers[verified]/[verified·changed]plus a word-match ratio. Sources with no snippet get[unverified](page live, content not checked) and are counted separately in the receipt.unsafe/forbiddenoutcomes mark[blocked]. - Provenance receipt:
web_searchresults now carry a one-line receipt at the top ofcontent— e.g.web-search-ext: exa-mcp · 1.2s · 5 results · liveness: 5 alive · freshness 24h not honored (keyless exa has no date filter)— naming the backend that actually served the result and surfacing every limitation instead of hiding it. - Freshness window:
freshnesssetting (any|24h|7d|30d) is sent on the wire where the backend supports it (Exa RESTstartPublishedDate, Firecrawltbs: qdr:d|w|m). The keyless Exa MCP path has no date filter, so the receipt says so explicitly instead of silently ignoring the request. - 429 diagnostics: a rate-limited backend's reported retry window is parsed from the standard
Retry-Afterheader (delta-seconds or HTTP-date) and/or structured response-body fields (retry_after_seconds, as number or string) and reported in human-readable form ("retry in ~22.1h"); it becomes that backend's cooldown (clamped bymaxCooldownSec, default 24 h), and the error names the unlock path (set the corresponding API key). - New config fields (all optional, all defaulted):
verifyLevel,livenessTimeoutMs,contentCheckBytes,contentCheckMinBytes,contentCheckMatchWords,contentCheckTimeoutMs,freshness,maxCooldownSec,fetchMaxChars. - Test suite grew from 10 to 39 mocked scenarios: L0/L1 markers, receipt shape, HEAD-405/501 fallback, verify-off zero-traffic, 429 retry-after (delta-seconds and HTTP-date headers, body fields, string values, clamp) + cooldown, freshness on the wire, fetch failover (429 and non-429), 404-as-result, SSRF guard matrix (IPv6/127-8/FQDN spellings incl. multi-dot), redirect re-validation, L0 status mapping, L1 deadlines (slow-drip, stalled mid-read, unstreamed text) + snippet-less
[unverified], abort during verification, dual-429, truncation, keyless-only plans. ROADMAP.md: version plan and per-version deliverables for 0.3.0+.
Changed
- Fetch-side SSRF guard: explicit URL fetches refuse non-http(s) schemes and loopback/private/link-local targets before any network traffic.
- SSRF guard hardened (both the verification probes and
web_fetch): IPv6 loopback/link-local/unique-local literals (including IPv4-mapped forms such as[::ffff:127.0.0.1]), the entire127.0.0.0/8loopback range, trailing-dot hostnames (localhost.), CGNAT and multicast ranges are all refused; addresses that cannot be confidently classified as public are refused (fail closed). - Firecrawl scrape requests now explicitly ask for both
markdownandhtmlformats, so the "scraped 404 is a result" path still works when markdown comes back empty. - 429 handling unified across all three backends (Exa REST, Exa MCP, Firecrawl): one
WEB_RATE_LIMITerror shape withretryAfterSec. - Attribution user-agent bumped to
dsh-web-search-ext/0.3.0.
v0.2.0
What's new
- Settings card on the Web (Settings → Plugins → Plugin configuration): edit the five config fields (
preferred,numResults,maxSnippetChars,rateLimitCooldownSec,firecrawlKeyless) with live save/discard, and manage both API keys. - API-key state is auto-discovered from the credentials layers (live process env →
~/.dsh/.credentials.yaml→.envfiles), refreshes oncredentials/reference-updated(file edits update the badges live, no restart), and renders read-only when the live process environment supplies the key — the host rejects UI writes that an environment value would shadow. - Card chrome mirrors the host's
PluginCarddesign language (shared design tokens, host chevron/loading icons, tsdown CSS-module injection); EN + ZH copy. - The client bundle registers under both install-form entry names (the scoped npm key and the unscoped link-install key), so the card loads whether the plugin was added from npm or via a local
link:install. docs/settings-ui-plan.md: the researched implementation plan (extension-point verdict, wire API, risks).
Changed
- Attribution user-agent bumped to match the package version.
v0.1.2
Docs hotfix.
Fixed an inaccurate README claim (EN + ZH): the Web 'Models' settings page manages LLM provider credentials only — it has no field for EXA_API_KEY / FIRECRAWL_API_KEY. The docs now name the real key channels (settings literal, credentials file, launch env var) and point to the settings-UI feature request (#5).
No behaviour change beyond the attribution user-agent version.
v0.1.1
Added the repository metadata field so the npm package links back to this repo (plugin-marketplace npm linkage, download counts). Attribution user-agent now carries the package version.
Published via the tag-triggered trusted-publishing workflow (first real run; SLSA provenance attestation attached).
v0.1.0
Initial release.
Multi-backend web_search provider for DeepSeek Harness (DSH), registered into the web seam under provider id web-search-ext:
- Exa backend — REST search with key; anonymous hosted MCP endpoint without a key
- Firecrawl backend — v2 search API (keyed or keyless), v1 envelope compatibility
- Preferred-backend ordering with automatic failover on any backend failure
- Per-backend 429 cooldown (default 60 s) with skipped-backend reporting
- Key resolution per backend: settings literal → credentials service → launch env var
- Bundle patch wiring the provider into the built-in web_search tool
- Test suite: 10 mocked failover/mapping scenarios + 3 live keyless smoke tests