From 2e625d107fc10f03d3e30b9e6f359d349e9ef26a Mon Sep 17 00:00:00 2001 From: onefreeman1337 Date: Thu, 2 Jul 2026 09:10:42 -0400 Subject: [PATCH] feat(agentkit): add OSF action provider for sanctions/entity/CVE data --- .../action_providers/__init__.py | 3 + .../action_providers/osf/README.md | 46 +++++ .../action_providers/osf/__init__.py | 5 + .../osf/osf_action_provider.py | 190 ++++++++++++++++++ .../action_providers/osf/schemas.py | 33 +++ .../tests/action_providers/osf/__init__.py | 0 .../tests/action_providers/osf/conftest.py | 30 +++ .../osf/test_osf_action_provider.py | 117 +++++++++++ 8 files changed, 424 insertions(+) create mode 100644 python/coinbase-agentkit/coinbase_agentkit/action_providers/osf/README.md create mode 100644 python/coinbase-agentkit/coinbase_agentkit/action_providers/osf/__init__.py create mode 100644 python/coinbase-agentkit/coinbase_agentkit/action_providers/osf/osf_action_provider.py create mode 100644 python/coinbase-agentkit/coinbase_agentkit/action_providers/osf/schemas.py create mode 100644 python/coinbase-agentkit/tests/action_providers/osf/__init__.py create mode 100644 python/coinbase-agentkit/tests/action_providers/osf/conftest.py create mode 100644 python/coinbase-agentkit/tests/action_providers/osf/test_osf_action_provider.py diff --git a/python/coinbase-agentkit/coinbase_agentkit/action_providers/__init__.py b/python/coinbase-agentkit/coinbase_agentkit/action_providers/__init__.py index 68573da62..ef3730f23 100644 --- a/python/coinbase-agentkit/coinbase_agentkit/action_providers/__init__.py +++ b/python/coinbase-agentkit/coinbase_agentkit/action_providers/__init__.py @@ -26,6 +26,7 @@ from .morpho.morpho_action_provider import MorphoActionProvider, morpho_action_provider from .nillion.nillion_action_provider import NillionActionProvider, nillion_action_provider from .onramp.onramp_action_provider import OnrampActionProvider, onramp_action_provider +from .osf.osf_action_provider import OsfActionProvider, osf_action_provider from .pyth.pyth_action_provider import PythActionProvider, pyth_action_provider from .ssh.ssh_action_provider import SshActionProvider, ssh_action_provider from .superfluid.superfluid_action_provider import ( @@ -54,6 +55,7 @@ "MorphoActionProvider", "NillionActionProvider", "OnrampActionProvider", + "OsfActionProvider", "PythActionProvider", "SshActionProvider", "SuperfluidActionProvider", @@ -75,6 +77,7 @@ "morpho_action_provider", "nillion_action_provider", "onramp_action_provider", + "osf_action_provider", "pyth_action_provider", "ssh_action_provider", "superfluid_action_provider", diff --git a/python/coinbase-agentkit/coinbase_agentkit/action_providers/osf/README.md b/python/coinbase-agentkit/coinbase_agentkit/action_providers/osf/README.md new file mode 100644 index 000000000..5cf979d12 --- /dev/null +++ b/python/coinbase-agentkit/coinbase_agentkit/action_providers/osf/README.md @@ -0,0 +1,46 @@ +# OSF Action Provider + +This directory contains the **OsfActionProvider** implementation, which provides actions to interact with **OSF (Open Source Filings)** — a marketplace of verifiable, provenance-stamped public-record data, paid per call over **x402** (USDC on Base mainnet). + +Every result carries a provenance URL to its authoritative primary U.S. government or public source, so an agent can independently verify each fact. + +## Directory Structure + +``` +osf/ +├── osf_action_provider.py # Main provider with OSF functionality +├── schemas.py # Action schemas +├── __init__.py # Main exports +└── README.md # This file + +# From python/coinbase-agentkit/ +tests/action_providers/osf/ +├── conftest.py # Test configuration and fixtures +└── test_osf_action_provider.py # Tests for the OSF action provider +``` + +## Actions + +- `lookup_entity`: Resolve and verify a company or person against authoritative public registries (CMS NPI, GLEIF LEI, FDIC, SEC EDGAR). For KYC, KYB, counterparty due-diligence, and onboarding. ($0.01 per call) +- `screen_entity`: Screen a name against the OFAC SDN, EU consolidated, and UK OFSI sanctions lists. Returns hit / no-hit, the matched list, and an audit receipt. For AML, KYC, and watchlist checks. ($0.05 per call) +- `check_cve_exploited`: Check whether a CVE is actively exploited (CISA KEV), with its EPSS exploit-probability and CVSS severity. For vulnerability management and patch prioritization. ($0.02 per call) + +Each action returns a JSON string containing the result, the HTTP status, and — on a successful paid call — the on-chain payment proof (transaction, network, payer). + +## Payment + +Calls are settled with USDC on Base mainnet via x402. The wallet provided to the agent must hold USDC on Base. Each action enforces a per-call maximum spend (via the x402 `max_amount` policy) set above the list price, so a small pricing change will not break calls while still capping spend per call. + +## Adding New Actions + +To add new OSF actions: + +1. Define your action schema in `schemas.py`. See [Defining the input schema](https://github.com/coinbase/agentkit/blob/main/CONTRIBUTING-PYTHON.md#defining-the-input-schema) for more information. +2. Implement the action in `osf_action_provider.py`. +3. Implement tests in `tests/action_providers/osf/test_osf_action_provider.py`. + +## Network Support + +OSF settles payments on **Base mainnet** only. + +For more information, visit the OSF data marketplace at [api.osf-master-server.com/mcp](https://api.osf-master-server.com/mcp). diff --git a/python/coinbase-agentkit/coinbase_agentkit/action_providers/osf/__init__.py b/python/coinbase-agentkit/coinbase_agentkit/action_providers/osf/__init__.py new file mode 100644 index 000000000..01cdea45c --- /dev/null +++ b/python/coinbase-agentkit/coinbase_agentkit/action_providers/osf/__init__.py @@ -0,0 +1,5 @@ +"""OSF (Open Source Filings) action provider for provenance-stamped public-record data.""" + +from .osf_action_provider import OsfActionProvider, osf_action_provider + +__all__ = ["OsfActionProvider", "osf_action_provider"] diff --git a/python/coinbase-agentkit/coinbase_agentkit/action_providers/osf/osf_action_provider.py b/python/coinbase-agentkit/coinbase_agentkit/action_providers/osf/osf_action_provider.py new file mode 100644 index 000000000..14c6c9857 --- /dev/null +++ b/python/coinbase-agentkit/coinbase_agentkit/action_providers/osf/osf_action_provider.py @@ -0,0 +1,190 @@ +"""OSF (Open Source Filings) action provider. + +Gives agents paid access to provenance-stamped, public-domain U.S. government and +public-record data over x402 (USDC on Base mainnet): entity resolution, sanctions +screening, and CVE exploitation checks. Every result carries a provenance URL to +its authoritative primary source, so an agent can independently verify each fact. +""" + +from __future__ import annotations + +import base64 +import json +from typing import Any +from urllib.parse import quote + +from x402 import max_amount, x402ClientSync +from x402.http.clients.requests import x402_requests +from x402.mechanisms.evm import EthAccountSigner +from x402.mechanisms.evm.exact.register import register_exact_evm_client + +from ...network import Network +from ...wallet_providers.evm_wallet_provider import EvmWalletProvider +from ..action_decorator import create_action +from ..action_provider import ActionProvider +from .schemas import CheckCveExploitedSchema, LookupEntitySchema, ScreenEntitySchema + +SUPPORTED_NETWORKS = ["base-mainnet"] + +DEFAULT_BASE_URL = "https://api.osf-master-server.com/x402" + +# Per-call spend ceilings in atomic USDC (6 decimals; 1_000_000 = $1.00). Each +# ceiling sits above OSF's list price so a small price change does not break +# calls, while the max_amount policy refuses to pay more than the ceiling for any +# single call. +_MAX_ENTITY_ATOMIC = 50_000 # $0.05 (list price $0.01) +_MAX_SCREEN_ATOMIC = 150_000 # $0.15 (list price $0.05) +_MAX_CVE_ATOMIC = 100_000 # $0.10 (list price $0.02) + +_TIMEOUT_SECONDS = 30 + + +class OsfActionProvider(ActionProvider[EvmWalletProvider]): + """Action provider for OSF (Open Source Filings) paid public-record data.""" + + def __init__(self, base_url: str = DEFAULT_BASE_URL) -> None: + super().__init__("osf", []) + self._base_url = base_url.rstrip("/") + + def _paid_get( + self, + wallet_provider: EvmWalletProvider, + path: str, + max_value_atomic: int, + ) -> str: + """Make a single x402-paid GET to an OSF endpoint and return a JSON string. + + Args: + wallet_provider: The EVM wallet provider that funds the call. + path: The OSF route path, already URL-encoded, e.g. "/identity/entity/Apple%20Inc". + max_value_atomic: Maximum the client will pay for this call, in atomic USDC. + + Returns: + A JSON string with the result and (on success) the on-chain payment proof, + or a JSON string describing the error. + + """ + try: + client = x402ClientSync() + register_exact_evm_client( + client, + EthAccountSigner(wallet_provider.to_signer()), + policies=[max_amount(max_value_atomic)], + ) + session = x402_requests(client) + url = f"{self._base_url}{path}" + response = session.request(method="GET", url=url, timeout=_TIMEOUT_SECONDS) + + # Payment proof header: v2 "payment-response" or v1 "x-payment-response" (base64 JSON). + payment = None + proof_header = response.headers.get("payment-response") or response.headers.get( + "x-payment-response" + ) + if proof_header: + try: + decoded = json.loads(base64.b64decode(proof_header)) + payment = { + "transaction": decoded.get("transaction"), + "network": decoded.get("network"), + "payer": decoded.get("payer"), + } + except Exception: + payment = None + + is_json = "application/json" in response.headers.get("content-type", "") + data = response.json() if is_json else response.text + + result: dict[str, Any] = { + "success": response.status_code == 200, + "status_code": response.status_code, + "data": data, + } + if payment: + result["payment"] = payment + return json.dumps(result) + except Exception as e: + return json.dumps( + { + "success": False, + "error": f"Error calling OSF: {e}", + "hint": ( + "Ensure the wallet holds USDC on Base mainnet and the per-call " + "spend ceiling covers the endpoint price." + ), + } + ) + + @create_action( + name="lookup_entity", + description="""Resolve and verify a company or person against authoritative public registries, paid per call over x402 (USDC on Base mainnet). + +Resolves against US healthcare providers (CMS NPI), global legal entities (GLEIF LEI), US banks (FDIC), and SEC filers / public companies (EDGAR CIK). + +Input: +- query: a name, or an identifier (NPI, LEI, FDIC certificate, or CIK) for an exact match. + +Returns legal name, status, type, jurisdiction, key identifiers, and a provenance URL to the official source, plus the on-chain payment proof. Use for KYC, KYB, counterparty due-diligence, provider verification, and onboarding.""", + schema=LookupEntitySchema, + ) + def lookup_entity(self, wallet_provider: EvmWalletProvider, args: dict[str, Any]) -> str: + """Resolve an entity by name or identifier against public registries.""" + validated = LookupEntitySchema(**args) + path = f"/identity/entity/{quote(validated.query, safe='')}" + return self._paid_get(wallet_provider, path, _MAX_ENTITY_ATOMIC) + + @create_action( + name="screen_entity", + description="""Screen a name against the OFAC SDN, EU consolidated, and UK OFSI sanctions lists, paid per call over x402 (USDC on Base mainnet). + +Input: +- name: the name to screen. + +Returns hit or no-hit, the matched list, a provenance URL, and an audit receipt, plus the on-chain payment proof. Use for AML, KYC, watchlist, and OFAC compliance checks before moving funds or onboarding a counterparty.""", + schema=ScreenEntitySchema, + ) + def screen_entity(self, wallet_provider: EvmWalletProvider, args: dict[str, Any]) -> str: + """Screen a name against OFAC, EU, and UK sanctions lists.""" + validated = ScreenEntitySchema(**args) + path = f"/screen/sanctions/{quote(validated.name, safe='')}" + return self._paid_get(wallet_provider, path, _MAX_SCREEN_ATOMIC) + + @create_action( + name="check_cve_exploited", + description="""Check whether a CVE is being actively exploited in the wild, paid per call over x402 (USDC on Base mainnet). + +Input: +- cve_id: the CVE identifier, e.g. CVE-2021-44228. + +Returns its presence on the US CISA Known Exploited Vulnerabilities (KEV) catalog, its EPSS exploit-probability score, and its CVSS severity, each with a provenance URL to the authoritative US government source, plus the on-chain payment proof. Use for vulnerability management, patch prioritization, and DevSecOps triage.""", + schema=CheckCveExploitedSchema, + ) + def check_cve_exploited(self, wallet_provider: EvmWalletProvider, args: dict[str, Any]) -> str: + """Check whether a CVE is on CISA KEV, plus its EPSS and CVSS scores.""" + validated = CheckCveExploitedSchema(**args) + path = f"/security/cve/{quote(validated.cve_id, safe='')}" + return self._paid_get(wallet_provider, path, _MAX_CVE_ATOMIC) + + def supports_network(self, network: Network) -> bool: + """Check whether the network is supported. OSF settles on Base mainnet only. + + Args: + network: The network to check. + + Returns: + True if the network is Base mainnet, otherwise False. + + """ + return network.network_id in SUPPORTED_NETWORKS + + +def osf_action_provider(base_url: str = DEFAULT_BASE_URL) -> OsfActionProvider: + """Create a new OSF action provider. + + Args: + base_url: Override the OSF base URL (defaults to the public mainnet API). + + Returns: + A new OSF action provider instance. + + """ + return OsfActionProvider(base_url=base_url) diff --git a/python/coinbase-agentkit/coinbase_agentkit/action_providers/osf/schemas.py b/python/coinbase-agentkit/coinbase_agentkit/action_providers/osf/schemas.py new file mode 100644 index 000000000..559c3befd --- /dev/null +++ b/python/coinbase-agentkit/coinbase_agentkit/action_providers/osf/schemas.py @@ -0,0 +1,33 @@ +"""Schemas for the OSF action provider.""" + +from pydantic import BaseModel, Field + + +class LookupEntitySchema(BaseModel): + """Input schema for resolving an entity against authoritative public registries.""" + + query: str = Field( + ..., + description=( + "A company or person name, or an identifier (CMS NPI, GLEIF LEI, " + "FDIC certificate, or SEC EDGAR CIK) to resolve." + ), + ) + + +class ScreenEntitySchema(BaseModel): + """Input schema for sanctions screening of a name.""" + + name: str = Field( + ..., + description="The name to screen against the OFAC SDN, EU consolidated, and UK OFSI sanctions lists.", + ) + + +class CheckCveExploitedSchema(BaseModel): + """Input schema for a CVE exploitation check.""" + + cve_id: str = Field( + ..., + description="The CVE identifier to check, e.g. CVE-2021-44228.", + ) diff --git a/python/coinbase-agentkit/tests/action_providers/osf/__init__.py b/python/coinbase-agentkit/tests/action_providers/osf/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/python/coinbase-agentkit/tests/action_providers/osf/conftest.py b/python/coinbase-agentkit/tests/action_providers/osf/conftest.py new file mode 100644 index 000000000..55fbdb72f --- /dev/null +++ b/python/coinbase-agentkit/tests/action_providers/osf/conftest.py @@ -0,0 +1,30 @@ +"""Fixtures for the OSF action provider tests.""" + +from unittest.mock import Mock + +import pytest + +from coinbase_agentkit.action_providers.osf.osf_action_provider import osf_action_provider +from coinbase_agentkit.network import Network +from coinbase_agentkit.wallet_providers.evm_wallet_provider import EvmWalletProvider + + +@pytest.fixture +def provider(): + """Return an OSF action provider instance.""" + return osf_action_provider() + + +@pytest.fixture +def mock_wallet_provider(): + """Return a mock EVM wallet provider reporting Base mainnet.""" + wallet = Mock(spec=EvmWalletProvider) + wallet.get_network.return_value = Network( + protocol_family="evm", + network_id="base-mainnet", + chain_id="8453", + ) + wallet.get_name.return_value = "mock_wallet" + wallet.get_address.return_value = "0x0000000000000000000000000000000000000000" + wallet.to_signer.return_value = Mock() + return wallet diff --git a/python/coinbase-agentkit/tests/action_providers/osf/test_osf_action_provider.py b/python/coinbase-agentkit/tests/action_providers/osf/test_osf_action_provider.py new file mode 100644 index 000000000..737b933b1 --- /dev/null +++ b/python/coinbase-agentkit/tests/action_providers/osf/test_osf_action_provider.py @@ -0,0 +1,117 @@ +"""Tests for the OSF action provider.""" + +import base64 +import json +from unittest.mock import Mock, patch + +import pytest + +from coinbase_agentkit.network import Network + +MODULE = "coinbase_agentkit.action_providers.osf.osf_action_provider" + + +def _mock_response(status_code=200, json_body=None, payment=None, content_type="application/json"): + """Build a fake requests-style response object.""" + response = Mock() + response.status_code = status_code + headers = {"content-type": content_type} + if payment is not None: + headers["payment-response"] = base64.b64encode(json.dumps(payment).encode()).decode() + response.headers = headers + response.json.return_value = json_body if json_body is not None else {} + response.text = json.dumps(json_body) if json_body is not None else "" + return response + + +@pytest.fixture +def mock_session(): + """Patch x402 client construction and the requests session. + + Yields the mocked session so a test can set ``mock_session.request.return_value``. + No real network call or payment occurs. + """ + with ( + patch(f"{MODULE}.x402ClientSync"), + patch(f"{MODULE}.register_exact_evm_client"), + patch(f"{MODULE}.EthAccountSigner"), + patch(f"{MODULE}.max_amount"), + patch(f"{MODULE}.x402_requests") as mock_x402_requests, + ): + session = Mock() + session.request.return_value = _mock_response(json_body={}) + mock_x402_requests.return_value = session + yield session + + +def test_provider_name(provider): + """Provider name is 'osf'.""" + assert provider.name == "osf" + + +def test_supports_base_mainnet(provider): + """OSF supports Base mainnet.""" + network = Network(protocol_family="evm", network_id="base-mainnet", chain_id="8453") + assert provider.supports_network(network) is True + + +def test_does_not_support_other_networks(provider): + """OSF does not support non-Base networks.""" + network = Network(protocol_family="evm", network_id="ethereum-mainnet", chain_id="1") + assert provider.supports_network(network) is False + + +def test_action_names(provider, mock_wallet_provider): + """The three actions are exposed with class-prefixed names.""" + names = {action.name for action in provider.get_actions(mock_wallet_provider)} + assert "OsfActionProvider_lookup_entity" in names + assert "OsfActionProvider_screen_entity" in names + assert "OsfActionProvider_check_cve_exploited" in names + + +def test_lookup_entity_success(provider, mock_wallet_provider, mock_session): + """A successful entity lookup returns data plus the payment proof.""" + body = {"service": "OSF Identity - Entity Lookup", "result": "FOUND"} + payment = {"transaction": "0xabc", "network": "eip155:8453", "payer": "0x1"} + mock_session.request.return_value = _mock_response(json_body=body, payment=payment) + + result = json.loads(provider.lookup_entity(mock_wallet_provider, {"query": "Apple Inc"})) + + assert result["success"] is True + assert result["status_code"] == 200 + assert result["data"] == body + assert result["payment"]["transaction"] == "0xabc" + called_url = mock_session.request.call_args.kwargs["url"] + assert called_url.endswith("/identity/entity/Apple%20Inc") + + +def test_screen_entity_builds_encoded_url(provider, mock_wallet_provider, mock_session): + """Sanctions screening encodes the name into the screen route.""" + mock_session.request.return_value = _mock_response(json_body={"result": "NO_HIT"}) + provider.screen_entity(mock_wallet_provider, {"name": "Jane Doe"}) + called_url = mock_session.request.call_args.kwargs["url"] + assert called_url.endswith("/screen/sanctions/Jane%20Doe") + + +def test_check_cve_builds_url(provider, mock_wallet_provider, mock_session): + """CVE check builds the security route with the CVE id.""" + mock_session.request.return_value = _mock_response(json_body={"exploited": True}) + provider.check_cve_exploited(mock_wallet_provider, {"cve_id": "CVE-2021-44228"}) + called_url = mock_session.request.call_args.kwargs["url"] + assert called_url.endswith("/security/cve/CVE-2021-44228") + + +def test_non_200_marks_unsuccessful(provider, mock_wallet_provider, mock_session): + """A non-200 response is reported as unsuccessful with its status code.""" + mock_session.request.return_value = _mock_response(status_code=402, json_body={}) + result = json.loads(provider.lookup_entity(mock_wallet_provider, {"query": "X"})) + assert result["success"] is False + assert result["status_code"] == 402 + + +def test_exception_is_handled(provider, mock_wallet_provider, mock_session): + """An exception during the call is returned as an error JSON, not raised.""" + mock_session.request.side_effect = RuntimeError("boom") + result = json.loads(provider.lookup_entity(mock_wallet_provider, {"query": "X"})) + assert result["success"] is False + assert "boom" in result["error"]