Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
6ec3a2e
fix(ci): make cla-check work for fork PRs (#553)
ajianaz Sep 11, 2026
96cdb7c
chore(deps): pin rustls 0.23.45 + drop deprecated f32 import in vecto…
ajianaz Oct 4, 2026
a5bcf6e
fix(review): report chunked findings when chunks return empty summari…
ajianaz Oct 4, 2026
33df675
security(config): don't send API key to base_url from project .cora.y…
ajianaz Oct 7, 2026
0630b36
fix(mcp): harden stdio framing, notifications, and tool parameter han…
ajianaz Oct 7, 2026
147dd75
fix(index): key files by project, exact-match callers, auto-prune, ho…
ajianaz Oct 7, 2026
9e8afeb
security(upgrade): harden self-update and installer verification (#572)
ajianaz Oct 7, 2026
5df7552
fix(llm): harden response parsing, prompt injection and stream limits…
ajianaz Oct 7, 2026
66eb8bf
refactor(index): single seam for resolving project root and opening t…
ajianaz Oct 7, 2026
40cbb8f
fix(affected): unify affected-tests and dead-code queries between CLI…
ajianaz Oct 7, 2026
3da0b40
refactor(index): one index session for index/watch/serve/brain/MCP se…
ajianaz Oct 7, 2026
1dd7ac4
refactor(review): split deterministic analysis from the LLM call (#577)
ajianaz Oct 7, 2026
f1f1116
docs(changelog): document changes since v0.15.0 under [Unreleased] (#…
ajianaz Oct 8, 2026
359840f
refactor(llm): unify parse/repair/retry policy behind a Transport sea…
ajianaz Oct 8, 2026
f620602
fix(secrets): scan high-confidence secrets in test/fixture paths (#583)
ajianaz Oct 8, 2026
02951b9
fix(watch): reindex only files that changed; --filter restricts what …
ajianaz Oct 8, 2026
ee5bb85
refactor(store): single review_store module owns review-history SQL (…
ajianaz Oct 8, 2026
c6a02aa
fix(findings): make --severity filter and list colouring case-insensi…
ajianaz Oct 8, 2026
f69fc28
fix(test): isolate unit tests from the real data dir; bound vector in…
ajianaz Oct 8, 2026
d4a6a46
fix(install): add curl timeouts and size caps; changelog for #578-#58…
ajianaz Oct 8, 2026
48979f3
chore(release): v0.16.0 (#592)
ajianaz Oct 8, 2026
e0b1234
feat(review): rule-scoped inline suppression via cora-ignore (#554) (…
ajianaz Oct 8, 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
28 changes: 5 additions & 23 deletions .github/workflows/cla-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ on:
permissions:
pull-requests: write
contents: read
statuses: write

jobs:
cla-check:
Expand Down Expand Up @@ -46,9 +45,10 @@ jobs:
env:
PR_AUTHOR: ${{ github.event.pull_request.user.login }}

- name: Comment on PR (unsigned only)
if: steps.check.outputs.signed != 'true'
uses: actions/github-script@v7
- name: Comment on PR (same-repo, best effort)
if: steps.check.outputs.signed != 'true' && github.event.pull_request.head.repo.full_name == github.repository
continue-on-error: true
uses: actions/github-script@v9
with:
script: |
const author = '${{ github.event.pull_request.user.login }}';
Expand Down Expand Up @@ -96,27 +96,9 @@ jobs:
});
}

- name: Set commit status
uses: actions/github-script@v7
with:
script: |
const signed = '${{ steps.check.outputs.signed }}' === 'true';
await github.rest.repos.createCommitStatus({
owner: context.repo.owner,
repo: context.repo.repo,
sha: '${{ github.event.pull_request.head.sha }}',
state: signed ? 'success' : 'failure',
context: 'CLA Check',
description: signed
? '✅ CLA signed'
: '❌ CLA not signed — sign at https://codecoradev.github.io/cla',
target_url: signed
? 'https://github.com/codecoradev/.github/blob/main/.cla/signatures.json'
: 'https://codecoradev.github.io/cla',
});

- name: Fail if not signed
if: steps.check.outputs.signed != 'true'
run: |
echo "::error title=CLA not signed::Sign the CodeCoraDev CLA at https://codecoradev.github.io/cla — once the signature PR is merged, this check turns green automatically (re-run it or wait ~1 hour)."
echo "❌ CLA not signed by ${{ github.event.pull_request.user.login }}"
exit 1
13 changes: 13 additions & 0 deletions .github/workflows/cora-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,19 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
# SECURITY: this job checks out untrusted PR code while holding
# CORA_API_KEY (pull_request_target). A PR-supplied .cora.yaml could set
# provider.base_url and exfiltrate the key. CORA_BASE_URL (env) always
# takes precedence over project config, so require it to be pinned.
- name: Require pinned CORA_BASE_URL
env:
CORA_BASE_URL: ${{ secrets.CORA_BASE_URL }}
run: |
if [ -z "$CORA_BASE_URL" ]; then
echo "::error::Secret CORA_BASE_URL must be set so a PR-supplied .cora.yaml cannot redirect the API key."
exit 1
fi

- name: Checkout PR head
uses: actions/checkout@v4
with:
Expand Down
6 changes: 3 additions & 3 deletions AGENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ src/
│ ├── mod.rs
│ ├── protocol.rs # JSON-RPC 2.0 types
│ ├── server.rs # Stdio transport + request dispatch
│ └── tools.rs # 15 tool handlers (review, search, brain, debt, ...)
│ └── tools.rs # 18 tool handlers (review, search, brain, debt, ...)
├── formatters/ # Output format implementations
│ ├── mod.rs
│ ├── pretty.rs # Human-readable terminal output
Expand Down Expand Up @@ -448,7 +448,7 @@ When submitting cora to directories, aggregators, or showcases (Trendshift, etc.
>
> Features: diff-based AI code review, static security scanning, quality gate,
> language-specific analyzers, secret detection, custom rule engine, code intelligence
> (symbol index, call graph, semantic search via Brain Mode), MCP server with 15 tools,
> (symbol index, call graph, semantic search via Brain Mode), MCP server with 18 tools,
> SARIF output, and multi-project global database.

### Key Metrics to Mention
Expand All @@ -457,7 +457,7 @@ When submitting cora to directories, aggregators, or showcases (Trendshift, etc.
- Lines of Rust code (26,400+)
- CI checks (10)
- GitHub Marketplace action published
- MCP server with 15 tools
- MCP server with 18 tools
- Apache-2.0 license
- Active development cadence

Expand Down
45 changes: 44 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,46 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.16.0] - 2026-10-08

### Added

- **Rule-scoped inline suppression with `cora-ignore:`.** A source comment such as `// cora-ignore: Hardcoded password or secret in variable` (also `#`, `--`, `/* */`, `<!-- -->`; comma-separated rules) suppresses findings with that exact title (case-insensitive) on the same line, or on the next line when the marker line holds only a comment. Applies to both static-scanner and LLM findings; other rules and other lines stay visible, and a bare `cora-ignore` without rules suppresses nothing. Coexists with `ignore.rules` / `ignore.files` (#554).

### Security

- **Project `.cora.yaml` can no longer redirect your API key.** `provider.base_url` from a discovered project config is ignored unless `CORA_TRUST_PROJECT_CONFIG=1`; `base_url` must be `https` (plain `http` only for loopback); LLM error bodies echoed to the terminal are length-capped (#563).
- **Hardened `cora upgrade` and `install.sh`.** Checksums are matched by exact filename; downloads go to a random 0700 temp dir; only the single `cora` binary entry is extracted (symlink/hardlink entries are rejected); every request has a timeout and a size cap; the version probe does not follow redirects and the tag is validated. `CORA_UPGRADE_SKIP_CHECKSUM` now also requires `CORA_UPGRADE_I_UNDERSTAND=1`. `install.sh` fails closed when the checksums file or entry is missing (opt out with `CORA_SKIP_CHECKSUM=1`) and falls back to `shasum -a 256` (#572).
- **`install.sh` requests now have timeouts and size caps.** Every `curl` call sets `--connect-timeout`, `--max-time` and `--max-filesize` (256 MiB archive, 1 MiB checksums/API JSON), matching `cora upgrade`, so a stalled or hostile server cannot hang or flood the installer (#580).
- **Hardened LLM response handling.** Review and scan system prompts tell the model to treat diff content as untrusted data; the diff fence is longer than any backtick run in the diff; the SSE stream errors on a line over 1 MiB or more than 16 MiB of content (#573).
- **Secrets in test and doc files are no longer invisible to the static security scanner.** Test and doc paths still skip the noisy general rules but now run high-confidence checks (AWS keys, private-key headers, GitHub/Slack/Stripe live tokens; values containing `EXAMPLE` are ignored) (#573).

### Fixed

- **`cora watch` reindexed every source file on each cycle.** Change detection now reindexes only files that changed, and `--filter` restricts what is indexed instead of only gating the trigger (#578, #584).
- **Secrets in test, fixture and example files were skipped entirely by the secrets scanner.** High-confidence secrets are now reported in those paths too (#579, #583).
- **`cora findings list --severity` never matched, and severity colouring never applied.** Both are now case-insensitive (#586, #588).
- **Unit tests wrote to the real data directory and could block on the global vector index lock.** Tests are isolated from the real data dir, and the lock wait is bounded (#587, #589).
- **`cora affected` never matched naming-convention tests.** The CLI took the file extension (`rs`) as the file stem. CLI and MCP now share one query, with escaped `LIKE` wildcards, batched queries and one deduplicated pattern list (`{stem}_test`, `test_{stem}`, `{stem}.test`, `{stem}.spec`, `tests/{stem}`, `__tests__/{stem}`). MCP dead-code now honors `analysis.entry_point_patterns` like the CLI (#575).
- **`cora --config <file> serve` ignored `--config`.** The global option is now passed through (#576).
- **MCP `brain_search` ignored the configured embedding backend.** It now resolves the backend from the project config (#576).
- **Review and indexing disagreed about which files to skip.** Review-time scanners now use the same patterns as the indexer (`ignore.files` + `index.skip_files`) (#576).
- **Review scanners could resolve a different project than indexing** when run from a subdirectory or workspace member. Every entry point now resolves the project root the same way, and review no longer creates an empty `cora.db` when no index exists (#574).
- **Files from different projects overwrote each other's fingerprints** (perpetual reindex). Files are keyed by `(project_id, path)` (schema v8). `callers`/`callees`/`trace` match names exactly instead of by substring (`run` no longer matches `rerun`), and index runs prune stale files and their edges (#565).
- **MCP server robustness.** Stdin is framed as bytes and decoded as UTF-8; garbage input yields a `-32700` parse error; notifications get no response and `notifications/cancelled` no longer stops the server; `tools/call` without a name returns `-32602`; `limit`/`depth`/`min_lines` are clamped; `cora.install` requires `confirm: true` (#564).
- **Chunked review printed "No issues found" while reporting issues** when chunks returned empty summaries (#562).
- **CI:** the CLA check works for fork PRs (#553); `rustls` pinned to 0.23.45 for RUSTSEC-2026-0285 and a deprecated `f32` import removed so clippy passes on rustc 1.99 (#561).

### Changed

- **Ignore patterns now use a single matcher** for the index walk, review scanners, `cora scan --include/--exclude` and `watch --filter`. Behavior changes you may notice in `ignore.files` and related options:
- `**/*.test.ts` no longer matches `footest.ts`, and `vite.config.*` no longer matches `vite.configx`.
- Patterns with a wildcard in the middle (`src/*.rs`, `*.gen.*`) now match; they were silently ignored before, so files you thought were excluded may now actually be excluded.
- Patterns without a `/` also match by basename, in `cora scan --include/--exclude` and `watch --filter` too.
- An invalid glob matches literally instead of being dropped (#577).
- **`cora index --watch` now runs the same watcher as `cora watch`.** It checks every 500 ms (was 2 s), skips hidden directories, and prints `Reindexed: ...` (#576).
- **Internal refactors with no CLI change:** one seam for opening the index (`IndexBridge`, #574), one index-session module (#576), and review split into a deterministic stage testable without an LLM (#577), LLM parse/repair/retry policy unified behind one `Transport` seam (#582), and review-history SQL owned by a single `review_store` module (#585).

## [0.15.0] - 2026-08-31

### Added
Expand Down Expand Up @@ -898,7 +938,10 @@ Benchmarked on the cora-code repository (1,864 symbols, 115 Rust files, x86_64):
- **Cross-platform** — Linux (x86_64, ARM64), macOS (Apple Silicon), Windows (x86_64)
- **MIT License** — fully open source

[Unreleased]: https://github.com/codecoradev/cora-code/compare/v0.13.0...develop
[Unreleased]: https://github.com/codecoradev/cora-code/compare/v0.16.0...develop
[0.16.0]: https://github.com/codecoradev/cora-code/compare/v0.15.0...v0.16.0
[0.15.0]: https://github.com/codecoradev/cora-code/compare/v0.14.0...v0.15.0
[0.14.0]: https://github.com/codecoradev/cora-code/compare/v0.13.0...v0.14.0
[0.13.0]: https://github.com/codecoradev/cora-code/compare/v0.12.0...v0.13.0
[0.12.0]: https://github.com/codecoradev/cora-code/compare/v0.11.1...v0.12.0
[0.11.1]: https://github.com/codecoradev/cora-code/compare/v0.11.0...v0.11.1
Expand Down
10 changes: 5 additions & 5 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "cora-code"
version = "0.15.0"
version = "0.16.0"
edition = "2024"
description = "CLI-first AI code review — BYOK, diff/scan/branch, pre-commit hooks"
license = "Apache-2.0"
Expand Down Expand Up @@ -93,6 +93,7 @@ tree-sitter-ruby = { version = "0.23", optional = true }
tree-sitter-php = { version = "0.24", optional = true }
tree-sitter-scala = { version = "0.26", optional = true }
tree-sitter-javascript = { version = "0.25", optional = true }
tempfile = "3"

[features]
default = ["tree-sitter"]
Expand All @@ -116,7 +117,6 @@ tree-sitter = [
[dev-dependencies]
assert_cmd = "2"
predicates = "3"
tempfile = "3"
tokio-test = "0.4"

[profile.release]
Expand Down
18 changes: 18 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,24 @@ brain:

> **Note:** If you select `pretrained` but cora was built without the `pretrained-embed` feature, it falls back to `hashing` with a warning.

## Inline Suppression (`cora-ignore`)

`ignore.rules` hides a finding title across the whole project. To suppress one finding at one location, put a marker in a source comment:

```ts
const bytesPerToken = 4; // cora-ignore: Hardcoded password or secret in variable

// cora-ignore: Rule A, Rule B (comment-only line: applies to the next line)
const next = compute();
```

- Syntax: `cora-ignore: <rule>[, <rule>...]`, matched as a substring of the line, so it works after `//`, `#`, `--`, inside `/* */` or `<!-- -->`, etc. The marker is case-insensitive.
- `<rule>` is the finding **title**, matched exactly and case-insensitively (not a substring, unlike `ignore.rules`).
- Scope: the same line; plus the next line when the marker line contains only a comment.
- A bare `cora-ignore` (no `:` or empty rule list) suppresses nothing, so every suppression names its rule.
- Applies to both deterministic scanner and LLM findings, in `cora review`. Other rules on the same line and the rest of the project stay visible. Works alongside `ignore.rules` and `ignore.files`.
- Markers are read from the diff (added and context lines); a marker outside the diff's context window is not seen.

## Ignore Files

Exclude files or directories from **all** cora operations — review, scan, and indexing. This is the broadest exclusion mechanism.
Expand Down
3 changes: 3 additions & 0 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,9 @@ Environment variables override configuration file settings:
| `CORA_MODEL` | Override the model name | No |
| `CORA_BASE_URL` | Override the API base URL | No |
| `CORA_CONFIG` | Path to alternative config file | No |
| `CORA_TRUST_PROJECT_CONFIG` | Set to `1` to honour `provider.base_url` from a discovered project `.cora.yaml` (otherwise ignored with a warning, since it would receive your API key) | No |

> **Security:** `provider.base_url` must be `https://` (plain `http://` only for `localhost`, `127.0.0.1`, `[::1]`). A `base_url` found only in a discovered project `.cora.yaml` is ignored unless `CORA_TRUST_PROJECT_CONFIG=1`; set it via `--base-url`, `CORA_BASE_URL`, an explicit `--config` file, or your global config instead.

Provider-specific keys are auto-detected: `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `GROQ_API_KEY`, `ZAI_API_KEY`

Expand Down
50 changes: 35 additions & 15 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -50,15 +50,15 @@ detect_arch() {
# Fallback: the GitHub REST API (subject to 60 req/hour anonymous limit).
get_latest_version() {
# Try the web redirect first — does not count against the API rate limit.
VERSION=$(curl -sI "https://github.com/${REPO}/releases/latest" \
VERSION=$(curl -sI --connect-timeout 10 --max-time 30 "https://github.com/${REPO}/releases/latest" \
| grep -i '^location:' \
| sed -E 's|.*/tag/([^[:space:]]+).*|\1|' \
| tr -d '\r')

# Fallback to the REST API if the redirect didn't yield a tag.
if [ -z "$VERSION" ]; then
warn "Redirect lookup failed, falling back to GitHub API..."
VERSION=$(curl -fsSL "https://api.github.com/repos/${REPO}/releases/latest" \
VERSION=$(curl -fsSL --connect-timeout 10 --max-time 30 --max-filesize 1048576 "https://api.github.com/repos/${REPO}/releases/latest" \
| grep '"tag_name":' \
| sed -E 's/.*"([^"]+)".*/\1/')
fi
Expand Down Expand Up @@ -104,26 +104,37 @@ install() {
CHECKSUM_FILE="${TEMP_DIR}/checksums-sha256.txt"

info "Downloading from: $DOWNLOAD_URL"
if ! curl -fsSL "$DOWNLOAD_URL" -o "$ARCHIVE"; then
if ! curl -fsSL --connect-timeout 10 --max-time 600 --max-filesize 268435456 "$DOWNLOAD_URL" -o "$ARCHIVE"; then
error "Failed to download ${ARCHIVE_NAME}"
fi

# Verify SHA256 checksum (prevents MITM / corrupted download).
info "Downloading checksums..."
if curl -fsSL "$CHECKSUMS_URL" -o "$CHECKSUM_FILE"; then
# Mandatory: a missing checksums file or entry is fatal unless the user
# explicitly opts out with CORA_SKIP_CHECKSUM=1.
if [ "${CORA_SKIP_CHECKSUM:-}" = "1" ]; then
warn "CORA_SKIP_CHECKSUM=1 set - checksum verification DISABLED. The binary is NOT verified."
else
info "Downloading checksums..."
if ! curl -fsSL --connect-timeout 10 --max-time 30 --max-filesize 1048576 "$CHECKSUMS_URL" -o "$CHECKSUM_FILE"; then
error "Failed to download checksums. Refusing to install an unverified binary (set CORA_SKIP_CHECKSUM=1 to override, unsafe)."
fi
info "Verifying SHA256 checksum..."
EXPECTED=$(grep -F "$ARCHIVE_NAME" "$CHECKSUM_FILE" | awk '{print $1}')
if [ -n "$EXPECTED" ]; then
# Exact filename match (optionally prefixed with '*' or './').
EXPECTED=$(awk -v n="$ARCHIVE_NAME" '{f=$2; sub(/^\*/, "", f); sub(/^\.\//, "", f); if (f == n) {print $1; exit}}' "$CHECKSUM_FILE")
if [ -z "$EXPECTED" ]; then
error "Checksum for ${ARCHIVE_NAME} not found in checksums file. Refusing to install (set CORA_SKIP_CHECKSUM=1 to override, unsafe)."
fi
if command -v sha256sum >/dev/null 2>&1; then
ACTUAL=$(sha256sum "$ARCHIVE" | awk '{print $1}')
if [ "$ACTUAL" != "$EXPECTED" ]; then
error "Checksum mismatch! Expected: ${EXPECTED}, got: ${ACTUAL}"
fi
info "Checksum verified: $EXPECTED"
elif command -v shasum >/dev/null 2>&1; then
ACTUAL=$(shasum -a 256 "$ARCHIVE" | awk '{print $1}')
else
warn "Checksum for ${ARCHIVE_NAME} not found in checksums file — skipping verification"
error "Neither sha256sum nor shasum found; cannot verify checksum (set CORA_SKIP_CHECKSUM=1 to override, unsafe)."
fi
else
warn "Failed to download checksums — skipping verification"
if [ "$ACTUAL" != "$EXPECTED" ]; then
error "Checksum mismatch! Expected: ${EXPECTED}, got: ${ACTUAL}"
fi
info "Checksum verified: $EXPECTED"
fi

# Verify archive contents before extraction (CWE-22 path traversal).
Expand All @@ -133,8 +144,17 @@ install() {
error "Archive contains unsafe paths (absolute or directory traversal) — refusing to extract"
fi

# Reject symlink/hardlink entries (first char of the verbose mode string:
# 'l' = symlink; bsdtar lists hardlinks with 'h').
if tar -tvzf "$ARCHIVE" | grep -qE '^[lh]'; then
error "Archive contains symlink/hardlink entries - refusing to extract"
fi

info "Extracting..."
tar -xzf "$ARCHIVE" -C "$TEMP_DIR"
# Extract only the single binary entry (tolerate an optional ./ prefix).
tar -xzf "$ARCHIVE" -C "$TEMP_DIR" "${BINARY_NAME}" 2>/dev/null \
|| tar -xzf "$ARCHIVE" -C "$TEMP_DIR" "./${BINARY_NAME}" \
|| error "Binary '${BINARY_NAME}' not found in archive"

mkdir -p "$INSTALL_DIR"
mv "${TEMP_DIR}/${BINARY_NAME}" "${INSTALL_DIR}/"
Expand Down
Loading
Loading