Add tag-based document indexing allowlist - #277
Open
MichelleAntunes wants to merge 2 commits into
Open
Conversation
added 2 commits
September 9, 2026 07:23
Introduces an 'AI knowledge' system tag that controls whether documents are indexed by Context Chat. Admins can toggle between two modes on the settings page: - Index all documents (default, existing behavior) - Index only documents tagged 'AI knowledge' (tag is inherited from parent folders) Switching modes automatically triggers a full reindex or a cleanup of untagged documents from the index, and the admin page reflects live indexing status via a new IndexCompletionService shared between the normal indexing flow and the tag-cleanup flow. - lib/Service/FsEventService.php: hasAiKnowledgeTag() checks the tag on a node and its ancestors - lib/Controller/ConfigController.php: triggers the appropriate background job when index_mode changes, and clears the app config cache after saving - lib/BackgroundJobs/UntaggedCleanupSchedulerJob.php + UntaggedCleanupCrawlJob.php: remove untagged documents from the index, mirroring SchedulerJob/StorageCrawlJob's shape - lib/Command/CleanupUntagged.php: occ command to trigger cleanup manually - lib/Service/IndexCompletionService.php: extracted from QueueController; live-state completion check (crawl jobs + queue + pending actions) instead of a single global last_enqueued_db_id, which was unreliable across concurrent per-storage crawls - src/components/ViewAdmin.vue: admin UI for the mode toggle Signed-off-by: MichelleAntunes <miichelleantunes@outlook.com>
- lib/Controller/ConfigController.php: extract the index_mode-specific job-scheduling logic out of setAdminConfig() into a private handleIndexModeChange() method, so the generic config-saving method stays free of feature-specific logic - package.json: use a caret range (^7.3.0) for @nextcloud/dialogs, matching the convention used by every other dependency in this file, instead of an exact pin. Note: versions above 7.3.0 change the toast's default CSS position (bottom-left instead of top-right, matching integration_openai's behavior) - this is a pre-existing upstream inconsistency, not something introduced by this PR, and is left unresolved by choice Signed-off-by: MichelleAntunes <miichelleantunes@outlook.com>
MichelleAntunes
requested review from
julien-nc and
marcelklehr
as code owners
September 9, 2026 05:44
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #278.
Why this matters
Context Chat currently indexes every document matching its mimetype whitelist, with no way to exclude anything. For organizations handling sensitive or confidential material — legal, healthcare, HR, compliance-regulated industries — there's currently no way to control which content gets sent to an LLM for embedding. Everything eligible is indexed by default, which is the wrong default for these environments: it means adopting Context Chat requires either accepting that every eligible document becomes part of the AI's knowledge base, or not adopting it at all.
This PR takes an allow-list approach rather than a block-list one, on purpose: with an allow-list, nothing is exposed to the AI unless someone explicitly marks it as safe to include. A block-list ("index everything except X") is easy to get wrong by omission — someone forgets to exclude a folder, and it's silently indexed. An allow-list fails safe: if you forget to tag something, it simply isn't indexed, which is the safer failure mode for sensitive content.
This also has to work for instances that are already running Context Chat today, with documents already indexed under the existing "index everything" behavior. The feature is opt-in: existing instances keep indexing everything by default, and only change behavior if an admin deliberately switches modes. Nothing changes for anyone who doesn't touch the new setting.
How it works
Admins get a new toggle on the Context Chat admin settings page:
The tag is a regular Nextcloud system tag, so it uses infrastructure that already exists and that admins/users already understand, rather than introducing a new concept. Tagging a folder covers everything inside it (tag inheritance walks up the file's ancestor folders), so an admin can mark, say, a single top-level "AI knowledge" folder and everything moved into it becomes eligible, without needing to tag every individual file.
Switching modes has real consequences that need to happen automatically, not just at settings-save time:
Both of these are background jobs, not something the admin has to wait on synchronously — the admin page's indexing status (already existing in Context Chat) reflects live progress for both flows, so the admin can see when the switch has actually finished taking effect, not just when the setting was saved.
Design decisions worth calling out
Why extend the existing event-listener pattern instead of a new mechanism: Nextcloud already has a system tag mechanism and Context Chat already listens for filesystem events (
FileListener.php) to decide what to index. This PR follows that same shape, listening forTagAssignedEvent/TagUnassignedEventin addition to the existing file events, rather than introducing a parallel, separate indexing pipeline just for tags.Why the completion-check logic was extracted and rewritten, not just extended: The existing "has initial indexing completed?" check relied on a single global
last_enqueued_db_idmarker. On instances with multiple users (each with their own storage), that marker gets overwritten by whichever user's crawl job runs last — so a small storage finishing first could make the check falsely report "complete" while a larger storage still had thousands of files queued. This is a pre-existing issue that became directly relevant here because the new tag-cleanup job needed the same completion check, and duplicating the flawed logic would have meant two places to fix later instead of one. The rewritten check (IndexCompletionService) verifies live state instead — are there still crawl jobs of either kind scheduled, are there still files in the queue, are there still pending queue actions — rather than trusting a single stored marker that can go stale.Why the config cache is cleared explicitly after saving: Nextcloud's app config layer is cached (APCu) and shared across PHP processes, including the long-running background worker process. Without clearing the cache after saving
index_mode, the worker could keep operating on the old value for some time after the admin toggled the setting, even though the database already had the new value.Changes
lib/Service/FsEventService.php:hasAiKnowledgeTag()checks the "AI knowledge" tag on a node and walks up its ancestors for folder-level inheritance; returnstrueunconditionally when the app is in "index all" mode, so tag-checking has zero effect on existing installs that don't opt inlib/Controller/ConfigController.php:handleIndexModeChange()detects an actual mode change (not just any settings save) and schedules the appropriate background job; also clears the app config cache after savinglib/BackgroundJobs/UntaggedCleanupSchedulerJob.php+UntaggedCleanupCrawlJob.php: discover all mounts and remove untagged documents from the index in batches, mirroring the existingSchedulerJob/StorageCrawlJobshape rather than introducing a different batching patternlib/Command/CleanupUntagged.php:occ context_chat:cleanup-untaggedcommand to trigger the same cleanup manually, useful for admins scripting deployment or recovering from an interrupted runlib/Service/IndexCompletionService.php: extracted fromQueueControllerfor the reasons above, and used by both the normal indexing flow and the new tag-cleanup flowsrc/components/ViewAdmin.vue: admin UI for the mode toggle; the UI update is optimistic (switches immediately for responsiveness) and reverts to the previous value if the save request fails, so the displayed state never lies about what's actually savedTesting
Tested end-to-end on a real multi-user Nextcloud instance: tagged specific folders and individual files, confirmed tagged documents get indexed and untagged ones get removed, confirmed folder-tag inheritance works correctly for files inside a tagged folder, confirmed switching modes back and forth triggers the correct job each time, confirmed the admin page's status accurately reflects "running" vs "finished" throughout both flows (including across multiple users' storages, which is what surfaced the completion-check issue described above), and confirmed a real Context Chat query only returns tagged sources when tag-only mode is active.
🤖 AI (if applicable)