v0.5.20
What's new in v0.5.20
v0.5.20 closes the runtime side of the ack workflow opened by v0.5.17. The CI side (TOML in repo, versioned, change via PR review) is now complemented by a daemon HTTP API (POST / DELETE / GET on /api/findings/{signature}/ack, GET /api/acks) backed by a JSONL append-only file at ~/.local/share/perf-sentinel/acks.jsonl. An SRE on call can now mute a finding without a PR cycle on the application repo, while the team-agreed permanent acks in the TOML baseline stay immutable from the runtime side. The two sources are unioned at query time with TOML winning on conflict, so an operator who tries to write a daemon ack on a signature already covered by the TOML baseline gets a 409 Conflict rather than a silent no-op.
A behavior change ships alongside: GET /api/findings now filters out acked findings by default (mirroring the CLI 0.5.17 --acknowledgments default). Pass ?include_acked=true to restore the pre-0.5.20 shape, with each acked entry annotated by acknowledged_by: { source: "toml" | "daemon", ... } so clients can render both sources distinctly. The two diagnostic endpoints /api/findings/{trace_id} and /api/export/report keep their previous shape: per-trace and full-report views may need to surface acked findings even on the default path.
The daemon ack store is hardened against local-filesystem attacks (Trojan Source defense extended with U+00AD and U+2060-U+2064, O_NOFOLLOW plus post-open mode-& 0o077 check on Unix, mode 0o600 on creation, atomic compaction via tmp + rename). Optional X-API-Key authentication on POST and DELETE (constant-time compare via subtle::ConstantTimeEq) protects deployments that bind the daemon to a non-loopback interface. The default 127.0.0.1 posture from previous releases is unchanged. Per-field caps (256 bytes on by, 1024 bytes on reason, 4 KiB per JSONL line, 64 MiB on the file, 10 000 active acks) bound disk and memory usage under adversarial input.
The release also clarifies the Report.warning_details lifecycle introduced in 0.5.19: cold_start is documented as transient (auto-clears on first batch), ingestion_drops as sticky (cumulative since daemon start, cleared only on restart). New transient-vs-sticky table in docs/METRICS.md plus operator workflow guidance in docs/RUNBOOK.md and the FR mirrors. This closes the third B3 lab followup investigated 2026-05-04, alongside two earlier followups (process collector cfg-gating, OTLP rejected reasons coverage) re-audited and confirmed as no action needed.
Helm chart 0.2.23 ships in lockstep, bumping the default daemon image tag to ghcr.io/robintra/perf-sentinel:0.5.20.
Added
- Three new daemon endpoints on the existing HTTP query API:
POST /api/findings/{signature}/ackto acknowledge a finding at runtime,DELETE /api/findings/{signature}/ackto revoke, andGET /api/acksto list active runtime acks. The signature uses the same canonical<finding_type>:<service>:<sanitized_endpoint>:<sha256-prefix>format as the CI TOML workflow, so an operator can copy a value fromanalyze --format json | jq '.findings[].signature'straight into a curl call. - JSONL append-only persistence at
~/.local/share/perf-sentinel/acks.jsonlby default (configurable via[daemon.ack] storage_path). Eachackand eachunackis one line,tail -f-able for live audit. The file is replayed and atomically rewritten viatmp+renameat every daemon restart, so anack/unackchurn loop cannot accumulate forever. ResolvedTomlAckwrapper indaemon::query_apipre-parses the TOMLexpires_at: Option<String>intoOption<DateTime<Utc>>once at startup. The hot-pathlookup_acknow does a single datetime comparison per finding instead of achrono::NaiveDate::parse_from_strper match.AckStore::snapshot_activereturnsArc<HashMap<String, AckEntry>>(a single atomic refcount inc, no data copy). The active map sits behindRwLock<Arc<HashMap>>, so concurrentGET /api/findingspolls take a cheap clone of theArcand never block writers. Writers pay an O(N) HashMap clone outside the write lock to keep readers wait-free.- Optional API key authentication via
[daemon.ack] api_key. When set,POSTandDELETErequire anX-API-Keyheader that matches the configured secret (constant-time compared viasubtle::ConstantTimeEq).GET /api/acksandGET /api/findingsstay unauthenticated by design (loopback reads). Empty strings and keys shorter than 12 characters are rejected at config load. - Audit
byfield resolved with priority order on every write:X-User-IdHTTP header, then JSON bodyby, then"anonymous"fallback. BiDi and invisible Unicode characters are stripped before persistence (crate::report::sarif::strip_bidi_and_invisible), with the sanitization set extended to cover U+00AD (soft hyphen) and U+2060-U+2064 (word joiner plus invisible operators). ?include_acked=truequery parameter onGET /api/findings. When passed, returns the full set with each acked entry annotated byacknowledged_by: { source, ... }. Thesourcediscriminant is"toml"for CI baseline acks and"daemon"for runtime acks.- TOML and JSONL interop: the daemon reads
.perf-sentinel-acknowledgments.toml(path configurable via[daemon.ack] toml_path) at startup as immutable baseline and unions with the JSONL store. TOML wins on conflict. APOSTon a signature already covered by an active TOML ack returns409 Conflictwith a clear message pointing the operator at the TOML file rather than silently writing a JSONL line that would be shadowed on read. - Hardened file open on Unix:
O_NOFOLLOWplus atokio::fs::symlink_metadatapre-check (SymlinkRefusedtyped error rather than the kernel'sELOOP), mode0o600on creation, and a post-openpermissions().mode() & 0o077check that rejects pre-existing weak-mode files. The startup compaction unconditionally rewrites the file with mode0o600, eliminating any weak-permission window an attacker could plant before daemon launch. - Hard caps: 64 MiB on the JSONL file, 4 KiB per line (allocation-bounded via byte-by-byte read, not just post-allocation check), 10 000 active acks, 1024 bytes on the signature, 256 bytes on
by, 1024 bytes onreason, with UTF-8-boundary truncation shared with the existing span-event sanitization (crate::event::truncate_field). - 20 unit tests in
crates/sentinel-core/src/daemon/ack.rscovering replay, compaction, expiry, BiDi stripping, signature validation, file-size caps, parse errors with line numbers, concurrent-write integrity, symlink refusal, and weak-permission reset at startup. - 7 integration tests in
crates/sentinel-core/src/daemon/query_api.rscoveringPOST/DELETE/GET, the?include_ackedflag, TOML-wins conflict resolution (now returning409), theX-API-Keyauth path, and a regression test that pins the JSON shape ofFindingResponseagainst future serde-flatten collisions. - 3 config tests covering the new
[daemon.ack]section and theapi_keylength validator (empty rejected, < 12 chars rejected, 12+ accepted). docs/QUERY-API.mdanddocs/FR/QUERY-API-FR.md: full reference for the three new endpoints with curl examples, request and response shapes, status code matrix, and a "TOML and JSONL interop" section that documents the conflict resolution rules.docs/RUNBOOK.mdanddocs/FR/RUNBOOK-FR.md"Acknowledging findings at runtime" section with three curl examples (no-auth, auth via TOML config, churn-and-compaction), plus the transient-vs-sticky lifecycle clarification oncold_startandingestion_drops.docs/METRICS.mdanddocs/FR/METRICS-FR.md: transient-vs-stickyReport.warning_detailstable.docs/CONFIGURATION.mdanddocs/FR/CONFIGURATION-FR.md: new[daemon.ack]section with field reference and rationale (why no/tmpfallback, why mode0600, what theapi_keylength floor means)..gitleaks.tomlallowlist for the documented finding-signature pattern, anchored on the actualFindingTypediscriminants. Matches against the full source line viaregexTarget = "line", never on the bare 16-hex tail in isolation, so a real high-entropy secret nearby is still detected.
Changed
GET /api/findingsdefault behavior: filters out acked findings by default (CI TOML plus daemon JSONL union). Pass?include_acked=trueto restore the pre-0.5.20 behavior. Aligned with the CLI 0.5.17--acknowledgmentsdefault for consistency. The per-trace/api/findings/{trace_id}and/api/export/reportendpoints keep their previous shape.crate::acknowledgments::is_ack_activepromoted topub(crate)so the daemon side reuses the same TOML expiry check without duplicating thechrono::NaiveDateparse logic.crate::event::truncate_fieldpromoted topub(crate)and reused bydaemon::ackfor thebyandreasonfield caps. Single source of truth for byte-and-char-boundary truncation across the crate.init_ack_resourceserror policy split by source: when the operator explicitly set[daemon.ack] storage_pathor[daemon.ack] toml_path, a load failure on that path is fatal at startup with a typedDaemonError::AckTomlLoad { path, source }orDaemonError::AckStoreInit { path, source }. When the path was resolved from the default (dirs::data_local_dir()or.perf-sentinel-acknowledgments.tomlin CWD), failures are logged at WARN and the daemon stays up so OTLP ingestion,/metricsand/healthkeep serving. The three ack endpoints return503 Service Unavailableuntil the operator fixes the default path and restarts.- Helm chart
0.2.22to0.2.23,appVersion0.5.19to0.5.20, default daemon image tag points atghcr.io/robintra/perf-sentinel:0.5.20. Theartifacthub.io/imagesannotation is updated in lockstep.
Behavior
GET /api/findingsshape change is opt-out, not opt-in. Existing clients that scrape this endpoint and alert on the count will silently miss critical findings if those findings are acked. Audit your monitoring after the upgrade and either pass?include_acked=trueor rely on the newacknowledged_byannotation to render both sets.- Backward compatibility on the other endpoints.
/api/findings/{trace_id},/api/explain/{trace_id},/api/correlations,/api/status,/api/export/reportkeep their pre-0.5.20 JSON shape byte-for-byte. - Default-path ack init failure is non-fatal. A daemon launched without
[daemon.ack] storage_pathwhose default location cannot be resolved (missingHOME, denied write perms on~/.local/share) logs a WARN and continues without an ack store. Operator-supplied paths still fail loud at startup, the right place for typo and permission diagnosis. - TOML wins on conflict, with a loud signal. A
POSTon a TOML-acked signature returns409 Conflictinstead of writing a no-op JSONL line. Pre-0.5.20 the daemon-side write would have been silently shadowed by the TOML baseline on read. [daemon.ack] api_keyshorter than 12 characters is rejected at config load. Pre-0.5.20 the shipped behavior was warn-only at <16. The daemon does not rate-limitPOST /ack, so a co-resident attacker on the loopback interface could brute-force shorter keys in a tractable window. 12 characters of[a-z0-9-]puts the brute-force horizon past 10^17 attempts, well beyond any realistic deployment.- Process metrics, OTLP rejected counter, structured warnings from 0.5.19 are unchanged. The transient-vs-sticky doc clarification is documentation-only.
Documentation
- New "Acknowledging findings at runtime" section in
docs/RUNBOOK.mdanddocs/FR/RUNBOOK-FR.mdwith three curl examples and a TOML-vs-JSONL audit-source comparison table. - New
[daemon.ack]section indocs/CONFIGURATION.mdanddocs/FR/CONFIGURATION-FR.mdwith field reference, rationale for the no-/tmpfallback, and the mode0o600discipline. - Three new endpoints documented in
docs/QUERY-API.mdanddocs/FR/QUERY-API-FR.mdwith request and response shapes, status code matrix, and the TOML and JSONL interop section. - Transient-vs-sticky table for
Report.warning_detailsindocs/METRICS.mdanddocs/FR/METRICS-FR.md, with operator workflow text in the corresponding RUNBOOK entries.
Install
Prebuilt binaries (Linux amd64 / arm64, macOS arm64, Windows amd64):
curl -LO https://github.com/robintra/perf-sentinel/releases/download/v0.5.20/perf-sentinel-linux-amd64
chmod +x perf-sentinel-linux-amd64
sudo mv perf-sentinel-linux-amd64 /usr/local/bin/perf-sentinelLinux binaries are statically linked against musl and run on any distribution (Alpine, Debian, RHEL, Ubuntu any version) regardless of glibc version, and inside FROM scratch images.
From crates.io:
cargo install perf-sentinel --version 0.5.20Docker:
docker run --rm -p 4317:4317 -p 4318:4318 \
ghcr.io/robintra/perf-sentinel:0.5.20 watch --listen-address 0.0.0.0Helm chart 0.2.23 ships alongside, see the matching chart-v0.2.23 release for the chart-side details.
What's Changed
- chore(ci)(deps): bump SonarSource/sonarqube-scan-action from 7.1.0 to 8.0.0 in the other-actions group by @dependabot[bot] in #13
- chore(ci)(deps): bump taiki-e/install-action from 2.75.22 to 2.75.30 in the ci-actions group by @dependabot[bot] in #11
- chore(ci)(deps): bump github/codeql-action from 4.35.2 to 4.35.3 in the security-actions group across 1 directory by @dependabot[bot] in #12
Full Changelog: v0.5.19...v0.5.20