Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
5697cf1
ci: add hardening regression and stress workflow
bpstr Sep 12, 2026
f00a58e
hardening: bound source ingestion and reject special files
bpstr Sep 12, 2026
b5ce15d
hardening: validate semantic vector storage before allocation
bpstr Sep 12, 2026
168e06c
hardening: eliminate redundant vector batch clone
bpstr Sep 12, 2026
f791d98
hardening: fail closed on incomplete filesystem discovery
bpstr Sep 12, 2026
c9ef47f
fix: remove million-symbol relationship cache cap
bpstr Sep 12, 2026
14e976c
fix: prevent unresolved relative imports from crossing repositories
bpstr Sep 12, 2026
5b2300f
fix: scale macOS watcher with recursive handler roots
bpstr Sep 12, 2026
8582861
hardening: make watcher overflow nonblocking and recoverable
bpstr Sep 12, 2026
2255d0d
fix: terminate stdio-owned watcher tasks on transport shutdown (#13)
bpstr Sep 12, 2026
96cc9d9
hardening: fail indexing when a parse worker panics (#14)
bpstr Sep 12, 2026
c586d08
hardening: reject non-finite embeddings at vector boundaries (#16)
bpstr Sep 12, 2026
7e30a38
perf: buffer vector payload writes (#15)
bpstr Sep 12, 2026
5dfdee7
hardening: surface read worker panics as fatal pipeline errors
bpstr Sep 12, 2026
6d48e3c
hardening: fail phase 1 after fatal read worker failure
bpstr Sep 12, 2026
cfd0caf
hardening: reject non-finite persisted semantic vectors
bpstr Sep 12, 2026
b3e5ccb
hardening: make semantic ranking total-order and finite
bpstr Sep 12, 2026
d6dcd2a
refactor: keep legacy-vector hardening diff surgical
bpstr Sep 12, 2026
91aa36e
refactor: keep semantic ranking hardening diff surgical
bpstr Sep 12, 2026
f90a7c8
Merge pull request #17 from bpstr/hardening/read-worker-panic
bpstr Sep 12, 2026
5e164ba
ci: revalidate semantic hardening on batch-3 base
bpstr Sep 12, 2026
9462869
Merge pull request #18 from bpstr/hardening/legacy-nonfinite-vectors
bpstr Sep 12, 2026
fed7268
ci: stage batch-4 vector lookup patch
bpstr Sep 12, 2026
2b67bd5
ci: stage semantic top-k optimization
bpstr Sep 12, 2026
78929c7
ci: make semantic top-k patch matcher robust
bpstr Sep 12, 2026
8dc5b4b
ci: fix semantic helper insertion marker
bpstr Sep 12, 2026
58b7959
perf: batch vector candidate lookups
github-actions[bot] Sep 12, 2026
a459b68
ci: remove batch-4 vector patch workflow
bpstr Sep 12, 2026
be8d529
x
bpstr Sep 12, 2026
c3b8214
ci: remove temporary file
bpstr Sep 12, 2026
30fe85f
ci: stage vector batch dedupe correction
bpstr Sep 12, 2026
3aaa06f
perf: select semantic top-k before sorting
github-actions[bot] Sep 12, 2026
5e1be6a
ci: remove semantic top-k patch workflow
bpstr Sep 12, 2026
31a17f8
fix: preserve first-match batch vector reads
github-actions[bot] Sep 12, 2026
5c611b4
ci: remove vector dedupe patch workflow
bpstr Sep 12, 2026
8829b1f
Merge pull request #20 from bpstr/perf/semantic-top-k
bpstr Sep 12, 2026
e06bab4
Merge pull request #19 from bpstr/perf/vector-batch-lookup
bpstr Sep 12, 2026
0a5cfb1
hardening: validate vector storage physical layout
bpstr Sep 12, 2026
a194e14
ci: stage vector generation replacement hardening
bpstr Sep 12, 2026
e4eb63d
hardening: replace vector generations atomically
github-actions[bot] Sep 12, 2026
30345a6
ci: remove vector generation patch workflow
bpstr Sep 12, 2026
1b59d9b
Merge pull request #22 from bpstr/hardening/vector-generation-replace…
bpstr Sep 12, 2026
f9b8e1c
chore: prepare isolated review source snapshot
bpstr Sep 12, 2026
f3049d5
ci: compile reviewed baseline without invoking embedding providers
bpstr Sep 12, 2026
0eeb175
ci: stage hash-verified review changes (part 1)
bpstr Sep 12, 2026
b1b3d4f
ci: stage hash-verified review changes (part 2)
bpstr Sep 12, 2026
c00c05e
ci: stage hash-verified review changes (part 3)
bpstr Sep 12, 2026
36c4a44
ci: stage hash-verified review changes (part 4)
bpstr Sep 12, 2026
c7f1cda
ci: validate hardening changes with offline fixtures
bpstr Sep 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions .github/workflows/hardening.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
name: Hardening

on:
pull_request:
branches: [main]
workflow_dispatch:
schedule:
- cron: '30 1 * * *'

env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: 1

jobs:
reliability:
name: Reliability regressions
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- uses: Swatinem/rust-cache@v2
with:
cache-on-failure: true
- name: Run hardening regression tests
run: cargo test hardening_ --all-features -- --nocapture
- name: Run all library tests
run: cargo test --lib --all-features

linux-watcher:
name: Linux watcher regressions
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Run watcher hardening tests
run: cargo test hardening_watcher_ --all-features -- --nocapture

macos-watcher:
name: macOS watcher witness
runs-on: macos-latest
timeout-minutes: 25
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Compile macOS-specific watcher path
run: cargo check --all-features
- name: Run watcher hardening tests
run: cargo test hardening_watcher_ --all-features -- --nocapture

stress:
name: Stress and recovery smoke
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Build release binary
run: cargo build --release
- name: Generate synthetic repository
run: |
set -euo pipefail
root="$RUNNER_TEMP/codanna-hardening"
mkdir -p "$root/src"
for i in $(seq 1 5000); do
printf 'pub fn symbol_%s() -> usize { %s }\n' "$i" "$i" > "$root/src/file_$i.rs"
done
echo "$root" > "$RUNNER_TEMP/hardening-root"
- name: Index synthetic repository
run: |
set -euo pipefail
root="$(cat "$RUNNER_TEMP/hardening-root")"
/usr/bin/time -v ./target/release/codanna index "$root" 2>"$RUNNER_TEMP/index-metrics.txt"
- name: Validate indexed repository remains queryable
run: |
set -euo pipefail
root="$(cat "$RUNNER_TEMP/hardening-root")"
cd "$root"
"$GITHUB_WORKSPACE/target/release/codanna" retrieve search-symbols query:symbol_4999 limit:5
- name: Upload metrics
uses: actions/upload-artifact@v4
with:
name: hardening-metrics-${{ github.run_id }}
path: ${{ runner.temp }}/index-metrics.txt
if-no-files-found: error
91 changes: 91 additions & 0 deletions .github/workflows/review-workbench.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
name: Review workbench

on:
push:
branches: [work/review-20260912]
paths: [.github/workflows/review-workbench.yml]

permissions:
contents: read

defaults:
run:
shell: bash

jobs:
baseline:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
- name: Preserve reviewed source changes
run: cat review.patch.part*.b64 > "$RUNNER_TEMP/review.b64"
- uses: actions/checkout@v6
with:
ref: 1b59d9b8a2f9d20204faea66671712dff66e6c75
persist-credentials: false
- name: Apply hash-verified source changes
run: |
python3 - <<'PY'
import base64, gzip, hashlib, json, os
from pathlib import Path
raw = gzip.decompress(base64.b64decode(Path(os.environ['RUNNER_TEMP'], 'review.b64').read_bytes()))
assert hashlib.sha256(raw).hexdigest() == '866ea79094bd2ceddf6e06c0d126ae3ef395b5c5d733016a8089e9c5bdd388c7'
prepared = []
for change in json.loads(raw):
path = Path(change['path'])
assert not path.is_absolute() and '..' not in path.parts and '.git' not in path.parts
old = path.read_bytes() if path.exists() else b''
if change['old'] is None:
assert not path.exists()
else:
assert hashlib.sha256(old).hexdigest() == change['old'], str(path)
lines = old.decode().splitlines(keepends=True)
for first, last, replacement in reversed(change['edits']):
lines[first:last] = replacement.splitlines(keepends=True)
new = ''.join(lines).encode()
assert hashlib.sha256(new).hexdigest() == change['new'], str(path)
prepared.append((path, new))
for path, new in prepared:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_bytes(new)
Path(os.environ['RUNNER_TEMP'], 'changed-rust.txt').write_text('\n'.join(str(p) for p, _ in prepared if p.suffix == '.rs'))
print(f'Applied {len(prepared)} verified source changes')
PY
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt
- uses: Swatinem/rust-cache@v2
with:
cache-on-failure: true
- name: Format only changed Rust files
run: |
xargs -d '\n' rustfmt --edition 2024 --config skip_children=true < "$RUNNER_TEMP/changed-rust.txt"
git add -N .
git diff --check
- name: Rust regressions (mock embeddings, no providers)
run: cargo test --locked --all-features hardening_review_ -- --nocapture 2>&1 | tee "$RUNNER_TEMP/rust-review.log"
- name: Real HTTP transport fixtures (semantic indexing disabled)
run: cargo test --locked --all-features --test cli_tests test_serve_http_sessionless -- --nocapture 2>&1 | tee "$RUNNER_TEMP/http-review.log"
- uses: actions/setup-node@v4
with:
node-version: '22'
- name: Plugin security regressions (no package installs)
run: node --test tests/security/plugin-boundaries.test.cjs 2>&1 | tee "$RUNNER_TEMP/node-review.log"
- name: Export exact tested patch
if: always()
run: |
git add -N .
git diff --binary > "$RUNNER_TEMP/review-tested.patch"
git diff --stat > "$RUNNER_TEMP/review-stat.txt"
- uses: actions/upload-artifact@v4
if: always()
with:
name: review-validation
path: |
${{ runner.temp }}/review-tested.patch
${{ runner.temp }}/review-stat.txt
${{ runner.temp }}/*review.log
retention-days: 3
1 change: 1 addition & 0 deletions review.patch.part00.b64
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
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
Loading