fix(mcp): refuse config and ack writes in read-only mode (#2101) - #2103
Open
mvalentsev wants to merge 1 commit into
Open
fix(mcp): refuse config and ack writes in read-only mode (#2101)#2103mvalentsev wants to merge 1 commit into
mvalentsev wants to merge 1 commit into
Conversation
) Read-only gated on _MUTATING_TOOLS, which is the palace-write set the peer-writer lease consults to decide which calls need the palace mine lock. Two tools change state without touching the palace, so they are correctly absent from that set and were served on a --read-only server anyway: mempalace_hook_settings rewrites ~/.mempalace/config.json via MempalaceConfig.set_hook_setting, and mempalace_memories_filed_away unlinks ~/.mempalace/hook_state/last_checkpoint on both branches. Add _READ_ONLY_REFUSED_TOOLS and point the dispatch gate and the tools/list filter at it. _MUTATING_TOOLS and the peer-writer path are unchanged: adding the two names there instead would put a config-only tool under the palace lease, so a server that lost the lease to a peer would answer -32001 for a call the lease has no say over. mempalace_reconnect stays reachable on purpose and the comment records why, since clearing ChromaBackend._quarantined_paths lets the reopen rename a segment directory. The two --read-only help strings and the matching row in the remote-server guide said "the mutating tools", which now names the narrower set.
mvalentsev
marked this pull request as ready for review
July 29, 2026 14:50
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this PR do?
Closes #2101.
Read-only mode gated on
_MUTATING_TOOLS, which is the palace-write set: the peer-writer lease uses it to decide which calls need the palace mine lock. Two tools change state without touching the palace, so they are rightly absent from it and were served on a--read-onlyserver anyway:mempalace_hook_settingsrewrites~/.mempalace/config.jsonviaMempalaceConfig.set_hook_setting.service.WRITE_TOOLSalready classifies it as a write, and the daemon uses that as an allowlist, so read-only was the odd one out.mempalace_memories_filed_awayunlinks~/.mempalace/hook_state/last_checkpointon both branches. Consuming that file is the tool's contract and it takes no arguments, but it still deletes state that outlives the process for a client with no write access.This adds
_READ_ONLY_REFUSED_TOOLSand points the dispatch gate and thetools/listfilter at it._MUTATING_TOOLSand the peer-writer path are untouched.#1930 fixed this class by adding the missing names to
_MUTATING_TOOLSdirectly, which worked becausecheckpointanddelete_by_sourceare palace writes. Neither tool here is, so that move would put them under the lease: with a peer holding the lock, this server would refusemempalace_hook_settingswith-32001.test_peer_writer_guard_does_not_gate_hook_settingspins that it does not.To find the rest of the class I grepped every write primitive in
mcp_server.py, mapped each to its handler, then read the handlers outside_MUTATING_TOOLSand followed them intoconfig.py,knowledge_graph.pyandbackends/. Nothing else of that shape came up.mempalace_reconnectstays reachable on purpose: it is not write-free either, since clearingChromaBackend._quarantined_pathslets the reopen rename a segment directory, but it is the only way a read-only server picks up an external writer's changes, and_SQLITE_INTEGRITY_ALLOWED_TOOLSalready keeps it reachable for recovery.Three costs, stated rather than buried:
mempalace_hook_settingswith no arguments is a read ("Call with no args to view"). A read-only server can no longer report its own hook settings, and there is no CLI equivalent.tools/listcannot advertise a tool as half-available, so hiding and refusing keeps both sides symmetric. Happy to make the gate argument-aware instead if you prefer.integrations/openclaw/SKILL.mdtells agents to callmempalace_memories_filed_awayat session start, so that now no-ops under read-only. That file describes the full 36-tool surface, which read-only exists to reduce, and every other hidden tool breaks its instructions the same way.The two
--read-onlyhelp strings and the matching row in the remote-server guide said "the mutating tools", which now names the narrower set, so they say "the tools that change state" instead.How to test
With
~/.mempalace/config.jsonholding{"hooks": {"desktop_toast": false}}:Before: prints
1, the call succeeds,desktop_toastflips totrue. After: prints0, the call returns-32003, the file is byte-identical. Same over stdio (mempalace-mcp --read-only) and withMEMPALACE_MCP_READ_ONLY=1.Five tests are new. The two named after the tools each make the same call twice, once with the gate off to show the write lands and once with it on to show the file is byte-identical, so neither survival assertion can pass vacuously.
test_read_only_refuses_exactly_the_refused_setasks the gate which tools it refuses and compares that with the set, which also catches a renamed tool leaving a dead name behind.test_read_only_refuses_every_daemon_write_toolkeeps read-only from being laxer thanservice.WRITE_TOOLS; it is the one that would have caught the original gap.Checklist
python -m pytest tests/ -v)ruff check .)