Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .github/workflows/hermes-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,19 @@ jobs:
with:
python-version: "3.12"

# plugin.yaml is authoritative for X-TF-Client-Version on directory installs,
# so pyproject.toml drifting past it ships a stale version header silently.
- name: plugin.yaml version matches pyproject.toml
run: |
set -euo pipefail
manifest=$(sed -n 's/^version:[[:space:]]*//p' plugin.yaml | head -1 | tr -d "\"' ")
project=$(sed -n 's/^version[[:space:]]*=[[:space:]]*"\(.*\)"/\1/p' pyproject.toml | head -1)
echo "plugin.yaml=$manifest pyproject.toml=$project"
if [ "$manifest" != "$project" ]; then
echo "::error file=hermes/plugin.yaml::plugin.yaml is $manifest but pyproject.toml is $project"
exit 1
Comment thread
londondavila marked this conversation as resolved.
fi

- run: python -m pip install --upgrade pip

- run: python -m pip install -e . -r requirements-dev.txt
Expand Down
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,17 @@ TinyFish Web Agent provides AI-powered web automation using natural language ins

Want to contribute to these integrations? We love adding TinyFish Web Agent to new ecosystems! Check out individual integration directories for setup and development instructions.

## Identifying your integration to TinyFish

Every integration that calls the REST API directly must send two headers on every request. Today `hermes` and `n8n` do; `dify` does not yet.

| header | value |
|---|---|
| `X-TF-Client-Name` | the bare integration token — the same value used for `api_integration` where one exists: `hermes`, `n8n` |
| `X-TF-Client-Version` | the integration's own published version |

Do not send `X-TF-Request-Origin`; `api` is the transport and the server derives it. Integrations that wrap the `tinyfish` SDK must set `TF_CLIENT_NAME` / `TF_CLIENT_VERSION` alongside `TF_API_INTEGRATION` instead — `langchain` and `google-adk` set only `TF_API_INTEGRATION` today. Without these, telemetry cannot tell the integration from a hand-written curl.
Comment thread
londondavila marked this conversation as resolved.
Outdated

## License

These integrations are licensed under the MIT License.
1 change: 1 addition & 0 deletions hermes/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ __pycache__/
.mypy_cache/
.ruff_cache/
.pytest_cache/
uv.lock
2 changes: 1 addition & 1 deletion hermes/plugin.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: tinyfish
version: 0.1.0
version: 0.1.1
description: "First-party TinyFish provider plugin for Hermes Agent: Search and Fetch over the TinyFish REST APIs with API-key auth, plus credit-gated Browser sessions."
author: TinyFish
kind: backend
Expand Down
2 changes: 1 addition & 1 deletion hermes/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "tinyfish-hermes"
version = "0.1.0"
version = "0.1.1"
description = "TinyFish web provider plugin for Hermes Agent - Search and Fetch over the TinyFish REST APIs"
readme = "README.md"
license = { text = "MIT" }
Expand Down
19 changes: 11 additions & 8 deletions hermes/tests/test_rest_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@

from tinyfish_hermes import rest_client

_AUTH_HEADERS = {
"X-API-Key": "tf_test",
"Accept": "application/json",
"X-TF-Client-Name": "hermes",
"X-TF-Client-Version": "0.1.1",
}


def _response(
method: str,
Expand Down Expand Up @@ -61,7 +68,7 @@ def fake_get(url: str, **kwargs: Any) -> httpx.Response:
"page": 2,
"purpose": "research",
},
"headers": {"X-API-Key": "tf_test", "Accept": "application/json"},
"headers": _AUTH_HEADERS,
"timeout": 12.5,
}

Expand Down Expand Up @@ -115,11 +122,7 @@ def fake_post(url: str, **kwargs: Any) -> httpx.Response:
"ttl": 300,
"per_url_timeout_ms": 2500,
},
"headers": {
"X-API-Key": "tf_test",
"Accept": "application/json",
"Content-Type": "application/json",
},
"headers": {**_AUTH_HEADERS, "Content-Type": "application/json"},
"timeout": 22.0,
}

Expand Down Expand Up @@ -283,7 +286,7 @@ def fake_delete(url: str, **kwargs: Any) -> httpx.Response:
is True
)
assert captured["url"] == f"{rest_client.BROWSER_URL}/sess_123"
assert captured["headers"] == {"X-API-Key": "tf_test", "Accept": "application/json"}
assert captured["headers"] == _AUTH_HEADERS
assert captured["timeout"] == 9.0


Expand Down Expand Up @@ -424,7 +427,7 @@ def fake_get(url: str, **kwargs: Any) -> httpx.Response:
assert call() == {"items": []}
assert captured == {
"url": expected_url,
"headers": {"X-API-Key": "tf_test", "Accept": "application/json"},
"headers": _AUTH_HEADERS,
"timeout": 11.0,
}

Expand Down
42 changes: 41 additions & 1 deletion hermes/tests/test_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,23 @@
import tinyfish_hermes as plugin


def test_version_prefers_installed_distribution_metadata(
def test_version_prefers_adjacent_manifest_over_stale_metadata(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
manifest = tmp_path / "plugin.yaml"
manifest.write_text("version: 1.2.3\n", encoding="utf-8")
monkeypatch.setattr(plugin, "_PLUGIN_MANIFEST", manifest)
monkeypatch.setattr(plugin.metadata, "version", lambda name: "9.8.7")

assert plugin._resolve_version() == "1.2.3"


def test_version_uses_installed_metadata_when_no_manifest(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
monkeypatch.setattr(plugin, "_PLUGIN_MANIFEST", tmp_path / "missing.yaml")
monkeypatch.setattr(plugin.metadata, "version", lambda name: "9.8.7")

assert plugin._resolve_version() == "9.8.7"


Expand Down Expand Up @@ -53,3 +62,34 @@ def test_public_version_is_exported() -> None:
assert isinstance(plugin.__version__, str)
assert plugin.__version__
assert "__version__" in plugin.__all__


def test_version_rejects_non_ascii_manifest_value(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
manifest = tmp_path / "plugin.yaml"
manifest.write_text("version: 0.1.1é\n", encoding="utf-8")
monkeypatch.setattr(plugin, "_PLUGIN_MANIFEST", manifest)

def missing_distribution(name: str) -> str:
raise metadata.PackageNotFoundError(name)

monkeypatch.setattr(plugin.metadata, "version", missing_distribution)

assert plugin._resolve_version() == "0+unknown"


def test_version_degrades_to_metadata_when_manifest_is_non_ascii(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
manifest = tmp_path / "plugin.yaml"
manifest.write_text("version: 0.1.1é\n", encoding="utf-8")
monkeypatch.setattr(plugin, "_PLUGIN_MANIFEST", manifest)
monkeypatch.setattr(plugin.metadata, "version", lambda name: "9.8.7")

assert plugin._resolve_version() == "9.8.7"


def test_manifest_version_matches_the_pinned_header_value() -> None:
manifest = Path(plugin.__file__).resolve().parents[1] / "plugin.yaml"
assert plugin._version_from_plugin_manifest(manifest) == "0.1.1"
17 changes: 13 additions & 4 deletions hermes/tinyfish_hermes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,21 @@ def _version_from_plugin_manifest(path: Path | None = None) -> str | None:
return None


def _resolve_version() -> str:
def _version_from_metadata() -> str | None:
try:
installed_version = metadata.version(_DISTRIBUTION_NAME)
return metadata.version(_DISTRIBUTION_NAME)
except Exception: # directory installs must not depend on package metadata
installed_version = ""
return installed_version or _version_from_plugin_manifest() or "0+unknown"
return None


def _resolve_version() -> str:
# Wheels omit plugin.yaml: an adjacent manifest beats stale dist metadata.
# httpx sends headers as ASCII, so one stray byte fails every request; a
# garbled source degrades to the next candidate, not straight to the fallback.
for candidate in (_version_from_plugin_manifest(), _version_from_metadata()):
if candidate and candidate.isascii():
return candidate
return "0+unknown"


__version__ = _resolve_version()
Expand Down
10 changes: 9 additions & 1 deletion hermes/tinyfish_hermes/rest_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

import httpx

from . import __version__

SEARCH_URL = "https://api.search.tinyfish.ai"
FETCH_URL = "https://api.fetch.tinyfish.ai"
FETCH_MAX_URLS = 10
Expand All @@ -31,7 +33,13 @@ class TinyFishWalletNotFound(TinyFishRestError):


def _headers(api_key: str) -> dict[str, str]:
return {"X-API-Key": api_key, "Accept": "application/json"}
# Without these, telemetry cannot tell the plugin from raw curl.
return {
"X-API-Key": api_key,
"Accept": "application/json",
"X-TF-Client-Name": "hermes",
"X-TF-Client-Version": __version__,
}


def _json_headers(api_key: str) -> dict[str, str]:
Expand Down