CHANGELOG.md
Rama main · réplica de lectura del repositorio original.
# Changelog
All notable changes to **Aegis Latent Core** are documented in this file.
**Last verified:** 2026-10-30 UTC
**Published release:** `v5.0.3`, fourteen synchronized anchors, **Aegis Commercial License 1.0**, published 2026-10-30. GitHub Release, both PyPI distributions, npm and GHCR were read back; the gateway reached PyPI at 2026-10-30T07:13:42Z (`docs/RELEASE_STATUS.md` §1.0e).
**Published release (2026-09-24):** `v5.0.1`, fourteen synchronized anchors, **published on every surface except PyPI `aegis-latent-core`, which followed on 2026-09-26 (`docs/RELEASE_STATUS.md` §1.0b)** — the signed tag (`gitsign verify-tag` passed), the GitHub Release with 31 assets (`SHA256SUMS` sweep 15 of 15), PyPI `aegis-latent-sdk` `5.0.1`, npm `aegis-latent-sdk` `5.0.1`, and both GHCR images (`cosign verify` and provenance attestations verified against the exact workflow identity) were read back; see `docs/RELEASE_STATUS.md` §1.0a.
**Previous release (2026-09-16):** `v5.0.0`, fourteen synchronized anchors, **published on every surface except PyPI `aegis-latent-core`** — signed tag, GitHub Release with 31 assets, PyPI `aegis-latent-sdk`, npm `aegis-latent-sdk`, and both GHCR images were read back; the gateway distribution on PyPI remains `4.1.2`. Source metadata does not establish external lifecycle state, which requires independent readback; see `docs/RELEASE_STATUS.md` §1.0 for what was and was not verified. There is no `4.2.0` or `4.4.0`; both numbers were skipped deliberately and no artifact was ever published under either.
**Historical all-surface baseline (readback 2026-09-04):** `v4.1.2` signed annotated tag at `860f14177d94c194e5ae7156017d6fa74264e429`, GitHub Release with 31 assets, PyPI `aegis-latent-core` `4.1.2`, PyPI `aegis-latent-sdk` `4.1.2`, npm `aegis-latent-sdk` `4.1.2`, GHCR gateway image `sha256:b3f6aadc…f80710` and dashboard image `sha256:27e1bbc2…d92398`
**Historical GitHub baseline:** `v4.0.1`, a lightweight tag targeting `6469904380218584ae0b5221334bc9a46500f5ba`
**Immutable source baseline:** `fdace8844568eb788216740b2cb5daf187d99d3b` (fourteen `4.0.0` anchors)
**Release baseline:** published `v5.0.3` (fourteen synchronized `5.0.3` anchors), read back on GitHub Release, both PyPI distributions, npm and GHCR on 2026-10-30; recorded in `docs/RELEASE_STATUS.md` §1.0e
**Documentation verification baseline:** Public claims remain controlled by `docs/CLAIMS_MATRIX.md`; framework references are contribution mappings, not certifications.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [5.0.3] — 2026-10-30
Published on the annotated tag, GitHub Release (31 assets), PyPI
`aegis-latent-core` and `aegis-latent-sdk`, npm `aegis-latent-sdk` and GHCR,
with a 15-of-15 `SHA256SUMS` sweep. The gateway reached PyPI at
2026-10-30T07:13:42Z; the per-surface record is `docs/RELEASE_STATUS.md` §1.0e.
### Changed
- **Licence boundary.** The gateway, control plane and dashboard move from
Apache-2.0 to the Aegis Commercial License 1.0 (`LICENSE`, `NOTICE`,
`COMMERCIAL.md`, per-file headers, package metadata). The SDKs in `sdk/python`
and `sdk/typescript` and the examples stay Apache-2.0. Release 5.0.2 and
earlier keep the terms they were published under; those terms cannot be
revoked. See `docs/legal/LICENSE_TRANSITION_5.0.3.md`.
- **All fourteen version anchors synchronized to 5.0.3**, including the Python
and TypeScript SDKs, the dashboard, the Rust crate and lockfile, and the Helm
chart. `scripts/verify_release_contract.py` reports `READY`.
- **Password hashing** moves from scrypt `n=16384` to `n=65536` (64 MiB). Legacy
hashes still verify and are rewritten on the next successful login.
- **Control plane (aegis-cloud) 5.0.3**: FastAPI 0.162, pytest 9.2,
pytest-asyncio 1.4, mypy 2.4, ruff 0.17 and Python 3.13. Adds provider
attribution on usage events, first-party product events, tenant and operator
statistics, a read-only audit API, atomic API-key rotation, account
export/deletion, JSON logs with CloudWatch EMF metrics, host allow-listing and
request-id sanitisation.
### Security
- API responses carry a sanitised `X-Request-Id`, a Host allow-list and
`default-src 'none'` CSP already enforced by middleware; the new analytics
routes inherit them.
- `GET /v1/ops/stats` is disabled unless `OPS_TOKEN` is set and returns 404, not
403, when the token is absent or wrong.
## [Unreleased]
### Added
- **Durability barriers during WAL rotation (`aegis/core/crypto_audit.py`)**: Fsync failures during segment rotation now immediately abort the rotation and latch failure onto the commit engine (`_commit_engine.fail`), preventing unfsynced segments from being renamed. On POSIX systems, directory fsync is enforced after segment creation/rename to guarantee filesystem namespace persistence prior to notifying waiters.
- **Fail-closed WAF Layer-2 evaluation & mixed-spacing evasion detection (`aegis/proxy/waf.py`)**: Unhandled exceptions in layer-2 WAF scoring now fail closed (`allowed=False`, `reason="Layer-2 evaluation unavailable"`), eliminating silent bypasses unless explicitly configured in shadow mode. Added `_MIXED_SPACED_RUN` regular expression to detect prompt injection evasion using mixed punctuation delimiters.
- **O(log N) MMR consistency proof reconstruction & strict root validation (`aegis/core/mmr.py`)**: `get_consistency_proof` now descends only the mountain straddling the prefix boundary in O(log N) rather than scanning all nodes. Strictly validates `old_count` within bounds, verifies `old_root == "0"*64` for empty accumulators, and raises `ValueError` on root mismatch or pruned nodes. `verify_inclusion` now constructs and validates canonical `MMRInclusionProofV1`.
- **CryptoShredder transactional safety (`aegis/core/crypto_shredder.py`)**: Automatic transaction rollback and connection closure on subject key generation or shred deletion errors, preventing reuse of indeterminate database states.
- **RFC 3161 PKIStatus validation (`aegis/core/rfc3161_timestamper.py`)**: Strictly enforces single-octet DER INTEGER encodings within 0..5, preventing empty octets from parsing as status 0.
- **ISO 27037 integrity seal format validation (`aegis/core/iso27037_evidence.py`)**: Strictly verifies 64-character lowercase hexadecimal format before constant-time comparison.
- **Terminal outbox durability & compaction hardening (`aegis/proxy/terminal_outbox.py`, `aegis/core/seccomp_guard.py`)**: Atomically rolls back prefix on short writes via `ftruncate`, enforces byte budgets with pre-append compaction, preserves active file descriptors across replace, and adds `TERMINAL_OUTBOX_SYSCALLS` (`ftruncate`, `dup`, `dup3`, `chmod`, `fchmodat`) to the seccomp profile.
- **GlobalSequencer divergence detection & lifecycle hardening (`aegis/core/ha.py`)**: Detects conflicting nodes sharing a predecessor (`prev_hash`) and marks sequence as diverged; safely handles closed event loops and stopped states during admission.
- **RustWal payload validation (`aegis_rust_v2/src/wal.rs`)**: Explicitly rejects empty string payloads to reserve empty frames for the recovery terminator.
- **Comprehensive hardening test suites**: 16 dedicated test suites covering app budgets, timestamp verification, cryptographic vault isolation, HA sequencer divergence, stream admission, input policies, and WAL durability barriers.
## [5.0.2] — 2026-10-01
`5.0.2` moves the source line to the Apache License, Version 2.0. Every release up to and including `5.0.1` was
published under the GNU Affero General Public License v3 or a separate commercial licence and **keeps those terms**;
this release changes the licence of nothing already published (`docs/legal/LICENSE_TRANSITION_5.0.2.md`, `CLM-122`).
The signed tag, the GitHub Release assets (`SHA256SUMS`, SBOM, build-provenance attestations) and the registry and
image artifacts are produced by the release workflows from this exact commit; verify them against `SHA256SUMS` and the
attestations rather than this text. Benchmark figures in the documentation are `5.0.1` measurements; none was
re-run for `5.0.2`.
### Licence
- **`LICENSE` is now the verbatim Apache License, Version 2.0** (SHA-256 `cfc7749b…c523d30`, pinned by
`tests/test_license_files.py`). `NOTICE` carries the Section 4(d) attribution, the licence pointer and the
Section 6 trademark reservation. The AGPLv3 text and the "AGPLv3 or commercial" grant are gone from the source
tree.
- **Every first-party source file carries a three-line header** naming `SPDX-License-Identifier: Apache-2.0`,
applied and kept idempotent by `scripts/apply_license_headers.py`; the CI "License Headers" job re-runs it and
fails on any diff. `evidence/`, `investor_packs/` and `site/` are historical or generated and are not rewritten.
- **Package metadata declares `Apache-2.0`** for the gateway and the Python SDK (PEP 639 `license` plus
`license-files`, with the `License ::` classifier removed), `aegis-rust`, both Cargo crates, the TypeScript SDK and
the dashboard lockfile.
- **The licence travels with every distribution** (Apache-2.0 Section 4(a)): `LICENSE` and `NOTICE` are copied into
`sdk/python/` and `sdk/typescript/` (and listed in the npm `files`), and into `/licenses/` in the gateway, air-gap
and dashboard images, whose OCI `licenses` label reads `Apache-2.0`.
- **The dependency policy follows.** `scripts/license/license_scan.py` treats a strong-copyleft (GPL, AGPL)
component as **blocking** for an Apache-2.0 artifact unless it is not distributed with it, and an unresolved
licence as blocking. The committed inventory in `docs/compliance/LICENSE_AUDIT.md` still reflects 2026-09-09; a
scan run on 2026-10-01 in a development environment found one component (`spartan2`, optional `zk-spartan`
feature) declaring `license-file` rather than `license`, which the scanner reads as `UNKNOWN`. It is recorded as a
lead for counsel, not a result.
- **What is sold changes.** `COMMERCIAL.md` and the commercial documents describe services (support, pilots,
hardening, assurance work), not a second licence tier. The licence-entitlement engine (`aegis/licensing`,
`AEGIS_LICENSE_ENFORCEMENT`) keeps its behaviour — it is off by default and its public API is pinned — and is
documented as an entitlement signal, not a licence grant.
- **Not established.** That the owner's right to relicense is confirmed (it rests on the owner's instruction and the
repository's own statements; automation-authored commits and AI-assisted authorship are open counsel questions);
that any dependency set is Apache-compatible beyond the identifiers upstream projects declare; that the
investor-pack valuation inputs (a dual-licence model with an "AGPL friction" haircut) still hold. The packs are
left as dated snapshots.
### Version
- All fourteen release-contract anchors, `Cargo.lock`, the `envoy-wasm` crate, the image labels and the active
deployment literals read `5.0.2`; `SOURCE_RELEASE_TARGET_VERSION` and the inert-field warnings follow. The
deployment defaults that name `ghcr.io/juanlunaia/aegis-latent-core:5.0.2` resolve to nothing until that image is
published. The documentation-currency gate now treats `5.0.1` as a superseded baseline.
- No runtime path changed apart from version strings, headers, docstrings and one reworded licensing-engine error
message. Nothing was re-measured for `5.0.2`; the retained measurements are `5.0.1` measurements.
The entries below were recorded under `[Unreleased]` after `5.0.1` and ship with `5.0.2`.
### Added
- **`docs/compliance/CERTIFICATION_SCOPE.md`** (`CLM-120`, `LEGAL-REVIEW-REQUIRED`). For each framework a
regulated buyer names, it says whether any certificate exists and who receives it. Class F frameworks have
no certificate a software component could hold: HIPAA, GDPR, the EU AI Act, FRE 902(14), SEC 17a-4, NIST
AI RMF, CSF and SP 800-53, and OWASP LLM. For them it states, clause by clause, which functions the gateway
provides, which it provides in part, and which only the deployer can supply. Class O certificates attest an
organisation (SOC 2, ISO/IEC 27001 and 42001, HITRUST, PCI DSS, FedRAMP). Class P needs the product or its
module validated (FIPS 140-3, CAVP, Common Criteria, FDA/MDR). Class O and P stay "none". It is not legal
advice and no counsel has reviewed it.
- **`deploy/azure/phase0/00_query_prices.sh`** (`CLM-121`, `EXEC_BASELINE` G12). A read-only readback of
the four Azure list prices the investor pack tags `VERIFIED`, from the public Retail Prices API. It exits
`3` when a price has moved and `4` when a meter no longer answers. All four matched on 2026-09-30
(`evidence/benchmarks/azure/prices_verified_2026-09-30.json`); `tests/test_azure_price_readback.py` checks
it offline and keeps its expected values equal to the engine's.
### Changed
- `docs/compliance/AUDIT_READINESS.md` and `docs/DEVELOPER_INTEGRATIONS_GUIDE.md` no longer advise
configuring `tsa_url` for trusted time in the hardened posture: a strict gateway now refuses it
(`REG-D105`), and no out-of-process anchoring tool ships. `docs/assurance/TRL_CLOSURE.md` records the
2026-09-30 WAL crash-soak re-run (28,753 acknowledged commits, 0 lost, 0 torn tails) and the WAF
red-team findings the 23-case corpus had missed.
- **Investor pack refreshed a second time (2026-09-30, both editions).** The same three verified inputs
updated again (tests 7,891 passed / 38 skipped / 0 failed at `fd6b435`, claims 121, test-to-source ratio
1.29); five TRL rows extended with the security pass (`REG-D88`–`REG-D105`), the WAL soak re-run, the HA
epoch fix (`REG-D91`, which leaves the HA gap as chaos tests and a pilot) and the Azure list-price
readback; D9 objection 4 answered with the certification scope (`CLM-120`); section U2 added. No TRL
level, cost-to-close figure, chart or model number moved; signatures and provenance were not re-verified.
- **Behaviour change: unusable request bodies answer `400`, not `500`** (`REG-D98`, `CLM-118`). At
`/v1/chat/completions`, `/v1/completions` and `/v1/messages`, invalid UTF-8, an integer past Python's digit
limit, a body that is not a JSON object, and nesting deeper than 32 levels are refused `400` before
canonicalisation. The details are `Invalid JSON`, `Request body must be a JSON object` and
`JSON nesting exceeds 32 levels`. A body nested 33 levels or more was a `403` from the WAF depth guard and
is now a `400`; it is still recorded as a rejection node. `/v1/messages` answers a non-object body with the
new shared detail instead of `Anthropic request must be an object`. See `docs/UPGRADING.md`.
- **Behaviour change: a strict gateway refuses `tsa_url`** (`REG-D105`, `CLM-119`). RFC 3161 verification
runs the `openssl` binary, which the seccomp profile forbids, so the first anchor used to kill the
process. The lifespan now refuses the combination before the filter loads. Strict mode with
`require_seccomp` fails startup; development mode warns and runs without the filter. Anchor archived
segment manifests out of process instead.
- **Behaviour change: the WAF no longer refuses ordinary prose containing "dan" or "disregard previous"**
(`REG-D94`). "clinical guidance", "in accordance with", "Jordan", "Dante" and "disregard previous
correspondence" now pass. The DAN family needs a word boundary, and "disregard" needs an instruction
object, in both Layer 1 and the Rust pre-filter. Every DAN and disregard attack form in the tests is still
refused.
- **Behaviour change: `pqc-ml-dsa` signatures are verified only against pinned keys.** A verifier pins
the public half of its own ML-DSA identity and the keys in the new `AEGIS_TRUSTED_SIGNING_PUBLIC_KEYS`
(comma-separated hex). While that set is non-empty, a node signed under any other key reads `invalid` and
fails `verify_integrity()`. With neither, a signature that checks reads `unverified` (it was `valid`), and
`signature_assurance` counts the node as `UNSIGNED`. HA replicas that share a chain, and identities retired
by a rotation, must be listed or their nodes fail verification. See `docs/UPGRADING.md`.
- **Behaviour change: an `ed25519-fallback` signature that checks now reads `unverified`, not `valid`.** Its
key is minted for that one node and carried in it, so a check proves only that the record is internally
consistent. A mismatch still reads `invalid`, the chain tier stays `COMPROMISED_EPHEMERAL`, and
`verify_integrity()` is unchanged.
- **Behaviour change: the ledger refuses commits while faulted.** Every commit method raises
`LedgerFaultedError` (a `RuntimeError`) while the fault latch is not `healthy`; the gateway answers `503`
as for any other commit failure. Code that kept committing on a ledger after a failed write must reopen it
after repair. See `docs/UPGRADING.md`.
- **Dependency updates that Dependabot proposed (#210–#217) applied by hand in one change.** The eight
PRs were closed unmerged (each failed the release-contract test or the AI-context manifest hash because
it moved a governed file without its companions). Applied: `github/codeql-action` `init`, `autobuild`,
`analyze` and `upload-sarif` to `4.38.2`; `docker/setup-qemu-action` `4.4.0`, with the pinned SHA in
`scripts/verify_release_contract.py` moved to match; `anyio>=4.15.1` in `requirements.txt` and
`anyio==4.15.1` in `requirements.lock`; and ten crates in `aegis_rust_v2/Cargo.lock` (`aho-corasick`,
`blake3`, `crc32fast`, `crossbeam-queue`, `http`, `pyo3` `0.29.2`, `reqwest`, `serde` with the new `syn 3`,
`serde_json`, `tokio`). Checked locally: the CI lock-drift command (pip 25.2, pip-tools 7.5.2, Python 3.12)
reproduces `requirements.lock` byte for byte; `cargo check --locked --all-targets` passes.
**Not applied: `hatchling` `1.32.4` (#214).** It writes `Metadata-Version: 2.5`, which the `twine==6.2.0`
pinned in five publish workflows rejects (`twine check` fails on both the wheel and the sdist; `twine`
`7.0.0` accepts it). The wheel members were otherwise identical, but a backend bump needs `twine` moved in
the same change and a live upload to prove PyPI takes the metadata, so Dependabot now ignores `hatchling`
and the pin stays at `1.28.0` until that is done deliberately.
- **Brevo set up through its API (2026-09-30), nothing sent.** Seven contact attributes, the "Opted in"
list (id 3) and three inactive templates created and read back; no campaign exists. The exporter now emits
the account's own `NOMBRE`/`APELLIDOS`. Status recorded in `BREVO_PLAN.md`.
- **Brevo kit staged.** The connector was authorized on 2026-09-30 but answered
"requires authentication" in the sessions that prepared this, so none of its tools loaded and an unattended
session cannot complete the sign-in. Prepared instead: `tools/validation/brevo_export.py`, which turns the
owner's private contact sheet into a Brevo import file and refuses any row without a consent basis, a real
past consent date, a valid and unique address, a known segment and source (opted-out rows are skipped,
reports name rows and log ids, never addresses, and paths inside the repository are refused);
`tests/test_brevo_export.py` (21 tests); three email drafts and a header-only contact sheet under
`docs/commercial/validation/brevo/`; and the first steps in `BREVO_PLAN.md`. `.gitignore` blocks
`contacts_private*.csv` and `brevo_import*.csv`. Nothing is sent by the agent.
- **README: "Support the project" section (2026-09-30).** Two donation banners under
`docs/assets/donate/` link to a Stripe Payment Link (card) and a PayPal payment link, both created by the
owner's accounts. The text says a donation is voluntary, buys no licence, support, feature or influence,
and is not tax-deductible. Nothing in the software reads or depends on either link.
- **Public pricing signals recorded** in `docs/commercial/validation/PRICING_EVIDENCE.md`: six adjacent
LLM-observability and gateway products, read through Pricing Optimizer AI on 2026-09-30, dated and
attributed, with what the snapshot does not show. No figure in the Enterprise Pricing Guide changed.
- CI: `faulthandler_timeout` raised from 60 to 180 seconds in `ci.yml` and `forensic.yml`. The option exits
the process when a single test outlives it, and `test_integrity_across_one_hundred_thousand_nodes`
(marked `slow`, about 60 seconds on a loaded runner) crossed it on Python 3.11 three times, killing an
xdist worker; it stays a hang detector, three times looser.
- **Investor pack refreshed (2026-09-30, both editions).** Three verified inputs updated (tests 7,673
passed / 42 skipped / 0 failed, claims 113, test-to-source ratio 1.26), seven TRL rows extended with the
2026-09-29 qualification evidence and its limits, the wind-down wording and the T1 issuer precondition
corrected, and a section listing every change added. No TRL, chart or model number moved; signatures and
provenance were not re-verified.
- **Release truth corrected: the gateway is on PyPI at `5.0.1`.** `publish_pypi_gateway.yml`
run `36224961909` published `aegis-latent-core` `5.0.1` on 2026-09-26; the public PyPI API,
read back 2026-09-29, shows both files equal to the GitHub Release assets. The statements
that `pip install aegis-latent-core` still gets `4.1.2` were replaced across the corpus, the
`5.0.1` publication is recorded in `docs/RELEASE_STATUS.md` §1.0b and `CLM-113`, and
`UC-047` is amended with its original text kept. Statements about `5.0.0` and earlier stand.
- The count of refused claims in sales copy (`PROVE_IT.md`, `POSITIONING.md`, `ONE_PAGER.md`, `OUTBOUND_SEQUENCES.md`)
read 42; the register has 70 rows at 2026-09-29.
- Three remaining statements that `pip install aegis-latent-core` gets `4.1.2` (`COMMERCIAL.md`,
`docs/USAGE_EXAMPLES.md`, `docs/corporate/PRODUCT_ONE_PAGER.md`) were corrected to `5.0.1`.
- `examples/demo.py` runs again: it declares development mode, builds its API-key principal
mapping, and expects two nodes per request. It had failed five of nine checks on `main`.
### Fixed
- **The gateway killed itself the first time a post-lockdown feature ran** (`REG-D103`, `REG-D104`,
`CLM-119`). The seccomp profile had been measured on the request path. WAL rotation, which S3 archival
requires, called `chmod` on a path, and `mkdir` on a directory that already existed. S3 archival and
cryptographic shredding write SQLite databases whose syscalls were loaded only for the HA sequence store.
Each feature was killed with SIGSYS on first use; this was reproduced with the real filter at `8e03b31`.
Rotation now uses `fchmod` on the open descriptor and creates directories only when missing. The lifespan
loads `SQLITE_SYSCALLS` (`fchown`, `ftruncate`, `geteuid`, `pread64`, `pwrite64`) only when archival,
shredding or the SQLite sequence store is configured. `tests/test_seccomp_runtime_paths.py` runs each flow
under the real filter in a child interpreter. Windows before Python 3.13 has no `os.fchmod`; there, and
only there, the ledger keeps the path-based `chmod` (no seccomp filter exists on that platform). The first
version of this fix called `os.fchmod` unconditionally, and the Windows CI job caught every ledger failing
to open with `AttributeError`. `tests/test_wal_rotation.py` now covers a host without `os.fchmod`.
- **On Python 3.13 with glibc older than 2.37, the first shredded commit was still killed** (`REG-D107`,
`CLM-119`). The fix above was tested on glibc 2.39. Before glibc 2.37, `qsort` calls `sysinfo` once per
process, on its first sort of 1 KiB or more, and OpenSSL inside `cryptography` sorts on a cipher's first
use. CI's "Test (Python 3.13)" job on Ubuntu 22.04 (glibc 2.35) caught the kill. Python 3.11 and 3.12 sort
their own sysconf table at start-up, before lockdown, which hid the call. The default profile now allows
`sysinfo`, which only reads uptime, load and memory totals; the container profile already allowed it. The
root cause was traced on CI's own Python builds in an Ubuntu 22.04 rootfs, and a new test calls the
syscall directly, so it fails without the fix on any glibc. Evidence: `evidence/registry/reg-d107_closure.txt`.
- **Five malformed-body classes answered `500`** (`REG-D98`). One parser now answers `400` at all three
model endpoints (see Changed). A chat text block whose `text` is null or a list no longer raises in the
WAF.
- **The YARA rule-header regex was cubic on a run of spaces** (`REG-D102`). 2000 spaces took 2.3 s and now
take 0.00006 s. `aegis/core/yara_engine.py` is not on the gateway path.
- **A commit could be reported durable and then never replay.** `specs/aegis_invariants.tla`, rewritten to
model write failures, torn tails, crashes, replay and repair, found it: request A's write tears part-way
through a line and latches `wal_persist_failed`; request B, admitted by the gateway before the latch,
commits anyway, its line lands after the torn bytes and its caller is told it is durable; replay stops at
the torn line, and B is never read back. The gateway's `_require_intact_ledger` read of the latch could not
close the window because the read and the commit are not atomic. `CryptographicAuditLedger` now checks the
latch under its own lock before anything reaches the MMR or the WAL, in all four commit methods
(`LedgerFaultedError`, `CLM-114`, `REG-D88`). A replica in HA mode that opens a faulted WAL still starts, lifts its
epoch, records no handover, and refuses governed traffic until repair (`REG-D93`). `tests/test_ledger_fault_gate.py`,
`tests/ha/test_ha_lease.py::test_a_faulted_chain_starts_without_a_handover_record`.
- **A rewritten WAL re-signed under an attacker's own ML-DSA key verified.** `signature_status` checked each
signature against the public key the node itself records, so write access to the WAL was enough to replace
records, re-sign them with a fresh key and pass `verify_integrity()`. `specs/aegis_ledger_immutability.tla`
reproduces it (`aegis_ledger_immutability_unpinned.cfg`). Keys are now pinned (`CLM-115`, `REG-D89`; the
`ed25519-fallback` reading above is `REG-D90`); `tests/test_signing_key_pinning.py` (21 tests).
- **After Redis lost its data, an HA chain stopped admitting until an operator restored the epoch counter by
hand.** The counter restarted at `1`, below epochs the chain and the global sequence already record, and
the fence refused every node the new holder wrote. A replica now lifts its lease epoch above the highest
handover epoch its WAL records before it writes its own handover, in one Lua script that re-checks holder
and epoch and is serialised with renewal and release (`ChainLease.raise_epoch_above`, `CLM-116`, `REG-D91`).
`tests/ha/test_ha_lease.py`, `tests/ha/test_ha_chaos.py`.
- **Formal artifacts that could not fail.** `specs/aegis_invariants.smt2` asserted a predicate with its own
negation and `specs/aegis_stream_buffer.smt2` asserted `retained_bytes > R_max` right after defining them
equal, so both returned `unsat` whatever the arithmetic; the three TLA+ models had no configuration that
could violate an invariant, and the Lean file had no failure path. Every check is now falsifiable: SMT files
declare the expected result of every query and include a satisfiable one; each TLA+ model has witness and
counterexample configurations that must be violated; the Lean file must prove nine named theorems with
only the standard axioms. The session-manager model now describes the class that exists
(`SessionLifecycleManager`), and `tests/test_session_manager_conformance.py` checks its invariants on the
real class. See `docs/formal/FORMAL_VERIFICATION.md` (`CLM-026`, `REG-D92`). Before-and-after output for
`REG-D88`–`REG-D93` against `main` is in `evidence/registry/reg-d88_d93_closure.txt`.
- **The embedded engine kept calling the provider and issuing receipts on a WAL it could not replay.**
`aegis.wrap` and `AegisEmbedded.guard_request` never consulted the ledger's fault state. After a kill left
a torn last line, replay stopped there and latched `wal_corrupt`, yet every later wrapped call reached the
provider, committed a node and attached `_aegis_evidence`; a restart read back only the records before the
tear, so those receipts pointed at records nobody could reach. The gateway has refused this since
`_require_intact_ledger` (`503`); the embedded engine now does the same. `guard_request` is checked before
any inspection and raises `AegisEmbeddedError` (not `AegisBlockedError`) when the state is anything other
than `healthy`, before the provider is contacted, and commits nothing, including for a prompt the WAF would
have blocked and in `shadow` mode. The engine still opens, so the state can be inspected; recovery is
`tools/wal_repair.py` and a new engine. Behaviour change for callers: a wrapped call on an unhealthy chain
now raises instead of succeeding. `CryptographicAuditLedger` now also refuses commits while faulted (entry below), so a call
already admitted when a fault latches fails at its commit; the provider call it made is not recalled.
`tests/test_embedded_mode.py`.
- **`HSMSigningBackend` RSA-PSS signing failed on every real PKCS#11 token.** It called
`pkcs11.mechanisms.RSA_PKCS_PSS_PARAMS`, which exists only in the unit-test mock; the real
library takes a `(hash, mgf, salt length)` tuple. ECDSA now falls back to raw `CKM_ECDSA` over a
host-computed SHA-256 digest on tokens without `CKM_ECDSA_SHA256`. Found by running the adapter
against SoftHSM 2.6.1 (`tests/test_hsm_softhsm.py`).
- **The kill-and-recover soak could report acknowledged commits as lost.** A `SIGKILL` between
`write()` and the newline leaves a torn last line. Replay latches `wal_corrupt` on it, which is
the state the gateway answers 503 for, but the harness's next worker opened the ledger anyway,
appended after the torn line and acknowledged commits that replay could never reach. It showed up
as a rare, unrepeatable `Forensic checks (Python 3.13)` failure of
`test_killed_committer_loses_no_acknowledged_commit`. `tools/qualification/wal_crash_soak.py` now
applies `tools/wal_repair.py` to a torn tail between rounds and counts it
(`torn_tails_repaired`), its worker exits 3 on a ledger that is not healthy, and damage that is
not a single torn last line still fails the soak. Gateway behaviour is unchanged.
### Added
- **Kani harnesses for the token-bucket arithmetic** (`aegis_rust_v2/src/rate_limit.rs`, `mod verification`):
the refill and consume steps the limiter's compare-and-swap loops commit are factored into functions, and
five harnesses check them bit-precisely: capacity construction, refill bounds, refill exactness for every
non-negative gain, gain positivity, and consume. Arithmetic only; no atomics, clock or map is modelled.
`cargo kani` verifies all eleven harnesses in the crate in about 25 seconds, and CI's Kani step is renamed
to cover both files.
- **Operating cadence** (`docs/cadence/`, `tools/cadence/cadence.py`): weekly report, monthly recompute
(re-runs the seed-42 engine and compares it byte for byte with the committed pack) and a kill-switch
register covering the six kill criteria, the budget alerts and the tranche gates, plus a draft
wind-down path for counsel. The tools read only owner-recorded facts, estimate nothing, send nothing
and never write `KILL_ACK.json`.
- **Raise execution drafts and tools** (`docs/raise/`, `tools/raise/`): data-room index and hash
manifest, SAFE sheet, tranche evidence packs, monthly-update generator and a pre-drafted downgrade
memo. `tranche_gate.py` counts a gate as met only from a dated, referenced record the owner wrote;
the tooling sends, signs and files nothing and estimates no figure.
- **TRL closure evidence** (`docs/assurance/TRL_CLOSURE.md`): `tools/qualification/wal_crash_soak.py`
(kill-and-recover; 60 rounds, 27,735 acknowledged commits, 0 lost), `tests/ha/test_ha_chaos.py`
(a real partition and a Redis restart), `deploy/observability/` (alert rules and dashboard checked
against the exported metrics), `tools/sales/build_verifier_kit.py`, and retained WAF corpus and soak
reports under `evidence/qualification/`. Finding: a Redis that loses its data resets the lease
epoch counter, and the sequence fence then stops the chain admitting. Fixed in the same release: the
epoch is lifted above the chain's own record (entry under Fixed).
- **Assurance pipeline** (`docs/assurance/`, `tools/assurance/`): a penetration-test SOW draft scoped to the
evidence path, a vendor SOC 2 readiness assessment, an escrow execution plan with
`escrow_manifest.py` (records a tag's tracked files and digests, refuses key material, checks a restore),
and `ASSURANCE_STATUS.md` with a machine-readable copy that tests keep from running ahead of its
evidence. No independent assurance exists; every item is `NOT_STARTED` or `DRAFTED`.
- **Validation engine** (`tools/validation/`, `docs/commercial/validation/`): a blank 60-row validation log,
outbound batch 1 (30 first touches by segment, sent by the owner), a buyer-interview script, a pilot
generator that fills the pilot template and refuses a pilot that breaks its own rules, and
`kill_switch.py`, which evaluates the six D10.3 model-kill criteria and halts until the owner records `SI`.
Nothing in it is a prospect, reply or result.
- **Legal stack drafts** (`docs/legal/`): commercial licence template, paid pilot agreement template,
escrow term sheet, founder-to-company IP assignment draft, open-core decision memo and questions for
counsel. All are unreviewed drafts marked `[COUNSEL-REVIEW-REQUIRED]`; none is signed or offered.
`tests/test_legal_drafts.py` keeps the banner on and the signature rows empty.
- **Verification-first sales surface** (`tools/sales/`, `site/`, `scripts/lint_sales_copy.py`): a
verifier kit (`prove_it.py --demo` accepts one synthetic record and rejects two forgeries), two static
pages (landing and data room) built by `build_site.py`, a banned-word copy lint, and
`tests/test_sales_surface.py`. Lighthouse 12.8.2 scored both pages 100/100/96/100 on 2026-09-29.
- **Azure Kit Creator** (`deploy/azure/kit-creator/aegis_azure_kit.py`): `doctor`, `estimate`,
`create` and `verify` generate a self-contained, secret-free Azure VM kit (Bicep, Docker
Compose, Caddy TLS, lifecycle scripts). Secrets are generated on the VM at first boot.
- `deploy/azure/vm/`, the kit source: strict-mode gateway with the WAL on a Premium Managed
Disk, Redis on an internal-only network, and a budget alert.
- `deploy/azure/phase0/` (`phase0_guardrails.sh`, `budget.bicep`): providers, tagged
resource group and subscription budget alerts. Partially closes `REG-H07`.
- `deploy/azure/benchmarks/`: harness that records raw-disk `fsync`, the repository harness
and `/health` latency on an Azure VM.
- `tests/test_azure_kit.py`: offline checks for the kit (validation, checksums, secret scan,
Bicep compilation, compose posture, cloud-init rendering).
### Security
- **WAF evasions closed, each reproduced over HTTP first** (`REG-D95`–`REG-D97`, `REG-D99`–`REG-D101`,
`CLM-117`). Every payload below reached the provider before this fix:
- Unicode tag characters, a word joiner, bidi isolates, variation selectors and combining marks, each
splitting "ignore previous instructions".
- Star, slash, pipe and padded letter-spacing, and phrases written with no spaces at all.
- Base64-wrapped Layer-1 phrases.
- Layer-2-only phrases placed in `prompt` or `system` instead of `messages`.
Also fixed:
- The template pattern was quadratic on unclosed `{{`. 32 KB took 3.45 s, and a 1 MiB body could hold a
worker for about an hour; it now takes 0.031 s.
- The Layer-2 obfuscation markers never matched.
Each fix counters one named encoding. Alternating separators, other encodings, split fields and
paraphrase still pass Layer 1, and the WAF remains detection, not an injection boundary (`UC-042`).
Layer 2 now scores tool descriptions and metadata, so it can refuse benign ones.
- **Audit dashboard: `next` `16.3.4` → `16.3.8` (GHSA-vcvr-r3jv-pc5j, critical).** The advisory covers remote
code execution through `next/og` `ImageResponse` in `16.2.0`–`16.3.5` and was published while this branch
was open; `npm audit --audit-level=high` in the Audit Dashboard job failed on it. The dashboard does not
import `next/og`, so no route calls the vulnerable renderer, but the package ships in the dashboard image and
the gate is right to refuse it. After the bump, `npm ci`, typecheck, the 6 vitest tests, `next build`,
`npm audit` (0 vulnerabilities) and the client-bundle secret check all pass locally. No published image
changes until the next release.
- **Gateway runtime: `urllib3` `2.7.0` → `2.8.0`** (`REG-D106`). Three advisories against `2.7.0` were published
on 2026-09-30 (GHSA-8988-9cw3-xx77, GHSA-gh4c-6fx4-qh6g, GHSA-vxq7-64xx-v4gw). `pip-audit`, which the
Security Scan job runs against `requirements.lock`, failed on them on this branch and would fail on `main`.
`urllib3` enters only through `requests`, and no `aegis` module imports either, so no gateway request path
reaches it; the floor is raised in `requirements.txt` and `pyproject.toml` all the same. The lock was
regenerated with CI's toolchain and only `urllib3` changed; its hashes equal the PyPI wheel and sdist.
`pip-audit` now reports no known vulnerabilities for the lock and for `requirements.txt`. Evidence:
`evidence/registry/reg-d106_closure.txt`.
### Documentation
- Azure: [Quickstart](docs/operations/AZURE_QUICKSTART.md), [Install Options](docs/operations/AZURE_INSTALL_OPTIONS.md)
(advantages, disadvantages, costs; Container Apps recorded as unsupported),
[Kit Creator](docs/operations/AZURE_KIT_CREATOR.md) and
[Azure Benchmarks](docs/benchmarks/AZURE_BENCHMARKS.md) with the retained artifact under
`evidence/benchmarks/azure/`. `docs/PLATFORM_COMPATIBILITY.md` and `REG-H07` now state
what the one live deployment on 2026-09-26 did and did not establish.
- The verified `5.0.1` publication is recorded: `docs/RELEASE_STATUS.md` §1.0a,
`CLM-112`, and the readback transcript
`evidence/v5_0_1_release_readback_2026-09-24.md`. The README, `AGENTS.md` and
every document that said `5.0.1` was published nowhere now state what the
readback found.
## [5.0.1] — 2026-09-24
**Published 2026-09-24 on every surface except PyPI `aegis-latent-core`,** from
`main` at `46db6c0` under the Sigstore-signed tag `v5.0.1`. Read back the same
day: the GitHub Release (31 assets, `SHA256SUMS` sweep 15 of 15), PyPI and npm
`aegis-latent-sdk` `5.0.1` (byte-identical to the release assets), and the GHCR
gateway and dashboard images. The tag, image signatures and build-provenance
attestations were verified against the exact publishing workflow identities
(`docs/RELEASE_STATUS.md` §1.0a). The gateway distribution is still not on PyPI:
`pip install aegis-latent-core` gets `4.1.2`. Before the release, on 2026-09-21,
the same surfaces showed nothing for `5.0.1`.
**Release gatekeeper verdict (2026-09-24): HALTED.** A full pre-release
verification pass found two release-blocking defects in the gateway's in-process
seccomp control and two supply-chain consistency defects; they are flagged, not
fixed. Every check and its result is in
[`RELEASE_HALTED_CRITICAL_ERRORS.md`](RELEASE_HALTED_CRITICAL_ERRORS.md).
**Remediation (2026-09-24, PR #205): the halt is resolved in source.** Every
blocker and every open finding above was fixed, and ten more were found and
fixed along the way. The current state is
[`RELEASE_READINESS_v5.0.1.md`](RELEASE_READINESS_v5.0.1.md), and the
per-item evidence is `evidence/registry/reg-d67_d86_closure.txt`. The owner
then merged it and authorized the release recorded above.
### Added — multi-replica operation (`AD-17`, `CLM-108`)
- `AEGIS_HA_MODE` (`single`, the default, is unchanged):
- **`active_passive`:** replicas on one chain compete for a Redis writer lease
carrying a monotonic epoch. Only the holder admits governed requests; a
standby answers `/health` and refuses the rest; every handover is recorded
in the chain as a signed node.
- **`active_active`:** each replica writes its own chain under its own lease,
and every durable node is appended to one hash-linked global sequence in
PostgreSQL. Appends are compare-and-append, with a per-chain continuity
check and an epoch fence in the same transaction. Admission is refused
while sequencing lags beyond `AEGIS_HA_MAX_SEQUENCING_LAG_SECONDS`.
- `python -m aegis.core.ha verify` re-derives the sequence and checks each
WAL against it.
- Startup and the Helm chart refuse shapes that would fork evidence.
- Operator guide: `docs/operations/HIGH_AVAILABILITY.md`.
- **Container smoke test** (`scripts/container_smoke_test.py`). It runs the
shipped image in its hardened posture: strict mode, its own kill-mode seccomp
filter, the AppArmor profile, a read-only root, no capabilities, and an HTTPS
upstream reached by hostname. It covers auth and scope refusals, a WAF block,
streaming, the evidence chain, a restart, a graceful stop, SIGKILL failover,
two active replicas and `verify`. CI runs it on every change, and
`publish_oci.yml` runs it on the signed commit before any image is pushed.
- **Audit readiness** (`docs/compliance/AUDIT_READINESS.md`). It gives the
assessor procedure and control matrices for SOC 2, HIPAA §164.312, ISO/IEC
27001:2022 Annex A, the EU AI Act and MiFID II/MiFIR. It is technical evidence
only; no certification or compliance is claimed.
- **Evidence collector** (`scripts/collect_audit_evidence.py`, standard library
only). It snapshots a live gateway's health, readiness, `/metrics`, audit
integrity and capability reports, with `SHA256SUMS` over every file. The
audit key comes from `AEGIS_AUDIT_KEY` and never from argv, output or a
redirect.
- `python -m aegis.auth.principal` generates `AEGIS_API_KEY_PRINCIPALS_JSON`
from keys on stdin.
- `docs/MAINTAINER_HANDBOOK.md` covers access to transfer, build, gates, the
release order and an escrow deposit.
### Fixed — remediation of the gatekeeper findings (PR #205)
- **Seccomp, P0:**
- `REG-D67`: the gateway killed itself on `io_uring_enter` on a bare host.
- `REG-D68`: the filter was skipped inside Docker.
- `REG-D78`: the shipped image was killed on its first request to any named
host (`uname`, `sendmmsg`).
- `REG-D83`: `ctypes.util.find_library` ran `ldconfig`; AppArmor denied it
at startup, and an authenticated capability probe killed the gateway.
- **Seccomp, P1:**
- `REG-D82`: the filter did not cover threads started before lockdown; it is
now loaded with thread synchronization.
- `REG-D84`: every graceful stop ended in SIGSYS.
- **Supply chain:**
- `REG-D69`: the images install exactly the hash-pinned lock.
- `REG-D70`: `cachetools` added to the lock.
- `REG-D86`: `prometheus-client` added to the lock, and the AppArmor
profile now lets its process collector read the gateway's own fd limit.
Without it the shipped image served no `/metrics`, and the posture alert
could not fire.
- **Deployment:**
- `REG-D79`: a `…/v1` backend URL produced `/v1/v1/…`.
- `REG-D80`: the AppArmor profile blocked the WAL and the interpreter.
- `REG-D81`: the hardened compose could not start strict.
- **Lower severity:**
- `REG-D73`: unhandled exceptions now return a JSON 500.
- `REG-D74`: `[all]` no longer installs the dev toolchain.
- `REG-D75`: an unauthenticated gateway binds loopback by default.
- `REG-D60`: example variables that nothing read were removed, with a guard
test.
- `REG-026`: `mypy --strict aegis_server` is clean, and CI runs it.
- `REG-D03`: `chacha20` 0.10.2.
- `REG-D85`: two test modules leaked real-time scheduling into the rest of
the run.
- `REG-D87`: the SQLite sequence store could lose the race to convert a new
file to WAL (`database is locked`), and then hang its process at exit on
the unclosed connection. It now retries within the busy timeout and always
closes the connection on failure.
- **Citation** (`REG-D77`): MiFID record-keeping is now cited as Directive
2014/65/EU Art. 16(6)/(7) and MiFIR Art. 25(1), checked against the adopted
texts. Counsel's confirmation is still recommended.
### Security
- `prometheus-client` 0.26.0 (Apache-2.0 AND BSD-2-Clause) joins the runtime
lock. `pip-audit --require-hashes` reports no known vulnerabilities in the 37
locked packages.
### Fixed — found by the 2026-09-24 release gatekeeper pass
- **`main`'s Documentation Gates job was red** (`REG-D66`). PR #202's README
restructure removed the `## Verified metrics` heading that
`docs/RUST_BUILD.md:35` linked to, so `verify_links.sh` failed 1 of 1,387 links
on `main` (CI run `35971846398`, job "Documentation Gates"). The link now
targets the Rust-versus-Python MMR measurements in `docs/BENCHMARKS.md`,
which are what the sentence describes. (#203 separately restored the README
anchor on `main`; both now resolve.)
- **CodeQL alert 736** (`REG-D65`) on `scripts/generate_sdk_bundle_fixture.py`:
the fixture ledger was closed in a `finally` block; it is now a `with` block,
which `CryptographicAuditLedger.__exit__` already supports. Behaviour is
unchanged: `tests/test_sdk_bundle_contract.py` passes 4/4 before and after.
- **Two false README statements** (`REG-D72`): "floor 90% enforced" (CI and the
`Makefile` enforce `--cov-fail-under=65`; 90% was a one-off mission floor,
`REG-D36`), and a quickstart line promising `X-Aegis-Proof-Status` on a
non-streaming call (that header is streaming-only). The quickstart now also
discloses `REG-D67` and its workaround.
- **This changelog was not exhaustive** (`REG-D71`): six PRs merged after the
`v5.0.0` tagged commit (`b2e4335`) had no entry here — recorded below. The
`[5.0.0]` heading still read "unreleased source target" eight days after
`5.0.0` was published and read back; it now carries the publication date.
- **Two security documents said `requirements.lock` is what the released image
installs** (`REG-D69`, `UC-069`) — it is not; corrected in
`docs/security/BUILD_SCRIPT_ATTESTATION.md` and
`docs/security/DEPENDENCY_RISK_REGISTER.md`. The build defect itself is open.
### Flagged, not fixed — release-blocking (see the halt report)
*All fixed on PR #205 — see the Remediation entries above. Kept as recorded at the halt.*
- `REG-D67` — on a Linux host with `libseccomp`, outside Docker, the gateway's
own `SCMP_ACT_KILL_PROCESS` filter kills it on `io_uring_enter` (libuv via
`uvloop`) about two seconds after startup, before any request. Workaround:
`UV_USE_IO_URING=0`.
- `REG-D68` — `SeccompGuard` treats `/.dockerenv` as a sandbox and skips the
filter, so the image's baked-in strict mode with `AEGIS_REQUIRE_SECCOMP=true`
refuses to start under Docker (code path demonstrated; not run on a Docker
engine).
- `REG-D69` — `deploy/docker/Dockerfile` and the air-gapped build resolve
dependency ranges at build time instead of installing `requirements.lock`.
- `REG-D70` — `requirements.txt` (and so the lock and the SBOM) omits
`cachetools`, a core dependency; lock-based installs run the in-memory rate
limiter with an unbounded bucket map.
- Lower severity, also open: `REG-D73` (unhandled exceptions return a
`text/plain` 500, no traceback), `REG-D74` (the `[all]` extra installs the dev
toolchain), `REG-D75` (default bind `0.0.0.0` even with authentication
disabled). `REG-D76` records that `cargo audit` now runs (0 vulnerabilities)
and that two allowlist entries are stale.
### Recorded retroactively — PRs merged after the `v5.0.0` tag with no entry here
- **#182 — AKS strict-mode deployment.** `LSMGuard` reads AppArmor confinement
from `/proc/self/attr/{apparmor/,}current` before probing securityfs, which
pods do not mount; the seccomp allowlist gained the syscalls the event loop
and WAL use after lockdown (`epoll_pwait`, `ioctl`, `newfstatat`, `openat`,
`fsync`, `flock`, …) with `SCMP_ACT_KILL_PROCESS` as the default action, and
`tests/test_seccomp_enforced_serving.py` serves HTTP behind the real filter;
the Helm chart wires `AEGIS_AUTH_IDENTITY_HMAC_KEY` and
`AEGIS_API_KEY_PRINCIPALS_JSON` and accepts extra volumes; `deploy/azure/aks/`
adds a values overlay, cluster resources, a Key Vault CSI sync and a bootstrap
script.
- **#184 — commercial corpus reconciled; master defect registry seeded.**
One price book (`docs/commercial/ENTERPRISE_PRICING_GUIDE.md`, every figure
`[HYPOTHESIS-UNVALIDATED]`); new `ARTIFACT_INVENTORY.md`, `CLAIM_LEDGER.md`,
`POSITIONING.md`, `SALES_KIT/` and `docs/PROVE_IT.md` (a recorded end-to-end
verification transcript); `docs/REGISTRY.md` and `REGISTRY_HUMAN_PACK.md`
created.
- **#185 — Registry Wave 1.** `REG-010`: `RAGInjectionScanner` now runs on tool,
function and declared retrieval content at all three governed admission sites,
after the WAF and before the forwarder (`CLM-097`,
`AEGIS_RAG_INJECTION_SCANNING`, `AEGIS_RAG_INJECTION_BLOCK_THRESHOLD`).
`REG-012`: payload digests are keyed to the subject so erasure erases.
`REG-017`: `tools/wal_repair.py`, a dry-run-by-default repair for a torn WAL
tail that refuses (exit 2) a bad line anywhere but the end and writes a backup
first.
- **#186 — a 75-agent specialist roster** under `.claude/agents/`, each
definition restating the `AGENTS.md` evidence rules.
- **#187 — Registry Wave 2.** `REG-049`: optional WAL free-space preflight
before dispatch (`AEGIS_WAL_MIN_FREE_BYTES`, default off; `CLM-100`).
`REG-011`: concurrent-append races tested against live Postgres and DynamoDB
Local. `REG-013`: the traceparent propagator is wired. `REG-050`: stream
admission gauges (`CLM-102`). `REG-054`: a stale `mmr.py` comment. Plus
`VERIFIED`/`DOCUMENTED` dispositions for `REG-014`–`REG-016`, `REG-018`–`REG-020`,
`REG-022`, `REG-025`, `REG-026`, `REG-040`, `REG-047` and `REG-060`.
- **#202 — documentation sweep and README restructure** (`REG-D64`): benchmark
documents re-baselined to `v5.0.1` with the 2026-09-24 measurements,
prospectus and changelog header drift corrected, and a currency regression pin
added (`tests/test_documentation_currency.py`).
### Changed
- **Global version bump to `5.0.1`** across the fourteen synchronized anchors and
every deployment literal the release contract binds: `pyproject.toml`,
`aegis/__init__.py`, both SDKs (`sdk/python/pyproject.toml`,
`sdk/python/src/aegis_sdk/__init__.py`, `sdk/typescript/package.json` and its
lockfile), `dashboard/package.json` and its lockfile, `aegis_rust_v2/Cargo.toml`,
`Cargo.lock` (the `aegis_rust` package entry), `aegis_rust_v2/pyproject.toml`,
`deploy/helm/Chart.yaml` (`version` and `appVersion`), `deploy/helm/values.yaml`
(`image.tag`), `deploy/docker/Dockerfile`, `deploy/docker/Dockerfile.airgap`,
`deploy/docker/docker-compose.yml`,
`deploy/docker/docker-compose.enterprise.yml` (both services),
`deploy/k8s/aegis-operator/operator.py`, `deploy/k8s/aegis-operator/crd.yaml`,
`connectors/envoy-wasm/Cargo.toml` and its lockfile, `scripts/vendor_wheels.sh`,
`scripts/install_aegis.sh`. Dependency pins that merely contain the string
(`protobuf>=5.0.0`, `redis>=5.0.0`, `sugarss ^5.0.0`) are deliberately unchanged.
- **Publication statements restructured rather than renumbered.** Every artifact
that said "the source release target is `v5.0.0` **and it was published
2026-09-16**" now separates the two facts: the target is `5.0.1` (published
nowhere, §1.0a) and `v5.0.0` is the most recent published release, with its
readback record intact. A blind string swap would have made the publication claim
false against the new target.
- **The same class one layer out, closed and gated (`REG-D56`).** The restructure above covered the artifacts whose sentence fused *target* and *publication*; it did not reach the wider corpus, where 53 files still named `5.0.0` as their checked-out source baseline — invisible to the repository-wide currency gate because its superseded-token was pinned to `4.1.2` (whose comment still said "because that is `5.0.0`"). 116 sentences restructured into the same two-fact form, review stamps untouched (`AUD-37` owns content reviews; noted there); the gate now carries both superseded tokens, frames `registr(?:y|ies)`, exempts the three dated records it would otherwise misread (`IMPLEMENTATION_LOG_5.0.1.md`, `STATE_MANIFEST.md`, `docs/commercial/ARTIFACT_INVENTORY.md`), and flags the `not wired in <version>` warning family (`_STALE_WIRING`); the user-facing "Not wired in 5.0.0" strings (15 warnings, 4 presets, one doc row, one test docstring) now name `5.0.1`, pinned by `test_inert_warnings_name_the_checked_out_release`; `dashboard/package-lock.json`'s linked-SDK copy and the two agent-facing files (`.github/copilot-instructions.md`, `.claude/agents/release-truth-auditor.md`) corrected; the AI-context manifest regenerated (83 files). `tests/test_documentation_currency.py`.
- **Two SDK READMEs corrected.** Both said the registry's `aegis-latent-sdk`
`5.0.0` "matches this source tree's SDK"; after the bump it no longer does, and
the sentence now says so.
- `AUD-35` comments in `deploy/docker/docker-compose.enterprise.yml` and
`deploy/helm/templates/statefulset.yaml` no longer pin an open finding to a
version number.
- **`dev` extra carries the two packages whose tests nothing installed.**
`pyarrow>=16.0.0` and `prometheus-client>=0.20.0` are added to `[dev]` because
every suite-running job installs `-e ".[dev]"`: with them only in `lakehouse`
and `metrics`, the Parquet exporter's twelve tests and seven `/metrics` tests
skipped in **every** environment, including the end-to-end registry test
`CLM-102` cites as its `LOCALLY TESTED` proof (`REG-D38`, `REG-D40`). No
runtime dependence changes; both extras still carry them for operators.
- **`[tool.coverage.report] precision = 2`.** `--cov-fail-under` is decided by
`coverage.results.should_fail_under(total, floor, precision)`, and the default
precision of `0` rounds the total *up* before comparing, so a run in
`(floor - 0.5, floor)` printed `FAIL Required test coverage … not reached` and
still exited `0`. That hole applies to the repository's own
`--cov-fail-under=65` (`Makefile:46`, `.github/workflows/ci.yml:306-309`) as
much as to any stricter invocation; the displayed and the enforced percentage
are now the same number (`REG-D41`).
### Fixed (unreleased, since the `5.0.0` source target)
All of the audit remediation on this branch ships under this version. Per-register
detail is in `docs/REGISTRY.md`; the summary:
- **REG-D27 (AUD-23)** — five defects: non-finite floats could reach the sealed
audit WAL line (now refused before the lock, `allow_nan=False` as backstop);
`hardware_token` canonical fields were delimiter-free and NUL-shiftable (now
length-prefixed, with NUL rejected); `wal_backup.restore()` wrote the live WAL
in place while its docstring said "atomically" (now temp file + `os.replace` +
directory fsync); the `worm_ledger` seal helpers read the whole segment (now
streaming, peak <1 MiB on a >8 MiB file); the httpx relay buffered any upstream
body (now bounded by `max_stream_response_bytes`). Tests:
`tests/test_crypto_audit_wal_json.py`, `tests/test_wal_backup_atomic_restore.py`,
`tests/test_worm_ledger_bounds.py`, `tests/test_forwarder_response_cap_httpx.py`.
- **REG-D26 (AUD-22)** — compliance wording: 64 locations across 26 files carried
SOC 2 / HIPAA / GDPR / ISO 27001 / FedRAMP / PCI / admissibility claims without
the style guide's qualifier. Corrected, including two generators and the
OpenAPI description served at `/docs`;
`tests/test_compliance_wording_gate.py`.
- **REG-D23 (AUD-19)** — documentation currency: four statements were **false**,
not merely stale (SDK guide, FAQ, deployment guide, release status). Corrected
against the readback record and gated repository-wide at sentence granularity:
`tests/test_documentation_currency.py`.
- **REG-D24 (AUD-20)** — the MiFID II and MAR modules are built but unwired; they
are in `docs/CLAIMS_MATRIX.md` as `CLM-103`/`CLM-104` with corrected citations,
and wiring-or-retire is `AUD-36`; `tests/test_regulatory_input_claims.py`.
- **REG-D22 (AUD-18)** — the Makefile and CI now share one canonical gate scope;
`tests/test_gate_scope_parity.py`.
- Earlier rows (`REG-D05` … `REG-D18`, `REG-D25`, plus the legacy register's 66
terminal rows) are recorded in `docs/REGISTRY.md` with per-row evidence under
`evidence/registry/`.
- **REG-D37** — the guarded-pickle allow-list never reached nested values:
`_validate_allowed` tested `isinstance(obj, allowed)` before recursing, and
`dict`/`list` are themselves in `DEFAULT_ALLOWED`, so the recursion branches
were unreachable and `{"k": <anything>}` passed. A `set` payload carries no
`GLOBAL` opcode (`EMPTY_SET`/`ADDITEMS`), so the post-load check was the only
guard and it never fired. Fixed by reordering; with the old order restored,
seven tests fail. The module is allowlisted for import reachability, so the
exposure was library callers of `safe_pickle_load`/`safe_pickle_dump`, not the
request path. `tests/test_safe_serialization_failclosed.py`.
- **REG-D38** — the Parquet exporter's twelve tests ran nowhere: no workflow
installs the `lakehouse` extra, so the module sat at 0% coverage while
`CLM-071`'s `LOCALLY TESTED` rested on a suite that module-level-skipped.
Fixed by the `dev`-extra rule above, plus a tree-wide sweep over every
`pytest.importorskip` so a future silent skip fails the build.
`tests/test_optional_backend_declarations.py`.
- **REG-D39** — two tests reddened every full-suite run on a loaded machine for
reasons that were not defects. The streaming test truncated silently when its
own 30 s duration cap fired under contention (its in-loop memory bound held on
every iteration), and the audit-export test's unbounded writer thread crossed
the endpoint's documented 1000-node limit, which is correct product behaviour.
Both fixed by removing the wall-clock dependency, with one *added* assertion
(`outcomes == ["complete"]`) that turns a truncation into a failure. No bound
was relaxed.
- **REG-D40** — the `metrics` extra was installed by no job, so seven `/metrics`
tests skipped in every environment (see the `dev`-extra entry above). The skip
sits inside those tests, which is why `REG-D38`'s module-level sweep could not
see it; it was found by checking that fix against the authoritative skip
taxonomy (`pytest -rs`) instead of the source. Installing the extra also
exposed a pre-existing order coupling in `tests/test_observability_new.py`:
two reload tests were passing on globals left behind by a neighbouring test,
because `importlib.reload` never clears the namespace. Those two now skip with
the mechanism in their `reason=`, and the branch-only attributes they patch
are created with `raising=False` rather than relied on.
- **REG-D41** — the coverage gate itself, described under *Changed*.
### Tests
- **Coverage: 88.67% → 90.06%** for the 5.0.1 mission order's
`--cov-fail-under=90`: 20,300 statements / 2,300 missed → 20,298 / 2,018, so
284 statements are newly covered, and the suite is
`7,201 passed, 115 skipped, 0 failed, exit 0`. The repository's own enforced
floor is unchanged at 65%.
- New test files: `test_error_response_hygiene.py`,
`test_no_defect_markers_in_shipped_code.py`,
`test_safe_serialization_failclosed.py`, `test_forwarder_sse_framing.py`,
`test_rate_limiter_reservation.py`, `test_dependencies_identity_helpers.py`,
`test_config_validation_branches.py`, `test_gossip_runtime_lifecycle.py`,
`test_observability_without_the_metrics_extra.py`; plus new cases in
`test_optional_backend_declarations.py` (the availability-gate table).
- `aegis/core/observability.py` 65% → **98.43%**: installing the extra made the
real metrics branch the one that executes, which left the `except ImportError`
fallback untested; it is covered by re-importing the module under a private
name with `sys.modules["prometheus_client"] = None` (what makes the import
raise), in-process, because coverage does not follow a subprocess.
### Fixed — the declared signature scheme is now covered by the signature
`node.signature_scheme` was a self-declared label: verification dispatched on it, and `REG-D06`
fenced it by the shape of the material beside it, but the label itself was not an input to the
signed bytes — the signing path learned its scheme as the *result* of signing (`_sign` returned
it), so it could not exist before the payload was built. A relabel inside one material-shape
class (the presence-only tiers `pqc-ml-dsa` / `pkcs11-*`) changed which verifier was consulted
and nothing else, so a deployment holding the claimed tier's key would verify a claim nobody
made.
`_sign` is now `_sign_bound(build_payload)`: the tier is selected first and the payload is
rebuilt per attempt with that attempt's label appended, on all three record-creating paths
(`commit_forensic`, `commit_rejection`, `commit_forensic_summary`). `HSMSigningBackend` gained
`scheme_label()`, which resolves the label from the token key's `CKA_KEY_TYPE` before a
signature exists; a backend that cannot answer learns it from one extra, ledger-cached
signature instead of assuming one — a cost, never a weaker binding. The label is validated
against the closed scheme vocabulary, so the `|` delimiter cannot be smuggled into it.
Compatibility is additive rather than version-gated: `signed_payload_candidates_for` offers the
scheme-bound shape first and the older shapes only for signatures actually made over them, so
chains written before this change keep verifying unchanged and no `node_hash` moved. Two
boundaries stay published as `UC-054`: for records written before 2026-09-21 the label is still
unanchored, and a tier this build has no verifier for still reads `unverified` in-build — what
the binding changes is that such a claim becomes *checkable* by a deployment that holds the
tier's key, instead of being trusted on shape alone.
Tests: `tests/test_signature_scheme_binding.py` (15, including the audit's relabel scenario
refused once a verifier for the claimed tier exists, its pre-binding control, and the HSM
mid-flight fallback), 5 added in `tests/test_hsm.py` (`scheme_label()` resolved without
signing, when the key is missing, when the backend is unavailable, when the token errors, and
on EC keys), and the failure-injection seam `_sign` → `_sign_bound` in
`tests/test_mmr_rollback.py`. Row `REG-D31`, ticket `AUD-27`.
### Added — durable terminal-evidence outbox (opt-in)
- **REG-D32** — `AEGIS_TERMINAL_OUTBOX_ENABLED` (default off) spools every terminal
commit a stream hands off at teardown to a content-free, `0600` spool beside the
WAL and replays whatever is still pending at the next start, before traffic.
Before this, a crash or SIGKILL between teardown and the handed-off commit lost
the node, and a full handoff queue dropped it. Every spool line is HMAC-authenticated
under a key derived from `AEGIS_SIGNING_KEY` (required when enabled); a line that
fails, is torn, or is mistyped is quarantined, never replayed. A recovered node is signed as
`stream-terminal-evidence-recovered` with `evidence_status` `recovered-terminal`,
carries no previews and is timestamped at replay; replay refuses a ledger whose
fault state is not `healthy` and skips a record already committed in the
retained window. `commit_forensic_summary` gains the matching `request_digest`
form. Three metrics: `aegis_terminal_outbox_pending`, `_recovered_total`,
`_errors_total`. `CLM-106`; `UC-052` amended; `CLM-046`'s stale "no durable
evidence record" sentence corrected.
### Fixed — found by the 2026-09-22 re-anchor and the closure work
- **REG-D42** — `main`'s Forensic workflow was red on 3.11/3.12/3.13: the gossip
native-accumulator refusal test presumed the Rust extension absent, and that
workflow builds it. The test now removes the module through `sys.modules`. The
`RLIMIT_NPROC` thread-exhaustion probe, which measured nothing as root, now drops
to `nobody` first. No product change.
- **REG-D34** — the strict doc gate's v4-publication rule read the last digit of a
decimal (`32.4 s`) as a version, and its disclaimer let `Python 3.4` waive a real
`v4.0.0 was published`. One shared version token now serves both halves.
- **REG-D33** — `scripts/` and `tools/` now pass `mypy --strict` (41 files) and CI's
typecheck job and `make type` both check them. The first strict run found
`tools/forensic/diagnose_aegis.py` calling the static `PQCSigner.verify` without
its public key, so the diagnostic reported the PQC signer FAILED on every
install where it works.
- **REG-D43** — `scripts/audit_documentation_corpus.py` failed its own contract at
`main` on an elided payload in `DOC-03`; the document now quotes the real test
payload, and the placeholder rule runs in the suite.
### Fixed — found on PR #196 after it merged
CodeQL, Codex and three of `main`'s own post-merge CI jobs raised these on
the PR that closed the registry to 100%; it auto-merged before the five bot
findings were addressed, and the three CI jobs were only found by reading
`main`'s post-merge runs directly rather than assuming them green. Fixed on
the branch restarted from `main` afterward.
- **REG-D45** — `streaming.py` and `terminal_outbox.py` imported each other's
types (`TYPE_CHECKING`-only, so never reachable, but a real cyclic-import
smell). `terminal_outbox.record()` now takes a structural `TerminalSummary`
`Protocol` instead of importing `streaming.StreamEvidenceSummary`.
- **REG-D46** — `commit_forensic_summary`'s `request_size` was assigned inside an
`if`/`elif` pair CodeQL could not prove exhaustive. It is a plain `if`/`else`
now; the exhaustiveness was already guaranteed three lines above.
- **REG-D47** — two `assert x.pop(...) == ...` statements in
`tests/test_terminal_outbox.py` compile out under `python -O`, silently
dropping the side effect the next assert depends on. Fixed at the flagged
site and at a structurally identical one this same PR had also added.
- **REG-D48** — a terminal-outbox open refused (missing `AEGIS_SIGNING_KEY`, or
a bad path) after the SIEM exporter and S3 archiver had already started;
`lifespan`'s `@asynccontextmanager` means a pre-`yield` exception skips the
entire post-`yield` shutdown half, so both leaked on every such refusal with
either configured. The outbox now opens before either starts.
- **REG-D49** — three blank lines in `reg-d42_fixed.txt` carried trailing
whitespace, against `AGENTS.md`'s own `git diff --check` pre-commit rule.
- **REG-D50** — CI's "License Headers" job was red: nine files added by the
prior closure commit carried no license block, and no local target runs the
checker that would have caught it before push.
- **REG-D51** — CI's "Rust Extension" job was red: the zk verifier-key
wire-decode ceiling (`MAX_VERIFIER_KEY_BYTES`, `zk_mmr.rs`) was a guess made
on a host where `zk-spartan` cannot run at all (`REG-D04`), and on the first
ADX-capable CI run to actually exercise it, the real key was ~10.5× over.
Raised to `1 << 27`, consistent with `DOC-08` §6.5's already-published "tens
of megabytes" for the shapes it measured.
- **REG-D52** — Security's "OSV Scanner" job was red: `anyio`, transitive via
`fastapi`/`httpx`/`starlette`/`uvicorn`, had no floor in `requirements.txt`,
so the scanner's resolution landed on 4.9.0 — carrying a critical TLS
IDNA-2003 host-name-spoofing advisory (`GHSA-82r6-8w77-94w6`) and a
process-pool undrained-`stderr` deadlock (`GHSA-5p39-cfhj-2xmp`), both fixed
in 4.14.2, already the pin in `requirements.lock`. `anyio>=4.14.2` added to
`requirements.txt` beside the existing `idna`/`urllib3` security floors;
`requirements.lock` regenerated (one line: `anyio`'s `# via` comment).
### Fixed — found on PR #197 after it merged
The same pattern one more time: the PR that fixed the eight findings above
auto-merged before its own review round — CodeQL and Sourcery — was
addressed. Fixed on the branch restarted from `main` afterward.
- **REG-D53** — CodeQL flagged all seven `...` stub bodies on the
`TerminalSummary` `Protocol` `REG-D45` added as "Statement has no effect".
`...` is the standard `Protocol`-stub idiom and never executes, but this
rule flags any bare Ellipsis statement regardless. Replaced with `pass`,
semantically identical and not matched by the rule.
- **REG-D54** — `commit_forensic_summary`'s docstring promises every field is
type-checked before the lock; `response_size`, `token_count` and
`elapsed_seconds` instead compared/inspected a value with no `isinstance`
check first, so a non-comparable input raised an incidental `TypeError`
rather than the documented `ValueError`; `final_marker_included` had no
check at all; `redaction_hits` never confirmed its keys were strings and
let a `bool` value through as a count. All five now get an explicit
`isinstance` check, following the file's own existing `request_size` idiom.
A second review round on the same PR found `redaction_hits` itself was
never checked to be a `dict` before conversion; fixed with one more
`isinstance` check ahead of it.
- **REG-D55** — `REG-D48` closed the leak for the terminal outbox's own
`open()` call raising, but not for anything raising *after* a successful
open — `replay_pending`, SIEM/S3 startup, the handoff worker, LSM, vault,
the forwarder, gossip or seccomp — each of which still skips `lifespan`'s
entire post-`yield` shutdown half by the same `@asynccontextmanager`
mechanics. `lifespan` now wraps that whole startup span in a
`contextlib.ExitStack` that closes the outbox on any exception, disarmed
only once startup fully succeeds. A second review round on the same PR
found the same gap one layer further out — once the handoff worker itself
started, nothing stopped *it* (or the forwarder, SIEM exporter, S3
archiver, analysis workers, or gossip mesh) if a later step then raised.
`ExitStack` became `contextlib.AsyncExitStack`, with a stop callback
registered for every one of those resources right after it starts.
### Added — `aegis-sdk`: offline bundle verification and gateway audit status
- **`aegis-sdk verify <bundle.zip>`** (`CLM-107`) checks a forensic bundle
without contacting any gateway: member set, canonical manifest and its
self-seal, each evidence file's size and SHA-256, the ledger slice's CID, every
MMR inclusion proof, an optional `--trusted-root`, and the Ed25519 manifest
signature against a public key supplied out of band. Exit `0` only when the
signature verified; `3` (`INCOMPLETE`) when nothing failed but it was not
checked — unsigned, no key, or `cryptography` absent. Digests, CIDs and proofs
are stdlib-only; the signature step needs the new `[verify]` extra.
- **`aegis-sdk audit <gateway_url>`** prints `/v1/audit/health` (and
`/integrity`) as sorted JSON. The key comes from `AEGIS_AUDIT_API_KEY`, is
never sent over plain HTTP to a non-loopback host, and never follows a
redirect.
- Named `aegis-sdk` because the gateway already installs `aegis`.
`sdk/shared/forensic-bundle-v1.zip` is a gateway-issued signed fixture
(`scripts/generate_sdk_bundle_fixture.py`); `tests/test_sdk_bundle_contract.py`
fails if a freshly built bundle's shape drifts from it.
### Added — real benchmarks and a documentation fact-check pass
- **`scripts/run_benchmarks_5.0.1.py`** measures `commit_forensic` latency
percentiles, concurrent-thread commit throughput against one shared WAL
writer lock, peak RSS under simulated concurrent streaming ingestion, and
Ed25519/ML-DSA-65 sign+verify timing — against real backends, no invented
numbers. Retained: `evidence/benchmarks/benchmarks_5.0.1_2026-09-24.json`;
documented in `docs/benchmarks/BENCHMARK_METHOD.md` §8, including why the
streaming-memory phase must run before the thread-heavy commit phases in
the same process (`ru_maxrss` is a whole-process high-water mark).
- `README.md` now cites five of these numbers, a fresh test-suite count
(7,444 parallel / 7,449 serial, 2026-09-24), and the `aegis-sdk` CLI added
earlier in this section.
### Fixed — two stale documentation defects found by cross-checking, not assuming
- **REG-D62** — `docs/ROADMAP.md`, `docs/architecture/DECISIONS.md` (AD-12)
and `docs/institutional/DOC-02_CRYPTOGRAPHIC_FORENSIC_BLUEPRINT.md` all
still said MMR v2 was unwired and v1 the ledger's default, contradicting
`docs/CLAIMS_MATRIX.md`'s own already-correct `CLM-064` row: `auto` has
started every new chain on v2, and the Rust accumulator has implemented v2
as well as v1, since this session's earlier work. Corrected all three to
state only what remains open — no in-place v1→v2 upgrade for an existing
chain.
- **REG-D63** (`DOCUMENTED`) — `aegis/core/dosage_hallucination.py` and
`aegis/core/clinical_claim_detector.py` are implemented, tested, and
reachability-allowlisted, but carried no `CLAIMS_MATRIX`/
`UNSUPPORTED_CLAIMS`/`ROADMAP` row anywhere. `UC-068` now states the
general hallucination/output-correctness boundary precisely — no code path
validates a response against ground truth — without overclaiming that no
hallucination-adjacent code exists. Whether to wire, rename or delete
either module is left open, an `AUD-35`-shaped decision outside this pass.
### Fixed — the forensic preview cap is configuration, and guarded
- **REG-D59** — `AEGIS_MAX_FORENSIC_BYTES` (`0`–`65,536`, default unchanged)
now reaches the ledger and the streaming path; before, the gateway had no
such setting, `.env.example`'s `1048576` was read by nothing, and the stream
proxies hard-coded 65,536, so a lowered ledger cap alone would have failed
every long stream's terminal commit. Where zero-knowledge proving is compiled
in, startup warns when the cap exceeds `256`, the largest measured
(`DOC-08` §6.3); it never lowers the cap itself. **Upgrade note:** an
environment still carrying the old example's `1048576` now refuses to start
(`docs/UPGRADING.md` §8).
- **REG-D60** (recorded, open) — five other example variables are read by
nothing; each awaits a wire/rename/delete decision.
- **REG-D61** — CodeQL flagged `REG-D57`'s test for importing
`aegis.proxy.app` both ways (PR #200 merged first); it now uses one
`from aegis.proxy.app import …`, which reads the same binding `lifespan` uses.
### Fixed — two red test jobs PRs #198 and #199 merged with
- **REG-D58** — PR #199 filed `AUD-39` and added an `import aegis` edge to a
test without re-running `scripts/generate_module_inventory.py`, so
`docs/MODULE_INVENTORY.md` was stale on `main` and both
`tests/test_module_inventory_current.py` checks failed there. Regenerated
with the generator.
- **REG-D57** — `test_a_much_later_startup_failure_stops_the_handoff_worker_too`
passed under `pytest -n auto` and failed in CI's serial Forensic job on
every Python version. An earlier test reloads `aegis.proxy.forwarder` on
purpose; after that, a fresh `from aegis.proxy.forwarder import
LLMForwarder` is a different class object from the one `app.py` holds, so
the test patched a class the app never calls. Both lifespan tests now patch
the classes `lifespan` actually resolves.
## [5.0.0] — 2026-09-16
**Published 2026-09-16** on every surface except PyPI `aegis-latent-core`: signed
annotated tag `c34d412` at commit `b2e4335`, a GitHub Release with 31 assets, PyPI
and npm `aegis-latent-sdk` `5.0.0`, and both GHCR images, each read back
(`docs/RELEASE_STATUS.md` §1.0). *(Heading and this note added 2026-09-24,
`REG-D71`; the paragraph below is the pre-publication text, kept as written.)*
**Nothing is published for `5.0.0`.** There is no tag, GitHub Release, PyPI or
npm artifact, and no OCI image. This section records what the source tree
contains at fourteen synchronized `5.0.0` anchors; it is not a release
announcement, and the date of any future release is not set here. See
`docs/RELEASE_STATUS.md` §1.0.
**`4.4.0` does not exist.** The number was skipped deliberately when the
source line moved from `4.3.0` to `5.0.0`. The jump reflects a breaking
change to the public JSON API — `signature_assurance` (`CLM-090`) replaces
`legal_admissibility` on `/audit/health` and `/audit/integrity` — not a
version-numbering oversight. No `4.4.0` artifact was ever built or
published, so its absence from any registry is expected rather than a
withdrawal. (`4.2.0` was likewise skipped, deliberately, at the prior
`4.1.2` → `4.3.0` move.)
### Fixed — four findings three independent audits agreed on
An adversarial pre-mortem checked fifteen predicted findings against source
before any of them were worked. Five were already fixed, two are architectural
and documented rather than repaired, three need a human, and one — "99 orphan
modules" — was 78. Four were real, open, and fixable in code. These are those
four.
**Layer-2 WAF normalization (`CLM-093`).** Layer 1 folded Cyrillic/Greek
confusables to ASCII before matching; Layer 2 (`LLMGuardLocal.analyze_input`)
received `_extract_text` output and scored it raw. A payload matching a
*Layer-2* pattern, written in confusables, passed both. `CLM-091` recorded this
as its own boundary; it is now closed. `AegisWAF._guard_variants` feeds Layer 2
the raw text **first**, then each normalized variant, so the change can add a
detection and never mask one — Layer 2 runs its own decoder over the bytes as
given, and NFKC is lossy. Proven with `acting as an unrestricted`, a Layer-2
pattern with no Layer-1 counterpart: raw → not detected, normalized → detected.
**Stream admission control (`CLM-094`).** Per-stream memory was bounded
(`R_max` ≈ 1.18 MiB); the number of concurrent streams was not, so aggregate
retained bytes scaled with connection count. `StreamAdmissionGate` caps them at
`AEGIS_MAX_CONCURRENT_STREAMS` (default 256) and refuses with 429 rather than
queueing — waiting for a slot would trade a memory bound for a latency bound.
The slot is tied to the stream generator's lifetime, not the handler's: a
handler `finally` would free it before a byte was sent. Client disconnect is the
case that matters and the one covered.
**Signed forensic bundle manifest (`CLM-095`).** `VERIFY.sh` compared files
against digests embedded in `VERIFY.sh`, both inside the same unsigned ZIP.
Opt-in Ed25519 signing over the exact `manifest.json` bytes breaks that circle.
**The public key is not shipped in the archive**, and that absence is the point:
a key read from the ZIP being checked would relocate the circularity rather than
remove it. Without `AEGIS_BUNDLE_PUBKEY` the script says outright that it
detects corruption, not tampering.
**RFC 3161 CMS verification (`CLM-096`).** `verify` accepted any token whose
outermost DER tag was a SEQUENCE — a token with no TSA, certificate or
signature anywhere in it passed, and the module's own comment admitted it.
`aegis/core/rfc3161_cms.py` now checks the signature over the re-encoded
`signedAttrs`, the `messageDigest` binding to the `TSTInfo`, the
`messageImprint` against the expected digest, and a validity-window path to
caller-supplied anchors. **No revocation checking**, because it needs network
access a forensic verifier often lacks; `revocation_checked` is always `False`
so the gap cannot read as done.
One pre-existing assertion was **inverted rather than deleted**:
`test_valid_stamped_package` asserted `valid is True` for a stub token, which
pinned the weak check in place. It now asserts rejection, under a name that says
why.
### Changed — three prescribed fixes were redesigned rather than applied
The directive carried its own `PD-X1`: halt and redesign a fix that introduces a
regression or contradiction. Four did, and the reasoning is recorded here
because "we did not do this" is worth more than a silent omission.
- **`timestamp: float` → `int` microseconds was not applied.** Floats never
reach a JCS float serializer: `_jcs_bytes` *rejects* them, and the MMR leaf
formats the timestamp as `f"{…:.9f}"`, which is deterministic cross-language.
The change would alter every leaf hash, root, signature and issued proof for
zero determinism gain, and contradict `UPGRADING.md` §6's byte-identity
promise for a release that is now published.
- **The group-commit staging buffer was not built.** The append happens under
the lock so the next committer can read the tip hash; staging serialises that
and costs the coalescing. The real invariant — no caller is ever told a record
is durable when it is not, and the fault latches — is now pinned by
`TestWhatAFsyncFailureLeavesBehind`. That work also established, and now
documents, that rejected records *are* present in the WAL file: the bytes are
written before the `fsync` that failed.
- **"v1 → v2 migration" was replaced by cross-signed anchoring.** A root is its
construction, so recomputing v1 leaves under v2 yields a root no verifier ever
witnessed and breaks every issued proof. `tools/anchor_v1_chain_into_v2.py`
records where the v1 chain ended instead, and leaves it byte-for-byte intact.
- **No valuation guide was written.** A sweep found **zero** valuation claims in
the corpus; the figures in circulation came from the external audits. Adding a
dollar range would have injected the unevidenced claims the task was meant to
remove.
### Added — NFS/EFS lock guidance, and the commercial readiness register
`docs/operations/STORAGE_REQUIREMENTS.md` already warned against network
filesystems on `fsync` grounds. It now also documents the independent
`flock`/`lockd` failure: an ungracefully killed pod cannot release its WAL lock,
the server holds it through its reclaim timeout, and the replacement pod sits in
`CrashLoopBackOff` that resolves itself minutes later — a storage property
routinely misfiled as a gateway race.
`docs/commercial/COMMERCIAL_READINESS.md` records the six actions no agent can
perform — pen test, SOC 2, escrow, commercial licence, design partners, second
maintainer — each marked `NOT STARTED`, with the acceptance test for the second
maintainer being a security fix shipped end to end *without the founder*.
### Fixed — the corpus cited a backpressure figure measured before group commit existed
Nine documents, `CLM-034` among them, quoted `p99 836.35 ms` from
`evidence/execution_2026-08-20/backpressure_stall_report.json` as the in-tree
baseline. That report was produced at `20fa011`. The coalesced group-commit
engine (`aegis/core/group_commit.py`, `CLM-082`) first entered the tree on
2026-09-10 in `b791638`, so the retained figure measures a ledger that fsynced
once per committed node — behaviour the tree no longer has.
The harness was re-executed three times at `88e01f0` with the same parameters
(0.25 s offered window, 10,000 offered RPS, 2.0 ms injected `fsync` delay, 64
workers). Recorded in
`evidence/backpressure_group_commit_remeasurement_2026-09-16.md` with raw
reports in `evidence/execution_2026-09-16/`:
| | 2026-08-20 (`20fa011`) | 2026-09-16 (`88e01f0`) |
|---|---|---|
| p50 | 167.290 ms | 33.545 ms |
| p95 | 504.704 ms | 41.176 ms |
| p99 | 836.351 ms | 51.875 ms |
| max | 2,290.622 ms | 59.726 ms |
| `fsync` calls | 2,501 | 200 |
| Drain time | 6.630 s | 1.573 s |
Both runs: 2,500 offered, 2,500 durable, 0 failures, 0 missing identifiers, 0
duplicates, chain integrity valid.
**The older number is not withdrawn and not wrong** — it is a correct
observation of the tree it measured, and it stays in the corpus labelled as
superseded for current-state citation only. **The two runs are on different
hosts**, so the millisecond delta is not a controlled speedup measurement and
must not be quoted as one; the `fsync`-call count is the host-independent part,
and 200 calls for 2,500 records is the coalescing `CLM-082` describes.
Three boundaries are now stated wherever the figure appears, because the old
citation invited all three misreadings: this is **queueing latency under
deliberate oversubscription, not per-request overhead** (re-running at
`--fsync-delay-ms 0` moves p50 by about a millisecond, which is what shows the
injected disk delay is not the dominant term); it is an **in-process commit
measurement, not end-to-end API latency**; and the per-commit figure someone
actually wants when asking what Aegis costs is
`evidence/evidence_path_measurements_2026-09-03.md` §2 — `commit_forensic` with
a real `fsync` per node at **808.565 µs/op** — which none of the backpressure
runs is.
Updated: `docs/CLAIMS_MATRIX.md` (`CLM-034`), `docs/benchmarks/BENCHMARK_METHOD.md`,
`docs/benchmarks/BENCHMARK_RESULTS.md`, `docs/benchmarks/README.md`,
`docs/BENCHMARKS.md`, `docs/FAQ_TECHNICAL.md`,
`docs/compliance/COMPLIANCE_MAPPING.md`,
`docs/assurance/AUDIT_EVIDENCE_INDEX.md`, `evidence/INDEX.md`.
### Fixed — three documents said "twenty Safe Harbor detectors"; `_SAFE_HARBOR_PATTERNS` holds seventeen
`docs/CLAIMS_MATRIX.md`, `docs/privacy/PII_REDACTION_BOUNDARIES.md` and
`docs/architecture/ARCHITECTURE.md` had settled on "twenty" while
`docs/compliance/HIPAA_TECHNICAL_INPUTS.md` (which enumerates the labels),
`docs/corporate/CORPORATE_FAQ.md`, `docs/privacy/DATA_PROCESSING_CHECKLIST.md`
and the code itself said seventeen. `PII_REDACTION_BOUNDARIES.md` contradicted
itself sixteen lines apart.
Corrected to seventeen, and `tests/test_safe_harbor_detector_count.py` now pins
both the count and the label set to `_SAFE_HARBOR_PATTERNS`, failing with a
message that names every document carrying the number. This asserts a count,
not a coverage property: seventeen pattern categories are not the eighteen
identifiers of 45 CFR 164.514(b)(2), and `CLM-016`/`CLM-057` remain the
boundary.
### Fixed — `aegis/core/formal_proofs.py` claimed proofs in `.v` (Coq) files that do not exist
The module docstring said proofs were the "Target: Formal Verification via
Coq/Lean"; the class docstring said "the full proofs reside in .v (Coq)
files"; a module-level comment called a Python dict of axiom strings
"FORMAL SPECIFICATIONS (COQ-STYLE)... used in the formal proof files (.v)".
There is no Coq toolchain and no `.v` file anywhere in this repository —
`find . -iname "*.v"` returns nothing. The real, mechanically checked
formal artifacts are under `specs/`: a Lean 4 theorem
(`specs/AegisVerification.lean`), a Z3 SMT2 formula
(`specs/aegis_invariants.smt2`), and TLA+/TLC models, all documented with
their actual bounds in `docs/formal/FORMAL_VERIFICATION.md`.
`FormalVerificationSuite` itself is an empirical checker — it runs a stated
property against real implementation functions on concrete inputs and
returns a bool, the same epistemic weight as a unit test — not a proof
engine, and never was; the `"Target: Mathematical Certainty"` startup log
line overstated that too. All three false/overstated claims are corrected
to describe what this module actually does and where the real proofs
actually live. Behavior is unchanged; `aegis/core/formal_proofs.py` is not
imported anywhere in this repository, so this is a documentation-only fix.
### Fixed — the enterprise server trusted every direct client's forwarded headers
`aegis_server/main.py`'s `main()` passed `forwarded_allow_ips="*"` to uvicorn
unconditionally. With `proxy_headers=True`, that told uvicorn to trust
`X-Forwarded-For`/`X-Forwarded-Proto` from **any** peer that could reach the
listener, not just a real reverse proxy — letting any direct client spoof
its own source IP (defeating IP-based allowlisting downstream) or claim
`X-Forwarded-Proto: https` over a connection that was never TLS.
`EnterpriseSettings` gains `trusted_proxy_cidrs` (env
`AEGIS_TRUSTED_PROXY_CIDRS`, default `127.0.0.1,::1`); `main()` now passes
`get_trusted_proxy_cidrs()` instead of the hardcoded wildcard.
`validate_runtime_invariants()` refuses startup in strict mode if `"*"`
appears anywhere in the resolved list — and because that check runs from
the ASGI lifespan rather than from `main()` itself, it also covers
`uvicorn aegis_server.main:app` launched directly, which is how
`deploy/docker/docker-compose.enterprise.yml` actually starts the service.
That compose file was never exposed to the hardcoded default in source: its
command line passes no `--forwarded-allow-ips` flag, so it already ran
under uvicorn's own conservative `127.0.0.1` CLI default. The defect was
live, reachable code nonetheless — `python -m aegis_server.main`, or the
module's own `if __name__ == "__main__"` block — that this repository's one
sample deployment simply didn't exercise.
`main()`'s docstring also claimed it was "registered as
`aegis-enterprise-server` in pyproject.toml." It isn't: `[project.scripts]`
registers only `aegis` and `aegis-server`, both mapped to
`aegis.proxy.app:main`. Corrected alongside the fix rather than left to
mislead the next reader about how this code is actually invoked.
`CLM-092` and `DOC03-C022` are the controlled claims.
### Fixed — the homoglyph normalizer existed and was never wired into the WAF
`aegis/core/homoglyph_normalizer.py` mapped Cyrillic/Greek/letterlike/fullwidth
confusables to ASCII, had its own passing unit tests, and was imported by
nothing in `aegis/`. Meanwhile the WAF's own comment claimed NFKC — which
`AegisWAF._normalize_text` does apply — "collapses Unicode lookalike
characters (full-width letters, homoglyphs)". That's true for full-width
letters and false for homoglyphs: Cyrillic `а` (U+0430) and Greek `η`
(U+03B7) are canonically distinct from Latin `a`/`n`, not compatibility
variants, so NFKC leaves them untouched. A payload spelling "ignore previous
instructions" with Cyrillic or Greek lookalikes had no literal ASCII
substring for any Layer-1 critical pattern to match, and reached Layer 2
unmodified either way.
`_normalize_text` now runs `HomoglyphNormalizer` after zero-width stripping
and NFKC, so Layer 1 sees the ASCII form. **Layer 2 is untouched by this
fix** — `LLMGuardLocal.analyze_input` scans text `AegisWAF._extract_text`
extracts directly, bypassing `_normalize_text` (and therefore NFKC too)
entirely; that gap predates this change and remains open. The stale WAF
comment is corrected alongside the fix. `CLM-091` and `DOC03-C004`
(promoted from `ROADMAP` to `IMPLEMENTED`, Layer-1-scoped) are the
controlled claims; `tests/test_waf_hardening.py` carries the Cyrillic and
Greek regression tests.
### Fixed — `legal_admissibility` read current config instead of chain history; replaced with `signature_assurance`
**The defect:** `CryptographicAuditLedger.legal_admissibility` returned
`"High"` whenever a signing key was configured *at read time*, without ever
consulting the chain it was describing. `_signing_key` is fixed once at
construction, but `_load_from_wal()` replays history written under a
*prior* process's configuration — so a WAL written entirely under the
ephemeral `ed25519-fallback` tier (no key configured on day one) silently
reported `"High"` the instant a later process reopened it with a key. The
fallback-signed history was never re-examined; the property only asked
"is a key configured now," never "what actually signed these nodes."
**The fix:** `signature_assurance` (new) computes a per-node tier from
`AuditNode.signature_scheme` — what each node was actually signed with —
and reduces the whole chain to its weakest tier. Five tiers, ranked to
match `_sign()`'s own priority order: `UNSIGNED` (defensive floor for
unrecognized/corrupt data, unreachable via a live commit), `COMPROMISED_EPHEMERAL`
(`ed25519-fallback`), `SYMMETRIC_AUTHENTICATED` (`hmac-sha256`),
`ASYMMETRIC_SOFTWARE` (`pqc-ml-dsa`), and `ASYMMETRIC_HARDWARE_ATTESTED`
(the two `pkcs11-*` HSM schemes — previously unhandled by this property
at all). Weakest-link on purpose: one fallback-signed node keeps the whole
chain's reported assurance at `COMPROMISED_EPHEMERAL`, regardless of how
every other node was signed or how the ledger is configured now. An empty
chain has no signing history to report, so it falls back to the tier the
next commit would actually use under current configuration, rather than a
claim about history that doesn't exist yet.
**Breaking change to the public JSON API.** `/audit/health` and
`/audit/integrity` now return `signature_assurance` in place of
`legal_admissibility`; `aegis/proxy/schemas.py` and
`dashboard/src/lib/contracts.ts` were updated to match. This does **not**
touch `aegis.core.iso27037_evidence.EvidencePackage.legal_admissibility` —
a separate field with its own `Admissible`/`Conditional`/`Compromised`
vocabulary and operator-override path, which keeps its name and now
derives its unreviewed default from the corrected chain scan instead of
the old config check — nor `ForensicPDFReportBuilder`'s caller-supplied
`legal_admissibility` parameter, which was never derived from the ledger.
`CLM-090` is the controlled claim. This tier classifies which signing
mechanism produced a signature; it is not a legal conclusion, and nothing
here is legal advice.
### Added — `shredding_version`, and a corrected claim about enabling shredding on an existing chain
The last genuinely missing piece of A3. `sealed_ciphertext` already told a
reader *that* a node was sealed; nothing said *how*. `AuditNode` gains
`shredding_version`: empty for a node written with shredding off or before
the field existed, `v1-aesgcm256-sha256` for one `CryptoShredder` sealed.
Unbound, like the rest of that field group — `mmr_leaf_hash` is itself not a
`node_hash` input, so binding the label while leaving what it labels unbound
would buy nothing.
`from_dict`'s default is conditional rather than flat: a node the shredder
sealed *before* this field existed has real `sealed_ciphertext` and no
`shredding_version` key, and `v1-aesgcm256-sha256` is the only construction
that has ever produced ciphertext here, so a non-empty `sealed_ciphertext`
with no version key can only mean that one. A flat `""` default would have
reported a genuinely sealed legacy record as unsealed.
**`docs/CLAIMS_MATRIX.md` (`CLM-068`) previously stated shredding "cannot be
enabled on an existing chain" and that doing so "means starting a new
chain." Measuring the actual behavior — not assuming it — found the
opposite: `_seal_leaf` has always branched on the shredder's presence at
each commit, so re-opening an existing WAL with the flag newly on seals
every write from that point forward and leaves history untouched. The
resulting mixed chain — some leaves are payload digests, some are sealed —
verifies, because neither form was ever a `node_hash` input.
`shredding_version` is what now lets a reader tell them apart without
inferring it from whether `sealed_ciphertext` happens to be empty. Corrected
in `CLM-068` and its register row rather than left standing.
### Added — a zero-knowledge inclusion proof, and what it cannot say
`aegis_rust_v2/src/zk_mmr.rs` implements an R1CS circuit over Spartan proving
that a leaf whose canonical bytes end with `,"waf_verdict":"passed"}` is
included under a public MMR root — without disclosing the leaf. Behind the
default-off `zk-spartan` feature, so **no published wheel contains it** and no
request path calls it.
[DOC-08](docs/institutional/DOC-08_ZERO_KNOWLEDGE_INCLUSION.md) is the normative
boundary and `CLM-089` the controlled claim. The short form: the proof attests
that **the ledger contains a record asserting a WAF pass**. It does not
re-execute the WAF, does not attest the WAF is correct, and says nothing about
prompt safety, model behavior, or the upstream provider.
**The verdict is a circuit constant, not a checked field.** The hashed preimage
is `0x00 ‖ prefix ‖ ,"waf_verdict":"passed"}` with the suffix compiled in, so a
leaf recording `blocked` — or recording nothing — has no satisfying witness at
all rather than being checked and rejected. SHA-256 rather than a SNARK-friendly
hash, because the circuit has to be about the MMR that exists.
Verification takes a root **and** a verifier key, both of which must be obtained
independently of whoever supplied the proof. Key derivation is deterministic in
the public shape, which is what makes refusing a prover-supplied key practical;
a second independent `setup` verifying the first's proof is asserted by test.
**Three things measurement changed.**
- `spartan2`'s prover is **not a satisfiability oracle**: given an unsatisfiable
witness it returns `Ok` with an inert proof rather than an error. Soundness
holds — the object verifies against neither the honest root nor any other —
but a caller would have shipped worthless bytes. `prove` now verifies its own
output before returning, which is why it takes the verifier key.
- The circuit compares the walk against **every** peak rather than an indexed
one, at roughly 511 constraints each, so the peak index never enters the
public shape.
- **Leaf length decides feasibility.** At the default `max_forensic_bytes` of
65 536 a real leaf needs on the order of 10⁸ constraints and is not provable
at any practical cost. The construction is usable only where that cap is small
or zero, and nothing yet warns when a configuration puts proofs out of reach.
The proof shape — leaf length, path depth, peak count — is **public**, so a
verifier learns roughly how large the request and the ledger are. Leaf contents,
leaf position and every other leaf stay undisclosed. No cost figure is claimed:
`aegis_rust_v2/tests/zk_mmr_cost.rs` prints a curve on demand, but an
`#[ignore]`d harness on one host is not a reproducible artifact.
`aegis.crypto` now re-exports `generate_zk_proof`, `verify_zk_proof`,
`zk_verifier_key`, `has_zk_native` and `split_passed_leaf` through
`aegis/core/zk_native.py`, which imports the extension lazily so a pure-Python
checkout still imports the package and refuses rather than simulating wherever a
proof cannot be produced.
`aegis/core/zk_proof.py` is untouched and remains the non-real stub `CLM-019`
describes. The facade deliberately carries both surfaces and they must not be
blurred: `ZKProver`/`ZKVerifier` are the stubs, these functions are the circuit.
This is a separate surface, not a promotion of that one.
### Changed — the zero-knowledge tests now run in CI
`cargo test --release` builds default features, so the circuit's tests would
have existed without ever executing. Two steps were added rather than one
widened: the circuit's own tests run with `--features zk-spartan`, and a second
feature-enabled wheel is built so the Python proving tests execute somewhere
instead of skipping. A bare `zk_mmr` filter looked sufficient and was not — cargo
matches test names, so it selected the 16 unit tests and silently skipped all
seven end-to-end ones.
### Fixed — two wrong numbers in the `zk-spartan` dependency comments
The feature adds **66** crates, not the 117 the manifest claimed nor the 5 stated
when it landed; `cargo tree --edges normal` resolves 160 by default and 226 with
it. Enabling it also **reintroduces `block-buffer 0.10.4`**, the crate the
`sha2 = "0.11"` migration removed — `spartan2` pins the `digest 0.10` line
through `halo2curves`, `sha2 0.10` and `sha3 0.10`. The removal claim beside
`sha2` is now scoped to the default build, where it is true.
### Added — the WAF verdict is now recorded where a proof can reach it
Groundwork for the zero-knowledge inclusion proof, and a correction to an
epistemic gap that existed independently of it.
**The outcome was real but unrecorded.** A blocked request raises before the
durable commit, so a committed node already *implied* the WAF had allowed it.
An implication is not evidence: nothing in the record said so, and nothing could
prove it. `commit_forensic` now accepts `waf_verdict`, the proxy passes `passed`
at the durable commit, and the value lands in two places that matter — the bytes
the MMR commits to (`build_merkle_leaf`) and the node signature
(`_build_signed_payload`).
Putting it in the **leaf** is the part that makes it provable. A signature binds
the verdict to a node, but the tree would know nothing about it; a statement
about the verdict can only be proved against a root if the verdict is inside the
bytes that root commits to.
**Additive, and that is asserted as bytes.** The field is omitted from the leaf
and from the signed payload whenever it is empty, so a node without a verdict
produces byte-identical output to everything written before the field existed.
The test pins the legacy envelope literally rather than recomputing it, since a
test that recomputes with the code under test follows that code wherever it
drifts.
**No SDK wire boundary.** Both SDKs reconstruct exactly one envelope — the A2A
receipt — and the A2A path records no verdict, so those bytes are unchanged and
every published SDK keeps verifying every A2A receipt. Forensic leaves, which
may now carry a verdict, are not reconstructed by any SDK: inclusion is verified
from a leaf *hash*.
**Tampering fails closed in both directions, with no version flag to keep in
step.** Verification rebuilds the payload from the node's own stored verdict, so
blanking a recorded verdict rebuilds four fields where five were signed, and
forging one onto a node that never had it rebuilds five where four were signed.
Both mismatch. The vocabulary is closed and delimiter-free, which is load-bearing
rather than tidy: the payload is `"|".join(...)`, so an unconstrained verdict
would be a serialisation ambiguity rather than merely an unknown label.
Recorded as `CLM-088`. **It records that the WAF allowed the request. It does
not establish that the WAF is correct**, and it is not itself a proof — it is
what makes one possible.
### Added — the evidence commit is now traced, and the `pqc` extra carries only what it uses
Two owner decisions from the tracing and PQC verification passes.
**`aegis.wal.commit`.** The span fabric had `aegis.waf.check` and
`aegis.forward` wired and nothing at all on the durable commit. For an evidence
product that was the wrong operation to leave dark: it is the one the gateway
exists to perform, and the one whose latency an operator most needs attributed.
The span wraps the whole durability gate, `fsync` included. It is entered on the
event loop while the work runs on a worker thread, which is deliberate — it
measures the caller's wait, which is the latency a durability gate actually
imposes.
Attributes stay content-free, the same rule metric labels follow: no payload, no
tenant, no subject value. A span is as exportable as a metric. `tests/
test_wal_commit_span.py` drives a real governed request through the ASGI app
with a stubbed upstream and asserts the span by **observing** it rather than by
reading the source; one test asserts the content-free property directly. Teeth
checked by deleting the span — five of eight tests fail, and restoring returns
to green.
The unwired W3C facade in `aegis/telemetry/otel.py` stays unwired and is now
marked `[ROADMAP]` for propagation in `docs/security/SECURITY_CONTROLS.md`.
Every gateway trace is still a new root; nothing here changes that, and the
control row says so rather than leaving a reader to find out.
**`oqs-python` removed from the `pqc` extra.** Nothing in this tree imports it.
It was declared alongside the library the code does import, so the extra
installed an unused liboqs binding; with `kyber-py` now declared, keeping it
would leave supply-chain surface in a security-relevant extra for no capability.
The `pqc` extra installs `kyber-py` only.
### Fixed — the `pqc` extra installed the wrong library, so the feature was never on
`aegis/core/mlkem_session.py` imports `kyber_py.kyber.Kyber1024` behind a
try/except and tells the operator to `pip install kyber-py`. The `pqc` extra
declared `oqs-python` instead — a different library that **no module in this
tree imports**. `kyber-py` was declared nowhere: not in `pyproject.toml`, not in
`requirements.txt`, not in the lock.
So `pip install aegis-latent-core[pqc]` installed something unused and left the
post-quantum backend switched off, and `backend_available()` returned `False` on
a tree that had been asked for exactly that capability.
The failure was silent in both directions. The import guard turned a missing
dependency into a graceful degradation rather than an error, and **thirty-two
tests covering those paths skipped instead of failing** — the suite stayed green
while the cryptographic code it reports on was never executed. Installing
`kyber-py` moves the suite from 6 748 passed / 58 skipped to 6 780 passed / 26
skipped.
`kyber-py` is now declared in the `pqc` extra, so the extra enables the feature
it advertises, and in `dev`, so CI executes those thirty-two tests instead of
skipping them. `tests/test_optional_backend_declarations.py` binds the extra to
the import — it reads `pyproject.toml`, needs none of the optional packages
installed, and fails if the two drift apart again. Its teeth were checked by
reverting the extra and observing two tests fail.
With the backend present, a real two-party exchange was driven end to end: an
initiator and a responder independently derive the **same** 32-byte secret.
Recorded as `CLM-087`, scoped deliberately to the **KEM primitive**.
**It is not TLS.** There is no TLS listener, no `SSLContext`, and no cipher-suite
or named-group negotiation in that module — it is an application-layer exchange
between two parties that already have a channel, and the gateway's ingress TLS
is terminated by the customer, outside this repository. The parameter set is
**ML-KEM-1024**, not the TLS hybrid group `X25519MLKEM768`, which the module
docstring cites only as an analogy for the composition. The post-quantum half is
pure Python, so no constant-time, side-channel or FIPS-validation property is
claimed. The register entry blocks each of those phrasings by name.
**`oqs-python` is left in place and flagged rather than removed.** Nothing in
this tree imports it, so it appears to be dead weight in an extra whose name
implies it is the post-quantum backend; dropping a declared dependency changes
what a consumer's install resolves to, which is the owner's call rather than a
side effect of fixing a different defect.
### Fixed — a security-control row that cited the wrong tracing module
A verification pass over the tracing surface found that this repository has
**two** of them, and that the control table pointed at the wrong one.
`aegis/core/observability.py` holds the real OpenTelemetry integration. It is
wired: `setup_otel()` is called from the gateway lifespan, and `record_span()
wraps exactly two operations — `aegis.waf.check` and `aegis.forward`. It is
optional, no-oping when the `otel` extra is absent.
`aegis/telemetry/otel.py` is **not OpenTelemetry**. It imports nothing from the
OTel SDK; it is a pure standard-library facade implementing W3C `traceparent`
and `tracestate` parsing and injection behind an attribute allowlist. It is
complete and tested, and **nothing in the request path uses it** — the only
references outside its own module are its package re-export and its test.
`docs/security/SECURITY_CONTROLS.md` cited that second module as the evidence
for "OpenTelemetry tracing", and described it as having an optional dependency,
which it does not have. Three errors in one row: wrong module, wrong technology,
wrong dependency story. The row now cites `observability.py` and states what is
actually wired; a second row records the W3C facade as present but unwired, so a
reader does not mistake one for the other.
Two limits are now written down rather than left to be discovered:
- **Two spans are wired, and the evidence commit path carries none.** The
`SpanName` enum in the unwired module declares four intended spans —
`aegis.gateway.request`, `aegis.policy.evaluate`, `aegis.proof.verify`,
`aegis.siem.export` — and not one of those names is in use.
- **There is no W3C trace-context propagation on the wired path.** No propagator
is registered, and no `traceparent` is extracted from an incoming request or
injected into an upstream one, so a caller's trace does not continue through
the gateway: every gateway trace is a new root. The unwired module is exactly
the code that would fix this, which is why leaving both in the tree without
saying so was the hazard.
No claim changed, because no claim covered this. `README.md` already lists the
span model under "Not built. No dates.", and `ENTERPRISE_READINESS.md` already
says "Partial; hooks exist, a tested model does not". Both were accurate; the
security-control row was the outlier.
### Added — attestation backends that refuse, and a replay that cannot escape a test
`aegis/core/tee_manager.py` already carried the hard part: a verifier protocol,
an exact `AttestationPolicy`, and a fail-closed evaluator that compares nonce
and report data with `hmac.compare_digest`. What it had no implementations of
was the protocol itself, so a deployment had a seam and nothing to put in it.
`aegis/core/tee_backends.py` adds three. `NitroBackend` and `SevSnpBackend`
refuse in every case, and refuse in two distinguishable ways on purpose: no
device node means the workload is not running on the platform it believes it is,
while a present device node means the platform is right and the vendor verifier
is the missing piece. Neither reads a COSE_Sign1 document, an SNP report, a
certificate chain or a TCB version, because no such verifier is integrated —
device presence is discovery evidence, which is the position
`TEEManager.initialize_enclave` already took.
`RecordedAttestationBackend` replays a captured document so the policy path can
be exercised without hardware. It authenticates nothing: it ignores the evidence
bytes entirely and returns a fixture.
**It needs no guard rail, because the existing evaluator already defeats it.** A
recorded document carries the nonce and issue time it was captured with, both
frozen, while production issues a fresh random nonce per challenge and reads a
real clock. So the replay fails `compare_digest` on the nonce and fails the
`max_age` bound. It can satisfy policy only when the caller supplies the very
nonce it was recorded against *and* pins the clock to the recording — which is
to say, in a test. Two tests assert exactly that, one per rejection. The
`bind_nonce` option deliberately defeats the freshness check, and exists only so
that policy-field rejection (a wrong measurement, a stale TCB status) can be
tested without the nonce check masking which field did the rejecting.
Protocol conformance is checked by mypy rather than assumed: each backend
inherits `AttestationVerifier` explicitly rather than satisfying it
structurally. `TEEManager` accepts the protocol structurally either way, but
naming it as a base makes mypy reject a drifted `verify` signature at build
time — otherwise the drift surfaces at runtime in a deployment that has the
hardware, which is the most expensive place to find it. The check was confirmed
to have teeth by drifting a signature and observing mypy reject it.
**No attestation is performed anywhere in this tree and none is claimed.**
Recorded as `CLM-086`, whose register entry blocks "hardware-attested",
"confidential computing", "runs in a TEE", and the specific trap of reading a
passing test as a verified quote.
### Added — the three verification gates that were missing, and the test-suite bug one of them exposed
A release-tooling audit listed seven gates as absent from CI. Four were already
there and passing, which today's run proves: `lean` and TLA+ are installed from
pinned revisions by the `Formal Verification (Z3, Lean, TLC)` job, `z3` comes
from the distribution package in the same job, and `syft` backs `Generate SBOM`.
They were missing from a *development container*, not from CI. Three were
genuinely absent and are added here.
**`pytest -n auto` — and it did not work.** Enabling xdist made the whole suite
abort before a single test ran: `Different tests were collected between gw3 and
gw1`. The cause is a real defect that only parallel collection exposes.
`tests/auth/test_mtls_v4.py::test_rejects_certificate_outside_validity_window`
is parametrized over **freshly generated X.509 certificates**, so pytest derived
each test id from the PEM bytes — which differ in every process. Every worker
collected a different set of ids and xdist refused to proceed. Naming the two
cases (`not_yet_valid`, `already_expired`) fixes the ids across processes, and
replaces a 2 KB base64 blob in the test name with the case it covers. The suite
now runs in **47.7 s instead of 118.9 s**, which is the difference between a
gate a contributor runs before pushing and one they skip.
**Miri, on a pinned nightly, over the digest buffer.** Miri executes real Rust
and reports undefined behaviour — invalid aliasing, uninitialised reads,
out-of-bounds arithmetic — that compiles and passes ordinary tests. It runs
`tests/block_buffer_panic_safety.rs`, covering the `block-buffer` cursor beneath
every SHA-256 and HMAC call on the evidence path, where a torn cursor is a
*wrong digest* rather than a crash.
Nightly is not a preference. Miri ships only as a nightly component and
`rustup component add miri` against stable fails outright, so the audit's
"install `cargo miri` on the stable toolchain" has no implementation; the date
is pinned instead so a nightly regression cannot turn the gate red by surprise.
The scope is one target rather than `--lib`, for three measured reasons recorded
in `docs/formal/FORMAL_VERIFICATION_LIMITS.md`: Miri cannot execute foreign
functions and the default build links PQClean C; it does not service `mmap`, so
the WAL is unreachable under it; and `--lib` drags tokio, reqwest and vendored
OpenSSL through Miri's own compilation, which had not finished after fifteen
minutes locally. Recorded as `CLM-085`.
**`slsa-verifier`, installed and executed.** Generating an attestation and
verifying one are different capabilities and only the first was present. The
verifier is installed on every run of the SBOM job — not only at release, since
a verifier that first appears at release time is one nobody has tested — and a
version check proves the binary executes.
### Fixed — a jitter gate that measured the runner, not the code
Turning on `-n auto` also surfaced a second latent defect, and this one only
parallel *execution* could expose.
`tests/test_determinism.py::TestSpawnBackgroundDeterminism::test_spawn_background_jitter_sigma`
asserted σ < 100µs on wall-clock dispatch jitter and failed at **σ = 189.66µs on
Python 3.12 while 3.11 and 3.13 passed the same commit** — the signature of a
host property, not a code property.
Four sibling checks in that same file already carry
`skipif(CI == "true")` on the identical quantity, with reasons that name this
exact failure ("shared CI runners have unbounded scheduling variance",
"multi-millisecond scheduling outliers (observed 90ms)"). This one had no such
guard and passed only because a serial suite left the runner idle. It was a
latent flake, and parallelism made it reliable rather than causing it.
Fourteen runs of the dispatch loop — six idle, eight under a parallel suite —
show why σ cannot be the instrument: the median held within ±2% (4.39–4.56µs)
and the interquartile range stayed under 1.05µs, while **σ swung eightfold
(1.23–9.82µs) even on the idle machine**, landing within a ninth of the single
largest sample in every run. One preemption sets it; 189.66µs is one ~1.9ms
preemption in 100 samples. A coefficient-of-variation bound would have flaked
for the same reason, since CV inherits σ's outlier sensitivity.
The property is now asserted twice rather than weakened. Median and IQR — both
outlier-resistant — are bounded at 100µs and run everywhere including CI; the
absolute σ bound keeps the IEC 62443 framing and is guarded exactly as its four
siblings are. The robust pair is deliberately coarser and is documented as such:
it catches a wrapper that becomes uniformly slow or broadly dispersed, not a
small change in tail shape. Its teeth were verified by injecting a 250µs
synchronous block into `_spawn_background`, which it caught at a median of
375.02µs.
### Fixed — `CLM-023` contradicted `CLM-054`
`CLM-023` stated the tree has "no cargo-fuzz workspace or Kani harness".
`CLM-054` documents the Kani harnesses, and `cargo kani` verifies five of them.
The cargo-fuzz half was accurate and stands; the Kani half was false and is
corrected.
### Fixed — four documentation statements that contradicted the code or each other
A conflict audit put ten alleged contradictions to the corpus. Six of them were
already correct and are recorded here so nobody re-opens them: the wiring status
of `GrammarFrontierAutomaton` (wired, on by default) and `CryptoShredder`
(wired, off by default) is stated accurately in `ARCHITECTURE.md`; the two
benchmark runs are already delineated, and no document pairs the 10,000-request
count with the 836 ms latency; `AEGIS_MMR_HASH_SCHEME` is already documented as
defaulting to `auto` in both places; pre-admission rejection nodes are already
specified in `CLM-060`; "the gateway ships from source only" already appears
nowhere except in the register of forbidden phrasings; and the fourteen version
anchors already agree — at `4.3.0`.
Four statements were genuinely wrong, two of which the audit did not name:
- **npm SDK version.** `README.md` contradicted *itself*: its release banner
recorded the 2026-09-04 readback of npm `aegis-latent-sdk` `4.1.2`, while a
later section said npm "still carries `4.0.0`". Corrected there and in
`SUPPORT_MODEL.md` and `PROCUREMENT_CHECKLIST.md`. `BUYER_GUIDE_US.md` also
says `4.0.0`, and is left alone: it describes the historical `v4.0.x`
baseline, where that was the observation.
- **WAL corruption semantics.** `AUDIT_ENDPOINTS.md`, `SECURITY_CONTROLS.md`
and `CONTROL_TO_EVIDENCE_MATRIX.md` still said commits remain permitted after
`wal_corrupt` and that the request path does not check the fault state.
`_require_intact_ledger` has refused all three governed endpoints with `503`
since 2026-09-03.
- **`DOC-01` disagreed with itself.** Its Table 2 had been corrected and says so
explicitly; two later rows — falsification test `DOC01-FALS-007` and risk
`DOC01-RISK-003` — still described the superseded fail-open behaviour.
`DOC01-RISK-003` is now the risk that actually remains: corruption that replay
parses successfully raises no fault, so the ingress guard never fires.
- **`FAQ_TECHNICAL.md` said there is no gossip transport.** There is one now,
and the gateway starts it. Rewritten to say what did *not* change, which is
the part that matters: the mesh reconciles the CRDT accumulator, not the
ledger.
### Added — an ML-DSA backend seam, and a measurement that corrects two of our own conclusions
`pqcrypto-mldsa`, `pqcrypto-traits` and `pqcrypto-internals` carry
`RUSTSEC-2026-0166`, `-0162` and `-0163` because upstream PQClean was archived.
None is a defect; all three are unmaintained-status notices with no fixed
version. `aegis_rust_v2/src/pqc_trait.rs` makes reacting to one cheap:
`PostQuantumSigner` states the ML-DSA-65 contract in FIPS 204 byte encodings —
bytes rather than types, because the two candidate crates model keys with
incompatible type systems and the wire format is both what they agree on and
what Aegis persists. `pqc.rs` now names only the selected backend, so the PyO3
class, its methods and the exported FFI symbols are identical either way.
Both backends are optional dependencies, so the swap genuinely *removes* the C
stack rather than shadowing it: under `--no-default-features --features
pure-rust-pqc` the dependency tree contains `ml-dsa` and none of
`pqcrypto-mldsa`, `pqcrypto-traits`, `pqcrypto-internals` or `paste`.
It does not strand evidence, and that is measured rather than assumed. The
`cross_backend` tests (`cargo test --features pqc-compat-tests`) assert that
`ml-dsa` verifies a PQClean signature, that PQClean verifies an `ml-dsa`
signature, and that `ml-dsa` derives the **same** public key from the persisted
4032-byte expanded secret — so nodes already in a WAL stay verifiable, a
rollback stays open, and the durable signing identity survives without
re-keying.
**The default is unchanged, and that is the measurement's verdict.** On this
repository's hardware, both signing arms hedged: verify 45,961 → 65,805 ns
(1.43x), sign 135,443 → 650,361 ns (**4.80x**). Signing is per-commit work on
the evidence path, so 4.8x is a real cost to pay against an advisory that
reports no defect. The swap is a flag an operator takes deliberately.
Two things we had written down turned out to be wrong, and both are corrected
in place rather than quietly dropped:
- `pqc.rs` said closing the `verify` timing gap "requires a verifier that does
not exit early — an upstream change in `pqcrypto-mldsa` or a different
implementation". A different implementation now measures at the **same**
`p = 0.0000` with a **larger** class delta. The proposed remedy does not work.
- More usefully, it explains why. When two independent implementations of one
specification give the same result, the effect belongs to the experiment. The
`verify` experiment's classes are one repeated signature versus 1024 varying
ones, **all valid** — two sets of *public* inputs — and **ML-DSA verification
consumes no secret**: public key, public message, public signature. A timing
difference across public inputs discloses nothing they did not already
disclose. Signing, which does touch the secret key, meets the non-detection
threshold on both backends (`p = 0.8399` and `p = 0.8828`).
No constant-time claim is made or lifted for either backend; the release gate in
`docs/security/PQC_CONSTANT_TIME.md` stands closed. Recorded as `CLM-084`, with
the roadmap in `docs/security/DEPENDENCY_RISK_REGISTER.md`.
Also fixed: `cargo fmt --check` was failing on four pre-existing diffs in
`aegis_rust_v2/src/crdt_mmr.rs`, unrelated to this change and unrelated to any
behaviour. Reformatted so the gate passes.
### Added — the gateway now starts the gossip mesh, and receipts can be checked across replicas
`GossipDaemon` existed and converged, but nothing started it: enabling
`AEGIS_GOSSIP_ENABLED` configured a mesh that never ran. `aegis/consensus/runtime.py`
closes that. The gateway's lifespan now starts both halves — the daemon that
reaches out to peers on a timer, and an mTLS listener on its own port that lets
peers reach in — and stops them on shutdown, daemon first so a peer's in-flight
round does not log a failure that is only shutdown.
Placed **before** the seccomp filter, deliberately: reading the three PEM files
needs `openat` and binding the listener needs a socket the filter is about to
forbid, so starting it afterwards fails looking like a TLS fault. Two settings
are new, `AEGIS_GOSSIP_BIND_HOST` and `AEGIS_GOSSIP_BIND_PORT`, and the Helm
chart now renders them so the listener, the `containerPort` and the port the
peer list is written against cannot drift apart.
Misconfiguration is loud rather than silent. Each of the three PEM settings is
checked by name before TLS sees it, a missing native `CausalMmr` is a startup
failure rather than a Python stand-in that would compute roots no other replica
agrees with, and the listener is waited on via `server.started` rather than a
connect probe — the kernel accepts into the backlog as soon as the socket is
bound, so a probe cannot tell "serving" from "bound but never scheduled". Under
strict enforcement a failure to start is fatal; otherwise it is an error log and
the gateway continues without the mesh. It is never skipped quietly: a replica
asked to join a mesh that did not join looks healthy while diverging from every
peer.
`aegis.core.a2a.verify_cluster_receipt` is the verifier-side half. A cluster has
no single root — each replica keeps its own single-writer WAL and its own
accumulator — so it searches a supplied set of per-replica **ledger** roots and
returns the name of the replica whose root verifies the receipt, which is where
an auditor goes for the surrounding evidence. Two boundaries are stated in the
code and in `CLM-083` because both are easy to get wrong: a match is *one
replica's word, not the cluster's* — replicas do not attest to each other's
ledgers, so this is not a quorum and not corroboration — and the roots are
**ledger** roots, not gossip roots, since the mesh reconciles a different
structure whose root cannot verify a receipt at all.
Recorded as `CLM-083`; `CLM-080` updated to say the mesh is started by the
gateway rather than merely importable.
### Changed — concurrent WAL commits now share one `fsync` (group commit)
A commit did not return until its record was `fsync`ed, and that `fsync` was
issued **inside the ledger lock**, one per record. So concurrent commits did not
merely each pay for a device round trip — they paid for them *in series*. Under
32 concurrent writers, measured on this repository's own filesystem, that was
p99 55.5 ms and 1,065 commits/s.
`aegis/core/group_commit.py` coalesces them. An `fsync` makes everything already
written to the descriptor durable, not just the caller's own record, so one call
can retire a whole burst. Records are written and ordered under the ledger lock;
the lock is then released, and one waiter issues a single `fsync` covering every
record written while it was in flight. Same workload after: **p99 24.8 ms,
1,536 commits/s, and 66 `fsync` calls in place of 400** — reproducible with
`tools/benchmarks/run_group_commit.py`.
No artificial delay is used, and that is deliberate. Batching does not need a
timer: while the syncer is inside its `fsync`, later writers pile up behind it,
and the next syncer retires all of them. A fixed linger would tax the
uncontended path — the common case — to help a case that already batches on its
own, so `AEGIS_COMMIT_BATCH_TIMEOUT_MS` is only ever paid when another committer
is already waiting. `AEGIS_COMMIT_BATCH_MAX_SIZE` bounds that wait; it cannot
bound what an `fsync` covers, because `fsync` is not scopable to part of a file.
Two properties changed and both are stated rather than glossed:
- **A node enters the in-memory chain before its batch is synced.** It has to:
the next committer reads the tip to link against it. Nothing observes the node
in the meantime, because the commit call has not returned, and a crash in that
window loses the in-memory chain too — replay rebuilds a shorter, internally
consistent chain from the WAL. The previous invariant, "each node is fsynced
before the in-memory chain is updated", no longer holds and has been corrected
wherever it was written down.
- **A batch is durable together or not at all.** A failed `fsync` raises
`WalDurabilityError` to *every* commit waiting on it and latches
`wal_persist_failed`, which `_require_intact_ledger` already reads to answer
503 and stop extending the chain. Individual nodes are deliberately not
unwound: later nodes have already linked against them, so there is no single
node whose removal leaves a consistent chain. The engine stays latched after
the disk recovers, because a descriptor that lost a write may have lost it
silently.
WAL rotation and `close()` both `fsync` before replacing or releasing the
descriptor, which makes pending records durable as a side effect; both now tell
the engine so, and — where they previously swallowed the error — report a failed
`fsync` to it instead, so waiters fail closed rather than being released against
a write that never landed.
Recorded as `CLM-082`. `CLM-059` was widened: `wal_persist_failed` is now also
reachable from a durability failure at commit time, not only from startup
replay.
### Fixed — concurrent appends forked the enterprise storage chain (P0)
The analytics path read the chain tip, then built and **signed** the node — an
await that may reach an HSM — and only then wrote. Two background tasks
therefore read the same tip and both appended against it. Measured on SQLite
before the fix: ten concurrent appends produced **ten nodes all naming one
predecessor**. A hash chain with two successors at a point has no single
history, and an integrity sweep over either branch passes, so this is not a
corruption to be detected later; it is a state the store must not reach.
Two layers, and neither is redundant:
- `write_node_atomic(..., expected_prev_hash)` appends **only if the tip is
still what the caller read**, and raises `ConcurrentChainMutationError`
otherwise, writing nothing. `prev_hash` also moves out of the `node_data`
JSON into a column with a uniqueness constraint, which makes a fork
*unrepresentable* rather than merely detectable — in a linear chain no two
nodes share a predecessor, so the constraint is exactly the invariant. It
covers what a compare-and-append alone cannot: on an empty table there is no
row to lock, so two genesis writers would otherwise both commit.
- A process-wide lock spans the whole read-tip → sign → append sequence, which
is what keeps this process's MMR consistent with what it writes. It does
nothing across workers or hosts; the storage guard is the only thing that
does.
**What this does not fix.** An append refused because *another process* moved
the tip is reported and lost, not retried. A retry would have to re-add an MMR
leaf, and this process's MMR is a separate accumulator from the shared chain,
so a second leaf for one request would leave the two disagreeing. Reconciling
them needs the MMR and the store to commit together, which they do not. And a
store that forked before the constraint existed now refuses to initialise,
naming the duplicate — repairing it means choosing which branch is the record,
which no rule here can decide.
Only the SQLite path is verified. The PostgreSQL (`FOR UPDATE`
compare-and-append plus a unique partial index) and DynamoDB
(`TransactWriteItems` against a tip item) implementations are written against
their documented primitives but were not executed: `asyncpg`, a PostgreSQL
server and a DynamoDB endpoint are all absent here. `CLM-081` records that.
### Fixed — a gossip round that failed said nothing about why
`GossipDaemon.run_round` logged `failed (%s)` with the exception alone. Several
of the exceptions that reach there carry no message, so an operator saw
`round with peer replica-1 failed ()` and learned nothing — and a reproducible
fault looked like an unexplained one. The log now carries the exception type,
which is what actually distinguishes a refused certificate from a timeout from
a rejected state.
### Added — cross-replica reconciliation, off by default
- **`CausalMmr` has a transport.** The join-semilattice was implemented and
tested but nothing ran it: there was no way to move a replica's leaf set
between processes, so the convergence it promised was reachable only from a
single test that held both replicas in one interpreter.
`aegis/consensus/` supplies the missing half — a peer set, a schedule, and
anti-entropy over mutual TLS. Each round compares roots first, so a converged
cluster exchanges one 32-byte digest per peer per interval; only a mismatch
triggers a state exchange, which moves leaves in both directions at once.
There is no log to be at the end of and no leader to be behind: a replica
that misses any number of rounds catches up completely on its next
successful one.
The wire format is new Rust (`encode_state`/`decode_state`). Its decoder is
the part that faces a peer who may not be friendly, so it is canonical
(clock entries must ascend, or one state would have many encodings),
bounded (a leaf count is checked against a ceiling *and* against the bytes
actually present, because a trusted length prefix is an out-of-memory
primitive), and total — every truncation returns an error rather than
panicking, which matters behind a PyO3 boundary where an unwind aborts the
interpreter rather than raising. The sender's own replica id is deliberately
absent from the wire: it would break canonicality, and it would put a
forgeable claim of identity beside the authenticated one the certificate
already establishes.
**Verification is not optional and there is no flag to disable it.** A peer
supplies leaves that enter every replica's accumulator, so authenticating
peers is the entire security boundary. Convergence is asserted over three
replicas on real loopback TLS sockets with `CERT_REQUIRED` in both
directions, through a simulated partition and heal; a client presenting no
certificate, or one from another CA, is refused at the handshake.
**What it does not do.** It reconciles the CRDT accumulator, *not* the
ledger: each replica's WAL stays its own chain and nothing about a receipt,
root or inclusion proof changes (`CLM-012` unchanged). It is not consensus,
not Byzantine fault tolerance and not a total order — `join` reconciles
replicas that disagree about ordering, not replicas that lie, and a replica
contributing fabricated leaves has them merged like any other. There is no
membership protocol; the peer list is static configuration.
Rendered onto the existing StatefulSet rather than a DaemonSet. A DaemonSet
has no `volumeClaimTemplates` and so cannot give each replica its own WAL
claim — the single-writer property the chart is built around — while the
StatefulSet plus its headless Service already provides what a mesh needs and
a DaemonSet does not: a stable per-replica DNS name a certificate can be
issued for.
### Added — cryptographic erasure on the ledger commit path, off by default
- **`CryptographicAuditLedger` can seal what it commits.** `CryptoShredder`
existed and was tested but nothing called it, so the property it was written
for — erase a subject without moving the tree — was unavailable to any
deployment. With `AEGIS_ENABLE_CRYPTOGRAPHIC_SHREDDING=true` the ledger now
encrypts each leaf under a per-subject AES-256-GCM key and commits
`SHA-256(0x00 || nonce || ciphertext)` in its place; `crypto_shred(subject_id)`
destroys that key, and `open_sealed_leaf(node)` reads a node back while the key
still exists.
The mechanism is that the commitment is computable from the ciphertext alone.
A verifier who cannot read a leaf can still recompute what the tree committed
to, so **destroying the key invalidates no proof**: `tests/test_crypto_shredder_integration.py`
asserts on a live ledger that after erasure the inclusion proof still verifies,
the plaintext no longer opens, the root is unchanged, `verify_integrity()`
still passes, and another subject's records are still readable.
**Off by default.** Sealing is a per-commit decision rather than a per-chain
one — `_seal_leaf` branches on whether the shredder is configured at the
moment each leaf is built — so the flag *can* be enabled for a chain that
already holds records: commits from that point forward are sealed, earlier
leaves stay exactly as written, and the resulting mixed chain still passes
`verify_integrity()`. What has no migration path is the reverse; nothing
seals leaves already committed as plain digests.
An unchanged deployment commits payload digests exactly as before, and
`crypto_shred()` on such a ledger raises rather than returning a false
success, so a retention job cannot mistake a no-op for an erasure.
Three costs an operator owns. The WAL carries ciphertext, so its growth tracks
payload size. The key vault (`<wal_path>.shredder.db` by default) becomes the
only mutable component in an append-only design: lose it and every plaintext
is gone at once, restore it from a pre-erasure backup and every erasure through
it is undone. And a shredded node **keeps its request and response digests**,
so erasure removes the ability to read a payload, not the ability to confirm a
guess about it. `CLM-068` states what may be claimed; no regulatory conclusion
follows from any of it.
### Changed — documentation that described three components as unwired
`4.3.0` wires the grammar frontier (on by default), the v2 hash scheme (on for
new chains) and the shredder (off by default). The claims matrix rows for the
first two were updated when they landed, but the descriptive corpus still told
readers that the streaming path uses `StreamingDeidentifier` alone, that v2 "is
not the default", and that the shredder "is not wired in" — statements that were
true when written and are now false. Corrected across `ARCHITECTURE.md`,
`DECISIONS.md`, `PII_REDACTION_BOUNDARIES.md`, `DATA_RETENTION.md`,
`MMR_PROOF_V1.md`, `FAQ_TECHNICAL.md`, `ROADMAP.md`, `PLATFORM_OPERATOR_GUIDE.md`,
`BACKUP_RESTORE.md`, `DOC-02`, `DOC-03`, `DOC-05`, `CLAIM_EVIDENCE_GRAPH.md`,
`UNSUPPORTED_CLAIMS.md` and `CLAIMS_MATRIX.md`.
Wiring narrows nothing these documents bound. `UC-037` still blocks every
erasure conclusion and gains one: "Aegis erases on request" is now also
forbidden, because the default build does not. The blocked-wording row for
`CLM-064`–`CLM-068` gains "the grammar frontier replaced the de-identifier" (it
runs *after* it, and replacing it would have dropped eighteen detectors) and
requires "for new chains" wherever v2 is called enabled.
### Fixed — three defects that each failed quietly
- **Importing the proxy module took the WAL's exclusive lock (P0).**
`aegis/proxy/app.py` ended with `app = create_app()`, and constructing the app
opens the WAL and claims its single-writer lock. So *importing the module for
any reason at all* claimed it, and the documented factory `create_proxy_app()`
then failed with `WalWriterConflictError` against a writer that was the
importing process itself. Anything that imported the module without wanting a
running gateway paid the same price: a test collector, a CLI reading a
version, a worker importing one helper.
The attribute is now lazy (PEP 562 module `__getattr__`), so the app is built
on first *access* rather than on import. `uvicorn.run("aegis.proxy.app:app")`
and `from aegis.proxy.app import app` both resolve through it, so the ASGI
entry point is unchanged, and repeated access still returns one app.
- **Every commit signed under a throwaway post-quantum key (P1).**
When the Rust extension was present and no HSM was configured, `_sign` called
`generate_pqc_keypair()` **on every commit** and discarded the private half
immediately. Each node was signed by a different one-shot identity that nobody
holds — which attributes nothing, links nothing, and cannot be checked against
any published key, while being recorded under the `pqc-ml-dsa` scheme label as
though it could. It also put an ML-DSA keygen on the commit path.
The tier now signs under a persistent identity, configured by
`AEGIS_PQC_IDENTITY_PATH` and created on first use (written `0600`; it holds
the raw ML-DSA-65 private key and needs the custody any signing secret does).
Two ways that identity could still stop attributing anything, both raised in
review and both fixed here. **An identity provisioned elsewhere is checked
rather than trusted**: one whose mode lets group or other read it is refused,
and signing falls through to HMAC rather than claiming ML-DSA under a key the
whole host can read. It is refused rather than silently `chmod`-ed, because
tightening the mode does not un-expose a key that has already been readable
and would hide the provisioning mistake. And **publication is now
create-if-absent rather than replace**: two processes pointed at one absent
path both find it missing and both generate a keypair — ML-DSA keygen holds
that window open — so the one that published second used to overwrite the
first's file while continuing to sign under its own discarded key. Measured
before the fix, six concurrent processes produced up to four distinct signing
keys, with every one of them signing under a key that was not the one left on
disk; a shared temporary filename also let one process consume another's file
mid-write and drop silently to HMAC. The bytes now go to a per-process
temporary and are linked into place, so the loser adopts the winner's identity
instead of orphaning its own nodes.
**With no identity configured the tier is skipped and signing falls through to
HMAC-SHA256**, rather than minting a key per signature. That changes the
recorded `signature_scheme` for deployments that had the Rust extension and no
HSM — from `pqc-ml-dsa` to `hmac-sha256` — which is a weaker claim and a true
one, where the old label was a stronger claim than the evidence supported.
- **New chains defaulted to the MMR construction v2 exists to replace (P1).**
`mmr_hash_scheme` defaulted to `v1-asciihex`, which admits the leaf/interior
type confusion documented in `aegis/core/mmr.py`. The default is now `auto`:
a **new** chain starts on `v2-binary-domain-separated`, and an **existing**
chain reopens under whatever scheme its WAL recorded. Pinning a scheme
explicitly stays fail-closed — a WAL written under the other one is still
refused with `mmr_scheme_mismatch`.
`auto` rather than a plain v2 default is the point: the scheme decides every
root a chain has recorded, so defaulting to v2 outright would refuse to open
every chain already in the field, turning an upgrade into an outage.
### Fixed — v2 defects the new default exposed
Moving new chains to v2 surfaced four places that computed or compared digests
under the v1 construction regardless of the proof in hand. Each was latent: v1
was the only scheme any ledger used, so none could fire.
- **`aegis/core/a2a.py`** hardcoded the v1 leaf digest, so a receipt issued by a
v2 ledger could not be verified at all. The digest now follows the version the
receipt's own proof declares, which is self-describing — a verifier reads it
off the receipt rather than having to know how the issuer was configured.
- **Both SDK verifiers** (`sdk/python`, `sdk/typescript`) had the same hardcoded
v1 leaf digest and are fixed the same way.
- **`verify_portable_inclusion_hash`** required the trusted root in hex, but a
v2 proof *transports* its root as unpadded base64url — so a caller passing the
root straight from the proof document it received, which is the obvious thing
to do, was rejected. Both encodings are now accepted and normalised; this is
not a relaxation, since both decode to the same 32 bytes and anything that is
neither still fails.
- **The `X-Aegis-MMR-Format` response header** was the literal string
`aegis-mmr-inclusion-v1`. It now reports the version of the proof actually
served: the header tells a client which construction to verify under, so
advertising v1 while serving a v2 proof sent it to the wrong leaf digest.
**Wire compatibility.** A receipt from a v2 chain cannot be verified by an SDK
build that predates these fixes, including the published `4.1.2` packages. Both
in-repo SDKs are fixed, but gateway and SDKs must move together; a v2 gateway in
front of older clients breaks receipt verification.
### Fixed
- **A ledger configured for MMR v2 would have recorded a leaf digest that
disagreed with its own accumulator.** `crypto_audit.py` computed the digest it
stores on each committed node as `sha256(leaf)` in three places — the v1
construction, hardcoded — while the accumulator appended
`sha256(0x00 || leaf)` under v2. Replay rebuilt a different root from the
recorded digests and the integrity check reported an intact chain corrupt.
Nothing shipped could reach it, because until this release no ledger could
select v2; it would have fired the moment one did. The digest now comes from
`MerkleMountainRange.leaf_digest`, which applies the accumulator's own scheme.
### Added
- **The audit ledger can select the domain-separated MMR construction.**
`CryptographicAuditLedger(mmr_hash_scheme=…)` and `AEGIS_MMR_HASH_SCHEME`
accept `v2-binary-domain-separated`, which applies the RFC 6962 §2.1 domain
tags — `0x00` on leaves, `0x01` on interior nodes, `0x02` on the bagged root —
and hashes raw digests rather than their hex text. **The default stays v1**,
so no existing deployment changes.
v2 has existed in `aegis/core/mmr.py` and both SDK verifiers since `4.3.0`,
wired to nothing, and the module recorded two blockers. Both are now cleared.
`aegis_rust_v2/src/mmr.rs` implemented v1 only, so an accelerated deployment
running Python v2 would have disagreed with the Rust root on every leaf; it
now implements both under the same scheme names, and
`tests/test_mmr_v2_migration.py` asserts the two accumulators agree across a
37-leaf rollover under each. The Rust constructor still defaults to v1 and
refuses an unrecognised scheme name rather than falling back.
**There is no in-place migration, and there cannot be one.** The scheme
decides every root a chain has recorded, and a root cannot be recomputed under
a different construction without rewriting the history it commits to. So a
scheme belongs to a chain: the ledger reads the proof version its WAL recorded
and refuses to open it under a different one, reporting fault state
`mmr_scheme_mismatch` before replay rather than replaying to a different root
and reporting intact evidence as corrupt. Selecting v2 means starting a new
chain; existing v1 chains stay verifiable under v1.
What this establishes is leaf/interior-node type distinctness and nothing
further. The v1 weakness is that a leaf whose payload is the concatenated hex
of two child digests hashes identically to the node over those children — the
Rust and Python tests both demonstrate the collision under v1 and its absence
under v2. It is not a break of SHA-256, and it does not let anyone forge a
proof against a root they do not control.
### Changed
- **The grammar-frontier automaton now runs on the governed streaming path.**
It was implemented and tested at `4.3.0` but wired to nothing, so it changed
no runtime behaviour. It is now composed with the Safe Harbor de-identifier
in `aegis/core/stream_redactor.py`, selected by `AEGIS_STREAMING_ENGINE`,
which defaults to `grammar_frontier`.
**Composed, not substituted.** The automaton declares four rules; the Safe
Harbor set declares twenty. Replacing one with the other would have taken
`EMAIL`, `ADDRESS`, `MRN`, `URL`, `TRACK_DATA`, `CVV` and the
Luhn-and-brand-validated `PAN` — eighteen detectors in all — off the evidence
path in exchange for two new ones. Both stages run instead, de-identifier
first, so the path gains `INSTR_OVERRIDE` and `SYS_LEAK` matching without
losing anything.
**Response redaction off means off.** With both PHI and PCI detector families
disabled the de-identifier is a pass-through that withholds nothing, and the
frontier stage does not run either. Adding it there would have turned "no
redaction" into "some redaction plus a 28-character holdback" — changing both
the bytes a deployment emits and when it emits them — for an operator who
asked for neither.
**Two effects to know about where redaction is on.** Output bytes change for
streams containing instruction-override or system-prompt-disclosure phrasing:
that text is now replaced. And the per-stream holdback grows by the
28-character frontier, so `R_max` rises by 112 bytes; the composite reports
the summed window, so the ceiling added in `CLM-078` still covers everything
the stream retains. Setting `AEGIS_STREAMING_ENGINE=deidentifier` restores the
previous behaviour exactly.
Measured overhead is +10 to +12 µs per chunk at p50 and +3.8 to +22.7 µs at
p99 across five runs of `benchmarks/bench_streaming_engine.py`, against the
5 ms p99 budget the change was held to. The p99 figure sits at the harness's
noise floor — earlier runs at the same sample size measured the new stage as
*faster*, which it cannot be — so it is recorded as "too small for this
harness to separate from variance" rather than as a number. Artifacts and
that boundary are in `evidence/streaming-engine/4.3.0/`.
### Added — `docs/UPGRADING.md`, the missing `4.1.2` → `5.0.0` migration guide
`5.0.0` is a major version because it breaks the public JSON API, and until now
nothing told a consumer on `4.1.2` how to move. The guide covers the seven
changes that can break a working deployment or integration, ordered by how
quietly they break it.
Two are the ones that bite silently. The `legal_admissibility` →
`signature_assurance` rename (`CLM-090`) removes the old field outright with no
alias, and the guide states plainly that a retained `"High"` from a `4.x`
deployment is **not** evidence the chain was signed well — the old property
returned `"High"` whenever a key was configured at read time, before it ever
examined chain history — so `== "High"` must not be migrated to "anything but
`Compromised`", because `SYMMETRIC_AUTHENTICATED` would have reported `"High"`
before and is materially weaker. The `forwarded_allow_ips` change (`CLM-092`)
breaks any deployment whose proxy reaches the gateway from a non-loopback
address, and the symptom — client IPs collapsing to the proxy's address, taking
IP allowlisting, rate limiting and audit attribution with them — looks like a
configuration problem rather than an upgrade consequence.
The rest: the MMR v2 default and its SDK wire boundary, the stricter WAF
normalization changing block decisions on previously-passing traffic, the `pqc`
extra now installing a library the code actually imports, the two additive node
fields, and the corrected shredding-on-an-existing-chain behaviour. Rollback is
covered too, including the asymmetry that a chain started on v2 cannot be read
by a `4.1.2` gateway.
### Security — rustls bumped to 0.23.45, clearing `RUSTSEC-2026-0285`
`RUSTSEC-2026-0285` — "TLS 1.3 handshake messages incorrectly accepted across
encryption level boundaries", severity 5.3 (medium), disclosed 2026-09-14 —
affects rustls 0.23.41, which this tree pinned transitively through
`hyper-rustls` 0.27.9 and `tokio-rustls` 0.26.4. The advisory's own remedy is
`Upgrade to >=0.23.45`.
`cargo update -p rustls --precise 0.23.45` moves rustls to 0.23.45 and
`rustls-webpki` to 0.103.15: an eight-line lockfile diff touching those two
crates and nothing else. `cargo build --release`, `cargo test --locked` (70
passed) and `cargo clippy --locked --all-targets --all-features -- -D warnings`
are all clean on the result.
The advisory had been red on CI across four PRs and was stood down on each time
as unrelated to the diff, which it was. What it was not is unreal: a Sonatype
lookup for `pkg:cargo/rustls@0.23.41` returned `NO_DATA_FOR_VERSION` with a
security sub-score of 100, and taking that as the answer would have been the
wrong call. The fixed version came from the failing CI job's own output, which
prints the advisory's `Solution:` line verbatim.
### Fixed — ten documents asserted that cryptographic shredding could not be enabled on an existing chain
`CLM-068` was corrected on 2026-09-15, after measuring the real behaviour, to
record that enabling `AEGIS_ENABLE_CRYPTOGRAPHIC_SHREDDING` on a chain that
already holds records **is** supported: `_seal_leaf` branches per commit, not
per chain, so an existing WAL re-opened with the flag on seals everything from
that point forward, leaves earlier leaves exactly as written, and the resulting
mixed chain still passes `verify_integrity()`. Ten locations across nine files
still asserted the opposite — that sealing "changes what the MMR commits to" so
adoption "means starting a new chain": `docs/FAQ_TECHNICAL.md`,
`docs/institutional/CLAIM_EVIDENCE_GRAPH.md` (`CEG-029`, including a
falsification trigger that would have fired on correct behaviour),
`docs/institutional/DOC-02_CRYPTOGRAPHIC_FORENSIC_BLUEPRINT.md` (two places),
`docs/institutional/DOC-05_REGULATORY_DOSSIER.md`,
`docs/institutional/UNSUPPORTED_CLAIMS.md` (`UC-037`),
`docs/privacy/DATA_RETENTION.md`, `docs/architecture/DECISIONS.md`,
`docs/architecture/ARCHITECTURE.md`, `docs/PLATFORM_OPERATOR_GUIDE.md`, and this
changelog's own `[5.0.0]` entry. All ten now match `CLM-068`, which
`AGENTS.md` makes the controlling register.
Several of them reached the false claim by analogy to `AD-12`'s rule that the
**MMR hash scheme** belongs to a chain. That rule is real and is untouched; the
analogy was what was wrong. The genuine asymmetry — stated in each corrected
passage — is the reverse direction: nothing retroactively seals leaves already
committed as plain digests, so enabling the flag does not make existing history
shreddable.
### Added — symmetric-signing warning at ledger construction
An HMAC-only ledger now says so when it is built: *"Symmetric signing provides
no non-repudiation; any key holder can forge."* `CLM-090`'s five-tier lattice
already reported `SYMMETRIC_AUTHENTICATED` for such a chain, but only to a
caller who went looking for it. The check reads configuration only — an HMAC key
present, no available HSM backend, no configured PQC identity path — rather than
calling `_configured_signing_ceiling()`, because that would resolve the PQC
identity eagerly and defeat the deliberate laziness of `_pqc_identity`.
### Added — letter-spacing and leetspeak de-obfuscation in the WAF
`CLM-091` wired homoglyph mapping into `AegisWAF._normalize_text`, which closes
the Cyrillic/Greek class of bypass. It does not touch two other classes where
every character is already the ASCII one it appears to be: single-character
spacing (`i g n o r e`, `i.g.n.o.r.e`) and digit-for-letter substitution
(`1gn0r3`). `_scan_content` now tests each critical pattern against a **variant
set** — the canonical normalization plus a spacing-collapsed form, a
leet-folded form, and both together — instead of a single string.
Variants are additive and the canonical form is always first in the set, so a
variant can add a detection and can never mask one. The collapse requires at
least four characters separated by a **consistent** separator, which keeps
ordinary prose (`a b`, `I am a user`) intact and stops a space from bridging two
dot-separated runs into a single token. Folding stays out of `_normalize_text`
itself so that patterns which legitimately contain digits still see unfolded
text.
### Added — four boundaries the unsupported-claims register did not carry
`docs/institutional/UNSUPPORTED_CLAIMS.md` gains `UC-039` (a ZK inclusion proof
shows a recorded verdict, not WAF correctness or semantic safety), `UC-040`
(TEE attestation is unimplemented here, and an enclave's property is memory
against the host — not protection from an upstream model provider or from a
key-holding operator), `UC-041` (HMAC authenticates the key, not a party, so no
non-repudiation follows), and `UC-042` (the WAF is finite pattern detection that
reduces exposure to known encodings; it is not an injection boundary, and each
normalization added above can be defeated by an attacker who reads the table).
Each of the four was already bounded correctly in `docs/CLAIMS_MATRIX.md` and in
the relevant institutional volume. What was missing was the row in the register
that carries *blocked phrasing*, which is what keeps the limit from being
re-stated loosely elsewhere; each new row therefore lists the wording it blocks.
### Added — `STATE_MANIFEST.md`
The Phase 0 baseline the audit directive asks for, and the one deliverable of it
never produced across #177, #178 and #179: measured repository state, version
anchors, test counts, verifier results, static-analysis results, supply-chain
tool availability, and environment versions — each with the command that
produced it, and an explicit blocking reason wherever a command could not run.
### Added — `scripts/verify_import_reachability.py`, a CI-gated import-reachability audit
Walks the real static import graph, by AST, from this repository's actual
entrypoints (`aegis`, `aegis.proxy.app`, `aegis_server.main`, `aegis.engines`,
`aegis.crypto`, `aegis.forensics`) and checks that every `.py` module under
`aegis/`, `aegis_server/` and `integrations/` is either reached or explicitly
declared — in `pyproject.toml`'s `[tool.coverage.run]` omit list, or the new
`scripts/import_reachability_allowlist.txt`. Wired as a step in the
`Market Hardening Gates` CI job.
The first real run found the coverage-omit "roadmap" list itself had drifted:
six modules it still called roadmap (`adversarial_filter`, `blockchain_anchor`,
`entropy_analysis`, `sandbox_l1`, `taint_analysis`, `xdp_dynamic_segmentation`)
are now reachable from real entrypoints, and a seventh entry (`sandbox_l2.py`)
no longer exists. All seven are corrected in `pyproject.toml`. Separately, 78
modules were found reachable from nothing this audit's entrypoint set covers;
they are recorded, not individually adjudicated, in the new allowlist file —
see its header for what that distinction means and does not mean.
### Added — `docs/PLATFORM_COMPATIBILITY.md`
A SUPPORTED/PARTIAL/UNTESTED/UNSUPPORTED matrix across OS, architecture,
Python version, cloud target, storage backend and deployment mode, each cell
backed by a named CI job, test, or an explicit absence of one. Notable
findings from building it: the arm64 container image is cross-built via QEMU
but never executed in CI; the `storage-s3` pyproject extra declares `boto3`
with no corresponding chain-storage provider module; and multi-pod
deployments give each replica an independent single-writer ledger rather than
one globally ordered one (`UC-005`).
### Added — `docs/RELEASE_EPISTEMIC_STATEMENT.md`
An index — not a new ledger — of what this release's evidence establishes,
organized as verified / not-verified-and-why / not-verifiable-in-this-
environment / requires-external-validation, each row linking to the document
that actually carries the evidence (`CLAIMS_MATRIX.md`,
`institutional/UNSUPPORTED_CLAIMS.md`, `PLATFORM_COMPATIBILITY.md`,
`RELEASE_STATUS.md`, `enterprise/ENTERPRISE_READINESS.md`). Section 3 is the
one genuinely new artifact: a single list of tools, services, and hardware
confirmed absent from this session's environment (`moto`, a live PostgreSQL
server, a live DynamoDB endpoint, `cosign`, `gh attestation verify`, `helm`,
a live Kubernetes cluster, macOS/arm64 hardware, a FIPS lab, an independent
circuit auditor), so their absence is not mistaken for a negative finding.
### Fixed — two stale claims in `docs/enterprise/ENTERPRISE_READINESS.md`
Both predated this session's `4.3.0` → `5.0.0` version convergence and the
`4.1.1` → `4.1.2` publication that preceded it. "SDKs are published at
`4.0.0` while source is `4.1.2`" was wrong on both numbers: every registry
surface is published through `4.1.2`, and source has moved to the unpublished
`5.0.0`. Corrected to name the real current gap — a registry install today
gets `4.1.2`'s pre-rename `legal_admissibility` field, not `5.0.0`'s breaking
`signature_assurance` rename (`CLM-090`).
## [4.3.0] — unreleased source target
**Nothing is published for `4.3.0`.** There is no tag, GitHub Release, PyPI or
npm artifact, and no OCI image. This section records what the source tree
contains at fourteen synchronized `4.3.0` anchors; it is not a release
announcement, and the date of any future release is not set here. See
`docs/RELEASE_STATUS.md` §1.0.
**`4.2.0` does not exist.** The number was skipped deliberately when the source
line moved from `4.1.2` to `4.3.0`. No `4.2.0` artifact was ever built or
published, so its absence from any registry is expected rather than a
withdrawal.
### Security
- **`vitest` upgraded to 4.1.11 in `sdk/typescript`** (`GHSA-82fw-gwwq-j7x9` /
`CVE-2026-84373`). `@vitest/mocker` registered a redirect mock's target path
without checking it against the dev server's file-serving allowlist, so a
client that could reach Vite's unauthenticated HMR socket could read
arbitrary local files. Affected 2.1.0 up to 4.1.11; the SDK pinned 3.2.7. The
dashboard was already on 4.1.11 but its lock file carried a stale copy of the
SDK's metadata, so both locks were refreshed. `npm ci` reproduces both trees.
This was the only advisory affecting a pinned version anywhere in the
repository. `tests/security/test_dependency_containment.py` fails the build
if either project's declared version or resolved lock entries fall back
behind the fix.
- **Quadratic ReDoS in the PHI de-identifier's `EMAIL` pattern, found and
fixed.** The unbounded local-part class matched separator-dense runs such as
`1-1-1-…` in full at every start position, then backtracked the length of the
run before advancing one character. Measured at 2.0 s for 16 KB and 12.29 s
for 40 KB — a clean 4× per doubling. Model output is attacker-influenced text
on the evidence path, so this was one request per stalled worker. Bounding
each run at the limits RFC 5321 already imposes (64 octets local part, 255
domain, 63 final label) makes the scan linear: the same 40 KB input now takes
73.8 ms, and 80 KB takes 152 ms. No deliverable address is rejected by the
bound, and every existing detection test passes unchanged.
- **Type confusion in portable inclusion-proof verification, found by fuzzing
and fixed.** `MMRInclusionProofV1.from_dict` checked that the field *set*
matched the schema but not that the integer fields held integers, so a proof
carrying `"leaf_count": ""` reached the range comparison in
`verify_portable_inclusion_hash` and raised `TypeError` instead of returning
`False`. A verifier that throws on hostile input is a denial of service
against the auditor running it, and an exception caught too broadly one frame
up becomes an accidental "valid". Fixed at both layers: `from_dict` refuses a
non-integer `leaf_index`, `leaf_count` or `peak_index` (excluding `bool`,
which is an `int` subclass), and the verifier type-checks defensively so a
proof built directly or through `dataclasses.replace` also fails closed.
- **Rust digest stack migrated to `sha2` 0.11 / `hmac` 0.13**, removing
`block-buffer 0.10.4` from the tree. Hash output is unchanged byte for byte,
asserted against published external vectors rather than against the
implementation's own prior output: FIPS 180-4 for SHA-256 (including the
56-byte case that straddles a block boundary) and RFC 4231 for HMAC-SHA-256,
comparing full digests rather than prefixes. The tests this replaced compared
a six-character prefix and compared `hmac_sign` with itself, which holds for
any deterministic function including a wrong one. Verified with 49 Rust
tests, 106 Python MMR parity tests under `AEGIS_REQUIRE_RUST=1`, and a clean
Miri run over the digest tests including a caught-panic case.
**This remediates a real advisory.** Row `0.31` of the scanner report flags
`block-buffer 0.10.4` at CVSS 6.3 with an **empty CVE ID field** — which is
why an OSV query returns nothing, and why this entry previously recorded that
no primary source existed. A caught panic could leave an `EagerBuffer` or
`ReadBuffer` cursor violating its invariant, after which `get_pos()` reaches
`unreachable_unchecked`; the crate sat under every SHA-256 and HMAC call on
the evidence path via `digest 0.10`. Upstream fixed it with a `ResetGuard`
whose `Drop` restores the invariant during unwind.
- **Both published `block-buffer` proofs ported and run under Miri.**
`aegis_rust_v2/tests/block_buffer_panic_safety.rs` reproduces the advisory's
own two tests — a panic inside `compress`, a panic inside `gen_block` — and
adds a third for a panic inside `read_fn` that neither proof covers. All
three pass under `cargo +nightly miri test` against the `block-buffer 0.12.1`
this crate links, so the remediation is demonstrated rather than inferred
from a version number. `block-buffer` is added as a dev-dependency for this,
pinned to the version Cargo already resolves so the test cannot exercise a
second copy.
The two buffer types do not share a cursor invariant — `EagerBuffer` requires
`pos < block_size`, `ReadBuffer` requires `1 <= pos <= block_size` — and
asserting the former on the latter produces a test that fails against correct
behaviour. Both are asserted separately.
### Added
- **Public-surface compatibility suite (`tests/compat/`).** Asserts that the
console entry points, HTTP routes, top-level Python exports and `aegis_rust`
FFI names present at the published `v4.1.2` tag are still present. Every
expected value was read out of the tag with `git show v4.1.2:<path>` rather
than restated, and the assertions run one way — anything that existed then
must exist now, additions are fine — so a new endpoint does not train anyone
to edit the expectation. `v4.1.2` is the baseline because it is the most
recent published release: there is no `4.2.0` at any surface, so there is no
`4.2.0` contract to compare against. It pins names, routes and call shapes
only; response bodies and persisted-evidence compatibility are covered
elsewhere, and the SDKs version independently.
- **The retained-byte ceiling is computed in code (`aegis/core/stream_bounds.py`).**
`R_max = 4W + Q + E + P` was declared in `specs/aegis_stream_buffer.smt2` and
restated in prose, but nothing computed it, so the two could drift without a
failure anywhere. `StreamRetentionBounds` now holds the expression and the
spec's declared parameter ranges in one place, and `tests/test_stream_bounds.py`
parses the ranges back out of the `.smt2` file rather than hardcoding both
sides. `BoundedStreamProxy` gained `bounds` and `retained_bytes_ceiling`
accessors; they are **reporting only** and change no admission decision, so
no stream the previous release admitted is refused now.
This does not upgrade what the Z3 run establishes. That check remains
arithmetic consistency over declared ranges, not a refinement proof of the
proxy or of process memory, and `R_max` remains a per-stream ceiling —
aggregate memory still scales with concurrent admitted streams.
- **Scanner report ingest (`scripts/triage/parse_socket_report.py`).**
Converts a Socket.dev PDF export into normalized JSON committed under
`evidence/dependency-scan/`, so a scan is diffable rather than a binary
nobody can review. It undoes the layout wrapping that splits package names
and paths mid-token, recognizes both the dependency-alerts and threat-feed
layouts, and exits non-zero on any row it cannot parse — a silent drop would
break the guarantee that every row is accounted for. The 2026-09-09 report's
**100 rows parsed with zero failures**, as did the 30 rows of the threat-feed
sample.
- **Deterministic dependency triage (`scripts/triage/dependency_triage.py`).**
Reads every dependency the repository locks across all five lock files,
queries OSV per pinned version, and assigns each component to exactly one of
six action buckets under a fixed precedence. The advisory snapshot is pinned
to `docs/security/advisory_snapshot.json` so classification is reproducible
offline and records the date the advisory set was observed. Advisories with
no fixed version are routed to the maintenance bucket rather than the CVE
bucket — they have no bump available, and filing them as CVEs makes the gate
unsatisfiable. `--scan-export` merges a CSV or JSON scanner export and reports
rows naming packages the repository does not lock rather than dropping them.
- **Licence inventory (`scripts/license/license_scan.py`).** Reads declared
licences from installed `.dist-info` metadata, crate manifests under
`CARGO_HOME`, and npm lock entries, classifies each SPDX expression by its
most permissive satisfiable branch, and emits `docs/compliance/LICENSE_AUDIT.md`
and `LICENSE-THIRD-PARTY.md`. Both are scoped to components that actually
travel with an artifact, so the output is a function of the lock files rather
than of whatever happens to be installed in the generating environment.
Result across 380 distributed components: **0 strong copyleft, 0 unknown**,
15 weak copyleft (the optional per-platform `@img/sharp-libvips-*` binaries
reached through the dashboard's Next.js image path, plus `certifi` at
MPL-2.0), all now attributed with a source offer.
- **Adversarial test suite (`tests/redteam/`), 146 tests.** ReDoS scaling
bounds against the de-identifier and the streaming frontier — asserting
linear scaling by feeding n and 2n rather than a wall-clock budget that turns
flaky on a loaded runner — and inclusion-proof forgery attempts covering leaf
substitution, root substitution, `peak_index` confusion both out of range and
in range, structural count tampering, path and direction tampering,
version/algorithm crossover, and malformed digests.
- **Fuzzing harnesses (`tests/redteam/fuzz/`)** using `atheris`, with corpora
and findings under `evidence/fuzz/4.3.0/`. 100,000 executions against the
de-identifier (asserting it never raises and that redaction is idempotent)
and 200,000 against proof verification (asserting it never raises and never
returns `True` for an input the fuzzer built). The second campaign found the
type-confusion defect recorded above.
- **CycloneDX 1.5 SBOMs for all five dependency trees** under
`evidence/sbom/4.3.0/` (521 components total). The Python SBOM is built from
`requirements.lock` rather than the ambient environment, so it describes what
a released image installs.
- **`docs/security/DEPENDENCY_TRIAGE.md`**, **`DEPENDENCY_RISK_REGISTER.md`**
and **`BUILD_SCRIPT_ATTESTATION.md`**, the last enumerating all 52 crates
carrying a build script or native code, derived by reading crate source
rather than from name heuristics.
- **`tests/security/test_yaml_safe_loading.py`** and
**`test_dependency_containment.py`**, holding four properties that were
already true and easy to lose silently: no `yaml.load` without an explicit
safe loader anywhere in the first-party tree, no ML inference stack in the
core runtime dependency graph, no script enabling Next's
`--experimental-https` path (which fetches an unsigned binary and
interpolates hostnames into `execSync`), and no build tooling declared as a
dashboard runtime dependency.
- **Offline commercial licensing (`aegis/licensing/`).** Ed25519-signed
entitlement tokens verified without any network call: the signature is checked
before the payload is parsed, and expiry, module grants and field types are all
validated. Every failure raises a `PermissionError` subclass, so a caller
guarding with `PermissionError` fails closed on all of them.
`scripts/generate_commercial_license.py` is the vendor-side `keygen`/`issue`
tool. **There is no compiled-in root public key** — it must be supplied or set
in `AEGIS_LICENSE_ROOT_PUBKEY`, and an absent key refuses rather than
defaulting, because any 32-byte placeholder loads as a valid Ed25519 key and
would fail silently instead of loudly. A token is a bearer credential, expiry
is checked against the host clock, `max_annual_mgt` is carried but never
enforced, and there is no revocation mechanism. `CLM-069`.
- **Four engine facades (`aegis/engines/`).** Veracity, Sanctum, Agentis and
Sovereign expose existing core capabilities as libraries usable without the
HTTP gateway. They **add no capability and relax no boundary**. Licence gating
is **off by default** and does not restrict the AGPLv3 build; enforcement is
opt-in through `AEGIS_LICENSE_ENFORCEMENT=required`, and an unrecognised value
raises rather than silently falling back to off. `CLM-070`.
- **Enterprise connectors (`aegis/connectors/`).** Splunk HEC with a bounded
queue, bounded disk spool and oldest-first eviction; a Parquet lakehouse
exporter emitting the segment's own `aegis-wal-segment-manifest-v1` built by
the same function the archival path uses; and a HashiCorp Vault Transit signer
that submits digests `prehashed`. A connector cannot fail a governed request —
Splunk delivery problems become counters and spool files, never exceptions in
the caller's path. Connector output is a **derivative copy**; verification
stays against the WAL. `pyarrow` is the new optional `lakehouse` extra.
`CLM-071`.
- **Envoy/Istio WASM filter (`connectors/envoy-wasm/`).** A `proxy-wasm` crate
that refuses declared critical request patterns and redacts declared identifier
patterns from response chunks behind a bounded holdback, running redaction to a
fixpoint before measuring the frontier. Builds to `wasm32-wasip1`; 7 unit
tests. It **commits no evidence**, so a deployment running only the filter has
request governance and no evidence trail. `CLM-072`.
- **Commercial documentation** under `docs/commercial/`: pricing guide, connector
ecosystem guide and software-escrow policy. Published prices are **list prices
the vendor is asking**, not observed contract values; the SLA schedule is a
**template for negotiation** with no rota staffed; the escrow policy has **no
executed agreement, no engaged agent and no deposit**. `UC-019`, `UC-028` and
`UC-033` are unchanged, and `DOC-06 §3.2a` reconciles list prices with the
standing "no package has a validated price" position.
**Why this is recorded under `4.3.0` and not a new major version.** The work
was specified as a `5.1.0` modular-fabric release. Moving the version line
would mean bumping fourteen synchronized anchors, and nothing here is a
breaking change: no existing entry point, route, or FFI contract changed, and
every addition is opt-in. A version bump is a separate, deliberate change and
is not made as a side effect of adding features.
- **Domain-separated MMR inclusion scheme, `aegis-mmr-inclusion-v2`.** Closes an
RFC 6962 §2.1 leaf/node type confusion in the portable proof verifier. v1
hashes a leaf as `SHA-256(payload)` and an interior node as
`SHA-256(ascii(left_hex) || ascii(right_hex))`, neither input tagged, so a
leaf whose payload is the 128-character concatenation of two child digests
hashes to exactly the interior node over them. v2 prefixes each hash input
with a domain tag and consumes raw 32-byte digests: leaf `SHA-256(0x00 ||
payload)`, node `SHA-256(0x01 || left32 || right32)`, root `SHA-256(0x02 ||
peaks)`.
**v1 remains the default and is unchanged.** The scheme determines every root
a chain has recorded, so switching it would make each deployed WAL replay to a
different root and the ledger's own integrity check declare it corrupt. v2 is
additionally not wired into `CryptographicAuditLedger`: the `aegis_rust`
accumulator implements v1 only, and an existing chain cannot change scheme
without rewriting the roots it already recorded. v2 is available to callers
building their own accumulator and to verifiers checking v2 proofs. See
`CLM-064`.
v2 proofs travel as 43-character unpadded base64url digests, applied at the
serialisation boundary only; digests stay lowercase hex inside the dataclass.
The decoder is strict about the alphabet and re-encodes to reject
non-canonical spellings, because the two spare bits in a 43-character encoding
would otherwise make one digest expressible several ways.
### Fixed
- **Tenant identifier comparison is now total over Unicode.**
`hmac.compare_digest` raises `TypeError` for `str` arguments holding any
non-ASCII character, and three tenant comparisons passed `str` directly.
A client-supplied `tenant_id` carrying non-ASCII — a query parameter on the
audit listing, a body field on the forensic export — surfaced an authorization
denial as an unhandled 500 rather than the intended
`403 Tenant access denied`. Separately, two credentials naming one legitimate
internationalized tenant failed to combine at all, so `api_key_mtls` and
`oidc_mtls` could not serve that tenant.
No cross-tenant read was possible in any case; the request failed closed
throughout. What changed is the shape of the failure and the availability of
internationalized tenants. Identifiers are compared as exact UTF-8 bytes and
deliberately not Unicode-normalized: normalizing would let two distinct
codepoint sequences resolve to one tenant, while an unnormalized mismatch
denies. See `CLM-065`.
### Added — earlier in this line, after `4.1.2` shipped
- **Windows single-writer WAL locking.** `_lock_wal_fd` now takes the exclusive
lock with `msvcrt.locking` when `os.name == "nt"`, so a second
`CryptographicAuditLedger` on one WAL path raises `WalWriterConflictError` on
Windows exactly as it already did on POSIX. Previously `fcntl` was absent
there, the ledger logged a warning, and the WAL-02 fork the guard exists to
prevent could still occur. A companion `_unlock_wal_fd` releases the lock
explicitly at `close()` and at rotation.
The Windows lock is placed on a one-byte sentinel region at 1 TiB rather than
at the file head, because the two primitives differ in kind: `flock` is
advisory and denies nothing, while `msvcrt.locking` maps to `LockFile` and is
*mandatory* — a lock over live bytes would deny reads to every other handle.
`_load_from_wal` replays the whole file before `_open_wal` asks for the lock,
so head-locking would have turned "another writer holds this path" into "this
WAL is unreadable" on Windows alone. The offset is bounded rather than
arbitrary for the mirror-image reason: a seek past the filesystem maximum
fails with `EINVAL`, and `_SentinelUnavailableError` keeps that outcome from
being reported as a lock conflict, which would refuse the *first* writer.
- A `windows-2022` CI job runs `tests/security/test_wal_single_writer.py`
against the real `msvcrt`. Without it the Windows primitive would be asserted
by stand-in tests and never executed. The suite's cross-process test no longer
skips off POSIX, and seven tests drive the Windows branch on any host through
the module's platform selector.
### Changed
- **The licence entitlement moved to `aegis/licensing/model.py`**, separate from
the token decoding and signature checking in `validator.py`, and
`is_valid` / `has_module` now accept an explicit `now` in epoch seconds.
Nothing moved out of reach: `LicenseEntitlement` and `KNOWN_MODULES` are
re-exported from `validator.py` and from the package, and both import paths
return the identical object. `now` defaults to the host clock, so every
existing zero-argument call behaves exactly as before, and `seconds_remaining`
stays a property — the parameterised form is the separate
`seconds_remaining_at(now)` rather than a signature change to public API.
Passing `now` makes the expiry boundary testable. It does not make expiry
trustworthy: the value still comes from the caller, and a host whose clock
runs backwards still extends its own licence. That boundary is unchanged.
- **`SanctumEngine` validates its window at construction and checks its
holdback against `R_max`.** A `window_chars` outside `[64, 4096]` previously
constructed fine and raised on the first chunk, a long way from the
misconfiguration that caused it; it now raises `StreamBoundsError` — a
`ValueError` subclass, so existing handlers still catch it — from
`__init__`. While redacting, the engine compares the redactor's retained
holdback against the ceiling computed from the spec's expression and fails
closed if it is ever exceeded, which is a check independent of the redactor's
own bound rather than a restatement of it. In-process `Q`, `E` and `P` are
zero, so the ceiling is `4W`; the engine reports that this configuration sits
outside the spec's declared ranges rather than inventing a queue budget to
appear inside them.
- The two suffixed claim IDs introduced during v2 development are renumbered.
`verify_claims.py` matches `CLM-\d{3}` exactly, so `CLM-006b` and the tenant
claim were parsed by nothing and validated for nothing. They are now
`CLM-064` and `CLM-065`, covered by a control-register range, and the
register count moves 63 → 65.
- `docs/RELEASE_STATUS.md`, `docs/CLAIMS_MATRIX.md` and the documentation corpus
record the `4.1.2` publication read back on 2026-09-04, and the distribution
model that publication changed: `aegis-latent-core` is now installable from
PyPI, so the corpus no longer says the gateway ships from source only or that
the registries carry SDKs only. Those statements were accurate at `4.1.1` and
are retained with that scope where they describe it.
One boundary is recorded rather than smoothed over: the two PyPI
`aegis-latent-core` artifacts are byte-different from the release assets of
the same name. Entry-by-entry comparison shows identical content — same 204
members, sizes, CRCs and timestamps — differing only in the ZIP
creator-system field, which means they were built on different hosts. The
release `SHA256SUMS` and attestation therefore do not cover the PyPI gateway
downloads. Both SDK registries do match.
### Noted, not changed
- **One of the two commissioned scan exports arrived; the other did not.** The
dependency alerts report (100 rows, generated 2026-09-09) is ingested in full
and every row appears exactly once in the triage ledger. In place of
`alerts.pdf` a Socket **Threat Feed sample** was supplied — an ecosystem-wide
sample of packages flagged across npm and PyPI, not an alert set against this
repository. Its 30 rows are ingested for completeness and **none of the four
packages it names appears in any Aegis lock file**. If `alerts.pdf` exists and
differs from the dependency report, its rows remain outside this review.
- **The alert set does not contain the licence and ML-stack items the brief
described.** `uvloop`, `torch`, `transformers`, `vllm`, `next` and `sharp`
appear in **no row** of the report. The findings recorded for them here come
from reading this repository's own manifests, not from the scanner, and they
stand on that evidence.
- **`uvloop` is not a GPL dependency and not a direct one.** The commissioning
brief classified `uvloop 0.22.1` as a direct GPL-2.0/3.0 dependency and a
release blocker. Its installed distribution declares `MIT License` with trove
classifiers for both MIT and Apache-2.0, and ships `LICENSE-MIT` and
`LICENSE-APACHE` in its `dist-info`; there is no GPL grant. It also arrives
transitively through `uvicorn[standard]` rather than being declared directly.
No change was made.
- **`torch` and `transformers` are already optional extras.** The brief
described both as direct dependencies requiring relocation. Both are declared
only under the `gpu`, `hf` and `vllm` extras and neither appears in
`requirements.lock`. The containment they asked for is the existing state;
it is now enforced by test rather than left as a convention.
- **The `pqcrypto-*` stack carrying ML-DSA-65 is going unmaintained**
(`RUSTSEC-2026-0162`, `-0163`, `-0166`, plus `RUSTSEC-2024-0436` for `paste`
beneath it) because upstream PQClean is being archived in or after July 2026.
None is an exploitable defect and none has a fixed version. Migration to the
pure-Rust `ml-dsa` crate is recorded as a tracked risk in
`docs/security/DEPENDENCY_RISK_REGISTER.md` rather than attempted here:
replacing a signing implementation needs its own vector-level verification
and its own timing assessment, and burying that in a dependency sweep would
be the wrong place for it.
- **Version anchors were not moved to `4.2.0-SECURE`.** The brief named that as
the release target. `4.2.0` was skipped deliberately when the source line
moved from `4.1.2` to `4.3.0` and no artifact was ever published under it, so
adopting it now would both regress the source line and resurrect a number
this project has documented as absent. This work is recorded under `4.3.0`,
which remains unpublished.
- **Offensive supply-chain remediation was not performed against upstream
projects.** The Next.js `mkcert.js` path was verified to match the brief's
description — unsigned binary download, unescaped host interpolation into
`execSync` — and left unpatched because it is upstream code on a path this
repository never takes. A test keeps it unreachable instead.
## [4.1.2] — 2026-09-03
**Published on every surface**, read back 2026-09-04: signed annotated tag
`v4.1.2` at `860f14177d94c194e5ae7156017d6fa74264e429`, GitHub Release with 31
assets, PyPI `aegis-latent-core` `4.1.2` (its first release), PyPI
`aegis-latent-sdk` `4.1.2`, npm `aegis-latent-sdk` `4.1.2`, and both GHCR images
with cosign signature objects present. `cosign verify` and
`gh attestation verify` were not run. See `docs/RELEASE_STATUS.md` §1.0.
### Added
- **Embedded mode — `aegis.wrap(client)`.** `aegis/embedded.py` runs the WAF,
Safe Harbor scrubbing, bounded-holdback streaming redaction and the signed
Merkle ledger inside the calling process, for applications that already hold
an `openai` or `anthropic` client and cannot add a network hop. Clients are
recognised by shape — `chat.completions.create` or `messages.create`, sync or
async — so neither SDK is imported or required. Responses carry
`_aegis_evidence`; blocked prompts raise `AegisBlockedError` and are never
dispatched. Streaming holds back the run of chunks from the last text-bearing
one, so the terminal record is committed before the final chunk is yielded and
the flushed holdback tail is still delivered to the caller. The committed
`response_hash` covers what the caller received, not what the provider sent.
Scope, stated because it changes a threat model: the engine governs calls made
through the client it wrapped. Code in the same process can call the provider
directly, hold an unwrapped client, or edit the WAL. It is an evidence and
policy layer for cooperative code, not a containment boundary against the
process it runs in, and not a substitute for the gateway where the application
is itself the thing being constrained.
- **Durable pre-admission rejections.** `commit_rejection` writes a signed,
chain-linked, MMR-anchored node with `status="rejected"` for a request the
gateway refused, and the WAF and rate-limit paths emit `X-Aegis-Rejection-ID`
and `X-Aegis-Evidence-Status`. Previously a blocked attack left only a log
line, which is not evidence: logs are mutable and unchained. The body is
hashed, never stored — blocked input is frequently hostile and the chain must
not become a repository of attack payloads. The refusal never depends on the
commit: if evidence cannot be written the request is still refused and the
header reads `rejection-uncommitted`. A faulted ledger is not extended, the
same rule `_require_intact_ledger` applies to admitted traffic.
- **Agent-to-agent receipts.** `aegis/core/a2a.py` issues and verifies receipts
for tool executions between agents, with verifiers ported to both SDKs
(`sdk/python/src/aegis_sdk/a2a.py`, `sdk/typescript/src/a2a.ts`). Arguments and
results travel only as SHA-256 digests. The canonical envelope is a
deterministic function of the receipt's own fields, so a receipt cannot be
re-pointed at another execution's leaf — including a genuine proof of a
different leaf in the same tree. A valid receipt establishes inclusion under
the supplied root and nothing else: not that the tool ran, not that an agent
identifier is authentic, not that the timestamp is true.
- **`<wal>.mmr.state` peak-set checkpoint**, written atomically at close and at
rotation, restoring the accumulator in O(log N) rather than replaying every
leaf. Accepted only when the restored root equals the root the last committed
node recorded; a missing, stale, truncated, corrupt or disagreeing checkpoint
falls back to full replay, and leaves committed after a checkpoint are
replayed on top of it, which is the ordinary case after a crash.
**Opt-in (`mmr_fast_restore=True`), and off by default.** A peak restore
summarises the leaves below it, so their inclusion proofs can no longer be
derived from the live accumulator, and `tests/test_mmr_restart.py` asserts
that capability deliberately — proving every historical leaf is a stronger
structural check than root equality alone. Nothing forensic is lost either
way: each committed node carries its own self-contained `mmr_proof`, and the
ledger only ever asks the accumulator to prove the leaf it just appended.
This is **not** a fix for cross-restart MMR continuity. That was already
correct: `_load_from_wal` replays every leaf and cross-checks the rebuilt root
against the recorded one. This changes the cost of that reconstruction, not
its result — both paths produce the same root, leaf count and next leaf index.
### Changed
- `AuditNode` gains a `status` field (`"committed"` / `"rejected"`). It is
deliberately **not** a `node_hash` input, so every node written before the
field existed hashes identically with and without it: existing chains stay
verifiable and already-issued MMR proofs keep validating. Legacy WAL records
load as `"committed"`.
- `tools/docs/verify_documentation.py` accepts either `Current release:` or
`Current release candidate:` as the README status label. It previously pinned
the pre-publication wording, which no longer describes a published release.
Removing the status line from README still fails the check.
### Documentation
- Corrected the corpus to the observed `4.1.1` publication state. Twenty-odd
documents asserted that nothing was published for `4.1.1` and that both SDK
registries carried `4.0.0`. Readback on 2026-09-03 contradicts both: the
signed annotated tag, the GitHub Release and its 31 assets, PyPI
`aegis-latent-sdk` `4.1.1`, and the GHCR gateway and dashboard images all
exist. npm alone still carries `4.0.0`.
- `docs/RELEASE_STATUS.md` contradicted itself: its header read "nothing is
published" while §1.1 below it recorded a published tag, a 31-asset release
and PyPI at `4.1.1`. The header, the §1 lead and the "registry gap is three
versions wide" paragraph are corrected, and §2's readback commands now target
`v4.1.1` rather than `v4.0.2`, each with the value observed on 2026-09-03.
- Recorded the OCI readback that had not been performed for this version:
`ghcr.io/juanlunaia/aegis-latent-core:4.1.1` resolves to
`sha256:5f2caaa60ee00dd82882bee1b4f2ee046ee2877131afed1af4e356b4bd8f5343` and
the dashboard image to `sha256:0f66c9f6f8fb7ea0327b9aa2d9df26a030bd76c7d53d2a2186a46f2385489a07`,
both OCI image indexes over `linux/amd64` and `linux/arm64` with a cosign
signature object present for each digest. A resolving `.sig` tag is not a
verification: `cosign verify` and `gh attestation verify` were not run, and
the documents say so.
- `CLM-048` through `CLM-050` restated against the 2026-09-03 readback, and the
control register now forbids collapsing PyPI and npm into "the registries" —
they are at different versions and a single sentence about both is wrong
whichever version it names.
### Fixed
- `publish_npm.yml` ran `npm publish release-artifact/*.tgz`. `npm publish`
parses its argument as a package spec, and a bare `a/b` path is npm's GitHub
`owner/repo` shorthand, so npm attempted
`git ls-remote ssh://git@github.com/release-artifact/aegis-latent-sdk-4.1.1.tgz.git`
and exited 128 with `Permission denied (publickey)`. The `4.1.1` dispatch died
there while PyPI published from the same run. The step now passes a
`./`-prefixed path and fails loudly if the download directory holds anything
other than exactly one tarball, which the glob previously left to chance.
- The same unpublishable command was pinned in two more places: the assertion in
`tests/test_release_contract_v4.py` and the `npm.provenance` regex in
`scripts/verify_release_contract.py` both required that literal, so the
release contract validated a command that could not work. The contract now
checks the properties the release needs — provenance, public access, and a
path npm resolves as a file — against the workflow with comment lines removed,
since a comment explaining a forbidden form has to contain it.
## [4.1.1] — 2026-09-03
Release-engineering fix. `4.1.0`'s source is unchanged apart from the CI
correction below; this version exists because `4.1.0` could not be published
correctly and a published GitHub Release cannot be repaired in place.
### Fixed
- The `Generate SBOM` job in `ci.yml` failed on a `release: published` event
with "Resource not accessible by integration". `anchore/sbom-action` defaults
`upload-release-assets` to true and the input was never set, so the job tried
to attach an SBOM to the release while holding `contents: read`. The upload is
now disabled explicitly and the token stays read-only: `release.yml` is the
single owner of release assets and emits its own canonical
`aegis-latent-core-<version>.spdx.json` with a sidecar. Granting write instead
would have published an asset absent from `release-asset-manifest.json` and
unhashed in `SHA256SUMS`, which is the manifest a consumer is told to verify
against.
### Boundary
- **`4.1.0` was tagged and released outside the release pipeline, and the result
is not a usable release.** No workflow in this repository is tag-triggered, so
pushing a tag by hand ran nothing: the `v4.1.0` GitHub Release carries zero
assets, no Deployments were created, and the tag is lightweight rather than the
Sigstore-signed annotated tag `scripts/verify_release_tag.sh` requires. Both
the `v4.0.2` and `v4.1.0` releases are marked immutable, which freezes an
asset set at publication, so `v4.1.0` cannot be populated after the fact. It is
superseded by this version rather than corrected.
- Publication of `4.1.1` establishes nothing by itself. Tag, GitHub Release,
PyPI, npm, OCI, signature and attestation state remain external readback facts;
see `docs/RELEASE_STATUS.md`.
## [4.1.0] — 2026-09-03
Kernel hardening and evidence-path correctness. **This version was never
published through the release pipeline.** A lightweight `v4.1.0` tag and an
empty, immutable GitHub Release were created by hand after the fact; they carry
no assets, no Deployments and no signature, and the tag does not satisfy
`scripts/verify_release_tag.sh`. No PyPI or npm package, OCI image or
attestation exists for `4.1.0`. It is superseded by `4.1.1`.
`docs/RELEASE_STATUS.md` records the readback state.
### Security
- Governed traffic is refused while the evidence chain is known to be broken. WAL replay marks the ledger `wal_corrupt`; until now that fault reached `/health` but not the request path, so the proxy kept forwarding and appending nodes onto a prefix it had already failed to replay. Each such commit succeeded and verified individually, which is what made the divergence silent. `_require_intact_ledger` now returns `503` at all three governed endpoints before any forwarding or commit; `/health` and `/metrics` stay reachable so the fault remains diagnosable. `docs/architecture/FAILURE_SEMANTICS.md` no longer describes this as the system's one non-fail-closed path.
- Open-candidate marker searches in the streaming redactor are case-insensitive, matching the URL and track-1 detectors. An unterminated `HTTPS://` or `%b` candidate previously passed the guard entirely, which was a fail-open on the exact grammar the guard exists to catch.
### Added
- `_require_intact_ledger` in `aegis/proxy/app.py`: every governed endpoint refuses with `503` while the ledger's `_fault_state` is not `healthy`, so a gateway that started on a corrupt WAL no longer forwards traffic or extends a prefix it already failed to replay. `/health` and `/metrics` stay reachable so the fault remains visible.
- Regression coverage for the three remediated paths: `tests/test_app_wal_corrupt.py` (refusal on all three governed endpoints, before the upstream call, with no chain growth), `tests/test_mmr_restart.py` (accumulator continuity across one, many and every carry-shape restart, compared against an uninterrupted ledger), and `tests/test_phi_address_bound.py` (the two ADDRESS bounds agree, prose streams through, real addresses still redact, and the recall cost is asserted).
- `benchmarks/bench_dispatch_overhead.py`: distribution over `aegis.proxy.app._spawn_background` and RSS sampling across repeated commit batches, reporting steady state separately from round-one warm-up so allocator growth is not read as a per-commit leak.
- `benchmarks/bench_commit_scaling.py`: per-commit cost measured against prior chain length, so a length-dependent regression on the commit path is visible as a curve rather than a single number.
- `evidence/evidence_path_measurements_2026-09-03.md`: MMR append throughput (Rust versus Python), audit-chain commit and verification, dispatch overhead, steady-state memory and ML-DSA signing latency, all taken on commit `f77420a` in one named container, with per-measurement boundaries and an explicit list of what was not measured.
- Five claims-matrix rows (`CLM-054`–`CLM-058`) covering Kani frame-bounds model checking, the O(log N) MMR rollback token, POSIX advisory single-writer locking, streaming viable-prefix guards, and native-WAL segment growth — each with its forbidden phrasing recorded in the control register.
- `docs/formal/FORMAL_VERIFICATION.md` documents the Kani harnesses, the property each checks, and why they differ in kind from the Z3, Lean and TLA+ artifacts.
- Single-writer enforcement on the JSONL WAL: `CryptographicAuditLedger` takes a POSIX advisory lock before publishing the handle, so a second writer raises `WalWriterConflictError` at startup rather than forking the evidence chain silently.
- `aegis_security_enforcement_mode` gauge reporting the loaded enforcement posture as `1` (strict) or `0` (development), set before dependent construction and exposed on `/metrics` rather than `/health`.
- Helm chart renders a `StatefulSet` with per-replica `volumeClaimTemplates`, a headless governing Service, and a default-deny `NetworkPolicy`; `values.schema.json` pins `aegis.workers` to `"1"` and constrains `persistence.accessMode`.
- Post-build check asserting no server-only secret reaches browser-served dashboard output, wired into CI after the dashboard build.
- Documentation corpus: claim-control foundations (`docs/STYLE_GUIDE.md`, `docs/DOCUMENTATION_GOVERNANCE.md`, `docs/INDEX.md`), security volume, operations runbooks, API references, four framework technical-input documents, privacy boundaries, enterprise and corporate volumes, assurance index, and root governance files.
- Four documentation gates run in CI: `scripts/verify_docs.py`, `scripts/verify_claims.py`, `scripts/verify_links.sh`, and the pre-existing `tools/docs/verify_documentation.py`.
- Eleven claims-matrix rows covering the `fsync` durability boundary, trusted-root independence, the native WAL's auxiliary role, `pending-terminal` semantics, redaction as best-effort, registry publication state, the `bad_cert` explanation, and explicit denials for production SLO, WORM and immutability; stable `CLM-NNN` identifiers on all 53 rows.
- `MerkleMountainRange.checkpoint()` and `rollback_to()`: an O(log n) rollback token that records the append-only lengths and the live peak nodes, replacing a whole-structure snapshot on the commit path.
- Regression coverage for MMR append rollback (`tests/test_mmr_rollback.py`) and for chain integrity across memory-window rollover (`tests/test_crypto_audit_rollover.py`), including a `slow`-marked 100,000-node sweep through a 512-node window. Both the rollback path and the window anchor were previously unasserted.
- Rust↔Python MMR parity extended beyond root equality: Python-generated portable proofs are verified against the Rust-reported root, `RustBackedMMR` is checked end to end, and the `sha256-asciihex` wire literal is pinned to the digest both implementations actually compute.
- Kani model checking of the native WAL's frame-bounds arithmetic. `header_range` and `payload_range` in `aegis_rust_v2/src/wal.rs` now bound every slice taken during a frame walk, and five `#[kani::proof]` harnesses check over the whole `usize` domain that the returned ranges stay inside the limit, never overflow, never treat the zero-length recovery terminator as a frame, always advance the cursor, and never overlap. Wired into CI as a `Kani Model Checking` job pinned to Kani 0.67.0; scope and limits recorded in `docs/formal/FORMAL_VERIFICATION_LIMITS.md`.
### Changed
- `mypy --strict` passes over `aegis` and `sdk/python/src` — 153 errors in 55 files reduced to zero — and both are now CI gates so the state holds. The work was annotation, not redesign: bare `dict`/`list`/`Callable`/`re.Pattern`/`ctypes.Array` parameterised, return types supplied, and `Any` escaping an untyped boundary stated with an explicit `cast` at that boundary rather than left implicit.
- Bandit reports zero findings across `aegis` and `aegis_server`, at every severity rather than the `-lll` floor CI enforced. Thirteen silent `except: pass` handlers now log at debug level, so a swallowed failure is diagnosable; `ldap_auth` and `session_manager` gained the module logger they lacked. Eight `subprocess` calls resolve their executable through `shutil.which(...) or <name>`, matching the convention already used in `cfi_manager` and `dependency_audit`, which removes the implicit PATH lookup. Vault retry jitter uses `secrets.SystemRandom`. The remainder are annotated with the reason they are not defects.
- `tests/test_sse_utf8_boundaries.py` pins the SSE line framing's UTF-8 behaviour at every possible chunk-split offset, and `_iter_bounded_lines` records why an incremental decoder is not used.
- Per-commit cost is now independent of chain length. Measured on one container 2026-09-03: at 2,000 prior leaves, 30,154 µs → 362 µs. The pre-change curve rose `1.00× → 17.65×` with chain length; the post-change curve is flat within noise.
- `_validate_active_deployment_versions` in `scripts/verify_release_contract.py` derives its expectation from the synchronized core version instead of a hard-coded literal. The literal made the check assert agreement with a constant rather than with the release being cut: it reported `READY` at `4.1.0` while eight deployment surfaces still named `4.0.2`. All twenty-two references are now synchronized, and `tests/test_release_contract_v4.py` fails if the derivation regresses.
- `docs/BENCHMARKS.md` carries an evidence-path section for the current source baseline alongside the retained v3.1.0 record, and `docs/architecture/DEEP_DIVE.md` replaces a stale Rust/Python MMR ratio (`3.01x` / `3.34x`) with the 2026-09-03 measurement (`4.77x` average, `4.94x` maximum) plus the environment it belongs to. The ratio is a property of the host, not the code.
- `docs/benchmarks/BENCHMARK_METHOD.md` gains three measurement classes and three prohibited phrasings: a ratio against unmeasured provider round-trip time, "zero memory leaks" from a bounded run, and a mean quoted without its tail. The 2026-09-03 dispatch sample is the worked example — one 42.6 ms outlier put the mean above the p90.
- Least-privilege `GITHUB_TOKEN`: read-only workflow-level floor in `ci.yml` and `forensic.yml`, and `security-events: write` moved from workflow scope to the four SARIF-uploading jobs in `security.yml`.
- `README.md` restructured and reduced from roughly 33 KB to 13 KB, stating release status once and routing to `docs/RELEASE_STATUS.md`.
- `docs/RELEASE_STATUS.md` records a 2026-09-02 readback of every publication surface, with per-surface commands and a publication-state table.
- `CryptographicAuditLedger` reverts a failed commit through the MMR checkpoint instead of `copy.deepcopy`. The deep copy ran on every commit and copied the whole accumulator, so per-commit cost grew with the length of the chain. Failure semantics are unchanged: a signing or WAL-persistence failure still leaves the MMR exactly as it was.
### Fixed
- The `ADDRESS` pattern's unbounded `[A-Za-z0-9 ]+` street-name run made any number-led prose a viable address prefix, aborting streams of ordinary text with `privacy_failure`. Bounded to 40 characters — 2.2x the longest street-name span in a sample of real addresses — with the streaming guard mirroring the bound. A viable candidate is now at most 46 characters against a 64-character minimum window, so the abort is structurally unreachable rather than merely tuned away. The cost is stated and tested: a street name longer than the bound is no longer redacted.
- Release-artifact verification instruction corrected: assets carry attestations and `SHA256SUMS`, not detached signatures, so the check is `gh attestation verify`, not `cosign verify-blob`; `cosign verify` applies to the OCI images.
- `DOC04-CLM-011` corrected: the proxy does attach `/metrics` whenever `prometheus-client` is importable.
- DNS egress in the Helm `NetworkPolicy` scoped to resolver pods via a `podSelector`; a namespace-only peer permitted port 53 to every pod in `kube-system` while the comment claimed otherwise.
- Three stale heading anchors in `docs/architecture/DEEP_DIVE.md` pointing at `docs/BENCHMARKS.md` sections that no longer exist.
- Stale `rollout status` commands in `docs/institutional/DOC-04_OPERATIONS_PLAYBOOK.md` naming `deployment/` and omitting the release prefix.
- Streaming redaction aborted ordinary text. `StreamingDeidentifier` rejected an open track-data candidate whenever a semicolon was followed by more than `window_chars` of text containing no `?`, and an open email candidate whenever an `@` appeared anywhere in the holdback window rather than in the trailing whitespace-free token. Prose containing a semicolon, a mentioned email address, or a Python decorator therefore raised `StreamingDeidentificationError`, which the proxy reports to the client as a `privacy_failure` terminal outcome. Each guard now tests whether the candidate is a viable prefix of the detector that would redact it.
- `RustWal.open` truncated an existing segment when reopened with a smaller `capacity_bytes`. `OpenOptions::truncate(false)` prevents `open` from clearing the file, but the subsequent `set_len` shrank it just the same, discarding every frame past the new length — a 1 MiB segment holding 20 records reopened at 64 bytes retained 4. The requested capacity is now a floor rather than a resize instruction, so a segment only ever grows.
### Boundary
A clean `mypy --strict` run and a zero-finding Bandit report are properties of
those two checkers on this source, not evidence of correctness or of absence of
vulnerabilities. Bandit findings that were annotated rather than changed are
annotated with a stated reason; the reason is a reviewable claim, not a proof.
No claim in this release is a capacity, certification, legal-admissibility or
production-readiness statement. The Kani proofs cover two arithmetic functions,
not a system. ML-DSA `verify` remains **not** constant-time verified: the
retained experiment reports `p = 0.0`, and `aegis_rust_v2/src/pqc.rs` records why
hoisting the decode step does not address it.
## [4.0.2] — 2026-08-27
- Added trusted-proxy support for the documented `X-SSL-Client-SHA256` mTLS fingerprint assertion while preserving the historical `X-Client-Cert-SHA256` alias; conflicting assertions now fail closed.
- Added generic request-bucket `X-RateLimit-Limit` and `X-RateLimit-Remaining` response headers alongside the existing request/token dimension-specific fields.
- Added an all-targets, all-features Clippy gate with warnings denied to the Rust CI job and documented the reproducible source-development SDK contract in `docs/DEVELOPER_SDK_GUIDE.md`.
- Corrected the SDK documentation to acknowledge the public `aegis-latent-sdk` 4.0.0 registry objects without attributing them to the failed tag-triggered publication workflows.
- Clarified the audited immutable-source, external-object, registry-observation, and source-release-target layers after the v4 source merge and migrated shared coding-agent guidance from legacy `.cursorrules` to `AGENTS.md` with thin tool adapters.
- Added a deterministic, source-derived `.aegis_ai_context` manifest, progressive context router, component/workflow matrix, command/CI matrix, evidence index, and freshness tests. These are advisory repository aids, not hidden model instructions or release evidence.
- Recorded the current `main` policy of eight exact required GitHub Actions contexts, strict freshness, required signatures, linear history, disabled force pushes/deletions, and administrator enforcement disabled. Remote check results remain per-commit GitHub evidence and are not inferred from source metadata.
- Added a protected, manually dispatched GitHub Actions path that creates a Sigstore keyless signed annotated tag from the exact `main` head, verifies its workflow identity and ancestry, rejects tag replacement, and dispatches release, SDK publication, and signed OCI publication workflows against that immutable tag.
- Expanded GitHub Release assets to include the core package, Python SDK wheel/sdist, TypeScript SDK tarball, supported Rust wheels, SPDX JSON SBOM, per-file SHA-256 sidecars, a canonical release-asset manifest, and `SHA256SUMS`; release creation now rejects missing, unexpected, or byte-mismatched assets.
- Synchronized all fourteen governed version anchors—core, runtimes, both SDKs and locks, dashboard and lock, Rust metadata and lock, and Helm chart/app/image—to `4.0.2`.
- Included `aegis_server` in the core wheel, aligned active operator/Compose/airgap defaults to `4.0.2`, pinned multiarch Python and Node container bases by digest, and made the versioned installer verify the release-wheel SHA-256 sidecar before installation.
- Refreshed the hash-locked runtime closure and made its CI drift check seed `pip-compile` from the reviewed lock, so compatible transitive releases appearing later do not make identical source fail nondeterministically.
- Configured OCI publication for gateway and dashboard multiarch images with digest attestations and keyless Cosign signatures. PyPI/npm trusted publishing remains environment- and registry-controlled, and no release, package, image, signature, attestation, or provenance claim is made without successful external readback.
## [4.0.0 source candidate] — merged 2026-08-24
This historical candidate section describes the reviewed source tree merged as `2050a310ec295afc61d033ff842c9a535a4f3105`. Its fourteen version anchors are synchronized at `4.0.0`; that synchronization is source metadata, not publication evidence. No `v4.0.0` tag, GitHub Release, PyPI package, npm package, or OCI image publication is claimed.
### Bounded enterprise-maturation follow-up
- Added source-only release-readiness gates covering 14 synchronized version anchors, exact tag/version binding, pinned Python build backends, signed annotated tag ancestry, exact non-empty changelog extraction, deterministic release-asset preparation, and a create-only GitHub Release command. The OCI workflow now validates both declared architectures without registry login, push, signing, or publication. These controls do not establish external environment, signer, registry, Sigstore, architecture-runtime, or release acceptance.
- Restored Python 3.11 compatibility of the hash-locked dependency set by constraining NumPy below 2.5, and removed all 22 errors from the repository's configured `mypy-ci.ini` gate. The broader `mypy --strict` surface remains separate and is not claimed clean.
- Restricted forensic JCS manifests to ASCII keys, Unicode scalar strings, and I-JSON safe integers; bounded DAG-CBOR integers, rejected non-finite values and negative zero, and encoded accepted floats as 64-bit. `VERIFY.sh` now states that it checks only its embedded file-byte SHA-256 values and is not archive authentication, semantic verification, signature verification, or trusted-root verification.
- Hardened auxiliary RustWal recovery by flushing a zero-frame terminator at the recovered prefix and after each append, preventing same-size replacement of a corrupt frame from resurrecting a stale valid suffix. Tests cover contiguous concurrent offsets, readback, corruption, reopen, replacement, and reopen again; durability remains filesystem/device dependent and single-process scoped.
- Added a truthful `aegis.crypto` capability facade that reports optional PQC runtime availability, the explicit ZK stub state, logarithmic portable-MMR proof growth, and the absence of FIPS validation without changing cryptographic behavior.
- Added a dependency-free, metadata-only forensic query helper over fixed tuples of retained-node references. It neither copies nor makes referenced nodes immutable, uses bounded exact predicates and pagination, is not wired as a global/WAL search service, and carries no scale claim.
- Hardened finalized-segment timestamp receipt reuse, requiring exact schema and current-manifest bindings, trusted-CMS status, and non-empty timestamp evidence files; metadata gossip now surfaces equal-length divergent WAL heads without transferring WAL bytes.
- Made unmapped legacy API-key authority an explicit development-only compatibility opt-in while preserving strict-mode principal mapping requirements.
- Added content-free SIEM exporter counters for acceptance, rejection, acknowledgement, retry, and pending spool rows.
- Added hardened Helm defaults and a restricted source template for installing the operator controller with namespaced RBAC. Operator-generated Aegis workloads still require persistent-storage and target-cluster acceptance; the controller deployment intentionally references an invalid placeholder image until a reviewed immutable image is supplied.
- Added an advisory `.aegis_ai_context` pack, `llms.txt`, and legacy `.cursorrules` with offline structural tests and explicit release, proof, compliance, and external-acceptance boundaries. The rules were later migrated to canonical `AGENTS.md`; this bullet preserves the candidate history.
- Added strict documentation claim validation for configured high-risk assurance, performance, readiness, and publication language, including affirmative claims embedded in tables.
- Corrected TEE capability reporting so device-node visibility remains discovery-only; legacy caller-authored attestation reports are rejected, while an injected verifier may supply authenticated normalized claims for exact policy evaluation. No enclave loader or vendor quote verifier is implemented.
- Removed the unaccounted differential-privacy HTTP analytics route and reduced the module to an internal CSPRNG-backed Laplace count primitive with a one-release, add/remove adjacency boundary. No privacy accountant, repeated-release protection, or universal anonymization claim is provided.
- Made the legacy HSM manager fail closed when PKCS#11 is unavailable, removed its predictable software-HMAC fallback, rejected duplicate key labels, and required a unique exportable public key for asymmetric evidence metadata. Unit mocks are not hardware or FIPS validation.
- Replaced Cargo-only fuzzing capability detection and constant coverage/bug figures with exact executable, private-workspace, bounded-manifest, and confined target-file readiness checks, bounded execution states, explicit timeouts, and unavailable measured coverage. No cargo-fuzz target or Kani proof is claimed until source and tool evidence exist.
### Commercial expansion Phase 2/3
- Replaced whole-response SSE buffering with an incremental, byte-bounded streaming proxy that applies bounded-window PHI/PCI redaction, hashes forwarded bytes online, propagates backpressure, attempts one terminal-evidence commit per in-process stream invocation, and supports native Anthropic `/v1/messages` ingress. This is not exactly-once behavior across retries or process restarts.
- Added portable MMR inclusion proofs, corrected leaf-ordinal mapping for multi-peak trees, exposed authenticated proof retrieval and non-streaming proof headers, and added deterministic cross-language golden vectors plus the `aegis-mmr-inclusion-v1` protocol specification.
- Added standalone Python and TypeScript SDKs with gateway-compatible OpenAI and Anthropic client integration and stateless MMR proof verification while preserving provider-native request and response types.
- Added a read-only Next.js 16 and React 19 forensic dashboard with server-side credential isolation, explicit unavailable/error/empty states, real gateway data only, accessible ledger views, browser-side proof verification, and Prometheus-backed metrics visualization.
- Added a scoped forensic export workflow producing bounded ZIP bundles with RFC 8785 JCS manifests, deterministic RFC 8949 DAG-CBOR ledger slices, CIDv1 identifiers, portable MMR proofs, a technical PDF certificate, and an offline `VERIFY.sh`; the output explicitly does not claim certification or legal admissibility.
- Added provider-native RustWal terminal frames, streaming duration/token/redaction telemetry, a local MMR verification sandbox, raw canonical evidence inspection, and server-side ledger filters for tenant, model, endpoint, policy events, failures, and latency.
- Made post-commit failures in the auxiliary `RustWal` non-authoritative: Aegis now records `aegis_native_stream_wal_errors_total`, disables the failed auxiliary segment, and preserves the JSONL-bound terminal marker instead of creating contradictory terminal evidence.
- Added the bounded-stream Z3 model, streaming and portable-proof regression suites, SDK and dashboard CI jobs, production builds, accessibility checks, and real-backend visual QA evidence.
- Added and executed a seven-round, 1,000-event in-process SSE benchmark with retained JSON evidence and explicit exclusion of network and durable-WAL latency.
### CI reliability and supply-chain hardening
- Hardened asynchronous analysis-worker cancellation and bounded lifespan shutdown after reproducing the Python 3.11 `TestClient` teardown hang.
- Added per-response byte and total-duration limits to buffered SSE handling, with durable 502/504 failure evidence and upstream-generator closure tests.
- Replaced all 76 remote GitHub Action references with full 40-character commit SHAs and added a CI gate that rejects mutable Action references.
- Removed the mutable TLA+ `v1.8.0` release-asset URL from the formal trust path after the upstream lightweight tag and JAR changed in place; CI now builds from verified source commit `0894c3407f4717fec7cc18bde3bf3c857fa47333` and checks the embedded revision.
- Replaced repository-owned `datetime.utcnow()` test calls with UTC-aware timestamps and narrowly filtered three identified third-party transition warnings.
- Added explicit CI job timeouts and faulthandler stack dumps so a future non-progress condition terminates with diagnostic evidence.
- Corrected the source-SBOM job to catalog an extracted deterministic archive, validated SPDX generation on pull requests, and verified the post-merge Sigstore attestation for the exact source digest.
- Enabled repository-level SHA enforcement and a selected allowlist of 31 direct and observed transitive Action paths; active reruns of CI, Security, and Forensic CI pass under the hardened policy.
- Expanded `main` branch protection to 13 required CI contexts, including Python 3.11 and source SBOM, and enabled signed-commit and administrator enforcement.
### Final remediation verification
- Merged PR #95 as signed squash commit `8907a6db75cff2a3bd6a551ef7983f53bda17027` and the SBOM correction PR #96 as signed squash commit `43677edca6d39a2b4078187d3676d5a286627846`.
- Final GitHub Python 3.11.16 execution: `5,392 passed, 83 skipped in 64.34s`, `92%` line coverage, followed by a clean locked-runtime dependency audit.
- Remediation-baseline `main` CI passed all 14 jobs, including Python 3.11/3.12/3.13, formal verification, Market Hardening, source SBOM, Docker provenance/SBOM, and keyless image signing.
- Final Security workflow passed CodeQL, Bandit, dependency audit, Trivy, OSV Scanner, and Cargo Audit; Forensic CI also passed under the selected-action policy.
- Private Dependabot, code-scanning, and secret-scanning alert inventories remain unenumerated because the active integration token returns HTTP 403; this is recorded as missing authority rather than a zero-alert result.
### Institutional documentation and claim controls
- Added a six-volume institutional suite covering mechanistic architecture, cryptography and forensics, threat modeling, operations, regulatory review, and commercial procurement, plus a claim-evidence graph, unsupported-claims report, document-control record, and deterministic corpus audit.
- Corrected positive regulatory and evidentiary wording in code documentation: regex PHI handling is best-effort redaction, application sealed segments are not regulatory WORM, GxP objects are support hooks rather than validation, and software-generated integrity labels do not determine legal admissibility.
- Superseded the interim whole-response streaming buffer with the incremental bounded streaming contract documented above; aggregate concurrent memory remains deployment-dependent on configured per-stream queue/window limits and active stream count.
- Added `cbor2>=5.9.0` to runtime dependencies for deterministic DAG-CBOR evidence export after local dependency auditing detected advisories in the previously installed generation-only version.
### Formal and native WAL hardening
- Added bounded Z3, Lean, and TLC artifacts with pinned reproducible execution and explicit non-refinement boundaries.
- Serialized native WAL reserve/write/flush publication, added checked arithmetic and recovery CRC validation, and added concurrent/rejected-append regressions.
## [3.1.0] — 2026-08-18
### Product and documentation
- Repositioned the public product as an AI Governance and Evidence Gateway with a complete US-English README, repository map, buyer guide, product brief, commercial strategy, and explicit claim boundaries.
- Replaced stale v2.x security and commercial language with a current support policy, disclosure path, deployment boundary, licensing summary, procurement blockers, and assurance roadmap.
- Marked `Samples/` dashboards as static demo-only artifacts with synthetic telemetry; sample values are not runtime, customer, cryptographic, compliance, or capacity evidence.
### Security and evidence
- Added a versioned HMAC keyring with atomic reload, one active key, overlap verification keys, expiry, non-secret `key_id` metadata, and fail-closed initial loading.
- Added exporter metadata for the signing key ID used for compliance bundles.
- Added an injectable `fsync_fn` seam to the WAL ledger for deterministic authorized fault injection while retaining `os.fsync` as the production default.
- Expanded WAF critical coverage for persona overrides and added a pinned local corpus with observed bypass and false-positive metrics.
### Verification harnesses
- Added a backpressure/fsync-stall harness for offered 10k requests/s, durable request correlation, missing/duplicate evidence detection, latency percentiles, and WAL integrity. The retained run offered traffic for 0.25 seconds and committed 2,500 of 2,500 requests with zero failures, zero missing/duplicate IDs, valid chain integrity, and 836.3514210795984 ms p99 commit latency; accepted capacity is not claimed.
- Added WAF corpus reporting with corpus SHA-256, per-case verdicts, Wilson 95% interval, zero observed bypasses, zero false positives, and explicit HTTP/2/Nuclei non-execution boundaries.
- Added a three-instance local key-rotation exercise; 2,239 signatures were recorded with zero failed commits and zero unverifiable records. Secret-manager, orchestrator, and clock-skew acceptance remain open.
- Added a native ML-DSA timing harness with 1,000,000 samples per operation. `sign` met the declared non-detection threshold (`p=0.8521504207157158`); `verify` did not (`p=0.0`), so no constant-time claim is approved.
- Added regression tests for key rotation, WAF corpus behavior, and fsync fault injection.
- Documented that a local result is not a production SLO, accepted-capacity claim, universal WAF guarantee, constant-time proof, or certification.
### Final verification
- Final release checkout: `5,442 passed, 37 skipped, 47 warnings` in 68.08 s with `93.91%` line coverage; pytest exit status 0.
- Documentation reconstruction added the US-English developer quickstart, platform operator guide, architecture document, benchmark result record, rollback runbook, privacy boundary, compliance contribution map, and technical/security/procurement FAQs.
- Blocking static and supply-chain gates: Ruff check, Ruff format, Bandit, pip-audit requirements, pip-audit environment, `git diff --check`, Helm lint, and Cargo tests all exited status 0.
- The ML-DSA timing gate remains intentionally non-green for `verify` (`p=0.0`); the release blocks any constant-time verification claim and retains the residual risk in the public security documentation.
### Documentation boundary
- Framework references use NIST, W3C, CISA, IETF, HHS, ISO, EUR-Lex and AICPA sources as review lenses. The repository does not claim SOC 2, HIPAA, FedRAMP, EU AI Act conformity, GDPR legal basis, FIPS 140 validation or court admissibility.
### Versioning
- Bumped active Python, Rust, package, Docker, Helm, and script version anchors to `3.1.0`.
## [3.0.1] — 2026-08-17
### Security and evidence
- Upstream non-200 responses, circuit-open responses, and forwarding exceptions now commit signed durable request-response evidence before the terminal error is returned.
- Successful, streaming, and terminal error responses expose `X-Aegis-Evidence-Status: durable` together with request/session identifiers for external verification.
- Added regression coverage for chat and completions upstream errors, circuit-open behavior, and network forwarding faults.
### Performance and verification
- Added a live TCP workload harness covering mixed chat/health traffic, bounded concurrency, induced upstream latency, periodic 503 faults, and circuit-breaker opening.
- Validated 400-request steady traffic, 1000-request burst traffic, periodic 503 faults, and ten-request breaker opening against the local checkout with zero missing evidence-status headers in the valid runs.
- Bumped the Python package, Python entrypoints, Helm chart, Docker metadata, Rust crate, and maturin package to `3.0.1`.
### Verification
- Final checkout gate: `5374 passed, 80 skipped, 47 warnings`; Ruff lint/format, Bandit, pip-audit, and Helm lint exited with status 0.
- Coverage gate: `93%` line coverage measured by `pytest-cov`; residual warnings remain documented telemetry.
## 3.0.0 — 2026-08-14
This is a historical changelog entry; no corresponding public GitHub Release or tag is currently available.
### Security and evidence
- Production mode is now explicitly `strict` by default and rejects missing authentication, durable evidence, strong signing, request bounds, required kernel controls, or distributed rate limiting.
- Redis rate-limit failures raise `RateLimitBackendUnavailable` and are rejected at the HTTP boundary instead of failing open.
- Non-sandbox Seccomp failures raise; LSM exposes a fail-closed assertion for strict startup.
- The forensic ledger accepts `require_strong_signing` and rejects the ephemeral Ed25519 fallback when enabled. The proxy persists and fsyncs request/response evidence before returning a successful governed response.
### Performance and concurrency
- Response analysis is dispatched through a bounded worker queue and is no longer executed synchronously on the client-visible request path.
- Per-session analyzer state is serialized to prevent races in baseline, EMA, and previous-logit state.
- Streaming responses are bounded and committed before SSE emission.
### Configuration and supply chain
- Backend URLs and air-gap allowlist entries reject unsupported schemes, userinfo, malformed ports, and non-canonical entries.
- Enterprise lifespan uses the injected settings instance consistently.
- `cryptography` is constrained to `>=50.0.0,<51.0.0`; `requirements.lock` pins `50.0.0` with official PyPI hashes to remediate the audited `CVE-2026-69247` / `PYSEC-2026-3552` affected range.
- README, `.env.example`, and `DEPLOYMENT_GUIDE.md` now describe implemented behavior and residual risk instead of certification claims.
### Verification
- Isolated Python 3.12 baseline: `5373 passed, 80 skipped, 47 warnings in 24.17s`.
## [2.4.1] — 2026-06-24
### Summary
Commercial deployment release. Closes all remaining P0 security-theater items
identified in the 2026-06-24 roadmap audit; ships the complete cross-platform
abi3 wheel matrix; upgrades Helm chart to v2.4.1; introduces the enterprise
multi-vertical Docker Compose; and restructures COMMERCIAL.md with concrete
SLA tiers and AGPL §13 enforcement mechanics.
**Audit baseline at release:** 5,451 tests · 5 skipped · 95.18% branch
coverage · `ruff`/`mypy`/`bandit` clean · `cargo test` 26 passing · pyo3 0.29
/ `edition = "2021"` · 8-target abi3 wheel matrix (manylinux2014 + musllinux ×
x86_64/aarch64/armv7, macOS Intel/ARM, Windows MSVC) · all simulation-debt
entries driven to zero (`tests/test_no_simulation_markers.py` asserts 0).
### Security
- **De-simulated `CFIManager`** (ROADMAP P0.2). `aegis/core/cfi_manager.py`
previously hardcoded `is_cfi_enabled = True # Simulation result`, always
returning a positive CFI attestation regardless of the binary under inspection.
Rewritten with three real ELF detection tiers: LLVM CFI (`__cfi_check` /
`__cfi_prototype` symbols via `pyelftools`), GCC/LLVM unwind tables
(`.eh_frame` / `.eh_frame_hdr` sections), and Intel CET IBT + Shadow Stack
(`GNU_PROPERTY_X86_FEATURE_1_AND` in `.note.gnu.property`). Falls back to
`readelf`/`nm` subprocess if `pyelftools` is not installed. 18 tests including
KAT against the real Rust `.so` binary and malformed-file edge cases
(`tests/test_cfi_manager.py`).
- **De-simulated `MTEGuard`** (ROADMAP P0.2). `aegis/core/mte_guard.py`
previously fabricated ARM MTE support on every platform (`self._hardware_support
= True`) and simulated `PR_SET_TAGGED_ADDR_CTRL` success without issuing the
syscall. Rewritten with real `/proc/cpuinfo` `mte` flag parsing, `AT_HWCAP2`
bit 18 (`HWCAP2_MTE`) auxiliary-vector check, and a real `prctl(55, 1)` call
via `ctypes.CDLL("libc.so.6")`. Returns `False` on x86/non-ARM hosts. 14 tests
cover the no-hardware path and monkeypatched hardware paths; 2 ARM integration
tests skip cleanly on CI (`tests/test_mte_guard.py`).
- **De-simulated `DependencyAuditor`** (ROADMAP P0.4). `aegis/core/dependency_audit.py`
previously calculated `SHA-256(f"{name}_{version}_AUDITED")` as the audit hash
and re-computed the exact same string for verification — always passing. Replaced
with a real `pip-audit -f json` invocation (`DependencyAuditor.scan()` returning
`VulnerabilityFinding` dataclasses) and real `importlib.metadata` RECORD hash
verification using URL-safe base64 (PEP 658). `DependencyInternalizer.verify_supply_chain()`
now delegates to both. 24 tests including tamper detection and real certifi hash
match (`tests/test_dependency_audit.py`). Also registered `slow` pytest mark.
- **De-simulated `XDPDynamicSegmenter`** (ROADMAP P0.3). `aegis/core/xdp_dynamic_segmentation.py`
previously added IPs only to an in-memory Python `set` and logged
`# Simulation: eBPF_map_update(...DROP)` — no packet was ever dropped at the
kernel level. Replaced with `_FirewallBackend` that auto-detects nftables (`nft`)
→ iptables → NONE and issues real kernel rules (`nft add/delete element` or
`iptables -I/-D INPUT -s <ip> -j DROP`). `block_ip_immediately()` returns `True`
only when a kernel rule is installed; the application-layer-only fallback path logs
an explicit "APPLICATION-LAYER ONLY" advisory. 27 tests cover backend detection,
idempotency, kernel failure fallback, and zone blackhole/active transitions
(`tests/test_xdp_dynamic_segmentation.py`).
- **Hardened `DependencyAuditor` against B607 partial-path subprocess start**.
`pip-audit` is now resolved via `shutil.which()` in `__init__`; a `DependencyAuditorError`
is raised immediately if the tool is absent — no partial-path fallback. `subprocess.run`
annotated `# noqa: S603`. Test updated to patch `shutil.which`.
- **De-simulated `sandbox_l1.py`** (ROADMAP P0.2). Previously skipped the
entire rule-addition step ("we simulate the rule addition") and called
`seccomp_load` with zero allowlist rules and `SCMP_ACT_KILL` — any loaded
filter would have immediately killed the process. Rewritten with real
`seccomp_syscall_resolve_name` + `seccomp_rule_add` calls via ctypes for
every syscall in the allowlist; default action changed to
`SCMP_ACT_ERRNO(EPERM)` (safe); `apply_filter()` returns `True` only when
`seccomp_load()` succeeds; `build_filter_without_loading()` validates the
filter safely in tests. 18 tests including real subprocess filter load and
mocked-library failure paths (`tests/test_sandbox_l1.py`).
- **De-simulated `panic_mode.py`** (ROADMAP P0.2). `_zeroize_critical_memory` previously only
logged "Zeroizing..." and "complete" with a `# Simulation` comment and no actual write.
Replaced with real `ctypes.memset` over registered ``bytearray``/``memoryview`` buffers;
``register_sensitive_buffer()`` added so callers can enlist secret-bearing buffers.
`_isolate_network` previously only logged with `# Simulation: calls XDPDynamicSegmenter...`.
Replaced with real subprocess calls to `nft add rule ... drop` or `iptables -P INPUT/OUTPUT/FORWARD DROP`;
returns `False` and logs a CRITICAL advisory when no kernel firewall tool is available.
- **De-simulated `root_ca_gateway.py`** (ROADMAP P0.2). `import_signed_certificate`
had a `# Simulation of decoding the physical transfer` comment despite doing real
JSON decoding. Comment removed. `fetch_certificate(request_id)` previously ignored
the `request_id` parameter ("In a real system, we would match the request_id");
now iterates `_inbound_buffer` and matches by `cert.ca_serial == request_id`.
- **De-simulated `memory.py`** (ROADMAP P0.2). `HardenedMemoryManager.initialize_hardened_allocator`
previously set `_allocator_type = "mimalloc"` via a "Simulation mode" comment
even when neither `libmimalloc.so` nor `libhardened_malloc.so` appeared in
`/proc/self/maps`. Now logs a warning and sets `_allocator_type = "standard"`
honestly when no hardened allocator is detected.
- **De-simulated `memory_invariants.py`** (ROADMAP P0.2). Previously computed
golden hashes from a `f"STATE_{start}_{end}"` string (always matching itself,
never detecting any modification). Rewritten with `_read_range()` reading the
actual bytes from the process's own virtual address space via `/proc/self/mem`,
and `_hash_range()` computing real SHA-256 digests. `register_invariant()`
returns `False` when the range is unreadable. `verify_invariants()` re-reads
each range and detects real modifications; unmapped pages after registration
are logged as CRITICAL. 16 tests including real ctypes buffer tampering
detection, unmapped-address handling, and mock-based unreadable-after-register
coverage (`tests/test_memory_invariants.py`).
- `tests/test_no_simulation_markers.py` — `KNOWN_SIMULATION_DEBT` shrunk from
23 → 16 as `cfi_manager.py`, `mte_guard.py`, `dependency_audit.py`,
`xdp_dynamic_segmentation.py`, `sandbox_l1.py`, `memory.py`, and
`memory_invariants.py` are removed. Debt-count assertion updated to `== 16`.
- **Removed two fake post-quantum modules that manufactured false cryptographic
assurance** (ROADMAP P0.1). `aegis/core/pqc.py` advertised "ML-DSA (Dilithium)"
signatures but computed HMAC-SHA512 padded with random bytes; `aegis/core/
pqc_provider.py` was a SHAKE-256 "simulation" whose `verify()` accepted **any**
128-byte signature regardless of message. Both are deleted.
### Added
- `aegis/core/pqc_signer.py` — the single real `PQCSigner` over genuine ML-DSA-65
(FIPS 204) via the Rust `pqcrypto-mldsa` backend: real keypair (pk 1952 / sk
4032 / sig 3309 bytes), `sign`/`verify`, honest `backend` reporting (never a
simulation label), `require_real` mode, and no simulated fallback. 20 KAT-style
tests prove forgery, tamper, wrong-key, and truncation rejection
(`tests/test_pqc_signer.py`).
### Changed
- `aegis/core/pqc_tls.py` rewritten as a **real** hybrid post-quantum key
exchange: X25519 ECDH (`cryptography`) composed with ML-KEM-1024
(`aegis.core.mlkem_session`) via `HKDF-SHA256`, TLS-1.3-style initiator/
responder protocol. The previous module's "X25519" and "Kyber" secrets were
both `sha256(priv ‖ pub)` — not a Diffie-Hellman and not post-quantum. The new
module refuses to downgrade to classical-only when ML-KEM is unavailable. 14
tests prove key agreement and tamper-breaks-agreement (`tests/test_pqc_tls.py`).
- `aegis/core/artifact_signing.py` rewritten with two honestly-labelled **real**
schemes — `HMAC_SHA512` and real `ML_DSA_65` (via `PQCSigner`) — fixing a
comment that labelled HMAC as ML-DSA and a verify path that re-signed instead
of doing asymmetric verification with the published public key. 10 tests
(`tests/test_artifact_signing.py`).
- `tests/test_no_simulation_markers.py` — a **ratchet** CI guard: no new `aegis/`
module may introduce a `# SIMULATION` marker, and de-simulated modules must be
removed from the 23-entry `KNOWN_SIMULATION_DEBT` allowlist (shrink-only).
### Fixed
- `tests/test_determinism.py::test_no_outlier_exceeds_500us` now skips on shared
CI runners (`HERMES_SANDBOX`/`CI`), where multi-tenant kernel-scheduler
preemption produces millisecond-scale dispatch outliers unrelated to the code
under test (a 90 ms outlier was observed). The hard <500 µs bound remains
enforced on dedicated CPU-isolated hardware.
## [2.4.1] - 2026-06-24
Release-hardening and capability-expansion release. Twenty roadmap controls were
implemented and tested across all five target domains (Defense, Healthcare,
Industrial, Enterprise HA, Forensics), advancing the roadmap scorecard to
151/193 (~78%). All 4,575 tests pass (3 skipped) at 94.79% coverage.
### Added
**Defense & Government (Domain 1)**
- ML-KEM-1024 (FIPS 203) session-key bootstrap (`aegis/core/mlkem_session.py`).
- Cross-domain solution (CDS) guard with classification-domain transfer
sanitization (`aegis/core/cds_guard.py`).
- Offline license validation — HMAC-SHA256 over canonical JSON, no phone-home,
enforced key separation (`aegis/core/offline_license.py`).
- Pinned CA bundle for air-gapped signature verification, SHA-256 DER
fingerprints, no runtime CA fetch (`aegis/core/pinned_ca_bundle.py`).
- LSM confinement guard — AppArmor/SELinux detection and advisory enforcement
(`aegis/core/lsm_guard.py`, `deploy/apparmor/aegis.profile`).
- Hardware-bound session tokens (TPM 2.0 / software backends, HMAC-SHA256
binding) (`aegis/core/hardware_token.py`).
- Rust build hardening — full RELRO, noexecstack, embedded size profile
(`aegis_rust_v2/.cargo/config.toml`).
- **Fully air-gapped Docker image** — `deploy/docker/Dockerfile.airgap` with
sha256-pinned base, `pip install --no-index --find-links /wheels`, vendored
wheel workflow (`scripts/vendor_wheels.sh`), and `make docker-airgap`.
**Healthcare & Life Sciences (Domain 2)**
- GxP Installation/Operational Qualification (IQ/OQ) protocols with JSON evidence
artifacts (`tools/qualification/iq_checks.py`, `oq_checks.py`).
**Industrial Automation & OT (Domain 3)**
- Real-time scheduling via `sched_setscheduler` (FIFO/RR/DEADLINE)
(`aegis/core/rt_scheduler.py`).
- CPU affinity pinning via `sched_setaffinity` (`aegis/core/cpu_affinity.py`).
- Gossip-protocol WAL synchronization for disconnected edge nodes
(`aegis/core/gossip_wal_sync.py`).
- CRDT audit-node ordering with vector clocks for deterministic distributed
ordering (`aegis/core/crdt_ordering.py`).
**Enterprise Hyperscale & HA (Domain 4)**
- Raft consensus state machine (Ongaro & Ousterhout 2014) — leader election,
log replication, SHA-256 entry-hash tamper detection (`aegis/core/raft_consensus.py`).
- Split-brain prevention via monotonic fencing tokens (Kleppmann 2016),
gating every WAL write (`aegis/core/split_brain.py`).
- Kubernetes operator — `AegisProxy` CRD + kopf controller
(`deploy/k8s/aegis-operator/`).
**Advanced Forensics & WAF (Domain 5)**
- Semantic similarity clustering of jailbreak families (SimHash + Hamming)
(`aegis/core/semantic_sim_clustering.py`).
- Token-split reassembly WAF detector for boundary-split attack patterns
(`aegis/core/token_split_detector.py`).
- Court-ready forensic PDF report with SHA-256 seal, no external PDF dependency
(`aegis/core/forensic_pdf_report.py`).
- Anonymized threat-intelligence sharing with STIX 2.1 indicator export
(`aegis/core/ti_sharing.py`).
**Tooling & Benchmarks**
- New `benchmarks/bench_crypto_audit.py` — measures HMAC signing, durable
`commit_forensic()`, and `verify_integrity()` throughput (documented as
Claim 4 in `docs/BENCHMARKS.md`).
### Changed
- Bumped version to 2.4.1 across `pyproject.toml`, `aegis`, `aegis_server`,
Docker images, README, and deployment guide.
- README test evidence and badges refreshed to 4,575 passing tests.
### Fixed
- **Rust extension test linking:** `aegis_rust_v2/Cargo.toml` had
`default = ["extension-module"]`, which made `cargo test` omit the libpython
link and fail with undefined `Py*` symbols. Changed to `default = []`;
maturin still enables `pyo3/extension-module` via `[tool.maturin] features`,
so production wheels are unaffected. (`cargo test --release` → 23 passed.)
- **Version drift:** `aegis_server.__version__` and the standard
`deploy/docker/Dockerfile` were stale at 2.3.0; the `aegis_server` `/health`
and `/ready` endpoints now report the correct release version.
- **Forensic tool environment bug:** `tools/forensic/forensic_checks.py` invoked
`cargo test` with an empty environment (no `PATH`), which prevented the Rust
toolchain from resolving; it now inherits the process environment.
- Resolved 30 lint findings across recently added consensus, qualification, and
test modules (import ordering, unused imports, `StrEnum` migration, ambiguous
identifiers); full repository is now `ruff` lint- and format-clean.
- Skipped a flaky concurrent-load scheduling-jitter test on shared CI runners
(`tests/test_determinism.py`), which require dedicated real-time hosts.
### Security
- `aegis_server.crypto` previously imported `hvac` eagerly at package level,
breaking the HMAC-only compliance-export path on installs without the optional
`vault` extra; `VaultSigner` is now lazy-imported. (Carried from 2.4.0.)
## [2.4.0] - 2026-06-21
### Added
- Broad roadmap expansion across Defense, Healthcare, Industrial, Enterprise,
and Forensics domains (SCIM 2.0, RBAC/ABAC zero-trust, LDAP/AD, WORM ledger,
RAG injection scanning, SLO burn-rate alerting, DFIR export formats, and more).
### Fixed
- `aegis_server.crypto` eagerly imported `hvac` at package level, breaking the
HMAC-only compliance export path on installs without the optional `vault`
extra; `VaultSigner` is now lazy-imported. *(Severity: Low)*
## 2.3.0 - 2026-06
This is a historical changelog entry; no corresponding public GitHub Release or tag is currently available.
### Fixed
- mTLS settings were defined in `AegisSettings` but never applied to the uvicorn
listener or the upstream `httpx` client. *(Severity: High)*
- `ResponseAnalyzer` thresholds were hardcoded, ignoring `AegisSettings`;
alerting could not be tuned at runtime. *(Severity: Medium)*
## [2.2.0] - 2026-05
### Fixed
- Audit chain signing key was derived from the first sorted API key; unannounced
rotation silently invalidated the chain. *(Severity: High)*
- `/docs` and `/redoc` were exposed unconditionally in all deployment modes.
*(Severity: Medium)*
- `prev_hash` always pointed to the genesis node due to a wrong `ORDER BY`
direction in `list_nodes()`. *(Severity: Critical)*
- Concurrent `BackgroundTask` writes could fork the audit chain (no chain lock).
*(Severity: Critical)*
[3.0.1]: https://github.com/JuanLunaIA/aegis-latent-core/releases/tag/v3.0.1
[2.4.1]: https://github.com/JuanLunaIA/aegis-latent-core/releases/tag/v2.4.1
[2.4.0]: https://github.com/JuanLunaIA/aegis-latent-core/releases/tag/v2.4.0
[2.2.0]: https://github.com/JuanLunaIA/aegis-latent-core/releases/tag/v2.2.0
## Related documents
- [`README.md`](README.md)
- [`docs/CLAIMS_MATRIX.md`](docs/CLAIMS_MATRIX.md)
- [`docs/benchmarks/BENCHMARK_RESULTS.md`](docs/benchmarks/BENCHMARK_RESULTS.md)
- [`docs/SECURITY_ASSURANCE_ROADMAP.md`](docs/SECURITY_ASSURANCE_ROADMAP.md)
- [`SECURITY.md`](SECURITY.md)