Skip to content

fix: harden MCP HTTP transport — bearer auth, loopback default, cleanup confirm gate (#13) - #28

Merged
codenamekt merged 9 commits into
mainfrom
fix/mcp-http-auth
Jul 11, 2026
Merged

codenamekt merged 9 commits into
mainfrom
fix/mcp-http-auth

Conversation

@codenamekt

Copy link
Copy Markdown
Owner

Addresses the highest-impact parts of #13: the open network surface and the fleet-wide delete path.

Problem

  • The HTTP transport had no authentication and bound to 0.0.0.0 by default (server.py, cli.py), so any client that could reach the port could call every tool — read all agents' memory, write poisoned entries, delete data, and scrape /metrics.
  • memory_cleanup was an unconfirmed, unscoped, fleet-wide DELETE any client (or a confused LLM) could trigger.

Changes

1. Bearer-token auth on the HTTP transport

HEXUS_API_TOKEN, when set, requires Authorization: Bearer <token> on every HTTP request (MCP calls and /metrics), compared with hmac.compare_digest. Implemented as a thin ASGI wrapper around the streamable-http app so it also covers /metrics and — importantly — only gates http scopes, letting lifespan/websocket pass through so the session manager still starts. When the token is unset, a prominent WARNING is logged.

2. Secure-by-default bind address

CLI --host (and the FastMCP instance) now default to 127.0.0.1 instead of 0.0.0.0. Docker is unaffecteddocker/entrypoint.sh passes --host 0.0.0.0 explicitly. Serving on a non-loopback host without a token logs a loud warning. (Behavior change for direct hexus-mcp serve users who relied on the old 0.0.0.0 default — now opt-in.)

3. memory_cleanup confirm gate

Defaults to a dry run: without confirm=true it returns the counts it would delete (new cleanup_stale_records(dry_run=True) runs SELECT count(*) instead of DELETE) and deletes nothing. Mirrors memory_forget's dry-run-by-default. The scheduled background cleanup is unchanged (it calls the store method directly, not the tool).

Testing

tests/test_http_auth.py covers the wrapper: 401 with no/wrong token (downstream never reached), pass-through with the correct token, WWW-Authenticate on 401, and lifespan pass-through. I validated the exact wrapper logic with a standalone async harness (5/5 pass); the suite runs under the docker test profile in CI. Ruff format + lint clean.

Explicitly out of scope (larger follow-ups, still tracked in #13 / #19)

These are noted so reviewers know this PR closes the network-surface hole and the fleet-wipe path without claiming to make identity tamper-proof.

🤖 Generated with Claude Code

Toby and others added 2 commits July 6, 2026 21:47
…up gate (#13)

The HTTP transport had no authentication and bound to 0.0.0.0 by default,
so any network-adjacent client could read/write/delete every agent's memory
and scrape /metrics. `memory_cleanup` was also an unconfirmed, fleet-wide
DELETE any client could trigger.

- HEXUS_API_TOKEN: when set, an ASGI wrapper requires
  `Authorization: Bearer <token>` on every HTTP request (MCP calls + /metrics),
  constant-time compared. lifespan/websocket scopes pass through untouched so
  the streamable-http session manager still starts. When unset, a prominent
  WARNING is logged at startup.
- Secure default: CLI `--host` and the FastMCP instance now default to
  127.0.0.1 instead of 0.0.0.0. Docker is unaffected (the entrypoint passes
  `--host 0.0.0.0` explicitly). Serving on a non-loopback host without a token
  logs a loud warning.
- memory_cleanup now defaults to a dry run: without `confirm=true` it reports
  the counts it WOULD delete (via new cleanup_stale_records(dry_run=True)) and
  deletes nothing. The scheduled background cleanup is unchanged (it calls the
  store method directly).

Adds tests/test_http_auth.py covering the wrapper (401 without/with wrong
token, pass-through with the right one, www-authenticate header, and lifespan
pass-through).

Note: agent identity is still client-asserted, and by-id reads/mutations
(fetch_full/confirm/reject) remain unscoped — binding identity to the
authenticated credential and agent-scoping those ops are larger,
behavior-changing follow-ups tracked in #13 and #19.

Refs #13

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread hexus/store.py
limit_date = datetime.now(timezone.utc) - timedelta(days=ttl)
if dry_run:
# table/ts_col are internal literals (not caller-supplied).
cur.execute(

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: f-string SQL interpolation of table/column names, currently safe but fragile.

table and ts_col are interpolated directly into SQL via f-strings on lines 1778 and 1784. While currently safe because the values come from a hardcoded targets list defined at L1764-1768, this pattern bypasses psycopg2's parameterized query protection. If the targets list is ever extended to accept any form of external input (e.g., config-driven table names), this becomes a SQL injection vector.

Consider adding a whitelist guard (e.g., assert table in {"conversations", "memory_entries", "delegations"}) before the cur.execute call to make the invariant explicit and auditable.


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

@kilo-code-bot

kilo-code-bot Bot commented Jul 7, 2026

Copy link
Copy Markdown

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Files Reviewed (1 file)
  • hexus/store.py
Previous Review Summaries (2 snapshots, latest commit 1a8484a)

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

Previous review (commit 1a8484a)

Status: No Issues Found | Recommendation: Merge

Files Reviewed (6 files)
  • .github/workflows/ci.yml
  • hexus/ccr/__init__.py
  • hexus/pipeline/__init__.py
  • hexus/store.py
  • hexus/webhook/__init__.py
  • pyproject.toml

Previous review (commit 30f38c3)

Status: 1 Issue Found | Recommendation: Address before merge

Overview

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

SUGGESTION

File Line Issue
hexus/store.py 1777 f-string SQL interpolation of table/column names — currently safe (hardcoded targets) but fragile without an assertion guard
Files Reviewed (4 files)
  • hexus/store.py - 1 issue
  • mcp_server/cli.py - 0 issues
  • mcp_server/server.py - 0 issues
  • tests/test_http_auth.py - 0 issues

Fix these issues in Kilo Cloud


Reviewed by deepseek-v4-pro · Input: 18.1K · Output: 4K · Cached: 120.1K

codenamekt and others added 2 commits July 6, 2026 22:03
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>
@codenamekt
codenamekt merged commit 223c19c into main Jul 11, 2026
8 checks 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