Skip to content

RepoNeedle 1.2.0: version bump, release documentation and directory-link traversal fix - #10

Merged
krishika08 merged 2 commits into
mainfrom
release/v1.2.0
Nov 12, 2026
Merged

krishika08 merged 2 commits into
mainfrom
release/v1.2.0

Conversation

@krishika08

Copy link
Copy Markdown
Owner

Summary

Release preparation for RepoNeedle 1.2.0, plus one security fix found during final release checks. This PR does not tag, publish to PyPI or update the Hugging Face Space; those are separate steps after merge.

The PR has two commits.

1. 376f092 — Bump version to 1.2.0 and update release documentation

  • Version set to 1.2.0 in pyproject.toml and reponeedle/__init__.py.
  • New root CHANGELOG.md, reviewed against the code. It covers CLI JSON and anchors, the MCP server, indexing security, diagnostics, abstention disabled by default, optional extras, backward compatibility, migration and known limitations. No retrieval improvement is claimed.
  • Pre-release wording updated across README, KNOWN_LIMITATIONS, CLAIMS_AND_EVIDENCE and docs/.
    • MCP installation now points to pip install "reponeedle[mcp]>=1.2.0", with 1.2.0 marked upcoming until published and a from-source fallback.
    • Historical benchmark provenance is unchanged. Supplemental and experimental results are still labelled as not official ARB scores.
  • tests/test_documentation_consistency.py: replaced a check that skipped itself once the version reached 1.2, because a skip fails the strict Linux job. The new checks apply to any version.

2. 2b4da11 — Never traverse directory links during indexing; bound directory traversal

Problem. from_directory used os.walk(followlinks=False), which relies on os.path.islink(). That returns False for Windows NTFS junctions, so junctions were descended into. Reproduced with synthetic files:

Case Before After
Junction to the repository root (cycle) 83 directory visits for 3 real directories; each file indexed up to 27×; stopped only at the path length limit 3 visits, each file indexed once, src/up recorded
Junction to the repository's parent (escape and cycle) 15 directories outside the root listed; outside file names recorded in diagnostics 0 outside directories visited; only the link path recorded

Outside file contents were already not read (file-level containment from the earlier security fix). On Linux and macOS, directory symlinks were already not followed.

Fix. Changes are limited to from_directory traversal. Ranking, packing, abstention and from_documents (used by all benchmarks) are unchanged.

  • Directory symlinks and junctions are never traversed on any platform.
    • is_directory_link() uses os.lstat: S_ISLNK, or the mount-point reparse tag on Windows. Python 3.10–3.12 share one code path, which matters because Python < 3.12 has no os.path.isjunction.
    • Detection fails closed: an entry that cannot be inspected is treated as a link.
  • Each physical directory (st_dev, st_ino) is entered at most once, so traversal is bounded by the real directory tree.
  • New, defaulted diagnostics fields: skipped_directories, skipped_directory_paths and skipped_directories_outside_repository. They record in-repository paths only. A count-only warning, CLI notice and MCP startup note appear for links that resolve outside the repository.
  • Behaviour change (Windows only): a directory reachable only through a link (for example a junction to a sibling checkout) is no longer indexed. Files under an in-repository link target are indexed once, under their real path. This is documented in CHANGELOG (security entry, compatibility and migration) and KNOWN_LIMITATIONS §5a item 2c.

Compatibility. The new model fields have defaults. Existing keys and their order are unchanged. JSON without the new keys still validates. The from_directory signature is unchanged.

Tests

  • tests/test_indexing_security.py: platform-neutral tests (real symlinks on Linux; junction fallback on Windows):

    • directory link to outside the repository;
    • cycle to an ancestor;
    • cycle through the parent (including the outside-file marker check);
    • mutual link cycle;
    • valid in-repository directory link (indexed once, no warning);
    • normal nested directories;
    • broken directory link;
    • fail-closed detection;
    • repeated physical directory identity.

    The last test is simulated: os.stat is patched so one directory reports the repository root's identity. It is not a real bind mount, which would need root privileges.

  • tests/test_windows_junctions.py: Windows-only junction tests, including detection without os.path.isjunction. They skip on Linux, so this file is not in the strict no-skip job.

  • The strict mcp-security-linux job (REPONEEDLE_FAIL_ON_SKIP=1) runs tests/test_indexing_security.py, so the directory tests run there with real Linux symlinks.

Local validation (Windows)

Check Result
Full suite, Python 3.12 (mcp + api extras), final tree 495 passed, 7 skipped (file-symlink creation not permitted on this Windows host)
Commit 376f092 alone, full suite 459 passed, 0 failed
Python 3.11 (no os.path.isjunction): security, junction and diagnostics suites 54 passed (skips: mcp not installed; file symlinks)
Reproduction after the fix, Python 3.11 and 3.12 Bounded; 0 outside directories visited
Build + twine check --strict (version bump step) PASSED; wheel and metadata report 1.2.0
Clean wheel installs: base (3.11), [mcp] (3.12), [api] (3.12) 14/14, 14/14, 13/13

Not included

  • No tag, PyPI publication or Hugging Face update. The demo API and Space still bundle the 1.1.0 engine.
  • After merge: tag v1.2.0 to run the release workflow (version match → tests → build → publish), then remove the "upcoming" wording.

🤖 Generated with Claude Code

krishika08 and others added 2 commits November 11, 2026 11:32
- Set the version to 1.2.0 in pyproject.toml and reponeedle/__init__.py.
- Add CHANGELOG.md for 1.2.0 (reviewed against the code): CLI JSON and
  anchors, MCP server, indexing security, indexing diagnostics, abstention
  disabled by default, optional extras, backward compatibility, migration
  and known limitations. No retrieval improvement is claimed.
- Replace pre-release wording ("planned 1.2.0", "development branch only",
  "from source only") in README, KNOWN_LIMITATIONS, CLAIMS_AND_EVIDENCE and
  docs/. MCP installation points to `pip install "reponeedle[mcp]>=1.2.0"`,
  with 1.2.0 marked upcoming until it is published and a from-source
  fallback. Historical benchmark provenance is unchanged.
- tests/test_documentation_consistency.py: replace the check that skipped
  itself once the packaged version reached 1.2 (a skip fails the strict
  Linux CI job) with checks that apply to any version, and fail if
  "planned <packaged version>" wording remains.

The demo API and Hugging Face Space still bundle the 1.1.0 engine and are
not changed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rsal

RepoNeedleEngine.from_directory walked with os.walk(followlinks=False),
which decides via os.path.islink(). That is False for Windows NTFS
junctions, so junctions were descended into:

- A junction pointing to an ancestor directory caused repeated traversal
  until the path length limit (reproduced: 83 directory visits for 3 real
  directories, each file indexed up to 27 times).
- A junction pointing outside the repository caused outside directories
  to be listed (15 in the reproduction), and outside file names were
  recorded in indexing diagnostics. Outside file contents were already
  not read (file-level containment).

On Linux and macOS, directory symlinks were already not followed.

Fix (from_directory traversal only; ranking, packing, abstention and
from_documents are unchanged):
- Directory symlinks and junctions are never traversed on any platform.
  is_directory_link() uses os.lstat (S_ISLNK, or the mount-point reparse
  tag on Windows), so Python 3.10-3.12 share one code path. It fails
  closed: an entry that cannot be inspected is treated as a link.
- Each physical directory (st_dev, st_ino) is entered at most once, so
  cycles that are not links (e.g. bind mounts) are also bounded.
- Skipped directories are recorded by in-repository path only:
  skipped_directories, skipped_directory_paths and
  skipped_directories_outside_repository (new, defaulted fields). A
  count-only warning, CLI notice and MCP startup note are emitted for
  links resolving outside the repository.
- Files under an in-repository link target are indexed once, under their
  real path. A directory reachable only through a link is not indexed
  (a behaviour change on Windows only; documented).

Tests: platform-neutral directory-link tests in test_indexing_security.py
(real symlinks on Linux in the strict no-skip CI job; junction fallback on
Windows), including a simulated repeated-physical-identity test (os.stat
patched; not a real bind mount) and a fail-closed detection test.
Windows-only junction tests are in test_windows_junctions.py, which is
not part of the strict job.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@krishika08
krishika08 merged commit aa7ba70 into main Nov 12, 2026
10 checks passed
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.

1 participant