relayshield-agentic-attack-surface / relayshield_smolagents_tool.py
relayshieldadmin's picture
Add try-it-live demo key fallback (20/day shared quota) and try-and-buy conversion nudge
683ec2d verified
Raw
History Blame Contribute Delete
26.9 kB
"""RelayShield security tools for smolagents.
Nine agent-specific security checks from RelayShield (https://api.relayshield.net/developers),
a live threat-intelligence API. Mirrors the tools shipped as the "RelayShield Agentic Attack
Surface" MCP server on Hugging Face Spaces
(https://huggingface.co/spaces/relayshieldadmin/relayshield-agentic-attack-surface).
v2 (2026-07-19) — response to community feedback on the v1 release: added five new tools
(oauth_watchlist, supply_chain, session_risk, nhi_exposure, secret_scan — grouped as an
"agent authority / credential exposure" family alongside the original four), and switched
every tool from a human-formatted string to a typed structured result:
{
"outcome": "finding" | "no_known_finding" | "error",
"recommended_action": "allow" | "review" | "deny" | "defer",
"reason_codes": [...],
"evidence": [...],
"coverage": {"complete": bool, "scope": "what was actually checked"},
"freshness": {"observed_at": "...", "expires_at": null},
"error": {"kind": "...", "message": "...", "retryable": bool} | null
}
"no_known_finding" deliberately does not mean "safe" — it means nothing was found in the
sources and scope actually queried. A caller using these as a pre-action gate should check
"error" before trusting "outcome", since a failed check is not the same as a clean one.
Setup:
pip install smolagents requests
No signup required to try these tools: if RELAYSHIELD_API_KEY isn't set, they
fall back automatically to a shared, quota-capped demo key (20 calls/day,
shared across everyone using the default — plenty to try a few tools, not
enough for production use). Every result carries a "_demo_notice" field
while the fallback is active, pointing at real signup.
export RELAYSHIELD_API_KEY="your-key-here" # optional — unlocks your own quota
Usage:
from relayshield_smolagents_tool import RelayShieldSupplyChainTool
tool = RelayShieldSupplyChainTool()
result = tool.forward(vendor_domains=["vendor.example.com"])
Get a key at https://api.relayshield.net/developers?source=hf-smolagents ($499/mo for 10,000 calls,
or pay-per-call via x402 USDC with no key required for the equivalent /v1/payg/* routes).
"""
import os
import requests
from smolagents import Tool
API_BASE_URL: str = "https://api.relayshield.net"
# Shared, quota-capped (20 calls/day, all callers combined) fallback key — lets
# these tools work with zero signup. Deliberately a separate, low-privilege key
# from any server-side-only demo key elsewhere in RelayShield's stack: this one
# lives in a public pip package and public HF Space file, so it must be safe to
# have scraped and reused by anyone, hence the shared daily cap rather than an
# unlimited key. Get your own uncapped key at
# https://api.relayshield.net/developers?source=hf-smolagents
_DEMO_API_KEY = "rs_demo_729639c084f7a638"
_DEMO_NOTICE = (
"This call used RelayShield's free shared demo key (20 calls/day, shared across "
"everyone using the default — no signup required). Get your own key for reliable, "
"unshared access: https://api.relayshield.net/developers?source=hf-smolagents"
)
def _relayshield_headers(api_key: str) -> dict[str, str]:
return {"Content-Type": "application/json", "X-RS-API-KEY": api_key}
def _get_api_key() -> str:
return os.environ.get("RELAYSHIELD_API_KEY") or _DEMO_API_KEY
def _using_demo_key() -> bool:
return not bool(os.environ.get("RELAYSHIELD_API_KEY"))
def _post(path: str, payload: dict) -> tuple[dict | None, dict | None]:
"""POSTs to a RelayShield metered endpoint. Returns (data, error).
error is None on success, or a schema-shaped
{"kind": ..., "message": ..., "retryable": bool} dict on failure — distinguishing
auth/rate_limited/timeout/upstream/malformed_response/other so a caller using these
tools as a policy gate can tell "the check failed" apart from "the check found nothing."
"""
api_key = _get_api_key()
try:
resp = requests.post(
f"{API_BASE_URL}{path}",
json=payload,
headers=_relayshield_headers(api_key),
timeout=15,
)
except requests.Timeout:
return None, {
"kind": "timeout",
"message": f"RelayShield API call to {path} timed out after 15s.",
"retryable": True,
}
except requests.RequestException as exc:
return None, {
"kind": "upstream",
"message": f"RelayShield API call to {path} failed: {exc}",
"retryable": True,
}
if resp.status_code in (401, 403):
return None, {
"kind": "auth",
"message": f"RelayShield API call to {path} returned {resp.status_code} — check RELAYSHIELD_API_KEY.",
"retryable": False,
}
if resp.status_code == 429:
message = f"RelayShield API call to {path} was rate-limited."
if _using_demo_key():
message += (
" The shared demo key's daily quota (20 calls/day, shared across everyone "
"using the default) is exhausted for today. Get your own key: "
"https://api.relayshield.net/developers?source=hf-smolagents"
)
return None, {"kind": "rate_limited", "message": message, "retryable": True}
if resp.status_code >= 500:
return None, {
"kind": "upstream",
"message": f"RelayShield API call to {path} returned {resp.status_code}.",
"retryable": True,
}
if resp.status_code >= 400:
return None, {
"kind": "other",
"message": f"RelayShield API call to {path} returned {resp.status_code}: {resp.text[:200]}",
"retryable": False,
}
try:
response_json = resp.json()
except ValueError:
return None, {
"kind": "malformed_response",
"message": f"RelayShield API call to {path} returned non-JSON content.",
"retryable": False,
}
# A valid-but-non-dict JSON body (list/string) is treated as malformed rather than
# crashing on .get() — same class of bug CodeRabbit caught in the CrewAI PR's first
# review pass on this project's earlier tools.
if not isinstance(response_json, dict):
return None, {
"kind": "malformed_response",
"message": f"RelayShield API call to {path} returned an unexpected response shape.",
"retryable": False,
}
return response_json.get("data", {}), None
def _outcome_for_severity(highest_severity: str | None, found: bool = True) -> tuple[str, str]:
"""Maps a RelayShield severity string to (outcome, recommended_action).
CRITICAL escalates to a deny recommendation; HIGH/MEDIUM/LOW are surfaced as findings
for human/agent review rather than an automatic block, since these are enrichment
signals, not a certified detection.
"""
if not found or highest_severity in (None, "NONE", "CLEAN"):
return "no_known_finding", "allow"
if highest_severity == "CRITICAL":
return "finding", "deny"
return "finding", "review"
def _structured_result(
outcome: str,
recommended_action: str,
*,
reason_codes: list[str] | None = None,
evidence: list | None = None,
scope: str,
coverage_complete: bool = True,
observed_at: str | None = None,
) -> dict:
result = {
"outcome": outcome,
"recommended_action": recommended_action,
"reason_codes": reason_codes or [],
"evidence": evidence or [],
"coverage": {"complete": coverage_complete, "scope": scope},
"freshness": {"observed_at": observed_at, "expires_at": None},
"error": None,
}
if _using_demo_key():
result["_demo_notice"] = _DEMO_NOTICE
return result
def _error_result(error: dict) -> dict:
result = {
"outcome": "error",
"recommended_action": "defer",
"reason_codes": [],
"evidence": [],
"coverage": {"complete": False, "scope": ""},
"freshness": {"observed_at": None, "expires_at": None},
"error": error,
}
if _using_demo_key():
result["_demo_notice"] = _DEMO_NOTICE
return result
class RelayShieldMCPRiskTool(Tool):
"""Typosquat / reputation / registration-age risk check for MCP servers
and agent tool registries, backed by RelayShield's live threat-intel API."""
name = "relayshield_mcp_registry_risk"
description = (
"Checks an MCP server URL or package name for typosquat risk against known MCP "
"ecosystem domains, presence in RelayShield's criminal IOC corpus, and domain-"
"registration age. Use this before connecting an agent to an unfamiliar MCP server "
"or tool registry. Returns a structured result with outcome/recommended_action, "
"not just a verdict string."
)
inputs = {
"server_url": {
"type": "string",
"description": "Full URL of the MCP server to check, e.g. 'https://example.com/mcp'. Provide this or package_name.",
"nullable": True,
},
"package_name": {
"type": "string",
"description": "Package name of the MCP server if no server_url is available. Checks are more limited without a server_url.",
"nullable": True,
},
}
output_type = "object"
def forward(self, server_url: str | None = None, package_name: str | None = None) -> dict:
if not server_url and not package_name:
return _error_result({
"kind": "other",
"message": "Provide either server_url or package_name.",
"retryable": False,
})
payload: dict[str, str] = {}
if server_url:
payload["server_url"] = server_url
if package_name:
payload["package_name"] = package_name
data, error = _post("/v1/metered/mcp-registry-risk", payload)
if error:
return _error_result(error)
findings = data.get("findings", [])
outcome, action = _outcome_for_severity(data.get("verdict"), found=bool(findings))
return _structured_result(
outcome, action,
reason_codes=[f.get("type", "unknown") for f in findings],
evidence=findings,
scope=f"Checked {data.get('queried', server_url or package_name)!r} against known-MCP-domain typosquat "
"distance, RelayShield's criminal IOC corpus, and RDAP registration age.",
)
class RelayShieldPromptInjectionBreachTool(Tool):
"""Checks whether an email's credentials were exposed via a breach specifically
sourced from a prompt-injection attack against an AI agent, as opposed to
traditional phishing/malware-sourced breaches."""
name = "relayshield_prompt_injection_breach"
description = (
"Checks an email address for credential exposure sourced specifically from "
"prompt-injection attacks against AI agents (distinct from ordinary breach/phishing "
"sources). Use this to vet an agent identity or user account before granting it "
"elevated trust or access."
)
inputs = {
"email": {
"type": "string",
"description": "Email address to check for credential exposure sourced from prompt-injection attacks against AI agents.",
},
}
output_type = "object"
def forward(self, email: str) -> dict:
data, error = _post("/v1/metered/prompt-injection-breach", {"email": email})
if error:
return _error_result(error)
sessions = data.get("sessions", [])
severities = [s.get("severity") for s in sessions if s.get("severity")]
highest = max(severities, key=lambda s: {"CRITICAL": 4, "HIGH": 3, "MEDIUM": 2, "LOW": 1}.get(s, 0), default=None)
outcome, action = _outcome_for_severity(highest, found=data.get("found", False))
return _structured_result(
outcome, action,
reason_codes=["prompt_injection_sourced_breach"] if data.get("found") else [],
evidence=sessions,
scope=f"Checked {email} against stolen-session records flagged as prompt-injection-sourced "
"(heuristic keyword classifier over dump-announcement text, not confirmed attribution).",
)
class RelayShieldTechStackCVETool(Tool):
"""Checks a declared technology stack (or a domain's stored stack) against
actively-exploited CVEs and high-EPSS-score vulnerabilities."""
name = "relayshield_tech_stack_cve"
description = (
"Checks a declared technology stack (e.g. nginx, WordPress, LangChain, CrewAI, n8n) "
"against actively-exploited CVEs (CISA KEV) and high-EPSS-score vulnerabilities. Covers "
"AI agent orchestration frameworks and their common companion infrastructure. Use this "
"before deploying or continuing to run a given technology stack in production."
)
inputs = {
"tech_stack": {
"type": "array",
"description": "List of declared technology product names, e.g. ['nginx', 'wordpress', 'cisco ios']. Provide this or domain.",
"nullable": True,
},
"domain": {
"type": "string",
"description": "Alternative: pull the declared tech_stack from a domain's stored profile instead of listing it inline.",
"nullable": True,
},
}
output_type = "object"
def forward(self, tech_stack: list[str] | None = None, domain: str | None = None) -> dict:
if not tech_stack and not domain:
return _error_result({
"kind": "other",
"message": "Provide either tech_stack or domain.",
"retryable": False,
})
payload: dict = {}
if tech_stack:
payload["tech_stack"] = tech_stack
if domain:
payload["domain"] = domain
data, error = _post("/v1/metered/tech-stack-cve", payload)
if error:
return _error_result(error)
# v1 shipped tool read data["matched_cves"], a key the API has never returned —
# the real keys are all_matches/critical_cves. Fixed here.
matched = data.get("all_matches", [])
critical = data.get("critical_cves", [])
highest = "CRITICAL" if critical else ("HIGH" if matched else None)
outcome, action = _outcome_for_severity(highest, found=bool(matched))
return _structured_result(
outcome, action,
reason_codes=[c.get("cve_id", "unknown") for c in critical] if critical else [],
evidence=matched,
scope=f"Checked {data.get('tech_stack_queried', tech_stack or domain)} against CISA KEV and "
"high-EPSS-score CVEs.",
)
class RelayShieldBulkIdentityRiskTool(Tool):
"""Scores up to 10 organizational domains, each with up to 5 associated agent/
employee emails, for combined breach/infostealer/session/CVE risk in one call."""
name = "relayshield_bulk_identity_risk"
description = (
"Scores up to 10 organizational domains, each with up to 5 associated agent or "
"employee emails, for combined breach/infostealer/session/CVE risk in one call. Built "
"for AI agent governance and identity-posture use cases — the entry point for scoring "
"many identities per organization in one pass rather than N sequential calls."
)
inputs = {
"targets": {
"type": "array",
"description": (
"Up to 10 objects, each shaped like "
"{'domain': 'acme.com', 'agents': ['ceo@acme.com']} — 'agents' is optional, up to 5 per domain."
),
},
}
output_type = "object"
def forward(self, targets: list[dict]) -> dict:
data, error = _post("/v1/metered/bulk-identity-risk", {"targets": targets})
if error:
return _error_result(error)
results = data.get("results", [])
critical = data.get("critical_count", 0)
high = data.get("high_count", 0)
highest = "CRITICAL" if critical else ("HIGH" if high else None)
outcome, action = _outcome_for_severity(highest, found=bool(critical or high))
return _structured_result(
outcome, action,
reason_codes=[r.get("domain", "unknown") for r in results
if r.get("domain_risk") in ("CRITICAL", "HIGH") or r.get("highest_agent_risk") in ("CRITICAL", "HIGH")],
evidence=results,
scope=f"Scored {data.get('queried', len(results))} domain(s) for combined breach/infostealer/session/CVE risk.",
)
class RelayShieldOAuthWatchlistTool(Tool):
"""Checks an email for OAuth-app exposure via breach history and active
stealer-log corpus matches — connected SaaS apps and delegated access risk."""
name = "relayshield_oauth_watchlist"
description = (
"Checks an email address for OAuth-connected-app exposure: apps matched against breach "
"history, plus stolen OAuth/session tokens found in criminal stealer logs. Use this to "
"assess risk inherited from an agent or user's connected SaaS applications and delegated "
"access before trusting that identity's current authority."
)
inputs = {
"email": {
"type": "string",
"description": "Email address to check for OAuth app exposure.",
},
}
output_type = "object"
def forward(self, email: str) -> dict:
data, error = _post("/v1/metered/oauth-watchlist", {"email": email})
if error:
return _error_result(error)
matched_apps = data.get("matched_apps", [])
stolen_tokens = data.get("stolen_tokens", [])
outcome, action = _outcome_for_severity(
data.get("highest_severity"), found=bool(matched_apps or stolen_tokens)
)
return _structured_result(
outcome, action,
reason_codes=(["oauth_app_breach_match"] if matched_apps else [])
+ (["stolen_oauth_token"] if stolen_tokens else []),
evidence=matched_apps + stolen_tokens,
scope=f"Checked {email} against HIBP breach history for OAuth-app matches and RelayShield's "
"stealer-log corpus for stolen session/OAuth tokens.",
observed_at=data.get("checked_at"),
)
class RelayShieldSupplyChainTool(Tool):
"""Checks up to 10 vendor domains for breach/infostealer exposure — third-party
and supply-chain risk inherited through vendor relationships."""
name = "relayshield_supply_chain"
description = (
"Checks up to 10 vendor domains (or vendor emails) for breach and infostealer exposure, "
"returning a per-vendor risk level and an aggregate dark-web exposure score. Use this to "
"vet third-party vendors, MCP server operators, or supply-chain dependencies before an "
"agent integrates with or grants access to them."
)
inputs = {
"vendor_domains": {
"type": "array",
"description": "Up to 10 vendor domains to check, e.g. ['vendor.example.com']. Provide this or vendor_emails.",
"nullable": True,
},
"vendor_emails": {
"type": "array",
"description": "Alternative: vendor email addresses — the domain portion is extracted automatically.",
"nullable": True,
},
}
output_type = "object"
def forward(self, vendor_domains: list[str] | None = None, vendor_emails: list[str] | None = None) -> dict:
if not vendor_domains and not vendor_emails:
return _error_result({
"kind": "other",
"message": "Provide either vendor_domains or vendor_emails.",
"retryable": False,
})
payload: dict = {}
if vendor_domains:
payload["vendor_domains"] = vendor_domains
if vendor_emails:
payload["vendor_emails"] = vendor_emails
data, error = _post("/v1/metered/supply-chain", payload)
if error:
return _error_result(error)
results = data.get("results", [])
highest = data.get("highest_risk")
outcome, action = _outcome_for_severity(highest if highest != "CLEAN" else None, found=highest not in (None, "CLEAN"))
return _structured_result(
outcome, action,
reason_codes=data.get("critical_vendors", []) + data.get("high_risk_vendors", []),
evidence=results,
scope=f"Checked {data.get('domains_checked', 0)} vendor domain(s) for breach and infostealer exposure.",
observed_at=data.get("checked_at"),
)
class RelayShieldSessionRiskTool(Tool):
"""Checks an email for active/reusable stolen session material found in
RelayShield's stealer-log corpus — session hijack and AiTM (adversary-in-the-middle)
exposure that can bypass normal authentication controls."""
name = "relayshield_session_risk"
description = (
"Checks an email address for active or reusable stolen session material (cookies, "
"tokens) found in criminal stealer logs — exposure that can bypass MFA/authentication "
"entirely, not just a password. Use this to assess whether an agent or user identity "
"currently has hijackable sessions in circulation."
)
inputs = {
"email": {
"type": "string",
"description": "Email address to check for active session/AiTM exposure.",
},
}
output_type = "object"
def forward(self, email: str) -> dict:
data, error = _post("/v1/metered/session-risk", {"email": email})
if error:
return _error_result(error)
sessions = data.get("sessions", [])
outcome, action = _outcome_for_severity(data.get("highest_severity"), found=data.get("found", False))
return _structured_result(
outcome, action,
reason_codes=[s.get("service_category", "unknown") for s in sessions],
evidence=sessions,
scope=f"Checked {email} against RelayShield's stolen-session corpus for reusable session/cookie material.",
)
class RelayShieldNHIExposureTool(Tool):
"""Checks a domain (or vendor domains) for exposed non-human-identity credentials —
API keys, service-account tokens, PATs, and other machine credentials found in
criminal stealer logs."""
name = "relayshield_nhi_exposure"
description = (
"Checks a domain, or up to 10 vendor domains, for exposed non-human-identity (NHI) "
"credentials — API keys, service-account tokens, personal access tokens, and other "
"machine identities found in criminal stealer logs. Use this to assess exposure of the "
"machine credentials an agent or its supply chain actually runs on, not just human logins."
)
inputs = {
"domain": {
"type": "string",
"description": "Your own domain to check. Provide this or vendor_domains (or both).",
"nullable": True,
},
"vendor_domains": {
"type": "array",
"description": "Up to 10 vendor/supply-chain domains to check.",
"nullable": True,
},
}
output_type = "object"
def forward(self, domain: str | None = None, vendor_domains: list[str] | None = None) -> dict:
if not domain and not vendor_domains:
return _error_result({
"kind": "other",
"message": "Provide either domain or vendor_domains.",
"retryable": False,
})
payload: dict = {}
if domain:
payload["domain"] = domain
if vendor_domains:
payload["vendor_domains"] = vendor_domains
data, error = _post("/v1/metered/nhi-exposure", payload)
if error:
return _error_result(error)
findings = data.get("findings", [])
outcome, action = _outcome_for_severity(data.get("highest_severity"), found=data.get("found", False))
return _structured_result(
outcome, action,
reason_codes=[f.get("type", "unknown") for f in findings],
evidence=findings,
scope=f"Checked {data.get('domains_checked', 0)} domain(s) against RelayShield's stealer-log "
"corpus for exposed non-human-identity credentials.",
observed_at=data.get("checked_at"),
)
class RelayShieldSecretScanTool(Tool):
"""Checks a domain (or vendor domains) for secrets/credentials exposed in
public GitHub/GitLab repositories."""
name = "relayshield_secret_scan"
description = (
"Checks a domain, or up to 5 vendor domains, for secrets and credentials exposed in "
"public GitHub/GitLab repositories. Use this to assess whether an agent's own domain, or "
"a vendor/supply-chain dependency's domain, has leaked credentials sitting in indexed "
"public source code."
)
inputs = {
"domain": {
"type": "string",
"description": "Your own domain to check. Provide this or vendor_domains (or both).",
"nullable": True,
},
"vendor_domains": {
"type": "array",
"description": "Up to 5 vendor domains to check.",
"nullable": True,
},
}
output_type = "object"
def forward(self, domain: str | None = None, vendor_domains: list[str] | None = None) -> dict:
if not domain and not vendor_domains:
return _error_result({
"kind": "other",
"message": "Provide either domain or vendor_domains.",
"retryable": False,
})
payload: dict = {}
if domain:
payload["domain"] = domain
if vendor_domains:
payload["vendor_domains"] = vendor_domains
data, error = _post("/v1/metered/secret-scan", payload)
if error:
return _error_result(error)
findings = data.get("findings", [])
outcome, action = _outcome_for_severity(data.get("highest_severity"), found=data.get("found", False))
return _structured_result(
outcome, action,
reason_codes=[f.get("severity", "unknown") for f in findings],
evidence=findings,
scope=f"Checked {data.get('domains_checked', 0)} domain(s) against public GitHub/GitLab "
"repositories for exposed secrets.",
observed_at=data.get("checked_at"),
)