diff --git a/python/coinbase-agentkit/CHANGELOG.md b/python/coinbase-agentkit/CHANGELOG.md index 533c0e889..1d095465d 100644 --- a/python/coinbase-agentkit/CHANGELOG.md +++ b/python/coinbase-agentkit/CHANGELOG.md @@ -2,6 +2,10 @@ +### Added + +- Added Optume Translations x402 Legal Translation and Document Parser Action Provider (`OptumeActionProvider`). + ## [0.7.4] - 2025-10-03 ### Fixed diff --git a/python/coinbase-agentkit/coinbase_agentkit/action_providers/__init__.py b/python/coinbase-agentkit/coinbase_agentkit/action_providers/__init__.py index 68573da62..5da4aab73 100644 --- a/python/coinbase-agentkit/coinbase_agentkit/action_providers/__init__.py +++ b/python/coinbase-agentkit/coinbase_agentkit/action_providers/__init__.py @@ -26,6 +26,10 @@ 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 .optume_translations_x402.optume_action_provider import ( + OptumeActionProvider, + optume_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 +58,7 @@ "MorphoActionProvider", "NillionActionProvider", "OnrampActionProvider", + "OptumeActionProvider", "PythActionProvider", "SshActionProvider", "SuperfluidActionProvider", @@ -75,6 +80,7 @@ "morpho_action_provider", "nillion_action_provider", "onramp_action_provider", + "optume_action_provider", "pyth_action_provider", "ssh_action_provider", "superfluid_action_provider", diff --git a/python/coinbase-agentkit/coinbase_agentkit/action_providers/optume_translations_x402/optume_action_provider.py b/python/coinbase-agentkit/coinbase_agentkit/action_providers/optume_translations_x402/optume_action_provider.py new file mode 100644 index 000000000..d971419d6 --- /dev/null +++ b/python/coinbase-agentkit/coinbase_agentkit/action_providers/optume_translations_x402/optume_action_provider.py @@ -0,0 +1,271 @@ +""" +Optume Translations x402 Action Provider for Coinbase AgentKit (Python). + +Enables autonomous AI agents using Coinbase AgentKit to natively execute turnkey +legal-grade translations over x402 micropayments settled on Base Layer-2. +""" + +import json +import urllib.error +import urllib.parse +import urllib.request +from typing import Any + +from pydantic import BaseModel, Field + +# Graceful import of Coinbase AgentKit ActionProvider base class & decorator +try: + from coinbase_agentkit.action_providers.action_decorator import create_action + from coinbase_agentkit.action_providers.action_provider import ActionProvider + + HAS_AGENTKIT = True +except ImportError: + HAS_AGENTKIT = False + + class ActionProvider: + """Fallback ActionProvider base class when coinbase-agentkit is not installed.""" + + def __init__( + self, name: str = "", dependencies: list[Any] | None = None + ) -> None: + self.name = name + + def get_actions(self, wallet_provider: Any = None) -> list[Any]: + return [] + + def create_action(*args: Any, **kwargs: Any) -> Any: + """Fallback decorator for create_action.""" + + def decorator(func: Any) -> Any: + return func + + return decorator + + +# --------------------------------------------------------------------------- +# Pydantic Input Schemas for AgentKit Actions +# --------------------------------------------------------------------------- + + +class RunFullPipelineInput(BaseModel): + raw_text: str | None = Field( + None, + description="Raw legal contract text to translate.", + ) + document_url: str | None = Field( + None, + description="Public URL of document to execute complete end-to-end 7-node Veritas legal translation pipeline.", + ) + source_language: str = Field( + "en", + description="ISO source language code (e.g. 'en').", + ) + target_language: str = Field( + "fr", + description="ISO target language code (e.g. 'fr', 'es', 'de', 'ar', 'zh').", + ) + + +# --------------------------------------------------------------------------- +# Safe Redirect Handler +# --------------------------------------------------------------------------- + + +class SafeRedirectHandler(urllib.request.HTTPRedirectHandler): + """ + Secure redirect handler preventing cross-origin token leakage and protocol downgrades. + """ + + def redirect_request( + self, + req: urllib.request.Request, + fp: Any, + code: int, + msg: str, + headers: Any, + newurl: str, + ) -> urllib.request.Request | None: + orig_parsed = urllib.parse.urlparse(req.full_url) + new_parsed = urllib.parse.urlparse(newurl) + + # Reject protocol downgrade from HTTPS to HTTP + if orig_parsed.scheme == "https" and new_parsed.scheme != "https": + raise urllib.error.HTTPError( + newurl, + code, + "Redirect rejected: protocol downgrade from HTTPS to HTTP is forbidden.", + headers, + fp, + ) + + # Reject cross-origin redirects + if orig_parsed.netloc.lower() != new_parsed.netloc.lower(): + raise urllib.error.HTTPError( + newurl, + code, + f"Redirect rejected: cross-origin redirect to {new_parsed.netloc} is forbidden.", + headers, + fp, + ) + + new_req = super().redirect_request(req, fp, code, msg, headers, newurl) + if new_req: + # Preserve X-402-Payment-Proof header across allowed redirects + proof_header = req.headers.get("X-402-payment-proof") or req.headers.get( + "X-402-Payment-Proof" + ) + if proof_header: + new_req.add_header("X-402-Payment-Proof", proof_header) + return new_req + + +# --------------------------------------------------------------------------- +# Optume Action Provider Implementation +# --------------------------------------------------------------------------- + + +class OptumeActionProvider(ActionProvider): + """ + Coinbase AgentKit Action Provider for Optume Translations x402 Services. + """ + + def __init__(self, base_url: str = "https://api.optranslations.com"): + if HAS_AGENTKIT: + super().__init__("optume", []) + else: + self.name = "optume" + + parsed = urllib.parse.urlparse(base_url) + + # Validate HTTPS-origin policy (allowing http only for localhost/127.0.0.1 in local dev) + if parsed.scheme not in ("https", "http"): + raise ValueError( + f"Invalid base_url scheme '{parsed.scheme}'. Must be HTTPS." + ) + if parsed.scheme == "http" and parsed.hostname not in ( + "localhost", + "127.0.0.1", + "testserver", + ): + raise ValueError("Insecure HTTP base_url allowed only for local testing.") + + self.base_url = base_url.rstrip("/") + + def supports_network(self, network: Any) -> bool: + """ + Check whether target EVM network is supported (Base Mainnet / Sepolia). + """ + net_str = str(network).lower().strip() + if hasattr(network, "network_id"): + net_str = str(network.network_id).lower().strip() + elif hasattr(network, "chain_id"): + net_str = str(network.chain_id).lower().strip() + + supported_bases = { + "base", + "8453", + "84532", + "eip155:8453", + "eip155:84532", + "base-mainnet", + "base-sepolia", + } + return net_str in supported_bases + + def _execute_x402_request( + self, + endpoint_path: str, + payload: dict[str, Any], + x402_payment_proof: str | None = None, + ) -> dict[str, Any]: + """ + Helper method to execute HTTP POST requests to Optume x402 endpoints with redirect security. + """ + url = f"{self.base_url}{endpoint_path}" + data = json.dumps(payload).encode("utf-8") + headers = {"Content-Type": "application/json"} + + if x402_payment_proof: + headers["X-402-Payment-Proof"] = x402_payment_proof + + req = urllib.request.Request(url, data=data, headers=headers, method="POST") + opener = urllib.request.build_opener(SafeRedirectHandler()) + + try: + with opener.open(req, timeout=30.0) as response: + body = response.read().decode("utf-8") + return json.loads(body) + except urllib.error.HTTPError as e: + body = e.read().decode("utf-8") if hasattr(e, "read") else str(e) + try: + error_json = json.loads(body) + except Exception: + error_json = {"error": str(e), "raw_body": body} + + return { + "status_code": e.code, + "x402_challenge": { + "price_usdc": e.headers.get("X-402-Price-USDC") + if hasattr(e, "headers") + else None, + "pay_to": e.headers.get("X-402-Pay-To") + if hasattr(e, "headers") + else None, + "network": e.headers.get("X-402-Network", "eip155:8453") + if hasattr(e, "headers") + else "eip155:8453", + }, + "response": error_json, + } + except Exception as e: + return {"error": str(e)} + + # --------------------------------------------------------------------------- + # AgentKit Decorated Action Methods + # --------------------------------------------------------------------------- + + @create_action( + name="veritas_legal_translation", + description="Turnkey legal-grade document translation engine combining all 7 Veritas pipeline nodes ($0.0005/word, min $0.05 USDC on Base L2).", + schema=RunFullPipelineInput, + ) + def veritas_legal_translation( + self, args: dict[str, Any], x402_proof: str | None = None + ) -> str | dict[str, Any]: + validated = ( + RunFullPipelineInput.model_validate(args) + if isinstance(args, dict) + else args + ) + payload = validated.model_dump() if hasattr(validated, "model_dump") else args + res = self._execute_x402_request( + "/api/v1/veritas/legal-translation", payload, x402_proof + ) + return json.dumps(res) if HAS_AGENTKIT else res + + def get_actions(self, wallet_provider: Any = None) -> list[Any]: + """ + Return the list of actions exposed to Coinbase AgentKit. + """ + if HAS_AGENTKIT and hasattr(super(), "get_actions"): + actions = super().get_actions(wallet_provider) + if actions: + return actions + + return [ + { + "name": "veritas_legal_translation", + "description": "Turnkey legal-grade document translation engine combining all 7 Veritas pipeline nodes ($0.0005/word, min $0.05 USDC on Base L2).", + "schema": RunFullPipelineInput, + "func": self.veritas_legal_translation, + }, + ] + + +def optume_action_provider( + base_url: str = "https://api.optranslations.com", +) -> OptumeActionProvider: + """ + Factory function for OptumeActionProvider. + """ + return OptumeActionProvider(base_url=base_url) diff --git a/typescript/agentkit/CHANGELOG.md b/typescript/agentkit/CHANGELOG.md index fa4b2af96..b1170e058 100644 --- a/typescript/agentkit/CHANGELOG.md +++ b/typescript/agentkit/CHANGELOG.md @@ -10,6 +10,8 @@ ### Patch Changes +- [#1036](https://github.com/coinbase/agentkit/pull/1036) Added Optume Translations x402 Legal Translation and Document Parser Action Provider (`OptumeActionProvider`). + - [#966](https://github.com/coinbase/agentkit/pull/966) [`b211701`](https://github.com/coinbase/agentkit/commit/b21170143825cb1892daaa8e52c68e9c8c446ae1) Thanks [@phdargen](https://github.com/phdargen)! - Bumped x402 packages and fix missing readContract interface - [#982](https://github.com/coinbase/agentkit/pull/982) [`c3dbef6`](https://github.com/coinbase/agentkit/commit/c3dbef60d1613effc9d9805816bec15f5510fdca) Thanks [@fffilimonov](https://github.com/fffilimonov)! - Added dTelecom action provider for decentralized voice services (WebRTC, STT, TTS) with x402 micropayments, and a voice agent example. diff --git a/typescript/agentkit/src/action-providers/index.ts b/typescript/agentkit/src/action-providers/index.ts index 9f7164086..e1a1abbf5 100644 --- a/typescript/agentkit/src/action-providers/index.ts +++ b/typescript/agentkit/src/action-providers/index.ts @@ -34,6 +34,7 @@ export * from "./wow"; export * from "./allora"; export * from "./flaunch"; export * from "./onramp"; +export * from "./optume_translations_x402/optumeActionProvider"; export * from "./vaultsfyi"; export * from "./x402"; export * from "./yelay"; diff --git a/typescript/agentkit/src/action-providers/optume_translations_x402/optumeActionProvider.ts b/typescript/agentkit/src/action-providers/optume_translations_x402/optumeActionProvider.ts new file mode 100644 index 000000000..64fbca3a7 --- /dev/null +++ b/typescript/agentkit/src/action-providers/optume_translations_x402/optumeActionProvider.ts @@ -0,0 +1,185 @@ +import { ActionProvider } from "@coinbase/agentkit"; +import { z } from "zod"; + +export const RunFullPipelineSchema = z.object({ + rawText: z + .string() + .optional() + .describe("Raw legal contract text to translate."), + documentUrl: z + .string() + .url() + .optional() + .describe("Public URL of legal document to execute complete 7-node Veritas pipeline."), + sourceLanguage: z + .string() + .default("en") + .describe("ISO source language code (default 'en')."), + targetLanguage: z + .string() + .default("fr") + .describe("ISO target language code (e.g. 'fr', 'es', 'de', 'ar', 'zh')."), +}); + +export class OptumeActionProvider extends ActionProvider { + private baseUrl: string; + + constructor(baseUrl: string = "https://api.optranslations.com") { + super("optume", []); + + const parsed = new URL(baseUrl); + if (!["https:", "http:"].includes(parsed.protocol)) { + throw new Error(`Invalid baseUrl protocol '${parsed.protocol}'. Must be HTTPS.`); + } + if ( + parsed.protocol === "http:" && + !["localhost", "127.0.0.1", "testserver"].includes(parsed.hostname) + ) { + throw new Error("Insecure HTTP baseUrl allowed only for local testing."); + } + + this.baseUrl = baseUrl.replace(/\/+$/, ""); + } + + public supportsNetwork(network: { networkId?: string; chainId?: string } | string): boolean { + let netStr = typeof network === "string" ? network : network.networkId || network.chainId || ""; + netStr = netStr.toLowerCase().trim(); + + const supportedBases = new Set([ + "base", + "8453", + "84532", + "eip155:8453", + "eip155:84532", + "base-mainnet", + "base-sepolia", + ]); + + return supportedBases.has(netStr); + } + + private validateRedirect( + response: Response, + currentUrl: string + ): { redirectUrl?: string; error?: string } { + if (response.status >= 300 && response.status < 400) { + const location = response.headers.get("Location"); + if (!location) { + return { error: `Redirect response missing Location header (HTTP ${response.status}).` }; + } + + const origParsed = new URL(currentUrl); + const newParsed = new URL(location, currentUrl); + + if (origParsed.protocol === "https:" && newParsed.protocol !== "https:") { + return { + error: "Redirect rejected: protocol downgrade from HTTPS to HTTP is forbidden.", + }; + } + + if (origParsed.host.toLowerCase() !== newParsed.host.toLowerCase()) { + return { + error: `Redirect rejected: cross-origin redirect to ${newParsed.host} is forbidden.`, + }; + } + + return { redirectUrl: newParsed.toString() }; + } + return {}; + } + + private buildChallengeResponse(response: Response, body: unknown): Record { + return { + statusCode: response.status, + x402Challenge: { + priceUsdc: response.headers.get("X-402-Price-USDC") || null, + payTo: response.headers.get("X-402-Pay-To") || null, + network: response.headers.get("X-402-Network") || "eip155:8453", + }, + response: body, + }; + } + + /** + * Return catalog of active x402 actions exposed to Coinbase AgentKit. + */ + public getActions() { + return [ + { + name: "veritas_legal_translation", + description: + "Turnkey legal-grade document translation engine combining all 7 Veritas pipeline nodes ($0.0005/word, min $0.05 USDC on Base L2).", + schema: RunFullPipelineSchema, + invoke: async (args: Record, proof?: string) => { + const parsed = RunFullPipelineSchema.parse(args); + let rawText = parsed.rawText || ""; + if (!rawText && parsed.documentUrl) { + const resp = await fetch(parsed.documentUrl); + if (!resp.ok) { + return JSON.stringify({ error: `Failed to download document from URL: ${resp.statusText}` }); + } + rawText = await resp.text(); + } + const body = { + raw_text: rawText, + source_language: parsed.sourceLanguage || "en", + target_language: parsed.targetLanguage || "fr", + }; + return JSON.stringify(await this.executeX402Request("/api/v1/veritas/legal-translation", body, proof)); + }, + }, + ]; + } + + public async executeX402Request( + endpointPath: string, + payload: Record, + x402PaymentProof?: string + ): Promise { + let targetUrl = `${this.baseUrl}${endpointPath}`; + const headers: Record = { + "Content-Type": "application/json", + }; + + if (x402PaymentProof) { + headers["X-402-Payment-Proof"] = x402PaymentProof; + } + + try { + let redirectsFollowed = 0; + const maxRedirects = 5; + + while (redirectsFollowed <= maxRedirects) { + const response = await fetch(targetUrl, { + method: "POST", + headers, + body: JSON.stringify(payload), + redirect: "manual", + }); + + const redirectCheck = this.validateRedirect(response, targetUrl); + if (redirectCheck.error) { + return { error: redirectCheck.error }; + } + if (redirectCheck.redirectUrl) { + targetUrl = redirectCheck.redirectUrl; + redirectsFollowed += 1; + continue; + } + + const body = await response.json(); + if (!response.ok) { + return this.buildChallengeResponse(response, body); + } + return body; + } + return { error: "Too many redirects followed." }; + } catch (error) { + return { error: String(error) }; + } + } +} + +export function optumeActionProvider(baseUrl: string = "https://api.optranslations.com"): OptumeActionProvider { + return new OptumeActionProvider(baseUrl); +}