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/amd64andlinux/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) andAGLEDGER_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 companionagledger_federation_outbound_projection_skipped_totalstays 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_affectedstatements** (#963, #964) - unfixable base-image CVEs ship with signed justifications, and/pre-deploychecks 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=truedefault, 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_changedevent. - 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_SIZEdefaults 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.
- **
chainIntegrityreads are batched on the compliance export** (#998) so the regulator packet clears the socket window at the 10k row cap. - **
vault_checkpointsis 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: truewith 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. - **
agentCardUrlandpeerUrlheld 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-uribumped 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.signedAton 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. - **
recordStatuson 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_heldevent, so a held cascade is observable and resolvable rather than silently stuck. - **
AGLEDGER_FEDERATION_SCHEMA_REF_POLICYstrict mode** (#910) — closes the inboundschemaRefbypass; 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
evidenceHashbinding, 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/REGISTEREDpast their deadline transition toTIMED_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
graceSecondsintake** (#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
enforcementOverridesin the schema (#928). - **Bulk create honors the batch-level
Idempotency-Keyheader** (#927), plus bulk field parity and a disputeterminalReasonleak fix (#926). - Gate fail-closed hardening — unresolvable
asyncRuleIdsfail 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_ACTIONSis pinned toINTENT_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
- **
recordTypeswebhook 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/fulfilledAtinstants (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.* - **
rulesConfigecho on schema writes;/acceptrationale aliases** (#855, testbed F-795/F-796) — schema create/update responses echo the resolvedrulesConfig; 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-inuuidv7(), retiring the polyfill. A rebase in that change droppedrecord_typesDDL and an expiry index; restored in #864, which also drops a write-only events index. - **Reject
gateMode: autooverride 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
fieldMappingruleId 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 asverified: 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_REJECTsettlement 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.shremote 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_URLas 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/elsein 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-Ofhonored 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-deployrelease 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.sqlregenerated 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 added003_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 whoseawaitingActoris 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-rowawaitingActoralternative.awaitingActorSql()is pinned toderiveAwaitingActorby a 432-combination equivalence sweep against real PostgreSQL./llms.txtrecovery 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 asverified: false,reason: record_projection_drift,driftFields[]. Sibling fix:PATCH /v1/records/{id}(DRAFT-stage edits) now notarizes aRECORD_UPDATEDchain 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) whileVAULT_ANCHOR_ENABLED=false; posture surfaced atGET /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).
deriveAwaitingActorreturnsprincipalfor DRAFT/REGISTERED (every forward action is principal/org-admin-only) and is acceptanceStatus-aware (counter-proposed awaits the principal). Performer-only viewers get wait-guidancenextStepson 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
allowedActionsplus arecoveryHintspelling the exact transition sequence for the caller's situation; DRAFT self-commitments get register → activate guidance (no counterparty to propose to);humanOversightis one strict Art. 14 shape everywhere (now also onPOST /v1/records/{id}/ai-impact-assessment) and any validation failure returns the full shape inrecoveryHintin 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 webhookeventTypesenum documents that it is exactly the subscribable set and explains the/v1/eventssuperset; dev boot withoutAPI_KEY_SECRETwarns about the fallback-secret 401 footgun. - Per-process OTel service name (#725). API and worker entrypoints declare
agledger-api/agledger-worker(operator-suppliedOTEL_SERVICE_NAMEstill wins); unwired env vars dropped from.env.example. install.shsplits the tag off--imageso it can't compose an invalid ref (#722);/llms-full.txtwording 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:devimage is built on demand bydeploy/scripts/build-dev.sh(--pullkeeps 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.sqlis immutable; the migrate runner refuses to rebase any applied migration's checksum.AGLEDGER_LAUNCH_FROZEN=trueships by default (helm configmap, basedeploy/compose/docker-compose.ymlmigrate service, and.env.example), andKNOWN_CHECKSUM_REBASESis 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.tsare retired (the rebase path is unreachable with an empty map); the throw-on-mismatch andLAUNCH_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). Newschema_subjects.default_gate_modecolumn (row-only metadata, not in the canonical manifest body/digest;auto|principal). When a record-create payload omitsgateMode, the engine resolves: explicit per-recordgateMode→ the type'sdefault_gate_mode→ engine defaultauto. Backfills the seededprincipal-gate-generic-v1example toprincipal(it previously ranautoand 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 withprincipalAgentId. TheagentIdalias is stripped — no back-compat (pre-launch posture; no consumers in flight).- DB column rename:
records.agent_id→records.performer_agent_id. Matchesverdict_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 viaKNOWN_CHECKSUM_REBASES. - A2A
create_recordintent:agentId(overloaded as principal alias) is replaced by explicitprincipalAgentId. The intent now acceptsprincipalAgentIdandperformerAgentIdas distinct fields. - Federation wire body:
agentIdfield on state-transition envelope renamed toperformerAgentId. 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.txtevent-payload docs updated to reflect the renamed field names./llms.txtreputation-endpoint paths (/v1/agents/{agentId}/...) and theagent.reference_addedevent are unchanged — thoseagentIdreferences 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_hashis 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}/attestationprojects the chain to sigstore-compatible bundles. Read-only export — chain is COSE_Sign1, projection is for tooling interop. - **
/predicates/<kind>/v1schema 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
Completioneverywhere (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=trueon/audit-export. Each entry'sintegrity.receiptis 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) atPOST /v1/scitt/entries(register a customer-prepared Signed Statement → 201 + Receipt asapplication/cose),GET /v1/scitt/entries/{id}(Transparent Statement),GET /.well-known/scitt-keys+/{kid}(CBOR COSE_KeySet, unauthenticated). 4xx/5xx on/v1/scitt/*returnapplication/concise-problem-details+cborper 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; anIdempotency-Keyheader is forward work.
Audit-chain integrity
- Cose/payload drift check.
validateVaultEntrydecodes the predicate fromcose_sign1and deep-equals against a re-projection ofrow.payloadvia the newbuildPredicateOnlyhelper. 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-exportreaders. Surfaces aschainIntegrityReason: 'payload_drift'+failure: 'payload_drift'+ per-entryintegrity.valid: false. Mirrored in@agledger/verifywith newCHAIN_PAYLOAD_DRIFTfailure code. - **SCRAPI routes go through
authorizeAction.**REGISTER_SCITT_ENTRY+READ_SCITT_ENTRYdeclared inauthority/declaration.ts; SCRAPI routes participate in the authority mutation pass (135 mutations, all detected).
Operational
- Customer-tunable rate-limit knobs.
RATE_LIMIT_ROUTE_MULTIPLIERscales 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. DefaultPOST /v1/recordsraised 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
/predicatesauth + SCRAPI response codes + media types. - SCRAPI error-message leakage (DB/ORM errors no longer included in the CBOR
detailfield on 500s; full err goes to server log). - Concurrent integration helpers:
verifyChainInvariantsupdated 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
$refswap on/predicates/{kind}/v1.
Internal
decodeCoseSign1ToClaimround-tripson_behalf_of+traceparentinto envelope metadata.extractEnvelopeExtensionspulls 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=100vsPRIORITY_STATE_TRANSITION=0) caused the signal POST to race ahead of the state-transition POST within the same pg-bossgroupConcurrency:1group on the receiver, soprojectSettlementSignalno-op'd on the missing row → noFEDERATION_SIGNAL_RECEIVEDaudit entry, nofederation.settlement.signalevent, nosignal.emittedwebhook delivery. Equalized toPRIORITY_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.completewebhook event was in the OpenAPI enum, subscribed by the dispatcher, recommended in/llms.txt— but never emitted. Auto-rollup firedverification.completefor the parent but not the cascading variant. Now emits both alongsidesettlement.signalfromapplyParentRollupOutcome.- Federation REJECTED state was dispatched to peers as webhook event
record.rejected, a name not inWEBHOOK_EVENT_TYPES— targeted subscriptions could not match. Normalized torecord.proposal_rejectedto 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_changednow carriesprincipalAgentId+agentId;federation.settlement.signalnow carriesoutcomeHash,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
assertWebhookOwnershape: 404 only on genuine missing, 403 +WEBHOOK_NOT_OWNEDrecoveryHint on exists-but-not-yours. ConstantsWEBHOOK_NOT_OWNED_CODE+WEBHOOK_NOT_OWNED_HINTexported fromwebhooks.service.tsand shared with admin route. - Compliance read had an orthogonal-ID leak: URL
/v1/records/:recordId/compliance-records/:complianceRecordIdauthorized only:recordId, then the service fetched the compliance record by:complianceRecordIdalone — 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 assertsrecord.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 indb/types.tswith zeroappendToVaultcallsites.
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.signalevent in the same transaction as the row upsert. Pre-fix the receiver's/audit-exportcarriedchainIntegrity: true, entries: [](vacuous) andGET /v1/eventsreturned 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_RECEIVEDentry types (declared but never written before) now anchor the inbound chain on the receiver, signed by THIS Server's vault key, withpeerHubId+ state on the canonical payload.BUS_TO_PERSISTEDmapping 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-labelwhenever the source was registered viaPOST /v1/schemaswithout an explicitpublisher(the standalone defaultlocalis reserved on the import path). The 409 now carries an actionablerecoveryHintspelling out both recoveries: (a) re-register on the source withpublisher: "<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.txtreordered 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, noperformerAgentId) now produces a record withagent_iddefaulted to the caller, so it's also a valid delegation parent. Pre-fix it committed withagent_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 explicitperformerAgentId. Also tightenedvalidateDelegationRulescircular-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.txtis 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-scopeddisplayName/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 → principalAgentIdso federation peer-agent sync surfaces the principal when the performer is null. - #298: stripped the federation h2 opt-in env var (
allowH2: falseis 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.jsonforrecord.recordedevent + 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.
NotFoundErrorfor 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_checkpointsDLP 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_idcolumn renamed topeer_hub_id(P1 + DB rename).- Dropped dead V0 federation config.
docs/FEDERATION-ARCHITECTURE.mdretired (V0 design draft, superseded bydocs/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 inKNOWN_CHECKSUM_REBASES.- Migration runner auto-prunes orphan
_migrationsrows so deleted-and-folded migration files leave no residue. - Re-audit added
vault_checkpointsDLP, chain-FK RESTRICT, contract_type rename. - Pre-deploy skill collapses 3 DB-bound test projects into one vitest run.
Fixed — pre-deploy
fast-uri3.1.1+ override.- Dropped V1-removed env vars from defaults.
- Federation
agentDirectoryItemSchema.peerHubIdremoved (regen openapi). - Shellcheck SC2001 suppression on parity-check
sedlines.
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.txtlicense endpoints + docs URL.**/admin/license//admin/license/reloadwere 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 tohttps://agledger.ai/docs. Port aligned with Compose convention (localhost:3001). - **
scripts/init.tspost-install next-steps.** DetectsKUBERNETES_SERVICE_HOSTand prints curl-flavored verify hints when running inside K8s; Compose users see thedocker compose upflow unchanged. Stops misleading K8s operators with Compose-only commands when invoked viakubectl exec. - **
deploy/scripts/helm-install.shAurora TLS.** New--ca-cert <path>/--no-ca-certflags + 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 aninstall-smoke-test-v1contract instead of hardcoding theACH-PROC-v1built-in (which ships DISABLED post-v0.22.11). Agent body shape usesenterpriseId+name. Schema-count assertion no longer expects 11 built-ins. - **
deploy/docker/Dockerfile**org.opencontainers.image.documentationlabel corrected tohttps://agledger.ai/docs(visible to every customer'sdocker inspect). - **
scripts/generate-license.ts** operator warning corrected toGET /v1/admin/license/instance-id. - README.md docs links corrected (NXDOMAIN →
agledger.ai/docs/*). - **Inline
nextStepshref inadmin.system.routes.ts:84** (the bug-class root cause). Returnedhref: '/admin/license'to LLM agents — agents following the response verbatim 404. Migrated to a helper inshared/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-transitionswas 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
onRequestwhilebearerAuthruns aspreHandler— sorequest.federationGatewaywas alwaysnullwhen the keyGenerator ran.** The'gateway'keyGenerator silently fell back torequest.ip(fed:${gateway_id ?? ip}). Two gateways behind one egress NAT shared a single bucket; theRATE_LIMIT_EXEMPT_OWNERSenv var couldn't exempt federation peers because their gateway_id was never inspected. Fixed by settinghook: 'preHandler'onfederationRateLimitso the rate-limit fires afterbearerAuthpopulatesrequest.federationGateway.exemptAwareMaxnow also checksrequest.federationGateway?.gateway_idagainst the exempt set, so operators can exempt a known peer by gateway UUID instead of by IP. Six new unit tests infederation.route-helpers.test.tspin 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
InboundRejectioncarrying audit context out of the innerwithTransaction. The publicprocessXwrappers catch it viaauditAndRethrowRejectionAFTER the inner tx has rolled back and released itsfed:${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
verifyfor 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'sidle_in_transaction_session_timeout=300sserver-side default even when the SET failed. Switched topool.options.verify, which gates the client's release on the SET callback. Strengthened the regression test to assert the EXACT configured ms value matchesidleInTxTimeoutMs. - **CLAUDE.md
Database & Pool Safetychecklist 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 andInboundRejectionas 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_timeoutSET on every new pool connection** —src/db/connection.tsadds apool.on('connect')handler that runsSET idle_in_transaction_session_timeout = Non every new backend. Postgres FATAL-terminates a backend that sits idle in tx past the timeout (SQLSTATE25P03), 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(default30000) — plumbed throughsrc/config.ts,.env.example,deploy/compose/.env.example, helmdatabase.idleInTxTimeoutMs, and the helm configmap. Set to0to disable. - New counter
agledger_db_pool_connect_set_failures_total{setting}— increments when a connect-time SET fails. Documented indocs/operations/OBSERVABILITY.mdwith the recommended alert rule (rate > 0over 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-zeroidle_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/TRUNCATEon everyaudit_vaultpartition (was parent-only —agledger_apphad full DML on every monthly partition through the bulkGRANT ... ON ALL TABLES). - FK-backing indexesidx_tarc_signing_key,idx_tar_record(cascade lookup was seq-scanning). - REVOKE + BEFORE TRUNCATE fortenant_admin_read_export_manifests; BEFORE TRUNCATE fortenant_admin_read_sequences. - DROP nine dead tables (proxy_*,notarized_*,notarization_transitions,agent_platform_identities) plus 25 monthly partitions ofproxy_tool_calls— leftovers from removed proxy/notarization features. Removedproxy_tool_callsfromensure_future_partitions. - **
ensure_future_partitionshardened toSECURITY DEFINER** withSET search_path = public, pg_tempandREVOKE EXECUTE FROM PUBLIC; GRANT EXECUTE TO agledger_app. The function ran asINVOKERand the worker calls it asagledger_app, which has onlyUSAGEon schemapublic. 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 preflight —
assertPostgresVersion()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 viaMIGRATION_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 SQLSTATEs —
57P03(Aurora failover),53300(saturation),08001/08004/08006(connect rejection), plusEPROTO(TLS handshake transient). Previously only OS-level errno codes triggered retry. - **Dry-run no longer mutates
_migrations.checksum** —--dry-runwas silently writing through the rebase + backfill paths. Both UPDATEs are now gated byif (!dryRun). - **
AGLEDGER_LAUNCH_FROZENenv flag** — when set, the runner refuses to rebase any migration checksum. Flipped on (withKNOWN_CHECKSUM_REBASEScleared) 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 andisNoTransactiondirective parsing; 8 integration tests (src/db/migrate.integration.test.ts) covering re-run, lock contention, dry-run preservation, rebase, backfill, andLAUNCH_FROZENrefusal.
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:backfillscope, accepts{ enterpriseId, source, mandates: [BackfillMandate] }with up to 100 entries per batch. Each item lands directly in its declared terminal state with backdatedcreatedAt/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_IMPORTentry (enum value already reserved in 001_consolidated.sql) withimported: true+ declaredsourcein 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 whitelist —
FULFILLED,REMEDIATED,EXPIRED,REJECTED,VERIFIED_FAIL,CANCELLED_DRAFT,CANCELLED_PRE_WORK,CANCELLED_IN_PROGRESS.FULFILLEDrequiresfulfilledAt. Timestamp ordering enforced (activatedAt/fulfilledAtmust not precedecreatedAt). - Cross-tenant safety — every
principalAgentIdandperformerAgentIdis verified to belong to the targetenterpriseIdbefore insert. An admin key cannot import a mandate citing an agent from a different tenant. - Agent-UX — response includes
adminMandateImportNextStepssteering the operator to an audit-export spot-check on the first imported mandate + a follow-up batch. - **
/llms.txtmention** — under the "Tenant + Agent Provisioning" admin section so LLM operators discover the endpoint. - 13 integration tests in
src/test/admin-import.scenario.integration.test.tscovering auth (agent rejected, admin withoutadmin:backfillrejected, 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.lookuphook on every outbound dispatcher** —src/shared/http-agent.tsexportsssrfLookup, plumbed intoHTTP_AGENT_OPTIONS.connect.lookup. Every Agent built bycreateHttpAgent()AND the globalfetch()dispatcher resolve hostnames through it. Private / link-local / loopback addresses not covered bySSRF_ALLOW_CIDRSfail the connect withcode: '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 worker —
categorizeHttpErrornow recognizesSSRF_BLOCKEDfor clean DLQ classification, and the function was refactored from if-ladder to lookup table (complexity 21 → 9). - **Global dispatcher wraps in
RetryAgent** —configureGlobalDispatcher()wraps theEnvHttpProxyAgentinundici.RetryAgentwithmaxRetries: 2, exponential backoff (minTimeout: 500ms,maxTimeout: 5000ms,timeoutFactor: 2), andretryAfter: 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 owncreateHttpAgent()Agent and pg-boss remains the retry layer. - 8 unit tests in
src/shared/http-agent.test.tscover 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** insrc/shared/next-steps.tscovers 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** sofast-json-stringify(which strips undeclared fields underadditionalProperties: false) lets the field through:retryDlqSchema,retryAllDlqSchema,bulkRevokeSchema,deactivateAccountSchema,addRateLimitExemptionSchema,removeRateLimitExemptionSchema,toggleApiKeySchema, plus the inline schemas forPOST /admin/auth-cache/flushandPATCH /admin/webhooks/:webhookId/circuit-breaker. Silent-strip ofnextStepshas 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.tsthat exercise the full serialization path. 10 unit tests added tonext-steps.test.tscovering the helper return shapes and URL interpolation (e.g.rate-limit-exempt-addinterpolatesownerIdinto 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 layer — src/shared/http-agent.ts is the new single source of truth:
HTTP_AGENT_OPTIONSdeclares{ 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 anEnvHttpProxyAgentas the process-wide dispatcher, carrying the same options. This accomplishes two things: 1. Every nativefetch()caller (OIDC discovery, JWKS, Vault secret load, telemetry heartbeat, support-bundle upload) inheritsallowH2: falsewithout 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.EnvHttpProxyAgentrespectsHTTP_PROXY/HTTPS_PROXY/NO_PROXYenv 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.tsasserts each explicit Agent is constructed withallowH2: false. Any refactor that drops the flag failspnpm test.- One additional unit test in
webhook-delivery.worker.test.tscovers the same invariant at the worker's own construction site.
Deferred, with issues filed:
- #297 — SSRF guard at dispatcher level via a custom
connectfunction (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.RetryAgentfor 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.txtprose** at both sites (the Lifecycle section and thePOST /v1/mandates/{id}/transitionline under Key Endpoints). The full enum lives in/openapi.jsonand is referenced from/llms-full.txt. - **
mandates.schemas.tstransition-description fixed.** Previously saidregister: DRAFT -> CREATED, which mixed internal and display state names. Now clarifies that displayCREATEDcovers both internalDRAFTandREGISTERED, 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 onInvalidActionError. - **
refreshUrl** — the concreteGET /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
ErrorResponseinsrc/shared/schemas.tssofast-json-stringifydoesn'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.txtand/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 dropa2a,proposals,a2a-agent-card; single-party deployments dropdisputes.- Content negotiation. When
Accept: text/markdownis present, the response content-type istext/markdown; charset=utf-8; otherwisetext/plain.Vary: Acceptis 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/reloador on schema-change LISTEN/NOTIFY.
Verification baseline at head
- Unit tests: 1030/1030 (59 files) —
llms-txt.test.tsgrows 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.tsgrows by 2 tests verifyingrecoveryHint+refreshUrlonInvalidActionErrorresponses. .env.exampledrift 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.createMandatenow checksschema_subjects.statusafter resolving the contract type; DISABLED types reject with422 /problems/unprocessable-stateand a recovery hint pointing atPATCH /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 byfindSubjectByType(..., { includeDisabled: true })) populatesstatus: '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 withstatus:'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[]todata: { 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_READandScopes.COMPLIANCE_READare agent-grantable again (removed fromADMIN_ONLY_SCOPES). All three agent profiles (agent-full,agent-readonly,agent-performer-only) now include both scopes by default. NewassertMandatePartyAccess()helper gates per-mandate audit/compliance routes: agents must be principal OR performer (tenant-adjacent agents get 403NOT_MANDATE_PARTY, narrower than the generalassertMandateAccess). 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-sidePOST /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()inmandates.service.tsnow derives it fromprincipal_agent_id !== null && agent_id !== null && principal_agent_id === agent_id. Type is non-optional onMandateResponse. - #289 — GET /v1/scope-profiles is genuinely public. Added to
PUBLIC_EXACT_ROUTESinsrc/middleware/auth.tsand to the OpenAPI security classifier'sPUBLIC_EXACTset insrc/plugins/swagger.ts. Source comment's "public endpoint" promise now matches runtime. - **#288 — Retired
/v1/mandates/summaryno 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 atGET /v1/mandates/search?status=...with a machine-readablerecoveryHint. Generic error handler also now detectsparams/idUUID-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
enterprise→admin.** The role that governs a tenant is nowadmin(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 onapi_keys.role(CHECK constraint enforcesadmin | agent | platform). - **API key prefix:
ach_(ent|age|pla)_*→agl_(adm|agt|plt)_*.** Aligns with the compact license string formatagl_(ent|dev)_v1_<base64url>. The auth middleware regex is now^agl_(adm|agt|plt)_. Real bug caught in pre-deploy:scripts/init.tsanddeploy/scripts/install.shstill minted/extracted with the oldach_pla_prefix — fresh installs would have produced unauthenticatable platform keys. Both fixed. - **Default scope profile names:
standard→admin-standard,restrictive→admin-observer,iac-pipeline→admin-iac,schema-manager→admin-schema. New:agent-performer-only. Retired:monitor(folded intoadmin-observer).** - **
POST /v1/mandatesis the unified create endpoint** for admin, agent, and platform callers. Agent callers defaultprincipalAgentIdto themselves; admin/platform callers must name a principal — explicitly viaprincipalAgentId, or implicitly by settingagentId/performerAgentId(the named performer becomes the principal — a self-commitment). Cross-tenant attempts return 403. - Mandate principal model collapsed.
mandates.principal_typecolumn dropped. Every mandate hasprincipal_agent_id NOT NULL(FK toagents). Self-commitment is a first-class pattern: whenprincipal_agent_id === agent_id, the vault entry carriesselfCommitment: trueand 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_vaultrows now carry actor fieldsactor_key_id,actor_role,actor_owner_id(all NOT NULL).** Folded into the canonical payload under a reserved_actorenvelope key (hash-chained — tamper breaksverifyChain). Cascade-cancel preserves the parent canceller's actor through the boss job data, so child cancellations attribute to the original actor instead ofSYSTEM_ACTOR. - **
enterprise_agentsN:M table dropped.** Agents are now 1:1 with tenants viaagents.enterprise_idFK (NOT NULL). 18 production sites that didINSERT INTO enterprise_agentswere retired; the agent's tenant is set at creation and cannot change. - **
api_keys.enterprise_idis required forrole IN ('admin','agent')** (cross-column CHECKchk_api_keys_tenant_invariant). NULL only forrole = 'platform'. Real bug caught in pre-deploy:admin.repository.insertApiKey+ service + provisioning reconciler weren't passingenterprise_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 matchingenterprises.*columns dropped. Sandbox tier rate-limit bucket dropped.requireVerifiedOrActivemiddleware (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 theprovisioning/YAML config-as-code directory. The Auth tag now exposes onlyGET /v1/auth/me+POST /v1/auth/keys/rotate. - **
POST /v1/mandates/agent** (use unifiedPOST /v1/mandateswithperformerAgentIdfor proposal-style flows). - **
GET /v1/mandates/agent/principal** (replaced by filtering/v1/mandatesby principal).GET /v1/mandates/agent/proposalsretained (agent-only listing of pending proposals). - **
GET /v1/mandates/summary** (use/v1/mandates/searchwith 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 inGET /v1/audit/enterprise-reportresponse (same SQL, moved toaudit-report.repository.ts). Dashboard had no first-party UI consumer and every endpoint duplicated existing data — the schemathesis-flaggedtotal_mandates/lifetime_mandatescolumn drift on/dashboard/agentswas a symptom of orphan-infrastructure bitrot.dashboard:readscope anddashboardscope profile removed; the profile's read-only-monitoring use case is served by the renamedadmin-observerprofile. - **
POST /v1/audit/enterprise-report/analyze(LLM-powered narrative analysis).** Wrapped an AWS-Bedrock-only Claude Haiku 4.5 call around/v1/audit/enterprise-reportdata. Removed because it cloud-locked the product and every input was already structured JSON.audit:analyzescope,llm_analysisfeature flag, and@aws-sdk/client-bedrock-runtimeoptional dependency all dropped. Prompt templates preserved atdocs/examples/enterprise-report-prompts.mdfor customers who want to replicate the narrative with their own model. - Sandbox runtime tier.
RATE_LIMIT_SANDBOXenv var,rateLimit.sandboxconfig key, and the oldRATE_LIMIT_ENTERPRISEenv 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_SANDBOXexample indocs/operations/DEPLOYMENT-HARDENING.md,trust_levelrow indocs/engineering/DESIGN-ENTITY-REFERENCES.md.docs/engineering/AGENT-TO-AGENT-IMPLEMENTATION.mdretained with a HISTORICAL banner pointing todocs/strategy/PRINCIPAL-MODEL-REWRITE.mdas canonical.
Added
- Cascade-cancel actor attribution.
MandateTransitionPayload+CascadeCancelJobDatagain optionalactor/parentActorfields;mandates.service.persistAndEmitTransitionforwards actor on emit;on-cascade-cancelpropagatespayload.actorintoboss.sendjob data;worker.tsusesparentActorwhen present (falls back toSYSTEM_ACTORfor in-flight pre-rewrite jobs). - **
pickNamedPrincipal(body)helper inmandates.routes** — extracts theprincipalAgentId ?? agentId ?? performerAgentIddefaulting 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 theagent-fullprofile 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 carryagent_id/contract_type/settlement_signalonly — the principal model is local.
Migration notes
- Dev / testbed: wipe DB and re-apply
001_consolidated.sql.pnpm db:migrateagainst an existing v0.19.x install will refuse with a checksum mismatch. Wipe viadocker compose down -v && docker compose up -d postgres && pnpm db:migrate. - Provisioning YAML:
role: enterprise→role: admin;scopeProfile: standard→scopeProfile: admin-standard; standalone agents now requireenterpriseSlug(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 unifiedPOST /v1/mandatesbody 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 (transitivefast-xml-parservia@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-Keystill produced 3–5 mandates.** v0.19.25 fixed a plausible symptom (xmax = 0distinguisher) and a real secondary bug (fire-and-forget onSend write) but missed the true root cause: the onSend error-release branch ranDELETE 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, throwsConflictError, 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-requestclaim_id uuidcolumn onidempotency_keys: the winning request stores its UUID,ON CONFLICT DO UPDATEleavesclaim_iduntouched, andrequest.idempotencyClaimis set only on the winner. onSend's UPDATE and DELETE both filter byclaim_id = $winner_uuid, so a conflicting request's writes affect zero rows. The new primitive is MVCC-independent — thexmax = 0distinguisher 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.sqldeclaresclaim_id uuid NOT NULLdirectly (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 parallelapp.injectcalls 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 aresrc/modules/provisioning/provisioning.repository.tslines 106 and 137 — provisioning runs single-query-per-row insidewithTransactionat 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./metricsPrometheus scrape continues to serve exactly the same data — OTel is an additional parallel stream, not a replacement. Newsrc/modules/metrics/otel-bridge.tsexportsdefineCounter/defineHistogram/defineGaugehelpers that declare a prom-client instrument AND bridge its.inc()/.observe()/.set()methods to a parallel OTel instrument via the@opentelemetry/apiglobal meter. Zero overhead when no MeterProvider is installed (OTel's no-op meter pattern).@opentelemetry/api,@opentelemetry/sdk-metrics, and@opentelemetry/exporter-metrics-otlp-httppromoted from transitive to explicitdependenciesso a futuresdk-noderefactor can't strand us. Docs updated indocs/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'sINSERT ... ON CONFLICT ... RETURNING in_flight, response_codecouldn't distinguish "I just inserted this row" from "row already existed, first request hasn't cached a response yet" — both look likein_flight=true, response_code=null, so two concurrent requests both fell through to the handler and both created mandates. Fixed by addingRETURNING (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 theinsertedflag. (2) The onSend cache-write was fire-and-forgetvoid pool.query(...), returning the response to the client before the UPDATE committed — a follow-up request could read the stalein_flight=true, response_code=nullrow and re-enter the handler. Fixed by awaiting the UPDATE/DELETE in onSend. Also migrated the hand-rolled 400/409 response shapes tothrow 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 insrc/test/concurrency.scenario.integration.test.ts— a parallel-POST repro that assertscount(*) FROM mandates = 1under 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 fromDate.now()-based idempotency keys torandomUUID()to eliminate test-level collisions under parallel load. - **
/llms.txt+/llms-full.txtwarm caches not invalidated onPOST /v1/schemas/POST /v1/schemas/import/PATCH /v1/schemas/.../versions/...(#283).** Threeschemas.handlers.tswrite paths never callednotifyInvalidation(), so the LISTEN/NOTIFY-based cache bust only fired on DELETE + admin flush. Customers registering a new custom contract saw stale/llms.txtfor up to 300 seconds (LONG_TTL). Fixed by addingnotifyInvalidation(db, { type: 'schema', contractType })after each successful schema mutation. Newsrc/test/discovery-cache.integration.test.tsregression guard: warms/llms.txt, POSTs a new contract, asserts the fresh contract appears without waiting for TTL. - **
POST /admin/discovery/reloadmounted at root instead of/v1/admin/(#282).** One-line path fix insrc/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
warnlevel 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.pubverification URL inSECURITY.md(#279).** Thecosign verifyinstruction pointed atraw.githubusercontent.com/agledger-ai/agledger-api/main/deploy/cosign.pub, which 404s for unauthenticated customers because the server repo is private. Now points athttps://agledger.ai/cosign.pub(verified live, already the canonical URL used indeploy/docker/DOCKER_HUB_README.md).docs/product/SECURITY-COMPLIANCE-ALIGNMENT.mdhad the same broken-private-repo pattern for the Policy link; now points atinstall/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=falsewith zero production callers.src/modules/portal/(service, repository, routes, passkey helpers, token helpers, tests),src/shared/email-templates.ts(portal-specific mail bodies;escapeHtmlinlined into the one remaining caller,auth/email-verification.service.ts),src/shared/password.ts(argon2 wrapper), andsrc/shared/email.test.tsall deleted.argon2and@simplewebauthn/serverdropped frompackage.json.PORTAL_*env vars, portal config block, portal public-route allowlist, portal Swagger security class, portal maintenance-queue cleanup tasks, andportalNextSteps()all gone. 9/v1/portal/*routes retired. - Dormant demo scaffolding.
config.demo.landingEnabled+DEMO_LANDING_ENABLEDenv var (zero readers insrc/).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 in001_consolidated.sqldirectly.KNOWN_CHECKSUM_REBASESgets 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.shnow splits main + tag push into two explicitgit pushinvocations instead of--follow-tags.--follow-tagssilently 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 existingls-remoteguard 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 refundprose dropped fromlifecycleSection()inllms.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
/outcomebody schema./outcomebullet no longer framed as encrypted-only. Same fix applied to Swagger tag descriptions. - F-376 (nextSteps workflow projection):
NextStepgains optionalworkflowStep/workflowTotal/workflowLabel/afterThisfields.mandateNextStepsandcompletionNextStepsproject these onto the primary forward action usinghasPerformer+verificationMode+operatingMode. Step math: 2 (+1 if verdict) without a performer, 4 (+1 if verdict) with one. Gated-modeCOMPLETION_ACCEPTEDno longer receives a workflow hint pointing at/outcomewhile the action list says "Check verification status".
Changed
- Exported
OperatingMode/VerificationModefromdb/types.ts; tightenednext-steps.tsparams offstring. 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.shguards 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-tagssilently 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 onmain; (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/reloadwith no body returned 400body must be object.** Regression vs. v0.19.10; broke the post-installcurlcommand shown in HelmNOTES.txt. Added apreValidationhook that defaultsrequest.bodyto{}when absent, matching the pattern used forPOST /v1/notarize/mandates/:id/accept. Body-shape validation still applies when a body is sent. Regression test added inadmin.scenario.integration.test.ts. - **F-417 (#277): state-dependent rejections returned 400 instead of 422, breaking the SDK's
UnprocessableErrorcontract across ~20 endpoints.** The bug that surfaced onPOST /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 threwValidationError(400) instead of a distinct 422 error. SDK customers using the documentedcatch (err instanceof UnprocessableError)branch hit the wrong path everywhere. Fixed at the root: - NewUnprocessableStateErrorclass (422,/problems/unprocessable-state) for state-dependent rejections. -InvalidActionErrormoved 400 → 422 (mirrors siblingInvalidTransitionError). - Migrated ~20 throw sites acrossmandates.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, andfederation-protocol.routes.tsfromValidationErrortoUnprocessableStateError. - Recategorized 4 authorization-style rejections ("only X can do Y") fromValidationErrortoForbiddenError(403) — these were never body errors. - Response shape forInvalidActionErrorunchanged (currentState,attemptedAction,allowedActions,/problems/invalid-action) — only the status code moved. - Updatedllms.txterror-recovery section to separate 400 (body malformed, readhint/suggestion/recoveryHint) from 422 (state issue, readcurrentState/allowedActions/detail). - 5 integration tests updated (state-rejection expectations 400 → 422), 1 new unit test forUnprocessableStateError.
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-interactivewith a WARN, matching theupgrade.shprecedent. - **F-415 (
install.shreconciliation).**COMPOSE_FILEandAGLEDGER_VERSIONreconciliation now runs on both fresh and existing.env, afterdetect_db_modehas setUSES_BUNDLED_PGauthoritatively. Existing-.envbranch prints a summary of what was added/updated and warns whenREGISTRATION_ENABLED=truelacks an opt-in marker.
Added
- **
upgrade.shconfig-state check.** New "Checking configuration state" step surfacesREGISTRATION_ENABLED=true-without-marker and missing-COMPOSE_FILEbefore the confirmation prompt.COMPOSE_FILEis auto-added (safe default);REGISTRATION_ENABLEDis warn-only (customer intent unknown). - **
remediate-env.shhelper.** Applies F-408 and F-410 fixes to an existing.envwith--dry-runand--non-interactivesupport. 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 --deletefromagledger-api/deploy/intoagledger-ai/install) wiped 5 install-script fixes that had been applied directly in the install repo instead of the canonicaldeploy/source. Testbed retest confirmed v0.19.16 still reproduced F-392, F-397, F-399, F-408, F-410. Re-applied toagledger-api/deploy/scripts/so the next mirror preserves them: - **F-392/F-405 (install.sh):**EXISTING_PLATFORM_KEYdetection reuses the previous platform key on reinstall and de-dupes any priorPLATFORM_API_KEYlines. Refuses to truncate non-empty.envif the filter produces an empty result. - **F-408 (install.sh):**REGISTRATION_ENABLEDdefaults tofalse(matches.env.exampleposture). Removed thesedline that flipped it totrue. - **F-410 (install.sh):** persistsCOMPOSE_FILEin.envviaupsert_env_varso manualdocker compose <cmd>from the install dir keeps the prod + bundled-pg overlays. - **F-399 (uninstall.sh):** fails loud withexit 2when stdin is not a TTY and--non-interactivewas 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 FILEhelper inlib-compose.sh.** Used byinstall.sh(COMPOSE_FILE,AGLEDGER_VERSION) andupgrade.sh(AGLEDGER_VERSION) — collapses 3 duplicatedgrep/sed/echopatterns.
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_vaultDDL-guard bypass.**DROP TABLE audit_vault*andDROP TABLE federation_audit_logpreviously bypassed theBEFORE TRUNCATEprotection triggers because DML triggers don't fire on DDL. Added asql_dropevent 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 —verifyVaultFulland/audit-exportcheckpoint 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 atupgrade.sh:144,remediate-env.sh:68, andinstall.sh:353usedcut -d= -f2- | tr -d '[:space:]', which doesn't strip trailing shell comments. Against a v0.19.16-seeded.envthat shipsREGISTRATION_ENABLED=true # Open self-service registration ..., the parsed value becametrue#Openself-serviceregistration(default:disabled)— the== "true"check failed, the F-408 warning never fired, andremediate-env.sh --dry-runreported "no remediation needed" on an actively exposed install. Replaced all three sites with a newget_env_valuehelper inlib-compose.shthat strips[[:space:]]+#.*$before returning the value, matching docker-compose's native parser behavior. Verified against the testbed reproduction. - **#269: stale
demo.agledger.aireferences.** The hosted demo service was retired on 2026-04-20. UpdatedAGENTS.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), andsrc/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,/livezreturned 401 in production.** The auth middleware's public-route allowlist was missing the new canonical aliases added in v0.19.14. Added toPUBLIC_EXACT_ROUTESwith regression coverage in the authorization integration suite. - **
install.shsilently failed to create the platform API key.** Network detection useddocker compose ps --format json | jq '.Networks | keys[0]', butNetworksis 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, thencompose_defaultas last resort. - Helm chart was published unsigned despite install-repo README instructing customers to verify it.
/deployskill 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 slsaprovenance1everywhere (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.sqlinto001_consolidated.sql, matching the pre-launch single-baseline strategy. Deployed v0.19.14 systems auto-rebase viaKNOWN_CHECKSUM_REBASES; existing_migrationsrow 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.tsasserting/healthz,/readyz, and/livezdon'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.shresolves 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,/statusunchanged — 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/reloadbody 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.tswrites.pemand.keyside-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 withcosign verify-attestation --type slsaprovenance. - Cosign bundle as release artifact. Standalone
agledger-<version>-cosign.bundlefile attached to the install-repo GitHub release alongside the SBOM. - Monitoring README.
agledger-ai/install/monitoring/README.mdsignposts the Grafana dashboard JSON for customers running their own Grafana outside the bundled monitoring profile. - **
.env.exampledocumentsAGLEDGER_LICENSE** with the full precedence chain.
Fixed
GET /.well-known/security.txtPolicy field pointed at the privateagledger-ai/agledger-apirepo, which 404s for any customer hitting the endpoint. Now points at the publicagledger-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 publicagledger-ai/installrepo.
Changed
/deployskill now includes steps 5d (cosign sign + bundle), 5e (SLSA provenance), 8 (mirror to install repo), and 9 (Docker Hub README sync). Newmirror-install-repo.shhelper runs shellcheck + install.sh smoke test before mirroring.wait_for_httphelper factored intolib-compose.sh; consumed by bothinstall.shand the mirror script.
Removed
deploy/scripts/bundle-airgap.sh— air-gap now usesinstall.sh --image <internal-registry>plus a documented DIYdocker saveflow. No tarball release asset.
0.19.132026-04-20
Fixed
- Licensing activation.
scripts/generate-license.tswrote 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_HEXinsrc/modules/licensing/license.service.tswas 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/pro→free/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/refundtransition enum), #266 (federation signing wire format public), #250 (opt-in dispute auto-escalation). - 0.19.10 (2026-04-17): Migration 002 —
BEFORE TRUNCATEtriggers onaudit_vault+federation_audit_logblock 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.txtrewrite, protobufjs + @fastify/static CVE overrides. - 0.19.7 (2026-04-16): Route-plugin splits (federation/admin/portal/mandates), 197
requiredScopesmigrated toScopes.*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-knownendpoint, 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_URLcascade, 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.orgfrom CORS allowlist - #137: Scripts:
services.sh,setup-cloudwatch-alarms.sh,setup-xray-tracing.sh activeIncidentsfrom/statusresponse schema (no source after CloudWatch removal)
Added
- #138: Static
openapi.jsonincluded as GitHub Release artifact (foragledger.ai/docs) - #139: CI sends
new-commitdispatch 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/statusand admin service)
Fixed
- #140: pgboss schema privileges not granted to
agledger_approle when DB role separation is in use — queue operations failed under separated roles .env.examplehadhttps://localhost:3000(should behttp)
Changed
- Deployment model: Self-hosted only (Docker Compose / Helm). No hosted API.
api.agledger.airetired. - 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.aitolocalhost:3000across 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_REBASESmap to migration runner for automatic checksum rebasing - #132:
PATCH /admin/webhooks/:id/circuit-breakerreturned 500 — PostgreSQLinconsistent types deduced for parameter $1(added::varcharcast) - #133: Expression engine boolean
== truecomparison confirmed correct; added diagnostic trace that notes emptymetadata/mandatecontext on evaluation failure - #135: pg-boss
#monitorcrashed 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 (
events→event_types) + cross-test isolation viaowner_idfilter
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— sharedDANGEROUS_SEGMENTSconstant (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 hardcodedmax: Nremaining) - Global rate limit
maxfunction now checksexemptIPsfor 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 inexecuteVerification(), 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 /outcomereturns 403/404 for unauthorized callers before surfacing body validation errors - #67:
GET /schemas/:type/rulesreturnsfieldMappingsfor custom types (was filtering out empty arrays) - #129:
CREATE ROLEin migration wrapped withEXCEPTION WHEN insufficient_privilegefor 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
_advisoryWarnings→advisoryWarningsin mandate response (convention consistency)
0.10.62026-03-25
Fixed
- #67: Schema rules endpoint returns
fieldMappingsandcommissionSourceFieldfor 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
subscriptionUrlandmandateIdin re-enqueued jobs (was missing, causing worker failures) - #107:
MANDATE_CREATEDaudit entries includechangedByfield for SOC 2 compliance (was conditionally omitted) - #108: Agent registration with
enterpriseIdno longer silently swallows auto-approval failures - #118: Worker polling interval reduced to 500ms for faster job pickup under load
- #121:
commissionPctallowed 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 /outcomeruns auth before body validation viaattachValidation: true
Added
- #118: Configurable worker concurrency via
WORKER_VERIFICATION_CONCURRENCY(default 5) andWORKER_WEBHOOK_CONCURRENCY(default 10) env vars - #122: Advisory-aware delegation depth checking in both enterprise and A2A mandate paths
- #121: Schema registration validates
commissionSourceFieldexists 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 —
assertPrincipalAgentnow requires explicitprincipal_agent_idfor agent keys - #116: Surface advisory warnings in completion response —
structuralValidation: 'WARNING'+warningsarray when schema validation is advisory - #117: Apply per-mandate
enforcementOverridesduring 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_ORDERandENFORCEMENT_OVERRIDE_FIELDSintoenterprise-config.schema.ts assertPrincipalAgentis 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 intosrc/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
dependsOnarray (#63) - Graph visualization backend (
GET /v1/mandates/:id/graph) (#29) - Secrets manager abstraction (
env,ssm,vaultproviders) (#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_idon 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_payloadTEXT 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 validTransitionsin 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