WebStack

August 24, 2026 · View on GitHub

WebStack

Integrated web search & fetch kernel plugin for DeepSeek Harness

One plugin. Every search layer. Hardened by default.

CI Release License: MIT Node Tests

English · 简体中文


WebStack registers a single neutral aggregator into the host ctx.web seam — both the search and fetch faces — and keeps every decision inside itself: layer routing (native / free / api / selfhosted / mcp), query-complexity banding, multi-engine fallback, RRF fusion, caching, credential resolution and a four-gate SSRF pipeline. It ships in coexist mode: the bundled cordis patch is empty, upstream selectors stay untouched, and switching layers is a runtime config change — never a re-patch.

Highlights

Search

  • Keyless free pool, zero-config — DuckDuckGo plus a Bing RSS lite channel work out of the box; free-tier engines are structurally barred from requiring keys.
  • Six keyed engines — Tavily · Brave · Exa · Jina · Firecrawl · AnySearch, with a least-in-flight key pool that swaps keys only on auth failures.
  • MCP engines — any search-oriented MCP server becomes an engine; the preset catalog ships version-pinned templates and bare npx commands are structurally rejected.
  • Native delegate — the native layer forwards to host built-ins and fails diagnosably when unavailable.

Intelligence

  • Deterministic hints layer — regex extraction of site: filters, quoted phrases, freshness words and locale; hard constraints push down to engines, soft preferences stay advisory.
  • Complexity banding — queries are banded simple / medium / complex; the band picks the engine set and enables fusion.
  • Tunable fusion — RRF merging with time-decay half-life, authority-domain boost and same-host diversity discount. Duplicate URLs keep their first-seen original string.

Resilience

  • Fallback + cooldowns — per-error-class decisions (retry once / move on / abort); rate limits and quota exhaustion cool engines down, honoring server Retry-After. Fused legs share a band-level budget race with real cancellation.
  • Two-tier cache — sha256 semantic fingerprints over every result-affecting dimension, LRU + single-flight in memory, optional durable tier (host storage or ~/.webstack/cache) with joint invalidation.
  • Session online mode — host-owned off / on / ask switch; on forces fresh, cache-skipping reads.

Safety

  • SSRF four gates — static URL check → DNS-resolved IP classification → per-hop redirect re-validation → bounded body read. Exemptions can only skip gate 2, never 1/3/4.
  • Credential chain — legacy literal → host credential reference → environment variable, resolved per operation into masked snapshots only; plaintext never enters logs, cache or the render tree.
  • Capability ladder — every optional host seam is probed before use; missing capabilities degrade gracefully instead of throwing.

Experience

  • Web UI — settings card with a staged-draft state machine and a session online toggle in the composer; both degrade to read-only/local when the host offers no writable surface. Keys are never rendered.
  • Model toolsweb_backend_status (side-effect-free diagnostics), web_batch_search (order-preserving fan-out, ≤10 queries, per-item isolation), web_history (replay/clear).
  • Bilingual throughout — zh/en diagnostics, error prescriptions and UI copy.

Quick start

Install through your DSH plugin mechanism, either from a release asset:

# grab dsh-webstack-<version>.tgz from GitHub Releases, then reference it in your bundle manifest
# bundle dependency example
dependencies:
  - name: dsh-webstack

That's it — the free layer works with no keys and no extra services. To go further:

search:
  layer: api            # switch to keyed engines
engines:
  tavily:
    key: tvly-...       # or a credentialRef / WEBSTACK_TAVILY_API_KEY env var
mcpServers:
  - id: ddg-mcp
    transport: stdio
    command: npx duckduckgo-mcp-server@0.1.2   # pinned versions only

Optional satellites (same repo, independently installable):

PackageWhat it adds
dsh-webstack-bridgeBrowser-render rescue for JS-heavy pages (MV3 extension + pairing protocol)
dsh-webstack-verticalsExperimental credential-free X/Twitter leg (off by default, opt-in)

Configuration

Full key set: src/settings/schema.ts. Hot = next operation picks it up; restart = structural change, reload required.

KeyDefaultModeNotes
enabledtruehotmaster switch; off = provider reports unavailable
search.layerfreehotnative / free / api / selfhosted / mcp
search.autoFallbacktruehotfalse = first-choice engine only
search.maxResults8hotrequest-level value wins when present
search.fusion.enabledtruehotRRF fusion switch
search.fusion.timeDecayHalfLifeH24hotfreshness half-life (hours); 0 disables decay
search.fusion.authorityBoost1.0hotauthority-domain weight multiplier
search.fusion.diversityDiscount0.85hotsame-host repetition discount
search.complexityRoutingtruehotoff = fixed medium-band width
fetch.pipelinet1hott1 / t1+t2 / t1+t2+t3
fetch.defaultModerawhotpreferred extract mode (chain may downgrade)
fetch.maxContentChars12000hotrendered budget; canonical derives ×4, capped at 8 MiB
mode.sessionOnlineoffhoton forces fresh cache-skipping reads
cache.enabledtruehotsearch-result cache switch
cache.ttlSearchMin / cache.ttlFetchMin10 / 60hotper-domain TTLs (minutes)
cache.persistmemoryhotdurable enables the L1 tier
safety.ssrfExempts[]hothost:port / IPv4 CIDR entries (gate-2 only)
engines.<id>.key / .credentialRefrestartper-engine credentials
mcpServers[]restartvalidated MCP entries register as engines
verticals.packEnabled + channels.xfalsehot*/restartvertical leg master + channel switches
verticals.selectorRules[]hotsite-specific extraction rules (hostSuffix + selector subset)
advanced.winProxyFallbackfalsehotprobe & inject the Windows system proxy (best-effort)

How a search flows

$\text{text} \text{query} → \text{extractHints} # \text{site}:/\text{quotes}/\text{freshness}/\text{locale} (\text{deterministic}) → \text{estimateBand} # \text{simple} | \text{medium} | \text{complex} → \text{planSearch} # \text{layer} \text{pool} \times \text{band} \text{width} \times \text{autoFallback} → \text{creds} # 3-\text{level} \text{chain}, \text{resolved} \text{once} \text{per} \text{operation} → \text{cache} # \text{sha256} \text{fingerprint} \text{over} \text{all} \text{dimensions} → \text{fallback} # \text{cooldown} \text{skip} · \text{retry}-\text{once} · \text{terminal} \text{abort} → \text{fuse} # \text{RRF} \times \text{decay} \times \text{authority} \times \text{diversity} → \text{seam} # \text{truncation} \text{stays} \text{with} \text{the} \text{platform} $

Fetch shares the hardened outbound channel: budgets → SSRF gates → optional site rules → extract chain (raw→fit) → status-as-data reporting, with a single browser-bridge rescue when the satellite is paired.

Stage-by-stage envelopes live in docs/BENCHMARK.md — reproduce locally with pnpm --filter dsh-webstack bench.

Development

pnpm install
pnpm lint              # biome across all packages
pnpm -r run check      # typecheck + test + build per package
pnpm --filter dsh-webstack bench

Requires Node.js ≥ 22.19 and pnpm ≥ 10. Zero native modules.

Documentation

DocContents
CHANGELOG.mdRelease notes
SECURITY.mdSecurity model, trust boundaries, disclosure
docs/AUDIT-W10.mdAdversarial audit round: findings & dispositions
docs/BENCHMARK.mdPerformance envelopes vs. budgets
docs/CALIBRATION.mdPlatform version baseline & upgrade procedure
docs/GOTCHAS.mdEngineering pitfalls, captured for future maintainers
docs/CONTRACTS.mdFrozen type contracts quick reference

Roadmap

  • Native-layer handle capture so native forwards directly to host built-ins.
  • Host locale probing for prompt sections (currently fixed zh/en).
  • Fetch-domain cache wiring.
  • Settings-surface editor for selector rules; more vertical channels.
  • npm publish automation.

License

MIT