From 9cdb77f3a6aa5cd928cab0cfb83f22526e27d108 Mon Sep 17 00:00:00 2001 From: Daniel DeGroff Date: Thu, 19 Feb 2026 17:23:17 -0700 Subject: [PATCH 1/3] docs: add Maybe Don't integration guides Add Maybe Don't as an MCP security proxy option on the MCP Settings page and as a third-party security analyzer on the Security & Action Confirmation guide. MCP Settings: - Docker quick-start for running Maybe Don't - Configuration examples for downstream MCP servers - Overview of CEL and AI-powered validation rules Security Guide: - MaybeDontAnalyzer setup and configuration - Risk level mapping table - Ready-to-run example with ConfirmRisky policy - Cross-links between MCP proxy and security analyzer integrations Co-Authored-By: Claude Opus 4.6 --- openhands/usage/settings/mcp-settings.mdx | 56 +++++++ sdk/guides/security.mdx | 177 ++++++++++++++++++++++ 2 files changed, 233 insertions(+) diff --git a/openhands/usage/settings/mcp-settings.mdx b/openhands/usage/settings/mcp-settings.mdx index bf2d3956..a9e8575e 100644 --- a/openhands/usage/settings/mcp-settings.mdx +++ b/openhands/usage/settings/mcp-settings.mdx @@ -192,3 +192,59 @@ 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: + +```bash +docker run -d --name maybe-dont \ + -p 8080:8080 \ + ghcr.io/maybedont/maybe-dont:latest +``` + +Then configure your downstream MCP servers in `maybe-dont.yaml`: + +```yaml +downstream_mcp_servers: + filesystem: + type: http + url: "http://localhost:8081/sse" + fetch: + type: http + url: "http://localhost:8082/sse" +``` + +Point OpenHands at Maybe Don't instead of the individual servers: + +```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`). + +### 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..55e01237 100644 --- a/sdk/guides/security.mdx +++ b/sdk/guides/security.mdx @@ -445,6 +445,183 @@ 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 \ + ghcr.io/maybedont/maybe-dont:latest +``` + +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 Maybe Don't before execution. + +Prerequisites: + Start Maybe Don't: + docker run -d --name maybe-dont -p 8080:8080 \ + ghcr.io/maybedont/maybe-dont:latest + +Environment variables: + LLM_API_KEY - API key for your LLM provider (required) + LLM_MODEL - Model to use (default: anthropic/claude-sonnet-4-5-20250929) + LLM_BASE_URL - Custom base URL for your LLM provider + MAYBE_DONT_GATEWAY_URL - Maybe Don't URL (default: http://localhost:8080) +""" + +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 + + +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)} 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_in_console(pending_actions) -> bool: + _print_blocked_actions(pending_actions) + while True: + try: + ans = ( + input("\nExecute these actions? (yes/no): ") + .strip() + .lower() + ) + except (EOFError, KeyboardInterrupt): + return False + if ans in ("yes", "y"): + return True + if ans in ("no", "n"): + return False + print("Please enter 'yes' or 'no'.") + + +def run_until_finished( + conversation: BaseConversation, confirmer: Callable +) -> None: + 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("Waiting for confirmation but no pending actions.") + if not confirmer(pending): + conversation.reject_pending_actions("User rejected") + continue + conversation.run() + + +# Configure LLM +api_key = os.getenv("LLM_API_KEY") +assert api_key, "LLM_API_KEY environment variable is not set." +llm = LLM( + usage_id="agent", + model=os.getenv("LLM_MODEL", "anthropic/claude-sonnet-4-5-20250929"), + base_url=os.getenv("LLM_BASE_URL"), + api_key=SecretStr(api_key), +) + +# Agent with terminal and file editor tools +agent = Agent(llm=llm, tools=[Tool(name=TerminalTool.name), Tool(name=FileEditorTool.name)]) + +# Conversation with Maybe Don't security analyzer +conversation = Conversation(agent=agent, workspace=".") +conversation.set_security_analyzer(MaybeDontAnalyzer()) +conversation.set_confirmation_policy(ConfirmRisky()) + +# 1) Safe command — LOW risk, executes automatically +print("\n1) Safe command (should execute automatically)...") +conversation.send_message("List the files in the current directory") +run_until_finished(conversation, confirm_in_console) + +# 2) Risky command — may be HIGH risk depending on configured policies +print("\n2) Potentially risky command (may require confirmation)...") +conversation.send_message("Delete all .tmp files in the current directory") +run_until_finished(conversation, confirm_in_console) + +print("\n=== Example Complete ===") +``` + + + --- ## Configurable Security Policy From 05cc68723a490b6262ee79f898141239108859aa Mon Sep 17 00:00:00 2001 From: Daniel DeGroff Date: Fri, 20 Feb 2026 10:20:09 -0700 Subject: [PATCH 2/3] fix: Docker examples that actually work out of the box - Add MAYBE_DONT_SERVER_LISTEN_ADDR=0.0.0.0:8080 (127.0.0.1 is unreachable from outside a container) - Disable AI validation and audit report (require OpenAI API key) - Use env vars for downstream MCP server config instead of YAML that has no volume mount instruction - Add volume-mount alternative for complex configs - Match inline example code to actual SDK file Co-Authored-By: Claude Opus 4.6 --- openhands/usage/settings/mcp-settings.mdx | 33 +++--- sdk/guides/security.mdx | 116 +++++++++++++++------- 2 files changed, 99 insertions(+), 50 deletions(-) diff --git a/openhands/usage/settings/mcp-settings.mdx b/openhands/usage/settings/mcp-settings.mdx index a9e8575e..6ebb10a4 100644 --- a/openhands/usage/settings/mcp-settings.mdx +++ b/openhands/usage/settings/mcp-settings.mdx @@ -205,27 +205,24 @@ Other options include: ### Quick Start -Start Maybe Don't with Docker: +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 ``` -Then configure your downstream MCP servers in `maybe-dont.yaml`: - -```yaml -downstream_mcp_servers: - filesystem: - type: http - url: "http://localhost:8081/sse" - fetch: - type: http - url: "http://localhost:8082/sse" -``` + + 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. + -Point OpenHands at Maybe Don't instead of the individual servers: +Then point OpenHands at Maybe Don't instead of the downstream server directly: ```toml [mcp] @@ -236,6 +233,16 @@ shttp_servers = [ 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. diff --git a/sdk/guides/security.mdx b/sdk/guides/security.mdx index 55e01237..1ea61f46 100644 --- a/sdk/guides/security.mdx +++ b/sdk/guides/security.mdx @@ -463,9 +463,16 @@ 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} @@ -509,19 +516,25 @@ 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 Maybe Don't before execution. +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: - Start Maybe Don't: - docker run -d --name maybe-dont -p 8080:8080 \ - ghcr.io/maybedont/maybe-dont:latest - -Environment variables: - LLM_API_KEY - API key for your LLM provider (required) - LLM_MODEL - Model to use (default: anthropic/claude-sonnet-4-5-20250929) - LLM_BASE_URL - Custom base URL for your LLM provider - MAYBE_DONT_GATEWAY_URL - Maybe Don't URL (default: http://localhost:8080) + 1. A running Maybe Don't Gateway instance. Quick start with Docker: + + docker run -p 8080:8080 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: Gateway 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 @@ -542,82 +555,111 @@ 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)} action(s):") + 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_in_console(pending_actions) -> bool: +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("\nExecute these actions? (yes/no): ") + 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( - conversation: BaseConversation, confirmer: Callable +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 - ) + pending = ConversationState.get_unmatched_actions(conversation.state.events) if not pending: - raise RuntimeError("Waiting for confirmation but no pending actions.") + 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") + 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, "LLM_API_KEY environment variable is not set." +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="agent", - model=os.getenv("LLM_MODEL", "anthropic/claude-sonnet-4-5-20250929"), - base_url=os.getenv("LLM_BASE_URL"), + usage_id="maybedont-security", + model=model, + base_url=base_url, api_key=SecretStr(api_key), ) -# Agent with terminal and file editor tools -agent = Agent(llm=llm, tools=[Tool(name=TerminalTool.name), Tool(name=FileEditorTool.name)]) +# Tools +tools = [ + Tool(name=TerminalTool.name), + Tool(name=FileEditorTool.name), +] + +# Agent +agent = Agent(llm=llm, tools=tools) # Conversation with Maybe Don't security analyzer -conversation = Conversation(agent=agent, workspace=".") +# 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()) -# 1) Safe command — LOW risk, executes automatically -print("\n1) Safe command (should execute automatically)...") -conversation.send_message("List the files in the current directory") -run_until_finished(conversation, confirm_in_console) +print("\n1) Safe command (LOW risk - should execute automatically)...") +conversation.send_message("List files in the current directory") +conversation.run() -# 2) Risky command — may be HIGH risk depending on configured policies print("\n2) Potentially risky command (may require confirmation)...") -conversation.send_message("Delete all .tmp files in the current directory") -run_until_finished(conversation, confirm_in_console) - -print("\n=== Example Complete ===") +conversation.send_message("Delete all files in the /tmp directory recursively") +run_until_finished_with_security(conversation, confirm_high_risk_in_console) ``` From fe56d301d3f838c2aee43bc6d90d095ed5e0bc43 Mon Sep 17 00:00:00 2001 From: Daniel DeGroff Date: Fri, 20 Feb 2026 10:27:47 -0700 Subject: [PATCH 3/3] fix: match inline example to SDK file (Docker env vars) Co-Authored-By: Claude Opus 4.6 --- sdk/guides/security.mdx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/sdk/guides/security.mdx b/sdk/guides/security.mdx index 1ea61f46..050a5499 100644 --- a/sdk/guides/security.mdx +++ b/sdk/guides/security.mdx @@ -520,15 +520,19 @@ 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 Gateway instance. Quick start with Docker: + 1. A running Maybe Don't instance. Quick start with Docker: - docker run -p 8080:8080 ghcr.io/maybedont/maybe-dont:latest + 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: Gateway URL (default: http://localhost:8080) + - 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