feat(ai): anonymous AI assistant on public shares #14
Reference in New Issue
Block a user
Delete Branch "feat/public-share-assistant"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Implements
docs/public-share-assistant-plan.md(fixed scope).What
An anonymous AI assistant on public shares: an unauthenticated viewer of a published share can ask an AI that answers strictly from that share's page tree ("chat with these docs"). The authenticated agent is untouched; no DB / no migration / nothing persisted.
How
Server
settings.ai.publicShareAssistant(default off) + optionalsettings.ai.provider.publicShareChatModel(a cheap model id only — driver/baseUrl/apiKey reuse the main chat provider; empty → falls back tochatModel).getChatModel(workspaceId, override)substitutes only the model id.POST /api/shares/ai/stream(@Public, SSE viapipeUIMessageStreamToResponse). Workspace resolved from the host (DomainMiddleware) — nomain.tschange.pageIdnot in the share tree → 404; provider unconfigured → 503; rate limited → 429.forShareread-only toolset (in-process, no identity, no loopback token):searchSharePages(the existingshareId && !spaceId && !userIdFTS branch — restricted descendants excluded),getSharePage(gated bygetShareForPage+share.idcheck, content via the public sanitizer: comment marks stripped, attachments tokenized),listSharePages. No write/comment/history/cross-space/external-MCP tools.stepCountIs(5)./shares/page-infoexposes anaiAssistantflag (only after theisSharingAllowedgate).Client: an ephemeral, text-only Ask-AI widget on the public shared page (shown only when the flag is set),
useChat→/api/shares/ai/stream,credentials: 'omit'. Admin toggle + model field in Settings → AI.Reasoning / decisions
shareId/workspaceId, so a malicious transcript can't widen it.ai_chats.creator_id NOT NULL→ no migration.Review findings & fixes (adversarial security review)
The review confirmed no leakage outside the share tree (verified each tool/tenant/funnel gate with code evidence; tried shareId/pageId swaps, slugId of another share, injected messages — all rejected) and approved the jest mapper. Fixed its findings:
X-Forwarded-Forspoofing undertrustProxy, letting an attacker run up the owner's AI bill. Added a second, IP-independent per-workspace cap (300/hour, in-memory, checked after access gates / before streaming → 429) so the owner's bill is bounded even if per-IP is evaded; documented the trusted-proxy requirement. Unit-tested.listSharePagesroot title fixed; self-documenting comment that the model override is id-only.Verification
pnpm --filter server build+pnpm --filter client build— clean.pnpm --filter server test -- public-share-chat— 16 pass (funnel ordering + 404/503 uniformity, prompt lock, model fallback,forSharescoping, per-workspace limiter threshold/window).POST /api/shares/ai/stream200, streamed a reply containing the page's exact facts ("42 kelvins"/"moonstone") → grounded in the shared content; a "list all workspace pages" probe was refused (no leak); with the toggle OFF the widget is hidden and the endpoint returns 404. No app errors. Screenshot captured.🤖 Generated with Claude Code
Ghost referenced this pull request2026-06-27 20:24:56 +03:00