Skip to content

feat(acl): close resolve() + neighbours() read-gaps#172

Merged
jrosskopf merged 2 commits into
mainfrom
feat/acl-read-gaps
Jun 15, 2026
Merged

feat(acl): close resolve() + neighbours() read-gaps#172
jrosskopf merged 2 commits into
mainfrom
feat/acl-read-gaps

Conversation

@jrosskopf

@jrosskopf jrosskopf commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Point 3 — read-gap ACLs. Two read tools returned references to owner-private instances the caller can't read, leaking their existence:

  • resolve returned the page ref (existence + page_id + skill) for any instance → now resolves an unreadable owner-private instance to not-found (page: null, exists: false).
  • neighbours returned edges to/from private instances → now drops edges whose other endpoint is an owner-private instance the caller can't read.

Both are always-on, mirroring the existing expand/list_instances/search read filter; admin + public unaffected.

Tests (real HTTP MCP): resolve_hides_owner_private_instance_from_non_owner, neighbours_filters_edges_to_owner_private_pages. Read-ACL regression green; clippy clean.

Remaining (lower severity, tracked): run_stored_query SQL row-filtering (hardest; stored queries are read-only meta-skill SELECTs); assign_event admin-gate (deferred pending a caller audit so it can't break a member write path); capture_event left open by design (members legitimately capture their own erasure/opt-in events).

🤖 Generated with Claude Code

resolve() returned a page ref (existence + page_id + skill) for ANY instance,
including owner-private ones the caller cannot read — leaking the existence/id
of another member's private records. Mirror the read filter (always-on, like
expand/list_instances/search): when resolve hits an owner-private instance the
caller can't read, return it as not-found (page: null, exists: false) — exactly
as expand returns a null page. Admin + public unaffected.

Test (real HTTP MCP): instance_acl.rs::resolve_hides_owner_private_instance_from_non_owner
(owner resolves own; non-owner gets absent + no page_id; admin + public resolve).

Remaining read-gaps (lower severity, tracked): neighbours (filter edges to
owner-private pages), run_stored_query (SQL row-filtering), assign_event
(admin-gate).
neighbours() returned graph edges to/from a page regardless of whether the
OTHER endpoint is an owner-private instance the caller can't read — leaking the
existence of links to private records. Filter (always-on): drop an edge whose
neighbour endpoint is an owner-private instance the caller can't read. Test:
neighbours_filters_edges_to_owner_private_pages (alice sees the edge from her
own event_profile; bob sees none).

resolve + neighbours now mirror the expand/list/search read ACL. Remaining
(lower severity): run_stored_query SQL row-filtering; assign_event admin-gate
(deferred pending a caller audit so it doesn't break a member write path).
@jrosskopf jrosskopf changed the title feat(acl): close the resolve() read-gap feat(acl): close resolve() + neighbours() read-gaps Jun 15, 2026
@jrosskopf jrosskopf merged commit eea02eb into main Jun 15, 2026
1 check passed
@jrosskopf jrosskopf deleted the feat/acl-read-gaps branch June 15, 2026 08:24
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