Changelog

Release-by-release changes to the AGLedger Server. Versions here match the image and chart tags on Docker Hub and the tags on the install repository, whose own changelog covers the installer rather than the Server.

Issue numbers reference the private engineering tracker and are kept for traceability; F-numbers are testbed findings.

1.3.42026-07-27

Federation delivery and crash-recovery hardening, driven by four rounds of mesh testing, plus audit-vault immutability and throughput work on the compliance and fan-out paths.

Added

  • Multi-arch release images (#961) - linux/amd64 and linux/arm64, with OCI source/description labels and a corrected Docker Hub page.
  • Zero-row recovery sizing knobs (#1009) - AGLEDGER_FEDERATION_ZERO_ROW_HORIZON_MINUTES (360) and AGLEDGER_FEDERATION_ZERO_ROW_BATCH_SIZE (50) are configurable. The horizon is the abandonment boundary for a crash-orphaned record, so operators sizing recovery for a high-traffic Server now have a lever. Documented under Federation Recovery Sweeps in /llms-full.txt.
  • **agledger_federation_zero_row_projections_excluded** (#1010) - a positive operator signal that inbound projections are being correctly held out of the recovery sweep. The companion agledger_federation_outbound_projection_skipped_total stays at 0 by design, so an increment there is a regression.
  • Configurable socket inactivity timeout (#981) - raised to 60s and tunable, with severed requests logged instead of dropped silently.
  • **OpenVEX not_affected statements** (#963, #964) - unfixable base-image CVEs ship with signed justifications, and /pre-deploy checks the attestation for currency so findings cannot accumulate unanswered between releases.

Fixed

  • Inbound projections are no longer re-federated (#1006) - a record received from a peer is a read-only view the origin owns. On the shipped AGLEDGER_DEFAULT_SHARE=true default, every projection a Server had ever received was a standing recovery candidate, producing origin-bound no-op deliveries and permanent 404 legs at third peers that never reached the DLQ. Testbed measured a pool of 9,403 on a Server holding ~21k records.
  • Recovery drains oldest-first (#1006) - the sweep serves the candidate closest to aging out of the window. Newest-first handed every slot to the rows furthest from expiry, so on a Server taking continuous federation traffic a genuine crash orphan was never reached and then left the window for good.
  • Crash-recovered records keep their federation events (#1003) - records recovered after a crash no longer permanently lose their federation.record.state_changed event.
  • Recovery sweep only considers records that can actually federate (#1002) - the share filter matches resolveEffectiveShare, so a suppressed population can no longer occupy every batch slot and starve the records that need recovering.
  • Outbound federation ceiling raised (#1005) - FEDERATION_OUTBOUND_BATCH_SIZE defaults to 4, lifting per-pod drain from 16 to 64 legs/sec. The drain had been pinned by the poll cadence rather than by the database or the peers, leaving peers minutes behind a bulk share with the stack idle.
  • Outbound fan-out batches its per-peer writes (#929) and post-commit dispatch is bounded (#989), with burst-constant lookups memoized.
  • **chainIntegrity reads are batched on the compliance export** (#998) so the regulator packet clears the socket window at the 10k row cap.
  • **vault_checkpoints is append-only** (#995) - checkpoints cannot be rewritten, and a checkpoint now names which chain it anchors.
  • Export row cap is surfaced, not silently applied (#982) - a truncated compliance export sets truncated: true with the full match count.
  • **Unbindable parent.* expression paths rejected at registration** (#983) - with per-rule failure reasons and documented helper semantics, instead of failing opaquely at evaluation.
  • **agentCardUrl and peerUrl held to the webhook URL guard** (#997).
  • **Duplicate type names disambiguated on /llms.txt** (#988) - by publisher and version.
  • Vault dump streams through a server-side cursor (#986) - so a large vault no longer builds the whole result set in memory.
  • Helm: traffic drains on rolling restarts behind an ALB (#985), and the migration Job honors scheduling constraints, defaulting to the api pod's (#984).
  • Three testbed findings F-819, F-820, F-821 (#994).
  • **Transitive fast-uri bumped to 4.1.1** (#990) for GHSA-v2hh-gcrm-f6hx.

1.3.32026-07-18

Audit-vault truncation hardening.

Fixed

  • Terminal-truncation detection for the per-org schema chains (#953, #955) - each org's schema chain is checkpointed and anchored, so deleting the chain tail (or the whole chain) surfaces in verification instead of reading as an intact shorter chain.
  • Platform-ops chain checkpoint + anchor (#954) - the platform-ops chain gets the same terminal-truncation detection.
  • Schema chain truncated to empty is flagged (#957, #958) - a chain emptied when no checkpoint survives verifies as truncated, not vacuously valid.

1.3.22026-07-13

Deterministic-gate, federation, and records hardening from the testbed sweep (F-799 through F-805), plus system TIME_OUT modeling in the state machine and new response fields agents were already reaching for.

Added

  • **signedStatement.signedAt on record responses** (#877) — the signed instant is surfaced directly, so a caller reads the moment the Server signed the statement without re-deriving it from the envelope.
  • **recordStatus on the verdict response** (#876) — a verdict reply carries the record's resulting state, so the caller doesn't need a follow-up GET to see where the record landed.
  • Resolvable principal holds (#913) — a principal-mode hold synthesizes a rollup completion and emits a record.gate_held event, so a held cascade is observable and resolvable rather than silently stuck.
  • **AGLEDGER_FEDERATION_SCHEMA_REF_POLICY strict mode** (#910) — closes the inbound schemaRef bypass; strict mode requires the referenced type to resolve locally before a federated record is accepted.
  • Inline completion evidence on audit-export (#878) — the audit export inlines completion evidence and documents the evidenceHash binding, so an offline auditor can bind evidence to the chain without a second fetch.
  • Vertical recipes — employment/FCRA adverse-action, software-delivery, and HMRC MTD-VAT recipes added to deploy/recipes/.

Fixed

  • System TIME_OUT modeled in the state machine (#916, #919) — PROPOSED/REGISTERED past their deadline transition to TIMED_OUT; the sweep now covers rework states, and the in-transaction TIME_OUT guard commits its terminalize in a fresh transaction (#917) with a non-stale reason (#925).
  • **Reachable graceSeconds intake** (#921, #922, #923) — grace-period seconds are accepted end-to-end and malformed grace input is rejected rather than silently coerced.
  • Terminal-reason labels are a closed typed set (#909) — reasons come from a fixed enum, not free prose; precise recovery of the terminal reason and dedup of enforcementOverrides in the schema (#928).
  • **Bulk create honors the batch-level Idempotency-Key header** (#927), plus bulk field parity and a dispute terminalReason leak fix (#926).
  • Gate fail-closed hardening — unresolvable asyncRuleIds fail the gate closed to match the sync branch (#912); cascade-rollup dead-skip, principal-mode DLQ auto-reject, and held-cascade event pre-fire (#905); tolerance-key intake/engine parity and a single source for the tolerance key (#895, #908).
  • Federation counter-signature verification — inbound Settlement Signal counterSignatures are verified against the pinned peer key, with idempotency-replay re-emits suppressed (#892, #903).
  • Vault fails closed on per-entry chain signature verification (#891), with signing-key drift detection and checkpoint anti-starvation (#896).
  • **Post-commit callback queue on withTransaction** (#915) — rollup and transition emits defer to commit, so a rollback drops the emit instead of leaking a webhook.
  • A2A unmapped intent actions fail closed (#907) — SUPPORTED_ACTIONS is pinned to INTENT_SCOPES.
  • **Provisioning normalizes recordTypes: ['*'] to NULL on the YAML path** (#875).

1.2.02026-07-04

Gate expressiveness, webhook routing, and the PG18 upgrade. Six deterministic-gate and provisioning fixes from the testbed wave, plus new vertical recipes.

Added

  • **recordTypes webhook subscription filter** (#861) — subscriptions can pin the record types they receive, for type-segregated routing instead of one firehose per event type.
  • **fieldMapping.maxTolerance** (#860) — a rule can pin or forbid per-record tolerance, so a principal-supplied tolerance can't widen a gate beyond the type's ceiling.
  • Parent record timestamps in gate expression rules (#865) — expression rules can bind the parent record's createdAt / activatedAt / fulfilledAt instants (snake_case aliases included), so a child gate can reason over when the parent progressed. *Corrected 2026-07-12 (#890): an earlier version of this entry described these as signed/verdict instants; the bound values are the engine-stamped row timestamps, and no verdict instant is exposed.*
  • **rulesConfig echo on schema writes; /accept rationale aliases** (#855, testbed F-795/F-796) — schema create/update responses echo the resolved rulesConfig; the accept route takes the rationale-field aliases agents were already sending, and PROPOSED responses carry body hints.

Fixed

  • **Native uuidv7() after in-place PG17 → PG18 upgrade** (#862) — adopts PostgreSQL 18's built-in uuidv7(), retiring the polyfill. A rebase in that change dropped record_types DDL and an expiry index; restored in #864, which also drops a write-only events index.
  • **Reject gateMode: auto override on rules-less principal-default types** (#859) — a record can no longer downgrade a principal-gated type to auto-settle when the type defines no rules to evaluate.
  • **Duplicate fieldMapping ruleId fails closed** (#853, testbed F-794) — colliding rule IDs are rejected at registration; the engine fails closed rather than silently evaluating one of them.
  • Provisioning validation batch (#858, #866, #867) — rules validation, manifest description parity, stale terminalReason, register-API top-level schema shape, and rejection of unknown keys.

Deployment

  • Vertical recipes — payments-disputes and content-moderation (#841) added to the install examples.

1.1.02026-06-25

Supply-chain assurance and integrity coverage. The release adds a malware-scan attestation and a signed conformance corpus, and closes an integrity-projection gap on terminal verdicts.

Added

  • **Terminal verdict covered by ?integrity=true** (#811) — the projection-drift check now re-verifies the terminal rollup verdict (aggregateOutcome), so an out-of-band edit to a settled record surfaces as verified: false.
  • Lifecycle hints on schema list + gate outcome on completion — the schema-list rows carry lifecycle hints and completion responses surface the gate outcome, both pinned by regression tests.
  • **Terminal OVERFLOW_REJECT settlement signal + tolerance-assisted settle disclosure** (#778, #824) — an overflow rejection now emits a terminal signal, and a settle that only passed via tolerance discloses that.

Deployment

  • Malware-scan attestation (ClamAV) + signed conformance corpus (#828, #832) — the release pipeline attaches a ClamAV malware-scan attestation and publishes the signed conformance corpus to the public install release; supply-chain surfaces lead with OpenSSF-aligned framing (#830).
  • **agl-deploy.sh remote orchestrator** (#820, #822, #823) — eval/dev remote bring-up, with distro-Docker fallback handling.
  • Vertical recipes — insurance, finance-KYC, and healthcare prior-auth (#834) added to the install examples.

Fixed

  • Installer surfaces AGLEDGER_EXTERNAL_URL as the signed issuer in the completion banner (#814); dev-only vite/esbuild advisories patched; stale version examples in deploy docs refreshed (#807).

1.0.32026-06-22

Second post-GA patch. CVE patches, the testbed handshake batch, and the Semgrep SAST gate moved to /pre-deploy.

Added

  • Optional grace window on key rotation (#793) — a rotated signing key can keep verifying for a configurable window instead of cutting over instantly.
  • **Per-claim, offline-verifiable evidence pack on /compliance/export** (#771) — each claim carries its own offline-verifiable evidence.
  • **if/then/else in custom schemas** (#761) — conditional subschemas are now allowed in registered types.

Fixed

  • Testbed handshake batch (#787, #795) — idempotency, overflow-terminal handling, on-behalf-of provenance, schema discovery, rationale aliases, OIDC error mapping, ops-summary anchoring reconcile, and scope docs.
  • **AGLedger-On-Behalf-Of honored on the verdict route** (#762).
  • pg-boss adapter hardened against silent state-value renames (#789).

Security

  • CVE patches before v1.0.3 — undici 8.5.0, protobufjs 7.6.4 (#779); OTel stack to 0.219.0 / core 2.8.0, fixing CVE-2026-54285 (#785).
  • **Semgrep SAST moved from CI to the /pre-deploy release gate** (#799, #800); runtime image slimmed (corepack/yarn/npm dropped, #797, #798, #805).

1.0.22026-06-10

Migration re-baseline (one-time, zero customers)

  • **001_consolidated.sql regenerated to be the only migration file again.** The v1.0.0 freeze accidentally shipped around a stray numbered migration (002_default_gate_mode.sql, landed two days pre-GA) and v1.0.1 added 003_record_updated_entry_type.sql; both are folded into the regenerated baseline (verified: pg_dump schema from the new single file is identical to the old 001+002+003 path). With zero customer installs this is the last moment the file set can be collapsed; the freeze is re-asserted as of this release and the window is closed.
  • Upgrade note: a database migrated on v1.0.0/v1.0.1 (none known to exist) hard-errors on the baseline checksum by design (LAUNCH_FROZEN, empty rebase map) — do a fresh install.
  • Policy going forward: at most ONE new numbered migration per release; unshipped deltas fold into the cycle's file.

1.0.12026-06-10

First post-GA patch. Agent-recovery surface (queue discovery + actor-aware signals), tamper-evident record reads, anchoring posture, and spec-accuracy fixes from the blind-agent review.

Added

  • **GET /v1/records?actionable=true** (#731 / testbed F-756) — the one-call "what awaits my action right now" recovery query for agent keys. Returns every record whose awaitingActor is the caller's structural side, across all statuses (open proposals, ACTIVE work, revision requests, counter-proposals/verdicts). Composes with ?role=; admin/platform keys get a 400 explaining the per-row awaitingActor alternative. awaitingActorSql() is pinned to deriveAwaitingActor by a 432-combination equivalence sweep against real PostgreSQL. /llms.txt recovery guidance rewritten around the canonical query.
  • **GET /v1/records/{id}?integrity=true** (#732) — tamper-evident record reads. Re-verifies the full audit chain (hashes, signatures, checkpoint cross-check) AND cross-checks the served row against what the verified chain asserts; out-of-band row edits surface as verified: false, reason: record_projection_drift, driftFields[]. Sibling fix: PATCH /v1/records/{id} (DRAFT-stage edits) now notarizes a RECORD_UPDATED chain entry (003_record_updated_entry_type.sql) — previously the only API mutation of contract content with no signed trace.
  • Tunable anchor cadence + posture visibility (#736) — VAULT_ANCHOR_INTERVAL_MINUTES (5–2880, default 360) drives the checkpoint+anchor sweep; production boot warns (warn-and-proceed) while VAULT_ANCHOR_ENABLED=false; posture surfaced at GET /v1/admin/ops-summary (vault.anchoring) and as a fourth protection layer in /llms-full.txt.

Fixed

  • Actor-aware recovery signals on pre-activation records (#730 / testbed F-755). deriveAwaitingActor returns principal for DRAFT/REGISTERED (every forward action is principal/org-admin-only) and is acceptanceStatus-aware (counter-proposed awaits the principal). Performer-only viewers get wait-guidance nextSteps on REGISTERED / accepted-PROPOSED / COUNTER_PROPOSED instead of principal-only steps that 403.
  • Gated lifecycle taught from the API surface (#734). Completion-on-pre-ACTIVE 422s carry viewer-filtered action verbs in allowedActions plus a recoveryHint spelling the exact transition sequence for the caller's situation; DRAFT self-commitments get register → activate guidance (no counterparty to propose to); humanOversight is one strict Art. 14 shape everywhere (now also on POST /v1/records/{id}/ai-impact-assessment) and any validation failure returns the full shape in recoveryHint in one shot.
  • Published spec accuracy (#716, #717, #718). POST /v1/admin/orgs (dev/test multi-Org only, never registered in production) dropped from the canonical OpenAPI spec; the webhook eventTypes enum documents that it is exactly the subscribable set and explains the /v1/events superset; dev boot without API_KEY_SECRET warns about the fallback-secret 401 footgun.
  • Per-process OTel service name (#725). API and worker entrypoints declare agledger-api / agledger-worker (operator-supplied OTEL_SERVICE_NAME still wins); unwired env vars dropped from .env.example.
  • install.sh splits the tag off --image so it can't compose an invalid ref (#722); /llms-full.txt wording accuracy (#743).

Deployment

  • CodeBuild fully retired (#720, #723). Release images reach internal ECR only via deploy/scripts/mirror-ecr.sh (mirror of the signed Docker Hub release, not a rebuild); the internal :dev image is built on demand by deploy/scripts/build-dev.sh (--pull keeps the runtime base CVE-current).

1.0.02026-06-08

General-availability release. The migration baseline is now frozen for the life of the major version.

Migration freeze (GA)

  • Baseline frozen at v1.0. 001_consolidated.sql is immutable; the migrate runner refuses to rebase any applied migration's checksum. AGLEDGER_LAUNCH_FROZEN=true ships by default (helm configmap, base deploy/compose/docker-compose.yml migrate service, and .env.example), and KNOWN_CHECKSUM_REBASES is now permanently empty — even without the flag, every checksum mismatch is a hard error. Every schema delta from here on is a new numbered migration (002_*.sql, …), forward-only and idempotent.
  • The two pre-launch auto-rebase happy-path tests in migrate.integration.test.ts are retired (the rebase path is unreachable with an empty map); the throw-on-mismatch and LAUNCH_FROZEN-refusal tests remain.

The items below shipped across the 0.24–0.27.x line and land in the GA release; per-patch detail is in the GitHub release notes.

Added (0.27.8)

  • Per-contract-type default gate mode (002_default_gate_mode.sql, #700 / testbed F-750). New schema_subjects.default_gate_mode column (row-only metadata, not in the canonical manifest body/digest; auto | principal). When a record-create payload omits gateMode, the engine resolves: explicit per-record gateMode → the type's default_gate_mode → engine default auto. Backfills the seeded principal-gate-generic-v1 example to principal (it previously ran auto and silently auto-settled). The first numbered migration after the consolidated baseline — shipped in v0.27.8, carried into the frozen GA file set.

Pre-launch — performerAgentId rename (#537)

  • agentId (the record-performer alias on RecordResponse / create body / list filters / webhook event payloads) is renamed to **performerAgentId** for explicit symmetry with principalAgentId. The agentId alias is stripped — no back-compat (pre-launch posture; no consumers in flight).
  • DB column rename: records.agent_idrecords.performer_agent_id. Matches verdict_statistics.performer_agent_id. Trigger column-list, FK constraint name (records_performer_agent_id_fkey), and indexes (idx_records_performer*) renamed. Function bodies (agledger_maintain_delegation_shell_flag, agledger_propagate_parent_principal_change) updated.
  • Folded into 001_consolidated.sql (pre-launch single-baseline pattern); old checksum auto-rebases via KNOWN_CHECKSUM_REBASES.
  • A2A create_record intent: agentId (overloaded as principal alias) is replaced by explicit principalAgentId. The intent now accepts principalAgentId and performerAgentId as distinct fields.
  • Federation wire body: agentId field on state-transition envelope renamed to performerAgentId. Bus event payloads (record.created, record.activated, record.cancelled, record.expired, verification.complete, settlement.signal, dispute.resolved, dispute.withdrawn, federation.record.state_changed) updated.
  • /llms.txt event-payload docs updated to reflect the renamed field names. /llms.txt reputation-endpoint paths (/v1/agents/{agentId}/...) and the agent.reference_added event are unchanged — those agentId references are the agent's own UUID, not a record-performer alias.

0.23.02026-05-19

Major release. Chain envelope cut over to canonical COSE_Sign1 (SCITT-aligned); SCITT Transparency Service surface (Receipts, Transparent Statements, SCRAPI) shipped; vocabulary aligned with SCITT (Receipt → Completion); audit-chain integrity gains a payload-drift check that catches privileged-DBA-bypass tampering of the visible payload jsonb.

Chain envelope cut-over (#482)

  • Canonical COSE_Sign1 (RFC 9052) replaces JCS + detached-Ed25519. Every audit-vault entry now stores a tagged COSE_Sign1 envelope (tag 18, EdDSA) over an in-toto v1 Statement payload, deterministically CBOR-encoded per RFC 8949 §4.2.1. payload_hash is sha256 of the COSE_Sign1 bytes. Chain mechanics (position + previous_hash) live at COSE protected header private label -65537; CWT_Claims (label 15) carry identity; actor private claim at -65539. SCITT-arch §6.13 compatible.
  • OIDC on-behalf-of (private label -65540) carries who-authorized identity using RFC 8693 framing (iss/sub/aud/iat/exp/act). Validated against the trust circle when present.
  • W3C traceparent (private label -65541) captures distributed-trace context at envelope level, separate from vendor-specific trace IDs in predicate.traces.
  • DSSE bundle export at /v1/records/{id}/attestation projects the chain to sigstore-compatible bundles. Read-only export — chain is COSE_Sign1, projection is for tooling interop.
  • **/predicates/<kind>/v1 schema discovery.** Customers can resolve the canonical predicate schema for each chain kind (record-state, settlement-signal, vault-checkpoint, schema-event, tenant-read, counter-attestation, federation-projection).
  • Vocabulary alignment with SCITT. Performer's evidence submission is now Completion everywhere (route, state, audit entry type, schema kind, SDK type). "Receipt" is now reserved for the SCITT cryptographic-inclusion-proof concept. Audit-vault entries are surfaced to customers as "Signed Statements." Identifiers (table names, internal types) unchanged.

SCITT Transparency Service surface

  • SCITT Receipts (draft-ietf-cose-merkle-tree-proofs-18). Opt-in via ?receipts=true on /audit-export. Each entry's integrity.receipt is a base64-encoded COSE_Sign1 with VDS=1 (RFC9162_SHA256) at protected label 395 and VDP at unprotected label 396. Per-record Merkle is lazy + LRU-cached.
  • Transparent Statements composition at unprotected label 394 (ARRAY) for stock SCITT consumers.
  • SCRAPI (draft-ietf-scitt-scrapi-09) at POST /v1/scitt/entries (register a customer-prepared Signed Statement → 201 + Receipt as application/cose), GET /v1/scitt/entries/{id} (Transparent Statement), GET /.well-known/scitt-keys + /{kid} (CBOR COSE_KeySet, unauthenticated). 4xx/5xx on /v1/scitt/* return application/concise-problem-details+cbor per RFC 9290.
  • Stock-library reference verifiers (pnpm verify:stock). Two off-the-shelf SCITT/COSE tools (go-cose + pycose) verify our envelopes with zero AGLedger code in their dependency tree. Locks the wire shape against drift.
  • SCITT entry idempotency documented (/llms.txt + DESIGN-SCRAPI.md): posting the same Signed Statement bytes twice is SCITT-spec-correct and creates two distinct entries; an Idempotency-Key header is forward work.

Audit-chain integrity

  • Cose/payload drift check. validateVaultEntry decodes the predicate from cose_sign1 and deep-equals against a re-projection of row.payload via the new buildPredicateOnly helper. Catches a privileged-DBA bypass (session_replication_role='replica' + UPDATE audit_vault SET payload = junk) that leaves chain hash + link valid but lies to /audit-export readers. Surfaces as chainIntegrityReason: 'payload_drift' + failure: 'payload_drift' + per-entry integrity.valid: false. Mirrored in @agledger/verify with new CHAIN_PAYLOAD_DRIFT failure code.
  • **SCRAPI routes go through authorizeAction.** REGISTER_SCITT_ENTRY + READ_SCITT_ENTRY declared in authority/declaration.ts; SCRAPI routes participate in the authority mutation pass (135 mutations, all detected).

Operational

  • Customer-tunable rate-limit knobs. RATE_LIMIT_ROUTE_MULTIPLIER scales every per-route cap in lockstep; per-route envs (RATE_LIMIT_POST_RECORDS, RATE_LIMIT_POST_RECORDS_BULK, RATE_LIMIT_POST_COMPLETIONS, RATE_LIMIT_POST_SCITT_ENTRIES) override specific spine endpoints. Default POST /v1/records raised from 100/min to 200/min.
  • Soak-test runner (pnpm soak). Mixed-agent workload + random-event auditor for pre-tag stress runs.
  • F-585 federation Settlement Signal projection (rolled forward from v0.22.35 chained-enqueue fix): cross-group signal precedence restored via chained enqueue; signal-on-state-transition pair preserves order across pg-boss groups.
  • Observability counters on 2 worker-side silent-drop paths (federation outbound publisher's 5 eventBus subscribers, marketplace tier-persist) so dropped events surface on dashboards instead of /dev/null.

Fixed

  • 5 fuzz findings on /predicates auth + SCRAPI response codes + media types.
  • SCRAPI error-message leakage (DB/ORM errors no longer included in the CBOR detail field on 500s; full err goes to server log).
  • Concurrent integration helpers: verifyChainInvariants updated for the post-#482 envelope (was silently broken; caught by local sweep).
  • pnpm override for brace-expansion ≥5.0.6 (GHSA-jxxr-4gwj-5jf2).
  • Discovery 404 $ref swap on /predicates/{kind}/v1.

Internal

  • decodeCoseSign1ToClaim round-trips on_behalf_of + traceparent into envelope metadata.
  • extractEnvelopeExtensions pulls envelope-level extensions out of the writer's freeform payload before predicate construction.
  • Schema-event chain key normalized to schema:${enterpriseId ?? '__platform__'} to match the engine's per-enterprise lock domain.
  • Migration baseline consolidated: 002_federation_nonces_logged.sql, 003_cose_sign1_chain.sql, and 004_scitt_entries.sql folded into 001_consolidated.sql. Dev DBs auto-rebase via KNOWN_CHECKSUM_REBASES; pre-launch single-baseline pattern preserved.

0.22.342026-05-18

Fixed — F-585 federation Settlement Signal projection race + reconnaissance-driven Group A sweep

  • F-585: federation outbound enqueue priority inversion (PRIORITY_SIGNAL=100 vs PRIORITY_STATE_TRANSITION=0) caused the signal POST to race ahead of the state-transition POST within the same pg-boss groupConcurrency:1 group on the receiver, so projectSettlementSignal no-op'd on the missing row → no FEDERATION_SIGNAL_RECEIVED audit entry, no federation.settlement.signal event, no signal.emitted webhook delivery. Equalized to PRIORITY_SIGNAL = PRIORITY_STATE_TRANSITION = 0; within-group FIFO + publisher's state-first enqueue order now preserves ordering. Regression test pins the invariant. F-564's cross-group signal precedence is shelved; #478 tracks restoring it via chained enqueue.
  • cascading.verification.complete webhook event was in the OpenAPI enum, subscribed by the dispatcher, recommended in /llms.txt — but never emitted. Auto-rollup fired verification.complete for the parent but not the cascading variant. Now emits both alongside settlement.signal from applyParentRollupOutcome.
  • Federation REJECTED state was dispatched to peers as webhook event record.rejected, a name not in WEBHOOK_EVENT_TYPES — targeted subscriptions could not match. Normalized to record.proposal_rejected to align with the local emit convention. record.remediated (no local-emit equivalent, fires only on peer post-arbitration projection) added to the enum.
  • Federation bus event payloads dropped wire-body fields. federation.record.state_changed now carries principalAgentId + agentId; federation.settlement.signal now carries outcomeHash, validUntil, idempotencyKey, counterSignature. All optional for subscriber back-compat. Both sender and receiver emit sites updated.
  • Admin webhook circuit-breaker override leaked cross-tenant webhook existence via 404 on owned-by-another-tenant lookup. Now mirrors assertWebhookOwner shape: 404 only on genuine missing, 403 + WEBHOOK_NOT_OWNED recoveryHint on exists-but-not-yours. Constants WEBHOOK_NOT_OWNED_CODE + WEBHOOK_NOT_OWNED_HINT exported from webhooks.service.ts and shared with admin route.
  • Compliance read had an orthogonal-ID leak: URL /v1/records/:recordId/compliance-records/:complianceRecordId authorized only :recordId, then the service fetched the compliance record by :complianceRecordId alone — caller authorized on their own recordId could combine it with any foreign complianceRecordId to read cross-record (potentially cross-tenant) compliance data. getComplianceRecord(pool, parentRecordId, complianceRecordId) now asserts record.record_id === parentRecordId; mismatch and missing both throw the same 404 so cross-record existence stays unrevealable.
  • Removed two unused audit entry types (SIGNAL_EMITTED, KEY_ROTATED) — declared in db/types.ts with zero appendToVault callsites.

Follow-ups filed

  • #478 — restore cross-group signal precedence via chained enqueue (defense-in-depth against future priority drift).
  • #479 — admin/platform reputation cross-tenant reads: intent unclear; product decision pending.

0.22.332026-05-17

Fixed — testbed v0.22.33 follow-up

  • #476 (F-584): receiver-side federation projection now writes an audit-vault entry and persists the federation.record.state_changed / federation.settlement.signal event in the same transaction as the row upsert. Pre-fix the receiver's /audit-export carried chainIntegrity: true, entries: [] (vacuous) and GET /v1/events returned 0 even when peers had pushed state, even though the v0.22.31 closure of F-554 had landed the sender-side observability. FEDERATION_STATE_SYNC + FEDERATION_SIGNAL_RECEIVED entry types (declared but never written before) now anchor the inbound chain on the receiver, signed by THIS Server's vault key, with peerHubId + state on the canonical payload. BUS_TO_PERSISTED mapping for both federation events was missing — added.
  • #475 (F-583): documented federation schema-mirror flow (GET /v1/schemas/{type}/manifest → POST /v1/schemas/import verbatim) hit 409 /problems/reserved-publisher-label whenever the source was registered via POST /v1/schemas without an explicit publisher (the standalone default local is reserved on the import path). The 409 now carries an actionable recoveryHint spelling out both recoveries: (a) re-register on the source with publisher: "<non-local>" so GET /manifest returns an import-ready body, or (b) skip GET /manifest entirely and import symmetrically on every peer with the same chosen publisher (matching digests by construction). /llms.txt reordered to put the publisher-coordination rule load-bearing in the POST /v1/schemas description and the GET /manifest verbatim-mirror line.
  • #474 (F-582): the documented 90% spine call POST /v1/records {type, criteria} (agent caller, no performerAgentId) now produces a record with agent_id defaulted to the caller, so it's also a valid delegation parent. Pre-fix it committed with agent_id=NULL, then any sub-record attempt hit 403 "Only the assigned agent on a record can be the principal of a sub-record" — the lifecycle gate from #471 worked, but only when the parent was created with explicit performerAgentId. Also tightened validateDelegationRules circular-check SQL to skip pure self-commitment rows so A → A → A chains compose; A → B → A and other true cycles still block. Capability check (assertAgentAcceptsType) now exempts agent-role self-commitment — avoids a hot-path DB roundtrip on the 90% spine for no policy gain (an agent self-attesting their own capability adds nothing).
  • #473 (F-581): closed as wontfix. Schema type names in unauthenticated /llms.txt is intentional, not a leak — agent grounding on the always-on discovery surface outranks schema-name metadata privacy. Customer guidance: don't put secrets in type names (use opaque IDs and store codenames in tenant-scoped displayName/description).

0.22.312026-05-10

Fixed — testbed v0.22.30 federation follow-ups

  • F-560/#441 follow-up: peering-token bootstrap now documented in /llms.txt.
  • #448: lowered federation outbound throughput defaults to FEDERATION_OUTBOUND_CONCURRENCY=16 / FEDERATION_OUTBOUND_POLLING_INTERVAL_SECONDS=1.0. Bench showed cranking both knobs together caused super-additive DB-CPU saturation on the 90% notarize path — heavy-fanout deployments must tune them together and re-measure POST /v1/records latency.
  • #449/#450/#451: webhook + federation pass from testbed v0.22.30.
  • #452: notarize-only records now fall back agentId → principalAgentId so federation peer-agent sync surfaces the principal when the performer is null.
  • #298: stripped the federation h2 opt-in env var (allowH2: false is intentional — V1 federation has not been validated against h2 stream framing / HPACK against the per-request Ed25519 signing scheme).
  • Federation projection-scenario handshake tests now mint a real peering token instead of stubbing.
  • F-558/F-560: regenerated openapi.json for record.recorded event + new schema metadata.

0.22.302026-05-09

Added — federation V1 lifecycle observability + DLQ recover (#432–#435)

  • Sender-side observability counters for federation outbound dispatch.
  • Receiver-side V1 Additive B projection (#430).
  • Federation schema discoverability surfaces (#433, #435).
  • DLQ recover path for stuck federation outbound jobs.

Fixed — pre-deploy + housekeeping

  • Baseline regen now excludes pg-boss schema (it's external state, not part of our migration history).
  • Nullable enum + ascii-digit pattern fixes flushed out by Schemathesis.
  • 8 stale issues closed; #425 first module pass landed.

0.22.292026-05-08

Fixed — federation P2P round 3

  • F-550: revoked-peer recovery path. A peer revoked mid-conversation could leave outbound jobs stuck; recovery now drains cleanly to DLQ.

0.22.282026-05-08

Fixed — federation P2P round 2 (#428)

Second pass on V1 federation P2P bugs surfaced by agledger-testbed.

0.22.272026-05-08

Fixed — federation P2P + agent-UX sweep

  • 5 V1 federation P2P bugs from agledger-testbed (#427).
  • A2A JSON-RPC §6 batch returned an object with numeric keys when it should return an array.
  • 422 errors now carry display-status (the customer-facing token, not the DB enum).
  • Idempotency / proposal hint / schema compat / rollup verdict agent-UX cleanups from local simulation sweep.
  • NotFoundError for records had a stale hint referencing the retired Gateway entry; fixed.
  • V1 vocabulary sweep on A2A surface.
  • New test locks V1 vocabulary out of live schema rows (baseline-hygiene).
  • CLAUDE.md: regen-driven baseline workflow documented, plus vault_checkpoints DLP guardrail.
  • Simplify pass: trimmed change-narration, dead params, cast-literal cleanup.

0.22.262026-05-08

Changed — V1 reset: collapse 3-mode federation to single Server role (#426)

Single biggest architecture shift since launch prep. AGLEDGER_MODE retired. Standalone / Gateway / Hub modes collapsed into a single sovereign-Server role per docs/V1-FRAME.md. Every install is the same shape.

  • V0 Hub/Gateway vocabulary scrubbed across non-federation surfaces (P2) and federation API surface (P3).
  • gateway_id column renamed to peer_hub_id (P1 + DB rename).
  • Dropped dead V0 federation config.
  • docs/FEDERATION-ARCHITECTURE.md retired (V0 design draft, superseded by docs/V1-FRAME.md).
  • V1 boot-crash on Helm fixed.
  • Deleted V0 federation Compose example.
  • Federation publisher fan-out parallelized.

Added — regen-driven baseline workflow

  • pg_dump-based baseline regeneration scripts (scripts/regenerate-baseline.sh + scripts/baseline-header.sql + scripts/baseline-footer.sql). 001 baseline is no longer hand-edited; the previous file's checksum lands in KNOWN_CHECKSUM_REBASES.
  • Migration runner auto-prunes orphan _migrations rows so deleted-and-folded migration files leave no residue.
  • Re-audit added vault_checkpoints DLP, chain-FK RESTRICT, contract_type rename.
  • Pre-deploy skill collapses 3 DB-bound test projects into one vitest run.

Fixed — pre-deploy

  • fast-uri 3.1.1+ override.
  • Dropped V1-removed env vars from defaults.
  • Federation agentDirectoryItemSchema.peerHubId removed (regen openapi).
  • Shellcheck SC2001 suppression on parity-check sed lines.

0.22.252026-05-07

Fixed — testbed AWS install sweep + agent-UX guardrail

Customer-faithful AWS EKS deploy of v0.22.24 (testbed-filed agledger-api#424 + install#2) surfaced 7 + 3 onramp blockers on customer surfaces. Fixed end-to-end across the API repo and the install repo. Plus a structural follow-up: catch the inline-href bug class with a lint rule, and prevent install-side commit drift from being silently overwritten by the next mirror.

  • **Helm chart templates/NOTES.txt license endpoints + docs URL.** /admin/license / /admin/license/reload were missing the /v1/ prefix — customers following the post-install message verbatim hit 404. Fixed to /v1/admin/license + /v1/admin/license/reload. https://docs.agledger.ai (NXDOMAIN) corrected to https://agledger.ai/docs. Port aligned with Compose convention (localhost:3001).
  • **scripts/init.ts post-install next-steps.** Detects KUBERNETES_SERVICE_HOST and prints curl-flavored verify hints when running inside K8s; Compose users see the docker compose up flow unchanged. Stops misleading K8s operators with Compose-only commands when invoked via kubectl exec.
  • **deploy/scripts/helm-install.sh Aurora TLS.** New --ca-cert <path> / --no-ca-cert flags + interactive prompt when "External database" is chosen. Defaults to the bundled /etc/ssl/certs/rds-global-bundle.pem (Aurora/RDS) which the migration pre-install hook needs. Was a hard install blocker for AWS Aurora customers.
  • **deploy/scripts/install.sh, support-bundle.sh, tests/smoke-test.sh.** Path corrections to /v1/conformance (was /conformance, 404). Smoke-test now registers an install-smoke-test-v1 contract instead of hardcoding the ACH-PROC-v1 built-in (which ships DISABLED post-v0.22.11). Agent body shape uses enterpriseId + name. Schema-count assertion no longer expects 11 built-ins.
  • **deploy/docker/Dockerfile** org.opencontainers.image.documentation label corrected to https://agledger.ai/docs (visible to every customer's docker inspect).
  • **scripts/generate-license.ts** operator warning corrected to GET /v1/admin/license/instance-id.
  • README.md docs links corrected (NXDOMAIN → agledger.ai/docs/*).
  • **Inline nextSteps href in admin.system.routes.ts:84** (the bug-class root cause). Returned href: '/admin/license' to LLM agents — agents following the response verbatim 404. Migrated to a helper in shared/next-steps.ts. Three more pre-existing inline-href violations (in admin.system + admin.vault) migrated to helpers in the same change.

Added — `agledger/no-inline-next-steps-href` ESLint rule

Error-level. Flags href: string literals inside nextSteps: arrays anywhere outside shared/next-steps.ts. Skips test files and external https:// URLs. Closes the gap between the existing CLAUDE.md guardrail ("all nextSteps must go through a helper") and runtime enforcement — the helper-layer sweep can't validate inline literals because they never reach the helper.

Added — `deploy/scripts/check-deploy-parity.sh`

Standalone diff -rq over the four canonical/install-mirror pairs (scripts, compose, helm/agledger, tests/smoke-test.sh, cosign.pub). Exits non-zero on content drift. mirror-install-repo.sh now runs a commit-history gate before rsync — refuses to mirror over unbackported install-side commits since the last release tag (ALLOW_INSTALL_DRIFT=1 escape hatch). Tag selector filters to canonical v\d+\.\d+\.\d+ to sidestep the known vv0.22.20 mistagged-tag gotcha.

Added — three `nextSteps` helpers

licenseInstanceIdNextSteps, supportBundleUploadNextSteps(bundleId), vaultRotateSigningKeyNextSteps in shared/next-steps.ts. The pre-existing inline returns at these sites had been silently violating the NextStep shape (missing description / method / href) — the strict helper return type catches the violation now.

Closes agledger-ai/agledger-api#424, agledger-ai/install#2.

0.22.132026-04-30

Fixed — Federation peer rate limits + hook ordering (testbed F-503 reframed)

Testbed reframed F-503: the original "federation outbound worker throughput-bound at ~3 jobs/sec" was misdiagnosed. The actual cause is hub-side rate-limiting of the gateway. Two distinct bugs, both closed here.

  • Federation peer route limits sized for hand-tested federation, not customer-rate traffic. POST /federation/v1/state-transitions was capped at 200/min/gateway — sustained 3.3 TPS hits the wall. Bumped to 5000/min (≈83 TPS sustained per peer). Other routes scaled proportionally: signals 100→2500, schemas/reputation 30→750, contract-types/criteria/agents-list/reputation-read 100→2500, heartbeat 15→300, agents-register 50→1500, catch-up 60→1500, peer-revocations/peer-agent-sync 30→750. Anti-flood routes unchanged (register, revoke, rotate-key, stream all stay at 5/min).
  • **Rate-limit was firing as onRequest while bearerAuth runs as preHandler — so request.federationGateway was always null when the keyGenerator ran.** The 'gateway' keyGenerator silently fell back to request.ip (fed:${gateway_id ?? ip}). Two gateways behind one egress NAT shared a single bucket; the RATE_LIMIT_EXEMPT_OWNERS env var couldn't exempt federation peers because their gateway_id was never inspected. Fixed by setting hook: 'preHandler' on federationRateLimit so the rate-limit fires after bearerAuth populates request.federationGateway. exemptAwareMax now also checks request.federationGateway?.gateway_id against the exempt set, so operators can exempt a known peer by gateway UUID instead of by IP. Six new unit tests in federation.route-helpers.test.ts pin the contract.

These limits are deliberately generous for now — they unblock customer-rate federation traffic. We will tighten back down once the rest of the federation hot path is settled and we have data on real customer rates.

0.22.122026-04-30

Fixed — F-507 root cause, federation inbound self-deadlock

v0.22.11 shipped idle_in_transaction_session_timeout as the F-507 fix. Testbed confirmed the recovery still landed at ~118 s (the recovery-sweep cycle) rather than the GUC's 30 s — meaning the GUC wasn't the load-bearing fix and the deadlock was self-resolving through some other mechanism. Root-cause investigation found a concrete self-deadlock in _processStateTransition: assertGatewaySequence would call appendFederationAudit(client, gatewayId, ...) on the held outer-tx client (taking pg_advisory_xact_lock(hashtext('fed:' || gatewayId))), then on validate-throws / hop-count-exceeded the handler called auditRejectedInbound(pool, ...) — which opens a SECOND connection that tries to take the SAME advisory lock. Postgres does not detect this as a deadlock because the wait cycle goes through an application-level await, not a row-lock wait; the outer backend sat idle-in-transaction holding the lock, every subsequent inbound from that gateway queued behind it, and the hub froze.

  • Throw-then-audit refactor. Inbound state-transition / settlement-signal / key-rotation handlers now throw a structured InboundRejection carrying audit context out of the inner withTransaction. The public processX wrappers catch it via auditAndRethrowRejection AFTER the inner tx has rolled back and released its fed:${gatewayId} lock — auditing on a fresh pool connection at that point cannot self-deadlock against the chain lock the inner tx held. Six call sites were converted (the two confirmed-vulnerable state-transition paths plus four trap-shaped sites in settlement signal and key rotation that were safe by call-order today but fragile under future edits).
  • **Pool verify for the GUC.** pool.on('connect', client => client.query('SET ...')) was fire-and-forget — pg's internal queue was supposed to serialize SET before the caller's first BEGIN, but the test only asserted != '0', which silently passed under bundled Postgres's idle_in_transaction_session_timeout=300s server-side default even when the SET failed. Switched to pool.options.verify, which gates the client's release on the SET callback. Strengthened the regression test to assert the EXACT configured ms value matches idleInTxTimeoutMs.
  • **CLAUDE.md Database & Pool Safety checklist note.** Documents the pattern shape ("never call a fresh-pool helper that takes the same advisory lock as the held outer tx") with the F-507 root-cause diagnosis and InboundRejection as the precedent for code reviews.

The v0.22.11 GUC stays in place as defense-in-depth — it remains the right belt for any future leak — but it is no longer the load-bearing fix. The structural change is what closes F-507.

Post-release note (testbed retraction): F-508 (the claim that v0.22.11's pool.on('connect') SET was silently ineffective and every backend reported 1d instead of 30s) was a probe artifact — current_setting() is session-local, so a SELECT pid, current_setting('idle_in_transaction_session_timeout') FROM pg_stat_activity query returns the *probing* session's GUC for every row regardless of pid. Re-probed correctly from inside the production pool (or via pg_show_all_settings(pid)), every fresh agledger pool client returns 30s. v0.22.11's pool.on('connect') was working all along; v0.22.12's switch to pool.options.verify is structural cleanup (eliminates a theoretical race window where a caller's first BEGIN could land before SET completes), not a fix-for-broken. F-507's structural fix (the InboundRejection refactor) was independently validated under the same 5-min × 8-parallel × 12 TPS soak that originally exposed the deadlock.

0.22.112026-04-30

Fixed — F-507 hub deadlock under sustained federation load

Closes F-507. v0.22.10 raised FEDERATION_OUTBOUND_CONCURRENCY from 1 to 8 (the F-503 throughput fix). Reproducible deadlock followed: under sustained load the hub stops accepting requests within ~2 minutes. Root cause — one transaction acquires pg_advisory_xact_lock, sits idle in tx with wait_event=ClientRead (event-loop drop, missed COMMIT/ROLLBACK on pool return), and Postgres holds the lock for the lifetime of the transaction. Every subsequent worker queues on Advisory waits, DATABASE_POOL_MAX fills, auth middleware times out at 5s, the pod becomes unresponsive. Recovery required manual pg_terminate_backend against the orphaned backend.

  • **idle_in_transaction_session_timeout SET on every new pool connection** — src/db/connection.ts adds a pool.on('connect') handler that runs SET idle_in_transaction_session_timeout = N on every new backend. Postgres FATAL-terminates a backend that sits idle in tx past the timeout (SQLSTATE 25P03), releasing every lock the session held. Default 30 seconds — tolerates legitimate slow handlers (audit-export, chain verify) while bounding deadlock recovery to within livez/readyz windows.
  • New env var DATABASE_IDLE_IN_TX_TIMEOUT_MS (default 30000) — plumbed through src/config.ts, .env.example, deploy/compose/.env.example, helm database.idleInTxTimeoutMs, and the helm configmap. Set to 0 to disable.
  • New counter agledger_db_pool_connect_set_failures_total{setting} — increments when a connect-time SET fails. Documented in docs/operations/OBSERVABILITY.md with the recommended alert rule (rate > 0 over 1m). Means at least one pool connection is missing the safety guard.
  • Regression test in src/db/connection.integration.test.ts — every new pool connection must show non-zero idle_in_transaction_session_timeout.

FEDERATION_OUTBOUND_CONCURRENCY=8 stays at the v0.22.10 default — throughput preserved.

Changed — Migration baseline consolidated to a single file

Pre-launch hygiene before v1.0. The eight migration files in src/db/migrations/ (the 001_consolidated.sql baseline + seven incremental upgrade-path files for BYPASSED, expired_at, string_overrides, federation_outbound_seq_cache, seq-skip reconciliation, REJECTED_INBOUND audit, and signal-seq index) collapsed into a single 001_consolidated.sql (~9.5K lines, down from ~12.6K). Existing dev DBs auto-rebase via KNOWN_CHECKSUM_REBASES. No behavior changes for fresh installs.

  • Schema fixes folded into the baseline: - REVOKE UPDATE/DELETE/TRUNCATE on every audit_vault partition (was parent-only — agledger_app had full DML on every monthly partition through the bulk GRANT ... ON ALL TABLES). - FK-backing indexes idx_tarc_signing_key, idx_tar_record (cascade lookup was seq-scanning). - REVOKE + BEFORE TRUNCATE for tenant_admin_read_export_manifests; BEFORE TRUNCATE for tenant_admin_read_sequences. - DROP nine dead tables (proxy_*, notarized_*, notarization_transitions, agent_platform_identities) plus 25 monthly partitions of proxy_tool_calls — leftovers from removed proxy/notarization features. Removed proxy_tool_calls from ensure_future_partitions.
  • **ensure_future_partitions hardened to SECURITY DEFINER** with SET search_path = public, pg_temp and REVOKE EXECUTE FROM PUBLIC; GRANT EXECUTE TO agledger_app. The function ran as INVOKER and the worker calls it as agledger_app, which has only USAGE on schema public. Production deployments would have silently failed to roll partitions on the first non-superuser worker run; bundled-Postgres dev hid the gap because the dev role is superuser. The function now applies the audit_vault partition REVOKE inline so newly-created monthly partitions get the same defense-in-depth gate as today's existing ones.

Changed — Migration runner launch-readiness pass

  • Postgres version preflightassertPostgresVersion() refuses pre-PG17 at the door. Without this, the consolidated 9.5K-line baseline fails thousands of statements deep with a confusing message.
  • Session-level safety SETs — every migration run sets statement_timeout=15min, lock_timeout=30s, idle_in_transaction_session_timeout=5min (env-tunable via MIGRATION_STATEMENT_TIMEOUT / MIGRATION_LOCK_TIMEOUT / MIGRATION_IDLE_IN_TX_TIMEOUT). The difference between "migration stuck in prod" and "migration failed loudly in prod."
  • Connect-retry covers Postgres SQLSTATEs57P03 (Aurora failover), 53300 (saturation), 08001/08004/08006 (connect rejection), plus EPROTO (TLS handshake transient). Previously only OS-level errno codes triggered retry.
  • **Dry-run no longer mutates _migrations.checksum** — --dry-run was silently writing through the rebase + backfill paths. Both UPDATEs are now gated by if (!dryRun).
  • **AGLEDGER_LAUNCH_FROZEN env flag** — when set, the runner refuses to rebase any migration checksum. Flipped on (with KNOWN_CHECKSUM_REBASES cleared) at the v1.0 freeze — see the 1.0.0 entry above. Before v1.0 the rebase mechanism stayed operational for dev DB drift.
  • 33 new tests — 25 unit tests (src/db/migrate.test.ts) covering errno + SQLSTATE + message-based connection-error detection and isNoTransaction directive parsing; 8 integration tests (src/db/migrate.integration.test.ts) covering re-run, lock contention, dry-run preservation, rebase, backfill, and LAUNCH_FROZEN refusal.

Added — `POST /v1/admin/mandates/import` (historical backfill)

Closes #293. The admin:backfill scope has been a dangling declaration since v0.20.0 — no route consumed it. This release gives it a purpose.

  • New endpoint POST /v1/admin/mandates/import — admin + admin:backfill scope, accepts { enterpriseId, source, mandates: [BackfillMandate] } with up to 100 entries per batch. Each item lands directly in its declared terminal state with backdated createdAt / activatedAt / fulfilledAt. No state machine walk, no webhook fan-out, no federation propagation, no reputation scoring, no pg-boss enqueue — this is archival data, not activity.
  • Vault stamping — every imported mandate gets exactly one BACKFILL_IMPORT entry (enum value already reserved in 001_consolidated.sql) with imported: true + declared source in the canonical payload. Dual Trail compliance crosscheck can distinguish historical data from live activity without ambiguity.
  • Atomic batches — one transaction per request. Any per-item validation failure rolls the whole batch back; partial-success reconciliation confusion never occurs.
  • Terminal-status whitelistFULFILLED, REMEDIATED, EXPIRED, REJECTED, VERIFIED_FAIL, CANCELLED_DRAFT, CANCELLED_PRE_WORK, CANCELLED_IN_PROGRESS. FULFILLED requires fulfilledAt. Timestamp ordering enforced (activatedAt / fulfilledAt must not precede createdAt).
  • Cross-tenant safety — every principalAgentId and performerAgentId is verified to belong to the target enterpriseId before insert. An admin key cannot import a mandate citing an agent from a different tenant.
  • Agent-UX — response includes adminMandateImportNextSteps steering the operator to an audit-export spot-check on the first imported mandate + a follow-up batch.
  • **/llms.txt mention** — under the "Tenant + Agent Provisioning" admin section so LLM operators discover the endpoint.
  • 13 integration tests in src/test/admin-import.scenario.integration.test.ts covering auth (agent rejected, admin without admin:backfill rejected, unauthenticated), happy path (single + batch + nextSteps shape), validation (FULFILLED without fulfilledAt, non-terminal status, timestamp ordering, batch size cap, missing enterprise, cross-tenant agent), and full-batch rollback on any item error.

Added — SSRF enforcement at the dispatcher layer + fetch-layer retry (#297, #299)

Closes #297 and #299. Previously SSRF protection was scattered: webhook delivery did a per-request DNS check, but OIDC discovery, JWKS, Vault secret load, telemetry heartbeat, and support-bundle upload only validated URL strings at configure time. DNS-rebinding or late-resolution could slip through to an internal address at connect time.

  • **connect.lookup hook on every outbound dispatcher** — src/shared/http-agent.ts exports ssrfLookup, plumbed into HTTP_AGENT_OPTIONS.connect.lookup. Every Agent built by createHttpAgent() AND the global fetch() dispatcher resolve hostnames through it. Private / link-local / loopback addresses not covered by SSRF_ALLOW_CIDRS fail the connect with code: 'SSRF_BLOCKED'. Uniform enforcement across webhook delivery, SIEM, federation hub, OIDC, JWKS, Vault, telemetry, and support-bundle paths.
  • **agledger_outbound_ssrf_blocked_total{host} counter** — bumps on every connect-time rejection, so operators can observe rebinding attempts.
  • Webhook workercategorizeHttpError now recognizes SSRF_BLOCKED for clean DLQ classification, and the function was refactored from if-ladder to lookup table (complexity 21 → 9).
  • **Global dispatcher wraps in RetryAgent** — configureGlobalDispatcher() wraps the EnvHttpProxyAgent in undici.RetryAgent with maxRetries: 2, exponential backoff (minTimeout: 500ms, maxTimeout: 5000ms, timeoutFactor: 2), and retryAfter: true. Retry applies to undici's safe-method defaults (GET/HEAD/OPTIONS/PUT/DELETE/TRACE) only — POST is never retried at the dispatcher layer (telemetry + support-bundle cannot afford duplicates). Webhook delivery is NOT wrapped — it uses its own createHttpAgent() Agent and pg-boss remains the retry layer.
  • 8 unit tests in src/shared/http-agent.test.ts cover IPv4 private ranges (loopback, RFC 1918, link-local AWS metadata), IPv6 loopback, { all: true } array form, public IP pass-through, and preservation of non-SSRF DNS error codes.

Related: issue #298 (H/2 opt-in for federation hub after benchmark) remains parked — no change in this release.

Closed — #278 OTel metrics + logs (was: "evaluate wiring")

Already wired in-tree. src/instrumentation.ts configures PeriodicExportingMetricReader + OTLPMetricExporter on NodeSDK whenever OTEL_EXPORTER_OTLP_ENDPOINT is set, and src/modules/metrics/otel-bridge.ts monkey-patches every prom-client metric so a single declaration emits on both /metrics (scrape) and OTLP (push). docs/operations/OBSERVABILITY.md:225 documents this. OTel-native logs export was evaluated and deemed marginal — pino → stdout → OTel Collector filelog receiver is the canonical Node path and already works without process-local changes.

Added — Admin-ops UX pass for LLM-driven operators

Customers installing AGLedger increasingly drive setup and ops through LLM tools (Claude Code, Cursor, MCP). The /admin/* surface previously treated nextSteps as agent-only — the mutation endpoints that platform-role LLMs call during install/triage (DLQ retry, rate-limit exemptions, auth-cache flush, circuit-breaker override, account deactivation, bulk-revoke, API-key toggle) returned bare results with no self-correction guidance. Now every admin mutation returns nextSteps with a verification action and, where relevant, the inverse action.

  • **New adminOpsNextSteps() dispatcher** in src/shared/next-steps.ts covers 6 admin-ops actions: dlq-retry, dlq-retry-all, rate-limit-exempt-add, rate-limit-exempt-remove, auth-cache-flush, circuit-breaker-override.
  • New per-endpoint helpers for admin.accounts mutations that the service layer wasn't injecting: accountDeactivationNextSteps, apiKeyToggleNextSteps (branches on active/inactive for the right inverse action), bulkRevokeNextSteps.
  • **9 admin response schemas declared nextSteps** so fast-json-stringify (which strips undeclared fields under additionalProperties: false) lets the field through: retryDlqSchema, retryAllDlqSchema, bulkRevokeSchema, deactivateAccountSchema, addRateLimitExemptionSchema, removeRateLimitExemptionSchema, toggleApiKeySchema, plus the inline schemas for POST /admin/auth-cache/flush and PATCH /admin/webhooks/:webhookId/circuit-breaker. Silent-strip of nextSteps has been the root cause of multiple past UX regressions; these explicit declarations prevent the next one.
  • 6 integration tests added to admin.scenario.integration.test.ts that exercise the full serialization path. 10 unit tests added to next-steps.test.ts covering the helper return shapes and URL interpolation (e.g. rate-limit-exempt-add interpolates ownerId into its inverse DELETE URL).

Changed — undici 7.24.x → 8.1.0, consolidated HTTP client configuration

undici v8 shipped feat!: enable h2 by default (PR #4828). Customer webhook endpoints and SIEM receivers haven't been benchmarked on HTTP/2, and federation hub multiplexing hasn't been exercised either.

Consolidated HTTP-agent layersrc/shared/http-agent.ts is the new single source of truth:

  • HTTP_AGENT_OPTIONS declares { allowH2: false, connect: { timeout: 5_000 }, bodyTimeout: 15_000, headersTimeout: 15_000, keepAliveMaxTimeout: 30_000 }.
  • createHttpAgent() factory — webhook-delivery worker, SIEM HTTP sender, federation hub transport, and webhook-test delivery all use it. Removes 4 identical-copy Agent configs.
  • configureGlobalDispatcher() at server bootstrap installs an EnvHttpProxyAgent as the process-wide dispatcher, carrying the same options. This accomplishes two things: 1. Every native fetch() caller (OIDC discovery, JWKS, Vault secret load, telemetry heartbeat, support-bundle upload) inherits allowH2: false without touching the call site. Previously the undici v8 global-dispatcher default (allowH2: true) would have reached these five sites uncovered by our explicit Agent pin. 2. Corporate-proxy installs now work. EnvHttpProxyAgent respects HTTP_PROXY / HTTPS_PROXY / NO_PROXY env vars — customers behind corporate firewalls had silent outbound-HTTP failures on OIDC discovery and Vault before this.

**Added timeouts to three unprotected fetch() calls** — oidc.ts OIDC discovery (5s), oidc.ts JWKS fetch (10s), secrets-provider.ts Vault load (10s). These could previously hang indefinitely on customer-configured endpoints, piling up requests on the auth path.

Regression guards:

  • src/shared/undici-v8.regression.test.ts asserts each explicit Agent is constructed with allowH2: false. Any refactor that drops the flag fails pnpm test.
  • One additional unit test in webhook-delivery.worker.test.ts covers the same invariant at the worker's own construction site.

Deferred, with issues filed:

  • #297 — SSRF guard at dispatcher level via a custom connect function (covers all outbound uniformly; current explicit pre-DNS check in webhook-delivery remains the behavioral target).
  • #298 — Opt HTTP/2 in for federation hub transport after a Gateway-↔-Hub benchmark; signing scheme has not been exercised on h2 multiplexing.
  • #299 — Evaluate undici.RetryAgent for the fetch-layer transient-failure paths (OIDC, Vault, telemetry, support-bundle); webhook delivery's pg-boss retry is out of scope.

0.20.22026-04-23

Changed — `/llms.txt` restructured to link-index style (F-428 / #294)

Testbed F-428 reported that /llms.txt advertised a "retired" register transition action and agents hit a 422 when they called it. The diagnosis was wrong — register is a live action from internal state DRAFT (display CREATED) — but the *signal* was real: the doc enumerated the action set in prose, which encouraged agents to pick an action from the doc instead of reading the mandate's own nextActions field. Display state CREATED covers two internal states (DRAFT and REGISTERED) with different allowed actions, so the enum-as-prose was fundamentally ambiguous.

  • **New top-of-doc ## Agent Grounding Directive.** Tells agents explicitly to ground decisions in the response body (nextActions, validTransitions, nextSteps, recoveryHint, refreshUrl) rather than in the static doc. The directive is load-bearing — it cannot be omitted via customization.
  • **Action enum removed from /llms.txt prose** at both sites (the Lifecycle section and the POST /v1/mandates/{id}/transition line under Key Endpoints). The full enum lives in /openapi.json and is referenced from /llms-full.txt.
  • **mandates.schemas.ts transition-description fixed.** Previously said register: DRAFT -> CREATED, which mixed internal and display state names. Now clarifies that display CREATED covers both internal DRAFT and REGISTERED, explaining why "which actions apply depends on current state" matters.

Added — 422 `INVALID_ACTION` self-correction pointers

POST /v1/mandates/{id}/transition returning 422 INVALID_ACTION now carries two additional fields so grounded agents self-correct without a second guess:

  • **recoveryHint** — machine-readable steer: "GET /v1/mandates/{id} and read nextActions... do not infer the action set from /llms.txt or /openapi.json enums." Sourced from a single static constant on InvalidActionError.
  • **refreshUrl** — the concrete GET /v1/mandates/{id} URL for this request, extracted from the original path so the agent can re-fetch without parsing the template.
  • Both fields are declared on ErrorResponse in src/shared/schemas.ts so fast-json-stringify doesn't silently strip them (feedback_fastify5_serialization).

Added — `/llms.txt` customer-customization surface

Customers tune /llms.txt per deployment to match the agent stack that consumes their install. All four knobs are optional and fail-open — file-read failures log a WARN and the built-in template takes over, so /llms.txt stays available (agent-discovery never-gate rule).

  • AGLEDGER_LLMS_TXT_OVERRIDE_PATH — serve this file verbatim, skip generation.
  • AGLEDGER_LLMS_TXT_PREPEND_PATH / AGLEDGER_LLMS_TXT_APPEND_PATH — wrap the generated body with customer-authored content. Applies to both /llms.txt and /llms-full.txt.
  • AGLEDGER_LLMS_TXT_OMIT_SECTIONS — comma list of sections to drop: a2a, proposals, disputes, schema-management, audit-compliance, verification-keys, federation, a2a-agent-card, optional. Unknown keys are ignored silently. Customers running standalone without A2A drop a2a,proposals,a2a-agent-card; single-party deployments drop disputes.
  • Content negotiation. When Accept: text/markdown is present, the response content-type is text/markdown; charset=utf-8; otherwise text/plain. Vary: Accept is set so shared caches don't serve the wrong content-type variant. Body is byte-identical.
  • 128 KB cap on each customization file. Cache busts via POST /v1/admin/discovery/reload or on schema-change LISTEN/NOTIFY.

Verification baseline at head

  • Unit tests: 1030/1030 (59 files) — llms-txt.test.ts grows by ~22 tests covering the new Grounding Directive assertions, the OMIT_SECTIONS matrix, and the route-level customization behavior (override / prepend-append / Accept negotiation). error-handler.test.ts grows by 2 tests verifying recoveryHint + refreshUrl on InvalidActionError responses.
  • .env.example drift guard: 2/2 passing against the 4 new env vars.

0.20.12026-04-23

Same-day patch — testbed-filed v0.20.0 findings #288/#289/#290/#291/#292/#286.

Fixed

  • #292 — DISABLED contract types silently accepted new mandates. mandates.service.createMandate now checks schema_subjects.status after resolving the contract type; DISABLED types reject with 422 /problems/unprocessable-state and a recovery hint pointing at PATCH /v1/schemas/{type}/enable. The CHANGELOG lifecycle claim in v0.20.0 ("two-state ACTIVE/DISABLED with disable/enable/delete verbs") is now genuinely enforced — previously the DISABLED flag was cosmetic.
  • #292 / #286 — GET /v1/schemas/:contractType now returns lifecycle metadata and resolves DISABLED types. New read-only getContractTypeAnyStatus() service function (backed by findSubjectByType(..., { includeDisabled: true })) populates status: 'ACTIVE' | 'DISABLED' on the response. Mandate creation and validation stay on the ACTIVE-only resolver so DISABLED types can't leak into write paths. A DISABLED type the customer owns now returns 200 with status:'DISABLED' instead of 404 — fixes the catalog inconsistency where list advertised it but GET 404'd.
  • #286 — GET /v1/schemas list entries now carry metadata. Response changes from data: string[] to data: { contractType, version, status, displayName, isBuiltin }[]. No per-type round-trip to check lifecycle. Wire-format break; SDK/CLI/MCP fan-out is on a follow-up cross-repo issue (see agledger-agents).
  • #291 — Agents can read audit/compliance for their own mandates (self-attestation). Scopes.AUDIT_READ and Scopes.COMPLIANCE_READ are agent-grantable again (removed from ADMIN_ONLY_SCOPES). All three agent profiles (agent-full, agent-readonly, agent-performer-only) now include both scopes by default. New assertMandatePartyAccess() helper gates per-mandate audit/compliance routes: agents must be principal OR performer (tenant-adjacent agents get 403 NOT_MANDATE_PARTY, narrower than the general assertMandateAccess). Routes migrated: GET /v1/mandates/:id/audit-export, /compliance-records, /compliance-records/:recordId, /ai-impact-assessment. Tenant-wide routes stay admin-only (/audit-vault/export, /compliance/export/*, write-side POST /compliance-records, POST /ai-impact-assessment). SIEM stream (/v1/siem/stream) unchanged — it was already role-scoped at the query level.
  • #290 — selfCommitment field on GET /v1/mandates/:id. Previously only the POST response carried it; GET/list/search/batch/chain/sub-mandates/update/transition/cancel all returned null. toResponse() in mandates.service.ts now derives it from principal_agent_id !== null && agent_id !== null && principal_agent_id === agent_id. Type is non-optional on MandateResponse.
  • #289 — GET /v1/scope-profiles is genuinely public. Added to PUBLIC_EXACT_ROUTES in src/middleware/auth.ts and to the OpenAPI security classifier's PUBLIC_EXACT set in src/plugins/swagger.ts. Source comment's "public endpoint" promise now matches runtime.
  • **#288 — Retired /v1/mandates/summary no longer returns a cryptic UUID-format 400.** Explicit 410 Gone handler registered for the path (Fastify's static-vs-parametric precedence makes it win over /:id). Body points at GET /v1/mandates/search?status=... with a machine-readable recoveryHint. Generic error handler also now detects params/id UUID-format failures and emits a path-level hint ("*The path segment must be a valid UUID. If you intended an action or were following docs for an older version, see /openapi.json or /llms.txt for current routes.*") instead of the misleading old schema-lookup hint.

Closed

  • #287 — Principal & key model overhaul. 3 of 4 items shipped in v0.20.0; the historical-import surface (item 4) refiled as a dedicated p2 issue (#293) rather than keeping #287 open as a grab-bag.

0.20.02026-04-23

Major release — pre-launch model collapse. Single-shot rename + structural simplification of the auth/principal surface to fix testbed-evidence drift (N=47 study showed 69–94% misroute rate on the dual-principal endpoint). Every change in this release is a contractual break vs. v0.19.x; semver-major signals it accordingly. Pre-launch, zero customers, no migration tax.

Unifying principle

> Named principal in the data model. Named actor in the audit chain. Two separate facts.

mandates.principal_agent_id is the accountable party (one named agent, NOT NULL). audit_vault.actor_key_id + actor_role + actor_owner_id is the chain of custody (recorded per state-change, hash-chained inside the canonical payload). Admin and platform keys can act on mandates; every action is signed as itself. No "enterprise ghost" principal, no impersonation, no restricted-role gatekeeping.

Changed (BREAKING)

  • **API key role enterpriseadmin.** The role that governs a tenant is now admin (clearer: it governs the enterprise, it is not the enterprise). All scopes, profiles, route descriptions, and SDK types updated. 'enterprise' is no longer a valid value on api_keys.role (CHECK constraint enforces admin | agent | platform).
  • **API key prefix: ach_(ent|age|pla)_*agl_(adm|agt|plt)_*.** Aligns with the compact license string format agl_(ent|dev)_v1_<base64url>. The auth middleware regex is now ^agl_(adm|agt|plt)_. Real bug caught in pre-deploy: scripts/init.ts and deploy/scripts/install.sh still minted/extracted with the old ach_pla_ prefix — fresh installs would have produced unauthenticatable platform keys. Both fixed.
  • **Default scope profile names: standardadmin-standard, restrictiveadmin-observer, iac-pipelineadmin-iac, schema-manageradmin-schema. New: agent-performer-only. Retired: monitor (folded into admin-observer).**
  • **POST /v1/mandates is the unified create endpoint** for admin, agent, and platform callers. Agent callers default principalAgentId to themselves; admin/platform callers must name a principal — explicitly via principalAgentId, or implicitly by setting agentId/performerAgentId (the named performer becomes the principal — a self-commitment). Cross-tenant attempts return 403.
  • Mandate principal model collapsed. mandates.principal_type column dropped. Every mandate has principal_agent_id NOT NULL (FK to agents). Self-commitment is a first-class pattern: when principal_agent_id === agent_id, the vault entry carries selfCommitment: true and auto-verdict auto-settle is blocked.
  • **mandates.created_by_key_id (NOT NULL) records the credential that issued the create request** — a separate fact from the principal. Admin keys can create mandates where the principal is an agent they govern; the audit chain shows both.
  • **audit_vault rows now carry actor fields actor_key_id, actor_role, actor_owner_id (all NOT NULL).** Folded into the canonical payload under a reserved _actor envelope key (hash-chained — tamper breaks verifyChain). Cascade-cancel preserves the parent canceller's actor through the boss job data, so child cancellations attribute to the original actor instead of SYSTEM_ACTOR.
  • **enterprise_agents N:M table dropped.** Agents are now 1:1 with tenants via agents.enterprise_id FK (NOT NULL). 18 production sites that did INSERT INTO enterprise_agents were retired; the agent's tenant is set at creation and cannot change.
  • **api_keys.enterprise_id is required for role IN ('admin','agent')** (cross-column CHECK chk_api_keys_tenant_invariant). NULL only for role = 'platform'. Real bug caught in pre-deploy: admin.repository.insertApiKey + service + provisioning reconciler weren't passing enterprise_id; every admin/agent key created via these paths would have failed the constraint. Fixed.
  • Migration 001 rewritten as a single consolidated baseline. Existing dev DBs need to wipe (the migration runner's checksum verifier rejects modified-in-place files; this is intentional). Pre-launch single-baseline strategy continues — 002+ ships post-launch.

Removed

  • **Trust levels (sandbox / active / verified).** agents.trust_level, agents.verified_at, agents.verification_method, and the matching enterprises.* columns dropped. Sandbox tier rate-limit bucket dropped. requireVerifiedOrActive middleware (18 call sites) gone. Trust-graduation event handler gone. Agent-card verification self-service route module gone.
  • Self-service registration & email-verification surface. AGLedger is self-hosted only — accounts are provisioned by an operator via POST /v1/admin/enterprises + POST /v1/admin/api-keys, or via the provisioning/ YAML config-as-code directory. The Auth tag now exposes only GET /v1/auth/me + POST /v1/auth/keys/rotate.
  • **POST /v1/mandates/agent** (use unified POST /v1/mandates with performerAgentId for proposal-style flows).
  • **GET /v1/mandates/agent/principal** (replaced by filtering /v1/mandates by principal). GET /v1/mandates/agent/proposals retained (agent-only listing of pending proposals).
  • **GET /v1/mandates/summary** (use /v1/mandates/search with status filters).
  • **PATCH /v1/admin/accounts/:id/trust-level** (entire endpoint family — trust gone).
  • **PUT/DELETE/GET /v1/admin/rate-limit-exemptions/ip/*** (the IP-specific variants — owner-level exemptions retained).
  • Dashboard module (6 routes). GET /v1/dashboard/{summary,metrics,audit-trail,agents,alerts,disputes} retired. All data was already available via primitive endpoints (/v1/mandates, /v1/agents, /v1/reputation/:id, /v1/disputes, /v1/audit-export) + Prometheus /metrics. Structuring and concurrent-chain detection continues to surface in GET /v1/audit/enterprise-report response (same SQL, moved to audit-report.repository.ts). Dashboard had no first-party UI consumer and every endpoint duplicated existing data — the schemathesis-flagged total_mandates/lifetime_mandates column drift on /dashboard/agents was a symptom of orphan-infrastructure bitrot. dashboard:read scope and dashboard scope profile removed; the profile's read-only-monitoring use case is served by the renamed admin-observer profile.
  • **POST /v1/audit/enterprise-report/analyze (LLM-powered narrative analysis).** Wrapped an AWS-Bedrock-only Claude Haiku 4.5 call around /v1/audit/enterprise-report data. Removed because it cloud-locked the product and every input was already structured JSON. audit:analyze scope, llm_analysis feature flag, and @aws-sdk/client-bedrock-runtime optional dependency all dropped. Prompt templates preserved at docs/examples/enterprise-report-prompts.md for customers who want to replicate the narrative with their own model.
  • Sandbox runtime tier. RATE_LIMIT_SANDBOX env var, rateLimit.sandbox config key, and the old RATE_LIMIT_ENTERPRISE env var removed. New shape: RATE_LIMIT_AGENT (500 req/min unauthenticated fallback) + RATE_LIMIT_ADMIN (1000 req/min admin keys + tenant agent keys); platform keys exempt.
  • Documentation drift swept. Stale Swagger info text (3 roles listed as enterprise/agent/platform, "16-state lifecycle", "9 contract types" — actual is 13), stale **Auth:** enterprise, ... lines in 14 route schema files, "Enterprise users only see…" prose in mandates/events schemas, retired endpoint references in /llms.txt (POST /v1/mandates/agent, GET /v1/mandates/agent/principal, /v1/enterprises/.../approval-config, /v1/mandates/{id}/audit), RATE_LIMIT_SANDBOX example in docs/operations/DEPLOYMENT-HARDENING.md, trust_level row in docs/engineering/DESIGN-ENTITY-REFERENCES.md. docs/engineering/AGENT-TO-AGENT-IMPLEMENTATION.md retained with a HISTORICAL banner pointing to docs/strategy/PRINCIPAL-MODEL-REWRITE.md as canonical.

Added

  • Cascade-cancel actor attribution. MandateTransitionPayload + CascadeCancelJobData gain optional actor/parentActor fields; mandates.service.persistAndEmitTransition forwards actor on emit; on-cascade-cancel propagates payload.actor into boss.send job data; worker.ts uses parentActor when present (falls back to SYSTEM_ACTOR for in-flight pre-rewrite jobs).
  • **pickNamedPrincipal(body) helper in mandates.routes** — extracts the principalAgentId ?? agentId ?? performerAgentId defaulting policy so admin and platform branches share one rule.
  • **agents.max_scopes text[] (NOT NULL)** — per-agent scope envelope. Caps what any key issued to this agent may hold; defaults to the agent-full profile scopes at agent creation.
  • **enterprises.config.agents.* JSONB toggles** — per-tenant policy for delegation, self-commitment exceptions, agent-provisioning-by-agents.
  • **AGLedger Federation Signing Scheme v1 (already shipped, called out here for the breaking-changes audit):** unchanged from v0.19.x, but the principal-model rewrite intentionally did not touch federation wire format. Federated mandate-offer payloads still carry agent_id / contract_type / settlement_signal only — the principal model is local.

Migration notes

  • Dev / testbed: wipe DB and re-apply 001_consolidated.sql. pnpm db:migrate against an existing v0.19.x install will refuse with a checksum mismatch. Wipe via docker compose down -v && docker compose up -d postgres && pnpm db:migrate.
  • Provisioning YAML: role: enterpriserole: admin; scopeProfile: standardscopeProfile: admin-standard; standalone agents now require enterpriseSlug (the agent must belong to a tenant defined in the same YAML or already provisioned). trustLevel: field is silently ignored — drop it.
  • SDK / CLI / MCP: cross-repo fanout will go out alongside this release on agledger-agents (TS + Python SDK + CLI + MCP server). The compact license + the new role names + the unified POST /v1/mandates body shape are the customer-facing breaks SDK consumers will see. The TS/Python SDKs will go to 0.6.0 in lockstep.

Verification baseline at tag time

  • Unit tests: 1009/1009 passing across 58 files.
  • Integration tests: 713/713 passing across 26 files (run against bundled Postgres 18 in Docker).
  • Lint: 0 errors, 3 pre-existing complexity warnings on mandates.service.ts:createMandate (acceptable, untouched since pre-rewrite).
  • Trivy container scan: 0 CRITICAL/HIGH on gcr.io/distroless/nodejs24-debian12:nonroot-based image.
  • pnpm audit --prod: 1 moderate (transitive fast-xml-parser via @aws-sdk/client-license-manager); no critical/high.

0.19.262026-04-21

Patch release — real root cause of the idempotency race that v0.19.25 only partially addressed.

Fixed

  • **Idempotency race (#284 — F-420 launch-blocker): concurrent POSTs with a shared Idempotency-Key still produced 3–5 mandates.** v0.19.25 fixed a plausible symptom (xmax = 0 distinguisher) and a real secondary bug (fire-and-forget onSend write) but missed the true root cause: the onSend error-release branch ran DELETE FROM idempotency_keys WHERE key = $1 AND owner_id = $2 AND in_flight = TRUE, which deleted any in-flight row for the key — including the winner's. Trace: request 1 claims the key and runs the handler; request 2 arrives, hits the conflict path, throws ConflictError, returns 409; request 2's onSend fires the DELETE and wipes request 1's active claim; request 3 arrives, sees no row, INSERTs fresh, and runs the handler a second time. Every 409 under load produced one extra mandate. Matched the testbed reproducer exactly — 10/10 batches showed 3–6 unique mandates from 20 parallel POSTs. Fixed with a per-request claim_id uuid column on idempotency_keys: the winning request stores its UUID, ON CONFLICT DO UPDATE leaves claim_id untouched, and request.idempotencyClaim is set only on the winner. onSend's UPDATE and DELETE both filter by claim_id = $winner_uuid, so a conflicting request's writes affect zero rows. The new primitive is MVCC-independent — the xmax = 0 distinguisher is gone. Affects the same routes as v0.19.25's fix: POST /v1/mandates (+transition/cancel), agent-side mandate mutations, POST /v1/webhooks, POST /v1/disputes (+evidence/escalate). 001_consolidated.sql declares claim_id uuid NOT NULL directly (no separate 002 — pre-launch single-baseline strategy) with a checksum rebase for any dev DB already on the pre-column schema. Concurrency test strengthened from 2 parallel app.inject calls to 20; a new targeted test asserts the winner's row survives past a concurrent 409. Broader audit: the only other (xmax = 0) call sites are src/modules/provisioning/provisioning.repository.ts lines 106 and 137 — provisioning runs single-query-per-row inside withTransaction at startup with no hook-split race surface, and the xmax trick works reliably against vanilla PG in every test; left unchanged for this patch.

0.19.252026-04-21

Patch release — idempotency race fix, OTel Phase 1 metrics export, schema agent-discovery cache invalidation fix, and one route-path correction.

Added

  • OTel metrics export alongside prom-client (#278 Phase 1). Setting OTEL_EXPORTER_OTLP_ENDPOINT (already used for traces) now also exports the full custom metric set over OTLP/HTTP on a 30-second interval. /metrics Prometheus scrape continues to serve exactly the same data — OTel is an additional parallel stream, not a replacement. New src/modules/metrics/otel-bridge.ts exports defineCounter/defineHistogram/defineGauge helpers that declare a prom-client instrument AND bridge its .inc()/.observe()/.set() methods to a parallel OTel instrument via the @opentelemetry/api global meter. Zero overhead when no MeterProvider is installed (OTel's no-op meter pattern). @opentelemetry/api, @opentelemetry/sdk-metrics, and @opentelemetry/exporter-metrics-otlp-http promoted from transitive to explicit dependencies so a future sdk-node refactor can't strand us. Docs updated in docs/operations/OBSERVABILITY.md.

Fixed

  • Idempotency race: parallel POSTs with the same key ran the handler twice. Two distinct bugs in src/middleware/idempotency.ts: (1) the preHandler's INSERT ... ON CONFLICT ... RETURNING in_flight, response_code couldn't distinguish "I just inserted this row" from "row already existed, first request hasn't cached a response yet" — both look like in_flight=true, response_code=null, so two concurrent requests both fell through to the handler and both created mandates. Fixed by adding RETURNING (xmax = 0) AS inserted (PostgreSQL sets xmax to 0 on fresh inserts, to the current txn id on ON CONFLICT UPDATE) and gating the "continue to handler" branch on the inserted flag. (2) The onSend cache-write was fire-and-forget void pool.query(...), returning the response to the client before the UPDATE committed — a follow-up request could read the stale in_flight=true, response_code=null row and re-enter the handler. Fixed by awaiting the UPDATE/DELETE in onSend. Also migrated the hand-rolled 400/409 response shapes to throw new ValidationError(...) / throw new ConflictError(...) for RFC 9457 consistency. Affects all routes with { idempotent: true }: POST /v1/mandates (+transition/cancel), agent-side mandate mutations, POST /v1/webhooks, POST /v1/disputes (+evidence/escalate). Three new integration tests in src/test/concurrency.scenario.integration.test.ts — a parallel-POST repro that asserts count(*) FROM mandates = 1 under two concurrent requests, RFC 9457 body-shape assertion on the 409 conflict response, and a handler-error release test that verifies a failed request releases the idempotency key for retry. Four test files migrated from Date.now()-based idempotency keys to randomUUID() to eliminate test-level collisions under parallel load.
  • **/llms.txt + /llms-full.txt warm caches not invalidated on POST /v1/schemas / POST /v1/schemas/import / PATCH /v1/schemas/.../versions/... (#283).** Three schemas.handlers.ts write paths never called notifyInvalidation(), so the LISTEN/NOTIFY-based cache bust only fired on DELETE + admin flush. Customers registering a new custom contract saw stale /llms.txt for up to 300 seconds (LONG_TTL). Fixed by adding notifyInvalidation(db, { type: 'schema', contractType }) after each successful schema mutation. New src/test/discovery-cache.integration.test.ts regression guard: warms /llms.txt, POSTs a new contract, asserts the fresh contract appears without waiting for TTL.
  • **POST /admin/discovery/reload mounted at root instead of /v1/admin/ (#282).** One-line path fix in src/modules/discovery/llms-txt.ts — aligns with every other admin endpoint's /v1/admin/* convention. No backwards-compat alias kept; the endpoint was new in v0.19.23 and no customer automation exists yet.

0.19.242026-04-21

Patch release — SWLA v1.7 §2.5(h) unlicensed-use log warning, and two customer-facing documentation fixes.

Added

  • SWLA §2.5(h) unlicensed-installation log warning (#280). Installations running without a license key now emit a prominent banner at warn level on startup, and re-emit every 24 hours while unlicensed. Contract obligation under SWLA v1.7 §2.5(h): the recurring warning is the documented objection that breaks the implied-license-by-acquiescence chain. Fail-open behavior is unchanged — all features remain enabled, no exit, no degradation. By design there is no env var to silence the warning; frequency can only be reduced, never hidden. Installations with any valid license source (AGLEDGER_LICENSE, AGLEDGER_LICENSE_KEY, AGLEDGER_LICENSE_KEY_FILE, or AWS Marketplace) see no change.

Fixed

  • **Broken cosign.pub verification URL in SECURITY.md (#279).** The cosign verify instruction pointed at raw.githubusercontent.com/agledger-ai/agledger-api/main/deploy/cosign.pub, which 404s for unauthenticated customers because the server repo is private. Now points at https://agledger.ai/cosign.pub (verified live, already the canonical URL used in deploy/docker/DOCKER_HUB_README.md). docs/product/SECURITY-COMPLIANCE-ALIGNMENT.md had the same broken-private-repo pattern for the Policy link; now points at install/SECURITY.md.

0.19.232026-04-21

Pen-test surface reduction + agent-discovery restructure. Net -2,300 LOC. See release notes at <https://github.com/agledger-ai/agledger-api/releases/tag/v0.19.23> for the full list of retired/added routes, removed env vars, and dropped deps. This CHANGELOG entry is a back-fill; per-line details live in the release notes.

0.19.212026-04-21

Portal retirement + demo scaffolding cleanup. Self-hosted UI is explicitly out of scope for the core image — customers get API + CLI + SDK + MCP. If a web UI is ever shipped, it will be an opt-in component outside this image.

Removed

  • Customer portal module (~2,734 LOC). Magic-link / password / passkey self-service signup scaffolding that had defaulted to PORTAL_ENABLED=false with zero production callers. src/modules/portal/ (service, repository, routes, passkey helpers, token helpers, tests), src/shared/email-templates.ts (portal-specific mail bodies; escapeHtml inlined into the one remaining caller, auth/email-verification.service.ts), src/shared/password.ts (argon2 wrapper), and src/shared/email.test.ts all deleted. argon2 and @simplewebauthn/server dropped from package.json. PORTAL_* env vars, portal config block, portal public-route allowlist, portal Swagger security class, portal maintenance-queue cleanup tasks, and portalNextSteps() all gone. 9 /v1/portal/* routes retired.
  • Dormant demo scaffolding. config.demo.landingEnabled + DEMO_LANDING_ENABLED env var (zero readers in src/). deploy/compose/grafana/provisioning/dashboards/json/agledger-overview.json (drifted from current instrumentation — covered ~9 of the ~35 metrics the server exposes); directory preserved via .gitkeep, rebuild tracked under #271. Telemetry README references updated from "demo EC2" to "testbed EC2". install.sh "demo-seed API calls" comment dropped.

Changed

  • Migration 002 retired; folded into 001. Pre-launch single-baseline strategy continues — 002 never shipped as a standalone numbered file. Portal table removal (organizations, contacts, passkey_credentials, magic_link_tokens, portal_sessions + their FK/indexes) lives in 001_consolidated.sql directly. KNOWN_CHECKSUM_REBASES gets the old 001 checksum so existing installs auto-rebase on next migrate run. Upgraders from v0.19.20 and earlier keep their (empty) portal tables as orphaned state — the server no longer references them; can be dropped in a future incremental migration if needed.
  • Mirror script hardening (#275). mirror-install-repo.sh now splits main + tag push into two explicit git push invocations instead of --follow-tags. --follow-tags silently skipped the v0.19.20 tag despite it being reachable from the pushed commit; splitting the combined push makes each step's exit status meaningful and the existing ls-remote guard still fails loud on drift.

Retained (called out because the rewrite touched adjacent code)

  • shared/email.ts + nodemailer — enterprise email-verification still uses them.
  • Admin rate-limit IP exemption — generic feature, no longer portal-captioned.
  • "Activation portal" phrasing in admin.system.routes.ts — license activation, unrelated to the customer portal.

0.19.202026-04-20

Testbed #274 polish — agent-discovery prose fixes + nextSteps workflow projection. /simplify caught a gated-mode hint/action contradiction pre-tag.

Fixed

  • F-383 (agent discovery): Stale /transition refund prose dropped from lifecycleSection() in llms.txt. Test strengthened to catch both pipe-separated and prose forms of retired transition names.
  • F-334 (agent discovery): New "Verification modes" subsection covers auto / principal / gated and the /outcome body schema. /outcome bullet no longer framed as encrypted-only. Same fix applied to Swagger tag descriptions.
  • F-376 (nextSteps workflow projection): NextStep gains optional workflowStep / workflowTotal / workflowLabel / afterThis fields. mandateNextSteps and completionNextSteps project these onto the primary forward action using hasPerformer + verificationMode + operatingMode. Step math: 2 (+1 if verdict) without a performer, 4 (+1 if verdict) with one. Gated-mode COMPLETION_ACCEPTED no longer receives a workflow hint pointing at /outcome while the action list says "Check verification status".

Changed

  • Exported OperatingMode / VerificationMode from db/types.ts; tightened next-steps.ts params off string. Unified the "needs principal verdict" predicate.
  • 16 new tests lock in workflow step math and the gated-mode regression.

Build / release-ops

  • **mirror-install-repo.sh guards against detached-HEAD silent failures (5f5b1c8).** v0.19.19 shipped to the install repo with wrong content because the mirror ran while the install repo was on a detached HEAD: commit landed orphaned, main never advanced, git push --follow-tags silently skipped the tag, and GitHub auto-created the tag at v0.19.18's commit when the release was cut. Two guards now fatal on drift: (1) precondition checks the install repo is on main; (2) post-push compares remote tag SHA to local tag SHA. (Complements the two-ref push split in v0.19.21.)

0.19.222026-04-21

Fixed

  • **F-416 (#276): POST /v1/admin/license/reload with no body returned 400 body must be object.** Regression vs. v0.19.10; broke the post-install curl command shown in Helm NOTES.txt. Added a preValidation hook that defaults request.body to {} when absent, matching the pattern used for POST /v1/notarize/mandates/:id/accept. Body-shape validation still applies when a body is sent. Regression test added in admin.scenario.integration.test.ts.
  • **F-417 (#277): state-dependent rejections returned 400 instead of 422, breaking the SDK's UnprocessableError contract across ~20 endpoints.** The bug that surfaced on POST /v1/mandates/{id}/transition (INVALID_ACTION) was the symptom of a broader contract drift: every "request is valid, resource state won't accept it" path threw ValidationError (400) instead of a distinct 422 error. SDK customers using the documented catch (err instanceof UnprocessableError) branch hit the wrong path everywhere. Fixed at the root: - New UnprocessableStateError class (422, /problems/unprocessable-state) for state-dependent rejections. - InvalidActionError moved 400 → 422 (mirrors sibling InvalidTransitionError). - Migrated ~20 throw sites across mandates.service.ts, disputes.service.ts, webhooks.service.ts (6 webhook-lifecycle guards), compliance.service.ts, proxy-ingestion.service.ts, verification/outcome.service.ts, federation.service.ts, and federation-protocol.routes.ts from ValidationError to UnprocessableStateError. - Recategorized 4 authorization-style rejections ("only X can do Y") from ValidationError to ForbiddenError (403) — these were never body errors. - Response shape for InvalidActionError unchanged (currentState, attemptedAction, allowedActions, /problems/invalid-action) — only the status code moved. - Updated llms.txt error-recovery section to separate 400 (body malformed, read hint/suggestion/recoveryHint) from 422 (state issue, read currentState/allowedActions/detail). - 5 integration tests updated (state-rejection expectations 400 → 422), 1 new unit test for UnprocessableStateError.

0.19.182026-04-20

Install-script DR-runbook fix and upgrade-path remediation for the F-408 / F-410 regressions left behind by v0.19.16.

Fixed

  • **F-414 (restore.sh): non-TTY stdin silently aborted with exit 0.** Piped into a DR runbook, the script read no answer and returned success without restoring anything. Now proceeds as implicit --non-interactive with a WARN, matching the upgrade.sh precedent.
  • **F-415 (install.sh reconciliation).** COMPOSE_FILE and AGLEDGER_VERSION reconciliation now runs on both fresh and existing .env, after detect_db_mode has set USES_BUNDLED_PG authoritatively. Existing-.env branch prints a summary of what was added/updated and warns when REGISTRATION_ENABLED=true lacks an opt-in marker.

Added

  • **upgrade.sh config-state check.** New "Checking configuration state" step surfaces REGISTRATION_ENABLED=true-without-marker and missing-COMPOSE_FILE before the confirmation prompt. COMPOSE_FILE is auto-added (safe default); REGISTRATION_ENABLED is warn-only (customer intent unknown).
  • **remediate-env.sh helper.** Applies F-408 and F-410 fixes to an existing .env with --dry-run and --non-interactive support. Idempotent; mirrors automatically via the existing rsync.

0.19.172026-04-20

Same-day re-apply of 5 install-script fixes wiped by the v0.19.16 mirror. Establishes the canonical-source rule for deploy-scripts.

Fixed

  • Install-repo mirror wipe. The v0.19.16 mirror script (rsync -a --delete from agledger-api/deploy/ into agledger-ai/install) wiped 5 install-script fixes that had been applied directly in the install repo instead of the canonical deploy/ source. Testbed retest confirmed v0.19.16 still reproduced F-392, F-397, F-399, F-408, F-410. Re-applied to agledger-api/deploy/scripts/ so the next mirror preserves them: - **F-392/F-405 (install.sh):** EXISTING_PLATFORM_KEY detection reuses the previous platform key on reinstall and de-dupes any prior PLATFORM_API_KEY lines. Refuses to truncate non-empty .env if the filter produces an empty result. - **F-408 (install.sh):** REGISTRATION_ENABLED defaults to false (matches .env.example posture). Removed the sed line that flipped it to true. - **F-410 (install.sh):** persists COMPOSE_FILE in .env via upsert_env_var so manual docker compose <cmd> from the install dir keeps the prod + bundled-pg overlays. - **F-399 (uninstall.sh):** fails loud with exit 2 when stdin is not a TTY and --non-interactive was not passed. - **F-397 (lib-compose.sh):** WARN to stderr when falling back to a stale cache after Docker Hub failure, with the cache age.

Changed

  • **New upsert_env_var KEY VALUE FILE helper in lib-compose.sh.** Used by install.sh (COMPOSE_FILE, AGLEDGER_VERSION) and upgrade.sh (AGLEDGER_VERSION) — collapses 3 duplicated grep/sed/echo patterns.

Notes

  • No API code changes. Migration 001 unchanged.

0.19.162026-04-20

Pre-pen-test testbed sweep — 22 of 23 F-391→F-413 findings closed, including a critical audit-vault DDL-guard bypass.

Fixed

  • **F-412 (critical): audit_vault DDL-guard bypass.** DROP TABLE audit_vault* and DROP TABLE federation_audit_log previously bypassed the BEFORE TRUNCATE protection triggers because DML triggers don't fire on DDL. Added a sql_drop event trigger (agledger_block_audit_drop) that blocks the drops at the DDL layer, with a documented bypass (SET LOCAL agledger.allow_audit_drop = 'on') for legitimate partition archival. Aurora/RDS lacks user-defined event triggers — verifyVaultFull and /audit-export checkpoint cross-check stays the detection layer for those paths.
  • F-391 through F-411, F-413 (22 findings). Install-script, discovery-surface, validation, and error-response polish across the first external-install batch. Full list and repros in testbed FINDINGS.md.

Notes

  • F-411 deferred to v0.19.17+ (not user-facing; cosmetic).

Fixed

  • **F-415 (#273): REGISTRATION_ENABLED detection defeated by inline comments in .env.** v0.19.18 landed the remediation code but the parser at upgrade.sh:144, remediate-env.sh:68, and install.sh:353 used cut -d= -f2- | tr -d '[:space:]', which doesn't strip trailing shell comments. Against a v0.19.16-seeded .env that ships REGISTRATION_ENABLED=true # Open self-service registration ..., the parsed value became true#Openself-serviceregistration(default:disabled) — the == "true" check failed, the F-408 warning never fired, and remediate-env.sh --dry-run reported "no remediation needed" on an actively exposed install. Replaced all three sites with a new get_env_value helper in lib-compose.sh that strips [[:space:]]+#.*$ before returning the value, matching docker-compose's native parser behavior. Verified against the testbed reproduction.
  • **#269: stale demo.agledger.ai references.** The hosted demo service was retired on 2026-04-20. Updated AGENTS.md (API base URL section now points customers at their own self-hosted instance), ARCHITECTURE.md (domain routing table, deployment diagram, Phase 6 checklist), PRODUCT-SPEC.md (Seed bullet), docs/strategy/INVESTOR-PLATFORM-SUMMARY.md (Deployments table), deploy/docker/DOCKER_HUB_README.md (removed Demo link), scripts/test-a2a-sdk.ts, scripts/test-a2a-agent.ts, scripts/generate-route-manifest.ts (example commands), and src/shared/email.test.ts (magic-link assertion).

0.19.152026-04-20

Patch release — three regressions caught by post-v0.19.14 smoke testing, plus single-baseline migration consolidation per the pre-launch strategy.

Fixed

  • **/healthz, /readyz, /livez returned 401 in production.** The auth middleware's public-route allowlist was missing the new canonical aliases added in v0.19.14. Added to PUBLIC_EXACT_ROUTES with regression coverage in the authorization integration suite.
  • **install.sh silently failed to create the platform API key.** Network detection used docker compose ps --format json | jq '.Networks | keys[0]', but Networks is a comma-separated string, not an object. The resulting fallback (docker-compose_default) didn't match the actual compose project name, so the init.js docker run couldn't reach Postgres. Replaced with a three-method fallback: docker inspect on the postgres container, Networks-as-string parse, then compose_default as last resort.
  • Helm chart was published unsigned despite install-repo README instructing customers to verify it. /deploy skill step 7 now signs the chart after publish.
  • **SLSA attestation used cosign --type slsaprovenance (v0.2 schema)** but our predicate is SLSA v1.0 shape (buildDefinition/runDetails); cosign silently accepted the attestation with an empty predicate. Switched to --type slsaprovenance1 everywhere (attest, verify, docs). v0.19.14 attestation was re-generated during release with the correct type.

Changed

  • Migrations consolidated back to single baseline. Folded 002_audit_vault_truncate_protection.sql into 001_consolidated.sql, matching the pre-launch single-baseline strategy. Deployed v0.19.14 systems auto-rebase via KNOWN_CHECKSUM_REBASES; existing _migrations row for 002 stays as a harmless history record. ensure_future_partitions() in 001 now also attaches TRUNCATE triggers to new audit_vault partitions (previously only added in 002, now unified).

Added

  • Three new regression tests in src/test/authorization.scenario.integration.test.ts asserting /healthz, /readyz, and /livez don't require auth.

0.19.142026-04-20

Launch-readiness build plan Phases 0, 1, 2, and 3 land together. Net effect: self-hosted install path is ready for a real customer from clone to running server, with full supply-chain artifacts attached to every release.

Added

  • Public install repo at [agledger-ai/install](https://github.com/agledger-ai/install). Scripts, compose files, Helm chart, cosign key, air-gap DIY guide. install.sh resolves the latest stable version from the Docker Hub tags API (no more stale hardcoded default).
  • Canonical Kubernetes health aliases. /healthz/health, /readyz/health/ready, /livez (trivial 200, no DB probe). Existing /health, /health/ready, /status unchanged — Helm chart probes keep working.
  • Compact license activation string. New AGLEDGER_LICENSE=agl_<tier>_v1_<base64url> env var as a paste-friendly alternative to the PEM file. Same Ed25519-signed envelope, same security model. Precedence: AGLEDGER_LICENSE > AGLEDGER_LICENSE_KEY > AGLEDGER_LICENSE_KEY_FILE. Active source logged at boot.
  • **POST /v1/admin/license/reload body support.** Accepts {"license": "agl_..."} or {"licenseKey": "-----BEGIN..."} for hot-reload without restarting. Empty body preserves existing env-reload behavior. 400 if both fields are set.
  • Generator emits both formats. scripts/generate-license.ts writes .pem and .key side-by-side; self-check parses both through the production parser before writing.
  • SLSA L2 build provenance. Every release now produces a signed in-toto attestation (agledger-<version>-slsa.intoto.jsonl) linking the Docker Hub image digest to its source commit and CodeBuild run. Customers verify with cosign verify-attestation --type slsaprovenance.
  • Cosign bundle as release artifact. Standalone agledger-<version>-cosign.bundle file attached to the install-repo GitHub release alongside the SBOM.
  • Monitoring README. agledger-ai/install/monitoring/README.md signposts the Grafana dashboard JSON for customers running their own Grafana outside the bundled monitoring profile.
  • **.env.example documents AGLEDGER_LICENSE** with the full precedence chain.

Fixed

  • GET /.well-known/security.txt Policy field pointed at the private agledger-ai/agledger-api repo, which 404s for any customer hitting the endpoint. Now points at the public agledger-ai/install/blob/main/SECURITY.md.
  • Several customer-facing docs referenced a raw.githubusercontent.com/agledger-ai/agledger-api/... URL that was never reachable (repo is private). Docs, Docker Hub README, and agledger-web install guide all rewritten to point at the public agledger-ai/install repo.

Changed

  • /deploy skill now includes steps 5d (cosign sign + bundle), 5e (SLSA provenance), 8 (mirror to install repo), and 9 (Docker Hub README sync). New mirror-install-repo.sh helper runs shellcheck + install.sh smoke test before mirroring.
  • wait_for_http helper factored into lib-compose.sh; consumed by both install.sh and the mirror script.

Removed

  • deploy/scripts/bundle-airgap.sh — air-gap now uses install.sh --image <internal-registry> plus a documented DIY docker save flow. No tarball release asset.

0.19.132026-04-20

Fixed

  • Licensing activation. scripts/generate-license.ts wrote PEM headers/footers with 3 dashes but the parser required 5 — every generated license was rejected. Generator now also self-checks its output before writing.
  • DEFAULT_PUBLIC_KEY_HEX in src/modules/licensing/license.service.ts was the placeholder zero key. Baked in the real Ed25519 public key; corresponding private key stored in AWS SSM SecureString /agledger/license-private-key (us-west-2).

Changed

  • docs/engineering/DESIGN-LICENSING.md: corrected stale tier names (starter_registered/profree/enterprise), replaced "Starter mode" with "Developer Edition", rewrote feature-gate section to match the actual audit-only behavior (no 403s, WARN logs only).

Closed

  • #267: CHANGELOG backfilled with condensed summaries for v0.11.0–v0.19.12. Full release notes remain on GitHub Releases.

0.19.0[0.19.12] - 2026-04-13 – 2026-04-19

Condensed — see GitHub Releases for full notes.

  • 0.19.12 (2026-04-19): Fastify/event-bus/Node-24 audits; cosign SSM key migrated us-east-1→us-west-2; dependency patch bumps.
  • 0.19.11 (2026-04-18): Closed #257 (dead settle/refund transition enum), #266 (federation signing wire format public), #250 (opt-in dispute auto-escalation).
  • 0.19.10 (2026-04-17): Migration 002 — BEFORE TRUNCATE triggers on audit_vault + federation_audit_log block out-of-band wipes. Closed #265 P0.
  • 0.19.9 (2026-04-17): PG/pg-boss audit, reputation cache-read fix (#258), GROUPING SETS collapse (#260), BRIN/fillfactor tuning.
  • 0.19.8 (2026-04-16): Agent-UX fixes (transition 400 enumerates currentState+allowedActions), /llms.txt rewrite, protobufjs + @fastify/static CVE overrides.
  • 0.19.7 (2026-04-16): Route-plugin splits (federation/admin/portal/mandates), 197 requiredScopes migrated to Scopes.* constants, Docker Hub README landing page.
  • 0.19.6 (2026-04-15): Node 24 LTS, supplier verification fix (#246).
  • 0.19.5 (2026-04-14): Pagination total:-1 semantics, 9 silent error fixes, /llms-agent.txt.
  • 0.19.4 (2026-04-14): Discovery hints in 404 responses (#248).
  • 0.19.3 (2026-04-14): 15+ bug fixes from pattern scans, 37 new admin tests, exploratory testing phases 1–10 (471 new tests).
  • 0.19.2 (2026-04-13): Compliance hardening, SBOM + cosign + Trivy pipeline, admin 403 fix.
  • 0.19.1 (2026-04-13): Security scan remediation, business metrics endpoints.
  • 0.19.0 (2026-04-13): Node 22 LTS, TypeScript 6.0, ES2025, Helm chart hardening.

0.18.0[0.18.3] - 2026-04-11 – 2026-04-12

Condensed — see GitHub Releases for full notes.

  • 0.18.3 (2026-04-12): Enforcement config wiring, configurable cascading-verification limits.
  • 0.18.2 (2026-04-12): 10 silent-failure fixes, federation mandate-offer state fix.
  • 0.18.1 (2026-04-11): 18 audit findings, cascade cancellation, durability hardening.
  • 0.18.0 (2026-04-11): A2A v1.0 spec alignment (10 wire format fixes).

0.17.0[0.17.3] - 2026-04-09 – 2026-04-11

Condensed — see GitHub Releases for full notes.

  • 0.17.3 (2026-04-11): Testbed findings #215–219.
  • 0.17.2 (2026-04-10): Verification keys .well-known endpoint, signing key registry.
  • 0.17.1 (2026-04-09): AWS Marketplace License Manager integration.
  • 0.17.0 (2026-04-09): HA patterns, telemetry receivers.

0.16.0[0.16.1] - 2026-04-07 – 2026-04-08

Condensed — see GitHub Releases for full notes.

  • 0.16.1 (2026-04-08): llms.txt, telemetry polish.
  • 0.16.0 (2026-04-07): Self-hosted identity refactor (AGLEDGER_EXTERNAL_URL cascade, relative Problem Details URIs, rebranded schema $ids).

0.11.0[0.15.x] - 2026-03-29 – 2026-04-07

Condensed — see GitHub Releases for full notes.

  • 0.15.x: Provisioning directory (Grafana-style YAML config-as-code), managed_by = 'provisioning'.
  • 0.14.x: Display status, deploy packaging (Dockerfile, Helm chart, scripts).
  • 0.13.x: Entity references, vault signing key rotation, licensing scaffold, built-in contract types.
  • 0.12.x: Federation completion (Phases C/D/E) — Gateway/Hub modes, cross-repo state sync.
  • 0.11.x: A2A v1.0 foundations, federation architecture.

0.10.92026-03-25

Removed

  • #137: ECS Fargate, ALB, WAF, RDS, CDK stacks (infra/ directory), CloudWatch alarms, SNS alerting
  • #137: Ops alerting module (/v1/ops/sns-webhook, /v1/ops/alerts, /v1/ops/status)
  • #137: agenticclearinghouse.org from CORS allowlist
  • #137: Scripts: services.sh, setup-cloudwatch-alarms.sh, setup-xray-tracing.sh
  • activeIncidents from /status response schema (no source after CloudWatch removal)

Added

  • #138: Static openapi.json included as GitHub Release artifact (for agledger.ai/docs)
  • #139: CI sends new-commit dispatch to self-hosted on main push — builds SHA-tagged images (sha-abc1234) for testbed
  • src/shared/db-health.ts — shared DB health check utility (replaces duplicated code in /status and admin service)

Fixed

  • #140: pgboss schema privileges not granted to agledger_app role when DB role separation is in use — queue operations failed under separated roles
  • .env.example had https://localhost:3000 (should be http)

Changed

  • Deployment model: Self-hosted only (Docker Compose / Helm). No hosted API. api.agledger.ai retired.
  • CI pipeline: Simplified — build + test + release tarball + notify self-hosted. No ECS deploy step.
  • API docs: demo.agledger.ai/docs (live) + agledger.ai/docs (static OpenAPI)
  • Database: Aurora PG 15+ recommended (was RDS PG 18)
  • Default URLs changed from api.agledger.ai to localhost:3000 across config, MCP, A2A, swagger
  • OpenAPI server URL changed from absolute to relative /v1
  • 20+ documentation files updated to reflect self-hosted model

0.10.82026-03-25

Fixed

  • #131: Migration checksum mismatch on v0.10.6→v0.10.8 upgrade — added KNOWN_CHECKSUM_REBASES map to migration runner for automatic checksum rebasing
  • #132: PATCH /admin/webhooks/:id/circuit-breaker returned 500 — PostgreSQL inconsistent types deduced for parameter $1 (added ::varchar cast)
  • #133: Expression engine boolean == true comparison confirmed correct; added diagnostic trace that notes empty metadata/mandate context on evaluation failure
  • #135: pg-boss #monitor crashed every 60s — pool adapter returned { rows: undefined } instead of { rows: [] } (defensive ?? [])
  • Webhook SQL OR-precedence bug: event_types IS NULL OR $1 = ANY(event_types) missing parentheses — inactive/paused/circuit-broken subscriptions could receive webhook deliveries
  • A2A integration test expected FORBIDDEN (-32004) but handler correctly returns TASK_NOT_FOUND (-32001) per OWASP A01:2021
  • Webhook integration tests: wrong field name (eventsevent_types) + cross-test isolation via owner_id filter

Added

  • #134: IP-based rate limit exemptions — PUT/DELETE /admin/rate-limit-exemptions/ip/:ip, GET /admin/rate-limit-exemptions/ips. Env var: RATE_LIMIT_EXEMPT_IPS. Covers unauthenticated portal routes.
  • #131: Additive migration 003_pg15_compat.sql — PG 15+ polyfill + managed PG role hardening for v0.10.6 upgrade path
  • src/shared/security.ts — shared DANGEROUS_SEGMENTS constant (consolidated from 4 duplicate definitions)
  • 5 new expression engine unit tests for boolean comparison edge cases (#133)

Changed

  • #134 / #136: All per-route rate limits across 13 route files now use exemptAwareMax() — enterprise and IP exemptions cascade to every route (zero hardcoded max: N remaining)
  • Global rate limit max function now checks exemptIPs for IP-based exemptions on all routes

0.10.72026-03-25

Fixed

  • #128: Principal verification mode (verificationMode: 'principal') now enforced across all code paths — defense-in-depth in executeVerification(), cascading verification respects principal/gated modes (F-308)
  • #121: Custom enterprise schema types now found during agent mandate creation — enterprise ID resolved before schema validation
  • #122: Delegation depth enforcement uses enterprise config (not defaults) for delegated mandates; advisory warnings now surfaced in API response as advisoryWarnings
  • #123: POST /outcome returns 403/404 for unauthorized callers before surfacing body validation errors
  • #67: GET /schemas/:type/rules returns fieldMappings for custom types (was filtering out empty arrays)
  • #129: CREATE ROLE in migration wrapped with EXCEPTION WHEN insufficient_privilege for managed PG (RDS, Aurora, CloudSQL, Azure)
  • #130: Preflight validates pg-boss schema creation permissions and connection pool headroom; added warning level (yellow ⚠)

Added

  • #127: UUIDv7 polyfill for PostgreSQL 15-17 — minimum PG version dropped from 18 to 15. Migration 002 auto-drops polyfill on PG18 upgrade.

Changed

  • Minimum PostgreSQL version: 18 → 15 (native uuidv7 on PG18+, pure-SQL polyfill on PG15-17)
  • Role separation is now optional — migrations gracefully skip on managed PG without CREATEROLE
  • Renamed _advisoryWarningsadvisoryWarnings in mandate response (convention consistency)

0.10.62026-03-25

Fixed

  • #67: Schema rules endpoint returns fieldMappings and commissionSourceField for built-in types (sync fallback was stripping them); platform keys can now read enterprise-scoped custom schemas without ?enterpriseId=
  • #101: Webhook DLQ retry now includes subscriptionUrl and mandateId in re-enqueued jobs (was missing, causing worker failures)
  • #107: MANDATE_CREATED audit entries include changedBy field for SOC 2 compliance (was conditionally omitted)
  • #108: Agent registration with enterpriseId no longer silently swallows auto-approval failures
  • #118: Worker polling interval reduced to 500ms for faster job pickup under load
  • #121: commissionPct allowed on top-level agent-to-agent mandates (was restricted to sub-mandates only)
  • #125: Enterprise config PATCH validates enforcement fields at the Fastify schema level (was accepting invalid enum values)
  • #126: Expression engine sum() supports single-arg form for flat numeric arrays (sum(evidence.line_totals))

Added

  • #84: Pagination metadata (total, hasMore, nextCursor) on 5 list endpoints: fleet capabilities, rate-limit exemptions, agent reputation, projects, schema versions

0.10.52026-03-25

Fixed

  • #120: Expression engine len() now handles strings (was array-only)
  • #119: Expression engine mandate context exposes camelCase aliases (chainDepth, contractType, createdAt)
  • #124: Enterprise enforcement config PATCH deep-merges nested objects (was JSONB shallow overwrite)
  • #123: POST /outcome runs auth before body validation via attachValidation: true

Added

  • #118: Configurable worker concurrency via WORKER_VERIFICATION_CONCURRENCY (default 5) and WORKER_WEBHOOK_CONCURRENCY (default 10) env vars
  • #122: Advisory-aware delegation depth checking in both enterprise and A2A mandate paths
  • #121: Schema registration validates commissionSourceField exists in mandateSchema and is numeric

Changed

  • Repository delegation depth safety cap raised from 5 to 10 (matches max configurable maxDelegationDepth)
  • Extract shared checkDelegationDepth() helper to deduplicate enforcement logic

0.10.42026-03-24

Fixed

  • #115: Block performer agents from enterprise transitions — assertPrincipalAgent now requires explicit principal_agent_id for agent keys
  • #116: Surface advisory warnings in completion response — structuralValidation: 'WARNING' + warnings array when schema validation is advisory
  • #117: Apply per-mandate enforcementOverrides during verification — both Phase 1 (completion submission) and Phase 2 (verification worker) now respect mandate-level overrides; reject tightening overrides at creation time

Changed

  • Upgrade Vitest 3 → 4.1.1 (workspace → projects config, poolOptions flattened)
  • Consolidate ENFORCEMENT_MODE_ORDER and ENFORCEMENT_OVERRIDE_FIELDS into enterprise-config.schema.ts
  • assertPrincipalAgent is now sync (no DB call needed)

Added

  • pg-boss Prometheus metrics: agledger_pgboss_queue_size{queue,state}, agledger_pgboss_queue_created_total{queue}
  • Heartbeat monitoring on Phase 2 verification queue (pg-boss 12.12+, 30s interval)

0.10.32026-03-24

Changed

  • Upgrade ESLint to v10, fix 20 lint errors
  • Code quality: migrate 36 manual transactions to withTransaction(), extract shared enterprise-scoping helpers, consolidate cursor/pagination into src/shared/cursor.ts, clean 48 unused functions + 108 unused types

0.10.22026-03-24

Added

  • Project entity (POST/GET/PATCH/DELETE /v1/projects) with enterprise scoping (#62)
  • Mandate dependencies — informational dependsOn array (#63)
  • Graph visualization backend (GET /v1/mandates/:id/graph) (#29)
  • Secrets manager abstraction (env, ssm, vault providers) (#12)
  • Enforcement config phases 2-3: per-field modes, per-mandate overrides, max submissions enforcement (#81)

Fixed

  • Auth regression: #113 fix over-blocked principals (#114)

0.10.12026-03-24

Added

  • Centralized test infrastructure (shared mock pool, SQL handler patterns)
  • external_task_id on mandates for CI/RPA integration (#61)

Changed

  • Custom schema flexibility: all 6 sub-items of #82
  • Enterprise enforcement config phase 1 (#81)

0.10.02026-03-24

Fixed

  • Auth regression fixes (#112, #113)

0.9.92026-03-24

Added

  • Webhook circuit breaker: per-subscription failure tracking with auto-pause (#89)

Fixed

  • Circuit breaker race condition and fan-out waste

0.9.82026-03-24

Added

  • FRIA compliance record type (Art. 27 EU AI Act) (#78)
  • Per-route OpenAPI security scheme auto-assignment (#85)

0.9.72026-03-24

Added

  • canonical_payload TEXT column for reliable hash verification (#77)

Fixed

  • Agent mandate visibility for enterprise-associated agents (#111)

0.9.62026-03-24

Changed

  • Dependency cleanup, drift fixes

0.9.52026-03-24

Fixed

  • 11 testbed bugs: webhook hardening, schema delete, admin mandates
  • OpenAPI generator (pnpm openapi)

0.9.42026-03-24

Fixed

  • 6 testbed bugs: UUIDv7 migration, schema param indexing, webhook DLQ/retry

0.9.32026-03-24

Added

  • Shared schema primitives for route definitions
  • Webhook DLQ management (pause, update, retry)
  • Auth cache invalidation tests

0.9.22026-03-24

Fixed

  • 4 testbed bugs: principal auth, audit signatures, rate limits, duplicate transitions

Changed

  • PostgreSQL 16 → 18 references updated

0.9.12026-03-23

Fixed

  • Version reporting, CI version guard

0.9.02026-03-23

Changed

  • Audit LLM analysis: Anthropic API → AWS Bedrock (IAM auth, no API key)

0.8.92026-03-23

Added

  • Migration 004: separate DB roles (app, owner, monitor)

0.8.72026-03-23

Changed

  • Upgrade PostgreSQL 16 → 18
  • UUIDv7 default primary keys via native uuidv7()

0.8.02026-03-23

Changed

  • BREAKING: Pre-launch schema changes — partitioning, column cleanup, naming (#84)
  • Migration consolidation into 001_consolidated.sql

0.7.32026-03-22

Added

  • Licensing skeleton: Ed25519 validation, admin endpoint, generate script

0.7.22026-03-22

Added

  • Constraint inheritance (none/advisory/enforced) for delegation chains
  • validTransitions in mandate responses
  • Error enrichment with field hints for agent self-correction

0.7.02026-03-21

Added

  • Customer portal: magic link auth, CRM-ready org/contacts model
  • Passkey (WebAuthn/FIDO2) authentication
  • Demo landing page

0.6.02026-03-20

Changed

  • DB pressure reduction, rate limit configurability
  • Cross-replica cache coherence via LISTEN/NOTIFY

0.5.02026-03-19

Added

  • Expression-based verification rules for per-customer customization
  • Schema registry RBAC

0.4.02026-03-18

Added

  • Agent-to-agent mandates (delegation, proposals, counter-proposals)
  • Webhook delivery system with HMAC signing, DLQ, circuit breaker
  • Encrypted mandate mode (AES-256-GCM)

0.3.02026-03-16

Added

  • Mandate lifecycle: 17-state machine, two-phase verification
  • Audit vault with SHA-256 hash chain + Ed25519 signatures
  • 11 built-in contract types
  • API key management with HMAC-SHA256 hashing