Skip to content

Commit 32fa312

Browse files
authored
Merge branch 'main' into update_redis
2 parents d2f59d1 + 1924099 commit 32fa312

13 files changed

Lines changed: 433 additions & 74 deletions

File tree

README.md

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,14 @@ Or add it directly to your Claude Desktop config (`claude_desktop_config.json`):
8888

8989
This exposes 3 tools to your AI agent: `check_message_safety`, `get_session_risk`, and `list_recent_escalations`.
9090

91+
For HTTP MCP, the server binds to `127.0.0.1` by default. If you expose it
92+
beyond localhost, set a bearer token first:
93+
94+
```bash
95+
export HUMANE_PROXY_ADMIN_KEY=your-secret-token
96+
humane-proxy mcp-serve --transport http --host 0.0.0.0 --port 3000
97+
```
98+
9199
---
92100

93101
## Available On
@@ -441,16 +449,27 @@ curl -X DELETE http://localhost:8000/admin/sessions/user-42 \
441449
```bash
442450
pip install humane-proxy[mcp]
443451
humane-proxy mcp-serve # stdio (default)
444-
humane-proxy mcp-serve --transport http --port 3000 # HTTP
452+
humane-proxy mcp-serve --transport http --port 3000 # HTTP on 127.0.0.1
445453
```
446454

455+
HTTP MCP is local-only by default. To bind publicly, pass `--host 0.0.0.0`
456+
explicitly and protect tool access with a bearer token:
457+
458+
```bash
459+
export HUMANE_PROXY_ADMIN_KEY=your-secret-token
460+
humane-proxy mcp-serve --transport http --host 0.0.0.0 --port 3000
461+
```
462+
463+
Clients must send `Authorization: Bearer your-secret-token` when the token is
464+
configured. Leave `HUMANE_PROXY_ADMIN_KEY` unset for stdio/local-only MCP.
465+
447466
Exposes three tools via Model Context Protocol:
448467

449468
| Tool | Description |
450469
|---|---|
451470
| `check_message_safety` | Full pipeline classification |
452-
| `get_session_risk` | Session trajectory (trend, spike, category counts) |
453-
| `list_recent_escalations` | Audit log query |
471+
| `get_session_risk` | Read-only session trajectory snapshot (trend, spike, category counts) |
472+
| `list_recent_escalations` | Bounded audit log query |
454473

455474
Available on the [Official MCP Registry](https://registry.modelcontextprotocol.io).
456475

humane_proxy/api/admin.py

Lines changed: 15 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from __future__ import annotations
2020

2121
import csv
22+
import hmac
2223
import io
2324
import json
2425
import logging
@@ -28,7 +29,7 @@
2829
from datetime import datetime, timezone
2930
from typing import Any
3031

31-
from fastapi import APIRouter, Depends, HTTPException, Query
32+
from fastapi import APIRouter, Depends, HTTPException, Query, Response
3233
from fastapi.responses import StreamingResponse
3334
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
3435

@@ -60,7 +61,7 @@ def _require_admin(
6061
"environment variable to enable it."
6162
),
6263
)
63-
if credentials is None or credentials.credentials != admin_key:
64+
if credentials is None or not hmac.compare_digest(credentials.credentials, admin_key):
6465
raise HTTPException(
6566
status_code=401,
6667
detail="Invalid or missing Bearer token.",
@@ -298,32 +299,21 @@ def get_session_risk(
298299
finally:
299300
conn.close()
300301

301-
from humane_proxy.risk.trajectory import analyze
302+
from humane_proxy.risk.trajectory import snapshot
302303

303-
# Build trajectory by replaying each escalation.
304-
trajectory = None
305-
for row in rows:
306-
rec = _row_to_dict(row)
307-
trajectory = analyze(
308-
session_id + "_admin_replay", # isolated session key
309-
rec["risk_score"],
310-
rec.get("category", "safe"),
311-
)
304+
trajectory = snapshot(session_id)
312305

313306
return {
314307
"session_id": session_id,
315308
"escalation_count": len(rows),
316309
"history": [_row_to_dict(r) for r in rows],
317-
"trajectory": (
318-
{
319-
"spike_detected": trajectory.spike_detected,
320-
"trend": trajectory.trend,
321-
"window_scores": trajectory.window_scores,
322-
"category_counts": trajectory.category_counts,
323-
}
324-
if trajectory
325-
else None
326-
),
310+
"trajectory": {
311+
"spike_detected": trajectory.spike_detected,
312+
"trend": trajectory.trend,
313+
"window_scores": trajectory.window_scores,
314+
"category_counts": trajectory.category_counts,
315+
"message_count": trajectory.message_count,
316+
},
327317
}
328318

329319

@@ -381,11 +371,11 @@ def get_stats(_: str = Depends(_require_admin)) -> dict:
381371
}
382372

383373

384-
@router.delete("/sessions/{session_id}", status_code=204)
374+
@router.delete("/sessions/{session_id}", status_code=204, response_class=Response)
385375
def delete_session_data(
386376
session_id: str,
387377
_: str = Depends(_require_admin),
388-
) -> None:
378+
) -> Response:
389379
"""Delete all escalation records for a session (privacy right to erasure)."""
390380
conn = _get_conn()
391381
try:
@@ -397,3 +387,4 @@ def delete_session_data(
397387
conn.close()
398388

399389
logger.info("Deleted %d records for session %s (admin request)", deleted, session_id)
390+
return Response(status_code=204)

humane_proxy/cli.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -555,13 +555,14 @@ async def _run_all():
555555
@click.option("--transport", "-t", default="stdio",
556556
type=click.Choice(["stdio", "http"]),
557557
help="Transport mode: stdio (default) or http")
558-
@click.option("--host", default="0.0.0.0", help="HTTP bind host (default: 0.0.0.0)")
558+
@click.option("--host", default="127.0.0.1", help="HTTP bind host (default: 127.0.0.1)")
559559
@click.option("--port", "-p", default=3000, type=int, help="HTTP bind port (default: 3000)")
560560
def mcp_serve(transport: str, host: str, port: int) -> None:
561561
"""Start the MCP server (requires [mcp] extra).
562562
563563
Use --transport stdio (default) for local integration with agents.
564-
Use --transport http for remote access and registry listing.
564+
Use --transport http for HTTP access. Set HUMANE_PROXY_ADMIN_KEY
565+
before exposing HTTP MCP beyond localhost.
565566
"""
566567
try:
567568
if transport == "http":

humane_proxy/escalation/query.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
"""Shared escalation query validation helpers."""
2+
3+
from __future__ import annotations
4+
5+
ALLOWED_ESCALATION_CATEGORIES = frozenset({"self_harm", "criminal_intent"})
6+
DEFAULT_ESCALATION_LIMIT = 20
7+
MAX_ESCALATION_LIMIT = 100
8+
9+
10+
def normalize_escalation_query(
11+
limit: int = DEFAULT_ESCALATION_LIMIT,
12+
category: str | None = None,
13+
) -> tuple[int, str | None]:
14+
"""Clamp escalation query size and validate optional category filters."""
15+
try:
16+
normalized_limit = int(limit)
17+
except (TypeError, ValueError):
18+
normalized_limit = DEFAULT_ESCALATION_LIMIT
19+
20+
normalized_limit = max(1, min(normalized_limit, MAX_ESCALATION_LIMIT))
21+
normalized_category = category.strip() if isinstance(category, str) else None
22+
if normalized_category == "":
23+
normalized_category = None
24+
25+
if normalized_category and normalized_category not in ALLOWED_ESCALATION_CATEGORIES:
26+
allowed = ", ".join(sorted(ALLOWED_ESCALATION_CATEGORIES))
27+
raise ValueError(f"category must be one of: {allowed}")
28+
29+
return normalized_limit, normalized_category

humane_proxy/integrations/autogen.py

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -59,16 +59,9 @@ def get_session_risk(session_id: str) -> str:
5959
Returns:
6060
JSON string with spike detection, trend, and category distribution.
6161
"""
62-
from humane_proxy.risk.trajectory import analyze
62+
from humane_proxy.risk.trajectory import snapshot, to_dict
6363

64-
result = analyze(session_id, 0.0, "safe")
65-
return json.dumps({
66-
"spike_detected": result.spike_detected,
67-
"trend": result.trend,
68-
"window_scores": result.window_scores,
69-
"category_counts": result.category_counts,
70-
"message_count": result.message_count,
71-
}, indent=2)
64+
return json.dumps(to_dict(snapshot(session_id)), indent=2)
7265

7366

7467
def list_recent_escalations(limit: int = 20, category: str = "") -> str:
@@ -81,11 +74,13 @@ def list_recent_escalations(limit: int = 20, category: str = "") -> str:
8174
Returns:
8275
JSON string with list of escalation records.
8376
"""
77+
from humane_proxy.escalation.query import normalize_escalation_query
8478
from humane_proxy.storage.factory import get_store
8579

80+
limit, category = normalize_escalation_query(limit, category)
8681
store = get_store()
8782
results = store.query(
88-
category=category if category else None,
83+
category=category,
8984
limit=limit,
9085
)
9186
return json.dumps(results, indent=2, default=str)

humane_proxy/integrations/crewai.py

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -94,30 +94,25 @@ class GetSessionRiskTool(BaseTool):
9494
args_schema: Type[BaseModel] = SessionRiskInput
9595

9696
def _run(self, session_id: str) -> str:
97-
from humane_proxy.risk.trajectory import analyze
97+
from humane_proxy.risk.trajectory import snapshot, to_dict
9898
import json
9999

100-
result = analyze(session_id, 0.0, "safe")
101-
return json.dumps({
102-
"spike_detected": result.spike_detected,
103-
"trend": result.trend,
104-
"window_scores": result.window_scores,
105-
"category_counts": result.category_counts,
106-
"message_count": result.message_count,
107-
}, indent=2)
100+
return json.dumps(to_dict(snapshot(session_id)), indent=2)
108101

109102
class ListEscalationsTool(BaseTool):
110103
name: str = "list_recent_escalations"
111104
description: str = "Query recent escalation events from the safety audit log."
112105
args_schema: Type[BaseModel] = ListEscalationsInput
113106

114107
def _run(self, limit: int = 20, category: str = "") -> str:
108+
from humane_proxy.escalation.query import normalize_escalation_query
115109
from humane_proxy.storage.factory import get_store
116110
import json
117111

112+
limit, category = normalize_escalation_query(limit, category)
118113
store = get_store()
119114
results = store.query(
120-
category=category if category else None,
115+
category=category,
121116
limit=limit,
122117
)
123118
return json.dumps(results, indent=2, default=str)

humane_proxy/integrations/llamaindex.py

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -56,16 +56,9 @@ def _get_session_risk(session_id: str) -> dict:
5656
dict
5757
``{"spike_detected": bool, "trend": str, "window_scores": list, ...}``
5858
"""
59-
from humane_proxy.risk.trajectory import analyze
59+
from humane_proxy.risk.trajectory import snapshot, to_dict
6060

61-
result = analyze(session_id, 0.0, "safe")
62-
return {
63-
"spike_detected": result.spike_detected,
64-
"trend": result.trend,
65-
"window_scores": result.window_scores,
66-
"category_counts": result.category_counts,
67-
"message_count": result.message_count,
68-
}
61+
return to_dict(snapshot(session_id))
6962

7063

7164
def _list_recent_escalations(limit: int = 20, category: str | None = None) -> list[dict]:
@@ -83,8 +76,10 @@ def _list_recent_escalations(limit: int = 20, category: str | None = None) -> li
8376
list[dict]
8477
List of escalation records.
8578
"""
79+
from humane_proxy.escalation.query import normalize_escalation_query
8680
from humane_proxy.storage.factory import get_store
8781

82+
limit, category = normalize_escalation_query(limit, category)
8883
store = get_store()
8984
return store.query(category=category, limit=limit)
9085

0 commit comments

Comments
 (0)