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.
- websearch_skill-0.1.0/.gitignore +42 -0
- websearch_skill-0.1.0/CHANGELOG.md +263 -0
- websearch_skill-0.1.0/LICENSE +21 -0
- websearch_skill-0.1.0/PKG-INFO +350 -0
- websearch_skill-0.1.0/README.md +321 -0
- websearch_skill-0.1.0/contracts/README.md +71 -0
- websearch_skill-0.1.0/contracts/agent-io.schema.json +165 -0
- websearch_skill-0.1.0/contracts/arxiv.schema.json +78 -0
- websearch_skill-0.1.0/contracts/envelope.schema.json +58 -0
- websearch_skill-0.1.0/contracts/extract.schema.json +119 -0
- websearch_skill-0.1.0/contracts/fetch.schema.json +111 -0
- websearch_skill-0.1.0/contracts/format.schema.json +216 -0
- websearch_skill-0.1.0/contracts/github.schema.json +74 -0
- websearch_skill-0.1.0/contracts/search.schema.json +163 -0
- websearch_skill-0.1.0/contracts/store.schema.json +158 -0
- websearch_skill-0.1.0/docker/searxng/README.md +62 -0
- websearch_skill-0.1.0/docker/searxng/core-config/settings.yml +23 -0
- websearch_skill-0.1.0/docker/searxng/docker-compose.yml +29 -0
- websearch_skill-0.1.0/docs/ARCHITECTURE.md +235 -0
- websearch_skill-0.1.0/docs/BENCHMARK.md +77 -0
- websearch_skill-0.1.0/docs/INSTALL.md +204 -0
- websearch_skill-0.1.0/pyproject.toml +102 -0
- websearch_skill-0.1.0/skills/web-search/SKILL.md +179 -0
- websearch_skill-0.1.0/src/websearch/__init__.py +8 -0
- websearch_skill-0.1.0/src/websearch/__main__.py +6 -0
- websearch_skill-0.1.0/src/websearch/cli.py +1014 -0
- websearch_skill-0.1.0/src/websearch/envelope.py +97 -0
- websearch_skill-0.1.0/src/websearch/errors.py +25 -0
- websearch_skill-0.1.0/src/websearch/layer1_search/__init__.py +68 -0
- websearch_skill-0.1.0/src/websearch/layer1_search/adapters/__init__.py +6 -0
- websearch_skill-0.1.0/src/websearch/layer1_search/adapters/ddgs_engine.py +82 -0
- websearch_skill-0.1.0/src/websearch/layer1_search/adapters/searxng.py +132 -0
- websearch_skill-0.1.0/src/websearch/layer1_search/canonical.py +100 -0
- websearch_skill-0.1.0/src/websearch/layer1_search/capability.py +42 -0
- websearch_skill-0.1.0/src/websearch/layer1_search/dedup.py +94 -0
- websearch_skill-0.1.0/src/websearch/layer1_search/fusion.py +71 -0
- websearch_skill-0.1.0/src/websearch/layer1_search/models.py +123 -0
- websearch_skill-0.1.0/src/websearch/layer1_search/port.py +67 -0
- websearch_skill-0.1.0/src/websearch/layer1_search/router.py +271 -0
- websearch_skill-0.1.0/src/websearch/layer2_extract/__init__.py +87 -0
- websearch_skill-0.1.0/src/websearch/layer2_extract/blocks.py +166 -0
- websearch_skill-0.1.0/src/websearch/layer2_extract/egress.py +91 -0
- websearch_skill-0.1.0/src/websearch/layer2_extract/exceptions.py +15 -0
- websearch_skill-0.1.0/src/websearch/layer2_extract/extractors/__init__.py +1 -0
- websearch_skill-0.1.0/src/websearch/layer2_extract/extractors/trafilatura_extractor.py +212 -0
- websearch_skill-0.1.0/src/websearch/layer2_extract/fetch_router.py +90 -0
- websearch_skill-0.1.0/src/websearch/layer2_extract/fetchers/__init__.py +1 -0
- websearch_skill-0.1.0/src/websearch/layer2_extract/fetchers/curl_cffi_fetcher.py +158 -0
- websearch_skill-0.1.0/src/websearch/layer2_extract/fetchers/httpx_fetcher.py +137 -0
- websearch_skill-0.1.0/src/websearch/layer2_extract/fetchers/util.py +50 -0
- websearch_skill-0.1.0/src/websearch/layer2_extract/models.py +198 -0
- websearch_skill-0.1.0/src/websearch/layer2_extract/pipeline.py +206 -0
- websearch_skill-0.1.0/src/websearch/layer2_extract/ports.py +49 -0
- websearch_skill-0.1.0/src/websearch/layer2_extract/quality.py +254 -0
- websearch_skill-0.1.0/src/websearch/layer2_format/__init__.py +83 -0
- websearch_skill-0.1.0/src/websearch/layer2_format/anthropic_blocks.py +51 -0
- websearch_skill-0.1.0/src/websearch/layer2_format/chunk.py +78 -0
- websearch_skill-0.1.0/src/websearch/layer2_format/dedup.py +234 -0
- websearch_skill-0.1.0/src/websearch/layer2_format/exceptions.py +7 -0
- websearch_skill-0.1.0/src/websearch/layer2_format/format_pipeline.py +240 -0
- websearch_skill-0.1.0/src/websearch/layer2_format/ids.py +36 -0
- websearch_skill-0.1.0/src/websearch/layer2_format/models.py +305 -0
- websearch_skill-0.1.0/src/websearch/layer2_format/ports.py +76 -0
- websearch_skill-0.1.0/src/websearch/layer2_format/renderer.py +132 -0
- websearch_skill-0.1.0/src/websearch/layer2_format/store/__init__.py +39 -0
- websearch_skill-0.1.0/src/websearch/layer2_format/store/_common.py +100 -0
- websearch_skill-0.1.0/src/websearch/layer2_format/store/memory_bm25.py +286 -0
- websearch_skill-0.1.0/src/websearch/layer2_format/store/sqlite_fts5.py +258 -0
- websearch_skill-0.1.0/src/websearch/layer2_format/tokens.py +38 -0
- websearch_skill-0.1.0/src/websearch/layer3_agentio/__init__.py +58 -0
- websearch_skill-0.1.0/src/websearch/layer3_agentio/facade.py +528 -0
- websearch_skill-0.1.0/src/websearch/layer3_agentio/fence.py +112 -0
- websearch_skill-0.1.0/src/websearch/layer3_agentio/mcp_server.py +418 -0
- websearch_skill-0.1.0/src/websearch/layer3_agentio/models.py +143 -0
- websearch_skill-0.1.0/src/websearch/layer3_agentio/pagination.py +47 -0
- websearch_skill-0.1.0/src/websearch/tool_arxiv/__init__.py +29 -0
- websearch_skill-0.1.0/src/websearch/tool_arxiv/client.py +300 -0
- websearch_skill-0.1.0/src/websearch/tool_arxiv/models.py +79 -0
- websearch_skill-0.1.0/src/websearch/tool_github/__init__.py +29 -0
- websearch_skill-0.1.0/src/websearch/tool_github/client.py +265 -0
- websearch_skill-0.1.0/src/websearch/tool_github/models.py +63 -0
- websearch_skill-0.1.0/tests/__init__.py +0 -0
- websearch_skill-0.1.0/tests/conftest.py +259 -0
- websearch_skill-0.1.0/tests/test_adapters.py +145 -0
- websearch_skill-0.1.0/tests/test_blocks.py +126 -0
- websearch_skill-0.1.0/tests/test_canonical.py +71 -0
- websearch_skill-0.1.0/tests/test_contracts.py +92 -0
- websearch_skill-0.1.0/tests/test_dedup.py +51 -0
- websearch_skill-0.1.0/tests/test_distribution.py +205 -0
- websearch_skill-0.1.0/tests/test_egress.py +61 -0
- websearch_skill-0.1.0/tests/test_engine_passthrough.py +117 -0
- websearch_skill-0.1.0/tests/test_extract_adapter.py +62 -0
- websearch_skill-0.1.0/tests/test_extract_contracts.py +110 -0
- websearch_skill-0.1.0/tests/test_extract_pipeline.py +159 -0
- websearch_skill-0.1.0/tests/test_fetch_router.py +129 -0
- websearch_skill-0.1.0/tests/test_fetchers.py +174 -0
- websearch_skill-0.1.0/tests/test_format_chunk.py +63 -0
- websearch_skill-0.1.0/tests/test_format_contracts.py +112 -0
- websearch_skill-0.1.0/tests/test_format_dedup.py +139 -0
- websearch_skill-0.1.0/tests/test_format_pipeline.py +272 -0
- websearch_skill-0.1.0/tests/test_fusion.py +87 -0
- websearch_skill-0.1.0/tests/test_layer1_search_e2e.py +146 -0
- websearch_skill-0.1.0/tests/test_layer2_extract_e2e.py +126 -0
- websearch_skill-0.1.0/tests/test_layer3_agentio.py +263 -0
- websearch_skill-0.1.0/tests/test_layer3_cli_e2e.py +123 -0
- websearch_skill-0.1.0/tests/test_layer3_contracts.py +75 -0
- websearch_skill-0.1.0/tests/test_layer3_fence.py +83 -0
- websearch_skill-0.1.0/tests/test_layer3_pagination.py +52 -0
- websearch_skill-0.1.0/tests/test_open_e2e.py +156 -0
- websearch_skill-0.1.0/tests/test_quality.py +127 -0
- websearch_skill-0.1.0/tests/test_robustness_polish.py +275 -0
- websearch_skill-0.1.0/tests/test_router.py +187 -0
- websearch_skill-0.1.0/tests/test_store.py +248 -0
- websearch_skill-0.1.0/tests/test_tool_arxiv.py +344 -0
- 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.
|