ingest: exclude the wiki from discovery, add a size ceiling, fix the UNC subprocess cwd - #131
Merged
Merged
Conversation
Three field-reported problems with a raw root on a Windows network share,
all in the discovery/child-process seam:
1. The wiki could become one of its own sources. With a raw root ABOVE the
wiki (CITADEL_RAW_DIRS=T:\ and the wiki at T:\llmWiki\ds\wiki), discovery
walked the generated pages back in run after run; only a hand-written
CITADEL_IGNORE_PATTERNS entry stopped it. Discovery now prunes the wiki
dir from every walk (lexically — no resolve(), no syscall per entry),
refuses a raw root that IS the wiki without arming the deletion sweep,
refuses an explicit path inside the wiki (including a git-backed wiki that
would otherwise digest as a repo source), announces the exclusion once per
run, and sweeps already-self-ingested keys out of the manifest + failures
catalog at run start.
2. No size-based discovery filter. Ignore patterns match names; nothing
matched size, so a folder of multi-GB sensor dumps was stream-hashed in
full just to end up recorded as unreadable binary. CITADEL_MAX_SOURCE_BYTES
skips those from the walk's own stat — never opened, never hashed, never
tracked — while reporting them (stderr NOTE, an Oversized report section,
an Oversized status bucket). Off by default; explicit paths bypass it.
3. Agent sessions ran with a UNC working directory. Path.resolve() rewrites a
mapped drive (T:\team-wiki) into \\server\share\team-wiki whatever the .env
said, and that was passed as the CLI's cwd — where copilot refuses to run
("environment blocks UNC/network paths") and git reads a different repo.
The resolved form stays the one identity; child processes (agent cwd +
directory grants, git -C) now get the non-resolved spelling citadel already
held. An alias is recorded only when both spellings of one directory are
known and resolution produced a UNC path, so POSIX and ordinary Windows
paths are unchanged.
Plus two doctor checks (wiki placement, child paths), docs, and tests.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U2dxmXzQpQmeYospgrjSi3
The note claimed "lies under a configured raw root", which is only true for the walk case — an explicitly named path inside the wiki is refused whether or not a raw root sits above it. Say what actually happened, and keep the raw-root advice as the conditional half. Also key the external-dirs grant test off the RESOLVED raw dir, exactly as _external_dirs looks it up, so the lookup cannot depend on tmp_path already being resolved. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U2dxmXzQpQmeYospgrjSi3
There was a problem hiding this comment.
Pull request overview
This PR hardens ingest discovery and child-process spawning for Windows/network-share workspaces by preventing self-ingest of the wiki, introducing an opt-in discovery size ceiling, and avoiding UNC working directories/grants for agent and git subprocesses.
Changes:
- Exclude the live wiki directory from raw discovery (including explicit paths) and prune any previously self-ingested wiki keys from manifest/failures.
- Add
CITADEL_MAX_SOURCE_BYTESto skip oversized files at discovery (reported via ingest report + status bucket + JSON). - Add a “native path” registry to prefer drive-letter spellings for subprocess
cwd, agent directory grants, and wiki-git-C; add correspondingdoctorchecks and documentation/tests.
Reviewed changes
Copilot reviewed 18 out of 18 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
citadel/config.py |
Adds native-path registry + CITADEL_MAX_SOURCE_BYTES parsing; path identity normalization via _path_id. |
citadel/ingest.py |
Excludes wiki from discovery, adds size ceiling support, reports exclusions/oversized, prunes untrackable keys. |
citadel/llm.py |
Spawns agent sessions using config.child_cwd() and grants external dirs in child-friendly spelling. |
citadel/wikigit.py |
Runs git with -C set to config.native_form(cwd) to avoid UNC issues on Windows. |
citadel/status.py |
Adds oversized bucket (render + --json) based on ingest’s stat-only walk. |
citadel/doctor.py |
Adds wiki placement and child paths checks for self-ingest clarity + UNC advisory. |
tests/test_ingest_discovery.py |
Adds coverage for wiki-exclusion behavior, pruning self-ingested keys, and the size ceiling behavior. |
tests/test_netdrive.py |
Adds unit tests for UNC detection, aliasing logic, subprocess cwd/grants, and wikigit -C usage. |
tests/test_doctor.py |
Adds tests for the new doctor checks and inventory listing. |
tests/test_status.py |
Updates status headers/JSON schema and adds oversized-bucket behavior test. |
tests/test_config_env.py |
Adds tests for CITADEL_MAX_SOURCE_BYTES parsing and negative-value clamping/warning. |
docs/configuration.md |
Documents the new size ceiling and clarifies wiki-under-raw and mapped-drive/UNC behavior. |
docs/troubleshooting.md |
Adds symptom-first entries for slow scans/huge files, UNC failures, and prior self-ingest. |
docs/maintenance.md |
Updates status semantics to include oversized bucket. |
citadel/templates/env.example |
Documents CITADEL_MAX_SOURCE_BYTES in the template. |
CHANGELOG.md |
Changelog entries for size ceiling, wiki self-ingest guard, and UNC child-cwd fix. |
CLAUDE.md |
Updates contributor guidance for new status bucket and UNC child-cwd behavior. |
.github/copilot-instructions.md |
Regenerated to match CLAUDE.md updates. |
Copilot review, config.py:332 — correct and load-bearing. The registry was only populated by _resolve_dir_entry, i.e. only for an ABSOLUTE CITADEL_*_DIR override. A default wiki (WORKSPACE_ROOT / "wiki", no override at all) and a relative override (CITADEL_WIKI_DIR=wiki, joined onto an already-resolved UNC root) both recorded nothing — so wikigit still ran `git -C \\server\share\...` on exactly the layout the reported git failures came from, even while the agent cwd was drive-letter friendly. native_form now falls back to the nearest ANCESTOR with an alias and rebases onto it. Sound because a drive mapping covers the whole subtree: if \\srv\share\team-wiki and T:\team-wiki are the same directory, so are their identically-named children. Still purely lexical, and an empty registry (every POSIX layout) short-circuits before any of it. The explicit record in _resolve_dir_entry stays for the one spelling nothing can derive: an absolute override pointing outside the workspace. Copilot's second comment (the wiki-exclusion NOTE wording) was already fixed in 1646cd9 — that review ran against the previous head. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U2dxmXzQpQmeYospgrjSi3
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 18 out of 18 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (1)
citadel/ingest.py:505
- The explicit repo-discovery wiki guard has the same
~expansion gap as _discover_walk:os.path.abspath(p)does not expand user home, so~/.../wikipaths could slip past the wiki exclusion and be treated as repo sources (e.g., when the wiki has a.gitunder CITADEL_WIKI_GIT).
for raw in paths:
p = Path(raw)
if _is_wiki_internal(Path(os.path.abspath(p))):
continue
Copilot review, ingest.py:408/502. The wiki guard for explicit paths used os.path.abspath without expanding ~, so an unexpanded path could not be recognized as wiki-internal. Reachable impact today was limited — such a path also fails os.stat and is dropped rather than ingested — but the guard should hold by construction, not by a downstream accident. Fixed at the root instead of in the guard: _explicit_path() expands ~ for every explicitly requested path in both _discover_walk and _discover_repos. That matches what every other configured path already gets (_resolve_dir_entry, workspace.init, CITADEL_WORKSPACE) and fixes a second, more concrete gap on the platform this PR is about: Windows cmd.exe (and PowerShell, for a native binary's arguments) does not expand ~, so `citadel ingest ~/ws/raw/notes.md` silently ingested nothing. os.path.expanduser, not Path.expanduser: the latter raises on an unresolvable home (~nosuchuser), and discovery must never raise on user input. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U2dxmXzQpQmeYospgrjSi3
Comment on lines
267
to
271
| tracked = set(manifest_dict) | set(failures_dict) | ||
| report.pending = sorted(key for key in _present_source_keys() if key not in tracked) | ||
| present, oversized = _walk_state() | ||
| report.pending = sorted(key for key in present if key not in tracked) | ||
| report.oversized = oversized | ||
| report.ignored = _ignored_names() |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 18 out of 18 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (2)
citadel/status.py:271
StatusReport.oversizedcurrently includes every oversized file seen on disk, even if that key is already tracked in the manifest/failures. That can make a source show up as both Ingested and Oversized, which contradicts this module’s own definition of oversized as “never tracked” and makesstatus --jsonambiguous for scripts.
tracked = set(manifest_dict) | set(failures_dict)
present, oversized = _walk_state()
report.pending = sorted(key for key in present if key not in tracked)
report.oversized = oversized
report.ignored = _ignored_names()
citadel/ingest.py:432
- For explicitly requested directories,
_scan_tree()is called withpas provided. If the user passes a relative directory like.(or any relative path that contains the wiki), the walk will build relative child paths and_is_wiki_internal()(which assumes absolute paths) will fail to recognize and prune the wiki subtree. Making the scanned root absolute (withoutresolve()) preserves the no-syscall property and keeps the wiki exclusion reliable for explicit directory ingest too.
if p.is_dir():
if not _is_repo_source(p):
_scan_tree(p, walk)
continue
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.
Three field-reported problems, all in the discovery / child-process seam that a raw root on a Windows network share exercises.
1. The wiki could become one of its own raw sources
With a raw root above the wiki —
CITADEL_RAW_DIRS=T:\and the wiki atT:\llmWiki\data-science\wiki, a normal way to say "scan this whole drive" — discovery walked the wiki's generated pages back in as sources, run after run, each pass citing the last. The wiki dir is not hidden and matches no ignore glob, so the only defense was a hand-writtenCITADEL_IGNORE_PATTERNSentry.ingest._is_wiki_internalprunes the wiki out of every walk. Containment isgrammar.is_within— purely lexical, noresolve()and noabspath()— so it costs nothing per directory entry and can never block or syscall on a dead mount._discover_repos, since aCITADEL_WIKI_GITwiki holds a.gitand would otherwise be digested as one repo source.citadel ingest(those files still exist on disk, so deletion detection would never have removed them).2. No size-based discovery filter
Ignore patterns match names; nothing matched size. Every untracked candidate is stream-hashed in full before anything can classify it, so a raw root that also holds machine data (the reported case: 617
.tdmssensor dumps, 10.6 GB in one folder) paid a full sha256 per file just to end up recorded as unreadable binary.CITADEL_MAX_SOURCE_BYTESskips anything larger straight from the walk's ownstat— never opened, never hashed, never written to the manifest or the failures catalog. Skips are reported, not silent: a stderr NOTE, an Oversized section on the run report, and an Oversized bucket incitadel status(--jsonincluded, as{"key", "size_bytes"}).Design calls:
0= no limit). Silently dropping a large-but-legitimate source — a 2 GB lecture recording underCITADEL_AUDIO_SUPPORT, a scanned archive PDF — would be worse than a slow scan, and citadel's contract is that everything under a raw root is either ingested or reported.citadel ingest big.tdmsbypasses the ceiling, exactly as it does for hidden names and ignore globs..exists()confirmation covers it; it simply stops being re-checked.CONFIG_WARNINGSentry (mirroringCITADEL_JOBS), socitadel doctornames the typo.3. Agent sessions ran with a UNC working directory
Path.resolve()rewrites a Windows mapped drive into its UNC form —T:\team-wiki→\\fileserver\share\team-wiki— whichever spelling the.envused, and that resolved path was what citadel passed as the agent CLI'scwd. The copilot backend then refuses to work at all ("environment blocks UNC/network paths"), and git treats the spelling as a different repository (safe.directory/core.filemode). The same workspace behaves perfectly when the usercds intoT:\...themselves.The resolved form stays the one identity everywhere (manifest keys, root containment, staging vs. live). What changes is only what a child process is handed:
config.NATIVE_FORMS/native_form()/child_cwd()— a tiny registry of non-resolved spellings, populated from what citadel already holds: theCITADEL_WORKSPACEvalue or the launching CWD (walked upward, since the marker walk resolves upward), and eachCITADEL_*_DIR/CITADEL_RAW_DIRSvalue on its way through_resolve_dir_entry.llm._run_session/_stream_subprocess(cwd),llm._external_dirs(the--add-dir/--include-directoriesgrants have to name paths the agent can reach from that cwd), andwikigit._git(-C).native_formis the identity function, so those invocations are byte-for-byte unchanged.WSL is documented as a non-workaround: a DrvFs mount of the same share fails differently (
Operation not permittedon the staging copy,core.filemodeongit init).Also
citadel doctorchecks: wiki placement (WARN when the wiki dir sits inside a walked raw root — discovery handles it, but the nesting still costs clarity: a stray citation into the wiki looks like legal provenance) and child paths (names the working directory sessions will actually run in; WARNs when only a UNC spelling is known, with the fix).config._path_idextracted as the one spelling-normalizer (source_rootsnow uses it).docs/configuration.md(the knob, plus new sections on wiki-under-raw and Windows mapped drives),docs/troubleshooting.md(three new symptom-first entries),docs/maintenance.md,citadel/templates/env.example,CLAUDE.md+ the generated.github/copilot-instructions.md, CHANGELOG.Tests
30 new offline tests across
test_ingest_discovery.py,test_netdrive.py,test_doctor.py,test_status.py,test_config_env.py— the UNC behavior is testable from Linux because the spelling predicates (_is_unc_path/_prefer_native) are pure string functions and the registry is a monkeypatchable config attribute.Gates run locally, all green:
uv run pytest -q— 1251 passed, 1 skippeduv run ruff check .— cleanuv run ruff format --check .— cleanCITADEL_WORKSPACE=corpora/beverages uv run python -m citadel lint— exit 0verify-corpuswas not run: it spawns real agent CLI sessions, and this change alters no prompt, rule, or session content — the agent-facing prompt is byte-identical, the size ceiling is off by default, and the native-form registry is empty on Linux, so a corpus run would exercise nothing new. Worth a run on a Windows share if you want the end-to-end confirmation of #3.🤖 Generated with Claude Code
https://claude.ai/code/session_01U2dxmXzQpQmeYospgrjSi3
Generated by Claude Code