Skip to content

feat(web): add Keenable as a native web search & extract provider (keyless-capable) - #49758

Open
IlyaGusev wants to merge 1 commit into
NousResearch:mainfrom
keenableai:feat/keenable-integration
Open

feat(web): add Keenable as a native web search & extract provider (keyless-capable)#49758
IlyaGusev wants to merge 1 commit into
NousResearch:mainfrom
keenableai:feat/keenable-integration

Conversation

@IlyaGusev

@IlyaGusev IlyaGusev commented Jun 20, 2026

Copy link
Copy Markdown

What does this PR do?

Adds Keenable (low-latency web search + page-to-markdown fetch for agents) as a native Web Search & Extract provider, following the plugin architecture from #25182 and the provider precedents in #29042 (xAI) / #46559 (iFlow).

Users select Keenable from the Web Search & Extract provider picker (or web.backend: keenable in config.yaml). It maps:

  • WebSearchProvider.search()GET /v1/search?query= (result count applied client-side)
  • WebSearchProvider.extract()GET /v1/fetch?url=

against https://api.keenable.ai (endpoints/DTOs per the published OpenAPI spec at docs.keenable.ai/api-reference). The X-API-Key header is sent when a key is configured; requests also send X-Keenable-Title: Hermes for client attribution.

The PR also ships two optional, separable surfaces for the same vendor (see Scope below): an optional CLI skill and an MCP catalog entry.

Motivation

Keenable fits the Web Search & Extract provider layer (search + page fetch in one provider, no browser sessions). Keyless, but opt-in. Keenable's free tier works without a key: when KEENABLE_API_KEY is unset the provider calls the /public endpoint variants (rate-limited) and omits X-API-Key, mirroring Keenable's official MCP client. To avoid the silent-default issue the keyless-Parallel revert (#46350) addressed, keenable is never auto-selectedis_available() stays key-gated so it is excluded from the no-credential fallback, and it serves keyless only when explicitly chosen via web.backend / web.*_backend. A KEENABLE_API_KEY raises rate limits.

Scope note (happy to split)

This PR bundles three logical pieces for one vendor. If reviewers prefer one-change-per-PR, I'll split into:

  1. Native providerplugins/web/keenable/ + core wiring (the primary change).
  2. Optional CLI skilloptional-skills/research/keenable-cli/.
  3. MCP catalog entryoptional-mcps/keenable/.

Type of Change

  • ✨ New feature (non-breaking change that adds functionality)
  • 🎯 New skill (optional, not bundled)

Changes Made

Native provider

  • plugins/web/keenable/{plugin.yaml,__init__.py,provider.py}KeenableWebSearchProvider (search + extract), keyless /public fallback, KEENABLE_API_URL override.
  • tools/web_tools.py — register keenable in the configured-backend sets, auto-detect candidate, _is_backend_available, and tool-metadata env vars.
  • hermes_cli/config.pyKEENABLE_API_KEY in OPTIONAL_ENV_VARS + the API-key display/allowlist lists.
  • agent/web_search_registry.py — added to the legacy fallback preference.

Optional skill + MCP catalog

  • optional-skills/research/keenable-cli/SKILL.md — CLI skill (HARDLINE-compliant: 56-char description, modern section order).
  • optional-mcps/keenable/manifest.yaml — remote HTTP MCP catalog entry (api.keenable.ai/mcp).

Packaging / attribution / docs

  • pyproject.tomldata-files target so the MCP manifest ships in the wheel.
  • scripts/release.pyAUTHOR_MAP entry.
  • website/docs/** + website/i18n/zh-Hans/** — Keenable added to provider/env-var tables (EN + zh).

Tests

  • tests/tools/conftest.py + tests/plugins/web/test_web_search_provider_plugins.py — existing provider-set/capability assertions updated to include keenable.

How to Test

# Provider discovery + capability + ABC conformance (no network)
uv run --extra dev pytest tests/plugins/web/test_web_search_provider_plugins.py -q
# MCP catalog + packaging
uv run --extra dev pytest tests/hermes_cli/test_mcp_catalog.py tests/test_packaging_metadata.py -q
# Live smoke (keyless works; export KEENABLE_API_KEY to raise limits)
hermes config set web.backend keenable
hermes -q "search the web for the latest on AI agents and summarize the top result"

Checklist

  • I've read the Contributing Guide
  • Commits follow Conventional Commits
  • PR contains only Keenable-related changes
  • pytest tests/ -q locally — not run by the author locally (no dev env); CI is the gate. Provider request/response shaping verified via injected-httpx checks (keyed + keyless paths).
  • Dedicated provider test (tests/tools/test_web_providers_keenable.py) — not yet added; existing provider-set tests updated instead. Happy to add to match the xAI/iFlow pattern.
  • Tested on Linux / Python 3.11 (request shapes + normalization)
  • Cross-platform: pure-Python httpx; skill declares all three platforms; MCP is a remote URL
  • Docs updated (EN + zh-Hans provider/env-var tables)
  • No new PyPI dependencies (reuses existing httpx)

Notes for maintainers

Endpoints/DTOs follow Keenable's published OpenAPI spec (docs.keenable.ai/api-reference). Live keyless smoke test passed against api.keenable.ai (/v1/search/public + /v1/fetch/public): search returns ranked results, fetch returns page markdown. The smoke test also caught and fixed a spec/live mismatch — /v1/search takes no count param, so limit is applied client-side. The keyed X-API-Key path is covered by injected-httpx checks (no raw API key on hand for a live keyed run). Squash-merge recommended.

@IlyaGusev
IlyaGusev requested a review from a team June 20, 2026 18:38
@alt-glitch alt-glitch added type/feature New feature or request P3 Low — cosmetic, nice to have comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint tool/web Web search and extraction comp/plugins Plugin system and bundled plugins labels Jun 20, 2026
@IlyaGusev IlyaGusev changed the title feat(web): add Keenable as a native web search & extract provider feat(web): add Keenable as a native web search & extract provider (keyless-capable) Jun 20, 2026

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for building this against the web-provider interface. The native-provider portion needs a scope change before it can be considered.

Problems

  • plugins/web/keenable/ is a new in-tree third-party vendor integration, which conflicts with the standalone-plugin policy in AGENTS.md:126-136. The optional skill and MCP manifest are separable contributions.
  • plugins/web/keenable/provider.py:64 unconditionally sends X-Keenable-Title: Hermes; AGENTS.md:118-121 requires an explicit generic opt-in for outbound attribution tagging.
  • plugins/web/keenable/provider.py:54-59 reads credentials directly from os.getenv, unlike the config-aware provider helper at agent/web_search_provider.py:59-81.
  • The PR's provider test changes are inventory assertions only (tests/plugins/web/test_web_search_provider_plugins.py:81-132); request shaping and keyless/keyed behavior remain untested.

Suggested changes

  • Preserve the optional skill/MCP work if desired, but distribute the native provider as a standalone plugin.
  • Remove or opt-in-gate attribution, use get_provider_env, and add mocked provider behavior tests.

Current tools/web_tools.py:231-327 already resolves registered providers generically, consistent with the #31873 discussion, so the vendor-specific backend-gate edits need a fresh port rather than a direct salvage.

This is an automated hermes-sweeper review.

Comment thread plugins/web/keenable/provider.py Outdated


def _keenable_headers(api_key: str) -> Dict[str, str]:
"""Request headers; ``X-API-Key`` only when a key is present (keyless otherwise)."""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use agent.web_search_provider.get_provider_env("KEENABLE_API_KEY") here and in is_available(). Direct os.getenv() bypasses Hermes-managed .env lookup for gateway, delegate-child, and subprocess contexts; see agent/web_search_provider.py:59-81.

Comment thread plugins/web/keenable/provider.py Outdated
def _endpoint(base_url: str, path: str, api_key: str) -> str:
"""Keyless calls hit the ``/public`` variant (no auth, rate-limited)."""
return f"{base_url}{path}" if api_key else f"{base_url}{path}/public"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This unconditionally tags every outbound request with Hermes attribution. AGENTS.md:118-121 requires a generic user-facing opt-in before adding outbound attribution or third-party identifier tagging; remove this header or implement that prerequisite.

@@ -96,6 +98,7 @@ def test_all_seven_plugins_present_in_registry(self) -> None:
("parallel", True, True),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a dedicated mocked provider test file covering keyed and keyless request URLs/headers, search normalization and truncation, extract result/error shapes, and config-managed credential lookup. This table only verifies inventory and capability declarations.

@teknium1 teknium1 added sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-contained Sweeper blast radius: contained — one narrow path / opt-in / few users labels Jul 14, 2026
Native web-search provider moved out to a standalone pip plugin
(hermes-keenable-web) per the standalone-plugin policy. This keeps only
the two separable, in-tree parts: the optional MCP catalog entry and the
research skill.
@ilya-bogin-keenable
ilya-bogin-keenable force-pushed the feat/keenable-integration branch from 1084011 to 1f7668d Compare July 18, 2026 14:04
@ilya-bogin-keenable

Copy link
Copy Markdown

Reworked this per the review.

I pulled the in-tree provider and all its wiring. It now ships as a standalone pip plugin, hermes-keenable-web (on PyPI), which is the standalone-plugin path AGENTS.md asks for. That plugin reads creds through get_provider_env, has mocked search/extract behavior tests, and registers via the hermes_agent.plugins entry point, so it lands in the Search Provider picker once installed. The attribution header question goes away with the in-tree provider too; the standalone build keeps to the same policy.

What's left in this PR is just the two separable pieces you flagged: the MCP catalog entry (optional-mcps/keenable/) and the research skill (optional-skills/research/keenable-cli/). Neither touches the provider path. Happy to adjust either if something's off.

@ilya-bogin-keenable

Copy link
Copy Markdown

@teknium1 could you take a look please?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/plugins Plugin system and bundled plugins P3 Low — cosmetic, nice to have sweeper:blast-contained Sweeper blast radius: contained — one narrow path / opt-in / few users sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data tool/web Web search and extraction type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants