Corpus file-tool review polish (PR #1940 follow-up)#1949
Conversation
Closes #1943. - rename_document (item 1): FolderDocumentService.rename_document now returns a (success, error, new_path, changed) 4-tuple. The tool reads `changed` instead of snapshotting the pre-rename DocumentPath, dropping an extra query and closing the snapshot/service status race that could mislabel a concurrent rename's status. Underlying data was always safe (select_for_update); only the tool-layer status string could drift. - require_user helper (item 4): the duplicated "no user injected / id not found -> PermissionError" guard in rename_document and delete_document is now a single require_user(user_id, caller) in core_tools/_helpers.py, preserving the same per-caller error messages. Read-only tools keep get_user_or_none. - delete_document description (item 3): now explains corpus DELETE is a higher tier than the write tools' permission and tells the LLM to report a permission-denied failure rather than retry-loop on it. The dedicated requires_delete_permission flag remains a tracked follow-up. - sanitize_corpus_filename unit test (item 5): direct coverage in test_shared_utils.py for empty/None fallback, all-special->underscores, path-separator collapse, no run-collapsing, leading-dot preservation, and truncation at MAX_FILENAME_LENGTH. - Item 2 intentionally not actioned: CorpusDocumentService. _check_document_in_corpus was made private deliberately (PR #1685) to signal its NO-PERMISSION-CHECK contract; its callers are all inside corpuses.services and gate corpus READ upstream, so the service-internal underscore is correct.
Code ReviewOverviewThis PR delivers surgical follow-up fixes from the PR #1940 review across four areas: eliminating an extra DB read + race condition in Positives
Issues / NitsMinor —
|
Closes #1943.
Addresses the PR #1940 code-review follow-up. Surgical changes scoped to the corpus file-management agent tools and the shared sanitiser.
Item-by-item
✅ Item 1 — extra DB read + status race in
rename_document(fixed)FolderDocumentService.rename_document(opencontractserver/corpuses/services/folder_documents.py) now returns a(success, error, new_path, changed)4-tuple. The newchangedboolean isTrueonly for a real rename andFalsefor a no-op (sanitised name already matched the current filename) or any failure.The tool (
opencontractserver/llms/tools/core_tools/documents.py) previously snapshotted the pre-renameDocumentPath.pathand diffed it against the service's returned path to label the response"renamed"vs"unchanged". That extraDocumentPathread is gone, and so is the snapshot‑vs‑service race window where a concurrent rename could mislabelstatus. (Data integrity was never at risk — the service'sselect_for_updateguarantees that; only the tool-layer status string could drift.)✅ Item 3 —
delete_documentWRITE‑vs‑DELETE gate (description update — the issue's stated minimum)The
delete_documentdescription (tool_registry.py) now spells out that deletion needs the corpus DELETE tier — higher than the write permissionrename_document/move_documentuse — and instructs the LLM to report apermission deniedfailure rather than retry-loop on it. A WRITE‑but‑not‑DELETE user still passes the coarse frameworkrequires_write_permissiongate and is correctly rejected byDocumentLifecycleService.soft_delete_document(defense in depth). Tightening this to a dedicatedrequires_delete_permissionflag (so the tool is filtered out before such a user ever sees it) is left as the tracked near-term follow-up the issue itself proposed.✅ Item 4 — repeated user-guard (fixed, DRY)
Added
require_user(user_id, caller)tocore_tools/_helpers.py(companion to the existingget_user_or_none). It collapses the 5‑line "no user injected / id not found →PermissionError" block thatrename_documentanddelete_documenteach repeated, preserving the exact same per-caller error messages. Read-only tools keep usingget_user_or_none(which tolerates anonymous access).✅ Item 5 — direct unit test for
sanitize_corpus_filename(added)New
TestSanitizeCorpusFilenameinopencontractserver/tests/test_shared_utils.pycovering: empty/None→ fallback, custom fallback, all‑special‑chars → underscores, path‑separator collapse (no directory traversal), runs not collapsed, leading‑dot preservation, and truncation atMAX_FILENAME_LENGTH(incl. truncate‑before‑sanitise).The review suggested making
CorpusDocumentService._check_document_in_corpuspublic. I deliberately kept it private: it was renamed from public → underscore‑prefixed in PR #1685 (documented inCHANGELOG.md) specifically to signal its "NO PERMISSION CHECK" contract. Its cross-service callers (lifecycle.py,folder_documents.py) all live inside thecorpuses.servicespackage and gate corpus READ upstream, so the leading underscore (service-internal, footgun-out-of-autocomplete) is the correct state. Reversing it would undo a security-conscious decision. The issue's own summary lists item 2 as a non-blocking follow-up.Verification
black,flake8,isort(profile=black) clean on all changed files.sanitize_corpus_filenameassertions validated standalone (all 11 pass).test_corpus_file_tools.pytraced against the new 4‑tuple /require_userpaths — all assertions remain consistent (status,path, andPermissionError/ValueErrormessages unchanged).Changelog fragments:
changelog.d/1943-corpus-file-tools-followup.{fixed,changed}.md.Generated by Claude Code