Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions openhands/usage/settings/mcp-settings.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<Note>
Maybe Don't works as a transparent proxy. Your existing MCP server configurations stay the same — you just route them through Maybe Don't.
</Note>

### 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
```

<Note>
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.
</Note>

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.

<Tip>
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.
</Tip>

For full documentation, see [maybedont.ai/docs](https://maybedont.ai/docs).
223 changes: 223 additions & 0 deletions sdk/guides/security.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,229 @@ agent = Agent(llm=llm, tools=tools, security_analyzer=security_analyzer)
</Tip>


### 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
```

<Note>
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.
</Note>

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

<Note>
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)
</Note>

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)
```

<RunExampleCode path_to_script="examples/01_standalone_sdk/40_maybedont_security_analyzer.py"/>

---

## Configurable Security Policy
Expand Down