Skip to content

fix: validate user-supplied namespace strings on mem_ns_* CRUD tools (#500)#501

Merged
memtomem merged 1 commit intomainfrom
fix/issue-500-validate-mem-ns-tools
Apr 26, 2026
Merged

fix: validate user-supplied namespace strings on mem_ns_* CRUD tools (#500)#501
memtomem merged 1 commit intomainfrom
fix/issue-500-validate-mem-ns-tools

Conversation

@memtomem
Copy link
Copy Markdown
Owner

Summary

Closes #500 — the kin gap PR #499 left on the namespace CRUD surface. After #496 / #499 gated every caller-supplied namespace= / target= override on session-start and mem_agent_share, the mem_ns_* tools still wrote user-supplied strings into either app state or the storage row with only an if not strip() empty-check.

The transitive bypass this closes

mem_ns_set(namespace="agent-runtime:foo:bar")    # accepted today
mem_session_start(agent_id="default")            # falls through to step 3
# -> effective_ns = app.current_namespace = "agent-runtime:foo:bar"
# -> sessions row lands with the same shape #496 closed elsewhere

mem_session_start's priority chain (server/tools/session.py:101-112) treats app.current_namespace as the step-3 fallback when agent_id == "default". With mem_ns_set ungated, an attacker who controls the value reaching it could land a malformed current_namespace and have it round-trip into the next session row through the fallback. Same user-visible outcome as the namespace= override #499 closes — different door.

What's gated

validate_namespace (the strict gate from #499) is now applied at every public mem_ns_* surface that accepts a user-supplied namespace string, before storage / state is touched:

  • mem_ns_set(namespace=) — closes the transitive bypass.
  • mem_ns_delete(namespace=) — read-shaped today, pulled under the gate for consistency.
  • mem_ns_rename(old=, new=) — both arms gated.
  • mem_ns_assign(namespace=, old_namespace=) — both arms gated.
  • mem_ns_update(namespace=) — lookup key gated.

The pre-existing .strip() calls on caller input go away — the validator's contract is forward-shield-on-input, and silent stripping of whitespace-padded values is exactly the kind of "sanitise and accept" behaviour the namespace gate is closing across surfaces.

Out of scope (matches #496 / #499)

  • No data migration. Forward gate only; any malformed namespace rows already in storage are left as-is.
  • No CLI mirror needed. There is no mm ns command surface today (CLI exposes mm agent register/share/list/migrate only, not the ns CRUD tools), so the issue's "CLI mirror" clause is vacuous for this set. If mm ns lands later, it inherits the gate via the existing pattern in mm session start --namespace / mm agent register.

Test plan

  • New tests/test_validate_namespace_ns_tools.py — per-tool hostile rejection parametrised over the existing HOSTILE_NAMESPACES suite, plus the end-to-end transitive-bypass regression: mem_ns_set("agent-runtime:foo:bar")mem_session_start(agent_id="default") cannot land an agent-runtime:foo:bar session row.
  • Existing tests/test_validate_namespace.py re-runs cleanly (no overlap; the ns-tool boundary was previously uncovered).
  • uv run ruff check packages/memtomem/src && uv run ruff format --check packages/memtomem/src packages/memtomem/tests — clean.
  • uv run pytest -m "not ollama" — 2850 passed, 46 deselected.

🤖 Generated with Claude Code

…500)

Closes the kin gap PR #499 left on the namespace CRUD surface: even
after #496 / #499 gated every caller-supplied ``namespace=`` /
``target=`` override on session-start and mem_agent_share, the
``mem_ns_*`` tools still wrote user-supplied strings into either app
state or the storage row with only an ``if not strip()`` empty-check.

The transitive bypass this closes:

    mem_ns_set(namespace="agent-runtime:foo:bar")     # accepted today
    mem_session_start(agent_id="default")             # falls through to
                                                      # current_namespace
    -> sessions row lands with the same shape #496 closed elsewhere

``mem_session_start``'s priority chain treats ``app.current_namespace``
as the step-3 fallback when ``agent_id == "default"``. With ns_set
ungated, an attacker who controls the value reaching it could land a
malformed ``current_namespace`` and have it round-trip into the next
session row through the fallback. Same user-visible outcome as the
``namespace=`` override #499 closes — different door.

This applies ``validate_namespace`` (the strict gate from #499) to
every public ``mem_ns_*`` surface that takes a user-supplied namespace
string before storage / state is touched:

* mem_ns_set(namespace=)              — closes the transitive bypass.
* mem_ns_delete(namespace=)           — read-shaped today but pulled
                                         under the gate for consistency.
* mem_ns_rename(old=, new=)           — both arms gated.
* mem_ns_assign(namespace=,
                old_namespace=)       — both arms gated.
* mem_ns_update(namespace=)           — lookup key gated.

The ``.strip()`` calls the prior code applied silently to caller input
go away — the validator's contract is forward-shield-on-input, and
silent stripping of whitespace-padded values is exactly the kind of
"sanitise and accept" behaviour the namespace gate is closing across
surfaces. Existing accepted shapes (``default``, ``shared``,
``archive:summary``, ``claude-memory:project-x``,
``agent-runtime:planner``, etc.) round-trip unchanged — pinned by the
ACCEPTED_NAMESPACES suite in test_validate_namespace.py.

CLI mirror: there is no ``mm ns`` command surface today (the CLI
exposes ``mm agent register/share/list/migrate`` only, not the ns CRUD
tools), so the issue's "CLI mirror" clause is vacuous for this set. If
a ``mm ns`` surface lands later, it must inherit the gate via the
existing pattern in ``mm session start --namespace`` /
``mm agent register``.

Out of scope: migrating any malformed namespace rows already in
storage. Same forward-only contract as #496 / #499 — this is a write
shield, not a corrective sweep.

Tests:

* test_validate_namespace_ns_tools.py — per-tool hostile rejection
  parametrised over the existing HOSTILE_NAMESPACES suite, plus the
  end-to-end transitive-bypass regression: ns_set ->
  session_start(agent_id="default") cannot land an
  ``agent-runtime:foo:bar`` session row.
* Re-runs cleanly under the existing ``test_validate_namespace.py``
  suite (no overlap; the ns-tool boundary was previously uncovered).
* Full ``pytest -m "not ollama"`` green (2850 passed).

Co-Authored-By: Claude <[email protected]>
@memtomem memtomem merged commit cb78142 into main Apr 26, 2026
7 checks passed
@memtomem memtomem deleted the fix/issue-500-validate-mem-ns-tools branch April 26, 2026 09:04
@github-actions github-actions Bot locked and limited conversation to collaborators Apr 26, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Validate user-supplied namespace strings on mem_ns_* CRUD tools (transitive bypass to session rows)

2 participants