Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
39 changes: 36 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,43 @@ jobs:

- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2

- name: Install nextest
run: cargo install cargo-nextest --locked

- name: Run tests
# --locked: CI must never silently re-resolve Cargo.lock (full-review
# BM8 — an unlocked run is how rmcp drifted to 1.7 unnoticed).
run: cargo test --locked
# nextest gives each test its own process, which is the only safe way
# to run the env-mutating setup/heartbeat/plugin tests — libtest's
# shared-process model lets them race. This matches `just test` and the
# coverage job below. --locked: CI must never silently re-resolve
# Cargo.lock (full-review BM8 — an unlocked run is how rmcp drifted to
# 1.7 unnoticed). env -u mirrors the Justfile's canonical invocation.
run: env -u CORTEX_API_TOKEN -u NO_AUTH cargo nextest run --locked

- name: Run doctests
# nextest does not execute doctests; run them explicitly so coverage
# matches the previous `cargo test` invocation.
run: cargo test --doc --locked

coverage:
name: Coverage
runs-on: ubuntu-latest
needs: [test]
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4

- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
with:
components: llvm-tools-preview

- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2

- name: Install coverage tools
run: |
cargo install cargo-llvm-cov --locked
cargo install cargo-nextest --locked

- name: Run coverage summary
run: env -u CORTEX_API_TOKEN -u NO_AUTH cargo llvm-cov nextest --summary-only --locked

deny:
name: Dependency Check (cargo-deny)
Expand Down
9 changes: 8 additions & 1 deletion .github/workflows/docker-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,15 @@ jobs:
- name: Clippy
run: cargo clippy --all-targets --locked -- -D warnings

- name: Install nextest
run: cargo install cargo-nextest --locked

- name: Run tests
run: cargo test --locked
# nextest isolates env-mutating tests per-process (matches `just test`).
run: env -u CORTEX_API_TOKEN -u NO_AUTH cargo nextest run --locked

- name: Run doctests
run: cargo test --doc --locked

build-and-push:
runs-on: ubuntu-latest
Expand Down
9 changes: 8 additions & 1 deletion .github/workflows/publish-crates.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,15 @@ jobs:
- name: Clippy
run: cargo clippy --all-targets --locked -- -D warnings

- name: Install nextest
run: cargo install cargo-nextest --locked

- name: Run tests
run: cargo test --locked
# nextest isolates env-mutating tests per-process (matches `just test`).
run: env -u CORTEX_API_TOKEN -u NO_AUTH cargo nextest run --locked

- name: Run doctests
run: cargo test --doc --locked

- name: Check version sync and changelog
run: bash scripts/check-version-sync.sh --require-changelog
Expand Down
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [1.21.4] - 2026-06-15

### Added

- Expand focused unit coverage across the setup, runtime, inventory, notification, and ingest layers to lift total line coverage to ~80%. New sidecar suites cover setup doctor/agent/first-run/systemd/AI-index/AI-watch/heartbeat-agent helpers, CLI global-flag and local-only routing, command-dispatch guardrails, runtime auth and inventory-refresh seams, inventory device/project/remote-config collectors, notification evaluator/dispatcher/queue/digest SQL helpers, Aurora logging helpers, receiver listener wiring, journald/syslog formatting, and error-signature DB contracts.
- Add a mocked Docker Engine HTTP fixture for the legacy central pull client plus log-frame mapping, doc-drift tests for Docker ingest guidance and coverage tooling, and deterministic admin REST coverage for `/api/file-tails` in the live smoke harness when `CORTEX_API_ADMIN_TOKEN` is set.

### Changed

- Reframe Docker log ingest docs around the host-local cortex agent path, documenting `CORTEX_DOCKER_*` as legacy central-pull compatibility for explicit Docker HTTP endpoints.
- Add repeatable coverage commands (`just coverage`, `just coverage-html`) and a CI coverage-summary job using `cargo-llvm-cov` with nextest.
- Extract the `dispatch_surface_gap` argument-mapper tests into a sidecar `dispatch_surface_gap_tests.rs`, honouring the repo's sidecar-test convention and keeping the production module under the 500-line size limit.
- Run CI tests via `cargo nextest run --locked` (with an explicit `cargo test --doc` step) in the `ci`, `docker-publish`, and `publish-crates` workflows, matching `just test` and the coverage job. The expanded env-mutating setup/heartbeat/plugin tests require per-process isolation, which libtest's shared-process model cannot provide.

### Fixed

- Drop empty host entries from `cortex deploy agent --hosts` comma lists and assert the evaluator future-row age test against `received_at`.
- Relax the graph-inventory lock-scope test timeout so coverage-instrumented builds do not fail on instrumentation overhead while still proving the write lock is released promptly.

## [1.21.3] - 2026-06-15

### Security
Expand Down
12 changes: 8 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ Key modules in `src/` (most are directories with sidecar `*_tests.rs` files):
| `syslog/` | UDP + TCP listeners, RFC 3164/5424 parsing, mpsc batch writer |
| `mcp/` | RMCP Streamable HTTP server, single `cortex` tool with action dispatch |
| `api.rs` | Always-on non-MCP REST API (`/api/*`); requires `CORTEX_API_TOKEN` at startup |
| `docker_ingest/` | Docker container log ingestion via remote docker-socket-proxy endpoints |
| `agent/`, `heartbeat_agent.rs` | Host-local cortex agent: heartbeat, syslog forwarding, and Docker log streaming from the local Docker socket |
| `docker_ingest/` | Legacy central pull compatibility path for explicit remote Docker Engine HTTP endpoints |
| `main.rs` | Entrypoint: `serve mcp` (full server with ingest) or `mcp` (stdio query-only) |

Tests: unit tests live in sidecar files beside their source modules (e.g. `src/db/queries_tests.rs`). Source files keep only the `#[cfg(test)] #[path = "..._tests.rs"] mod tests;` hook, so sidecar tests compile as module-local unit tests with `use super::*` access to private items. Run with `cargo test`.
Expand Down Expand Up @@ -169,9 +170,12 @@ CORTEX_API_TOKEN=your-api-token # REQUIRED at startup — /api/* is alwa
# Stored in the parent directory of CORTEX_DB_PATH as file-tails.json.
# Manage with: cortex file-tail list|status|add|remove|enable|disable

# Docker container log ingestion (disabled by default)
CORTEX_DOCKER_INGEST_ENABLED=false # set true to ingest from docker-socket-proxy hosts
CORTEX_DOCKER_HOSTS=host-a,host-b # comma-separated hostnames → http://<host>:2375
# Legacy central pull Docker ingestion compatibility mode (disabled by default)
# Current deployments use the host-local cortex agent, which streams Docker logs
# from unix:///var/run/docker.sock on each host. Keep CORTEX_DOCKER_* for
# compatibility fixtures or explicit remote Docker Engine HTTP endpoints only.
CORTEX_DOCKER_INGEST_ENABLED=false
CORTEX_DOCKER_HOSTS=host-a,host-b # comma-separated hosts → http://<host>:2375
CORTEX_DOCKER_RECONNECT_INITIAL_MS=1000
CORTEX_DOCKER_RECONNECT_MAX_MS=30000

Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

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

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "cortex"
version = "1.21.3"
version = "1.21.4"
edition = "2024"
rust-version = "1.86"
license = "MIT"
Expand Down
6 changes: 6 additions & 0 deletions Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ fmt:
test:
env -u CORTEX_API_TOKEN -u NO_AUTH cargo nextest run

coverage:
env -u CORTEX_API_TOKEN -u NO_AUTH -u CORTEX_DB_PATH cargo llvm-cov nextest --summary-only

coverage-html:
env -u CORTEX_API_TOKEN -u NO_AUTH -u CORTEX_DB_PATH cargo llvm-cov nextest --html

# Verify Cargo wrapper binary sync behavior
test-cargo-wrapper:
scripts/test-cargo-rustc-wrapper.sh
Expand Down
18 changes: 10 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -582,7 +582,7 @@ for token flags, sensitive assignments, Authorization headers, URL userinfo,
`curl -u`, and private-key blocks before storage. Scrubbing is best-effort, not
a compliance boundary.

**Important:** `hostname` is taken from the syslog message body, which any LAN device can set to an arbitrary value over UDP. For syslog entries, `source_ip` is the only trustworthy network identifier. For Docker ingest entries, `source_ip` identifies the configured Docker ingest host/container/stream and should be trusted only as far as the configured docker-socket-proxy endpoint and network path are trusted. `metadata_json` preserves source-specific context for debugging and correlation, but it is not an authorization boundary. Retention cutoffs use `received_at` (server clock) so that devices with misconfigured clocks cannot cause premature or indefinite log retention.
**Important:** `hostname` is taken from the syslog message body, which any LAN device can set to an arbitrary value over UDP. For syslog entries, `source_ip` is the only trustworthy network identifier. For Docker log entries from the current host-local cortex agent, trust follows the deployed agent host and its local Docker socket access. For legacy central pull entries, `source_ip` identifies the configured Docker host/container/stream and should be trusted only as far as the explicit remote Docker Engine endpoint and network path are trusted. `metadata_json` preserves source-specific context for debugging and correlation, but it is not an authorization boundary. Retention cutoffs use `received_at` (server clock) so that devices with misconfigured clocks cannot cause premature or indefinite log retention.

### Severity levels

Expand Down Expand Up @@ -665,7 +665,7 @@ Install as a Claude Code plugin. The plugin handles deployment automatically —
| `retention_days` | no | `90` | `0` = keep forever |
| `batch_size` | no | `100` | Number of parsed messages per SQLite batch |
| `write_channel_capacity` | no | `10000` | Internal parsed-message queue capacity before listener backpressure |
| `docker_ingest_enabled` | no | `false` | Pull container logs from remote `docker-socket-proxy` endpoints |
| `docker_ingest_enabled` | no | `false` | Legacy central pull compatibility mode for explicit remote Docker Engine endpoints; current deployments use the host-local agent |
| `fleet_hosts` | no | — | SSH aliases of fleet hosts. Used for Docker ingest (when enabled, each becomes `http://<alias>:2375`) and the `cortex-deploy-dropins` skill |

**SessionStart hook automation** (in server mode):
Expand Down Expand Up @@ -782,13 +782,15 @@ The plain JSON API is always mounted under `/api/*` on the same HTTP listener an
| `CORTEX_FLUSH_INTERVAL` | no | `500` | Batch flush interval in milliseconds |
| `CORTEX_WRITE_CHANNEL_CAPACITY` | no | `10000` | Internal parsed-message queue capacity |

#### Docker socket-proxy ingest
#### Docker log ingest

Optional pull-based Docker log ingestion keeps each remote host on its normal Docker logging driver and has cortex read container stdout/stderr through read-only `docker-socket-proxy` endpoints. This avoids configuring Docker's daemon-level syslog driver and does not block container startup when cortex is down.
The current deployment path is the host-local cortex agent. Each deployed agent reads Docker logs from that host's local Docker socket (`unix:///var/run/docker.sock`) and forwards the normalized rows into cortex. This keeps Docker's normal local logging behavior intact, avoids daemon-level syslog drivers, and does not require exposing a Docker API endpoint on the network.

The `CORTEX_DOCKER_*` settings below remain as a legacy central pull compatibility mode for explicit remote Docker Engine HTTP endpoints. Use them for fixtures or transitional deployments where cortex itself should connect to a Docker-compatible API. Older deployments used `docker-socket-proxy` for this endpoint, but that is no longer the recommended homelab path.

| Variable | Required | Default | Description |
|----------|----------|---------|-------------|
| `CORTEX_DOCKER_INGEST_ENABLED` | no | `false` | Enable remote Docker log ingestion |
| `CORTEX_DOCKER_INGEST_ENABLED` | no | `false` | Enable legacy central pull Docker log ingestion |
| `CORTEX_DOCKER_HOSTS` | one of the two | — | Comma-separated hostnames; each becomes `http://<name>:2375` with `allow_insecure_http = true`. Takes priority over `CORTEX_DOCKER_HOSTS_FILE`. |
| `CORTEX_DOCKER_HOSTS_FILE` | one of the two | — | Path to a TOML file with a `[[hosts]]` array (use when you need per-host `base_url` or TLS). If the file does not exist, a warning is logged and no hosts are loaded — the container will not crash. Mount the file into the container (e.g. under `/cortex-home` via `CORTEX_HOME_VOLUME`). |
| `CORTEX_DOCKER_RECONNECT_INITIAL_MS` | no | `1000` | Initial reconnect delay after host stream failure |
Expand All @@ -808,9 +810,9 @@ base_url = "http://app-host-b:2375"
allow_insecure_http = true
```

The docker-socket-proxy side only needs read access to containers, events, ping, and version endpoints: `CONTAINERS=1`, `EVENTS=1`, `PING=1`, `VERSION=1`, `POST=0`. `CONTAINERS=1` exposes the broader read-only Docker container API to anything that can reach the proxy, so bind it only on a trusted private network, firewall it to cortex, or put it behind authenticated TLS. Plain `http://` endpoints require `allow_insecure_http = true` in the hosts file so that this trust decision is explicit.
If this legacy pull path points at `docker-socket-proxy`, the proxy side only needs read access to containers, events, ping, and version endpoints: `CONTAINERS=1`, `EVENTS=1`, `PING=1`, `VERSION=1`, `POST=0`. `CONTAINERS=1` exposes the broader read-only Docker container API to anything that can reach the proxy, so bind it only on a trusted private network, firewall it to cortex, or put it behind authenticated TLS. Plain `http://` endpoints require `allow_insecure_http = true` in the hosts file so that this trust decision is explicit.

Docker ingest is intentionally not part of the default smoke test because it needs a live docker-socket-proxy-compatible endpoint and container log stream. For integration testing, run cortex with `CORTEX_DOCKER_INGEST_ENABLED=true` against a disposable docker-socket-proxy or mocked Docker HTTP fixture, emit a unique line from a short-lived container, then verify it with `cortex search` or `mcporter call ... action=search`. Container stdout/stderr rows use `source_ip=docker://<host>/<container>/<stream>`. Container lifecycle rows for actions such as `create`, `start`, `restart`, `die`, `stop`, `destroy`, `rename`, `oom`, and `health_status:*` use `source_ip=docker-event://<host>/<container>/<sanitized-action>`, `facility=docker`, and preserve the raw Docker event JSON.
Docker log ingest has two test boundaries: agent parity tests verify that deployed agents preserve local Docker socket streaming, and the legacy central pull client has a mocked Docker HTTP fixture. The default smoke test stays focused on live UDP/TCP syslog, MCP/REST actions, CLI parity, and managed file-tail ingest. For a full legacy pull integration check, run cortex with `CORTEX_DOCKER_INGEST_ENABLED=true` against a disposable Docker-compatible HTTP fixture, emit a unique line from a short-lived container, then verify it with `cortex search` or `mcporter call ... action=search`. Container stdout/stderr rows use `source_ip=docker://<host>/<container>/<stream>`. Container lifecycle rows for actions such as `create`, `start`, `restart`, `die`, `stop`, `destroy`, `rename`, `oom`, and `health_status:*` use `source_ip=docker-event://<host>/<container>/<sanitized-action>`, `facility=docker`, and preserve the raw Docker event JSON.

#### Managed file-tail ingest

Expand Down Expand Up @@ -1399,7 +1401,7 @@ Run the live smoke test against a running server:
bash scripts/smoke-test.sh
```

The smoke test seeds UDP and TCP syslog messages and verifies MCP search/tail results. Docker ingest coverage is handled by the explicit integration path described in the Docker socket-proxy ingest section because it requires an external Docker-compatible log endpoint.
The smoke test seeds UDP and TCP syslog messages and verifies MCP search/tail results. Docker log coverage is split by path: host-local agent parity is covered by agent deployment tests, the default live smoke covers file-tail/REST/CLI paths, and legacy central pull coverage uses the mocked Docker HTTP fixture or an explicit Docker-compatible endpoint.

---

Expand Down
2 changes: 1 addition & 1 deletion docker-compose.prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ services:
# Default tag is kept in sync by scripts/bump-version.sh (version canon);
# previously this was frozen at 1.0.0 while migrations moved forward —
# a stale binary against a newer schema (full-review OH1).
image: ghcr.io/jmagar/cortex:${CORTEX_VERSION:-1.21.3}
image: ghcr.io/jmagar/cortex:${CORTEX_VERSION:-1.21.4}
container_name: cortex
user: "${CORTEX_UID:-1000}:${CORTEX_GID:-1000}"
env_file:
Expand Down
Loading
Loading