diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 9ec74626..5a213032 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -86,9 +86,9 @@ Quality gates: `cargo check`, `cargo clippy`, `cargo test --lib --bins`, `dotnet ## Tooling rules (IMPORTANT) -- **Do NOT use the `codesearch` CLI/exe to investigate this repo.** Codesearch is the project under development and is currently potentially broken — using our own broken tool to debug itself is unreliable. -- **Codesearch must always be used via its MCP server tools** (when available), never via the bundled binary at the shell. -- **For this repo, fall back to `grep` / `Glob` / `Read`** for all discovery and navigation until codesearch is verified working again. +- **Use codesearch MCP tools first for discovery** on this repo. The MCP server is verified working and this repo is indexed (alias `codesearch-git`) — `search` / `find` / `explore` are the default for "where/what/how" questions, per the global codesearch-first rule. +- **Never use the bundled `codesearch` CLI/exe to investigate this repo.** It's the project under development and may be broken/locked; debugging it with its own shell binary is unreliable. MCP server tools only. +- **`grep` / `Glob` / `Read` remain correct for:** inspecting a specific git ref or fetched PR head (e.g. `git show FETCH_HEAD:path` — codesearch only indexes the on-disk working tree, not arbitrary refs), exact literal/regex matching, and any case where codesearch returns nothing useful. ## Notes diff --git a/.claude/commands/release.md b/.claude/commands/release.md deleted file mode 100644 index df062ed1..00000000 --- a/.claude/commands/release.md +++ /dev/null @@ -1,64 +0,0 @@ ---- -description: Cut a release — run /merge (feature → develop), then promote develop → master and push the version tag -argument-hint: [optional PR/release title] -allowed-tools: Bash(git:*), Bash(gh:*), Bash(cargo:*), Bash(grep:*), Read, Edit, Grep, Glob ---- - -# /release — full release: land on `develop`, promote to `master`, tag - -This is `/merge` **plus** the `develop → master` promotion and the version-tag push that -triggers the build/publish pipeline. - -## Branch & version facts (this repo) -- Flow: `feature/*` → PR → **`develop`** → PR → **`master`** → push tag `vX.Y.Z`. -- `master` is protected: PRs to it may come **only** from `develop` or `release/*` - (`.github/workflows/protect-master.yml`). -- Pushing a `vX.Y.Z` tag triggers `.github/workflows/release.yml` (builds Windows/Linux/macOS - archives, plain + `-with-csharp`, and publishes the GitHub release). **Push the tag only - AFTER the develop→master PR has merged.** -- The version is fixed by the feature-branch commit (the pre-commit hook bumps only on - feature branches). develop/master merges and the tag all carry that same version. - -## Guardrails -- NEVER use `--no-verify`. NEVER force-push shared branches. -- Push the tag exactly once, only after master has the release commit. -- If CI fails at any gate, STOP and report — do not promote or tag a red build. - -## Part 1 — land on `develop` (the `/merge` workflow) -Execute every step of **`/merge`** (README/CHANGELOG checks → commit → push → PR → auto-merge -to `develop`). Then **wait for the develop PR to actually merge** (auto-merge waits on CI): -- Capture the PR number (`PR=$(gh pr view --json number --jq .number)`), then poll - `gh pr view "$PR" --json state,mergedAt,mergeStateStatus` until `state` is `MERGED`. -- If checks fail, STOP and report. Do not proceed to Part 2. - -## Part 2 — promote `develop` → `master` -1. `git fetch origin && git checkout develop && git pull --ff-only origin develop`. -2. Determine the release version: `VERSION=v$(grep -m1 '^version' Cargo.toml | sed -E 's/.*"(.+)".*/\1/')`. -3. Open the release PR (source `develop`, which protect-master allows): - - `gh pr create --base master --head develop --title "Release $VERSION — " --body ""`. - - Title: prefix `Release $VERSION — ` then a short summary (or `$ARGUMENTS` if provided), - matching history (e.g. `Release v1.0.142 — serve responsive during warmup`). - - Body ends with: `🤖 Generated with [Claude Code](https://claude.com/claude-code)`. - - Capture the PR number: `RELEASE_PR=$(gh pr view develop --json number --jq .number)`. -4. This repo **disallows merge commits** — always use `--squash`, never `--merge`. - `gh pr merge "$RELEASE_PR" --auto --squash`. Wait until `state` is - `MERGED` (poll as in Part 1). If auto-merge is unavailable, `gh pr checks "$RELEASE_PR" --watch` - then `gh pr merge "$RELEASE_PR" --squash`. If CI fails, STOP. - -## Part 3 — tag the release -1. `git fetch origin --tags && git checkout master && git pull --ff-only origin master`. -2. Confirm the version on master matches: `grep -m1 '^version' Cargo.toml` equals `$VERSION` (minus the `v`). - If it does not match, STOP and report (do not guess a tag). -3. Guard against a double release: if `$VERSION` already exists as a tag - (`git tag -l "$VERSION"` non-empty, or `git ls-remote --tags origin "$VERSION"` non-empty), - STOP — the release was already cut. -4. `git tag "$VERSION" && git push origin "$VERSION"` → triggers `release.yml`. -5. Report the pushed tag and remind the user to watch the Actions "Release" run for artifacts. - -## Part 4 — keep `develop` in sync (only if needed) -If `master` ended up ahead of `develop` (e.g. a CHANGELOG/version edit merged only on master), -open a sync PR `master → develop` (or fast-forward develop) — matching the repo's post-release -sync convention (e.g. PR #90 "sync: backfill CHANGELOG … from master"). Skip if already in sync. - -## Report -develop PR URL, release PR URL, tag pushed (`vX.Y.Z`), final version, and sync action (if any). diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8e144691..6f2ba2f3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,6 +19,8 @@ jobs: steps: # pin@v4 - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + with: + persist-credentials: false # pin@stable - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # pin@v4 @@ -40,6 +42,8 @@ jobs: steps: # pin@v4 - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + with: + persist-credentials: false # pin@stable - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # pin@v4 @@ -58,6 +62,8 @@ jobs: steps: # pin@v4 - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + with: + persist-credentials: false # pin@v4 - uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 with: diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 7b07e5bd..d18fe332 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -14,6 +14,14 @@ jobs: name: Analyze runs-on: ubuntu-latest timeout-minutes: 30 + # Skip on fork pull requests: the restricted GITHUB_TOKEN for fork PRs + # cannot write `security-events` back to the upstream repo, so the + # `github/codeql-action/analyze` upload step fails with + # "Resource not accessible by integration". CodeQL still runs on push + # events to develop/master (where the token has full write scopes per + # the `permissions:` block below), so merged code is still scanned — + # this only skips the redundant, upload-failing fork-PR run. + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository permissions: actions: read contents: read @@ -26,7 +34,9 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # pin@v4 + with: + persist-credentials: false - name: Initialize CodeQL uses: github/codeql-action/init@v3 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 153da82d..41a26b92 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -39,6 +39,8 @@ jobs: steps: # pin@v4 - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + with: + persist-credentials: false - name: Install Rust # pin@stable @@ -126,6 +128,8 @@ jobs: steps: # pin@v4 - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + with: + persist-credentials: false - name: Install Rust # pin@stable diff --git a/AGENTS.md b/AGENTS.md index 5beea91f..047cd22c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -81,4 +81,4 @@ Common mistake: a subagent runs `/git pr create` with no explicit `--base`, the - **Deploy:** `..\copy-to-common.ps1` — builds + copies both binaries to `~/.local/bin/`. A running `codesearch.exe` is file-locked on Windows; stop serve before deploying. - **Canonical paths:** NEVER call `.canonicalize()` directly. Always use `safe_canonicalize()`. - **LMDB rule:** No two `EnvOpenOptions::open()` on same dir in same process. All access via `get_or_open_stores()` → `Arc`. -- **Tooling:** do not use the bundled `codesearch` binary to investigate this repo (it's the project under development). Use codesearch MCP tools when available, else `grep`/`Glob`/`Read`. +- **Tooling:** never use the bundled `codesearch` binary to investigate this repo (it's the project under development). Use codesearch **MCP tools first** for discovery (server verified working; this repo indexed as `codesearch-git`). `grep`/`Glob`/`Read` stay correct for a specific git ref / fetched PR head (codesearch only indexes the on-disk working tree), exact literal matching, or when MCP returns nothing. diff --git a/CHANGELOG.md b/CHANGELOG.md index 20e41f81..34b0a17b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.1.31] - 2026-07-23 + +**Security hardening sweep (Aikido) + community bug/dependency fixes.** + +### Added + +- **EmbeddingGemma retrieval support (#155, original work by @markschroedr, superseding #147).** Adds support for Google's EmbeddingGemma embedding model as an additional embedder option, alongside model-selection hardening and improved error messages for unsupported/misconfigured embedding models. +- **`CODESEARCH_ALLOWED_HOSTS` / `CODESEARCH_DISABLE_HOST_VALIDATION` (#149, reported by @stdweird).** rmcp's DNS-rebinding defence defaults the MCP transport's `Host`-header allowlist to loopback-only, rejecting container/service hostnames in containerised deployments. `CODESEARCH_ALLOWED_HOSTS` lets you extend the allowlist with a comma-separated hostname list; `CODESEARCH_DISABLE_HOST_VALIDATION=1` disables the check entirely (only safe behind a reverse proxy). See README `## Security`. +- **`raise_fd_limit()` at serve startup (#150, contributed by @tony-nexartis).** `codesearch serve`'s fd demand scales with registered repo count; under process supervisors with a low default `ulimit -n` (notably macOS launchd, 256), this could silently exhaust file descriptors and wedge `accept()` with `EMFILE` while the daemon still looked healthy. Serve now raises its own soft `RLIMIT_NOFILE` to the hard limit at startup (Unix only) and warns if the effective limit still looks insufficient for the repo count. +- **`persist-credentials: false`** added to every `actions/checkout` step across all GitHub Actions workflows, and the CodeQL workflow's floating `actions/checkout@v4` pinned to the same SHA already used elsewhere — reduces the blast radius of a compromised CI step and closes a supply-chain drift gap. +- **CodeQL skipped on fork PRs.** Fork-originated PRs carry a restricted `GITHUB_TOKEN` that cannot upload SARIF results to the upstream repo, which was failing the CodeQL check on every external contribution (e.g. #150) with a confusing "Resource not accessible by integration" error unrelated to the PR's actual code. The analyze job is now skipped for fork PRs (still runs on `develop`/`master` push, same-repo PRs, and the schedule). + +### Fixed + +- **Panic on multi-byte UTF-8 boundary in search snippets (#148, reported by @tony-nexartis).** Search-result snippet truncation byte-sliced content at a fixed offset, panicking whenever that offset landed inside a multi-byte character (box-drawing glyphs, CJK, emoji). Now truncates on a char boundary. +- **Path-traversal hardening (critical).** `codesearch index`'s project-path resolution no longer silently falls back to the raw, unvalidated path when canonicalization fails — it now fails fast with an actionable error. The `.NET` symbol-helper CLI (`scip-csharp`) now canonicalizes every path argument (`--solution`, `--project`, `--output`, `--symbols-file`) before use, closing several path-traversal vectors flagged by Aikido SAST. +- **Registering a `.git`/build-artifact directory as a project root.** `codesearch index`/repo registration now rejects a root whose own directory name matches an always-excluded name (`.git`, `.svn`, `node_modules`, etc.), preventing accidental indexing and search-exposure of internal VCS metadata. +- **ANSI/control-sequence injection in terminal output.** Search results and sync/reindex logs now strip ANSI escape sequences (CSI, OSC, Fe) and stray control characters from indexed file content before printing, so a maliciously crafted file can no longer manipulate the user's terminal (clear screen, hide output, rewrite the title bar, etc.). +- **Unix path-cache key collision.** The path-normalization cache used for file metadata unconditionally converted `\` to `/`, which on Unix (where `\` is a legal filename character, not a separator) could collapse a literal-backslash filename with an unrelated subdirectory path into the same cache key. The conversion is now gated to Windows only. +- **Dependency CVE remediation.** `rmcp` floor bumped `1.5.0 → 1.8.0` (3 CVEs fixed); ~100 transitive dependencies refreshed via `cargo update`, including security-relevant bumps to `quinn-proto`, `h2`, `hyper`, `tokio`, `rustls`, `openssl`, `zerocopy`, `zeroize`, `webpki-roots`, `aws-lc-rs`. + ## [1.1.30] - 2026-07-10 ### Added diff --git a/Cargo.lock b/Cargo.lock index 09bb6078..e4fcce82 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -122,9 +122,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.102" +version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" [[package]] name = "arbitrary" @@ -134,9 +134,9 @@ checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" [[package]] name = "arc-swap" -version = "1.9.1" +version = "1.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207" +checksum = "c049c0be4daef0b145cb3555416b3b8ef5b7888a38aea1a3a155801fe7b0810b" dependencies = [ "rustversion", ] @@ -149,14 +149,14 @@ checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] name = "arrayvec" -version = "0.7.6" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" [[package]] name = "arroy" @@ -171,7 +171,7 @@ dependencies = [ "memmap2", "nohash", "ordered-float", - "rand 0.8.6", + "rand 0.8.7", "rayon", "roaring", "tempfile", @@ -189,13 +189,13 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.89" +version = "0.1.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +checksum = "ae36dc4177970ef04fde5178d3e2429882def40e57a451f919c098f72baa6cec" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.3", ] [[package]] @@ -225,7 +225,7 @@ dependencies = [ "num-traits", "pastey 0.1.1", "rayon", - "thiserror 2.0.18", + "thiserror 2.0.19", "v_frame", "y4m", ] @@ -255,9 +255,9 @@ dependencies = [ [[package]] name = "aws-lc-rs" -version = "1.17.0" +version = "1.17.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ec2f1fc3ec205783a5da9a7e6c1509cc69dedf09a1949e412c1e18469326d00" +checksum = "00bdb5da18dac48ca2cc7cd4a98e533e8635a58e2361d13a1a4ee3888e0d72f1" dependencies = [ "aws-lc-sys", "zeroize", @@ -265,14 +265,15 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.41.0" +version = "0.43.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a2f9779ce85b93ab6170dd940ad0169b5766ff848247aff13bb788b832fe3f4" +checksum = "43103168cc76fe62678a375e722fc9cb3a0146159ac5828bc4f0dfd755c2224c" dependencies = [ "cc", "cmake", "dunce", "fs_extra", + "pkg-config", ] [[package]] @@ -371,9 +372,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.13.0" +version = "2.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" dependencies = [ "serde_core", ] @@ -416,12 +417,12 @@ dependencies = [ [[package]] name = "bstr" -version = "1.12.1" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" +checksum = "1f7dc094d718f2e1c1559ad110e27eeaae14a5465d3d56dd6dbd793079fbd530" dependencies = [ "memchr", - "serde", + "serde_core", ] [[package]] @@ -438,22 +439,22 @@ checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "bytemuck" -version = "1.25.0" +version = "1.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" dependencies = [ "bytemuck_derive", ] [[package]] name = "bytemuck_derive" -version = "1.10.2" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" +checksum = "f65693059b6b9c588b9f62fed1cedbf0a8b805631457ea162d68f0de186f3de5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -470,9 +471,9 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" [[package]] name = "bytes" -version = "1.11.1" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "cassowary" @@ -497,9 +498,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.63" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" +checksum = "c89588d05638b5b4594a3348a2d6c20277e43a7f5c5202b05cc56888475a47b8" dependencies = [ "find-msvc-tools", "jobserver", @@ -521,15 +522,15 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cfg_aliases" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" [[package]] name = "chacha20" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" dependencies = [ "cfg-if", "cpufeatures 0.3.0", @@ -579,9 +580,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.6.1" +version = "4.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +checksum = "d91e0c145792ef73a6ad36d27c75ac09f1832222a3c209689d90f534685ee5b7" dependencies = [ "clap_builder", "clap_derive", @@ -589,9 +590,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.6.0" +version = "4.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +checksum = "f09628afdcc538b57f3c6341e9c8e9970f18e4a481690a64974d7023bd33548b" dependencies = [ "anstream", "anstyle", @@ -601,14 +602,14 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.6.1" +version = "4.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061" dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 3.0.3", ] [[package]] @@ -649,6 +650,7 @@ dependencies = [ "hf-hub 0.3.2", "ignore", "indicatif 0.17.11", + "libc", "moka", "ndarray 0.16.1", "notify", @@ -656,7 +658,7 @@ dependencies = [ "num_cpus", "ort", "pretty_assertions", - "rand 0.8.6", + "rand 0.8.7", "ratatui", "rayon", "regex", @@ -774,9 +776,9 @@ dependencies = [ [[package]] name = "console" -version = "0.16.3" +version = "0.16.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d64e8af5551369d19cf50138de61f1c42074ab970f74e99be916646777f8fc87" +checksum = "4fe5f465a4f6fee88fad41b85d990f84c835335e85b5d9e6e63e0d06d28cba7c" dependencies = [ "encode_unicode", "libc", @@ -904,18 +906,18 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.15" +version = "0.5.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-deque" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb" dependencies = [ "crossbeam-epoch", "crossbeam-utils", @@ -923,27 +925,27 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.18" +version = "0.9.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-queue" -version = "0.3.12" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +checksum = "803d13fb3b09d88be9f4dbc29062c66b19bf7170867ceb746d2a8689bf6c7a26" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.21" +version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" [[package]] name = "crossterm" @@ -951,9 +953,9 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "crossterm_winapi", - "mio 1.2.1", + "mio 1.2.2", "parking_lot", "rustix 0.38.44", "signal-hook", @@ -1028,7 +1030,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn", + "syn 2.0.119", ] [[package]] @@ -1041,7 +1043,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn", + "syn 2.0.119", ] [[package]] @@ -1052,7 +1054,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core 0.20.11", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -1063,7 +1065,7 @@ checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ "darling_core 0.23.0", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -1091,9 +1093,9 @@ dependencies = [ [[package]] name = "der" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71fd89660b2dc699704064e59e9dba0147b903e85319429e131620d022be411b" +checksum = "a69dedd701da44b0536442edf09c81a64b0ab97a7a4a5e3d1971f00027cbc63d" dependencies = [ "pem-rfc7468", "zeroize", @@ -1105,7 +1107,6 @@ version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ - "powerfmt", "serde_core", ] @@ -1127,7 +1128,7 @@ dependencies = [ "darling 0.20.11", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -1137,7 +1138,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" dependencies = [ "derive_builder_core", - "syn", + "syn 2.0.119", ] [[package]] @@ -1204,7 +1205,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "block2", "libc", "objc2", @@ -1218,7 +1219,7 @@ checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -1295,7 +1296,7 @@ checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -1322,14 +1323,16 @@ checksum = "d817e038c30374a4bcb22f94d0a8a0e216958d4c3dcde369b1439fec4bdda6e6" [[package]] name = "exr" -version = "1.74.0" +version = "1.74.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4300e043a56aa2cb633c01af81ca8f699a321879a7854d3896a0ba89056363be" +checksum = "711fe42c9964295e01ee3fba3f9fe0e1d24b98886950d68efe81b1c76e21adf3" dependencies = [ "bit_field", "half", "lebe", "miniz_oxide", + "num-complex", + "pulp", "rayon-core", "smallvec", "zune-inflate", @@ -1343,9 +1346,9 @@ checksum = "9afc2bd4d5a73106dd53d10d73d3401c2f32730ba2c0b93ddb888a8983680471" [[package]] name = "fastembed" -version = "5.16.0" +version = "5.17.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "add59222e7bc3787285f993744b244cd454d78571845623606bdc45b22b23a4e" +checksum = "f3c8600c9ec79b51d60c19911fe14eac04fe9c2895e87d2a3e80e2213d645a32" dependencies = [ "anyhow", "hf-hub 0.5.0", @@ -1360,9 +1363,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" [[package]] name = "fax" @@ -1493,9 +1496,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +checksum = "a88cf1f829d945f548cf8fec32c61b1f202b6d93b45848602fc02af4b12ad218" dependencies = [ "futures-channel", "futures-core", @@ -1508,9 +1511,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" dependencies = [ "futures-core", "futures-sink", @@ -1518,15 +1521,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" [[package]] name = "futures-executor" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458" dependencies = [ "futures-core", "futures-task", @@ -1535,38 +1538,38 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" +checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" [[package]] name = "futures-macro" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] name = "futures-sink" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" +checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" [[package]] name = "futures-task" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" +checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" [[package]] name = "futures-util" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" dependencies = [ "futures-channel", "futures-core", @@ -1609,25 +1612,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", - "js-sys", "libc", "r-efi 5.3.0", "wasip2", - "wasm-bindgen", ] [[package]] name = "getrandom" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi 6.0.0", "rand_core 0.10.1", - "wasip2", - "wasip3", + "wasm-bindgen", ] [[package]] @@ -1642,9 +1643,9 @@ dependencies = [ [[package]] name = "globset" -version = "0.4.18" +version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3" +checksum = "e47d37d2ae4464254884b60ab7071be2b876a9c35b696bd018ddcc76847309cd" dependencies = [ "aho-corasick", "bstr", @@ -1655,9 +1656,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.14" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733" +checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155" dependencies = [ "atomic-waker", "bytes", @@ -1731,7 +1732,7 @@ version = "0.20.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d4f449bab7320c56003d37732a917e18798e2f1709d80263face2b4f9436ddb" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "byteorder", "heed-traits", "heed-types", @@ -1779,7 +1780,7 @@ dependencies = [ "indicatif 0.17.11", "log", "native-tls", - "rand 0.8.6", + "rand 0.8.7", "serde", "serde_json", "thiserror 1.0.69", @@ -1794,15 +1795,15 @@ checksum = "aef3982638978efa195ff11b305f51f1f22f4f0a6cabee7af79b383ebee6a213" dependencies = [ "dirs 6.0.0", "http", - "indicatif 0.18.4", + "indicatif 0.18.6", "libc", "log", "native-tls", - "rand 0.9.4", + "rand 0.9.5", "reqwest 0.12.28", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror 2.0.19", "ureq 3.3.0", "windows-sys 0.61.2", ] @@ -1831,9 +1832,9 @@ dependencies = [ [[package]] name = "http-body" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" dependencies = [ "bytes", "http", @@ -1841,9 +1842,9 @@ dependencies = [ [[package]] name = "http-body-util" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2" dependencies = [ "bytes", "futures-core", @@ -1866,9 +1867,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" +checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72" dependencies = [ "atomic-waker", "bytes", @@ -2048,12 +2049,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - [[package]] name = "ident_case" version = "1.0.1" @@ -2083,9 +2078,9 @@ dependencies = [ [[package]] name = "ignore" -version = "0.4.26" +version = "0.4.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b915661dd01db3f05050265b2477bcc6527b3792388e2749b41623cc592be67d" +checksum = "7f8a7b8211e695a1d0cd91cace480d4d0bd57667ab10277cc412c5f7f4884f83" dependencies = [ "crossbeam-deque", "globset", @@ -2145,8 +2140,6 @@ checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", "hashbrown 0.17.1", - "serde", - "serde_core", ] [[package]] @@ -2164,11 +2157,11 @@ dependencies = [ [[package]] name = "indicatif" -version = "0.18.4" +version = "0.18.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25470f23803092da7d239834776d653104d551bc4d7eacaf31e6837854b8e9eb" +checksum = "9433806cd6b4ec1aba79c021c7e4c58fb4c3b9977c085062e611ac929998fb0c" dependencies = [ - "console 0.16.3", + "console 0.16.4", "portable-atomic", "unicode-width 0.2.0", "unit-prefix", @@ -2197,9 +2190,9 @@ dependencies = [ [[package]] name = "inotify-sys" -version = "0.1.5" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +checksum = "c033f80b2c113cdf91ab7a33faa9cbc014726dcad99880c8609af2a370edf37d" dependencies = [ "libc", ] @@ -2214,7 +2207,7 @@ dependencies = [ "indoc", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -2237,7 +2230,7 @@ checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -2317,7 +2310,7 @@ dependencies = [ "jni-sys", "log", "simd_cesu8", - "thiserror 2.0.18", + "thiserror 2.0.19", "walkdir", "windows-link", ] @@ -2332,7 +2325,7 @@ dependencies = [ "quote", "rustc_version", "simd_cesu8", - "syn", + "syn 2.0.119", ] [[package]] @@ -2351,24 +2344,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" dependencies = [ "quote", - "syn", + "syn 2.0.119", ] [[package]] name = "jobserver" -version = "0.1.34" +version = "0.1.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" dependencies = [ - "getrandom 0.3.4", + "getrandom 0.4.3", "libc", ] [[package]] name = "js-sys" -version = "0.3.100" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2025f20d7a4fa7785846e7b63d10a76d3f1cee98ee5cb79ea59703f95e42162" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" dependencies = [ "cfg-if", "futures-util", @@ -2391,7 +2384,7 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07293a4e297ac234359b510362495713f75ea345d5307140414f20c69ffeb087" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "libc", ] @@ -2401,12 +2394,6 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" -[[package]] -name = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - [[package]] name = "lebe" version = "0.5.3" @@ -2421,9 +2408,9 @@ checksum = "0c2cdeb66e45e9f36bfad5bbdb4d2384e70936afbee843c6f6543f0c551ebb25" [[package]] name = "libc" -version = "0.2.186" +version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" [[package]] name = "libfuzzer-sys" @@ -2443,9 +2430,9 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libredox" -version = "0.1.17" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3" +checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652" dependencies = [ "libc", ] @@ -2496,9 +2483,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.32" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" [[package]] name = "loop9" @@ -2569,9 +2556,9 @@ checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" [[package]] name = "matrixmultiply" -version = "0.3.10" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a06de3016e9fae57a36fd14dba131fccf49f74b40b7fbdb472f96e361ec71a08" +checksum = "3f607c237553f086e7043417a51df26b2eb899d3caff94e6a67592ff992fedc7" dependencies = [ "autocfg", "rawpointer", @@ -2599,15 +2586,15 @@ dependencies = [ [[package]] name = "memchr" -version = "2.8.1" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "memmap2" -version = "0.9.10" +version = "0.9.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" +checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0" dependencies = [ "libc", ] @@ -2648,9 +2635,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" dependencies = [ "libc", "log", @@ -2694,7 +2681,7 @@ checksum = "e4db6d5580af57bf992f59068d4ea26fd518574ff48d7639b255a36f9de6e7e9" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -2772,7 +2759,7 @@ version = "0.31.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "cfg-if", "cfg_aliases", "libc", @@ -2824,7 +2811,7 @@ version = "6.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "crossbeam-channel", "filetime", "fsevent-sys", @@ -2871,9 +2858,9 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.4.6" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" dependencies = [ "num-integer", "num-traits", @@ -2885,6 +2872,7 @@ version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" dependencies = [ + "bytemuck", "num-traits", ] @@ -2902,7 +2890,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -2966,7 +2954,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", ] [[package]] @@ -3009,7 +2997,7 @@ version = "6.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc3cbf698f9438986c11a880c90a6d04b9de27575afd28bbf45b154b6c709e2" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "libc", "once_cell", "onig_sys", @@ -3033,11 +3021,11 @@ checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" [[package]] name = "openssl" -version = "0.10.80" +version = "0.10.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a45fa2aa886c42762255da344f0a0d313e254066c46aad76f300c3d3da62d967" +checksum = "77823a27f0babb03091cb9ed9ef80af3b39dbc82f97e8fa530374b7dafd87a45" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "cfg-if", "foreign-types", "libc", @@ -3053,7 +3041,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -3064,9 +3052,9 @@ checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] name = "openssl-sys" -version = "0.9.116" +version = "0.9.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f28a22dc7140cda5f096e5e7724a6962ca81a7f8bfd2979f9b18c11af56318c4" +checksum = "b47e7e6bb2c38cd930d25a23b40fa52e068c10e85f3e03a7f5ba5aaca5713695" dependencies = [ "cc", "libc", @@ -3205,7 +3193,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ "phf_shared", - "rand 0.8.6", + "rand 0.8.7", ] [[package]] @@ -3218,7 +3206,7 @@ dependencies = [ "phf_shared", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -3276,7 +3264,7 @@ version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "crc32fast", "fdeflate", "flate2", @@ -3285,9 +3273,9 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.13.1" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" +checksum = "3d20d5497ef88037a52ff98267d066e7f11fcc5e99bbfbd58a42336193aacec3" [[package]] name = "portable-atomic-util" @@ -3332,21 +3320,11 @@ dependencies = [ "yansi", ] -[[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn", -] - [[package]] name = "proc-macro2" -version = "1.0.106" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" dependencies = [ "unicode-ident", ] @@ -3367,14 +3345,37 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4488a4a36b9a4ba6b9334a32a39971f77c1436ec82c38707bce707699cc3bbcb" dependencies = [ "quote", - "syn", + "syn 2.0.119", ] +[[package]] +name = "pulp" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046aa45b989642ec2e4717c8e72d677b13edd831a4d3b6cf37d9a3e54912496a" +dependencies = [ + "bytemuck", + "cfg-if", + "libm", + "num-complex", + "paste", + "pulp-wasm-simd-flag", + "raw-cpuid", + "reborrow", + "version_check", +] + +[[package]] +name = "pulp-wasm-simd-flag" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d8f70e07b9c3962945a74e59ca1c511bba65b6419468acc217c457d93f3c740" + [[package]] name = "pxfm" -version = "0.1.29" +version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f" +checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea" [[package]] name = "qoi" @@ -3393,19 +3394,19 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" [[package]] name = "quinn" -version = "0.11.9" +version = "0.11.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" dependencies = [ "bytes", "cfg_aliases", "pin-project-lite", "quinn-proto", "quinn-udp", - "rustc-hash 2.1.2", + "rustc-hash 2.1.3", "rustls", "socket2", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", "tracing", "web-time", @@ -3413,21 +3414,22 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.14" +version = "0.11.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560" dependencies = [ "aws-lc-rs", "bytes", - "getrandom 0.3.4", + "getrandom 0.4.3", "lru-slab", - "rand 0.9.4", + "rand 0.10.2", + "rand_pcg", "ring", - "rustc-hash 2.1.2", + "rustc-hash 2.1.3", "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.18", + "thiserror 2.0.19", "tinyvec", "tracing", "web-time", @@ -3435,23 +3437,23 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.5.14" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" dependencies = [ "cfg_aliases", "libc", "once_cell", "socket2", "tracing", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "quote" -version = "1.0.45" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" dependencies = [ "proc-macro2", ] @@ -3470,9 +3472,9 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "rand" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" dependencies = [ "libc", "rand_chacha 0.3.1", @@ -3481,9 +3483,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.5", @@ -3491,12 +3493,12 @@ dependencies = [ [[package]] name = "rand" -version = "0.10.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" dependencies = [ "chacha20", - "getrandom 0.4.2", + "getrandom 0.4.3", "rand_core 0.10.1", ] @@ -3551,7 +3553,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" dependencies = [ "num-traits", - "rand 0.8.6", + "rand 0.8.7", +] + +[[package]] +name = "rand_pcg" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core 0.10.1", ] [[package]] @@ -3560,7 +3571,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "cassowary", "compact_str 0.8.2", "crossterm", @@ -3602,10 +3613,10 @@ dependencies = [ "num-traits", "paste", "profiling", - "rand 0.9.4", + "rand 0.9.5", "rand_chacha 0.9.0", "simd_helpers", - "thiserror 2.0.18", + "thiserror 2.0.19", "v_frame", "wasm-bindgen", ] @@ -3625,6 +3636,15 @@ dependencies = [ "rgb", ] +[[package]] +name = "raw-cpuid" +version = "11.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" +dependencies = [ + "bitflags 2.13.1", +] + [[package]] name = "rawpointer" version = "0.2.1" @@ -3662,13 +3682,19 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "reborrow" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03251193000f4bd3b042892be858ee50e8b3719f2b08e5833ac4353724632430" + [[package]] name = "redox_syscall" version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", ] [[package]] @@ -3690,34 +3716,34 @@ checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ "getrandom 0.2.17", "libredox", - "thiserror 2.0.18", + "thiserror 2.0.19", ] [[package]] name = "ref-cast" -version = "1.0.25" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +checksum = "216e8f773d7923bcba9ceb86a86c93cabb3903a11872fc3f138c49630e50b96d" dependencies = [ "ref-cast-impl", ] [[package]] name = "ref-cast-impl" -version = "1.0.25" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +checksum = "2c9283685feec7d69af75fb0e858d5e7378f33fe4fc699383b2916ab9273e03c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.3", ] [[package]] name = "regex" -version = "1.12.4" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" dependencies = [ "aho-corasick", "memchr", @@ -3727,9 +3753,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.14" +version = "0.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad" dependencies = [ "aho-corasick", "memchr", @@ -3847,9 +3873,9 @@ dependencies = [ [[package]] name = "rmcp" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0810a9f717d9828f475fe1f629f4c305c8464b7f496c3a854b58d29e65f4058e" +checksum = "1d1f571c72940a19d9532fe52dbea8bc9912bf1d766c2970bb824056b86f3f59" dependencies = [ "async-trait", "base64 0.22.1", @@ -3861,14 +3887,14 @@ dependencies = [ "http-body-util", "pastey 0.2.3", "pin-project-lite", - "rand 0.10.1", + "rand 0.10.2", "reqwest 0.13.4", "rmcp-macros", "schemars", "serde", "serde_json", "sse-stream", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", "tokio-stream", "tokio-util", @@ -3879,15 +3905,15 @@ dependencies = [ [[package]] name = "rmcp-macros" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6aefac48c364756e97f04c0401ba3231e8607882c7c1d92da0437dc16307904d" +checksum = "1aad0035b69380782d78ea95b508327e6deaa2235909053e596eea8f27b5e1d5" dependencies = [ "darling 0.23.0", "proc-macro2", "quote", "serde_json", - "syn", + "syn 2.0.119", ] [[package]] @@ -3918,9 +3944,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustc-hash" -version = "2.1.2" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" [[package]] name = "rustc_version" @@ -3937,7 +3963,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "errno", "libc", "linux-raw-sys 0.4.15", @@ -3950,7 +3976,7 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "errno", "libc", "linux-raw-sys 0.12.1", @@ -3959,9 +3985,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.40" +version = "0.23.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +checksum = "3c54fcab019b409d04215d3a17cb438fd7fbf192ee61461f20f4fe18704bc138" dependencies = [ "aws-lc-rs", "log", @@ -3987,9 +4013,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.14.1" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046" dependencies = [ "web-time", "zeroize", @@ -4036,9 +4062,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" [[package]] name = "ryu" @@ -4048,13 +4074,15 @@ checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "safetensors" -version = "0.7.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "675656c1eabb620b921efea4f9199f97fc86e36dd6ffd1fbbe48d0f59a4987f5" +checksum = "79b079b829cb27a1c3c374341345ed2e8b2c0c839034522cee576c140bd7f846" dependencies = [ "hashbrown 0.16.1", + "libc", "serde", "serde_json", + "tempfile", ] [[package]] @@ -4098,7 +4126,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn", + "syn 2.0.119", ] [[package]] @@ -4113,7 +4141,7 @@ version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "core-foundation 0.10.1", "core-foundation-sys", "libc", @@ -4138,9 +4166,9 @@ checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "serde" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" dependencies = [ "serde_core", "serde_derive", @@ -4148,22 +4176,22 @@ dependencies = [ [[package]] name = "serde_core" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.3", ] [[package]] @@ -4174,14 +4202,14 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] name = "serde_json" -version = "1.0.150" +version = "1.0.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" dependencies = [ "indexmap", "itoa", @@ -4257,7 +4285,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" dependencies = [ "libc", - "mio 1.2.1", + "mio 1.2.2", "signal-hook", ] @@ -4273,15 +4301,15 @@ dependencies = [ [[package]] name = "simd-adler32" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" [[package]] name = "simd_cesu8" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" dependencies = [ "rustc_version", "simdutf8", @@ -4325,15 +4353,15 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.15.1" +version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" [[package]] name = "socket2" -version = "0.6.4" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" dependencies = [ "libc", "windows-sys 0.61.2", @@ -4364,9 +4392,9 @@ dependencies = [ [[package]] name = "sse-stream" -version = "0.2.3" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3962b63f038885f15bce2c6e02c0e7925c072f1ac86bb60fd44c5c6b762fb72" +checksum = "c123f296ade4ec4b8b0f6162116e6629f5146922ca5ab40ca9d3c2e73ab4761e" dependencies = [ "bytes", "futures-util", @@ -4418,7 +4446,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn", + "syn 2.0.119", ] [[package]] @@ -4435,9 +4463,20 @@ checksum = "a7973cce6668464ea31f176d85b13c7ab3bba2cb3b77a2ed26abd7801688010a" [[package]] name = "syn" -version = "2.0.117" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" dependencies = [ "proc-macro2", "quote", @@ -4470,7 +4509,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -4493,7 +4532,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "core-foundation 0.9.4", "system-configuration-sys", ] @@ -4662,7 +4701,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.4.2", + "getrandom 0.4.3", "once_cell", "rustix 1.1.4", "windows-sys 0.61.2", @@ -4679,11 +4718,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.18" +version = "2.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" dependencies = [ - "thiserror-impl 2.0.18", + "thiserror-impl 2.0.19", ] [[package]] @@ -4694,25 +4733,25 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] name = "thiserror-impl" -version = "2.0.18" +version = "2.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.3", ] [[package]] name = "thread_local" -version = "1.1.9" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" dependencies = [ "cfg-if", ] @@ -4733,12 +4772,11 @@ dependencies = [ [[package]] name = "time" -version = "0.3.47" +version = "0.3.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +checksum = "3e1d5e639ff6bab73cb6885cc7e7b1de96c3f32c68ec55f3952614bec1092244" dependencies = [ "deranged", - "itoa", "num-conv", "powerfmt", "serde_core", @@ -4748,15 +4786,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" [[package]] name = "time-macros" -version = "0.2.27" +version = "0.2.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" dependencies = [ "num-conv", "time-core", @@ -4784,9 +4822,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" dependencies = [ "tinyvec_macros", ] @@ -4816,7 +4854,7 @@ dependencies = [ "monostate", "onig", "paste", - "rand 0.9.4", + "rand 0.9.5", "rayon", "rayon-cond", "regex", @@ -4824,7 +4862,7 @@ dependencies = [ "serde", "serde_json", "spm_precompiled", - "thiserror 2.0.18", + "thiserror 2.0.19", "unicode-normalization-alignments", "unicode-segmentation", "unicode_categories", @@ -4832,13 +4870,13 @@ dependencies = [ [[package]] name = "tokio" -version = "1.52.3" +version = "1.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" dependencies = [ "bytes", "libc", - "mio 1.2.1", + "mio 1.2.2", "parking_lot", "pin-project-lite", "signal-hook-registry", @@ -4849,13 +4887,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.7.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +checksum = "6328af13490e73a9b4694030fafd93f8c8c6a9dede33e821c3fc63eddf8042ba" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -4880,9 +4918,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.18" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b" dependencies = [ "futures-core", "pin-project-lite", @@ -4891,14 +4929,15 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.18" +version = "0.7.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" dependencies = [ "bytes", "futures-core", "futures-sink", "futures-util", + "libc", "pin-project-lite", "tokio", ] @@ -4925,7 +4964,7 @@ version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "bytes", "futures-util", "http", @@ -4970,7 +5009,7 @@ checksum = "050686193eb999b4bb3bc2acfa891a13da00f79734704c4b8b4ef1a10b368a3c" dependencies = [ "crossbeam-channel", "symlink", - "thiserror 2.0.18", + "thiserror 2.0.19", "time", "tracing-subscriber", ] @@ -4983,7 +5022,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -5040,9 +5079,9 @@ dependencies = [ [[package]] name = "tree-sitter" -version = "0.26.9" +version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dab76d0b724ba557954125188cf0633a1ca43199ced82d95c7b9c32cc3de1f3" +checksum = "af1c71c1c4cc0920b20d6b0f6572e7682cd07a6a2faec71067a31fa394c586df" dependencies = [ "cc", "regex", @@ -5274,12 +5313,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - [[package]] name = "unicode_categories" version = "0.1.1" @@ -5338,7 +5371,7 @@ dependencies = [ "ureq-proto", "utf8-zero", "webpki-root-certs", - "webpki-roots 1.0.7", + "webpki-roots 1.0.9", ] [[package]] @@ -5391,11 +5424,11 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.23.3" +version = "1.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7" +checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" dependencies = [ - "getrandom 0.4.2", + "getrandom 0.4.3", "js-sys", "serde_core", "wasm-bindgen", @@ -5457,27 +5490,18 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.3+wasi-0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" -dependencies = [ - "wit-bindgen 0.57.1", -] - -[[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +version = "1.0.4+wasi-0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" dependencies = [ - "wit-bindgen 0.51.0", + "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.123" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a254a4b10c19a76f09a27640e7ffbf9bc30bf67e16a3bf28aaefa4920fe81563" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" dependencies = [ "cfg-if", "once_cell", @@ -5488,9 +5512,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.73" +version = "0.4.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54568702fabf5d4849ce2b90fadfa64168a097eaf4b351ce9df8b687a0086aaf" +checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" dependencies = [ "js-sys", "wasm-bindgen", @@ -5498,9 +5522,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.123" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24a40fc75b0ec6f3746ceb10d36f53a93dcd68a93b11b6445983945d79eba0dc" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -5508,48 +5532,26 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.123" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "908f34bd9b9ce3d4caf07b72dfab63d61504d156856c6bd3cd87fa350cf3985b" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 2.0.119", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.123" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7acbf7616c27b194bbb550bf77ed0c2c3e5b7fd1260a93082b95fb7f47959b92" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" dependencies = [ "unicode-ident", ] -[[package]] -name = "wasm-encoder" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" -dependencies = [ - "leb128fmt", - "wasmparser", -] - -[[package]] -name = "wasm-metadata" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" -dependencies = [ - "anyhow", - "indexmap", - "wasm-encoder", - "wasmparser", -] - [[package]] name = "wasm-streams" version = "0.4.2" @@ -5576,23 +5578,11 @@ dependencies = [ "web-sys", ] -[[package]] -name = "wasmparser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" -dependencies = [ - "bitflags 2.13.0", - "hashbrown 0.15.5", - "indexmap", - "semver", -] - [[package]] name = "web-sys" -version = "0.3.100" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e0871acf327f283dc6da28a1696cdc64fb355ba9f935d052021fa77f35cce69" +checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" dependencies = [ "js-sys", "wasm-bindgen", @@ -5610,9 +5600,9 @@ dependencies = [ [[package]] name = "webpki-root-certs" -version = "1.0.7" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c" +checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b" dependencies = [ "rustls-pki-types", ] @@ -5623,14 +5613,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "webpki-roots 1.0.7", + "webpki-roots 1.0.9", ] [[package]] name = "webpki-roots" -version = "1.0.7" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" +checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" dependencies = [ "rustls-pki-types", ] @@ -5725,7 +5715,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -5736,7 +5726,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -6024,100 +6014,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" -[[package]] -name = "wit-bindgen" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" -dependencies = [ - "wit-bindgen-rust-macro", -] - [[package]] name = "wit-bindgen" version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" -[[package]] -name = "wit-bindgen-core" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" -dependencies = [ - "anyhow", - "heck", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" -dependencies = [ - "anyhow", - "heck", - "indexmap", - "prettyplease", - "syn", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" -dependencies = [ - "anyhow", - "bitflags 2.13.0", - "indexmap", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" -dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] - [[package]] name = "writeable" version = "0.6.3" @@ -6155,28 +6057,28 @@ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", "synstructure", ] [[package]] name = "zerocopy" -version = "0.8.52" +version = "0.8.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f" +checksum = "b5a105cd7b140f6eeec8acff2ea38135d3cab283ada58540f629fe51e46696eb" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.52" +version = "0.8.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" +checksum = "0fe976fb70c78cd64cccfe3a6fc142244e8a77b70959b30faf9d0ac37ee228eb" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -6196,15 +6098,15 @@ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", "synstructure", ] [[package]] name = "zeroize" -version = "1.8.2" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" [[package]] name = "zerotrie" @@ -6236,14 +6138,14 @@ checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] name = "zmij" -version = "1.0.21" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" [[package]] name = "zstd" diff --git a/Cargo.toml b/Cargo.toml index 9b3eebe0..3175657f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "codesearch" -version = "1.1.30" +version = "1.1.31" edition = "2021" authors = ["codesearch contributors"] license = "Apache-2.0" @@ -99,11 +99,16 @@ arroy = "0.5" heed = "0.20" bincode = "1.3" rand = "0.8" -rmcp = { version = "1.5.0", features = ["server", "client", "transport-io", "transport-streamable-http-server", "transport-streamable-http-client-reqwest", "macros"] } +# Bumped floor from 1.5.0 to 1.8.0 for CVE patches (Aikido group: rmcp priority 82, 3 CVEs). +# v2.x is available but is a breaking major bump — deferred. +rmcp = { version = "1.8.0", features = ["server", "client", "transport-io", "transport-streamable-http-server", "transport-streamable-http-client-reqwest", "macros"] } schemars = { version = "1.1.0", features = ["derive"] } reqwest = { version = "0.13", default-features = false, features = ["json", "rustls"] } sysinfo = { version = "0.38.4", default-features = true } +[target.'cfg(unix)'.dependencies] +libc = "0.2" + [dev-dependencies] criterion = { version = "0.5", features = ["html_reports"] } tempfile = "3.13" diff --git a/README.md b/README.md index 846ea84a..37e55351 100644 --- a/README.md +++ b/README.md @@ -117,6 +117,21 @@ codesearch index /path/to/my-project --force `codesearch index add` is intended to be run from inside the repo you want to register — pass the path explicitly if launched from elsewhere. First-time indexing takes 2–5 minutes; subsequent runs are incremental (10–30s) and branch switches re-index automatically. Use `codesearch index list/rm/prune` to manage registrations (see [Serve Mode](#serve-mode-multi-repo)). +### Embedding model + +The default quantized MiniLM model favors startup speed and a small download. For +multilingual text and notes, EmbeddingGemma 300M is available as a quantized ONNX +model with retrieval-specific query and document prompts: + +```bash +codesearch --model embeddinggemma-q4 index /path/to/notes --force +``` + +Changing models requires a full reindex because embedding dimensions and vector +spaces are model-specific. Keep the same model selected for later indexing runs. +Search rejects a `--model` value that differs from the indexed model and points +to the required `--force` rebuild instead of mixing incompatible vector spaces. + ## MCP Configuration codesearch connects to AI agents via MCP. Two modes: @@ -535,6 +550,8 @@ In the `codesearch serve` TUI, mounts appear in **italic/cyan**, distinguishing | `CODESEARCH_SERVE_PORT` | Serve mode port (default: 39725) | | `CODESEARCH_SERVE_API_KEY` | API key for management endpoints + all endpoints when serve binds to a non-localhost address (unset = no auth) | | `CODESEARCH_ALLOWED_ROOTS` | Semicolon-separated allowed roots for repo registration (unset = all allowed) | +| `CODESEARCH_ALLOWED_HOSTS` | Comma-separated hostname allowlist for the MCP streamable-HTTP transport (unset = loopback only: `localhost`, `127.0.0.1`, `::1`). Set this to your container/service hostname when serve runs behind a container network or reverse proxy — see [Security](#security). | +| `CODESEARCH_DISABLE_HOST_VALIDATION` | `1`/`true` disables the MCP transport's Host-header allowlist entirely (DNS-rebinding protection off). Only safe behind a reverse proxy/firewall that already restricts inbound Host headers — see [Security](#security). | | `CODESEARCH_MCP_MODE` | MCP mode: auto, client, local | | `CODESEARCH_REPOS_CONFIG` | Path to repos.json | | `CODESEARCH_REPO_IDLE_TIMEOUT_SECS` | Idle eviction timeout (default: 1800) | @@ -612,6 +629,29 @@ When `codesearch serve` is exposed beyond a single trusted user (shared dev mach Both are backward compatible: unset means no restriction (on a localhost bind). +### MCP transport host allowlist (DNS-rebinding protection) + +The MCP streamable-HTTP transport (via `rmcp`) validates the incoming `Host` header against an allowlist to defend against DNS-rebinding attacks. By default this allowlist is **loopback-only** (`localhost`, `127.0.0.1`, `::1`), which rejects requests carrying a container hostname or service-discovery name — a common trip-up in containerised/orchestrated deployments (Docker, Kubernetes, etc.) where the client connects via a non-loopback Host header. + +- **`CODESEARCH_ALLOWED_HOSTS`** — comma-separated list of extra allowed hostnames (e.g. `codesearch-serve,codesearch-serve.internal`), replacing the loopback-only default. Prefer this over disabling validation. +- **`CODESEARCH_DISABLE_HOST_VALIDATION`** — set to `1` or `true` to disable Host-header validation entirely. This removes the DNS-rebinding protection outright; only use it when serve is already fenced off by a reverse proxy or network policy that restricts which Host headers can reach it. + +Precedence: disable > custom allowlist > default (loopback-only). + +### Hardening against path traversal and injection + +Beyond the access-control gates above, codesearch applies several defense-in-depth mitigations at the filesystem and CLI boundary: + +- Project-path resolution (`index`, repo registration) fails fast on an unresolvable/malformed path instead of silently falling back to the raw, unvalidated input. +- The `.NET` symbol-helper CLI (`scip-csharp`) canonicalizes every path argument (`--solution`, `--project`, `--output`, `--symbols-file`) before use. +- Registering a project root that is itself a VCS/build-artifact directory (`.git`, `.svn`, `node_modules`, etc.) is rejected, preventing accidental indexing/exposure of internal VCS metadata. +- Terminal output (search results, sync/reindex logs) strips ANSI/control-sequence injection from indexed file content before printing, so a maliciously crafted file can't manipulate the user's terminal. +- On Unix, path-cache keys no longer collapse a literal backslash in a filename with a path separator (a Windows-only normalization rule is now gated to Windows). + +### Operational note: file-descriptor limits under process supervisors + +`codesearch serve`'s file-descriptor demand scales with the number of registered repos (each warm repo holds LMDB + full-text-index + file-watcher handles). Under a process supervisor with a low default open-file limit (notably **macOS launchd**, default soft `ulimit -n 256`), a large repo count can silently exhaust file descriptors: `accept()` then fails with `EMFILE` and the daemon looks alive to its supervisor while refusing new connections. Serve now raises its own soft `RLIMIT_NOFILE` to the hard limit at startup (Unix only) and logs a warning if the effective limit still looks insufficient for the registered repo count — but if you see repeated `EMFILE`/"Too many open files" in the logs, raise the **hard** limit for the service (e.g. launchd `SoftResourceLimits`/`HardResourceLimits`, systemd `LimitNOFILE=`, or `ulimit -n` in the service's environment). + ### Federation security model Federation is **operator-to-operator**, not end-user-facing. The only inputs that decide *where* requests go and *which key* they carry are the peer entries you register locally with `codesearch remote add` (stored in `~/.codesearch/repos.json`). No search query, MCP argument, or remote response ever becomes a request target or selects a key. diff --git a/helpers/csharp/OutputWriter.cs b/helpers/csharp/OutputWriter.cs index 1d8761bc..05b664c2 100644 --- a/helpers/csharp/OutputWriter.cs +++ b/helpers/csharp/OutputWriter.cs @@ -17,6 +17,7 @@ public static class OutputWriter public static async Task WriteAsync(ScipIndex index, string outputPath) { + outputPath = CanonicalizeOutputPath(outputPath); var dir = Path.GetDirectoryName(outputPath); if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir)) Directory.CreateDirectory(dir); @@ -28,6 +29,7 @@ public static async Task WriteAsync(ScipIndex index, string outputPath) /// Write find-refs output for the `find-refs` subcommand. public static async Task WriteRefsAsync(FindRefsOutput output, string outputPath) { + outputPath = CanonicalizeOutputPath(outputPath); var dir = Path.GetDirectoryName(outputPath); if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir)) Directory.CreateDirectory(dir); @@ -39,6 +41,7 @@ public static async Task WriteRefsAsync(FindRefsOutput output, string outputPath /// Write batch find-refs output for the `batch-find-refs` subcommand. public static async Task WriteBatchRefsAsync(BatchFindRefsOutput output, string outputPath) { + outputPath = CanonicalizeOutputPath(outputPath); var dir = Path.GetDirectoryName(outputPath); if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir)) Directory.CreateDirectory(dir); @@ -46,4 +49,20 @@ public static async Task WriteBatchRefsAsync(BatchFindRefsOutput output, string await using var stream = File.Create(outputPath); await JsonSerializer.SerializeAsync(stream, output, Options).ConfigureAwait(false); } + + /// + /// Canonicalizes and validates an output path before any File.Create call. + /// + /// SECURITY: Defense-in-depth against path traversal (Aikido group 30640677). + /// Callers in Program.cs already validate via RequireValidPath, but + /// this guard ensures OutputWriter remains safe if a new code path bypasses + /// the CLI parser (e.g. a future unit test calling WriteAsync directly with + /// an arbitrary string). resolves relative + /// segments (../..) and rejects malformed inputs. + /// + private static string CanonicalizeOutputPath(string outputPath) + { + ArgumentException.ThrowIfNullOrWhiteSpace(outputPath); + return Path.GetFullPath(outputPath); + } } diff --git a/helpers/csharp/Program.cs b/helpers/csharp/Program.cs index 1fa24c62..a4009e4e 100644 --- a/helpers/csharp/Program.cs +++ b/helpers/csharp/Program.cs @@ -564,6 +564,53 @@ private static bool TryRegisterMsBuild([System.Diagnostics.CodeAnalysis.NotNullW return args[++i]; } + /// + /// Reads the next arg value and validates it as a filesystem path. + /// + /// SECURITY: All CLI path arguments must go through this helper instead of + /// . canonicalizes + /// the path (collapsing "..", resolving relative segments, rejecting + /// malformed inputs), which prevents path-traversal attacks where a + /// crafted argument could read or write outside expected directories + /// (Aikido group 30640677). The .NET helper is invoked by the Rust parent + /// process; this is defense-in-depth, not the primary boundary. + /// + /// If true, the path must point to an existing file. + /// The canonicalized absolute path, or null + stderr message on failure. + private static string? RequireValidPath(string[] args, ref int i, string flag, bool mustExist) + { + var raw = RequireValue(args, ref i, flag); + if (raw is null) return null; + + if (string.IsNullOrWhiteSpace(raw)) + { + Console.Error.WriteLine($"{flag} must not be empty or whitespace"); + return null; + } + + string full; + try + { + // GetFullPath normalizes separators, resolves relative segments + // (../..), and rejects malformed inputs. This is the central + // path-traversal defense for CLI args. + full = Path.GetFullPath(raw); + } + catch (Exception ex) when (ex is ArgumentException or PathTooLongException or NotSupportedException) + { + Console.Error.WriteLine($"{flag}: invalid path '{raw}': {ex.Message}"); + return null; + } + + if (mustExist && !File.Exists(full)) + { + Console.Error.WriteLine($"{flag}: file not found: {full}"); + return null; + } + + return full; + } + private static (string? SolutionPath, string? ProjectPath, string OutputPath, string? ProjectFilter)? ParseIndexArgs(string[] args) { @@ -577,15 +624,15 @@ private static (string? SolutionPath, string? ProjectPath, string OutputPath, st switch (args[i]) { case "--solution": - solutionPath = RequireValue(args, ref i, "--solution"); + solutionPath = RequireValidPath(args, ref i, "--solution", mustExist: true); if (solutionPath is null) return null; break; case "--project": - projectPath = RequireValue(args, ref i, "--project"); + projectPath = RequireValidPath(args, ref i, "--project", mustExist: true); if (projectPath is null) return null; break; case "--output": - outputPath = RequireValue(args, ref i, "--output"); + outputPath = RequireValidPath(args, ref i, "--output", mustExist: false); if (outputPath is null) return null; break; case "--filter-project": @@ -626,7 +673,7 @@ private static (string SolutionPath, string Symbol, string OutputPath, string? P switch (args[i]) { case "--solution": - solutionPath = RequireValue(args, ref i, "--solution"); + solutionPath = RequireValidPath(args, ref i, "--solution", mustExist: true); if (solutionPath is null) return null; break; case "--symbol": @@ -634,7 +681,7 @@ private static (string SolutionPath, string Symbol, string OutputPath, string? P if (symbol is null) return null; break; case "--output": - outputPath = RequireValue(args, ref i, "--output"); + outputPath = RequireValidPath(args, ref i, "--output", mustExist: false); if (outputPath is null) return null; break; case "--filter-project": @@ -667,11 +714,11 @@ private static (string SolutionPath, IReadOnlyList Symbols, string Outpu switch (args[i]) { case "--solution": - solutionPath = RequireValue(args, ref i, "--solution"); + solutionPath = RequireValidPath(args, ref i, "--solution", mustExist: true); if (solutionPath is null) return null; break; case "--symbols-file": - symbolsFile = RequireValue(args, ref i, "--symbols-file"); + symbolsFile = RequireValidPath(args, ref i, "--symbols-file", mustExist: true); if (symbolsFile is null) return null; break; case "--symbols": @@ -679,7 +726,7 @@ private static (string SolutionPath, IReadOnlyList Symbols, string Outpu if (symbolsInline is null) return null; break; case "--output": - outputPath = RequireValue(args, ref i, "--output"); + outputPath = RequireValidPath(args, ref i, "--output", mustExist: false); if (outputPath is null) return null; break; default: @@ -694,11 +741,8 @@ private static (string SolutionPath, IReadOnlyList Symbols, string Outpu IReadOnlyList symbols; if (!string.IsNullOrEmpty(symbolsFile)) { - if (!File.Exists(symbolsFile)) - { - Console.Error.WriteLine($"batch-find-refs: symbols file not found: {symbolsFile}"); - return null; - } + // Existence + canonicalization already enforced by RequireValidPath + // above (mustExist: true). No redundant File.Exists here. symbols = File.ReadAllLines(symbolsFile) .Select(l => l.Trim()) .Where(l => !string.IsNullOrEmpty(l) && !l.StartsWith('#')) diff --git a/src/cache/file_meta.rs b/src/cache/file_meta.rs index d06173fe..7807abec 100644 --- a/src/cache/file_meta.rs +++ b/src/cache/file_meta.rs @@ -58,14 +58,37 @@ pub fn safe_canonicalize(path: &Path) -> std::io::Result { /// prefix (`\\?\C:\...`). Notify (FSW) events may use standard paths (`C:\...`). /// This function strips the UNC prefix and converts backslashes to forward slashes /// so that paths from different sources all map to the same key. +/// +/// **Platform behavior** (Aikido group 30641757, priority 46): +/// - **Windows**: backslash IS a path separator — converting it to `/` is +/// required for HashMap consistency across APIs. +/// - **Unix**: backslash is a **legal filename character** (not a separator). +/// A file literally named `foo\bar.rs` is distinct from `foo/bar.rs` (which +/// lives in subdirectory `foo`). Unconditionally converting `\` → `/` would +/// collapse these two unrelated files into one HashMap key, causing silent +/// metadata corruption (one file's chunks overwrite the other's). pub fn normalize_path(path: &Path) -> String { let s = path.to_string_lossy(); - s.trim_start_matches(r"\\?\").replace('\\', "/") + normalize_path_str(&s) } /// Normalize a path string (same logic as `normalize_path` but for `&str` input). +/// +/// See `normalize_path` for the platform-specific separator handling and +/// the Aikido 30641757 rationale. pub fn normalize_path_str(path: &str) -> String { - path.trim_start_matches(r"\\?\").replace('\\', "/") + let trimmed = path.trim_start_matches(r"\\?\"); + #[cfg(windows)] + { + trimmed.replace('\\', "/") + } + #[cfg(not(windows))] + { + // Backslash is a legal filename char on Unix — preserve it literally. + // UNC prefix is already stripped above (it's a no-op on Unix in + // practice, but defensive in case a Windows-style path string leaks in). + trimmed.to_string() + } } /// Normalize a filter path for prefix matching. @@ -451,6 +474,7 @@ mod tests { ); } + #[cfg(windows)] #[test] fn test_normalize_path_strips_unc_prefix() { let path = Path::new(r"\\?\C:\WorkArea\AI\codesearch\src\main.rs"); @@ -460,6 +484,7 @@ mod tests { ); } + #[cfg(windows)] #[test] fn test_normalize_path_converts_backslashes() { let path = Path::new(r"C:\WorkArea\AI\codesearch\src\main.rs"); @@ -479,6 +504,7 @@ mod tests { assert!(!result.starts_with(r"\\?\")); } + #[cfg(windows)] #[test] fn test_normalize_path_str_strips_unc() { assert_eq!(normalize_path_str(r"\\?\C:\foo\bar.rs"), "C:/foo/bar.rs"); @@ -491,6 +517,25 @@ mod tests { assert_eq!(normalize_path(path), "/home/user/project/src/main.rs"); } + /// Aikido 30641757 (priority 46): on Unix, a file whose name literally + /// contains a backslash (`foo\bar.rs`) is distinct from a file in a + /// subdirectory (`foo/bar.rs`). Both must NOT collapse to the same key. + #[cfg(not(windows))] + #[test] + fn test_normalize_path_preserves_unix_backslash_filenames() { + // Subdirectory file — forward slash is the separator. + let subdir = normalize_path(Path::new("foo/bar.rs")); + // Literal-backslash filename — backslash is part of the name on Unix. + let literal = normalize_path(Path::new("foo\\bar.rs")); + assert_ne!( + subdir, literal, + "Unix must NOT collapse `foo/bar.rs` and `foo\\bar.rs` into the same key" + ); + assert_eq!(subdir, "foo/bar.rs"); + assert_eq!(literal, "foo\\bar.rs"); + } + + #[cfg(windows)] #[test] fn test_normalize_path_mixed_separators() { // Mixed separators should be normalized to forward slashes @@ -498,6 +543,7 @@ mod tests { assert_eq!(normalize_path(path), "C:/Users/project/src/lib.rs"); } + #[cfg(windows)] #[test] fn test_normalize_path_str_mixed_separators() { assert_eq!( @@ -516,6 +562,7 @@ mod tests { ); } + #[cfg(windows)] #[test] fn test_normalize_path_deeply_nested() { // Deeply nested paths @@ -526,6 +573,7 @@ mod tests { ); } + #[cfg(windows)] #[test] fn test_normalize_path_consecutive_backslashes() { // Consecutive backslashes (edge case from file systems) @@ -533,6 +581,7 @@ mod tests { assert_eq!(normalize_path(path), "C://Double//Backslashes//file.rs"); } + #[cfg(windows)] #[test] fn test_migrate_paths_normalizes_keys() { let mut store = FileMetaStore::new("test-model".to_string(), 384); @@ -610,6 +659,7 @@ mod tests { // These test the exact bug patterns that have caused issues in production. // ========================================================================= + #[cfg(windows)] #[test] fn test_path_comparison_unc_vs_normal() { // UNC prefix (from Windows canonicalize) must match normal path @@ -618,6 +668,7 @@ mod tests { assert_eq!(unc, normal); } + #[cfg(windows)] #[test] fn test_path_comparison_backslash_vs_forward() { let backslash = normalize_path(Path::new(r"C:\WorkArea\src\main.rs")); @@ -625,6 +676,7 @@ mod tests { assert_eq!(backslash, forward); } + #[cfg(windows)] #[test] fn test_path_str_comparison_unc_vs_normal() { let unc = normalize_path_str(r"\\?\C:\WorkArea\src\main.rs"); @@ -632,6 +684,7 @@ mod tests { assert_eq!(unc, normal); } + #[cfg(windows)] #[test] fn test_path_comparison_stored_vs_walker() { // Simulates: FileMetaStore stored path vs FileWalker discovered path @@ -645,6 +698,7 @@ mod tests { ); } + #[cfg(windows)] #[test] fn test_path_filter_starts_with() { // Simulates: --filter-path src/ matching against stored paths @@ -657,6 +711,7 @@ mod tests { assert!(stored.starts_with(&filter_bs)); } + #[cfg(windows)] #[test] fn test_path_filter_with_unc_prefix() { // Agent sends UNC path as filter, stored paths are normalized @@ -683,6 +738,7 @@ mod tests { assert_eq!(from_path, from_str); } + #[cfg(windows)] #[test] fn test_normalize_path_relative_strips_project_root() { let root = normalize_path_str(r"C:\WorkArea\AI\codesearch"); @@ -709,6 +765,7 @@ mod tests { assert_eq!(normalize_filter_path("./src/"), "src/"); } + #[cfg(windows)] #[test] fn test_path_matches_filter_with_absolute_windows_path() { let root = normalize_path_str(r"C:\WorkArea\AI\codesearch"); diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 9f5a3c68..e29455d6 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -261,10 +261,7 @@ pub struct Cli { #[arg(long, global = true)] pub store: Option, - /// Embedding model to use (e.g., bge-small, minilm-l6-q, jina-code) - /// Available: minilm-l6, minilm-l6-q, minilm-l12, minilm-l12-q, paraphrase-minilm, - /// bge-small, bge-small-q, bge-base, nomic-v1, nomic-v1.5, nomic-v1.5-q, - /// jina-code, e5-multilingual, mxbai-large, modernbert-large + /// Embedding model to use (e.g., bge-small, jina-code, embeddinggemma-q4) #[arg(long, global = true)] pub model: Option, } @@ -871,6 +868,18 @@ async fn run_remote_reindex(peer_name: &str, alias: &str, force: bool, json: boo Ok(()) } +fn warn_if_heavier_model(model_type: ModelType) { + if model_type.is_heavier_than_default() { + crate::warn_print!( + "Warning: model '{}' produces {}-dimensional vectors (default: {}). \ + Expect higher vector-index RAM and disk usage.", + model_type.short_name(), + model_type.dimensions(), + ModelType::default().dimensions() + ); + } +} + pub async fn run(cancel_token: CancellationToken) -> Result<()> { let cli = Cli::parse(); @@ -881,9 +890,7 @@ pub async fn run(cancel_token: CancellationToken) -> Result<()> { "Unknown model: '{}'. Available models:", cli.model.as_deref().unwrap_or_default() ); - eprintln!(" minilm-l6, minilm-l6-q, minilm-l12, minilm-l12-q, paraphrase-minilm"); - eprintln!(" bge-small, bge-small-q, bge-base, nomic-v1, nomic-v1.5, nomic-v1.5-q"); - eprintln!(" jina-code, e5-multilingual, mxbai-large, modernbert-large"); + eprintln!(" {}", ModelType::valid_short_names()); std::process::exit(1); } @@ -918,6 +925,9 @@ pub async fn run(cancel_token: CancellationToken) -> Result<()> { if json { crate::output::set_quiet(true); } + if let Some(mt) = model_type { + warn_if_heavier_model(mt); + } let options = SearchOptions { max_results, per_file: if per_file == 0 { None } else { Some(per_file) }, @@ -927,7 +937,7 @@ pub async fn run(cancel_token: CancellationToken) -> Result<()> { sync, json, filter_path, - model_override: model_type.map(|mt| format!("{:?}", mt)), + model_override: model_type.map(|mt| mt.short_name().to_string()), vector_only, rrf_k: if rrf_k == 60.0 { None @@ -981,6 +991,9 @@ pub async fn run(cancel_token: CancellationToken) -> Result<()> { parsed }) .or(model_type); + if let Some(mt) = mt { + warn_if_heavier_model(mt); + } crate::index::add_to_index(add_path, global, mt, cancel_token.clone()) .await } @@ -1040,6 +1053,9 @@ pub async fn run(cancel_token: CancellationToken) -> Result<()> { if add || is_add_cmd { let effective_path = if is_add_cmd { None } else { path }; + if let Some(mt) = model_type { + warn_if_heavier_model(mt); + } crate::index::add_to_index( effective_path, global, @@ -1072,6 +1088,9 @@ pub async fn run(cancel_token: CancellationToken) -> Result<()> { ), } } else { + if let Some(mt) = model_type { + warn_if_heavier_model(mt); + } crate::index::index( path, dry_run, diff --git a/src/constants.rs b/src/constants.rs index df5c529c..531107bd 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -234,6 +234,31 @@ pub const SERVE_API_KEY_ENV: &str = "CODESEARCH_SERVE_API_KEY"; /// Example: `CODESEARCH_ALLOWED_ROOTS=/home/user/repos;/opt/code` pub const ALLOWED_ROOTS_ENV: &str = "CODESEARCH_ALLOWED_ROOTS"; +/// Environment variable to override the rmcp Streamable HTTP server's +/// `allowed_hosts` list (DNS-rebinding defence, GHSA-89vp-x53w-74fx). +/// +/// rmcp's default is loopback-only (`["localhost", "127.0.0.1", "::1"]`), +/// which rejects the container hostname in containerised deployments with +/// `WARN ... rejected request with disallowed Host header`. Setting this +/// env var to a comma-separated list of hostnames / `host:port` replaces +/// the default allowlist. +/// +/// When unset or empty, the rmcp default applies. See issue #149. +/// Example: `CODESEARCH_ALLOWED_HOSTS=codesearch.internal, codesearch:39725` +pub const ALLOWED_HOSTS_ENV: &str = "CODESEARCH_ALLOWED_HOSTS"; + +/// Environment variable to disable the rmcp Streamable HTTP server's +/// `Host` header validation entirely. +/// +/// **Dangerous**: turns off DNS-rebinding protection (GHSA-89vp-x53w-74fx). +/// Only set when codesearch runs behind a reverse proxy (nginx, Caddy, +/// Traefik) that itself validates the `Host` header against an allowlist. +/// Any other value leaves validation enabled. +/// +/// Accepts `1` or `true` (case-insensitive) to disable. +/// Example: `CODESEARCH_DISABLE_HOST_VALIDATION=1` +pub const DISABLE_HOST_VALIDATION_ENV: &str = "CODESEARCH_DISABLE_HOST_VALIDATION"; + /// Default base URL for connecting to a local `codesearch serve` instance. /// Used as the clap `--url` default and in `serve_base_url()`. /// diff --git a/src/db_discovery/repos.rs b/src/db_discovery/repos.rs index c557ba8a..c72f11f1 100644 --- a/src/db_discovery/repos.rs +++ b/src/db_discovery/repos.rs @@ -1311,6 +1311,10 @@ mod tests { } #[test] + #[cfg_attr( + windows, + ignore = "flaky on Windows: renaming a fresh .git tree races the AV/Search-indexer holding handles (os error 5); covered on Linux/macOS CI" + )] fn try_relocate_finds_renamed_parent() { let _serial = git_serial_lock(); let tmp = tempfile::tempdir().unwrap(); @@ -1334,6 +1338,10 @@ mod tests { } #[test] + #[cfg_attr( + windows, + ignore = "flaky on Windows: renaming a fresh .git tree races the AV/Search-indexer holding handles (os error 5); covered on Linux/macOS CI" + )] fn try_relocate_none_beyond_max_depth() { let _serial = git_serial_lock(); // Default max depth is 3. Bury the repo deeper than that below the @@ -1357,6 +1365,10 @@ mod tests { } #[test] + #[cfg_attr( + windows, + ignore = "flaky on Windows: renaming a fresh .git tree races the AV/Search-indexer holding handles (os error 5); covered on Linux/macOS CI" + )] fn relocate_missing_rewrites_only_moved_repos() { let _serial = git_serial_lock(); let tmp = tempfile::tempdir().unwrap(); @@ -1390,6 +1402,10 @@ mod tests { } #[test] + #[cfg_attr( + windows, + ignore = "flaky on Windows: renaming a directory races the AV/Search-indexer holding handles (os error 5); covered on Linux/macOS CI" + )] fn prune_stale_removes_unrelocatable_entries() { let _serial = git_serial_lock(); let tmp = tempfile::tempdir().unwrap(); @@ -1433,6 +1449,10 @@ mod tests { } #[test] + #[cfg_attr( + windows, + ignore = "flaky on Windows: renaming a fresh .git tree races the AV/Search-indexer holding handles (os error 5); covered on Linux/macOS CI" + )] fn try_relocate_finds_renamed_leaf() { let _serial = git_serial_lock(); let tmp = tempfile::tempdir().unwrap(); @@ -1467,6 +1487,10 @@ mod tests { } #[test] + #[cfg_attr( + windows, + ignore = "flaky on Windows: renaming a directory races the AV/Search-indexer holding handles (os error 5); covered on Linux/macOS CI" + )] fn try_relocate_none_without_recorded_remote() { let _serial = git_serial_lock(); let tmp = tempfile::tempdir().unwrap(); diff --git a/src/embed/batch.rs b/src/embed/batch.rs index 1f65d586..076234dc 100644 --- a/src/embed/batch.rs +++ b/src/embed/batch.rs @@ -1,4 +1,4 @@ -use super::embedder::FastEmbedder; +use super::embedder::{FastEmbedder, ModelType}; use crate::chunker::Chunk; use anyhow::Result; use std::sync::{Arc, Mutex}; @@ -89,13 +89,18 @@ impl BatchEmbedder { let total = chunks.len(); let _start = std::time::Instant::now(); let mut embedded_chunks = Vec::with_capacity(total); + let model_type = self + .embedder + .lock() + .map_err(|e| anyhow::anyhow!("Embedder mutex poisoned: {}", e))? + .model_type(); // Process in batches for chunk_batch in chunks.chunks(self.batch_size) { // Prepare texts for embedding let texts: Vec = chunk_batch .iter() - .map(|chunk| self.prepare_text(chunk)) + .map(|chunk| Self::prepare_text(chunk, model_type)) .collect(); // Generate embeddings @@ -103,7 +108,7 @@ impl BatchEmbedder { .embedder .lock() .map_err(|e| anyhow::anyhow!("Embedder mutex poisoned: {}", e))? - .embed_batch(texts)?; + .embed_documents(texts)?; // Combine chunks with embeddings for (chunk, embedding) in chunk_batch.iter().zip(embeddings) { @@ -117,12 +122,16 @@ impl BatchEmbedder { /// Embed a single chunk #[allow(dead_code)] // Reserved for single-chunk embedding pub fn embed_chunk(&mut self, chunk: Chunk) -> Result { - let text = self.prepare_text(&chunk); - let embedding = self + let mut embedder = self .embedder .lock() - .map_err(|e| anyhow::anyhow!("Embedder mutex poisoned: {}", e))? - .embed_one(&text)?; + .map_err(|e| anyhow::anyhow!("Embedder mutex poisoned: {}", e))?; + let text = Self::prepare_text(&chunk, embedder.model_type()); + let embedding = embedder + .embed_documents(vec![text])? + .into_iter() + .next() + .ok_or_else(|| anyhow::anyhow!("No embedding generated"))?; Ok(EmbeddedChunk::new(chunk, embedding)) } @@ -134,7 +143,7 @@ impl BatchEmbedder { /// - Signature (if available) /// - Docstring (if available) /// - Content - fn prepare_text(&self, chunk: &Chunk) -> String { + fn prepare_text(chunk: &Chunk, model_type: ModelType) -> String { let mut parts = Vec::new(); // Add context breadcrumbs (e.g., "File: main.rs > Class: Server") @@ -174,8 +183,10 @@ impl BatchEmbedder { } } - // Add main content - parts.push(format!("Code:\n{}", chunk.content)); + // Add main content. Only EmbeddingGemma distinguishes prose from code; + // existing models retain their historical input representation. + let label = model_type.content_label(&chunk.path); + parts.push(format!("{label}:\n{}", chunk.content)); parts.join("\n") } @@ -274,44 +285,27 @@ mod tests { } #[test] - #[ignore] // Requires model — flaky on CI without model cache - fn test_prepare_text() { - // Set a temporary cache directory to avoid creating .fastembed_cache in project root - let temp_dir = std::env::temp_dir().join("codesearch_test_cache"); - std::fs::create_dir_all(&temp_dir).ok(); - std::env::set_var( - "FASTEMBED_CACHE_DIR", - temp_dir.to_string_lossy().to_string(), - ); - - let embedder = Arc::new(Mutex::new(FastEmbedder::new().unwrap_or_else(|_| { - // For tests, create a mock if real embedder fails - panic!("Cannot create embedder in test"); - }))); - - let batch = BatchEmbedder::new(embedder); - + fn test_prepare_text_preserves_existing_models_and_labels_gemma_notes() { let mut chunk = Chunk::new( - "fn test() { println!(\"test\"); }".to_string(), + "A durable personal note".to_string(), 0, 1, - ChunkKind::Function, - "test.rs".to_string(), + ChunkKind::Block, + "notes.md".to_string(), ); - chunk.context = vec!["File: test.rs".to_string(), "Function: test".to_string()]; - chunk.signature = Some("fn test()".to_string()); - chunk.docstring = Some("/// Test function".to_string()); - - let text = batch.prepare_text(&chunk); + chunk.context = vec!["File: notes.md".to_string(), "Section: Ideas".to_string()]; - assert!(text.contains("Context: File: test.rs > Function: test")); - assert!(text.contains("Signature: fn test()")); - assert!(text.contains("Documentation: Test function")); - assert!(text.contains("Code:")); + let default_text = BatchEmbedder::prepare_text(&chunk, ModelType::default()); + let gemma_text = BatchEmbedder::prepare_text(&chunk, ModelType::EmbeddingGemma300MQ4); - // Clean up temp cache - let _ = std::fs::remove_dir_all(temp_dir); - std::env::remove_var("FASTEMBED_CACHE_DIR"); + assert_eq!( + default_text, + "Context: File: notes.md > Section: Ideas\nCode:\nA durable personal note" + ); + assert_eq!( + gemma_text, + "Context: File: notes.md > Section: Ideas\nText:\nA durable personal note" + ); } fn cosine_similarity(a: &[f32], b: &[f32]) -> f32 { diff --git a/src/embed/embedder.rs b/src/embed/embedder.rs index f554dc52..d18b0901 100644 --- a/src/embed/embedder.rs +++ b/src/embed/embedder.rs @@ -2,6 +2,8 @@ use anyhow::{anyhow, Result}; use fastembed::{EmbeddingModel as FastEmbedModel, InitOptions, TextEmbedding}; use ort::execution_providers::CPUExecutionProvider; +use crate::file::Language; + /// Available embedding models #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum ModelType { @@ -45,6 +47,8 @@ pub enum ModelType { MxbaiEmbedLargeV1, /// ModernBERT Embed Large - 1024 dimensions, latest architecture ModernBertEmbedLarge, + /// Quantized EmbeddingGemma 300M - 768 dimensions, multilingual retrieval + EmbeddingGemma300MQ4, } impl ModelType { @@ -70,6 +74,7 @@ impl ModelType { Self::MultilingualE5Small => FastEmbedModel::MultilingualE5Small, Self::MxbaiEmbedLargeV1 => FastEmbedModel::MxbaiEmbedLargeV1, Self::ModernBertEmbedLarge => FastEmbedModel::ModernBertEmbedLarge, + Self::EmbeddingGemma300MQ4 => FastEmbedModel::EmbeddingGemma300MQ4, } } @@ -89,7 +94,8 @@ impl ModelType { | Self::NomicEmbedTextV1 | Self::NomicEmbedTextV15 | Self::NomicEmbedTextV15Q - | Self::JinaEmbeddingsV2BaseCode => 768, + | Self::JinaEmbeddingsV2BaseCode + | Self::EmbeddingGemma300MQ4 => 768, // 1024 dimensions Self::BGELargeENV15 | Self::MxbaiEmbedLargeV1 | Self::ModernBertEmbedLarge => 1024, } @@ -113,6 +119,7 @@ impl ModelType { Self::MultilingualE5Small => "intfloat/multilingual-e5-small", Self::MxbaiEmbedLargeV1 => "mixedbread-ai/mxbai-embed-large-v1", Self::ModernBertEmbedLarge => "lightonai/modernbert-embed-large", + Self::EmbeddingGemma300MQ4 => "onnx-community/embeddinggemma-300m-ONNX (Q4)", } } @@ -125,6 +132,7 @@ impl ModelType { | Self::AllMiniLML12V2Q | Self::BGESmallENV15Q | Self::NomicEmbedTextV15Q + | Self::EmbeddingGemma300MQ4 ) } @@ -147,6 +155,7 @@ impl ModelType { Self::MultilingualE5Small => "e5-multilingual", Self::MxbaiEmbedLargeV1 => "mxbai-large", Self::ModernBertEmbedLarge => "modernbert-large", + Self::EmbeddingGemma300MQ4 => "embeddinggemma-q4", } } @@ -169,14 +178,14 @@ impl ModelType { Self::MultilingualE5Small, Self::MxbaiEmbedLargeV1, Self::ModernBertEmbedLarge, + Self::EmbeddingGemma300MQ4, ] } /// Comma-separated list of all valid model short names. /// - /// Single source of truth for the "valid models" message shown by the CLI - /// (`index add --model`) and the serve `POST /repos` error path, so the two - /// can never drift from the set `parse()` actually accepts. + /// Single source of truth for the "valid models" messages shown by the CLI + /// and the serve API, so they cannot drift from the set `parse()` accepts. pub fn valid_short_names() -> String { Self::all() .iter() @@ -204,9 +213,46 @@ impl ModelType { "e5-multilingual" | "multilinguale5small" => Some(Self::MultilingualE5Small), "mxbai-large" | "mxbaiembedlargev1" => Some(Self::MxbaiEmbedLargeV1), "modernbert-large" | "modernbertembedlarge" => Some(Self::ModernBertEmbedLarge), + "embeddinggemma-q4" | "embeddinggemma300mq4" => Some(Self::EmbeddingGemma300MQ4), _ => None, } } + + pub fn prepare_query(&self, text: &str) -> String { + match self { + Self::EmbeddingGemma300MQ4 => format!("task: search result | query: {text}"), + _ => text.to_string(), + } + } + + pub fn prepare_document(&self, text: &str) -> String { + match self { + Self::EmbeddingGemma300MQ4 => format!("title: none | text: {text}"), + _ => text.to_string(), + } + } + + /// Label prefix for a chunk's main content when building the embedding input. + /// + /// EmbeddingGemma benefits from distinguishing prose from source code, so + /// Markdown and plain-text files are labeled `Text`. Every other model keeps + /// the historical `Code` label unconditionally, leaving their embeddings — + /// and therefore existing indexes — byte-for-byte unchanged. + pub fn content_label(&self, path: &str) -> &'static str { + match self { + Self::EmbeddingGemma300MQ4 + if Language::from_path(std::path::Path::new(path)) == Language::Markdown => + { + "Text" + } + _ => "Code", + } + } + + /// Whether this model produces larger embeddings than the default model. + pub fn is_heavier_than_default(&self) -> bool { + self.dimensions() > Self::default().dimensions() + } } /// Fast embedding model using fastembed library @@ -315,6 +361,27 @@ impl FastEmbedder { .ok_or_else(|| anyhow!("No embedding generated")) } + pub fn embed_query(&mut self, text: &str) -> Result> { + let text = self.model_type.prepare_query(text); + self.embed_one(&text) + } + + pub fn embed_queries(&mut self, texts: Vec) -> Result>> { + let texts = texts + .into_iter() + .map(|text| self.model_type.prepare_query(&text)) + .collect(); + self.embed_batch(texts) + } + + pub fn embed_documents(&mut self, texts: Vec) -> Result>> { + let texts = texts + .into_iter() + .map(|text| self.model_type.prepare_document(&text)) + .collect(); + self.embed_batch(texts) + } + /// Get the dimensionality of embeddings pub fn dimensions(&self) -> usize { self.model_type.dimensions() @@ -361,6 +428,7 @@ mod tests { assert_eq!(ModelType::BGELargeENV15.dimensions(), 1024); assert_eq!(ModelType::MxbaiEmbedLargeV1.dimensions(), 1024); assert_eq!(ModelType::ModernBertEmbedLarge.dimensions(), 1024); + assert_eq!(ModelType::EmbeddingGemma300MQ4.dimensions(), 768); } #[test] @@ -383,12 +451,6 @@ mod tests { assert_eq!(model.dimensions(), 384); } - #[test] - fn test_all_models() { - let all = ModelType::all(); - assert_eq!(all.len(), 16); - } - #[test] fn test_short_name_round_trips_through_parse() { // Every model advertised by all() must parse back from its short_name. @@ -468,6 +530,10 @@ mod tests { ModelType::parse("jina-code"), Some(ModelType::JinaEmbeddingsV2BaseCode) ); + assert_eq!( + ModelType::parse("embeddinggemma-q4"), + Some(ModelType::EmbeddingGemma300MQ4) + ); assert_eq!(ModelType::parse("invalid"), None); } @@ -477,6 +543,50 @@ mod tests { assert!(ModelType::BGESmallENV15Q.is_quantized()); assert!(!ModelType::BGESmallENV15.is_quantized()); assert!(!ModelType::JinaEmbeddingsV2BaseCode.is_quantized()); + assert!(ModelType::EmbeddingGemma300MQ4.is_quantized()); + } + + #[test] + fn test_embeddinggemma_retrieval_prompts() { + let model = ModelType::EmbeddingGemma300MQ4; + assert_eq!( + model.prepare_query("Was wurde entschieden?"), + "task: search result | query: Was wurde entschieden?" + ); + assert_eq!( + model.prepare_document("Eine dauerhafte Notiz"), + "title: none | text: Eine dauerhafte Notiz" + ); + } + + #[test] + fn test_retrieval_prompts_leave_other_models_unchanged() { + let model = ModelType::AllMiniLML6V2Q; + assert_eq!(model.prepare_query("search text"), "search text"); + assert_eq!(model.prepare_document("document text"), "document text"); + } + + #[test] + fn test_content_label_only_distinguishes_prose_for_embeddinggemma() { + let gemma = ModelType::EmbeddingGemma300MQ4; + assert_eq!(gemma.content_label("notes.md"), "Text"); + assert_eq!(gemma.content_label("NOTES.MD"), "Text"); + assert_eq!(gemma.content_label("notes.markdown"), "Text"); + assert_eq!(gemma.content_label("notes.txt"), "Text"); + assert_eq!(gemma.content_label("src/lib.rs"), "Code"); + + let default = ModelType::default(); + assert_eq!(default.content_label("notes.md"), "Code"); + assert_eq!(default.content_label("notes.txt"), "Code"); + assert_eq!(default.content_label("src/lib.rs"), "Code"); + } + + #[test] + fn test_heavier_than_default_tracks_vector_dimensions() { + assert!(!ModelType::default().is_heavier_than_default()); + assert!(!ModelType::MultilingualE5Small.is_heavier_than_default()); + assert!(ModelType::EmbeddingGemma300MQ4.is_heavier_than_default()); + assert!(ModelType::ModernBertEmbedLarge.is_heavier_than_default()); } #[test] diff --git a/src/embed/mod.rs b/src/embed/mod.rs index 1c4a696b..24712800 100644 --- a/src/embed/mod.rs +++ b/src/embed/mod.rs @@ -172,7 +172,7 @@ impl EmbeddingService { let embedding = embedder_arc .lock() .map_err(|e| anyhow::anyhow!("Embedder mutex poisoned: {}", e))? - .embed_one(query)?; + .embed_query(query)?; // Store in cache self.query_cache.put(query, embedding.clone()); @@ -210,7 +210,7 @@ impl EmbeddingService { .lock() .map_err(|e| anyhow::anyhow!("Embedder mutex poisoned: {}", e))?; - let new_embeddings = embedder.embed_batch(queries_to_embed)?; + let new_embeddings = embedder.embed_queries(queries_to_embed)?; // Store in cache and add to results for (i, embedding) in new_embeddings.into_iter().enumerate() { diff --git a/src/file/mod.rs b/src/file/mod.rs index bd98f728..b98c3d1d 100644 --- a/src/file/mod.rs +++ b/src/file/mod.rs @@ -90,6 +90,27 @@ impl FileWalker { /// Walk files, returning detailed file information pub fn walk(&self) -> Result<(Vec, WalkStats)> { + // Security (Aikido group 30641794): refuse to walk a root whose own + // name is on the always-excluded list (e.g. `.git`, `.svn`, `node_modules`). + // The `filter_entry` closure below skips these names at depth >= 1, but + // it short-circuits on `depth() == 0` (the root). Without this guard, + // `codesearch index ./.git` would happily index every object, ref, and + // config file under `.git/`, exposing internal/sensitive metadata via + // search results. Fail fast at the walker's entry point so every caller + // (CLI `index`, HTTP `/repos`, `doctor`, `sync_database`, watcher) is + // covered uniformly. + if let Some(name) = self.root.file_name().and_then(|n| n.to_str()) { + if ALWAYS_EXCLUDED.contains(&name) { + anyhow::bail!( + "Refusing to index '{}' — this directory name is on the \ + always-excluded list (e.g. `.git`, `.svn`, `node_modules`). \ + Indexing it would expose internal/sensitive files via search. \ + Point the indexer at the parent project directory instead.", + self.root.display() + ); + } + } + let mut files = Vec::new(); let mut stats = WalkStats::new(); @@ -300,4 +321,34 @@ mod tests { assert_eq!(files.len(), 1); assert_eq!(files[0].path.file_name().unwrap(), "index.js"); } + + /// A root whose own name matches an `ALWAYS_EXCLUDED` entry (e.g. `.git`) + /// must be rejected at `walk()` time — otherwise the depth==0 short-circuit + /// in `filter_entry` would let every internal file be indexed. + /// Covers Aikido group 30641794. + #[test] + fn test_rejects_excluded_named_root() { + let parent = TempDir::new().unwrap(); + let git_root = parent.path().join(".git"); + fs::create_dir(&git_root).unwrap(); + fs::write(git_root.join("config"), "[core]").unwrap(); + fs::write(git_root.join("HEAD"), "ref: refs/heads/main").unwrap(); + + let walker = FileWalker::new(&git_root); + let err = walker.walk().unwrap_err(); + let msg = format!("{}", err); + assert!( + msg.contains("Refusing to index"), + "expected refusal message, got: {}", + msg + ); + assert!(msg.contains(".git"), "message should name the offender"); + + // Sanity: a non-excluded name in the same parent walks normally. + let ok_root = parent.path().join("real_project"); + fs::create_dir(&ok_root).unwrap(); + fs::write(ok_root.join("main.rs"), "fn main() {}").unwrap(); + let (files, _) = FileWalker::new(&ok_root).walk().unwrap(); + assert_eq!(files.len(), 1); + } } diff --git a/src/index/mod.rs b/src/index/mod.rs index ce9d36aa..efb19b84 100644 --- a/src/index/mod.rs +++ b/src/index/mod.rs @@ -88,8 +88,21 @@ fn get_db_path_smart( let project_path = path.as_deref().unwrap_or(Path::new(".")); // Canonicalize and strip any Windows UNC prefix (\\?\) via the central helper. - let canonical_path = - safe_canonicalize(project_path).unwrap_or_else(|_| PathBuf::from(project_path)); + // + // SECURITY: We deliberately propagate the error instead of falling back to + // the raw user-supplied path. The previous `unwrap_or_else` fallback silently + // bypassed canonicalization when the path did not exist or was inaccessible, + // which defeated every downstream `starts_with`/`join` containment check + // (Aikido group 30640695). Bailing here gives a clear error and guarantees + // every later comparison operates on a real, canonicalized absolute path. + let canonical_path = safe_canonicalize(project_path).map_err(|e| { + anyhow::anyhow!( + "Cannot resolve project path '{}': {}. \ + Ensure the path exists and is accessible before indexing.", + project_path.display(), + e + ) + })?; // Step 1: Handle --force flag — delete databases if force { @@ -512,7 +525,18 @@ pub async fn index_quiet( global: bool, cancel_token: CancellationToken, ) -> Result<()> { - index_with_options(path, false, force, global, None, true, cancel_token).await + index_quiet_with_model(path, force, global, None, cancel_token).await +} + +/// Index a repository quietly while selecting the model for a new index. +pub async fn index_quiet_with_model( + path: Option, + force: bool, + global: bool, + model: Option, + cancel_token: CancellationToken, +) -> Result<()> { + index_with_options(path, false, force, global, model, true, cancel_token).await } /// Internal index function with all options diff --git a/src/mcp/mod.rs b/src/mcp/mod.rs index f97f4fee..60820cca 100644 --- a/src/mcp/mod.rs +++ b/src/mcp/mod.rs @@ -52,6 +52,7 @@ mod tests { ); } + #[cfg(windows)] #[test] fn test_mcp_filter_matches_absolute_path_under_project_root() { let project_root = normalize_path_str(r"C:\WorkArea\AI\codesearch"); @@ -63,6 +64,22 @@ mod tests { )); } + // Unix counterpart: same logic, native (forward-slash) absolute paths. + // normalize_path_str deliberately does NOT rewrite '\' on Unix (backslash + // is a legal filename char — see file_meta.rs Aikido rationale), so the + // Windows-path variant above is meaningless here and is gated off. + #[cfg(unix)] + #[test] + fn test_mcp_filter_matches_absolute_path_under_project_root() { + let project_root = normalize_path_str("/work/codesearch"); + let filter = normalize_filter_path("src/"); + assert!(path_matches_filter( + "/work/codesearch/src/mcp/mod.rs", + &filter, + &project_root, + )); + } + #[test] fn test_mcp_filter_rejects_non_matching_path_under_project_root() { let project_root = normalize_path_str(r"C:\WorkArea\AI\codesearch"); @@ -83,6 +100,7 @@ mod tests { .collect() } + #[cfg(windows)] #[test] fn pick_filter_root_uses_routed_alias_root() { // serve single-project: the routed alias's own root, NOT the service @@ -111,6 +129,32 @@ mod tests { )); } + // Unix counterpart: native forward-slash paths (see cfg(windows) twin). + #[cfg(unix)] + #[test] + fn pick_filter_root_uses_routed_alias_root() { + let ar = roots(&[("myrepo", "/data/repos/myrepo")]); + let root = super::pick_filter_root( + "/data/repos/myrepo/src/foo.rs", + Some("myrepo"), + &ar, + "/some/other/hub/path", + ); + assert_eq!(root, normalize_path_str("/data/repos/myrepo")); + let filter = normalize_filter_path("src/"); + assert!(path_matches_filter( + "/data/repos/myrepo/src/foo.rs", + &filter, + &root + )); + let other = normalize_filter_path("tests/"); + assert!(!path_matches_filter( + "/data/repos/myrepo/src/foo.rs", + &other, + &root + )); + } + #[test] fn pick_filter_root_multi_picks_longest_matching_root() { // serve multi/group: no project_alias; choose the alias root the path @@ -390,6 +434,10 @@ mod tests { // === prefix_path_with_alias tests === + // Windows-only: backslash → '/' rewriting is a no-op on Unix by design + // (backslash is a legal Unix filename char). Forward-slash inputs are + // covered by test_path_prefix_no_alias / _empty_alias on all platforms. + #[cfg(windows)] #[test] fn test_path_prefix_windows_backslashes() { let result = @@ -414,6 +462,8 @@ mod tests { ); } + // Windows-only: mixed '/' and '\' only collapse to '/' on Windows. + #[cfg(windows)] #[test] fn test_path_prefix_mixed_separators() { let result = diff --git a/src/search/mod.rs b/src/search/mod.rs index 5a1ee085..3fcbf3eb 100644 --- a/src/search/mod.rs +++ b/src/search/mod.rs @@ -408,6 +408,14 @@ pub fn adapt_rrf_k(query: &str) -> (f64, f64) { /// Search the codebase pub async fn search(query: &str, path: Option, options: SearchOptions) -> Result<()> { let (db_path, project_path) = get_db_path(path.clone())?; + let requested_model = options + .model_override + .as_deref() + .map(|name| { + ModelType::parse(name) + .ok_or_else(|| anyhow::anyhow!("Unknown embedding model override '{name}'")) + }) + .transpose()?; if !db_path.exists() { if options.create_index { @@ -417,7 +425,8 @@ pub async fn search(query: &str, path: Option, options: SearchOptions) "🚀 No index found, creating one...".bright_cyan() )); let cancel_token = tokio_util::sync::CancellationToken::new(); - crate::index::index_quiet(path, false, false, cancel_token).await?; + crate::index::index_quiet_with_model(path, false, false, requested_model, cancel_token) + .await?; crate::output::print_info(format_args!("{}", "✅ Index created successfully!".green())); } else { println!("{}", "❌ No database found!".red()); @@ -437,21 +446,35 @@ pub async fn search(query: &str, path: Option, options: SearchOptions) // Read model metadata from database FIRST (needed for sync) let (model_type, dimensions, primary_language) = - if let Some(ref model_name) = options.model_override { - // User specified a model - use it (warning: may not match indexed data!) - let mt = ModelType::parse(model_name).unwrap_or_else(|| { - tracing::warn!( - "Unrecognized model override '{}', falling back to default model", - model_name - ); - ModelType::default() - }); - (mt, mt.dimensions(), None) - } else if let Some((model_name, dims, lang)) = read_metadata(&db_path) { + if let Some((model_name, dims, lang)) = read_metadata(&db_path) { // Use model from metadata if let Some(mt) = ModelType::parse(&model_name) { + if let Some(requested) = requested_model { + if requested != mt { + anyhow::bail!( + "Index uses embedding model '{}', but '--model {}' was requested. \ + Rebuild the index with `codesearch --model {} index {} --force` \ + before searching.", + mt.short_name(), + requested.short_name(), + requested.short_name(), + project_path.display() + ); + } + } (mt, dims, lang) } else { + if let Some(requested) = requested_model { + anyhow::bail!( + "Index metadata names unknown embedding model '{}', so '--model {}' \ + cannot be verified. Rebuild the index with \ + `codesearch --model {} index {} --force`.", + model_name, + requested.short_name(), + requested.short_name(), + project_path.display() + ); + } // Model name not recognized, fall back to default tracing::warn!( "Unrecognized model '{}' in database metadata, falling back to default model", @@ -463,6 +486,14 @@ pub async fn search(query: &str, path: Option, options: SearchOptions) ); (ModelType::default(), 384, None) } + } else if let Some(requested) = requested_model { + anyhow::bail!( + "Cannot verify '--model {}' because the index metadata is missing or invalid. \ + Rebuild the index with `codesearch --model {} index {} --force`.", + requested.short_name(), + requested.short_name(), + project_path.display() + ); } else { // No metadata, fall back to default (ModelType::default(), 384, None) @@ -962,7 +993,7 @@ pub async fn search(query: &str, path: Option, options: SearchOptions) let mut seen_files = std::collections::HashSet::new(); for result in &results { if !seen_files.contains(&result.path) { - println!("{}", result.path); + println!("{}", sanitize_for_terminal(&result.path)); seen_files.insert(result.path.clone()); } } @@ -972,7 +1003,10 @@ pub async fn search(query: &str, path: Option, options: SearchOptions) // Standard output println!("{}", "🔍 Search Results".bright_cyan().bold()); println!("{}", "=".repeat(60)); - println!("Query: \"{}\"", query.bright_yellow()); + println!( + "Query: \"{}\"", + sanitize_for_terminal(query).bright_yellow() + ); if let Some(pf) = options.per_file { println!( "Found {} results (showing up to {} per file)", @@ -1094,7 +1128,10 @@ fn sync_database(db_path: &Path, model_type: ModelType) -> Result<()> { } changes += 1; - println!(" 📝 {}", file.path.display()); + println!( + " 📝 {}", + sanitize_for_terminal(&file.path.display().to_string()) + ); // Delete old chunks if !old_chunk_ids.is_empty() { @@ -1124,7 +1161,7 @@ fn sync_database(db_path: &Path, model_type: ModelType) -> Result<()> { let deleted_files = file_meta.find_deleted_files(); for (path, chunk_ids) in &deleted_files { changes += 1; - println!(" 🗑️ {} (deleted)", path); + println!(" 🗑️ {} (deleted)", sanitize_for_terminal(path)); if !chunk_ids.is_empty() { store.delete_chunks(chunk_ids)?; } @@ -1144,6 +1181,77 @@ fn sync_database(db_path: &Path, model_type: ModelType) -> Result<()> { Ok(()) } +/// Strip ANSI escape sequences and terminal-control bytes from a string. +/// +/// Indexed content may contain CSI/OSC sequences (e.g. `\x1b[2J` clears the +/// screen, `\x1b[8m` hides text, `\x1b]0;...\x07` rewrites the window title). +/// If printed verbatim, the host terminal interprets them — enabling a range +/// of attacks from screen-clearing DoS to hidden-text obfuscation. This +/// helper strips: +/// * CSI sequences: `ESC [ ` +/// * OSC sequences: `ESC ] (BEL | ESC \\)` +/// * Single-char escape sequences: `ESC <0x40-0x5F>` +/// * Stray control characters except `\n` and `\t` +/// +/// Output is safe to feed into `Colorize` methods without risk of the inner +/// content breaking out of the color wrapper. Mitigates Aikido group 30641757 +/// (ANSI escape sequence injection in search output). +fn sanitize_for_terminal(s: &str) -> String { + let mut out = String::with_capacity(s.len()); + let mut chars = s.chars().peekable(); + while let Some(c) = chars.next() { + if c != '\x1b' { + if c == '\n' || c == '\t' || !c.is_control() { + out.push(c); + } + continue; + } + // ESC sequence — consume per ECMA-48 + match chars.peek().copied() { + None => break, + Some('[') => { + chars.next(); + while let Some(p) = chars.peek().copied() { + let code = p as u32; + if (0x30..=0x3f).contains(&code) || (0x20..=0x2f).contains(&code) { + chars.next(); + } else if (0x40..=0x7e).contains(&code) { + chars.next(); + break; + } else { + break; + } + } + } + Some(']') => { + chars.next(); + loop { + match chars.next() { + Some('\x07') => break, + Some('\x1b') => { + if matches!(chars.peek().copied(), Some('\\')) { + chars.next(); + } + break; + } + Some(_) => continue, + None => break, + } + } + } + Some(c2) => { + let code = c2 as u32; + if (0x40..=0x5f).contains(&code) { + chars.next(); + } + // ESC followed by something unexpected: drop the ESC, leave + // the next char to be processed normally on the next loop. + } + } + } + out +} + fn print_result( result: &crate::vectordb::SearchResult, show_file: bool, @@ -1152,20 +1260,22 @@ fn print_result( ) -> Result<()> { if show_file { println!("{}", "─".repeat(60)); - let file_display = format!("📄 {}", result.path); + let file_display = format!("📄 {}", sanitize_for_terminal(&result.path)); println!("{}", file_display.bright_green()); } // Show location and kind let location = format!( " Lines {}-{} • {}", - result.start_line, result.end_line, result.kind + result.start_line, + result.end_line, + sanitize_for_terminal(&result.kind) ); println!("{}", location.dimmed()); // Show signature if available if let Some(sig) = &result.signature { - println!(" {}", sig.bright_cyan()); + println!(" {}", sanitize_for_terminal(sig).bright_cyan()); } // Show score if requested @@ -1191,7 +1301,7 @@ fn print_result( // Show context if available if let Some(ctx) = &result.context { - println!(" Context: {}", ctx.dimmed()); + println!(" Context: {}", sanitize_for_terminal(ctx).dimmed()); } // Show content if requested @@ -1200,13 +1310,13 @@ fn print_result( if let Some(ctx_prev) = &result.context_prev { println!("\n {}:", "Context (before)".dimmed()); for line in ctx_prev.lines() { - println!(" │ {}", line.bright_black()); + println!(" │ {}", sanitize_for_terminal(line).bright_black()); } } println!("\n {}:", "Content".bright_yellow()); for line in result.content.lines().take(10) { - println!(" │ {}", line.dimmed()); + println!(" │ {}", sanitize_for_terminal(line).dimmed()); } if result.content.lines().count() > 10 { println!(" │ {}", "...".dimmed()); @@ -1216,15 +1326,26 @@ fn print_result( if let Some(ctx_next) = &result.context_next { println!("\n {}:", "Context (after)".dimmed()); for line in ctx_next.lines() { - println!(" │ {}", line.bright_black()); + println!(" │ {}", sanitize_for_terminal(line).bright_black()); } } } else { // Show a snippet - let snippet: String = result.content.lines().take(3).collect::>().join(" "); + let snippet: String = result + .content + .lines() + .take(3) + .map(sanitize_for_terminal) + .collect::>() + .join(" "); let snippet = if snippet.len() > 100 { - format!("{}...", &snippet[..100]) + // Truncate at the largest UTF-8 char boundary <= 100 bytes. + // Plain `&snippet[..100]` panics if byte 100 falls inside a + // multi-byte character (box-drawing separators, CJK, emoji) — + // see issue #148. + let cut = snippet.floor_char_boundary(100); + format!("{}...", &snippet[..cut]) } else { snippet }; @@ -1465,6 +1586,7 @@ mod tests { ); } + #[cfg(windows)] #[test] fn test_path_filter_matches_absolute_windows_path_under_root() { let project_root = normalize_path_str(r"C:\WorkArea\AI\codesearch"); @@ -1476,6 +1598,21 @@ mod tests { )); } + // Unix counterpart: native forward-slash absolute path. normalize_path_str + // intentionally leaves '\' untouched on Unix (see file_meta.rs Aikido + // rationale), so the Windows-path variant is gated off there. + #[cfg(unix)] + #[test] + fn test_path_filter_matches_absolute_unix_path_under_root() { + let project_root = normalize_path_str("/work/codesearch"); + let filter = normalize_filter_path("src/"); + assert!(path_matches_filter( + "/work/codesearch/src/index/mod.rs", + &filter, + &project_root, + )); + } + #[test] fn test_path_filter_rejects_non_matching_absolute_path_under_root() { let project_root = normalize_path_str(r"C:\WorkArea\AI\codesearch"); @@ -1493,4 +1630,95 @@ mod tests { let filter = normalize_filter_path("src/"); assert!(path_matches_filter("./src/lib.rs", &filter, &project_root)); } + + // ── sanitize_for_terminal ─────────────────────────────────────────────── + + #[test] + fn test_sanitize_strips_csi_clear_screen() { + // \x1b[2J = clear screen + assert_eq!(sanitize_for_terminal("hello\x1b[2Jworld"), "helloworld"); + } + + #[test] + fn test_sanitize_strips_csi_with_params() { + // \x1b[38;5;200m = set 256-color foreground + assert_eq!( + sanitize_for_terminal("\x1b[38;5;200mred\x1b[0m text"), + "red text" + ); + } + + #[test] + fn test_sanitize_strips_osc_bel_terminator() { + // \x1b]0;title\x07 = set window title, BEL terminator + assert_eq!(sanitize_for_terminal("a\x1b]0;title\x07b"), "ab"); + } + + #[test] + fn test_sanitize_strips_osc_st_terminator() { + // \x1b]0;title\x1b\\ = set window title, ST terminator + assert_eq!(sanitize_for_terminal("a\x1b]0;title\x1b\\b"), "ab"); + } + + #[test] + fn test_sanitize_strips_single_char_escape() { + // ESC M = Reverse Index (RI), in the 0x40-0x5F documented range + assert_eq!(sanitize_for_terminal("a\x1bM b"), "a b"); + } + + #[test] + fn test_sanitize_strips_control_chars_except_newline_tab() { + // NUL, BEL, backspace, vertical tab, form feed, CR — all stripped + assert_eq!( + sanitize_for_terminal("a\x00b\x07c\x08d\x0be\x0cf\rg"), + "abcdefg" + ); + // newline and tab preserved + assert_eq!(sanitize_for_terminal("a\nb\tc"), "a\nb\tc"); + } + + #[test] + fn test_sanitize_strips_back_to_back_escapes() { + // Two consecutive CSI sequences — both stripped + assert_eq!(sanitize_for_terminal("\x1b[2J\x1b[2Jcleared"), "cleared"); + } + + #[test] + fn test_sanitize_preserves_unicode() { + assert_eq!(sanitize_for_terminal("héllo → 世界 🦀"), "héllo → 世界 🦀"); + } + + #[test] + fn test_sanitize_preserves_empty_and_clean_strings() { + assert_eq!(sanitize_for_terminal(""), ""); + assert_eq!(sanitize_for_terminal("clean string"), "clean string"); + } + + #[test] + fn test_sanitize_truncated_escape_dropped_safely() { + // Truncated CSI at end of string — should not panic + assert_eq!(sanitize_for_terminal("text\x1b["), "text"); + // Truncated OSC at end of string + assert_eq!(sanitize_for_terminal("text\x1b]0;unterminated"), "text"); + // Lone ESC at end + assert_eq!(sanitize_for_terminal("text\x1b"), "text"); + } + + #[test] + fn test_byte_truncation_preserves_char_boundary() { + // Regression for issue #148: `&snippet[..100]` panicked when byte + // offset 100 fell inside a multi-byte character (box-drawing U+2500 + // in comment-art, CJK, emoji). 40 × U+2500 = 120 bytes, so byte 100 + // is inside char #34 (bytes 99..102). + let s: String = std::iter::repeat_n('─', 40).collect(); + assert!(s.len() > 100, "fixture must exceed 100 bytes"); + let cut = s.floor_char_boundary(100); + assert!(cut <= 100); + assert!(s.is_char_boundary(cut), "cut must land on a char boundary"); + let truncated = &s[..cut]; + // All chars are 3 bytes; cut must be a multiple of 3. + assert_eq!(cut % 3, 0); + assert_eq!(truncated.chars().count(), cut / 3); + // The pre-fix code (`&s[..100]`) would panic on this fixture. + } } diff --git a/src/serve/mod.rs b/src/serve/mod.rs index af3f08be..38817d19 100644 --- a/src/serve/mod.rs +++ b/src/serve/mod.rs @@ -35,12 +35,13 @@ use tracing::{info, warn}; use crate::cache::safe_canonicalize; use crate::constants::{ - ALLOWED_ROOTS_ENV, CHUNK_PATH, CSHARP_PREWARM_ENABLED_ENV, CSHARP_PREWARM_MAX_SYMBOLS, - CSHARP_SCIP_CONCURRENCY_DEFAULT, CSHARP_SCIP_CONCURRENCY_ENV, DB_DIR_NAME, DEFAULT_SERVE_PORT, - EXPLORE_PATH, FIND_PATH, HEALTHZ_PATH, HEALTH_PATH, LANG_CSHARP, MAX_INDEXING_SECS, - MAX_INDEXING_SECS_ENV, MCP_ENDPOINT_PATH, PERSIST_DEBOUNCE_SECS, REAPER_INTERVAL_SECS, - REMOTES_PATH, REPO_IDLE_TIMEOUT_ENV, REPO_IDLE_TIMEOUT_SECS, SEARCH_PATH, SERVE_API_KEY_ENV, - SERVE_PORT_ENV, STATUS_PATH, + ALLOWED_HOSTS_ENV, ALLOWED_ROOTS_ENV, CHUNK_PATH, CSHARP_PREWARM_ENABLED_ENV, + CSHARP_PREWARM_MAX_SYMBOLS, CSHARP_SCIP_CONCURRENCY_DEFAULT, CSHARP_SCIP_CONCURRENCY_ENV, + DB_DIR_NAME, DEFAULT_SERVE_PORT, DISABLE_HOST_VALIDATION_ENV, EXPLORE_PATH, FIND_PATH, + HEALTHZ_PATH, HEALTH_PATH, LANG_CSHARP, MAX_INDEXING_SECS, MAX_INDEXING_SECS_ENV, + MCP_ENDPOINT_PATH, PERSIST_DEBOUNCE_SECS, REAPER_INTERVAL_SECS, REMOTES_PATH, + REPO_IDLE_TIMEOUT_ENV, REPO_IDLE_TIMEOUT_SECS, SEARCH_PATH, SERVE_API_KEY_ENV, SERVE_PORT_ENV, + STATUS_PATH, }; use crate::db_discovery::repos::{config_dir, ReposConfig}; use crate::index::{CSharpRebuildNotifier, IndexManager, IndexingStatusCallback, SharedStores}; @@ -1507,7 +1508,7 @@ impl ServeState { tracing::warn!( "IndexManager init failed for '{}': {} - FSW not restarted, searches still work", alias, e - ); + ); } } } @@ -3735,6 +3736,168 @@ pub async fn run_tui_standalone(serve_url: String) -> Result<()> { /// Run the MCP serve mode. /// /// This is the entry point called from CLI when `codesearch serve` is invoked. +/// Extra fds reserved for everything that is not a repo store: +/// listener + accepted sockets, SSE sessions, log files, embedding +/// model files, federation clients. +#[cfg(unix)] +const FD_HEADROOM: u64 = 256; + +/// Rough per-repo fd demand: LMDB env + tantivy FTS segments + +/// file-watcher handles. Measured ~15-17 fds per warm repo on macOS; +/// 20 leaves margin for segment churn. +#[cfg(unix)] +const FDS_PER_REPO_ESTIMATE: u64 = 20; + +/// Raise the soft `RLIMIT_NOFILE` to the hard limit before opening +/// repo stores or binding the listener. +/// +/// serve's fd demand scales with registered repo count (LMDB + +/// tantivy + watcher handles per repo — ~1000 fds at 60 repos). +/// Under process supervisors the default soft limit is often 256 +/// (macOS launchd agents, some systemd/docker configs). Once the +/// process saturates that limit, `accept(2)` fails with `EMFILE` and +/// the axum accept loop retries silently — the daemon looks alive to +/// its supervisor while every new connection is refused or reset. +/// Raising soft → hard at startup is standard daemon practice +/// (nginx, envoy, postgres all do it) and turns a silent wedge into +/// an explicit, logged operator decision. +/// +/// Never fails the startup: on error we log and continue with the +/// inherited limit, then warn if it looks too small for the +/// registered repo count. +#[cfg(unix)] +fn raise_fd_limit(repo_count: usize) { + // SAFETY: getrlimit/setrlimit with a locally owned rlimit struct. + unsafe { + let mut lim = libc::rlimit { + rlim_cur: 0, + rlim_max: 0, + }; + if libc::getrlimit(libc::RLIMIT_NOFILE, &mut lim) != 0 { + warn!( + "Could not read RLIMIT_NOFILE ({}); continuing with inherited limit", + std::io::Error::last_os_error() + ); + return; + } + let before = lim.rlim_cur; + if lim.rlim_cur < lim.rlim_max { + // On macOS the kernel caps the effective per-process limit + // at kern.maxfilesperproc even when rlim_max is RLIM_INFINITY; + // clamp so setrlimit does not fail with EINVAL. + #[cfg(target_os = "macos")] + let target = { + let mut maxfiles: libc::c_int = 0; + let mut size = std::mem::size_of::(); + let name = std::ffi::CString::new("kern.maxfilesperproc").unwrap(); + if libc::sysctlbyname( + name.as_ptr(), + &mut maxfiles as *mut _ as *mut libc::c_void, + &mut size, + std::ptr::null_mut(), + 0, + ) == 0 + { + lim.rlim_max.min(maxfiles as libc::rlim_t) + } else { + lim.rlim_max + } + }; + #[cfg(not(target_os = "macos"))] + let target = lim.rlim_max; + + if target > lim.rlim_cur { + lim.rlim_cur = target; + if libc::setrlimit(libc::RLIMIT_NOFILE, &lim) != 0 { + warn!( + "Could not raise RLIMIT_NOFILE {} → {} ({}); continuing with inherited limit", + before, + target, + std::io::Error::last_os_error() + ); + lim.rlim_cur = before; + } else { + info!("Raised RLIMIT_NOFILE soft limit {} → {}", before, target); + } + } + } + + let estimated = (repo_count as u64) * FDS_PER_REPO_ESTIMATE + FD_HEADROOM; + // rlim_t width is platform-dependent (u64 on macOS/Linux glibc, + // but not guaranteed everywhere) — keep the explicit widening. + #[allow(clippy::unnecessary_cast)] + let soft = lim.rlim_cur as u64; + if soft < estimated { + warn!( + "⚠️ RLIMIT_NOFILE soft limit is {} but {} registered repos need an estimated {} fds \ + (LMDB + FTS + watcher handles per repo). When the limit is exhausted, accept(2) fails \ + with EMFILE and serve stops answering connections WITHOUT crashing. Raise the limit for \ + this process (launchd: SoftResourceLimits.NumberOfFiles; systemd: LimitNOFILE; \ + shell: ulimit -n) or reduce the number of registered repos.", + soft, repo_count, estimated + ); + } + } +} + +/// Build the rmcp `StreamableHttpServerConfig`, applying env-var overrides for +/// the DNS-rebinding `Host` header validation (GHSA-89vp-x53w-74fx, fixed +/// upstream in rmcp 1.4.0; default allowlist is loopback-only). +/// +/// Resolution order (first match wins): +/// 1. `CODESEARCH_DISABLE_HOST_VALIDATION=1|true` → `disable_allowed_hosts()` +/// (only safe behind a reverse proxy that validates Host itself). Logged +/// at WARN. +/// 2. `CODESEARCH_ALLOWED_HOSTS=host[,host:port,...]` → `with_allowed_hosts(...)` +/// (comma-separated, whitespace-trimmed, empties dropped). Logged at INFO. +/// 3. Both unset (or `ALLOWED_HOSTS` empty after trim) → rmcp loopback-only +/// default (`["localhost", "127.0.0.1", "::1"]`). +/// +/// See issue #149. +fn build_streamable_http_config() -> StreamableHttpServerConfig { + let config = StreamableHttpServerConfig::default(); + + if std::env::var(DISABLE_HOST_VALIDATION_ENV) + .map(|v| v == "1" || v.eq_ignore_ascii_case("true")) + .unwrap_or(false) + { + warn!( + "DNS rebinding protection (rmcp allowed_hosts) DISABLED via {DISABLE_HOST_VALIDATION_ENV}. \ + Only safe behind a reverse proxy that validates the Host header." + ); + return config.disable_allowed_hosts(); + } + + match std::env::var(ALLOWED_HOSTS_ENV) + .ok() + .map(|s| s.trim().to_owned()) + .filter(|s| !s.is_empty()) + { + Some(raw) => { + let hosts: Vec = raw + .split(',') + .map(|s| s.trim().to_owned()) + .filter(|s| !s.is_empty()) + .collect(); + if hosts.is_empty() { + warn!( + "{ALLOWED_HOSTS_ENV} was set but contained no valid host entries; \ + using rmcp loopback-only default" + ); + config + } else { + info!( + "Overriding rmcp allowed_hosts with {} entry/entries from {ALLOWED_HOSTS_ENV}: [{}]", + hosts.len(), + hosts.join(", ") + ); + config.with_allowed_hosts(hosts) + } + } + None => config, + } +} + pub async fn run_serve( host: Option, port: Option, @@ -3807,6 +3970,12 @@ pub async fn run_serve( } } + // Raise the fd soft limit BEFORE opening any repo store or binding + // the listener — fd demand scales with repo count and a 256-fd + // supervisor default wedges accept(2) silently (EMFILE). + #[cfg(unix)] + raise_fd_limit(config.repos.len()); + let serve_state = Arc::new(ServeState::new(config, None)); // Construct the bind address from resolved host + port. @@ -3870,7 +4039,10 @@ pub async fn run_serve( let mut session_manager = LocalSessionManager::default(); session_manager.session_config.keep_alive = None; let session_manager = Arc::new(session_manager); - let config = StreamableHttpServerConfig::default(); + + // Configure the rmcp Streamable HTTP server's DNS-rebinding defence + // (GHSA-89vp-x53w-74fx, fixed upstream in rmcp 1.4.0). See issue #149. + let config = build_streamable_http_config(); let mcp_service = StreamableHttpService::new(service_factory, session_manager, config); @@ -4287,7 +4459,14 @@ mod tests { ); assert!(!state.repos.contains_key("testalias")); - // Create a minimal DB so next call succeeds + // Recreate the DB directory + metadata so the next call succeeds. + // Deliberately do NOT open SharedStores directly here: the reopen below + // (get_or_open_stores → try_open_stores) creates the LMDB env itself + // (proven by `try_open_stores_creates_db_for_brand_new_repo`). Opening + // it directly first would open the same LMDB env twice in one process, + // which the AGENTS.md LMDB rule forbids; on Linux the first env is not + // always released before the reopen, making this test flaky. One open = + // deterministic. let db_path = repo_path.join(DB_DIR_NAME); std::fs::create_dir(&db_path).unwrap(); let meta = db_path.join("metadata.json"); @@ -4295,10 +4474,6 @@ mod tests { write!(f, "{{\"dimensions\":384}}").unwrap(); drop(f); - // Create the LMDB files (data.mdb and lock.mdb) by opening SharedStores directly - let _stores = SharedStores::new(&db_path, 384).unwrap(); - drop(_stores); - // Second call: should succeed without restart let res = state.get_or_open_stores("testalias", true).await; assert!(res.is_ok(), "expected ok after recreating DB, got: Err"); @@ -5234,4 +5409,117 @@ mod tests { // "all" is never stored — an unknown real group still errors. assert!(state.resolve_group_aliases("does-not-exist").is_err()); } + + /// Tests for `build_streamable_http_config` — DNS rebinding defence env vars + /// (`CODESEARCH_ALLOWED_HOSTS`, `CODESEARCH_DISABLE_HOST_VALIDATION`) added + /// for issue #149 / GHSA-89vp-x53w-74fx. + mod allowed_hosts_tests { + use super::*; + use std::sync::Mutex; + + /// Serialize env var mutations across parallel test threads (same pattern + /// as `allowed_roots_tests`). Different env vars from `allowed_roots_tests` + /// so cross-module parallelism is safe. + static ENV_LOCK: std::sync::OnceLock> = std::sync::OnceLock::new(); + + fn lock() -> std::sync::MutexGuard<'static, ()> { + ENV_LOCK.get_or_init(|| Mutex::new(())).lock().unwrap() + } + + fn clear_env() { + std::env::remove_var(ALLOWED_HOSTS_ENV); + std::env::remove_var(DISABLE_HOST_VALIDATION_ENV); + } + + #[test] + fn default_is_loopback_only() { + let _guard = lock(); + clear_env(); + let config = build_streamable_http_config(); + assert_eq!( + config.allowed_hosts, + vec![ + "localhost".to_string(), + "127.0.0.1".to_string(), + "::1".to_string(), + ] + ); + } + + #[test] + fn custom_allowed_hosts_replaces_default() { + let _guard = lock(); + clear_env(); + std::env::set_var(ALLOWED_HOSTS_ENV, "codesearch.internal, codesearch:39725"); + let config = build_streamable_http_config(); + assert_eq!( + config.allowed_hosts, + vec![ + "codesearch.internal".to_string(), + "codesearch:39725".to_string(), + ] + ); + } + + #[test] + fn disable_validation_clears_allowlist() { + let _guard = lock(); + clear_env(); + std::env::set_var(DISABLE_HOST_VALIDATION_ENV, "1"); + let config = build_streamable_http_config(); + assert!( + config.allowed_hosts.is_empty(), + "disable_allowed_hosts() should produce an empty allowlist" + ); + } + + #[test] + fn disable_validation_accepts_true_case_insensitive() { + let _guard = lock(); + clear_env(); + std::env::set_var(DISABLE_HOST_VALIDATION_ENV, "TRUE"); + let config = build_streamable_http_config(); + assert!(config.allowed_hosts.is_empty()); + } + + #[test] + fn disable_validation_ignores_other_values() { + let _guard = lock(); + clear_env(); + std::env::set_var(DISABLE_HOST_VALIDATION_ENV, "yes"); + let config = build_streamable_http_config(); + // Not "1" or "true" → rmcp default applies. + assert_eq!(config.allowed_hosts.len(), 3); + } + + #[test] + fn empty_allowed_hosts_falls_back_to_default() { + let _guard = lock(); + clear_env(); + std::env::set_var(ALLOWED_HOSTS_ENV, " , , "); + let config = build_streamable_http_config(); + assert_eq!( + config.allowed_hosts, + vec![ + "localhost".to_string(), + "127.0.0.1".to_string(), + "::1".to_string(), + ], + "all-empty entries should leave the rmcp default intact" + ); + } + + #[test] + fn disable_overrides_allowed_hosts() { + let _guard = lock(); + clear_env(); + std::env::set_var(ALLOWED_HOSTS_ENV, "codesearch.internal"); + std::env::set_var(DISABLE_HOST_VALIDATION_ENV, "true"); + let config = build_streamable_http_config(); + assert!( + config.allowed_hosts.is_empty(), + "DISABLE_HOST_VALIDATION takes precedence over ALLOWED_HOSTS" + ); + } + } } diff --git a/tests/cli_model_errors.rs b/tests/cli_model_errors.rs new file mode 100644 index 00000000..170c05ce --- /dev/null +++ b/tests/cli_model_errors.rs @@ -0,0 +1,84 @@ +use codesearch::ModelType; +use serde_json::json; +use std::fs; +use std::path::Path; +use std::process::Command; + +fn write_index_markers(project: &Path, model: &str, dimensions: usize) { + let db = project.join(".codesearch.db"); + fs::create_dir_all(db.join("fts")).expect("database directories should be created"); + fs::write(db.join("data.mdb"), []).expect("LMDB marker should be created"); + fs::write( + db.join("metadata.json"), + serde_json::to_vec(&json!({ + "model_short_name": model, + "dimensions": dimensions + })) + .expect("metadata should serialize"), + ) + .expect("metadata should be written"); +} + +#[test] +fn unknown_model_error_lists_every_supported_model() { + let output = Command::new(env!("CARGO_BIN_EXE_codesearch")) + .args(["--model", "not-a-model", "search", "query"]) + .output() + .expect("codesearch should start"); + + assert!(!output.status.success()); + let stderr = String::from_utf8(output.stderr).expect("stderr should be UTF-8"); + + for model in ModelType::all() { + assert!( + stderr.contains(model.short_name()), + "unknown-model error omitted '{}'", + model.short_name() + ); + } +} + +#[test] +fn search_rejects_a_model_that_does_not_match_the_index() { + let project = tempfile::tempdir().expect("temporary project should be created"); + write_index_markers(project.path(), "minilm-l6-q", 384); + + let output = Command::new(env!("CARGO_BIN_EXE_codesearch")) + .args(["--model", "embeddinggemma-q4", "search", "query", "--path"]) + .arg(project.path()) + .arg("--create-index=false") + .output() + .expect("codesearch should start"); + + assert!(!output.status.success()); + let stderr = String::from_utf8(output.stderr).expect("stderr should be UTF-8"); + assert!(stderr.contains("minilm-l6-q"), "{stderr}"); + assert!(stderr.contains("embeddinggemma-q4"), "{stderr}"); + assert!(stderr.contains("--force"), "{stderr}"); +} + +#[test] +fn search_rejects_an_override_when_the_index_model_is_unknown() { + let project = tempfile::tempdir().expect("temporary project should be created"); + write_index_markers(project.path(), "future-model", 768); + + let output = Command::new(env!("CARGO_BIN_EXE_codesearch")) + .args([ + "--model", + "embeddinggemma-q4", + "search", + "query", + "--sync", + "--path", + ]) + .arg(project.path()) + .arg("--create-index=false") + .output() + .expect("codesearch should start"); + + assert!(!output.status.success()); + let stderr = String::from_utf8(output.stderr).expect("stderr should be UTF-8"); + assert!(stderr.contains("future-model"), "{stderr}"); + assert!(stderr.contains("embeddinggemma-q4"), "{stderr}"); + assert!(stderr.contains("--force"), "{stderr}"); +}