Skip to content

Anonymous chat over the public corpus, off by default (#94) - #95

Merged
gangtao merged 1 commit into
mainfrom
feat/anonymous-chat
Sep 23, 2026
Merged

gangtao merged 1 commit into
mainfrom
feat/anonymous-chat

Conversation

@gangtao

@gangtao gangtao commented Sep 23, 2026

Copy link
Copy Markdown
Contributor

Closes #94 (v1 as decided there: chat only, global daily cap, no explore/MCP).

What

  • A reserved anonymous principal, served for a request with no credentials when TPK_ANONYMOUS_ACCESS is on. It holds exactly one capability, chat. Everything else — Explorer, source fragments and thinking trace, MCP, API tokens, corpus and user management — stays 401/403.
  • Anonymous can only access the public corpus. Its scope is the enabled entries with visibility = public, resolved live on every turn and enforced through the same ROLE_SCOPE mechanism a scoped role uses — so all six graph tools, and chat, inherit it with no new filtering code. Enforcement is per entry; a node's stored visibility is never consulted. The agent's system prompt lists only those entries (prompt building now follows ROLE_SCOPE too), so it never claims coverage of internal repos.
  • One global daily token budget for all anonymous traffic — TPK_ANONYMOUS_DAILY_TOKEN_LIMIT / [server].anonymous_daily_token_limit, default 100 000, 0 closes anonymous chat — metered and audited under the anonymous username. When spent: 429 with "sign in for your own budget".
  • A wrong or expired token is still 401. Only the absence of an Authorization header maps to anonymous; a bad credential never degrades to public access. anonymous is reserved as a username and role name, and can never log in.
  • Web: "Continue without signing in" on the login page (offered when /auth/me without a token answers with the anonymous identity); the shell shows only Chat, a "Browsing the public docs" badge and a Sign in button; the budget label reads "shared daily tokens (anonymous)"; the add-entry form warns when public is chosen (visibility cannot be changed after creation — noted as a follow-up).
  • Parser fix: every node now carries its entry's visibility. Doc-kind nodes inside internal entries were stamped public by a rule that predates any enforcement (1,363 in proton-enterprise@v3.3.1). Takes effect on re-ingest; not load-bearing, since access is decided per entry.

Off by default

With the flag off, behaviour is identical to today — every existing 401 test still passes, and /auth/me without a token is still 401.

Testing

  • tests/test_anonymous.py (real DB): principal capabilities; public_scope = enabled public entries only (not internal, not disabled), fails closed; gate off → all 401; gate on → anonymous for chat/usage/model only, every other endpoint closed; bad/Basic/empty tokens still 401; a real login still wins; the anonymous name is rejected for users and roles and cannot log in.
  • tests/test_server.py: an anonymous turn runs with ROLE_SCOPE = the public scope (and empty when there are no public entries — closed, never unrestricted); the global budget applies (not the per-user default); usage and audit rows say anonymous; no thinking/source events; /chat/usage reports the shared budget; limit 0 closes chat.
  • tests/test_agent.py: the prompt's corpus list follows ROLE_SCOPE.
  • tests/test_graphify_runner.py: nodes carry the entry's visibility.
  • pytest: 360 passed, 10 skipped, 1 failed — tests/test_ingest.py::test_ingest_repo_passes_extraction_and_backend, environment-dependent, failing on main too. Web build + sanitize clean.
  • Live, real agent (Opus 4.8) against a throwaway database with one public entry (a Kafka docs node) and one internal entry (KafkaSource::read), anonymous access on: the login button, badge and shared-budget label rendered; asked about both in one question, the anonymous agent answered the public doc and said of the internal one: "The graph I have access to covers only the public documentation repository (docs@main) — it does not include the Timeplus C++/engine source code." No thinking trace, no sources. The next turn hit the budget: "The shared daily budget for anonymous use is spent (20635/20000). Sign in for your own budget…". Sign in returned to the login page. Database dropped afterwards.

One bug found and fixed during that check: apiFetch treated any 401 as session loss, so the first non-chat 401 an anonymous session hit (the sidebar's /api/repos corpus tags) bounced it back to login. It now does so only when a token was actually sent.

Not in scope (as decided on #94)

Anonymous MCP and Explorer; a per-IP rate limit (the shared cap is the budgeting mechanism in v1); changing an entry's visibility after creation.

Rollout

Off by default, no migration, no re-ingest needed to turn it on (the parser fix only cleans labels). To enable on production: TPK_ANONYMOUS_ACCESS=1 (+ a budget) in the app env. Check first that every entry marked public really should be visible to the world.

🤖 Generated with Claude Code

A request with NO credentials becomes the reserved 'anonymous' principal when
TPK_ANONYMOUS_ACCESS is on: chat only (no explore, no source:view, no MCP, no
management); corpus scope = the ENABLED entries with visibility=public, resolved
live per turn and enforced through ROLE_SCOPE like a role's -- per entry, never
per node; the agent's prompt lists only those entries. One global daily token
budget (TPK_ANONYMOUS_DAILY_TOKEN_LIMIT, default 100k, 0 = closed) shared by all
anonymous traffic, metered and audited under the 'anonymous' username; when
spent, 429 with a sign-in hint. A wrong/expired token is still 401 -- never
downgraded to anonymous. 'anonymous' is reserved as a username and role name.

Web: 'Continue without signing in' on the login page (shown when /auth/me
answers anonymous), a 'Browsing the public docs' badge + Sign in button in the
shell, a shared-budget label, and a warning in the add-entry form when
visibility=public is chosen. apiFetch treats a 401 as session loss only when a
token was actually sent, so an anonymous session isn't bounced to login by the
first non-chat 401.

Parser: every node now carries its ENTRY's visibility; doc-kind nodes inside
internal entries were stamped 'public' by a rule that predates enforcement
(1,363 of them in proton-enterprise). Takes effect on re-ingest; not load-bearing
because access is decided per entry.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@gangtao
gangtao merged commit 950d69e into main Sep 23, 2026
2 checks passed
gangtao added a commit that referenced this pull request Sep 23, 2026
…he app container (#97)

docker-compose.yml lists the env vars it forwards to the app explicitly, so
TPK_ANONYMOUS_ACCESS / TPK_ANONYMOUS_DAILY_TOKEN_LIMIT (#95) and
TPK_MCP_HTTP_ENABLED / TPK_MCP_ALLOWED_HOSTS (#74) set in .env never reached the
container -- the login page could not offer anonymous access on a compose stack.
Both compose files now forward them; the k8s README lists them.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
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.

Anonymous chat over the public corpus (chat-only, global daily cap; no explore/MCP in v1)

1 participant