diff --git a/openhands/usage/settings/mcp-settings.mdx b/openhands/usage/settings/mcp-settings.mdx index bf2d3956..6ebb10a4 100644 --- a/openhands/usage/settings/mcp-settings.mdx +++ b/openhands/usage/settings/mcp-settings.mdx @@ -192,3 +192,66 @@ Other options include: - **Custom FastAPI/Express servers**: Build your own HTTP wrapper around stdio MCP servers. - **Docker-based proxies**: Containerized solutions for better isolation. - **Cloud-hosted MCP services**: Third-party services that provide MCP endpoints. + +--- + +## Security with Maybe Don't + +[Maybe Don't](https://maybedont.ai) provides AI guardrails — real-time monitoring and policy enforcement for AI agents. It sits between OpenHands and your downstream MCP servers, validating every tool call against configurable policies (CEL rules and AI-powered analysis) before forwarding it to the downstream server. + + + Maybe Don't works as a transparent proxy. Your existing MCP server configurations stay the same — you just route them through Maybe Don't. + + +### Quick Start + +Start Maybe Don't with Docker, configuring a downstream MCP server via environment variables: + +```bash +docker run -d --name maybe-dont \ + -p 8080:8080 \ + -e MAYBE_DONT_SERVER_LISTEN_ADDR=0.0.0.0:8080 \ + -e MAYBE_DONT_REQUEST_VALIDATION_AI_ENABLED=false \ + -e MAYBE_DONT_NATIVE_TOOLS_AUDIT_REPORT_ENABLED=false \ + -e MAYBE_DONT_DOWNSTREAM_MCP_SERVERS_FILESYSTEM_TYPE=http \ + -e MAYBE_DONT_DOWNSTREAM_MCP_SERVERS_FILESYSTEM_URL=http://host.docker.internal:8081/sse \ + ghcr.io/maybedont/maybe-dont:latest +``` + + + The `LISTEN_ADDR` must be `0.0.0.0` inside Docker so the port mapping works. AI validation is disabled here for a minimal setup — enable it by setting `MAYBE_DONT_VALIDATION_AI_API_KEY` instead. See [maybedont.ai/docs](https://maybedont.ai/docs) for full configuration options. + + +Then point OpenHands at Maybe Don't instead of the downstream server directly: + +```toml +[mcp] +shttp_servers = [ + "http://localhost:8080/mcp" +] +``` + +Maybe Don't discovers tools from all downstream servers and exposes them through a single endpoint. Policy rules can target specific tools or servers using name prefixes (e.g., `filesystem__read_file`). + +For more complex setups, you can volume-mount a `maybe-dont.yaml` config file instead of using environment variables: + +```bash +docker run -d --name maybe-dont \ + -p 8080:8080 \ + -e MAYBE_DONT_SERVER_LISTEN_ADDR=0.0.0.0:8080 \ + -v ./config:/home/maybedont/.config/maybe-dont:ro \ + ghcr.io/maybedont/maybe-dont:latest +``` + +### What It Validates + +- **Deterministic rules (CEL)**: Pattern-matching on tool names, arguments, and other request fields. Fast, predictable, and auditable. +- **AI-powered rules**: Context-aware validation using an LLM for nuanced decisions that pattern matching can't handle. + +Both rule types support `audit_only` mode, so you can observe what would be blocked before enforcing policies. + + + Maybe Don't also provides a [security analyzer integration](/sdk/guides/security#maybe-dont-security-analyzer) for validating *all* agent actions (shell commands, file operations, browser actions) — not just MCP tool calls. + + +For full documentation, see [maybedont.ai/docs](https://maybedont.ai/docs). diff --git a/sdk/guides/security.mdx b/sdk/guides/security.mdx index bbd30fad..050a5499 100644 --- a/sdk/guides/security.mdx +++ b/sdk/guides/security.mdx @@ -445,6 +445,229 @@ agent = Agent(llm=llm, tools=tools, security_analyzer=security_analyzer) +### Maybe Don't Security Analyzer + +> A ready-to-run example is available [here](#ready-to-run-example-maybe-dont)! + +[Maybe Don't](https://maybedont.ai) provides AI guardrails — real-time monitoring and policy enforcement for AI agents. It evaluates agent actions against configurable policies (deterministic CEL rules and AI-powered analysis). The `MaybeDontAnalyzer` sends each action to Maybe Don't's validation endpoint and maps the response to a `SecurityRisk` level. + +This provides two complementary layers of protection: + +- **Security analyzer** (this section): Validates *all* agent actions before execution — shell commands, file operations, browser actions, and tool calls. +- **[MCP proxy](/openhands/usage/settings/mcp-settings#security-with-maybe-dont)**: Validates and proxies MCP tool calls at execution time, with response validation. + +#### Setup + +Start Maybe Don't: + +```bash +docker run -d --name maybe-dont \ + -p 8080:8080 \ + -e MAYBE_DONT_SERVER_LISTEN_ADDR=0.0.0.0:8080 \ + -e MAYBE_DONT_REQUEST_VALIDATION_AI_ENABLED=false \ + -e MAYBE_DONT_NATIVE_TOOLS_AUDIT_REPORT_ENABLED=false \ + ghcr.io/maybedont/maybe-dont:latest +``` + + + AI validation is disabled here for a minimal setup. Enable it by setting `MAYBE_DONT_VALIDATION_AI_API_KEY` with your OpenAI API key. See [maybedont.ai/docs](https://maybedont.ai/docs) for full configuration. + + +Configure the analyzer: + +```python icon="python" focus={3,5-8} +from openhands.sdk.security import MaybeDontAnalyzer +from openhands.sdk.security.confirmation_policy import ConfirmRisky + +analyzer = MaybeDontAnalyzer(gateway_url="http://localhost:8080") + +conversation = Conversation(agent=agent, workspace=".") +conversation.set_security_analyzer(analyzer) +conversation.set_confirmation_policy(ConfirmRisky()) +``` + +The `gateway_url` can also be set via the `MAYBE_DONT_GATEWAY_URL` environment variable. When neither is provided, it defaults to `http://localhost:8080`. + +#### How It Works + +When the agent produces an action, the analyzer: + +1. Extracts the tool name, arguments, and agent reasoning from the `ActionEvent`. +2. Sends a validation request to Maybe Don't's `POST /api/v1/action/validate` endpoint. +3. Maps the `risk_level` response directly to a `SecurityRisk`: + +| `risk_level` | `SecurityRisk` | Meaning | +|---|---|---| +| `high` | HIGH | Policy denied the action | +| `medium` | MEDIUM | Policy would deny, but running in audit-only mode | +| `low` | LOW | Policy evaluated and approved | +| `unknown` | UNKNOWN | No policies configured, or Maybe Don't unreachable | + +With the default `ConfirmRisky()` policy, HIGH and UNKNOWN actions require user confirmation. MEDIUM and LOW actions proceed automatically. + +#### Ready-to-run Example Maybe Dont + + +Full Maybe Don't security analyzer example: [examples/01_standalone_sdk/40_maybedont_security_analyzer.py](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/40_maybedont_security_analyzer.py) + + +Validate agent actions against configurable security policies with Maybe Don't: + +```python icon="python" expandable examples/01_standalone_sdk/40_maybedont_security_analyzer.py +"""OpenHands Agent SDK — Maybe Don't Security Analyzer Example + +This example shows how to use the MaybeDontAnalyzer to validate agent actions +against policy rules configured in a Maybe Don't Gateway before execution. + +Prerequisites: + 1. A running Maybe Don't instance. Quick start with Docker: + + docker run -d --name maybe-dont -p 8080:8080 \ + -e MAYBE_DONT_SERVER_LISTEN_ADDR=0.0.0.0:8080 \ + -e MAYBE_DONT_REQUEST_VALIDATION_AI_ENABLED=false \ + -e MAYBE_DONT_NATIVE_TOOLS_AUDIT_REPORT_ENABLED=false \ + ghcr.io/maybedont/maybe-dont:latest + + For configuration, see: https://maybedont.ai/docs + + 2. Set environment variables: + - LLM_API_KEY: Your LLM provider API key + - MAYBE_DONT_GATEWAY_URL: Maybe Don't URL (default: http://localhost:8080) + +The Maybe Don't Gateway supports two layers of protection: + - Security Analyzer (this example): Pre-execution validation of ALL actions + - MCP Proxy (separate config): Execution-time validation of MCP tool calls + +For more information, see: https://maybedont.ai/docs +""" + +import os +import signal +from collections.abc import Callable + +from pydantic import SecretStr + +from openhands.sdk import LLM, Agent, BaseConversation, Conversation +from openhands.sdk.conversation.state import ( + ConversationExecutionStatus, + ConversationState, +) +from openhands.sdk.security.confirmation_policy import ConfirmRisky +from openhands.sdk.security.maybedont import MaybeDontAnalyzer +from openhands.sdk.tool import Tool +from openhands.tools.file_editor import FileEditorTool +from openhands.tools.terminal import TerminalTool + + +# Clean ^C exit: no stack trace noise +signal.signal(signal.SIGINT, lambda *_: (_ for _ in ()).throw(KeyboardInterrupt())) + + +def _print_blocked_actions(pending_actions) -> None: + print(f"\n🔒 Maybe Don't blocked {len(pending_actions)} high-risk action(s):") + for i, action in enumerate(pending_actions, start=1): + snippet = str(action.action)[:100].replace("\n", " ") + print(f" {i}. {action.tool_name}: {snippet}...") + + +def confirm_high_risk_in_console(pending_actions) -> bool: + """ + Return True to approve, False to reject. + Defaults to 'no' on EOF/KeyboardInterrupt. + """ + _print_blocked_actions(pending_actions) + while True: + try: + ans = ( + input( + "\nThese actions were flagged as HIGH RISK by Maybe Don't. " + "Do you want to execute them anyway? (yes/no): " + ) + .strip() + .lower() + ) + except (EOFError, KeyboardInterrupt): + print("\n❌ No input received; rejecting by default.") + return False + + if ans in ("yes", "y"): + print("✅ Approved — executing high-risk actions...") + return True + if ans in ("no", "n"): + print("❌ Rejected — skipping high-risk actions...") + return False + print("Please enter 'yes' or 'no'.") + + +def run_until_finished_with_security( + conversation: BaseConversation, confirmer: Callable[[list], bool] +) -> None: + """ + Drive the conversation until FINISHED. + - If WAITING_FOR_CONFIRMATION: ask the confirmer. + * On approve: set execution_status = IDLE. + * On reject: conversation.reject_pending_actions(...). + """ + while conversation.state.execution_status != ConversationExecutionStatus.FINISHED: + if ( + conversation.state.execution_status + == ConversationExecutionStatus.WAITING_FOR_CONFIRMATION + ): + pending = ConversationState.get_unmatched_actions(conversation.state.events) + if not pending: + raise RuntimeError( + "⚠️ Agent is waiting for confirmation but no pending actions " + "were found. This should not happen." + ) + if not confirmer(pending): + conversation.reject_pending_actions("User rejected high-risk actions") + continue + + print("▶️ Running conversation.run()...") + conversation.run() + + +# Configure LLM +api_key = os.getenv("LLM_API_KEY") +assert api_key is not None, "LLM_API_KEY environment variable is not set." +model = os.getenv("LLM_MODEL", "anthropic/claude-sonnet-4-5-20250929") +base_url = os.getenv("LLM_BASE_URL") +llm = LLM( + usage_id="maybedont-security", + model=model, + base_url=base_url, + api_key=SecretStr(api_key), +) + +# Tools +tools = [ + Tool(name=TerminalTool.name), + Tool(name=FileEditorTool.name), +] + +# Agent +agent = Agent(llm=llm, tools=tools) + +# Conversation with Maybe Don't security analyzer +# The analyzer calls the Maybe Don't Gateway to validate actions before execution. +# Gateway URL defaults to http://localhost:8080, or set MAYBE_DONT_GATEWAY_URL. +conversation = Conversation( + agent=agent, persistence_dir="./.conversations", workspace="." +) +conversation.set_security_analyzer(MaybeDontAnalyzer()) +conversation.set_confirmation_policy(ConfirmRisky()) + +print("\n1) Safe command (LOW risk - should execute automatically)...") +conversation.send_message("List files in the current directory") +conversation.run() + +print("\n2) Potentially risky command (may require confirmation)...") +conversation.send_message("Delete all files in the /tmp directory recursively") +run_until_finished_with_security(conversation, confirm_high_risk_in_console) +``` + + + --- ## Configurable Security Policy