From e54e0fec8129ded1831d1d625aeabeb6708df23e Mon Sep 17 00:00:00 2001 From: londondavila Date: Tue, 1 Sep 2026 11:39:11 -0700 Subject: [PATCH 1/5] feat(dify): identify plugin requests with X-TF-Client-Name/Version MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Search and Fetch calls from this plugin land in TinyFish telemetry as untagged request_origin=api. Runs were already tagged via the api_integration payload field, but search/fetch have no payload to carry it. The server reads X-TF-Client-Name / X-TF-Client-Version on every route. - _api_headers adds X-TF-Client-Name: tinyfish-dify and X-TF-Client-Version read from manifest.yaml; both call sites (_tf_request and run_sse's stream) already use it - tools/constants.PLUGIN_VERSION parsed from the manifest so the header cannot drift from the published version - bump 0.0.4 → 0.0.5 (both manifest version fields, matching prior bumps) - tests/test_headers.py: header contract and version/manifest agreement X-TF-Request-Origin deliberately unset: `api` stays the transport. Attributes new traffic only; needs a marketplace republish. Testing: pytest 2 passed. Repo has no dify lint/test CI; ruff findings in untouched tool files are pre-existing. Co-Authored-By: Claude Code Claude-Session: https://claude.ai/code/session_0138Vn8E8TfZkUWd3StqDgjp --- dify/manifest.yaml | 4 ++-- dify/tests/__init__.py | 0 dify/tests/test_headers.py | 26 ++++++++++++++++++++++++++ dify/tools/base.py | 9 +++++++-- dify/tools/constants.py | 17 +++++++++++++++++ 5 files changed, 52 insertions(+), 4 deletions(-) create mode 100644 dify/tests/__init__.py create mode 100644 dify/tests/test_headers.py diff --git a/dify/manifest.yaml b/dify/manifest.yaml index a96685e..ee00de3 100644 --- a/dify/manifest.yaml +++ b/dify/manifest.yaml @@ -1,4 +1,4 @@ -version: 0.0.4 +version: 0.0.5 type: plugin author: tinyfish name: tinyfish-web-agent @@ -32,7 +32,7 @@ plugins: tags: - productivity meta: - version: 0.0.4 + version: 0.0.5 arch: - amd64 - arm64 diff --git a/dify/tests/__init__.py b/dify/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dify/tests/test_headers.py b/dify/tests/test_headers.py new file mode 100644 index 0000000..b14b9d7 --- /dev/null +++ b/dify/tests/test_headers.py @@ -0,0 +1,26 @@ +from types import SimpleNamespace + +from tools.base import TinyfishMixin +from tools.constants import PLUGIN_VERSION + + +def _tool() -> TinyfishMixin: + tool = TinyfishMixin() + tool.runtime = SimpleNamespace(credentials={"api_key": "tf_test"}) # type: ignore[attr-defined] + return tool + + +def test_headers_identify_the_plugin() -> None: + headers = _tool()._api_headers + assert headers["X-API-Key"] == "tf_test" + assert headers["X-TF-Client-Name"] == "tinyfish-dify" + assert headers["X-TF-Client-Version"] == PLUGIN_VERSION + assert "X-TF-Request-Origin" not in headers + + +def test_plugin_version_matches_manifest() -> None: + with open("manifest.yaml", encoding="utf-8") as fh: + manifest_version = next( + line.split(":", 1)[1].strip() for line in fh if line.startswith("version:") + ) + assert PLUGIN_VERSION == manifest_version diff --git a/dify/tools/base.py b/dify/tools/base.py index d1bb0e5..ef85c34 100644 --- a/dify/tools/base.py +++ b/dify/tools/base.py @@ -2,7 +2,7 @@ import httpx -from tools.constants import API_BASE_URL +from tools.constants import API_BASE_URL, PLUGIN_VERSION class TinyfishMixin: @@ -10,7 +10,12 @@ class TinyfishMixin: @property def _api_headers(self) -> dict[str, str]: - return {"X-API-Key": self.runtime.credentials["api_key"]} + # Without these the server files every call as untagged `api`. + return { + "X-API-Key": self.runtime.credentials["api_key"], + "X-TF-Client-Name": "tinyfish-dify", + "X-TF-Client-Version": PLUGIN_VERSION, + } def _tf_request( self, diff --git a/dify/tools/constants.py b/dify/tools/constants.py index 0b0d72c..d2dea6c 100644 --- a/dify/tools/constants.py +++ b/dify/tools/constants.py @@ -1 +1,18 @@ +from pathlib import Path + API_BASE_URL = "https://agent.tinyfish.ai" + +_MANIFEST = Path(__file__).resolve().parents[1] / "manifest.yaml" + + +def _manifest_version() -> str: + try: + for line in _MANIFEST.read_text(encoding="utf-8").splitlines(): + if line.startswith("version:"): + return line.split(":", 1)[1].split("#", 1)[0].strip().strip("'\"") + except OSError: + pass + return "0+unknown" + + +PLUGIN_VERSION = _manifest_version() From d86135fdab253f7d77c75e4fec50e03db9ec7261 Mon Sep 17 00:00:00 2001 From: londondavila Date: Tue, 1 Sep 2026 11:46:53 -0700 Subject: [PATCH 2/5] chore(dify): keep tests out of the .difypkg, make the version test real Review fixes. The packager honors .difyignore only, so tests/ was shipping in the marketplace artifact. The manifest test re-parsed the file the same way the constant does and opened it relative to cwd, so it could only fail by being run from the wrong directory. - .difyignore: tests/ - test asserts a real semver and that the manifest carries it, path resolved from __file__ - comment: request_origin stays `api`; what changes is client_name Testing: pytest 2 passed from dify/ and from the repo root. Co-Authored-By: Claude Code Claude-Session: https://claude.ai/code/session_0138Vn8E8TfZkUWd3StqDgjp --- dify/.difyignore | 1 + dify/tests/test_headers.py | 15 ++++++++------- dify/tools/base.py | 2 +- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/dify/.difyignore b/dify/.difyignore index 4685c5e..c20647e 100644 --- a/dify/.difyignore +++ b/dify/.difyignore @@ -182,3 +182,4 @@ Thumbs.db # To prevent packaging repetitively *.difypkg +tests/ diff --git a/dify/tests/test_headers.py b/dify/tests/test_headers.py index b14b9d7..e126eb8 100644 --- a/dify/tests/test_headers.py +++ b/dify/tests/test_headers.py @@ -1,8 +1,12 @@ +import re +from pathlib import Path from types import SimpleNamespace from tools.base import TinyfishMixin from tools.constants import PLUGIN_VERSION +MANIFEST = Path(__file__).resolve().parents[1] / "manifest.yaml" + def _tool() -> TinyfishMixin: tool = TinyfishMixin() @@ -15,12 +19,9 @@ def test_headers_identify_the_plugin() -> None: assert headers["X-API-Key"] == "tf_test" assert headers["X-TF-Client-Name"] == "tinyfish-dify" assert headers["X-TF-Client-Version"] == PLUGIN_VERSION - assert "X-TF-Request-Origin" not in headers -def test_plugin_version_matches_manifest() -> None: - with open("manifest.yaml", encoding="utf-8") as fh: - manifest_version = next( - line.split(":", 1)[1].strip() for line in fh if line.startswith("version:") - ) - assert PLUGIN_VERSION == manifest_version +def test_plugin_version_is_a_real_release_version() -> None: + # The publish workflow greps `^version:` — same anchor the constant uses. + assert re.fullmatch(r"\d+\.\d+\.\d+", PLUGIN_VERSION), PLUGIN_VERSION + assert f"version: {PLUGIN_VERSION}\n" in MANIFEST.read_text(encoding="utf-8") diff --git a/dify/tools/base.py b/dify/tools/base.py index ef85c34..5854c91 100644 --- a/dify/tools/base.py +++ b/dify/tools/base.py @@ -10,7 +10,7 @@ class TinyfishMixin: @property def _api_headers(self) -> dict[str, str]: - # Without these the server files every call as untagged `api`. + # Without these, telemetry cannot tell the plugin from raw curl. return { "X-API-Key": self.runtime.credentials["api_key"], "X-TF-Client-Name": "tinyfish-dify", From 3304a8ce13325c579642dcff49c5fea125733839 Mon Sep 17 00:00:00 2001 From: londondavila Date: Tue, 1 Sep 2026 11:50:32 -0700 Subject: [PATCH 3/5] test(dify): anchor manifest version check to column 0 The substring check also matched the indented meta.version line. Co-Authored-By: Claude Code Claude-Session: https://claude.ai/code/session_0138Vn8E8TfZkUWd3StqDgjp --- dify/tests/test_headers.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/dify/tests/test_headers.py b/dify/tests/test_headers.py index e126eb8..9e23dd4 100644 --- a/dify/tests/test_headers.py +++ b/dify/tests/test_headers.py @@ -24,4 +24,9 @@ def test_headers_identify_the_plugin() -> None: def test_plugin_version_is_a_real_release_version() -> None: # The publish workflow greps `^version:` — same anchor the constant uses. assert re.fullmatch(r"\d+\.\d+\.\d+", PLUGIN_VERSION), PLUGIN_VERSION - assert f"version: {PLUGIN_VERSION}\n" in MANIFEST.read_text(encoding="utf-8") + # Column 0 only: meta.version is indented and is the manifest-format version. + assert re.search( + rf"^version: {re.escape(PLUGIN_VERSION)}$", + MANIFEST.read_text(encoding="utf-8"), + re.M, + ) From b7947fd1eb1188022cae1ea4ed71e9f992d52e02 Mon Sep 17 00:00:00 2001 From: londondavila Date: Tue, 1 Sep 2026 11:52:04 -0700 Subject: [PATCH 4/5] refactor(dify): bare client token; parse the manifest with yaml MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Aligns X-TF-Client-Name with the repo contract (bare token, same as api_integration) and replaces the hand-rolled version scanner with yaml.safe_load — pyyaml is already a hard dependency of dify_plugin and is the loader the Dify daemon uses on the same file. - X-TF-Client-Name: dify - constants: yaml.safe_load(manifest)["version"], 0+unknown on any failure - requirements: pyyaml explicit instead of transitive - test: column-0 anchor on the manifest check Testing: pytest 2 passed (uv run --with pytest --with httpx --with pyyaml). Co-Authored-By: Claude Code Claude-Session: https://claude.ai/code/session_0138Vn8E8TfZkUWd3StqDgjp --- dify/requirements.txt | 1 + dify/tests/test_headers.py | 4 ++-- dify/tools/base.py | 2 +- dify/tools/constants.py | 13 +++++++------ 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/dify/requirements.txt b/dify/requirements.txt index 69adc2d..53d07c9 100644 --- a/dify/requirements.txt +++ b/dify/requirements.txt @@ -1 +1,2 @@ dify_plugin>=0.4.0,<0.7.0 +pyyaml~=6.0 diff --git a/dify/tests/test_headers.py b/dify/tests/test_headers.py index 9e23dd4..f697c10 100644 --- a/dify/tests/test_headers.py +++ b/dify/tests/test_headers.py @@ -17,7 +17,7 @@ def _tool() -> TinyfishMixin: def test_headers_identify_the_plugin() -> None: headers = _tool()._api_headers assert headers["X-API-Key"] == "tf_test" - assert headers["X-TF-Client-Name"] == "tinyfish-dify" + assert headers["X-TF-Client-Name"] == "dify" assert headers["X-TF-Client-Version"] == PLUGIN_VERSION @@ -28,5 +28,5 @@ def test_plugin_version_is_a_real_release_version() -> None: assert re.search( rf"^version: {re.escape(PLUGIN_VERSION)}$", MANIFEST.read_text(encoding="utf-8"), - re.M, + re.MULTILINE, ) diff --git a/dify/tools/base.py b/dify/tools/base.py index 5854c91..f30fef0 100644 --- a/dify/tools/base.py +++ b/dify/tools/base.py @@ -13,7 +13,7 @@ def _api_headers(self) -> dict[str, str]: # Without these, telemetry cannot tell the plugin from raw curl. return { "X-API-Key": self.runtime.credentials["api_key"], - "X-TF-Client-Name": "tinyfish-dify", + "X-TF-Client-Name": "dify", "X-TF-Client-Version": PLUGIN_VERSION, } diff --git a/dify/tools/constants.py b/dify/tools/constants.py index d2dea6c..9ea0e55 100644 --- a/dify/tools/constants.py +++ b/dify/tools/constants.py @@ -1,18 +1,19 @@ from pathlib import Path +import yaml + API_BASE_URL = "https://agent.tinyfish.ai" +# Top-level `version` is the plugin; `meta.version` is the manifest format. _MANIFEST = Path(__file__).resolve().parents[1] / "manifest.yaml" def _manifest_version() -> str: try: - for line in _MANIFEST.read_text(encoding="utf-8").splitlines(): - if line.startswith("version:"): - return line.split(":", 1)[1].split("#", 1)[0].strip().strip("'\"") - except OSError: - pass - return "0+unknown" + version = yaml.safe_load(_MANIFEST.read_text(encoding="utf-8")).get("version") + except (OSError, yaml.YAMLError, AttributeError): + version = None + return str(version) if version else "0+unknown" PLUGIN_VERSION = _manifest_version() From 29de3428356d3ec2c7f8d464c02ae5a20db6d135 Mon Sep 17 00:00:00 2001 From: londondavila Date: Tue, 8 Sep 2026 10:29:21 -0600 Subject: [PATCH 5/5] docs: put dify back in the header-contract table #35 dropped `dify` from the `X-TF-Client-Name` row because it did not send the headers; this PR is the one that makes it comply. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Nw7GKwVBvSXErUW8wkxaYD --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bad79f9..c1867d4 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ Every integration that calls the REST API directly must send two headers on ever | 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-Name` | the bare integration token — the same value used for `api_integration` where one exists: `dify`, `hermes`, `n8n` | | `X-TF-Client-Version` | the integration's own published version | Do not send `X-TF-Request-Origin` yourself; `api` is the transport and the server derives it, and the `tinyfish` SDK already sets its own value. Integrations that wrap the SDK set `TF_CLIENT_NAME` / `TF_CLIENT_VERSION` alongside `TF_API_INTEGRATION` instead — the SDK turns those two into the headers above from `tinyfish>=0.5.0`, and ignores them before that. `langchain` and `google-adk` set only `TF_API_INTEGRATION` today. Without any of this, telemetry cannot tell the integration from a hand-written curl.