websearch-skill 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. websearch_skill-0.1.0/.gitignore +42 -0
  2. websearch_skill-0.1.0/CHANGELOG.md +263 -0
  3. websearch_skill-0.1.0/LICENSE +21 -0
  4. websearch_skill-0.1.0/PKG-INFO +350 -0
  5. websearch_skill-0.1.0/README.md +321 -0
  6. websearch_skill-0.1.0/contracts/README.md +71 -0
  7. websearch_skill-0.1.0/contracts/agent-io.schema.json +165 -0
  8. websearch_skill-0.1.0/contracts/arxiv.schema.json +78 -0
  9. websearch_skill-0.1.0/contracts/envelope.schema.json +58 -0
  10. websearch_skill-0.1.0/contracts/extract.schema.json +119 -0
  11. websearch_skill-0.1.0/contracts/fetch.schema.json +111 -0
  12. websearch_skill-0.1.0/contracts/format.schema.json +216 -0
  13. websearch_skill-0.1.0/contracts/github.schema.json +74 -0
  14. websearch_skill-0.1.0/contracts/search.schema.json +163 -0
  15. websearch_skill-0.1.0/contracts/store.schema.json +158 -0
  16. websearch_skill-0.1.0/docker/searxng/README.md +62 -0
  17. websearch_skill-0.1.0/docker/searxng/core-config/settings.yml +23 -0
  18. websearch_skill-0.1.0/docker/searxng/docker-compose.yml +29 -0
  19. websearch_skill-0.1.0/docs/ARCHITECTURE.md +235 -0
  20. websearch_skill-0.1.0/docs/BENCHMARK.md +77 -0
  21. websearch_skill-0.1.0/docs/INSTALL.md +204 -0
  22. websearch_skill-0.1.0/pyproject.toml +102 -0
  23. websearch_skill-0.1.0/skills/web-search/SKILL.md +179 -0
  24. websearch_skill-0.1.0/src/websearch/__init__.py +8 -0
  25. websearch_skill-0.1.0/src/websearch/__main__.py +6 -0
  26. websearch_skill-0.1.0/src/websearch/cli.py +1014 -0
  27. websearch_skill-0.1.0/src/websearch/envelope.py +97 -0
  28. websearch_skill-0.1.0/src/websearch/errors.py +25 -0
  29. websearch_skill-0.1.0/src/websearch/layer1_search/__init__.py +68 -0
  30. websearch_skill-0.1.0/src/websearch/layer1_search/adapters/__init__.py +6 -0
  31. websearch_skill-0.1.0/src/websearch/layer1_search/adapters/ddgs_engine.py +82 -0
  32. websearch_skill-0.1.0/src/websearch/layer1_search/adapters/searxng.py +132 -0
  33. websearch_skill-0.1.0/src/websearch/layer1_search/canonical.py +100 -0
  34. websearch_skill-0.1.0/src/websearch/layer1_search/capability.py +42 -0
  35. websearch_skill-0.1.0/src/websearch/layer1_search/dedup.py +94 -0
  36. websearch_skill-0.1.0/src/websearch/layer1_search/fusion.py +71 -0
  37. websearch_skill-0.1.0/src/websearch/layer1_search/models.py +123 -0
  38. websearch_skill-0.1.0/src/websearch/layer1_search/port.py +67 -0
  39. websearch_skill-0.1.0/src/websearch/layer1_search/router.py +271 -0
  40. websearch_skill-0.1.0/src/websearch/layer2_extract/__init__.py +87 -0
  41. websearch_skill-0.1.0/src/websearch/layer2_extract/blocks.py +166 -0
  42. websearch_skill-0.1.0/src/websearch/layer2_extract/egress.py +91 -0
  43. websearch_skill-0.1.0/src/websearch/layer2_extract/exceptions.py +15 -0
  44. websearch_skill-0.1.0/src/websearch/layer2_extract/extractors/__init__.py +1 -0
  45. websearch_skill-0.1.0/src/websearch/layer2_extract/extractors/trafilatura_extractor.py +212 -0
  46. websearch_skill-0.1.0/src/websearch/layer2_extract/fetch_router.py +90 -0
  47. websearch_skill-0.1.0/src/websearch/layer2_extract/fetchers/__init__.py +1 -0
  48. websearch_skill-0.1.0/src/websearch/layer2_extract/fetchers/curl_cffi_fetcher.py +158 -0
  49. websearch_skill-0.1.0/src/websearch/layer2_extract/fetchers/httpx_fetcher.py +137 -0
  50. websearch_skill-0.1.0/src/websearch/layer2_extract/fetchers/util.py +50 -0
  51. websearch_skill-0.1.0/src/websearch/layer2_extract/models.py +198 -0
  52. websearch_skill-0.1.0/src/websearch/layer2_extract/pipeline.py +206 -0
  53. websearch_skill-0.1.0/src/websearch/layer2_extract/ports.py +49 -0
  54. websearch_skill-0.1.0/src/websearch/layer2_extract/quality.py +254 -0
  55. websearch_skill-0.1.0/src/websearch/layer2_format/__init__.py +83 -0
  56. websearch_skill-0.1.0/src/websearch/layer2_format/anthropic_blocks.py +51 -0
  57. websearch_skill-0.1.0/src/websearch/layer2_format/chunk.py +78 -0
  58. websearch_skill-0.1.0/src/websearch/layer2_format/dedup.py +234 -0
  59. websearch_skill-0.1.0/src/websearch/layer2_format/exceptions.py +7 -0
  60. websearch_skill-0.1.0/src/websearch/layer2_format/format_pipeline.py +240 -0
  61. websearch_skill-0.1.0/src/websearch/layer2_format/ids.py +36 -0
  62. websearch_skill-0.1.0/src/websearch/layer2_format/models.py +305 -0
  63. websearch_skill-0.1.0/src/websearch/layer2_format/ports.py +76 -0
  64. websearch_skill-0.1.0/src/websearch/layer2_format/renderer.py +132 -0
  65. websearch_skill-0.1.0/src/websearch/layer2_format/store/__init__.py +39 -0
  66. websearch_skill-0.1.0/src/websearch/layer2_format/store/_common.py +100 -0
  67. websearch_skill-0.1.0/src/websearch/layer2_format/store/memory_bm25.py +286 -0
  68. websearch_skill-0.1.0/src/websearch/layer2_format/store/sqlite_fts5.py +258 -0
  69. websearch_skill-0.1.0/src/websearch/layer2_format/tokens.py +38 -0
  70. websearch_skill-0.1.0/src/websearch/layer3_agentio/__init__.py +58 -0
  71. websearch_skill-0.1.0/src/websearch/layer3_agentio/facade.py +528 -0
  72. websearch_skill-0.1.0/src/websearch/layer3_agentio/fence.py +112 -0
  73. websearch_skill-0.1.0/src/websearch/layer3_agentio/mcp_server.py +418 -0
  74. websearch_skill-0.1.0/src/websearch/layer3_agentio/models.py +143 -0
  75. websearch_skill-0.1.0/src/websearch/layer3_agentio/pagination.py +47 -0
  76. websearch_skill-0.1.0/src/websearch/tool_arxiv/__init__.py +29 -0
  77. websearch_skill-0.1.0/src/websearch/tool_arxiv/client.py +300 -0
  78. websearch_skill-0.1.0/src/websearch/tool_arxiv/models.py +79 -0
  79. websearch_skill-0.1.0/src/websearch/tool_github/__init__.py +29 -0
  80. websearch_skill-0.1.0/src/websearch/tool_github/client.py +265 -0
  81. websearch_skill-0.1.0/src/websearch/tool_github/models.py +63 -0
  82. websearch_skill-0.1.0/tests/__init__.py +0 -0
  83. websearch_skill-0.1.0/tests/conftest.py +259 -0
  84. websearch_skill-0.1.0/tests/test_adapters.py +145 -0
  85. websearch_skill-0.1.0/tests/test_blocks.py +126 -0
  86. websearch_skill-0.1.0/tests/test_canonical.py +71 -0
  87. websearch_skill-0.1.0/tests/test_contracts.py +92 -0
  88. websearch_skill-0.1.0/tests/test_dedup.py +51 -0
  89. websearch_skill-0.1.0/tests/test_distribution.py +205 -0
  90. websearch_skill-0.1.0/tests/test_egress.py +61 -0
  91. websearch_skill-0.1.0/tests/test_engine_passthrough.py +117 -0
  92. websearch_skill-0.1.0/tests/test_extract_adapter.py +62 -0
  93. websearch_skill-0.1.0/tests/test_extract_contracts.py +110 -0
  94. websearch_skill-0.1.0/tests/test_extract_pipeline.py +159 -0
  95. websearch_skill-0.1.0/tests/test_fetch_router.py +129 -0
  96. websearch_skill-0.1.0/tests/test_fetchers.py +174 -0
  97. websearch_skill-0.1.0/tests/test_format_chunk.py +63 -0
  98. websearch_skill-0.1.0/tests/test_format_contracts.py +112 -0
  99. websearch_skill-0.1.0/tests/test_format_dedup.py +139 -0
  100. websearch_skill-0.1.0/tests/test_format_pipeline.py +272 -0
  101. websearch_skill-0.1.0/tests/test_fusion.py +87 -0
  102. websearch_skill-0.1.0/tests/test_layer1_search_e2e.py +146 -0
  103. websearch_skill-0.1.0/tests/test_layer2_extract_e2e.py +126 -0
  104. websearch_skill-0.1.0/tests/test_layer3_agentio.py +263 -0
  105. websearch_skill-0.1.0/tests/test_layer3_cli_e2e.py +123 -0
  106. websearch_skill-0.1.0/tests/test_layer3_contracts.py +75 -0
  107. websearch_skill-0.1.0/tests/test_layer3_fence.py +83 -0
  108. websearch_skill-0.1.0/tests/test_layer3_pagination.py +52 -0
  109. websearch_skill-0.1.0/tests/test_open_e2e.py +156 -0
  110. websearch_skill-0.1.0/tests/test_quality.py +127 -0
  111. websearch_skill-0.1.0/tests/test_robustness_polish.py +275 -0
  112. websearch_skill-0.1.0/tests/test_router.py +187 -0
  113. websearch_skill-0.1.0/tests/test_store.py +248 -0
  114. websearch_skill-0.1.0/tests/test_tool_arxiv.py +344 -0
  115. websearch_skill-0.1.0/tests/test_tool_github.py +297 -0
@@ -0,0 +1,42 @@
1
+ # Research store (design of record; kept local, distilled into docs/ARCHITECTURE.md)
2
+ .research/
3
+
4
+ # Internal agent instruction files (kept local for the build agent, not public)
5
+ CLAUDE.md
6
+ AGENTS.md
7
+
8
+ # Python
9
+ __pycache__/
10
+ *.py[cod]
11
+ *$py.class
12
+ *.egg-info/
13
+ .eggs/
14
+ build/
15
+ dist/
16
+ *.so
17
+
18
+ # Virtualenv / uv
19
+ .venv/
20
+ venv/
21
+ .python-version
22
+
23
+ # Test / coverage / cache
24
+ .pytest_cache/
25
+ .ruff_cache/
26
+ .mypy_cache/
27
+ .coverage
28
+ htmlcov/
29
+ coverage.xml
30
+
31
+ # Editor / OS
32
+ .idea/
33
+ .vscode/
34
+ .DS_Store
35
+
36
+ # Local Claude Code state (settings.local.json etc.); NOT the .claude-plugin/ manifests.
37
+ .claude/
38
+
39
+ # Local secrets (search-engine API keys, SearXNG URL overrides)
40
+ .env
41
+ .env.*
42
+ !.env.example
@@ -0,0 +1,263 @@
1
+ # Changelog
2
+
3
+ Notable changes to this project. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); the project will follow
5
+ semantic versioning once it reaches a tagged release.
6
+
7
+ ## [Unreleased]
8
+
9
+ ### Changed
10
+
11
+ - fastmcp is now a base dependency, not the optional `mcp` extra, so `websearch mcp`,
12
+ `uvx websearch-skill mcp`, and the MCP-registry runner start the stdio server with one
13
+ command and no extra. The `[mcp]` extra stays as a no-op alias so older install commands
14
+ keep resolving. The package also installs a second console script, `websearch-skill`
15
+ (matching the distribution name), so `uvx websearch-skill <cmd>` resolves with no `--from`.
16
+ - The agent surface is plug-and-play: `web-search` (CLI) and the `web_search` MCP tool no
17
+ longer take engine-selection flags (`--engines`, `--no-ddgs`, `--ddgs-backends`, the MCP
18
+ `engines` parameter, or `WEBSEARCH_DDGS_BACKENDS`). The keyless multi-engine default
19
+ needs no flags. Engine and backend selection stays on the lower-level `search` command
20
+ for debugging. `web-search --offset` now actually pages results (the ddgs adapter maps
21
+ offset to a page), instead of silently returning page one.
22
+ - `fetch@1.2.0`: `FetchResult` gains a structured `failure_kind`
23
+ (`egress_refused` / `redirect_loop` / `transport_error` / `timeout` /
24
+ `dependency_missing`) so retriability is set from the cause, not by matching error text.
25
+
26
+ ### Fixed
27
+
28
+ - No command can dump a raw traceback. `web-fetch --page 0` (or negative), invalid
29
+ `open`/extract parameters, a store-open failure, and any unexpected error now return a
30
+ clean `invalid_request` / `internal_error` Envelope. `web-search` rejects a
31
+ whitespace-only query; an unknown `engines` value falls back to the default with a
32
+ warning instead of failing.
33
+ - `meta.elapsed_ms` is populated on every Layer-3 (`web_search` / `web_fetch` / `web_open`)
34
+ and format Envelope (it was always `0.0`); `_propagate_error` keeps the upstream
35
+ `trace_id` and uses a defined error code.
36
+ - Retriability precision: a permanent fetch failure (egress refusal, redirect loop, missing
37
+ optional dependency) is non-retriable; a transport error or timeout is retriable.
38
+ - SSRF egress guard uses an allowlist (`is_global`), closing CGNAT `100.64.0.0/10`
39
+ (RFC 6598) and any other non-public range the prior denylist missed.
40
+ - The page store (SQLite FTS5 and the BM25 fallback) serializes its methods with a lock and
41
+ writes each document in one atomic transaction, so concurrent MCP tool calls cannot race
42
+ the shared connection or leave a half-written document. `search().total` is the true match
43
+ count, not the capped top-k pool. MCP singletons build under a lock.
44
+ - curl_cffi response handling is fully guarded (a mid-body decode/reset becomes an
45
+ escalatable result, not a crash); GitHub/arXiv tolerate a malformed upstream shape with a
46
+ clean `upstream_error`; a non-positive chunk size no longer hangs; a NaN score no longer
47
+ corrupts result ordering; the error-title quality veto no longer tanks a long article
48
+ whose title merely contains a word like "forbidden" or a number like "404".
49
+ - The CLI pins stdout/stderr to UTF-8 (no `UnicodeEncodeError` under a C/POSIX locale), and
50
+ the `web-fetch` "more pages" hint suggests a command that actually works in context
51
+ (`web-open ... --persist-path` when persisting, else a re-fetch).
52
+
53
+ ### Added
54
+
55
+ - Distribution layer: a Claude Code plugin and marketplace (`.claude-plugin/`, with no
56
+ SKILL duplication, since `source: "./"` auto-discovers the root `skills/web-search/` and
57
+ root `.mcp.json`), a generic root `.mcp.json`, an MCP-registry `server.json`
58
+ (`io.github.hec-ovi/web-search`, PyPI/uvx), a PyPI Trusted-Publishing release workflow
59
+ (`.github/workflows/release.yml`, OIDC, no token), and `docs/INSTALL.md` covering every
60
+ harness route (`npx skills add`, `uvx`, the Claude plugin, Codex with its network-sandbox
61
+ caveat, OpenCode, Cursor, Hermes, OpenClaw, and the registry). Install with
62
+ `npx skills add hec-ovi/websearch-skill`, `/plugin install web-search@websearch-skill`, or
63
+ `uvx websearch-skill <cmd>`.
64
+ - Keyless multi-engine search out of the box: the `ddgs` adapter is treated as the
65
+ metasearch it is (Google, Brave, DuckDuckGo, Yandex, Yahoo, Startpage, Mojeek,
66
+ Wikipedia by default, with Bing and others selectable by name). The lower-level
67
+ `search` command can force a subset with `--ddgs-backends google,brave,mojeek`, and
68
+ `build_router` / `build_agent_io` take a `ddgs_backend` parameter. `ddgs` is the keyless
69
+ default; a self-hosted SearXNG is the optional broader engine. Public SearXNG instances
70
+ are not used as a default (most disable the JSON API and rate-limit automated clients).
71
+ - `arxiv@1.0.0` contract and a keyless `websearch arxiv` tool (also the MCP
72
+ `arxiv_search` tool): arXiv paper search over the official Atom API with field-targeted
73
+ search (`--field`), sorting, GET caching, and exponential backoff on HTTP 429. Returns
74
+ structured papers (authors, abstract, categories, abstract and PDF links).
75
+ - `github@1.0.0` contract and a keyless `websearch github` tool (also the MCP
76
+ `github_search` tool): GitHub repository search over the unauthenticated REST API with
77
+ typed fields (stars, forks, language, topics) and a clean `rate_limited` error on the
78
+ ~10 req/min limit. Code search needs a token and is left out of the keyless path.
79
+ - New cross-cutting error codes `upstream_error` and `rate_limited` for the extra tools.
80
+ - `docker/searxng/`: a lean one-container SearXNG self-host config (JSON API enabled, bot
81
+ limiter off, no Valkey needed) with its own README and a before-you-expose-it checklist.
82
+ - `docs/BENCHMARK.md`: a reproducible, same-query head-to-head against the web search
83
+ built into Claude Code, with the honest verdict (comparable on retrieval; this tool
84
+ wins on cost, privacy, control, and extraction).
85
+ - Frozen contracts as JSON Schema 2020-12: `envelope@1.0.0` (the cross-cutting wrapper
86
+ for every inter-layer message and CLI `--json` output) and `search@1.0.0` (the
87
+ Layer-1 search port). The semver rule (additive is MINOR, removal/rename/retype is
88
+ MAJOR) and the consumer-driven fixture check are documented in `contracts/README.md`.
89
+ - Layer 1 (search): a multi-engine router with isolated per-engine adapters behind an
90
+ `EngineAdapter` port. Ships a SearXNG adapter (keyless backbone, over httpx) and a
91
+ ddgs adapter (zero-config fallback).
92
+ - Provenance-aware weighted Reciprocal Rank Fusion (k=60) with mandatory
93
+ de-correlation: engines that share a correlation group count as one independent vote,
94
+ so a consensus bonus cannot amplify the same crawler agreeing with itself. The router
95
+ records the de-correlation in a warning.
96
+ - URL canonicalization and dedup with provenance merge, site include/exclude filtering,
97
+ concurrent fan-out, and per-engine fault tolerance (an error Envelope is returned only
98
+ when every selected engine fails).
99
+ - CLI entry point `websearch search` (`--json` emits the raw Envelope; exit code 1 on an
100
+ error Envelope).
101
+ - Test suite: an end-to-end test through the real CLI entry point with both external
102
+ boundaries stubbed (SearXNG via pytest-httpx, ddgs via a fake), plus focused
103
+ canonicalization, dedup, fusion, adapter, router fault-tolerance, and
104
+ contract-conformance tests that validate real output against the frozen schemas. CI
105
+ runs ruff and pytest on Python 3.11 to 3.13 via uv.
106
+ - Layer 2A contracts: `fetch@1.1.0` (`FetchRequest`, `FetchResult`) and `extract@1.0.0`
107
+ (`ExtractRequest`, `ExtractResult`, `ExtractSource`, `ExtractPayload`), the two
108
+ decoupled sub-ports of fetch and extract.
109
+ - Layer 2A (fetch + extract): a tiered fetch that starts on plain httpx and escalates to
110
+ curl_cffi browser TLS/JA3 impersonation only when it detects an anti-bot block (header
111
+ markers first, then gated body markers for Cloudflare, DataDome, PerimeterX, Akamai,
112
+ and Imperva), never on a 404 or a terminal block (rate limit, auth, legal). Extraction
113
+ defaults to Trafilatura, emitting clean Markdown plus plain text and metadata,
114
+ recovering raw schema.org JSON-LD and `og:type` with lxml, and computing a heuristic
115
+ `quality_score` and a cheap `page_type`. Browser/stealth fetch tiers and neural extract
116
+ engines are named in the contract enums but stay opt-in.
117
+ - CLI `websearch fetch` (`--json` emits the Envelope; `--output-format`, `--quiet`,
118
+ `--tier`, `--proxy`, `--allow-private-hosts`, and the extract options). No
119
+ output-length cap: `content_markdown` is never truncated; `max_bytes` is a transport
120
+ guard only (default 10 MB).
121
+ - SSRF egress guard: an http(s) scheme allowlist plus DNS resolution that refuses
122
+ private, loopback, link-local (the `169.254.169.254` metadata endpoint), reserved, and
123
+ multicast targets, applied before the first request and on every redirect hop.
124
+ - Test suite is now 188 tests (Layer 1 plus Layer 2A: block detection, quality scoring,
125
+ fetch tiers and escalation, the egress guard, extraction, and CLI end-to-end).
126
+ - Layer 2B contracts: `format@1.0.0` (`ResultInput`, `FormatRequest`, `FormatPayload`,
127
+ `FormatSidecar`, and the derived `AnthropicSearchResultBlock`) and `store@1.0.0`
128
+ (`PageInput`, `Passage`, `SearchPageRequest`, `SearchPageResult`, `PageDocument`,
129
+ `ResolveIndex`, `StoreConfig`), the two decoupled sub-ports of format and store.
130
+ - Layer 2B format: turns vendor-neutral results into one layout-stable Markdown
131
+ document plus a parallel JSON sidecar carrying identical data, ordered by descending
132
+ relevance and paginated. Near-duplicate dedup (byte-exact SHA-256 first, then a
133
+ pure-Python MinHash over word 4-gram shingles) folds duplicates into the best-scored
134
+ canonical and records `dropped_duplicates`. Progressive disclosure picks the render
135
+ mode: `auto` inlines full bodies when the page fits a token budget, otherwise an
136
+ index (a preview plus a stable id to resolve). The optional
137
+ `anthropic_search_result_blocks` view maps 1:1 onto Anthropic search_result content
138
+ blocks (source as a bare string, at least one non-empty text block, citations
139
+ all-or-nothing); it is off by default and Layer 3 owns the citations toggle.
140
+ - Layer 2B store: an ephemeral page index behind a `PageIndex` port with
141
+ `add`/`search`/`get`/`resolve_index`. The default adapter is SQLite FTS5 over an
142
+ in-memory connection (Python stdlib, BM25 ranking), with a runtime FTS5 probe that
143
+ falls back to a pure-Python BM25 index when FTS5 is not compiled into the local
144
+ SQLite. Adds are idempotent on url plus content hash; an arbitrary query is escaped so
145
+ FTS5 operators never raise a syntax error; persistence is the presence of a file path.
146
+ - CLI `websearch open <url> ...`: composes Layer 2A and 2B (fetch, extract, format,
147
+ index) into one paginated, deduped document, with `--mode`, `--body`,
148
+ `--body-char-budget`/`--no-truncate`, `--anthropic-blocks`, `--search` (BM25 passage
149
+ search over the opened pages), and `--persist-path`. Per-URL fetch failures surface as
150
+ warnings rather than failing the whole request.
151
+ - No output-length cap in Layer 2B either: full bodies are stored and echoed in the JSON
152
+ sidecar verbatim in both index and full modes; `body_char_budget` only offloads the
153
+ rendered Markdown view to the resolver, and `--no-truncate` disables even that.
154
+ - Test suite is now 272 tests (adds dedup, chunk-offset, renderer layout-stability,
155
+ both store adapters, format/store contract conformance, and the `open` end-to-end).
156
+ - Layer 3 contract: `agent-io@1.0.0` (`AgentSearchRequest`/`AgentSearchPayload`,
157
+ `AgentFetchRequest`/`AgentOpenRequest`/`AgentPage`/`AgentFetchPayload`, `FenceInfo`),
158
+ the consolidated agent-facing surface over Layers 1/2A/2B.
159
+ - Layer 3 (agent I/O): `web_search` (Layer 1), `web_fetch` (Layer 2A, fenced and
160
+ paginated), and `web_open` (paginate an already-fetched page from the Layer 2B store
161
+ by handle, no re-fetch), all over the same `Envelope`. The only cross-layer key is a
162
+ human-readable `handle` (`site~shorthash`), not an opaque id. Pagination is lossless
163
+ progressive disclosure, never a content cap.
164
+ - Untrusted-content fence: each fetched page's content is wrapped in delimiters carrying
165
+ a per-instance 128-bit random nonce (so injected text cannot forge the closing
166
+ marker), a data-only directive, and neutralization of any in-body copy of the marker,
167
+ with optional datamarking (`--datamark`). Documented as reducing, not eliminating,
168
+ indirect prompt injection (it prevents the boundary breakout, not persuasion).
169
+ - Optional FastMCP stdio server (`websearch mcp`, the `mcp` extra) exposing
170
+ `web_search`/`web_fetch`/`web_open`; the tool returns the same Envelope JSON the CLI
171
+ emits. New CLI commands `web-search`/`web-fetch`/`web-open`/`mcp`; the lower-level
172
+ `search`/`fetch`/`open` commands stay as the per-layer surfaces.
173
+ - A portable `SKILL.md` (`skills/web-search/`) to the Agent Skills standard (name plus
174
+ description), documenting the command grammar, the search/fetch/open decision table,
175
+ and the untrusted-content rule.
176
+ - Test suite is now 332 tests (adds the fence, token-budget pagination losslessness,
177
+ the agent-io facade and contract, and the FastMCP server and `web-*` CLI end-to-end).
178
+
179
+ ### Fixed
180
+
181
+ After an adversarial multi-agent review (eleven confirmed findings) and a fresh-agent
182
+ dogfooding pass of Layer 3:
183
+
184
+ - A page reached by a redirect is now keyed by the requested URL (and aliased under the
185
+ final URL), so a `handle` from `web_search` stays resolvable by `web_open` after a
186
+ redirect instead of diverging to the post-redirect URL.
187
+ - `web_search` no longer advertises a `next_offset` cursor, because the keyless backends
188
+ do not page results reliably (feeding it back re-showed earlier results); to get
189
+ different results, refine the query. The `offset` field stays plumbed for a backend
190
+ that honors it.
191
+ - The fence neutralizes any copy of its marker case-insensitively (a lowercase or
192
+ mixed-case copy previously survived verbatim in the body).
193
+ - `web_open` fails closed on the astronomically-unlikely same-site handle collision
194
+ (returns `not_opened`) rather than serving the wrong cached page; the short hash was
195
+ widened to 48 bits.
196
+ - A single-URL `web-fetch` failure preserves the specific cause in the error message
197
+ (it previously collapsed to a generic "all 1 url(s) failed").
198
+ - Doc and contract accuracy: `SKILL.md` surfaces the previously-omitted flags
199
+ (`--tier`, `--quiet`, `--datamark` on `web-open`, and others) and the full output
200
+ field list; `AgentPage.fence` is now required in the schema (it was always emitted);
201
+ and the envelope `meta.layer` description lists `agentio`, matching the code.
202
+
203
+ ### Fixed
204
+
205
+ After an adversarial multi-agent review (nine confirmed findings) and a fresh-agent
206
+ dogfooding pass of Layer 2B:
207
+
208
+ - Distinct pages with an empty or whitespace-only body are no longer folded as exact
209
+ duplicates (they all hashed to the empty-string digest); each body-less result, such
210
+ as a snippet-only or failed-extraction page, now survives as its own entry.
211
+ - The page-index query escaper strips control characters, so a NUL byte in a query no
212
+ longer raises a SQLite "unterminated string" error, and an arbitrary query stays safe.
213
+ - The pure-Python BM25 fallback now folds diacritics and tokenizes Unicode letters
214
+ (NFKD plus a Unicode word pattern), so accented and non-Latin queries match the same
215
+ pages as the SQLite FTS5 adapter instead of silently returning nothing; its IDF and
216
+ `resolve_index` ordering after a content change were also aligned with FTS5.
217
+ - The rendered Markdown status line never shows an impossible position (for example
218
+ "page 6 of 3") on a page past the last one, including when dedup shrinks the set.
219
+ - `websearch open --search` degrades a page-index failure to a warning instead of
220
+ leaking a traceback, so a successful fetch and format is never lost to a search error.
221
+
222
+ ### Fixed
223
+
224
+ After an adversarial multi-agent review and a fresh-agent dogfooding pass of Layer 1:
225
+
226
+ - URL canonicalization no longer crashes on a malformed port (e.g. a non-numeric or
227
+ out-of-range port) or an IPv6 literal. Previously that ValueError propagated through
228
+ dedup and aborted the entire search, defeating per-engine fault tolerance; now the
229
+ whole canonicalization body is guarded and IPv6 hosts keep their brackets.
230
+ - SearXNG and ddgs adapters tolerate malformed responses (non-object JSON, non-dict
231
+ entries), coerce upstream fields (score to float, publishedDate to str), and use a
232
+ valid ddgs region for BCP-47 language tags.
233
+ - The router bounds hung engines (a slow engine can no longer block the request past
234
+ its timeout) and reports unknown engine names instead of silently dropping them.
235
+ - The CLI returns a clean error Envelope on invalid input instead of an uncaught
236
+ traceback, and `--help` names the built-in engines.
237
+
238
+ After an adversarial multi-agent review (22 confirmed findings) and a fresh-agent
239
+ dogfooding pass of Layer 2A:
240
+
241
+ - Stopped treating the Imperva `x-iinfo` / `x-cdn` headers as a block (they are on every
242
+ Imperva-proxied response, not just challenges), scanned high-precision DataDome and
243
+ PerimeterX markers on any status/size, and wired the previously dead Akamai body
244
+ markers. Made the markdown-link regexes non-backtracking to remove a ReDoS path.
245
+ - Tuned the quality score so content-typed-but-thin pages (products, forums, listings)
246
+ no longer clear the 0.80 gate the way real articles do, widened the link-ratio band,
247
+ and made paragraph counting robust to single-newline markdown.
248
+ - Decode bodies with declared-charset then detection instead of a blind UTF-8 fallback,
249
+ default a 10 MB streaming transport guard, and skip the HTML extractor on non-HTML
250
+ responses with a surfaced warning.
251
+ - `--output-format text` now emits plain text (it was a silent no-op), added `--quiet`
252
+ for piping the body, and `request_id` is present in `meta` on every response path.
253
+
254
+ ### Notes
255
+
256
+ - `fusion.method: score_convex` is accepted but currently falls back to `weighted_rrf`
257
+ (a warning is emitted).
258
+ - Layer 2A still returns clean page content unmodified (so piping and composition stay
259
+ clean); the untrusted-content fence is applied at the Layer 3 agent boundary
260
+ (`web_fetch`/`web_open`), not in Layer 2A.
261
+ - The FastMCP server ships in the base install (fastmcp is a base dependency). Harness
262
+ packaging and distribution (npx skills add, the Claude plugin and marketplace, the MCP
263
+ registry `server.json`, and PyPI/uvx) are built; see `docs/INSTALL.md`.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Hector Oviedo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.