Skip to content

Release v1.1.31 — Aikido security sweep, EmbeddingGemma retrieval, fd-limit + host-allowlist fixes#160

Merged
flupkede merged 145 commits into
masterfrom
release/v1.1.31
Jul 23, 2026
Merged

Release v1.1.31 — Aikido security sweep, EmbeddingGemma retrieval, fd-limit + host-allowlist fixes#160
flupkede merged 145 commits into
masterfrom
release/v1.1.31

Conversation

@flupkede

Copy link
Copy Markdown
Owner

Release v1.1.31

Security hardening sweep (Aikido: critical path-traversal fixes, ANSI-injection sanitization, .git-root rejection, Unix path-cache collision fix, CI supply-chain hardening, dependency CVE updates) plus community bug/dependency fixes and the EmbeddingGemma retrieval feature.

Highlights

See CHANGELOG.md [1.1.31] for full details.

Note: this PR is from release/v1.1.31, a throwaway branch cut off develop with master merged in via -X ours to resolve the known squash-merge history divergence (documented in AGENTS.md). Content diff vs develop is empty — this is history reconciliation only, no functional difference from a direct develop→master merge.

🤖 Generated with worker automation

Test User and others added 30 commits June 23, 2026 15:58
Expose the read-only MCP tools (search, find, explore, get_chunk) over plain HTTP+JSON so a remote codesearch serve can be queried for federation WITHOUT an MCP session. Each REST handler constructs a per-request CodesearchService bound to the live ServeState, invokes the existing #[tool] method, and returns the tool's JSON payload unwrapped from CallToolResult. The new routes inherit the existing auth layers (require_auth_for_network on network binds), so no new auth code is needed. Adds a rest_routes_are_registered integration test.
Previously each CodesearchService started with embedding_service=None, so
per-request REST semantic search reloaded the ONNX model (~100ms-2s) on every
call. Mirror the symbol_registry pattern: ServeState now owns a shared
Arc<Mutex<Option<EmbeddingService>>> and exposes an embedding_service() getter;
new_for_serve binds to it, so the model loads once per serve instance (lazily
on first semantic query) and is reused by all MCP sessions AND REST handlers.
The standalone new() path keeps its own Arc. Addresses review remark on the
REST-endpoint commit.
… (Phase 2)

ReposConfig gains a `remotes` map of RemotePeer{url, api_key, group, timeout_secs}.
Group members may reference a remote peer via an "@"-prefix (e.g. "docs": ["@cloud"]);
resolve_group_targets/split_group_targets return mixed Local/Remote Target lists
(the virtual "all" group stays local-only). New `federation` module exposes a
FederationClient that calls the Stage-2 REST endpoints (/search, /chunk/:id) with
per-peer bearer auth over a shared reqwest client. The `search` tool, when its
group contains remotes, fans out to each peer concurrently, converts remote hits
into source-tagged SearchResultItems (chunk_ref = "peer:id"), and RRF-merges
local + remote ranked lists; unreachable peers degrade gracefully into a
`warnings` array (never hard-fail). The `get_chunk` tool transparently proxies
namespaced chunk_refs ("peer:id") back to the owning peer. 16 new unit tests
(config, federation client, merge/conversion helpers). find/explore federation
deferred.
The previous `f.score < 0.15` threshold was unsatisfiable: merge_ranked_lists
reassigns every score to the RRF value 1/(k+rank+1) (max ≈ 0.048 for k=20), so
every federated search that returned ANY hit was flagged low_confidence=true.
RRF-fused scores are not comparable to single-source embedding/BM25 thresholds,
so no score cutoff is meaningful here. Now low_confidence is flagged only when
the merged set is empty (a genuine "nothing found" signal to the agent). Also
drops the unused `_limit` parameter from build_federated_response.

Addresses review remark on the Phase 2 federation commit.
Add ReposConfig::project_groups() — inverse index mapping each repo alias to
the named group(s) it belongs to (sorted/deduped; excludes the virtual "all"
group and "@Remote" refs; omits aliases in no named group).

Expose it as a new `project_groups` field in the scope_required error and
sharpen `hint_for_agent` so an agent picking a single project is told to
prefer group= when that project belongs to a group (e.g. a separate config /
import-data repo). Adds 2 unit tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ects)

Add a `groups` field to RepoInfo listing the named group(s) each repo belongs
to (via ReposConfig::project_groups()), populated in both the serve and stdio
branches of list_projects. Lets an agent inspecting `status` see that e.g.
BAYR.Aprimo is part of group BAYER and prefer a cross-repo group= query.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ject_groups()

Addresses the sole (informational) review remark: clarify that project_groups()
excludes the virtual "all" group implicitly (it is never persisted in
self.groups), so a future change that starts persisting "all" must filter it
explicitly. Doc-comment only; no behavior change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…d deploy plan

- /healthz: fixed {"status":"ok"} body, no version/repo info, exempt from
  require_auth_for_network so container-orchestrator probes reach it on a
  network bind without the Bearer key. /health stays auth-protected.
- HEALTHZ_PATH constant; route registered; log-spam suppressed.
- Test: healthz reachable without key on simulated network bind, /health 401.
- docs/federation-cloud-deployment.md: phased Azure plan (role-assignment-free:
  SAS + inline ACA secrets), verified rights, PIM-Contributor on Aprimo RG.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…d keep-warm

Container artifacts:
- Dockerfile: multi-stage build + fastembed model pre-warm baked into the image
  (ONNX loads from a local path; never streamed from blob) + slim runtime with
  azcopy + git. .dockerignore keeps target/ and tool dirs out of the context.
- docker/entrypoint.sh: snapshot-restore -> azcopy sync (+ optional KB git pull)
  -> serve; background loop does incremental reindex + periodic index snapshot
  to a separate blob container. No FSW; full-on-start + incremental-on-timer.

serve cloud keep-warm (2h-idle-then-suspend for ACA scale-to-zero):
- --keep-warm-url / CODESEARCH_KEEP_WARM_URL + --idle-suspend-secs /
  CODESEARCH_IDLE_SUSPEND_SECS (default 7200). Serve self-pings its own ingress
  /healthz while the most-recent real tool call is younger than the idle window,
  then stops so ACA suspends; next real query wakes it. No Logic App, no managed
  identity, no role assignment.
- ServeState::most_recent_tool_call(); constants for env vars + defaults.

docs: option D (scale-to-zero + blob snapshot) with actual SSOT/Aprimo resources.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Three build-portability fixes found via review + real builds:
- COPY build.rs into the builder: env!("CARGO_PKG_VERSION_FULL") (main.rs,
  cli.rs) needs the var build.rs emits; it falls back to "0"/"unknown" without
  .git, so the build is reproducible without repo history.
- Drop BuildKit `--mount=type=cache`: ACR Tasks uses the classic builder which
  rejects `--mount`. ACR builds fresh anyway.
- Base images bookworm -> trixie (glibc 2.41): the prebuilt onnxruntime pulled
  by `ort` references glibc-2.38+ C23 symbols (__isoc23_strtoll), so linking on
  bookworm (2.36) failed. Runtime moved to trixie too; libssl3 dropped (reqwest
  uses rustls, no OpenSSL at runtime).

Verified: full image builds clean locally on trixie (all 24 steps).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…repo bake

Found via live ACA deployment + container logs:
- azcopy sync: drop --compare-hash=MD5. It stores the hash in a user_xattr,
  which the container overlayfs does not support -> every transfer failed
  ("1 Failed, 0 bytes") so docs never synced. Default sync (LMT+size) needs no
  xattr and works.
- Dockerfile: copy ONLY ~/.codesearch/models from the warmer, not the whole
  dir. The warmup writes a repos.json registering "/tmp/warm", which baked a
  stale "warm" repo into the runtime image.
- Repo registration: `serve --register` adds the alias but does NOT build the
  initial index (create_index is ignored in the CLI handler), and an unindexed
  repo is auto-pruned at warmup -> "Unknown alias 'docs'". The entrypoint now
  registers each repo via POST /repos (create DB + index + warm) once serve is
  live, instead of --register.

Verified end-to-end on Azure Container Apps: cold start -> auto-register ->
index -> federated /search returns the doc within ~5s.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…peer

ACA app codesearch-serve live in Delaware.SSOT/Aprimo (scale-to-zero, HTTPS,
snapshot + keep-warm). End-to-end verified: cold start -> auto-register ->
index -> federated search. Documents resources, FQDN, and the az-CLI emoji
log-stream caveat (build local + docker push, not az acr build).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Manage remote serve peers from the CLI instead of hand-editing repos.json:

  codesearch remote add <name> --url <url> [--api-key K] [--group G]
                               [--timeout-secs N] [--into-group LOCAL_GROUP]
  codesearch remote list
  codesearch remote rm <name>

Model (ReposConfig):
- add_remote(name, peer): validate non-empty name, reject a leading '@'
  (the reference prefix is added automatically), require non-empty url.
- remove_remote(name): drop the peer AND prune every "@name" reference from
  groups, dropping any group left empty.
- add_remote_to_group(group, name): idempotently push "@name" into a group
  (created on demand); rejects reserved "all" and unknown peers.
- groups_referencing_remote(name): sorted groups wiring a peer (for `list`).

--into-group wires "@name" into a local group in one step so the peer is
immediately queryable; without it, `add` prints the follow-up hint.

Hoisted the federation default timeout (15s) into
constants::DEFAULT_REMOTE_TIMEOUT_SECS so the client and the CLI share one
source of truth (no duplicated magic number).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- remote rm: trim the name before lookup/printing, matching the trimming
  `add`/`--into-group` already do (so `rm "  cloud  "` finds `cloud`).
- federation: demote the inert doc-comment above the `use ... as
  DEFAULT_TIMEOUT_SECS` alias to a plain comment (a `///` on a `use` is dead).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Decouples the memory-heavy index build from light serving so the long-running
Container App can run small (1-2 GiB) while a short-lived Container Apps Job does
the full embed on a big replica (4-8 GiB), avoiding OOM (exit 137) on serve.

CODESEARCH_RUN_MODE:
- serve (default): restore the prebuilt snapshot from blob and serve READ-ONLY.
  No register / reindex / snapshot — never does heavy work. Warns if no snapshot.
- index-job: restore (incremental) -> sync blob -> drive a local serve to
  build/force-reindex -> wait until /status clears "indexing" -> upload snapshot
  -> exit 0. New helpers: wait_healthz, register_or_reindex, wait_until_indexed.

Deployed: image tag v2-modes; ACA Job 'codesearch-indexer' (index-job, 2 vCPU/
4 GiB, Manual trigger); serve app resized to 1 vCPU/2 GiB with RUN_MODE=serve.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The /reindex?force=true endpoint returns 500 in the cloud deployment, so the
index-job could not pick up new/changed docs. Replace the reindex call with the
proven path: when the repo is already registered (restored from a snapshot),
DELETE /repos/{alias} then POST /repos for a clean full rebuild. The restored
embedding cache makes unchanged docs cache-hits, so only new/changed docs cost
real work — appropriate for the Job's big replica.

Also harden wait_until_indexed against the DELETE+POST race: phase 1 waits for a
build to actually be observable (status "indexing", or an already-ready state for
a cache-instant rebuild) so the brief post-DELETE gap is never mistaken for
"done"; phase 2 waits for "indexing" to clear with the repo present + ready.

docs/federation-cloud-deployment.md: document the serve/index-job split, the
codesearch-indexer Job, the 1 vCPU/2 GiB restore-only serve, and image v2.1.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The DELETE+POST rebuild is a full re-embed (~20-25 min for the 2737-doc corpus on
the job replica), not a cache-fast pass — observed in a live run. Correct the
entrypoint comment and deployment doc to state this plainly; the heavy cost is by
design (it lives in the Job, never in serve).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The cloud index-job's rebuild_repo used DELETE + POST /repos: it deleted the
~60 MB index dir then immediately reopened it (racy on the container overlayfs,
surfaced as "register failed" 500 + a stalled build that never refreshed the
snapshot), and it was a full ~20-25 min re-embed every run.

Switch to the safe, light path:
- already-registered repo (snapshot restored) -> POST /repos/<alias>/reindex
  (incremental, no force): opens the existing index in place, re-embeds only
  deltas, returns 202. No DB delete, no reopen race.
- not-yet-registered (cold build, no snapshot) -> POST /repos {path} (full build).
- /reindex?force=true still avoided (returns 500 in this deployment).

Honesty + safety:
- api_code() captures the HTTP status instead of swallowing it with >/dev/null,
  and a non-2xx/202 rebuild response now die()s (no silent "register failed").
- verify_index_ready() gates upload on GET /repos/<alias>/info chunks > 0, so a
  broken/empty build can never clobber a known-good snapshot.

Doc updated; job sizing noted as 4 vCPU / 8 GiB, 5400s.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Root cause of the v2.2 run failure (HTTP 500 "Repo 'docs' is locked by another
codesearch process"): sync_blob runs `azcopy sync --delete-destination=true`
from the blob (which holds only .md source files) into ${DOCS_DIR}, but the
search index lives INSIDE that dir at ${DOCS_DIR}/.codesearch.db. azcopy treated
the whole restored index as "extra" and DELETED it, leaving the incremental
reindex with no base + stale locks.

Fixes:
- sync_blob: --exclude-path=".codesearch.db" so the corpus sync never touches the
  index. This also protects the SERVE app's restored index on cold start (same
  code path) — previously a cold start could silently wipe the served index.
- restore_snapshot: delete stale .writer.lock / .tantivy-*.lock / lock.mdb that a
  prior snapshot baked in (the source of the "locked by another process" 500). A
  fresh container has no other process, so any lock present is stale.
- upload_snapshot: exclude *.lock / lock.mdb from the tar so future snapshots stay
  clean.
- rebuild_repo: accept HTTP 409 (reindex already in progress) as "wait for it"
  instead of die — serve's startup warmup may already be refreshing the repo.

The pre-upload verify guard correctly prevented the failed v2.2 run from
clobbering the good snapshot.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ng reindex)

The v2.3 run still hit HTTP 500 "Repo 'docs' is locked by another codesearch
process with write access" — but this time it was a LIVE lock, not a stale one,
and the index was no longer being deleted (the sync exclude worked).

Cause: serve's Phase-1 startup warmup (warmup_repo) opens every registered repo
in WRITE mode and runs an incremental refresh on startup, holding the LMDB write
lock for the whole refresh. The job's explicit POST /repos/<alias>/reindex opened
a SECOND write handle on the same env -> 500. The warmup already does exactly the
incremental refresh the job wanted.

Fix: for an already-registered repo, the job no longer issues any reindex. It lets
the startup warmup do the refresh and just waits for the repo to flip from "closed"
(mid-warmup) to "warm" (refresh done) in wait_until_indexed(), then verifies
chunks>0 and snapshots. Only the cold (unregistered) case still POSTs /repos.

The pre-upload verify guard again prevented the failed v2.3 run from clobbering
the good 69 MB snapshot. Doc updated to describe the warmup-driven refresh and the
two invariants (sync never touches .codesearch.db; never compete with the warmup).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ove/reindex repos on a peer)

Add REPOS_PATH constant and the remote index-management client surface:
ManagementOutcome (Ok/HttpError/Unreachable), RemoteStatus/RemoteRepoStatus,
RemoteRepoAdded/Removed/RemoteReindexResult structs, and FederationClient
methods list_repos/add_repo/remove_repo/reindex backed by a shared
send_management helper. Includes mock-peer unit tests (6 new, all passing).
Dead-code warnings are expected — Stage 2 (CLI) consumes this API.
`codesearch index rm <alias>` (without --remote) previously treated the
alias as a filesystem path and failed with os error 2 during canonicalize.
Now `remove_from_index` resolves the argument against registered aliases
first (ReposConfig::resolve), falling back to path interpretation only
when it is not a known alias. The resolved path is also forwarded to
try_delegate_rm_to_serve so serve delegation works by alias too.
Adds `#[command(visible_alias = "ls")]` to the three `List` variants so `codesearch index ls`, `codesearch groups ls`, and `codesearch remote ls` work as shortcuts for `list`.
Test User and others added 28 commits July 20, 2026 14:38
Mitigates Aikido finding group 35039595 (priority 30, LOW):
"GitHub Actions actions/checkout persists GITHUB_TOKEN to git config
on self-hosted runners, allowing subsequent steps to authenticate as
the repo via the saved credential helper."

Adds `with: persist-credentials: false` to every actions/checkout step:
- ci.yml: 3 steps (test-linux, test-windows, csharp-integration-tests)
- codeql.yml: 1 step (analyze job)
- release.yml: 2 steps (build matrix, build-macos)

No other checkout steps exist in the repo (protect-master.yml has none).
YAML syntax validated post-edit. No semantic behavior change — CI/release
jobs do not push back to the repo from these checkouts, so disabling the
auth helper is purely defensive.

Note: ci.yml/release.yml use pinned SHA 34e114876b0b11c390a56381ad16ebd13914f8d5
(pinned v4); codeql.yml uses floating @v4 tag (pre-existing inconsistency,
left untouched in this commit).
…641757)

Mitigates Aikido finding group 30641757 (priority 35, MEDIUM):
"ANSI escape sequence injection in search output" — indexed content
could embed CSI/OSC sequences (e.g. \x1b[2J clears screen, \x1b]0;...\x07
rewrites window title) that the host terminal would execute on print.

Changes:
- Add `sanitize_for_terminal(&str) -> String` helper in src/search/mod.rs
  Strips: CSI sequences (ESC [ ... <final 0x40-0x7E>),
          OSC sequences (ESC ] ... (BEL | ESC \)),
          single-char escape sequences (ESC <0x40-0x5F>),
          and stray control chars except \n and \t.
  Safe on truncated input — never panics.
- Apply to every user-controllable println! site in search/mod.rs:
    * print_result: result.path, result.kind, result.signature,
                    result.context, result.context_prev/next lines,
                    result.content lines, snippet
    * sync_database: file.path display, deleted-file path string
    * compact path: result.path
    * query string in standard output header
- Add 9 unit tests covering CSI/OSC/single-char/control-char/unicode/
  empty/truncated-input cases.

The `colored` crate wraps content but does not sanitize inner escapes;
sanitization happens BEFORE .bright_green() / .dimmed() / etc. so the
color wrapper cannot be broken out of.

Local cargo check blocked by pre-existing MSYS2 link.exe issue
(documented in PR #151) — no errors in src/search/mod.rs. cargo fmt
passes.
…:walk

Mitigates Aikido finding group 30641794 (priority 38, MEDIUM):
"Local client can register `.git` dir as repo, search excluded Git
metadata" — exposes internal/sensitive files (objects, config, refs)
via search results.

Root cause: `FileWalker::walk`'s `filter_entry` closure short-circuits
on `entry.depth() == 0` (the root entry), so the ALWAYS_EXCLUDED name
check is bypassed when the user points the indexer at a directory
whose own name is `.git` (or `node_modules`, `target`, etc.).

Fix: validate `self.root.file_name()` at the top of `walk()` and
bail! with an actionable error if the name matches an ALWAYS_EXCLUDED
entry. Covers every caller uniformly — CLI `index`, HTTP `/repos`,
`doctor`, `sync_database`, watcher — without needing to patch each
callsite. Pre-existing depth==0 short-circuit intentionally left in
place (now unreachable for excluded names; still correct for normal
roots whose names are not in the list).

Test: `test_rejects_excluded_named_root` builds a temp `.git` dir,
asserts walk() returns Err with "Refusing to index" + ".git" in the
message, and verifies a sibling non-excluded root walks normally.
…th (Aikido 30641757)

Mitigates Aikido finding group 30641757 (priority 46, MEDIUM):
"Improper Input Validation — backslash path collision on Unix".
Companion finding to the ANSI escape injection already fixed in
stage 1/3 (same group, different priority).

THREAT MODEL
On Unix, backslash is a legal filename character (not a path
separator). A file literally named `foo\bar.rs` is distinct from
`foo/bar.rs` (which lives in subdirectory `foo`). The previous
`normalize_path` / `normalize_path_str` unconditionally ran
`.replace('\\', "/")`, collapsing both into the key `foo/bar.rs`.
This caused silent HashMap collisions in `FileMetaStore`: one
file's chunks would overwrite the other's metadata, leading to
stale search results, missed re-indexing, or wrong chunk IDs.

FIX
Gate the backslash-to-forward-slash conversion behind `#[cfg(windows)]`:
- Windows: backslash IS a path separator — conversion is required
  for HashMap consistency across canonicalize/Notify/raw APIs.
- Unix: preserve backslash literally; it is part of the filename
  and must not be normalized away.

The `trim_start_matches(r"\\?\")` (UNC prefix strip) runs
unconditionally on both platforms — it is a no-op on Unix in
practice but defensive in case a Windows-style path string leaks
into a Unix process via config/migration.

TESTS
- Added `test_normalize_path_preserves_unix_backslash_filenames`
  (cfg(not(windows))): asserts `foo/bar.rs` and `foo\bar.rs`
  normalize to distinct keys.
- Gated 12 Windows-specific tests with `#[cfg(windows)]` because
  they explicitly assert backslash conversion using hardcoded
  `C:\...` / `\\?\C:\...` inputs. These tests document Windows
  behavior and have no meaning on Unix after the fix.

Files changed: src/cache/file_meta.rs (+50 / −2 net)

Validation:
- `cargo fmt --check src/cache/file_meta.rs` PASS
- `cargo check --lib --tests` fails ONLY at the pre-existing
  MSYS2 `/usr/bin/link` vs MSVC `link.exe` link step (no errors
  reference file_meta.rs). Authoritative validation will run in
  GitHub CI.
…deps)

Addresses Aikido dependency-vulnerability findings via semver-safe
`cargo update` plus an explicit floor bump for the highest-priority
direct dep.

Direct-dep change:
- rmcp 1.5.0 -> 1.8.0 (Aikido priority 82, 3 CVEs — impersonate data
  source). Major bump to v2.x available but breaking; deferred.
  Within-semver patch picks up the CVE fixes without API churn.

Cargo.lock refresh (`cargo update` with no Cargo.toml changes beyond
the rmcp floor bump above). Notable security-relevant transitive bumps:
- quinn-proto 0.11.14 -> 0.11.16 (Aikido priority 75, DOS)
- h2 0.4.14 -> 0.4.15
- hyper 1.10.1 -> 1.11.0
- tokio 1.52.3 -> 1.53.1
- rustls 0.23.40 -> 0.23.42
- openssl 0.10.80 -> 0.10.81
- zerocopy 0.8.52 -> 0.8.55
- zeroize 1.8.2 -> 1.9.0
- webpki-roots 1.0.7 -> 1.0.9
- aws-lc-rs 1.17.0 -> 1.17.3
- regex 1.12.4 -> 1.13.1
- safetensors 0.7.0 -> 0.8.0
Plus ~90 other minor/patch bumps. Net Cargo.lock diff: +391/-490 lines.

Deferred (separate concerns):
- rmcp v2.x major bump — breaking API changes, needs dedicated migration
- Blurred Aikido entries (we*zl p65, l*u p62, etc.) — cannot identify
  exact crates without `cargo audit`, which is itself blocked by the
  same MSYS2 `/usr/bin/link` link-step issue that blocks local builds.
  CI on GitHub Actions will surface anything still open after this bump.

Validation:
- `cargo metadata --no-deps` parses cleanly (Cargo.toml well-formed)
- `cargo check --lib` fails ONLY at link step (pre-existing MSYS2
  `/usr/bin/link` shadowing MSVC `link.exe`, documented in PRs #151-
  #153). No source-level errors, no unused-import warnings.
- `cargo fmt --check` N/A (no .rs files modified).
- Authoritative validation deferred to GitHub Actions CI on the PR.
🔒️ fix: harden path-traversal vectors and CI credential persistence (Aikido)
🛡️ fix: defense-in-depth for ANSI injection, .git root walk, and Unix backslash collision (Aikido)
deps: cargo update + rmcp floor bump 1.5.0 → 1.8.0 (Aikido p82)
…hardening)

Aligns .github/workflows/codeql.yml with the pinning policy already
followed by ci.yml and release.yml: replace the floating @v4 tag with
the pinned SHA 34e114876b0b11c390a56381ad16ebd13914f8d5 (# pin@v4).

The floating @v4 tag is mutable — if the action's tag is moved (accidentally
or via compromise), CI would silently start running whatever new SHA the
tag points to. Pinning to a specific SHA makes every CI run reproducible
and requires an explicit commit to change which code runs.

Related: Aikido follow-up to finding group 35039595 (GitHub Actions
persist-credentials). Same threat class (CI supply-chain integrity).

No behavior change — SHA 34e1148... is the exact commit @v4 currently
resolves to, verified via the existing pin in ci.yml:21 and release.yml:41.
🔒️ fix: pin actions/checkout SHA in codeql.yml (supply-chain hardening)
Scope prose-aware Text labels to EmbeddingGemma Markdown and plain-text chunks. Keep the historical Code label for all existing models so incremental indexing cannot mix document representations. Warn when explicitly selecting models with larger vector dimensions.
test_sanitize_strips_single_char_escape passed a String argument to
sanitize_for_terminal(s: &str), breaking cargo test --lib. Pass &str
literals to match the sibling sanitize tests. (only surfaces under
--lib test compilation, not plain cargo check)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Five tests hardcoded Windows absolute paths (C:\..., \?\C:\..., backslash
separators) and asserted separator-rewriting semantics that normalize_path_str
deliberately applies ONLY on Windows (backslash is a legal filename char on
Unix — see file_meta.rs Aikido 30641757 rationale). They therefore failed on
the Linux CI jobs (test-linux, csharp-integration-tests) while passing on
test-windows.

Gate the Windows-specific tests with #[cfg(windows)] and add #[cfg(unix)]
counterparts using native forward-slash paths for the three path-matching
tests, preserving Linux coverage. The two pure separator-handling tests
(backslashes / mixed) are Windows-only concepts; forward-slash behaviour is
already covered by test_path_prefix_no_alias/_empty_alias on all platforms.

Pre-existing develop breakage, unrelated to the EmbeddingGemma feature
(src/mcp/mod.rs is untouched by that work).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
.map(|l| sanitize_for_terminal(l)) -> .map(sanitize_for_terminal).
.lines() yields &str and sanitize_for_terminal takes &str, so the direct
function reference is valid. clippy -D warnings (Linux CI) flagged it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
missing_db_not_cached_as_conflicted opened SharedStores directly in the
test setup and then let get_or_open_stores open the same LMDB env again —
two opens of one env in a single process, which AGENTS.md's LMDB rule
forbids. On Linux the first env is not always released before the reopen,
so try_open_stores' open failed intermittently -> readonly -> Conflicted
-> Err (flaky). try_open_stores creates the env itself (see
try_open_stores_creates_db_for_brand_new_repo), so the direct pre-open was
redundant. Dropping it leaves a single deterministic open on both
platforms.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…wedges accept()

serve's fd demand scales with registered repo count (LMDB env +
tantivy FTS segments + file-watcher handles ≈ 15-20 fds per warm
repo). Under process supervisors the default soft limit is often 256
(macOS launchd agents, some systemd/docker configs). Once the process
saturates it:

- tantivy logs 'Too many open files' (errno 24) warnings, and
- accept(2) fails with EMFILE; axum's accept loop sleeps and retries
  silently, so the daemon looks alive to its supervisor while every
  new connection is refused or reset. No ERROR log, no exit — a
  silent wedge.

Observed in production: 60 registered repos (~1000 fds needed) under
a macOS LaunchAgent — serve answered for ~15s after start (until repo
warmup consumed the fd budget), then reset every connection while the
process stayed 'healthy', deterministically across restarts.

Fix, at run_serve startup before any store open or bind:

1. Raise the RLIMIT_NOFILE soft limit to the hard limit (standard
   daemon practice — nginx/envoy/postgres do the same). On macOS the
   target is clamped to kern.maxfilesperproc so setrlimit cannot fail
   with EINVAL. Failures are non-fatal and logged.
2. Log the raise at INFO.
3. If the effective limit still looks too small for the registered
   repo count (repos × 20 + 256 headroom), emit a loud actionable
   WARN naming the supervisor knobs (launchd
   SoftResourceLimits.NumberOfFiles, systemd LimitNOFILE, ulimit -n).

Verified at scale: with ulimit -n 256 and 60 registered repos, an
unpatched serve saturates at 255/256 fds (EMFILE in logs, wedge under
launchd); the patched serve logs 'Raised RLIMIT_NOFILE soft limit
256 → 61440', runs at ~300 fds, and answers MCP handshakes
indefinitely. cargo clippy -D warnings clean; cargo test --lib --bins
green (579 + 575).
🐛 fix: raise RLIMIT_NOFILE at serve startup — fd exhaustion silently wedges accept()
…events)

Fork PRs run with a restricted GITHUB_TOKEN that cannot write
`security-events` back to the upstream repo, so the analyze step's
SARIF upload fails with "Resource not accessible by integration"
for every external contributor PR (e.g. PR #150 from tony-nexartis).

Add a job-level `if:` that skips the entire analyze job when the
pull_request's head repo differs from the workflow's repository.
CodeQL still runs on:
  - push events to develop/master (post-merge, full write token)
  - same-repo PRs (full write token)
  - the weekly schedule
so no scanning coverage is lost — only the redundant, upload-failing
fork-PR run is skipped.

No behavior change for non-fork workflows.
🔧 ci: skip CodeQL on fork PRs (upload cannot write security-events)
…s env vars (#149)

Two unrelated fixes bundled in one PR per maintainer direction.

#148 — UTF-8 panic at src/search/mod.rs:1343
============================================
Pre-existing bug: `&snippet[..100]` byte-sliced a UTF-8 string, panicking
with "byte index 100 is not a char boundary" when byte 100 landed inside a
multi-byte character (box-drawing separators in comment art, CJK, emoji).
Originally flagged in PR #152 review as "out-of-scope, deferred"; reported
as issue #148 by @tony-nexartis.

Fix: use `str::floor_char_boundary(100)` (stabilized in Rust 1.82; we're on
1.95) to find the largest char boundary ≤ 100 bytes, then slice. 1-line
change at the print site. Regression test `test_byte_truncation_preserves_
char_boundary` in src/search/mod.rs constructs a 120-byte string of U+2500
box-drawing chars and asserts no panic + correct char-boundary cut.

#149 — Container hostname rejected by rmcp default allowlist
=============================================================
rmcp ≥ 1.4.0 added DNS-rebinding defence (GHSA-89vp-x53w-74fx,
CVE-2026-42559): `StreamableHttpServerConfig::allowed_hosts` defaults to
loopback-only `["localhost", "127.0.0.1", "::1"]`. Containerised
deployments (where the Host header is the container hostname, not
localhost) get `WARN ... rejected request with disallowed Host header`.
Reported as issue #149 by @stdweird.

Fix: expose two env vars, both read once at serve startup:

  CODESEARCH_ALLOWED_HOSTS=host[,host:port,...]
    Comma-separated list of hostnames / `host:port` authorities. Replaces
    the rmcp default allowlist. Whitespace-trimmed, empties dropped.

  CODESEARCH_DISABLE_HOST_VALIDATION=1|true
    Disables Host validation entirely (calls rmcp's `disable_allowed_hosts()`).
    DANGEROUS — only safe behind a reverse proxy that validates Host itself.
    Accepts `1` or `true` (case-insensitive); any other value is ignored.
    Takes precedence over CODESEARCH_ALLOWED_HOSTS.

New module-level helper `build_streamable_http_config()` in src/serve/mod.rs
encapsulates the resolution order (disable > custom > default). Called once
from `run_serve` in place of the previous inline `StreamableHttpServerConfig
::default()`. 7 unit tests in `mod allowed_hosts_tests` cover all branches.

Both env vars documented in src/constants.rs with the same comment style as
the existing ALLOWED_ROOTS_ENV / SERVE_API_KEY_ENV.

Validation
==========
- `cargo fmt --check` clean
- `cargo clippy --all-targets -- -D warnings` clean
- `cargo test --lib --bins`: 1188 passed, 36 ignored, 0 failed
  (includes 7 new allowed_hosts tests + 1 byte_truncation test)

Closes #148.
Closes #149.
🐛 fix: byte-boundary panic (#148) + rmcp allowed_hosts env vars (#149)
…weep)

Documents the security hardening sweep and follow-up fixes that landed in
develop since the [1.1.30] changelog entry, none of which had been
changelogged or documented in README:

- PR #151: critical path-traversal fixes (Rust + .NET) + CI persist-credentials
- PR #152: ANSI-injection sanitization, .git-root rejection, Unix backslash
  path-cache collision fix
- PR #153: CodeQL checkout SHA pinning
- PR #154: rmcp 1.5.0->1.8.0 + ~100 transitive dependency CVE updates
- PR #150 (external, @tony-nexartis): RLIMIT_NOFILE fd-exhaustion fix
- PR #156: skip CodeQL analyze on fork PRs (restricted GITHUB_TOKEN can't
  upload SARIF to upstream)
- PR #157: byte-boundary panic fix (#148, @tony-nexartis) + new
  CODESEARCH_ALLOWED_HOSTS / CODESEARCH_DISABLE_HOST_VALIDATION env vars
  (#149, @stdweird)

Also bumps Cargo.toml to 1.1.31 for this documentation/version-tracking
release. No functional code changes in this commit.
docs: changelog + README updates for PRs #150-#157 (Aikido security sweep)
@flupkede
flupkede merged commit a409dfc into master Jul 23, 2026
1 check passed
@flupkede
flupkede deleted the release/v1.1.31 branch July 23, 2026 14:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants