From f91cfbbe74db044610dbf0520236cae386232339 Mon Sep 17 00:00:00 2001 From: MarkovianProtocol Date: Mon, 29 Jun 2026 10:09:03 -0600 Subject: [PATCH 1/2] Add MarkovianStampTool for verifiable provenance receipts --- lib/crewai-tools/src/crewai_tools/__init__.py | 4 + .../src/crewai_tools/tools/__init__.py | 4 + .../tools/markovian_stamp_tool/README.md | 85 +++++++++++ .../tools/markovian_stamp_tool/__init__.py | 0 .../markovian_stamp_tool.py | 134 ++++++++++++++++++ .../tests/tools/markovian_stamp_tool_test.py | 71 ++++++++++ 6 files changed, 298 insertions(+) create mode 100644 lib/crewai-tools/src/crewai_tools/tools/markovian_stamp_tool/README.md create mode 100644 lib/crewai-tools/src/crewai_tools/tools/markovian_stamp_tool/__init__.py create mode 100644 lib/crewai-tools/src/crewai_tools/tools/markovian_stamp_tool/markovian_stamp_tool.py create mode 100644 lib/crewai-tools/tests/tools/markovian_stamp_tool_test.py diff --git a/lib/crewai-tools/src/crewai_tools/__init__.py b/lib/crewai-tools/src/crewai_tools/__init__.py index 12011ce394..c4593cac23 100644 --- a/lib/crewai-tools/src/crewai_tools/__init__.py +++ b/lib/crewai-tools/src/crewai_tools/__init__.py @@ -107,6 +107,9 @@ from crewai_tools.tools.json_search_tool.json_search_tool import JSONSearchTool from crewai_tools.tools.linkup.linkup_search_tool import LinkupSearchTool from crewai_tools.tools.llamaindex_tool.llamaindex_tool import LlamaIndexTool +from crewai_tools.tools.markovian_stamp_tool.markovian_stamp_tool import ( + MarkovianStampTool, +) from crewai_tools.tools.mdx_search_tool.mdx_search_tool import MDXSearchTool from crewai_tools.tools.merge_agent_handler_tool.merge_agent_handler_tool import ( MergeAgentHandlerTool, @@ -275,6 +278,7 @@ "LlamaIndexTool", "MCPServerAdapter", "MDXSearchTool", + "MarkovianStampTool", "MergeAgentHandlerTool", "MongoDBVectorSearchConfig", "MongoDBVectorSearchTool", diff --git a/lib/crewai-tools/src/crewai_tools/tools/__init__.py b/lib/crewai-tools/src/crewai_tools/tools/__init__.py index 18bf4e5638..449d311671 100644 --- a/lib/crewai-tools/src/crewai_tools/tools/__init__.py +++ b/lib/crewai-tools/src/crewai_tools/tools/__init__.py @@ -96,6 +96,9 @@ from crewai_tools.tools.json_search_tool.json_search_tool import JSONSearchTool from crewai_tools.tools.linkup.linkup_search_tool import LinkupSearchTool from crewai_tools.tools.llamaindex_tool.llamaindex_tool import LlamaIndexTool +from crewai_tools.tools.markovian_stamp_tool.markovian_stamp_tool import ( + MarkovianStampTool, +) from crewai_tools.tools.mdx_search_tool.mdx_search_tool import MDXSearchTool from crewai_tools.tools.merge_agent_handler_tool.merge_agent_handler_tool import ( MergeAgentHandlerTool, @@ -258,6 +261,7 @@ "LinkupSearchTool", "LlamaIndexTool", "MDXSearchTool", + "MarkovianStampTool", "MergeAgentHandlerTool", "MongoDBToolSchema", "MongoDBVectorSearchConfig", diff --git a/lib/crewai-tools/src/crewai_tools/tools/markovian_stamp_tool/README.md b/lib/crewai-tools/src/crewai_tools/tools/markovian_stamp_tool/README.md new file mode 100644 index 0000000000..23d10c9c32 --- /dev/null +++ b/lib/crewai-tools/src/crewai_tools/tools/markovian_stamp_tool/README.md @@ -0,0 +1,85 @@ +# MarkovianStampTool + +The **MarkovianStampTool** creates a verifiable provenance receipt for any text +using the [Markovian Protocol](https://markovianprotocol.com). It is useful when +an agent needs to prove that an output, decision, or document existed at a point +in time. + +Stamping commits a hash of the data to the chain, anchored to Bitcoin, and +returns a Merkle root plus a public verify URL. Anyone can confirm the record at +`https://api.quantsynth.net/verify/` with no account. + +Markovian proves that data existed, not that it is correct. Provenance, not +truth. + +--- + +## Description + +This tool: + +* Accepts any **text** and stamps it on the Markovian Protocol. +* Returns a **Merkle root**, **block height**, and a **public verify URL**. +* Requires **no account, wallet, or API key** for the free tier. +* Accepts an optional **API key** for attributed or pro usage. + +It prefers the [`markovian`](https://pypi.org/project/markovian/) SDK and falls +back to a plain HTTP POST when the SDK is not installed. + +--- + +## Installation + +```bash +pip install markovian +``` + +`requests` is already a dependency of `crewai-tools`, so the tool also works +without the SDK installed. + +--- + +## Arguments + +| Argument | Type | Required | Description | +| -------- | ----- | -------- | ---------------------------------------------------------------- | +| `data` | `str` | Yes | The content to stamp (an agent output, decision, or document). | +| `label` | `str` | No | Optional human-readable label attached to the stamp. | + +Constructor options: `api_key` (optional), `wallet` (optional), `base_url`, +`timeout`. `MARKOVIAN_API_KEY` is read from the environment when set. + +--- + +## Usage + +```python +from crewai_tools import MarkovianStampTool + +tool = MarkovianStampTool() +receipt = tool.run(data="The market thesis approved by the agent at 15:00 UTC.") +print(receipt) +``` + +Example output: + +``` +Markovian provenance receipt (markovian-provenance/v1): + merkle_root: 65ddefa2b8d3fb994f2a4037f9dd8278688138bf1c5eaa9cdb64c73c02663466 + block_height: 135213 + verify_url: https://api.quantsynth.net/verify/65ddefa2b8d3fb994f2a4037f9dd8278688138bf1c5eaa9cdb64c73c02663466 +``` + +Give an agent a one-line way to stamp its final answer: + +```python +from crewai import Agent +from crewai_tools import MarkovianStampTool + +agent = Agent( + role="Analyst", + goal="Produce analysis and stamp it for provenance.", + backstory="Stamps every deliverable so it can be independently verified.", + tools=[MarkovianStampTool()], +) +``` diff --git a/lib/crewai-tools/src/crewai_tools/tools/markovian_stamp_tool/__init__.py b/lib/crewai-tools/src/crewai_tools/tools/markovian_stamp_tool/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lib/crewai-tools/src/crewai_tools/tools/markovian_stamp_tool/markovian_stamp_tool.py b/lib/crewai-tools/src/crewai_tools/tools/markovian_stamp_tool/markovian_stamp_tool.py new file mode 100644 index 0000000000..9c76ef404c --- /dev/null +++ b/lib/crewai-tools/src/crewai_tools/tools/markovian_stamp_tool/markovian_stamp_tool.py @@ -0,0 +1,134 @@ +import json +import os +from typing import Any + +from crewai.tools import BaseTool, EnvVar +from pydantic import BaseModel, Field +import requests + + +MARKOVIAN_BASE_URL = "https://api.quantsynth.net" + + +class MarkovianStampToolInput(BaseModel): + """Input schema for MarkovianStampTool.""" + + data: str = Field( + ..., + description=( + "The content to stamp, for example an agent's final answer, a " + "decision, or any text whose existence you want to prove." + ), + ) + label: str | None = Field( + default=None, + description="Optional human-readable label to attach to the stamp.", + ) + + +class MarkovianStampTool(BaseTool): + """Stamp any data on the Markovian Protocol and return a verifiable receipt. + + Markovian is a content-agnostic provenance primitive. Stamping commits a + hash of the data to the chain, anchored to Bitcoin, and returns a Merkle + root plus a public verify URL. It proves the data existed at a point in + time, not that the data is correct: provenance, not truth. + + No account, wallet, or API key is required. Provide an api_key only for + attributed or pro usage. + """ + + name: str = "Markovian Stamp" + description: str = ( + "Create a verifiable provenance receipt for any text on the Markovian " + "Protocol. Returns a Merkle root and a public verify URL, anchored to " + "Bitcoin. Use it to prove an agent output existed at a point in time. " + "No account or API key required." + ) + args_schema: type[BaseModel] = MarkovianStampToolInput + + package_dependencies: list[str] = Field(default_factory=lambda: ["markovian"]) + env_vars: list[EnvVar] = Field( + default_factory=lambda: [ + EnvVar( + name="MARKOVIAN_API_KEY", + description="Optional API key for attributed or pro usage.", + required=False, + ), + ] + ) + + api_key: str | None = None + wallet: str | None = None + base_url: str = MARKOVIAN_BASE_URL + timeout: int = 30 + + def __init__( + self, + api_key: str | None = None, + wallet: str | None = None, + base_url: str = MARKOVIAN_BASE_URL, + timeout: int = 30, + **kwargs: Any, + ) -> None: + super().__init__(**kwargs) + self.api_key = api_key or os.environ.get("MARKOVIAN_API_KEY") + self.wallet = wallet + self.base_url = base_url + self.timeout = timeout + + def _stamp(self, data: str, label: str | None) -> dict: + """Stamp via the markovian SDK when available, else a plain POST.""" + try: + from markovian import MarkovianClient + + client = MarkovianClient(api_key=self.api_key, timeout=self.timeout) + return client.stamp(data, wallet=self.wallet, label=label) + except ImportError: + payload: dict = {"data": data, "label": label} + if self.wallet: + payload["wallet"] = self.wallet + headers = { + "User-Agent": "crewai-tools-markovian", + "Accept": "application/json", + "Content-Type": "application/json", + } + if self.api_key: + headers["X-API-Key"] = self.api_key + resp = requests.post( + f"{self.base_url}/stamp", + json=payload, + headers=headers, + timeout=self.timeout, + ) + resp.raise_for_status() + return resp.json() + + def _run(self, data: str, label: str | None = None, **_: Any) -> str: + try: + receipt = self._stamp(data, label) + except requests.Timeout: + return "Markovian stamp timed out. Please try again later." + except requests.HTTPError as exc: + status = exc.response.status_code if exc.response is not None else "?" + body = exc.response.text[:160] if exc.response is not None else "" + return f"Markovian stamp failed: HTTP {status} {body}" + except Exception as exc: + return f"Markovian stamp failed: {exc}" + + merkle_root = receipt.get("merkle_root") + if not merkle_root: + return f"Markovian stamp returned no merkle_root: {json.dumps(receipt)}" + + verify_url = receipt.get("verify_url", f"{self.base_url}/verify/{merkle_root}") + block_height = receipt.get("block_height") + lines = [ + "Markovian provenance receipt (markovian-provenance/v1):", + f" merkle_root: {merkle_root}", + f" block_height: {block_height}", + f" verify_url: {verify_url}", + ] + return "\n".join(lines) + + async def _arun(self, data: str, label: str | None = None, **kwargs: Any) -> str: + return self._run(data, label=label, **kwargs) diff --git a/lib/crewai-tools/tests/tools/markovian_stamp_tool_test.py b/lib/crewai-tools/tests/tools/markovian_stamp_tool_test.py new file mode 100644 index 0000000000..c0a98a2ef8 --- /dev/null +++ b/lib/crewai-tools/tests/tools/markovian_stamp_tool_test.py @@ -0,0 +1,71 @@ +from unittest.mock import MagicMock, patch + +import requests + +from crewai_tools import MarkovianStampTool + +SAMPLE_RECEIPT = { + "ok": True, + "stamp_id": 1, + "merkle_root": "65ddefa2b8d3fb994f2a4037f9dd8278688138bf1c5eaa9cdb64c73c02663466", + "data_hash": "b1000ed4a5bbf3bc3280c706e85374bec6ef5524ee877425b8454a8f5b73e1da", + "block_height": 135213, + "verify_url": "https://api.quantsynth.net/verify/65ddefa2b8d3fb994f2a4037f9dd8278688138bf1c5eaa9cdb64c73c02663466", +} + + +def test_run_uses_sdk_when_available(): + tool = MarkovianStampTool() + fake_client = MagicMock() + fake_client.stamp.return_value = SAMPLE_RECEIPT + + with patch.object(tool, "_stamp", return_value=SAMPLE_RECEIPT): + result = tool.run(data="hello world") + + assert SAMPLE_RECEIPT["merkle_root"] in result + assert SAMPLE_RECEIPT["verify_url"] in result + assert "135213" in result + + +def test_run_falls_back_to_post(monkeypatch): + monkeypatch.delenv("MARKOVIAN_API_KEY", raising=False) + tool = MarkovianStampTool() + + mock_resp = MagicMock() + mock_resp.json.return_value = SAMPLE_RECEIPT + mock_resp.raise_for_status.return_value = None + + def fake_import(name, *args, **kwargs): + if name == "markovian": + raise ImportError("no markovian") + return original_import(name, *args, **kwargs) + + import builtins + + original_import = builtins.__import__ + with patch("builtins.__import__", side_effect=fake_import): + with patch("requests.post", return_value=mock_resp) as mock_post: + result = tool.run(data="hello world") + + mock_post.assert_called_once() + assert SAMPLE_RECEIPT["merkle_root"] in result + + +def test_run_handles_http_error(): + tool = MarkovianStampTool() + err = requests.HTTPError() + err.response = MagicMock(status_code=500, text="boom") + + with patch.object(tool, "_stamp", side_effect=err): + result = tool.run(data="hello world") + + assert "Markovian stamp failed" in result + assert "500" in result + + +def test_run_handles_missing_merkle_root(): + tool = MarkovianStampTool() + with patch.object(tool, "_stamp", return_value={"ok": True}): + result = tool.run(data="hello world") + + assert "no merkle_root" in result From 95c90ac82c8a29912f00c54148865cb195943f80 Mon Sep 17 00:00:00 2001 From: MarkovianProtocol Date: Mon, 29 Jun 2026 10:27:23 -0600 Subject: [PATCH 2/2] Address review: guard non-dict receipt, fix test coverage and README fence --- .../tools/markovian_stamp_tool/README.md | 2 +- .../markovian_stamp_tool.py | 3 +++ .../tests/tools/markovian_stamp_tool_test.py | 27 ++++++++++++++++--- 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/lib/crewai-tools/src/crewai_tools/tools/markovian_stamp_tool/README.md b/lib/crewai-tools/src/crewai_tools/tools/markovian_stamp_tool/README.md index 23d10c9c32..90259e333f 100644 --- a/lib/crewai-tools/src/crewai_tools/tools/markovian_stamp_tool/README.md +++ b/lib/crewai-tools/src/crewai_tools/tools/markovian_stamp_tool/README.md @@ -63,7 +63,7 @@ print(receipt) Example output: -``` +```text Markovian provenance receipt (markovian-provenance/v1): merkle_root: 65ddefa2b8d3fb994f2a4037f9dd8278688138bf1c5eaa9cdb64c73c02663466 block_height: 135213 diff --git a/lib/crewai-tools/src/crewai_tools/tools/markovian_stamp_tool/markovian_stamp_tool.py b/lib/crewai-tools/src/crewai_tools/tools/markovian_stamp_tool/markovian_stamp_tool.py index 9c76ef404c..f073c2bc84 100644 --- a/lib/crewai-tools/src/crewai_tools/tools/markovian_stamp_tool/markovian_stamp_tool.py +++ b/lib/crewai-tools/src/crewai_tools/tools/markovian_stamp_tool/markovian_stamp_tool.py @@ -116,6 +116,9 @@ def _run(self, data: str, label: str | None = None, **_: Any) -> str: except Exception as exc: return f"Markovian stamp failed: {exc}" + if not isinstance(receipt, dict): + return f"Markovian stamp returned an unexpected response: {receipt}" + merkle_root = receipt.get("merkle_root") if not merkle_root: return f"Markovian stamp returned no merkle_root: {json.dumps(receipt)}" diff --git a/lib/crewai-tools/tests/tools/markovian_stamp_tool_test.py b/lib/crewai-tools/tests/tools/markovian_stamp_tool_test.py index c0a98a2ef8..e8a2d2d0a9 100644 --- a/lib/crewai-tools/tests/tools/markovian_stamp_tool_test.py +++ b/lib/crewai-tools/tests/tools/markovian_stamp_tool_test.py @@ -1,9 +1,12 @@ +import sys +from types import ModuleType from unittest.mock import MagicMock, patch import requests from crewai_tools import MarkovianStampTool + SAMPLE_RECEIPT = { "ok": True, "stamp_id": 1, @@ -14,14 +17,23 @@ } -def test_run_uses_sdk_when_available(): +def test_run_uses_sdk_when_available(monkeypatch): tool = MarkovianStampTool() + fake_client = MagicMock() fake_client.stamp.return_value = SAMPLE_RECEIPT + fake_client_cls = MagicMock(return_value=fake_client) - with patch.object(tool, "_stamp", return_value=SAMPLE_RECEIPT): - result = tool.run(data="hello world") + fake_module = ModuleType("markovian") + fake_module.MarkovianClient = fake_client_cls + monkeypatch.setitem(sys.modules, "markovian", fake_module) + result = tool.run(data="hello world") + + fake_client_cls.assert_called_once() + fake_client.stamp.assert_called_once() + args, kwargs = fake_client.stamp.call_args + assert args[0] == "hello world" assert SAMPLE_RECEIPT["merkle_root"] in result assert SAMPLE_RECEIPT["verify_url"] in result assert "135213" in result @@ -61,6 +73,15 @@ def test_run_handles_http_error(): assert "Markovian stamp failed" in result assert "500" in result + assert "boom" in result + + +def test_run_handles_non_dict_response(): + tool = MarkovianStampTool() + with patch.object(tool, "_stamp", return_value=["not", "a", "dict"]): + result = tool.run(data="hello world") + + assert "unexpected response" in result def test_run_handles_missing_merkle_root():