Release-cycle red-team found getShareForPage joins only the shares table, so it
does not exclude restricted descendants. The public share VIEW (getSharedPage)
compensates with hasRestrictedAncestor, but the assistant's getSharePage tool
and the controller funnel did not — so an anonymous caller could read a
restricted descendant's content (tool) or surface its title into the system
prompt (funnel) within an includeSubPages share.
- getSharePage: after the share-membership check and before returning content,
reject with the generic 'not part of this published share' message when
hasRestrictedAncestor(page.id) is true (page.id is the resolved UUID, so
slugId inputs work). Inject PagePermissionRepo.
- funnel: resolve the OPENED page to its UUID and treat a restricted opened page
as not-in-share (same uniform 404, fail closed if unresolvable) so its title
never reaches buildShareSystemPrompt.
search/list already exclude restricted subtrees (getPageAndDescendantsExcludingRestricted),
so these were the only two bypasses. Generic messages keep restricted
indistinguishable from not-in-share.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>