Summary
The MCP server's multi-agent isolation is a convention, not a control. On the HTTP transport there is no authentication, and agent identity is entirely client-asserted, so any network-adjacent client can read, write, and delete every agent's memories.
Findings
No authentication on the HTTP transport (critical)
mcp_server/server.py:386, mcp_server/cli.py:169 — FastMCP(name=..., host="0.0.0.0", port=8000) is created with no auth middleware, no token check, and no header inspection. The CLI defaults --host to 0.0.0.0.
- Scenario: any process that can reach port 8000 calls every tool —
memory_recall with agent_identity="" searches all agents, plus arbitrary writes/deletes.
Agent identity is purely client-asserted (critical)
X-Hermes-Session-Key is only referenced in a comment (hexus/__init__.py:533); nothing in server.py/tools.py extracts an HTTP header or binds a connection to an identity. Every tool takes agent_identity as a plain caller-chosen string.
- Scenario:
memory_forget(id=…, confirm=true, agent_identity="agent-b") deletes agent B's rows. The docstring claim "you can only delete rows you own" (server.py:727-732) is false.
Unscoped destructive / read-by-id tools (critical)
memory_cleanup (server.py:949-969) → store.cleanup_stale_records (store.py:1746) issues DELETE FROM memory_entries WHERE updated_at < %s with no agent filter and no confirm gate. One call deletes every agent's old memories fleet-wide.
memory_retrieve/headroom_retrieve (tools.py:912-929) → store.fetch_full(id) (store.py:1298) selects by sequential BIGSERIAL id only. Enumerate id=1..N to dump every agent's content.
memory_confirm/memory_reject and memory_summarize_session share the same by-id, unscoped pattern (cross-agent feedback poisoning → drives consolidation/deletion; cross-agent session reads).
Unauthenticated /metrics leaks the agent roster (high)
server.py:1180-1190 + _generate_metrics (server.py:69-220) emit per-agent-labelled series (hexus_memory_entries_count{agent_identity="…"}, delegation/recall counts). Any client can curl /metrics and enumerate every agent plus their data volumes.
agent_identity default asymmetry (medium, security-relevant)
Empty agent_identity means "this agent" in memory_search/memory_count (tools.py:291,476) but "ALL agents" in memory_recall (tools.py:232-234). The same input has opposite scoping depending on the tool.
Suggested direction
- Add an auth layer on the HTTP transport (bearer token / API key middleware), or bind to loopback by default and document a reverse-proxy for exposure.
- Derive
agent_identity from an authenticated credential (session key/token) rather than trusting the tool argument; treat the argument as a scope hint within the caller's own identity.
- Scope
fetch_full/confirm_entry/reject_entry/cleanup_stale_records/session reads by agent_identity.
- Gate
memory_cleanup behind confirm + scoping; gate /metrics behind auth.
- Make empty-
agent_identity semantics consistent across tools.
Filed from the 2026-07 codebase review.
Summary
The MCP server's multi-agent isolation is a convention, not a control. On the HTTP transport there is no authentication, and agent identity is entirely client-asserted, so any network-adjacent client can read, write, and delete every agent's memories.
Findings
No authentication on the HTTP transport (critical)
mcp_server/server.py:386,mcp_server/cli.py:169—FastMCP(name=..., host="0.0.0.0", port=8000)is created with no auth middleware, no token check, and no header inspection. The CLI defaults--hostto0.0.0.0.memory_recallwithagent_identity=""searches all agents, plus arbitrary writes/deletes.Agent identity is purely client-asserted (critical)
X-Hermes-Session-Keyis only referenced in a comment (hexus/__init__.py:533); nothing inserver.py/tools.pyextracts an HTTP header or binds a connection to an identity. Every tool takesagent_identityas a plain caller-chosen string.memory_forget(id=…, confirm=true, agent_identity="agent-b")deletes agent B's rows. The docstring claim "you can only delete rows you own" (server.py:727-732) is false.Unscoped destructive / read-by-id tools (critical)
memory_cleanup(server.py:949-969) →store.cleanup_stale_records(store.py:1746) issuesDELETE FROM memory_entries WHERE updated_at < %swith no agent filter and no confirm gate. One call deletes every agent's old memories fleet-wide.memory_retrieve/headroom_retrieve(tools.py:912-929) →store.fetch_full(id)(store.py:1298) selects by sequential BIGSERIAL id only. Enumerateid=1..Nto dump every agent's content.memory_confirm/memory_rejectandmemory_summarize_sessionshare the same by-id, unscoped pattern (cross-agent feedback poisoning → drives consolidation/deletion; cross-agent session reads).Unauthenticated
/metricsleaks the agent roster (high)server.py:1180-1190+_generate_metrics(server.py:69-220) emit per-agent-labelled series (hexus_memory_entries_count{agent_identity="…"}, delegation/recall counts). Any client can curl/metricsand enumerate every agent plus their data volumes.agent_identitydefault asymmetry (medium, security-relevant)Empty
agent_identitymeans "this agent" inmemory_search/memory_count(tools.py:291,476) but "ALL agents" inmemory_recall(tools.py:232-234). The same input has opposite scoping depending on the tool.Suggested direction
agent_identityfrom an authenticated credential (session key/token) rather than trusting the tool argument; treat the argument as a scope hint within the caller's own identity.fetch_full/confirm_entry/reject_entry/cleanup_stale_records/session reads byagent_identity.memory_cleanupbehindconfirm+ scoping; gate/metricsbehind auth.agent_identitysemantics consistent across tools.Filed from the 2026-07 codebase review.