Skip to content

fix: multi-agent isolation + SQL safety at the data layer (#19) - #29

Merged
codenamekt merged 3 commits into
fix/mcp-http-authfrom
fix/multi-agent-isolation
Jul 11, 2026
Merged

codenamekt merged 3 commits into
fix/mcp-http-authfrom
fix/multi-agent-isolation

Conversation

@codenamekt

Copy link
Copy Markdown
Owner

Closes #19. Stacked on #28 (fix/mcp-http-auth) — base is fix/mcp-http-auth; retarget to main once #28 merges.

Framing

The #19 review assumed a zero-trust multi-tenant model. This deployment is a trusted fleet that wants shared memory (agy ↔ hermes cross-recall). So the fixes split into genuine bugs (fixed unconditionally) and a configurable isolation policy that defaults to sharing. Decided with @codenamekt: reads shared, mutations scoped, default shared.

SQL safety (unconditional — the original "critical" items)

  • increment_recall_counts: allowlist the interpolated table identifier ({memory_entries, conversations, delegations}); reject anything else with ValueError. A table name is an identifier, not a bindable param, so an allowlist is the safe equivalent of parameterization.
  • replace() / remove(): new _escape_like() helper (escapes \ % _) + ESCAPE '\' on all three LIKE sites. remove(old_text="%") no longer wipes the whole (agent, target) scope; _ no longer matches any character.

Isolation policy — HEXUS_MEMORY_ISOLATION (default shared)

  • shared: reads/recall/search span all agents — one shared knowledge base for a trusted fleet.
  • strict: reads scoped to the caller's own identity.
  • Cross-agent mutations (confirm/reject/remove/forget/summarize-by-id) are always caller-scoped, in both modes.
  • Resolves the empty-agent_identity asymmetry (item C): empty means "all agents" in shared / "this agent" in strict, consistently across recall/hybrid_search/search/count.
  • Latent bug fixed: list_entries treated agent_identity=None as WHERE agent_identity = NULL (zero rows); now all-agents, matching search()/count().

By-id scoping (item B)

fetch_full / confirm_entry / reject_entry / summarize_session take an optional agent_identity and filter on it. fetch_full bypasses the id-keyed CCRCache when strict-scoped so the cache can't leak a row the caller doesn't own. Threaded through the MCP tools and the hermes in-process handlers (which pass self._agent_identity).

Server-derived identity (item A)

The HTTP transport publishes the authenticated X-Hermes-Session-Key as the caller identity (tools.current_caller ContextVar, set per-request inside the bearer-auth gate). It overrides a client-supplied agent_identity for writes/mutations, so an authenticated client can no longer act as another agent. stdio / in-process callers keep the pre-#19 behavior.

Note: #28's auth is a single shared HEXUS_API_TOKEN (an endpoint gate, not a per-agent credential), so there was no credential→identity mapping to derive from. The X-Hermes-Session-Key header is the fleet's actual per-agent signal, so that's the authoritative source here. A token→identity map is the stronger-isolation upgrade if this ever goes multi-tenant.

Tests

  • tests/test_http_auth.py: pure/no-DB tests for _escape_like, _resolve_isolation, the identity resolvers, and the _wrap_with_identity header→ContextVar middleware.
  • tests/test_mcp_server.py: DB-backed tests — table allowlist rejection, LIKE %/_ escaping, cross-agent confirm/reject/summarize blocked, shared-mode cross-agent read allowed, strict-mode read confinement + CCRCache non-leak.

Reviewer notes ⚠️

  • Authored in an env without pip/pytest/psycopg/mcp — validated with py_compile + a standalone run of the pure logic. Please run the full suite with PG_TEST_DSN set and mcp installed.
  • Item A relies on the ASGI middleware's ContextVar surviving into FastMCP's streamable-http tool dispatch (unit-tested at the wrapper level; end-to-end through FastMCP internals not run here). Degrades safely to the env default if it doesn't propagate on a given mcp version — worth a live check with a real X-Hermes-Session-Key request. Fallback: FastMCP get_http_headers() / request-context API.

🤖 Generated with Claude Code

Configurable isolation policy, SQL-safety fixes, by-id scoping, and
server-derived caller identity. Stacked on #28 (fix/mcp-http-auth).

SQL safety (unconditional):
- increment_recall_counts: allowlist the interpolated table identifier
  ({memory_entries, conversations, delegations}); reject anything else.
- replace()/remove(): escape LIKE metacharacters (\ % _) + ESCAPE '\',
  so remove(old_text="%") no longer wipes the whole (agent,target) scope.

Isolation policy (HEXUS_MEMORY_ISOLATION, default "shared"):
- shared: reads/recall/search span all agents (trusted-fleet default).
- strict: reads scoped to the caller's identity.
- Cross-agent MUTATIONS (confirm/reject/remove/forget/summarize by id)
  are always caller-scoped, in both modes.
- Resolves the empty-agent_identity asymmetry (item C): empty means
  "all agents" in shared / "this agent" in strict, consistently.
- list_entries now treats agent_identity=None as all-agents, matching
  search()/count().

By-id scoping (item B):
- fetch_full/confirm_entry/reject_entry/summarize_session take an
  optional agent_identity and filter on it; fetch_full bypasses the
  id-keyed CCRCache when strict-scoped so it can't leak another agent's
  row. Threaded through the MCP tools and the hermes handlers.

Server-derived identity (item A):
- HTTP transport publishes the authenticated X-Hermes-Session-Key as the
  caller identity (tools.current_caller ContextVar); it overrides the
  client-supplied agent_identity for writes/mutations, so an
  authenticated client can no longer act as another agent. stdio /
  in-process callers keep the prior behavior.

Tests: SQL-safety + isolation unit tests (no DB) in test_http_auth.py;
DB-backed cross-agent scoping tests in test_mcp_server.py.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread mcp_server/server.py
return
headers = dict(scope.get("headers") or [])
raw = headers.get(b"x-hermes-session-key", b"").decode("latin-1").strip()
token = tools.current_caller.set(raw or None)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: The tools.current_caller ContextVar is set per-request here, but the server-derived identity depends on this ContextVar propagating into FastMCP's tool dispatch. If FastMCP internally spawns tool execution in a context-isolated task, the ContextVar value is lost and _write_identity/_scope_identity fall through to the client-supplied agent_identity arg — defeating the isolation model.

The PR description flags this as untested end-to-end. Consider adding a runtime assertion or log warning when current_caller.get() returns None inside tool invocations on the HTTP transport to catch this failure mode early.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Comment thread hexus/store.py Outdated
When `agent_identity` is supplied the session lookup is scoped to that
agent, so a caller cannot summarize another agent's session by id.
"""
scope = "" if agent_identity is None else " AND agent_identity = %s"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: The scope variable is embedded into SQL via f-string interpolation. While currently safe (the variable is binary: "" or " AND agent_identity = %s"), this pattern is fragile — a future extension adding more dynamic clauses could introduce SQL injection. The rest of this class uses list-based clause construction (e.g. search() at line 1089, count() at line 1891). Consider building clauses as a list and joining them with " AND ".


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

@kilo-code-bot

kilo-code-bot Bot commented Jul 8, 2026

Copy link
Copy Markdown

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Files Reviewed (6 files)
  • .github/workflows/ci.yml
  • hexus/store.py
  • mcp_server/server.py
  • mcp_server/tools.py
  • pyproject.toml
  • tests/test_mcp_server.py

Resolved from Previous Review

  • WARNING: tools.current_caller ContextVar propagation risk — addressed with http_transport_active flag and runtime warning in _caller_identity
  • SUGGESTION: Fragile f-string SQL building in summarize_session — refactored to list-based clause construction (" AND ".join(clauses))
Previous Review Summary (commit 9a51fa7)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit 9a51fa7)

Status: 2 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 1
Issue Details (click to expand)

WARNING

File Line Issue
mcp_server/server.py 411 tools.current_caller ContextVar may not propagate into FastMCP tool dispatch, silently degrading server-derived identity to client-supplied arg

SUGGESTION

File Line Issue
hexus/store.py 2342 Fragile f-string SQL building for scope clause — use list-based clause construction like search()/count()
Files Reviewed (7 files)
  • README.md - 0 issues
  • hexus/__init__.py - 0 issues
  • hexus/store.py - 1 issue
  • mcp_server/server.py - 1 issue
  • mcp_server/tools.py - 0 issues
  • tests/test_http_auth.py - 0 issues
  • tests/test_mcp_server.py - 0 issues

Positive Observations

  • Table identifier allowlist (_RECALL_COUNT_TABLES) + ValueError rejection correctly addresses the SQL injection vector in increment_recall_counts.
  • _escape_like + ESCAPE '\' properly neutralizes % and _ wildcards in replace()/remove() LIKE queries.
  • Identity resolution chain (_caller_identity_write_identity/_scope_identity/_read_identity) is clean and the ContextVar design isolates the transport layer from tool function signatures.
  • _bump_entry_count refactoring cleanly eliminates duplicate SQL and includes field-level guardrail validation.
  • CCRCache bypass in fetch_full for strict mode correctly prevents an id-keyed cache from leaking rows across agents.
  • Tests cover both pure-logic and DB-backed scenarios for all three isolation features.

Fix these issues in Kilo Cloud


Reviewed by deepseek-v4-pro · Input: 20.5K · Output: 3.4K · Cached: 89.5K

@codenamekt
codenamekt merged commit 8864f28 into fix/mcp-http-auth Jul 11, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant