Skip to content

Demote read_context to pure retrieval; RLS user_id hardening - #475

Merged
jlunder00 merged 3 commits into
devfrom
feature/brief-1a-read-context-demotion
Jul 5, 2026
Merged

Demote read_context to pure retrieval; RLS user_id hardening#475
jlunder00 merged 3 commits into
devfrom
feature/brief-1a-read-context-demotion

Conversation

@jlunder00

Copy link
Copy Markdown
Owner

Summary

  • Matched pair with tether-premium PR (feature/brief-1a-cascade-traverse-depth) — merge together.
  • Demotes read_context from a scope-enforcing tool to pure retrieval, per the conversational-core design review (§5.1/§5.2): PermissionGate (interactive_agent_layer/permissions.py) is now the sole scope enforcer. This is safe to land ahead of brief 1b because the gate already enforces read_context scope today via can_use_tool/_check_read_context_scope (shipped in scope-wiring) — demoting the tool doesn't open an enforcement gap. Brief 1b will refine the gate (graded 2D envelope); this PR only removes the tool's now-redundant/inconsistent second enforcement layer.
  • Deletes the conversation_id_required error dict — conversation_id is fully optional now (credit-logs when present, silent no-log when absent).
  • Deletes the out_of_scope refusal machinery (get_node_tree_distance calls + error dicts) from execute_read_context and _add_children.
  • Renames Ntraverse_depth throughout (tool signature, internals, tests) — it's a cascade cost bound, not an authorization radius.
  • write_node_memory's read-before-write enforcement is unchanged (still requires conversation_id + a prior logged read).
  • Completes the RLS-hardening addendum (folded in from phase-0e): optional user_id explicit-bind param threaded through db/pg_queries/node_memory.py, db/pg_queries/nodes.py (including a get_node gap caught by code review — it previously had no such param at all, unlike get_node_by_path), and the read_context/read_node_memory/write_node_memory MCP tool call sites in server.py.

Test coverage notes

  • Real (non-mocked) regression test: execute_read_context(conversation_id=None) returns real root nodes (the "no mocks" lesson from a prior incident).
  • RLS-hardening acceptance tests in tests/db/test_pg_nodes.py and tests/tether_mcp/test_read_context_cascade.py verify the explicit user_id bind is a real filter (mismatched user_id never resolves another user's node). Sandbox caveat, noted in the test docstrings: the local dev Postgres role connects BYPASSRLS (same root cause as the pre-existing, out-of-scope failure of tests/db/test_rls.py::test_app_db_role_is_not_superuser), so "RLS denies without an explicit bind" can't be exercised as a deny path in this environment — only the explicit-bind filter logic itself, which is the part this hardening actually adds. This is a sandbox/CI-environment limitation, not a coverage gap in the change.

Test plan

  • tests/mcp/test_read_context_enforcement.py, tests/tether_mcp/test_read_context_cascade.py, tests/mcp/test_write_node_memory_enforcement.py, tests/db/test_pg_nodes.py all green
  • Full suite run: 1303 passed, 46 failed (identical pre-existing failures confirmed via git stash diff against this branch's base — zero regressions), 228 skipped
  • pr-review-toolkit:code-reviewer run on the diff; both findings addressed in a follow-up commit

jlunder00 added 3 commits July 4, 2026 19:05
…de_by_path)

Adds optional user_id param to log_node_read, has_read_node_in_conversation,
get_conversation_reads (db/pg_queries/node_memory.py) and get_node_by_path
(db/pg_queries/nodes.py). When provided, binds user_id as an explicit
WHERE/VALUES filter instead of relying solely on the app.current_user_id
session GUC -- defense-in-depth, correct even on an unscoped connection.
When None (default), behavior unchanged (RLS-only), backward compatible.

REMAINING (not done in this commit):
- Bind user_id=get_user_id() at call sites in tether_mcp/tools/read_context.py,
  read_node_memory.py, write_node_memory.py, server.py.
- Main BRIEF 1a change: conversation_id optional in execute_read_context,
  delete out_of_scope error dicts + get_node_tree_distance calls in
  execute_read_context (~243-268) and _add_children (~163-173), rename
  N -> traverse_depth.
- server.py tool docstring rewrite (no authorization claims).
- Test updates: tests/mcp/test_read_context_enforcement.py needs a full
  rewrite (its whole premise -- conversation_id required, out_of_scope
  errors -- is being removed). tests/tether_mcp/test_read_context_cascade.py
  needs get_node_tree_distance mocks removed + N->traverse_depth rename +
  delete test_conversation_id_required.
- REQUIRED regression test: real (non-mocked) execute_read_context with
  conversation_id=None returning real root nodes -- not yet written.
- RLS hardening acceptance test (mismatched user_id returns nothing on
  unscoped connection) -- not yet written.

See cc-context-store/tether/docs/handoffs/neon-audit-premium-handoff-2026-07-04.md
for full status and matched-pair contract with the premium side.
…sites

read_context is no longer a scope enforcer. PermissionGate
(interactive_agent_layer/permissions.py) is the sole enforcer per the
conversational-core design review (5.1/5.2); by the time a read_context
call reaches this module the gate has already judged whether it should
happen. This PR:

- Deletes the conversation_id_required error dict — conversation_id is
  now fully optional. Absent conversation_id means no read-credit
  logging, not a refusal.
- Deletes the out_of_scope refusal machinery (get_node_tree_distance
  calls + error dicts) from execute_read_context and _add_children.
  Cascade descent is still cost-bounded, just never produces an error.
- Renames N -> traverse_depth throughout (tool signature, internals,
  tests) to make clear it's a cost bound, not an authorization radius.
- server.py docstring rewritten to drop all authorization claims for
  read_context; M is a detail request the gate judges, not enforced
  here.
- write_node_memory's read-before-write enforcement is UNCHANGED (still
  requires conversation_id + a prior logged read).

RLS hardening (0e addendum, folded into this PR per the phase-1 plan):
- Completes the optional user_id bind at the read_context /
  read_node_memory / write_node_memory tool call sites (server.py),
  wiring get_user_id() into the already-hardened node_memory.py /
  nodes.py query functions from the prior WIP commit.
- Adds a regression test using the real (non-mocked) execute_read_context
  with conversation_id=None returning real root nodes, plus an RLS
  acceptance test proving a mismatched explicit user_id returns nothing
  even on an unscoped connection.

Matched pair with tether-premium feature/brief-1a-cascade-traverse-depth
(prompt_assembler.load_context_cascade kwarg N= -> traverse_depth=).
Code review of the read_context demotion commit caught an inconsistency:
get_node_by_path was hardened with an optional user_id explicit-bind
param, but get_node (used by read_context's node_ids branch and the
children section_types/children_count backfill in _build_node_response)
was not touched at all — leaving id-based lookups one RLS layer weaker
than path-based ones within the same tool. This adds the same optional
user_id param to get_node and wires it through read_context.py's two
call sites plus read_node_memory.py's node lookup.

Also corrects two RLS-hardening test docstrings that overstated what
they verify: the local dev Postgres role connects BYPASSRLS (same root
cause as the pre-existing tests/db/test_rls.py::
test_app_db_role_is_not_superuser failure), so "RLS denies without an
explicit bind" cannot be exercised in this sandbox. What the new tests
(tests/db/test_pg_nodes.py) and the corrected existing test actually
verify — meaningfully, independent of RLS/role privileges — is that the
explicit user_id bind is a real filter: a mismatched user_id never
resolves another user's node, and a matching one does.
@jlunder00
jlunder00 force-pushed the feature/brief-1a-read-context-demotion branch from e38abfc to b18062b Compare July 5, 2026 02:11
@jlunder00
jlunder00 merged commit 2dcb8e3 into dev Jul 5, 2026
7 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