Skip to content

Build1 publisher3 min readPublished

trelix's most useful release detail is an exit code, not the audit log

Six new feature areas landed in v3.0.0, every one off by default, and a verify command that refuses to call an unreadable database clean.

The Engineer · Build desk

Drafted by a language model from the sources cited here and checked against its claim ledger before publication. How we use AISend a correction

Illustration accompanying trelix's most useful release detail is an exit code, not the audit log
Generated illustration

What happened

  • The span from trelix v2.11.0 to v3.1.1 is six releases (v2.11.1, v2.12.0, v3.0.0, v3.0.1, v3.1.0, v3.1.1), the last dated 2026-08-15.
  • That span comprises 68 commits, 137 files changed, +19,829/-1,211 lines.
  • Pointed at something SQLite cannot open, trelix audit verify exits 2 rather than 0, because 'I could not check' and 'I checked and it is clean' must never collapse into the same green build.
  • After seeding three events and running sqlite3 audit.db "UPDATE audit_log SET principal='attacker' WHERE id=2", trelix audit verify --db audit.db reports the audit chain as TAMPERED with first divergent entry id 2, and exits with code 1.
  • Deleting the newest row instead is still caught, naming id 3, even though the surviving rows form a perfectly valid chain.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

trelix closed a six-release run with v3.1.1, dated 2026-08-15: 68 commits, 137 files changed, +19,829/-1,211 lines since v2.11.0 [1][2]. The write-up on dev.to leads with a hash-chained audit trail, but the more transferable decision is smaller than any feature in the release: `trelix audit verify` exits 2 when it cannot open the database, rather than 0 [3].

The demonstration is short. Seed three events, reach past the application with `sqlite3 audit.db "UPDATE audit_log SET principal='attacker' WHERE id=2"`, and verify reports the chain as tampered, names id 2 as the first divergent entry, and exits 1 [4]. Delete the newest row instead and it names id 3, even though the surviving rows form a valid chain [5]. Point it at a file SQLite cannot open and it exits 2 [3]. That is three outcomes where most check commands ship two, and the third is the one that keeps a broken check from being reported as a passing one [6].

The truncation catch needs its own machinery. Each row's `entry_hash` is `sha256(prev_hash || canonical_json(content))`, canonicalised with `sort_keys=True` and `separators=(",", ":")` [7]. Exactly eleven columns are hashed, in a fixed `_CONTENT_COLUMNS` tuple, and the DB-assigned `id` is deliberately excluded because a writer cannot hash a value it does not have until after the INSERT [8]. A hash chain is therefore structurally blind to a deleted tail [9], so an `audit_meta` table carries a running `count` and `head_hash`, upserted inside the same transaction as the insert, and `verify_chain` checks both against the chain it just walked [10]. `audit_log.id` is `INTEGER PRIMARY KEY AUTOINCREMENT` rather than a rowid alias, so a delete-then-refill cannot close the gap it made [11].

The trail is a separate SQLite file, defaulting to `<cwd>/.trelix/audit.db` and never the index database, on the grounds that the index is disposable and a trail that vanishes on reindex is not a trail [12]. It is stdlib `sqlite3`, 54 tests, and fail-open by default via `AuditConfig.fail_closed: bool = False` so a full disk is not an outage [13]. `trelix audit export --format ndjson` writes one JSON object per line for SIEM ingestion [14].

The discipline that costs more is the defaults. v3.0.0 carries six new feature areas plus an opt-in FTS5 declaration boost [15], and all of them ship off: `TRELIX_AUDIT_ENABLED`, `TRELIX_OIDC_ENABLED`, `TRELIX_LLM_THINKING_ENABLED` and `TRELIX_RETRIEVAL_COMPRESSION` all false, `declaration_boost_enabled` false, and `context_token_budget` still the same `12_000` integer as v2.12.0 [16]. A default v3.0.0 install assembles context byte-identically to a default v2.12.0 install, and there is a test asserting it rather than a release note claiming it [17]. The major bump is scope, not breakage [18].

The SSO half is scoped the same way: trelix is a resource server, not an OIDC client, with no `/login`, no `/callback`, no code exchange and no refresh [19]. Algorithms are asymmetric-only, defaulting to RS256 and ES256, and the check runs in `OidcVerifier.__init__`, again against the unverified JWS header in `authenticate`, and a third time in `jwt.decode` [20]. The test worth reading skips trelix's own header gate and signs a token with HS256 using a real 2048-bit public key's PEM as the HMAC secret, then asserts `jwt.decode` alone refuses it [21]. `Principal.principal_id` is subject plus issuer, never email, because email-keyed identity is an account-takeover primitive [22].

Two things to watch. Fail-open is the seam: because a dropped write never reaches the insert transaction that increments `audit_meta.count`, an event lost to a full disk leaves the chain and the count mutually consistent, so verify will call it clean [23]. And defaults tend to drift after the release that announces them, so the number worth rechecking at v3.2 is whether `context_token_budget` is still 12,000 and whether the four false flags are still false [16].

Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories