Skip to content

feat: converge first-adopter runtime configuration and package verification - #1632

Merged
jeremi merged 92 commits into
mainfrom
jeremi/first-adopter-wp1-wp2
Sep 27, 2026
Merged

jeremi merged 92 commits into
mainfrom
jeremi/first-adopter-wp1-wp2

Conversation

@jeremi

@jeremi jeremi commented Sep 27, 2026 •

Copy link
Copy Markdown
Member

Supersedes #1607.

Summary

This PR carries the first two config convergence work packages and the
first-adopter lifecycle fixes, rebased onto the audit simplification that
landed in #1560:

  • WP1, shared runtime configuration (Config convergence WP1: shared runtime config core, naming, and conformance gate #1408). Every runtime reads
    runtime.yaml through one loader in registry-platform-config. The loader
    shares blocks for listeners, secrets, the audit key, and the OIDC issuer
    and clients. A conformance gate proves each product refuses malformed
    configuration in the shared shape.
  • WP2, shared package format and verification (Config convergence WP2: one deployable package format with sum-file digests #1409). BReg, Casework,
    Evidence, Relay, Scheduling, Render, and Discovery all produce and verify
    one package envelope. The envelope is a SHA256SUMS file listing every
    other file, plus an optional REVISION, and the package digest is the
    sha256: of SHA256SUMS. Consumers are bound to the bytes that
    verification checked.
  • First-adopter lifecycle fixes across BReg, Casework, and Evidence,
    listed per product below.

The platform loader and the runtime adoptions cannot land separately without
a broken intermediate state, which is why one PR spans crates/,
products/, docs/site/, and release/. Each commit stays scoped to one
product.

Out of scope, and not claimed here:

  • database-ledger convergence or recorded activation (WP3, D2);
  • removal of BReg package signing, which this PR keeps;
  • a completed activation identity migration.

Per product

Platform

  • Runtime configuration. registry-platform-config adds:

    • the shared runtime configuration loader;
    • serializable shared blocks: listener.bind bounded to 128 characters, secret providers, the audit hashKeyRef block, the OIDC issuer, OIDC client admission, and static JWKS.
  • Loader refusals. The loader refuses:

    • environment expressions in authored files;
    • an unreadable file;
    • members beside a discovery jwksSource;
    • a query component in the issuer.

    Refusals withhold configured text.

  • Conformance gate. release/scripts/check-config-conformance.py covers seven products.

  • Package writer and verifier.

    • They are deterministic and refuse by name a changed, missing, or extra file, a symbolic link, or a special file.
    • A product can name package.root instead of its directory.
    • package.expectedDigest pins the digest.
  • Hooks. A lease commit that cannot be read back is recorded as an unknown disposition rather than a retryable one.

Base Registry Engine

  • Runtime configuration. breg and bregctl read --runtime-config
    through the shared loader.
  • Package format. bregctl package writes the shared envelope and takes
    --revision. Startup, apply, and operator tooling verify the envelope
    before any database authority is used, and the active and predecessor
    package loads are bound to the verified bytes. Package signing is
    unchanged.
  • Import authorities. Adds the import operation, which change control
    does not count as a direct write, and operator-opened import authorities:
    bregctl import-authority open|close|close-expired|list. An authority
    bounds a load by window, item volume, and package revision. Pinned input
    digests are labels the client announces, not server-side verification; the
    docs and --input-sha256 help say so.
  • Instance claim. A restored copy (logical restore) refuses to serve until
    an operator runs bregctl instance-claim adopt --acknowledge-original-retired. An existing registry adopts once after its
    first apply on this release.
  • Review recovery. bregctl review-recovery resubmit|close handles a
    review its authority lost, and the result poller orders live reviews first.
  • Migrations.
    • bregctl test --baseline-runtime-config rehearses successor migrations.
    • History rebaseline has no live-row limit.
    • Every reviewed chunked backfill chunk is journalled.
    • maxLength can be raised, and minLength lowered, on live data.
    • A refused statement names its SQLSTATE.
    • History-step SQL that the metadata check cannot read is refused.
    • Server-ended sessions are classified as connection failures.

Registry Casework

  • Doctor and readiness. The doctor names the failed check, and readiness
    fails on a stuck or failing reconciliation. The health state is migration
    0018_source_reconciliation_health.sql, which follows refactor(audit)!: replace chained journals with shared JSONL streams #1560's
    0017_audit_writer.sql.
  • Refusals. Casework refuses:
    • a policy package that would strand pinned in-flight work, including open work items from a removed source;
    • a BReg engine from another release, by name. A development build of the same version matches.
  • Metrics. An opt-in private metricsListener serves operator metrics,
    and /version reports the package digest. Metrics scrapes and dead
    database connections are bounded.
  • Configuration. Casework reads configuration through the shared loader.
    Named allowedClients are required behind an operator-controlled TLS
    terminator.
  • Package format. Casework serves the shared package format, binds served
    files to package identity, and validates consumers from one package load.

Evidence

  • Signing. Transit signing initialization failures name their fault.
  • Candidate checks. evidence check --without-audit-lock and
    evidencectl doctor --without-audit-lock check a candidate beside the
    running writer without taking its single-writer audit lock.
  • Issuer trust. A private CA for the access-token issuer is trusted
    through a named tlsTrustProfile.
  • Authoring. A derivation that reads a fact its source does not declare is
    refused at authoring time.
  • Configuration and packages. Evidence reads --runtime-config through the
    shared loader and serves the shared package format. evidencectl package
    replaces evidencectl build.

Relay, Scheduling, Render, Discovery

  • Relay.
    • Reads --runtime-config through the shared loader.
    • Serves the shared package format; relayctl package gains --dry-run and --revision.
    • Refuses environment expressions in authored files.
  • Scheduling.
    • Reads configuration through the shared loader, with the shared OIDC and audit key blocks.
    • schedulingctl package now writes a verified package directory (--output, --dry-run, --revision).
    • The runtime logs the served package identity.
  • Render.
    • Reads --runtime-config through the shared loader.
    • Adopts the shared envelope; registry-render package replaces seal.
  • Discovery.
    • Reads configuration through the shared loader.
    • Packages immutable indexes in the shared format.

Breaking changes and upgrade notes

  • Runtime configuration flag. --config and --runtime become
    --runtime-config on breg, evidence, relay, and registry-render.
  • Package format. Every runtime refuses a package directory without
    SHA256SUMS, and the product changelogs describe rebuilding with each
    product's package command.
  • Existing BReg registry. It must run bregctl instance-claim adopt once
    after its first apply on this release.
  • BReg engine upgrade (known break, BReg: an engine upgrade cannot test or apply a successor over a package built by the previous release #1633). Testing or applying a
    successor over a package built by the previous bregctl release does not
    work yet; see Known break.

CLI surface changes

The CLI reference record in docs/site/src/data/cli-reference.yaml is
re-stamped. Each changed catalog entry was checked against its clap
definition.

  • --runtime-config replaces older flags.
    • breg: replaces --config.
    • evidence: replaces the global --runtime; now on check, evaluate, and serve.
    • relay check|serve: replaces --runtime.
    • registry-render check|healthcheck|serve: replaces --runtime.
  • New bregctl commands.
    • import-authority open|close|close-expired|list
    • instance-claim status|adopt
    • review-recovery resubmit|close
  • Package commands.
    • bregctl package --revision <TEXT>
    • caseworkctl package --revision <TEXT>; --dry-run now reports packageDigest.
    • evidencectl package --revision <REVISION>
    • relayctl package --dry-run --revision <TEXT>; --output must not exist yet, and is optional with --dry-run.
    • schedulingctl package --output <DIRECTORY> --dry-run --revision <TEXT>; it now writes a new package directory.
  • Renamed or removed.
    • evidencectl build is removed; use evidencectl package.
    • registry-render seal is replaced by registry-render package.
    • registry-render check --seal is removed.
    • registry-render check|compile|validate --bundle accepts an authored bundle or a package directory.
    • evidencectl artifact inspect <PROJECT> names a deployment target whose runtime.yaml names the installed package.
  • New flags.
    • evidence check --without-audit-lock requires --require-runtime-dependencies.
    • evidencectl doctor --without-audit-lock requires --runtime-config.

Security review notes

Every change below is security-sensitive under AGENTS.md:

  • BReg narrative. products/breg/SECURITY-REVIEW-NOTES.md covers import
    authorities (BREG-SEC-109..113), the instance claim (BREG-SEC-114..117),
    audit retention, review recovery, and package byte binding. For each it
    gives the threat, defaults, enforcement point, tests, and accepted
    residuals. Accepted residuals:
    • pinned input digests are labels, not verification;
    • admission reserves no volume;
    • the runtime role can lower committed_items;
    • physical restores are not detected by the instance claim;
    • review-recovery close does not withdraw the review at the authority;
    • a loopback http OIDC issuer is still accepted in every environment.
  • Configuration secrets. The shared loader withholds configured text from
    errors, and it refuses environment expressions in authored files and
    substitution into *Ref fields.
  • Package provenance.
    • The package digest is an integrity digest over SHA256SUMS, not an authenticity proof.
    • Relay, Casework, Scheduling, Render, and Discovery packages are unsigned.
    • BReg keeps its signature over the package.
    • Every consumer reads the bytes that verification hashed.
  • Casework authentication. Casework requires named OIDC clients behind an
    operator-controlled terminator.
  • Evidence.
    • --without-audit-lock still proves the audit destination's modes, its write access, and a complete final entry, but not the single-writer lock. A second writer is not detected, so serve still refuses to start while another process holds the lock.
    • The private-CA trust profile is named and closed.

Issues

Closes #1416
Closes #1448
Closes #1454
Closes #1480
Closes #1491
Closes #1621

Refs #1408: WP1 is in place. BReg still reports configuration errors as
"document invalid" without the field, and only Relay refuses a config file
another local user can write (follow-up tickets).
Refs #1409: WP2 is in place for all seven products. The offline BReg
identity report absorbed from #1262 still needs confirming against the
ticket.
Refs #1410: WP3, the database-backed config ledger, is not started.
Refs #1418: D2, environment-neutral package with ledger-recorded activation,
is not started. BReg packages stay signed and environment-bound here.
Refs #1419: the peer mismatch is refused by name. BReg does not log
Casework's version, and an absent effects is still refused by decision.
Refs #1420: the import authority is in place. The authority-open doctor
advisory and an end-to-end script remain, plus the follow-ups in the
security notes.
Refs #1422: /version and the metrics listener are in place. Audit export is
replaced by shipping the JSONL audit stream (#1560), so no
caseworkctl audit export exists.
Refs #1423: the static fact-read check is in place. Runtime detection, the
fixture gate by default, the bregctl export-field warning, and
language-server findings remain.
Refs #1424: rehearsal is in place. It runs over empty tables, has no
scaffold command, and blocks engine upgrades (#1633).
Refs #1427: enum additions and type widening are in place. Narrowing with
generated validation remains.
Refs #1428: rebaseline has no row limit. Progress reporting remains.
Refs #1455: tlsTrustProfile is in place. Naming the failing dependency and
host in the generic unavailable message remains.
Refs #1352

Not closed by this PR:

Follow-ups

Known break

  • The upgrade rehearsal job fails on this PR by design; tracked in BReg: an engine upgrade cannot test or apply a successor over a package built by the previous release #1633,
    and it blocks the next release.
    "Upgrade rehearsal from the previous
    release" refuses because the v0.34.0 package has no SHA256SUMS
    (package.baseline.package.integrity_refused). Behind that refusal, the
    rehearsal recompiles the predecessor with this branch's compiler and
    refuses with migration.rehearsal.baseline_not_reproducible on a catalog
    fingerprint drift this branch introduces. With both gates bypassed in a
    local prototype, apply, instance-claim adopt, verify, and breg
    readiness all pass over a v0.34.0 database.

Verification

Run with the ci:full label: pull requests skip the heavy integration tier
(PostgreSQL, tutorial, upgrade rehearsal, and composition jobs), and this PR
touches every product's configuration loading, so those jobs are part of the
review evidence.

Local runs on macOS (Apple Silicon). CI job names are given where the local
step reproduces one.

On the final head:

Gate Result
Rust tests (breg) lane, --no-fail-fast pass except install_script (#1363, macOS only)
Public identifier catalog (both steps) pass
Standalone Cargo lockfiles (all four) pass
cargo deny check pass
BReg WASM executor (steps 2 to 6) pass
BReg product contracts, postgres lane pass
BReg product contracts, contracts lane pass except step 11 (macOS FIPS dylib load in a nested script; passes in CI)

On an earlier head of this branch (before the last fixes, which touch only
BReg package error mapping and BReg tests):

Gate Result
cargo fmt --check, cargo check --workspace --all-targets, clippy -D warnings pass
cargo test --workspace pass except install_script (macOS, #1363) and the BReg targets fixed since; an Evidence client run that failed during a local disk-full episode passes in CI
Evidence contracts and source neutrality (steps 2 to 9) pass
Scheduling, Discovery, and Relay client contracts pass
Relay V2 contracts, steps 3 to 5 and 9 pass (steps 6 to 10 need the docs working directory the local harness does not set; the CI job passes)
Casework and Scheduling PostgreSQL lanes pass (the Casework migration-lock test is intermittent under local load; see Follow-ups)
Docs site npm test and npm run check pass
Release tooling, release source model, wheel assembly, client sync pass
gitleaks over the branch commits no leaks

Not run locally: the upgrade rehearsal (see Known break), the Node.js native
binding tests (the macOS FIPS dylib does not load), and the
immediate-action examples (the same dylib). CI covers all three.

DCO

  • Every commit includes a Signed-off-by trailer.
  • I reviewed the submitted changes and am responsible for the contribution.

@jeremi jeremi added the ci:full Run the heavy integration tier on this pull request label Sep 27, 2026
A missing socket, a refused request, a key version Transit has not
created or has retired, and a public key that is not the governed one
all reported the same generic signing failure, so an operator could not
tell a proxy to restart from a keyVersion to change.

TransitSigner::initialize now returns a typed, value-free
TransitInitializationError, and Evidence reports the cause after
"runtime signing initialization failed:" in check and serve. The prefix
is unchanged, so evidencectl still classifies the refusal as a
dependency failure.

BREAKING: registry-platform-crypto TransitSigner::initialize returns
TransitInitializationError instead of SigningError.

Refs #1459

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
…ts audit lock

`evidencectl doctor --runtime-config` could not pre-check a candidate
while the instance it replaces was serving: the dependency check opens
the audit destination, and the running writer holds its lock, so the
check always refused.

`evidence check --require-runtime-dependencies --without-audit-lock`
and `evidencectl doctor --without-audit-lock` now prove every runtime
dependency the plain form proves except the lock itself. The audit
destination is checked with the platform `check_writable` preflight,
which validates the directory, any existing active file and lock
companion, and a complete final entry the way a writer would, without
taking the lock, after the hash key is checked as startup checks it.
The lock-free JSON report states that a second writer is not detected.
The plain form still refuses a held lock, and its refusal now names
the flag. Startup and the lock-free check share one assembly sequence
and differ only in the audit step, so the two cannot drift apart.

Refs #1491

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
…h a named profile

An issuer that serves its key set under a private certificate authority
could only be reached by pointing the whole process at another trust
store, which also widened every source connection. The bundle's
authentication block may now name a tlsTrustProfile, bound in
runtime.yaml exactly like a source profile. The CA is trusted beside the
system roots for the jwksUri connection alone; hostname verification
stays on and a local HTTP issuer cannot name a profile.

The platform gains ValidatedFetchUrl::immediate_get_with_additional_roots
and JwksFetcher::new_trusting_additional_roots to carry the roots.

Refs #1455

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
… not declare

A registry field rename passed every authoring gate: the source update
installed a fact schema without the old name, the candidate built, and
every request then failed at evaluation with a missing-fact refusal.
The operator learned from production traffic rather than from review.

The authoring library now reads the parsed answer function, without
running it, and names each literal fact read on its first parameter
that the question's source does not declare. evidencectl applies the
check wherever it reads a project, so check, fixtures run, build,
package, source diff, and source update refuse the rename before
anything is installed. The declared set is an inline operation's facts
or the properties of a referenced source's closed fact schema; an open
schema, a computed key, and a read outside answer are left to the
fixtures. The check is source-product neutral.

BREAKING: a project whose derivation reads an undeclared fact is now
refused as evidence.authoring.derivation-fact-undeclared.

Refs #1423

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
`bregctl test --baseline-runtime-config` now rebuilds the predecessor
schema from the verified active package's signed sources on the
disposable test database, requires it to reproduce the recorded
predecessor fingerprint, and runs the successor migration in apply order:
pre-assertions, compiler statements, reviewed steps, deferred constraints
and views, runtime ACL reconciliation, and post-assertions. It requires
the candidate schema fingerprint and rolls everything back in one
transaction. `package` needs the test receipt, so a plan the rehearsal
refuses can no longer be signed and fail only at `apply`.

The rehearsal runs over empty tables: it proves SQL validity, ordering,
and reachability of the target schema, not data-dependent failures.
Field-encryption backfill steps are skipped and reviewed fixture files
are not loaded. The operator docs now describe the reviewed-migration
file layout, the backup binding `apply` checks, and what `test` does and
does not rehearse.

Security: a PostgreSQL error message can quote row values. The
rehearsal failure reports carry only the SQLSTATE, its class, and the
schema, table, column, and constraint names, enforced where
`postgres/rehearsal.rs` maps the database error. The negative test
`real_postgres_rehearsal_refuses_a_reviewed_plan_activation_would_refuse`
plants a canary in a failing step and asserts it never reaches the
report.

Refs #1424

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
`history rebaseline` refused any registry holding more than 1,000 live
rows, so a larger registry could never regain snapshot coverage after an
erasure. The verification now reads each entity's live rows, their
journal heads, and the retained heads in the same record-identifier range
in pages of 1,000 inside the same transaction, proves each entity holds
exactly one retained journal head per live row, and keeps memory and
every per-entity statement flat as the registry grows.

The run is a read and write outage for every entity, as it was under the
old limit, and it now lasts as long as the registry is large. The
migration role lifts forced row security on every entity table before the
first page with ALTER TABLE, which holds each table in ACCESS EXCLUSIVE
mode until the transaction ends, so API reads wait as well as writes. The
migration role deliberately holds no BYPASSRLS authority, which is the
only way to read the rows without that lock, so the operator docs state
the outage and give a sizing and maintenance-window rule instead: time
the command against a restored copy, allow at least twice that, drain or
announce API traffic, and size the statement timeout for one scan of the
revision journal (the unindexed-head check) and the lock timeout for the
in-flight requests at the start.

BREAKING: `HistoryRebaselineError::LiveRowBudgetExceeded` and
`MAX_REBASELINE_LIVE_ROWS` are removed, and bregctl no longer emits
`history.rebaseline.live_rows.budget_exceeded` or
`field_encryption.erase_history.rebaseline.live_rows_budget_exceeded`.

Security: the threat is a baseline that vouches for a live row its
journal does not reproduce, or for a journal head whose live row is
gone. `verify_every_live_row_matches_its_journal_head` in
`history_migration.rs` compares every page's live rows with their heads
and the retained heads in the page's key range, then refuses any head
past the last live row, before the baseline commit is written. The
negative tests `rebaseline_refuses_a_mismatch_on_a_later_page`,
`rebaseline_refuses_a_journal_head_with_no_live_row`,
`rebaseline_refuses_a_journal_head_before_the_first_live_row` and
`rebaseline_refuses_a_journal_head_past_the_last_live_row` prove the
refusals past the first page and at both ends of the key range. Row
security is not weakened: the force is lifted only inside the migration
transaction and restored before it commits.

Refs #1428

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
A reviewed `chunked_backfill` step changed live rows without appending a
history revision, so snapshot and as-of reads kept serving the values the
backfill had replaced. Each chunk now captures the rows it selects,
runs its UPDATE, requires the changed row count to equal the selected
count, and appends one revision per changed row in one history commit,
all inside the chunk's own transaction. The field-encryption backfill
already journaled its chunks this way; both now share the page capture.

`bregctl test` classifies every step `apply` journals with the same
check, so a step the journal would refuse fails the rehearsal as
`migration.rehearsal.history_step_refused` instead of failing `apply`.

BREAKING: `chunkSize` for a reviewed chunked backfill is capped at
1,000 (it was 10,000), the history commit's member budget. Its SQL must
be one UPDATE of the declared entity that writes no record metadata and
uses no refused statement word outside comments and plain string
literals. A plan outside those limits that `test` and `package` accepted
before is now refused.

The lexical first check that journaled steps share sets aside comments
and plain string literals, accepts a line break after UPDATE, and
matches record metadata columns and statement words as whole words, so a
leading licence header or a member named `created_at_source` no longer
refuses a step. It reads a dollar-quoted body, a literal holding a
backslash, or an unterminated comment or literal as written, so a
statement word inside one still refuses the step: the scan never guesses
where such a construct ends. The parsed-statement validator and the
journal's metadata check stay behind it.

Security: the threat is retained history that diverges from the live
rows a reviewed backfill rewrote, so an as-of read or export serves
values the registry no longer holds and the journal cannot account for
the change. `execute_reviewed_chunk` in `postgres/interlock.rs` journals
every chunk through `prepare_reviewed_page_capture` and
`finish_reviewed_page_update`, and `check_reviewed_history_step` in
`history_migration.rs` refuses a step the journal cannot record, both at
apply and in the rehearsal. The negative tests
`reviewed_chunked_backfill_refuses_a_chunk_size_beyond_the_commit_budget`,
`chunked_backfill_refuses_the_statement_shapes_the_journal_cannot_hold`,
the `rank-dollar-quoted` case of
`real_postgres_rehearsal_refuses_a_reviewed_plan_activation_would_refuse`,
and the history assertions in
`real_postgres_backfill_and_destructive_recovery_are_bounded_resumable_and_activation_closed`
cover the refusals and the per-chunk revisions.

Refs #1480

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Removing a field or an entity from a registry package drops the live
column or table, but every revision snapshot recorded before the change
still holds the removed values, and so does every database backup. An
operator reviewing the diff could read the removal as erasure.

`bregctl diff` now reports each removed field or entity as the finding
`diff.history.removed_values_retained`, against the compiled diff, with
a message that names `bregctl history erase` as the only command that
removes the values, whole revisions of one record at a time. The change
and retention guides say the same.

Security: the threat is an operator treating a package removal as data
minimization or erasure while the values stay readable in retained
history and backups. `removed_value_findings` in
`crates/registry-bregctl/src/lib.rs` raises the finding for every
`field_removed` and `entity_removed` change, and
`a_removed_field_is_reported_as_retained_in_history_not_erased` proves
the finding for a removed field and its absence for an unchanged
candidate. The diff is offline, so it does not count the affected
snapshots, and there is still no command that removes one field from
history.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Raising a `text` field's `maxLength` was a `field_type_changed` change
that needed a reviewed migration, although every stored value already
meets the higher limit. It is now the compatible additive change
`field_length_widened`: the compiler replaces the column's inline length
check under the same lock and row validation a vocabulary code addition
uses, keeping the constraint name a fresh install gives it. A revision
recorded under the lower limit stays readable, and an action that sets
or requires the entity reports `action_target_fields_widened` and stays
additive instead of becoming a reviewed `action_changed`.

A `string` field's `maxLength` is its `varchar` column type, and a
column type change needs the dependent views and row policies rebuilt,
so raising it stays a reviewed `field_type_changed`. Lowering any limit
stays destructive.

Security: the threat is a live check swap that quietly admits values
the successor does not declare, or drops a second check on the column.
`replace_inline_field_check_statement` in `generated_ddl.rs` replaces
only the single unnamed check over the column and now also excludes the
field pattern's named check, and `FieldTypeSource::widens_text_length_of`
in `contract.rs` admits only a `text` to `text` change with a higher
limit. `text_length_widening_replaces_the_length_check_and_keeps_existing_rows`
proves the pattern still refuses and the higher limit still bounds, and
`text_length_widening_is_additive_and_replaces_the_length_check` and
`a_raised_text_limit_on_a_targeted_entity_keeps_the_action_contracts`
prove that narrowing and a `string` limit stay reviewed.

Refs #1427

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
The successor rehearsal in `bregctl test` and the rebaseline without a
live-row limit both change what an operator sees, and both remove or
tighten something a pipeline may rely on, so each gets a BREAKING entry
that links the operator guide describing it.

Refs #1424
Refs #1428

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
When PostgreSQL refused a compiler statement or a reviewed step after
`apply` began maintenance, the report said only
`apply.migration.failed`, so an operator had nothing to act on before
reaching for `migration reconcile`. A server refusal now keeps its
SQLSTATE and the table, column, and constraint names PostgreSQL
reported, and `apply` reports it as `apply.migration.statement_failed`
with that SQLSTATE and its class. A lost connection still reports
`apply.migration.failed`, and the target stays pinned either way.

`PostgresFailure` moves out of the rehearsal module so `apply` and the
rehearsal share one value-free error shape; it is now exported from
`registry_breg::postgres` under every feature set. A reviewed chunk's
refusal is classified like a transactional step's, so a chunk that
breaks a field pattern names the field.

BREAKING: a statement refusal after maintenance began reports
`apply.migration.statement_failed`, not `apply.migration.failed`, and
`registry-breg` reports it as the new `MigrationError::StatementFailed`
variant. Automation that matches the old code or matches
`MigrationError` exhaustively must add the new case.

Security: the threat is a PostgreSQL error message, detail, or hint that
echoes a stored or computed row value (a failed cast repeats its input,
a unique violation repeats the key) and would reach the apply report,
logs, or CI output. `PostgresFailure::from_error` in
`postgres/failure.rs` retains only the SQLSTATE and object names, and
`PostgresKernelError::from_statement_error` in `postgres/mod.rs` is the
only path that carries a refusal to `MigrationError::StatementFailed`,
whose Display is built from those fields alone. The negative test
`refused_step_reports_its_sqlstate` in `tests/postgres_migration.rs`
applies a step whose constant PostgreSQL refuses and asserts the
refusal names `SQLSTATE 22P02 (data exception)` and carries neither the
constant nor the managed schema name, and
`apply_reports_a_refused_statement_with_its_sqlstate_and_objects` pins
the report.

Refs #1424

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
A lower `minLength` on a `string` field under the same `maxLength` only
relaxes a column check, but the planner classified it as
`field_type_changed` and demanded a reviewed migration. It is now
`field_length_widened`, applied live by the same constraint swap a raised
`text` `maxLength` uses: the successor replaces the single inline check
over the column with the lower minimum, or drops it when the minimum
falls to 0, since a fresh install of the candidate declares none. The
catalog then matches a fresh install. Revisions recorded under the
higher minimum stay readable, and an action whose only change is the
lower minimum stays additive as `action_target_fields_widened`.

`text` has no `minLength`, so the live lowering applies to `string`.
Changing a `string` field's `maxLength` (its `varchar` column type) and
widening a `decimal` stay reviewed migrations: both alter a column type
that the generated views and row policies depend on, and the reviewed
path is where that dependency is handled.

Security: the threat is a live check swap that loosens a different
constraint than the one the change relaxes, or leaves the catalog
diverged from a fresh install so that the fingerprint no longer proves
the schema. `FieldTypeSource::lowers_string_min_length_of` in
`contract.rs` accepts only the same `maxLength` and a strictly lower
minimum, and `replace_inline_field_check_statement` in
`generated_ddl.rs` selects exactly one single-column check with
`INTO STRICT`, excluding compiler-named, temporal-order and pattern
constraints, and fails closed otherwise. Encrypted fields get no swap.
The negative cases in
`string_minimum_lowering_is_additive_and_replaces_or_drops_the_length_check`
(a raised minimum and a changed maximum stay destructive),
`a_lowered_string_minimum_keeps_recorded_values_readable`,
`a_lowered_string_minimum_on_a_targeted_entity_keeps_the_action_contracts`,
and the refusals and fingerprint equality in
`string_minimum_lowering_replaces_or_drops_the_length_check_and_keeps_existing_rows`
cover it.

Refs #1427

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
…ck reconciliation

A reconciliation that failed every pass left /ready green and doctor
reported a generic dependency failure, so an operator could not tell a
wedged source from a missing secret.

Readiness now fails after five consecutive failed passes for a source and
recovers on the next successful pass. Reconciliation and synchronization
continue past a failing subject. doctor names the check that refused
(casework.doctor.check-failed, doctor:/checks/<check>) with a value-free
cause, including the audit destination check, reports per-source
reconciliation health, and prints the package digest. An unmigrated
database is named as a schema that is not current.

BREAKING: caseworkctl JSON reports move to v1alpha2; migration 18 must be
applied before serving.

Refs #1448

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
…rop on the audit writer migration

Two branches each adding migration 17 would collide silently; the test fails when a version is missing, duplicated, or when AUDIT_OUTBOX_DROP_VERSION stops naming 0017_audit_writer.sql.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
… work

A package that removes a queue, profile or source, edits a pinned review
kind without a new version, or changes what a source read discloses used to
activate silently, leaving in-flight reviews and open work items that no
caller could finish. The runtime now compares the pinned work with the
package before any activation write and refuses by name, with counts,
unless package.acknowledgeStrandedWork names that exact package digest.
caseworkctl doctor reports the same comparison as pinnedWork and fails the
pinnedWork check by name.

BREAKING: casework serve refuses such a package; DoctorReport gains
pinnedWork.

Refs #1416

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Casework and the Base Registry Engine ship in lock-step, but nothing told
an operator when a rolling upgrade left them on different releases: the
adapter kept talking and failures surfaced later as opaque source errors.

BReg now reports its release on GET /v1/registry in the
Registry-Engine-Version header, the BReg client exposes it on the response
metadata, and the Casework adapter treats a different release as the
source being unavailable, naming both versions. caseworkctl doctor reports
the mismatch with the upgrade step to take.

BREAKING: a Casework deployment whose BReg runs another release is now
refused as unavailable until both run the same release.

Refs #1419

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
An operator had no way to scrape the runtime's health.

The runtime gains an optional metricsListener: a second, operator-private
socket serving /metrics and /version, refused unless it is loopback or
private and distinct from the API listener. Absent by default, so no new
socket opens.

Refs #980

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
…irst

A review authority restored from an older backup or replaced answers an
accepted review's result lookup with an empty 404. BReg left such a review
looking healthy, let it compete with live reviews for every poll batch, and
offered no supported way to submit it again or stop waiting.

The lookup now records result-unknown-to-authority, the poller serves a
review with a pending webhook completion first and an unknown one last, and
a pending completion makes its review due at once. bregctl review-recovery
resubmit submits the exact retained request again under its original
idempotency key, and close stops waiting with operator-closed. Both run
behind the verified operator boundary request retention uses, refuse by a
closed reason that names the state and code, and write an audit request
entry before the transaction and its response after the commit, naming the
request only by its keyed reference.

Refs #1454

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
A session the server ends mid-step (a terminated backend, a shutdown,
a failover, an idle-in-transaction timeout) arrives as a FATAL
database error, and it was reported as a refused statement with
ReconcileFailedMigration guidance. PostgresKernelError::from_statement_error
now maps SQLSTATE class 08, 57P01 to 57P05, and 25P03 to Connection, so
apply reports apply.migration.failed as it does for a client-side loss.
The verified-DDL path now classifies its errors through the same
function, so a statement timeout is Statement(57014) on both DDL paths.

BREAKING: the changelog entry now names the PostgresKernelError::Statement
variant, which breaks an exhaustive match on that enum as well as on
MigrationError.

Security note: threat is an operator acting on the wrong recovery
guidance after a server-ended session; both classifications keep the
target pinned, so this is a reporting fix, not a safety change.
Enforcement is sqlstate_ends_the_session in
crates/registry-breg/src/postgres/mod.rs. Negative tests are
a_server_ended_session_is_a_connection_failure_not_a_refused_statement
and failed_resume_and_ddl_timeout_are_fail_closed_on_real_postgres.

Refs #1424

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
The lexical check on a journaled reviewed step masked a line comment
through the next line feed only, while PostgreSQL also ends it at a
carriage return, so an assignment after `--\r` was hidden from the
scan. It also read a Unicode-escape identifier such as
U&"created\005Fat" as words other than the column it decodes to.
The mask now ends a line comment at either newline character, and any
U& identifier or string and the UESCAPE keyword are refused anywhere
in the statement.

The runtime metadata-equality check in journal_captured_changes now
also compares created_at and updated_at, captured as text in the same
transaction, so a step that changes either is refused even if a
spelling escaped both the lexical and the syntax-tree checks.

Docs: the reviewed-migration page says metadata may not be named
anywhere in the statement, even in a read; the retention page and the
erase-history lifecycle state that a rebaseline blocks reads as well
as writes, and that a paged verification statement is bounded by one
page of records and their retained revisions rather than a fixed cost.

Security note: threat is a reviewed step that rewrites record metadata
so the history journal no longer matches the live row. Enforcement is
reviewed_update_words and mask_comments_and_literals in
crates/registry-breg/src/history_migration.rs, with
CapturedEntityRow::keeps_record_metadata_of at journal time. Negative
tests are
chunked_backfill_refuses_the_statement_shapes_the_journal_cannot_hold
(carriage-return comment, both U& spellings) and
a_reviewed_step_that_changes_any_record_metadata_is_detected.

Refs #1480
Refs #1428

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Every runtime reads its runtime.yaml under the same file, parsing, and
substitution rules, and spells secret providers, database references, the
JWKS source, the package pin, and the listener the same way. The loader
refuses unsafe files, removed keys with their replacement, a wrong
envelope, and substitution inside secret references; the blocks carry a
canonical JSON Schema the conformance gate compares products against.

Ed25519 and ES256 key generation join ES384 so adopter tooling can mint
the key types the runtimes verify.

BREAKING: reject_deprecated_config_fields is removed; runtimes declare
removed keys on the loader.

Refs #1408

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
… loader

Scheduling now reads its runtime configuration through the shared
registry-platform-config loader and blocks, so its envelope, secret
providers, database, package, listener, and JWKS source share one
implementation and one schema with the rest of the stack.

BREAKING CHANGE: authentication.oidc.jwksUri is removed and refused with a
diagnostic naming jwksSource kind: uri; listener.bind is required; the
runtime configuration and every configured path are refused when they pass
through a symbolic link; an environment expression in the authored policy,
records, or fixtures is refused with the field that holds it.

Adds ${VAR} substitution in runtime.yaml string values outside *Ref fields
and the optional package.expectedDigest pin checked against the verified
package's policy digest.

Refs #1408

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Registry Render loads its runtime file with the platform loader and the
shared package, listener, and secret provider blocks, so the file follows
the same envelope, size, ownership, and substitution rules as the other
runtimes.

BREAKING CHANGE: runtime.yaml must declare
apiVersion registry.registrystack.org/render-runtime/v1alpha1 and kind
RenderRuntimeConfig. server.bind is listener.bind and is required,
server.shutdownGraceSeconds is listener.shutdownGraceSeconds, bundle.path
is package.root, audit.path must be absolute, and secret providers are
declared under secretProviders with an absolute file root. Removed keys,
among them audit.directory, audit.integrityKeyRef, and
audit.maxSegmentBytes, are refused with their replacement named. serve,
healthcheck, and check require --runtime-config FILE; the default runtime path and
the REGISTRY_RENDER_RUNTIME variable are gone. package.expectedDigest, when
set, must equal the sealed bundle hash reported by /health.

Refs #1408

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
A socket address parser accepts any number of leading zeroes in the
port, so an unbounded listener.bind text is refused before it is parsed.
The shared schema carries the same maxLength, and the Scheduling runtime
schema that embeds the block is regenerated.

Refs #1408

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
…loader

The Discovery runtime loads its runtime file with the platform loader and
the shared listener block, replacing its own YAML reader, so the file
follows the same envelope, path, size, and substitution rules as the other
runtimes. A loader refusal now names the file and field in the startup
error instead of reporting an opaque invalid configuration.

BREAKING CHANGE: runtime.yaml declares apiVersion
registry.registrystack.org/discovery-runtime/v1alpha1 and kind
DiscoveryRuntimeConfig instead of schemaVersion, and listener.address is
listener.bind. Both removed keys are refused with their replacement named.
The discovery binary takes --runtime-config FILE instead of --runtime; the
path must be absolute and free of symbolic links. The container image
passes the new flag in its default command.

Refs #1408

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
A runtime that renders its effective configuration, as Relay's authoring
tooling does, needs the blocks to write back the key names they were read
from. Optional fields are omitted rather than written as null, so the
generated schemas no longer claim a null default for them.

Refs #1408

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Relay now reads its deployment binding through the registry-platform-config
loader and shared blocks, so its envelope, secret providers, package,
listener, and JWKS source share one implementation and one refusal
vocabulary with the rest of the stack. Secrets resolve only through the
providers the file declares, and a file secret no longer resolves beside
whatever directory the runtime file happens to sit in.

BREAKING CHANGE: runtime.yaml opens with apiVersion
registry.registrystack.org/relay-runtime/v1alpha1 and kind
RelayRuntimeConfig. server.bind is listener.bind, packagePath is an
absolute package.root with an optional package.expectedDigest pin checked
against the package revision, and authentication.issuer is
authentication.oidc with issuer and a jwksSource of kind discovery or uri;
each removed key is refused with its replacement. The audit block keeps the
shared destination shape; audit.sink and audit.integrityKeyRef are refused
with a diagnostic naming audit.path. secret:file/ resolves under
secretProviders.file.root and secret:env/ needs
secretProviders.environment. relay check and relay serve take a required
absolute --runtime-config; the --runtime flag, the RELAY_RUNTIME variable,
and the default path are gone, and the container image passes the flag in
its default command.

The site changelog also gains the Scheduling entry and uses the file's
BREAKING marker for the Discovery and Render entries.

Refs #1408

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Environment substitution belongs to runtime.yaml. An authored file that
carries a ${VAR} expression would read as a literal and hide the
operator's intent, so a Relay registry.yaml and a Render bundle
manifest.yaml now refuse one with the field that holds it. Relay reports
contract.environment_expression; Render reports a manifest problem.

BREAKING CHANGE: a registry.yaml or bundle manifest.yaml holding a
${...} expression no longer loads.

Refs #1408

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Refs #1409

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Refs #1409

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Replace bundle and runtime revisions with the verified package digest
while preserving package-derived requirement revisions. Package
production outputs, dev staging, committed fixtures, frozen contracts,
and operator guidance now use the shared deterministic envelope.

Refs #1409
Refs #1352

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Replace manifest sealing with deterministic Registry Stack packages, bind verified captured bytes through rendering, and update Render contracts and examples.

Refs #1409

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Migrate discoveryctl and runtime startup to the shared package envelope,
preserve exact verified index bytes, and update the maintained operator
journey.

Refs #1409

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Require all seven adopters to prove the common package pin refusal, migrate maintained Evidence and Discovery references, and keep local BReg-Evidence development on generated verified packages.

Refs #1409

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Remove the obsolete runtime digest contract and direct maintained checks through an editable project, deployment target, and installed package.

Refs #1409

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Republish positive test packages after authored mutations, keep deliberate tamper cases negative, and align migrated package CLI expectations.

Refs #1409

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Refs #1409

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
…ma head

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
…be read back

Closes #1621
Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
…nces

An import run's input digest is computed by the client over the file it
reads and recorded on the run; the server never receives the file and
does not recompute it. The guides, the API reference, the ingestion-run
contract, the security matrix, and `import-authority open --input-sha256`
help now say so, instead of implying the pin binds what the chunks write.
The item volume stays the bound the server enforces.

The import guide also states that import is create only, so re-running
lines that already committed creates their records again.

Refs #1420

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
AGENTS.md requires tracked review notes for changes to authorization,
audit integrity, and release provenance. The notes cover import
authorities, the instance claim, audit retention, review recovery, and
package building and byte binding: the threat, the enforcement point and
defaults, the negative tests, and the residuals each change accepts,
including client-announced import digests, create-only re-runs, and
physical restores the instance claim does not detect.

Refs #1420
Refs #1454
Refs #1409

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
…and runtime config

Review the catalog change against the clap definitions: the --runtime-config
flag across breg, evidence, relay, and registry-render; the package --revision
and --dry-run options; bregctl import-authority, instance-claim, and
review-recovery; registry-render package replacing seal; evidencectl build
removed; and --without-audit-lock on evidence check and evidencectl doctor.

Refs #1409

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 27, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-27T15:31:36.542391Z 4945694 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4945694b7a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +889 to +891
finish_reviewed_page_update(&transaction, registry, target_package_revision, capture)
.await
.map_err(|_| PostgresKernelError::RegistryUnavailable)?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Verify the journaled rows match the executed chunk

When reviewed chunk SQL accidentally updates a different but equally sized set of records from the UUIDs in $1, affected == selected passes, while finish_reviewed_page_update rereads only the originally captured IDs and may journal fewer or zero changes. Its returned change count is discarded, so the transaction then commits the unjournaled updates and advances the checkpoint, corrupting revision history. Compare the journaled count with affected and refuse any mismatch.

AGENTS.md reference: products/breg/AGENTS.md:L10-L15

Useful? React with 👍 / 👎.

Comment on lines +1076 to +1080
transaction
.commit()
.await
.map_err(|_| ImportAuthorityError::Unavailable)?;
append_transitions(&self.audit, pending).await?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Make authority transitions and audit records recoverable

If the audit writer fails after the database commit—for example because the audit volume fills—the command returns unavailable but the authority transition remains committed. Retrying open now gets AlreadyOpen, while retrying close sees a terminal row, and neither path recreates the missing transition record; partial failure while appending several records has the same permanent gap. Persist an audit outbox with the transition or otherwise make the committed record replayable.

AGENTS.md reference: products/breg/AGENTS.md:L10-L13

Useful? React with 👍 / 👎.

Comment on lines 3973 to +3977
root: &Path,
context: &PredecessorPackageContext<'_>,
) -> Result<VerifiedPredecessorPackage> {
let shared = verify_shared_package(root)?;
load_predecessor_package_with_verified_envelope(root, context, &shared)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Accept the previous release's package during upgrade

An existing v0.34.0 registry's active package predates SHA256SUMS, but predecessor loading now unconditionally runs the new shared-envelope verifier. Consequently bregctl test/apply cannot load the active predecessor and the required forward upgrade rehearsal fails before any migration can run. Add a bounded compatibility path for the immediately previous signed package format, or an executable conversion step that works before successor apply.

AGENTS.md reference: products/breg/AGENTS.md:L10-L15

Useful? React with 👍 / 👎.

Comment on lines +416 to +417
let conflicts = crate::stranded_pinned_work(store, project, &adapters).await?;
match crate::pinned_work_verdict(&conflicts, package_digest, acknowledged) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Serialize the pinned-work check with policy activation

During a rolling deployment, the old Casework process can admit a review or create an open work item after this standalone inventory read but before the new process begins serving. The candidate can therefore pass the check, remove a queue, profile, source, or pinned policy dependency needed by that newly admitted work, and then activate with the work hidden or orphaned. Coordinate the inventory check and activation with an admission fence or database lock that every work-creation path also observes.

Useful? React with 👍 / 👎.

Comment on lines +1438 to +1439
side.run("evidencectl", "build", "--project", str(self.project), "--target",
str(self.target), "--output", str(upgraded))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Invoke the replacement Evidence packaging command

When the predecessor target needs the grammar migration, this path invokes the current evidencectl build, but the command added in this commit is a hidden retired spelling that unconditionally returns evidence.build.retired and instructs callers to use package. Thus the required Evidence upgrade rehearsal always aborts before the successor runtime is checked or served. Invoke evidencectl package here instead.

Useful? React with 👍 / 👎.

side.run("evidencectl", "build", "--project", str(self.project), "--target",
str(self.target), "--output", str(upgraded))
runtime = migrate_evidence_runtime(load_yaml(self.runtime))
runtime["package"]["root"] = str(upgraded / "bundle")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Point the upgraded runtime at the package root

For the current evidencectl package format, SHA256SUMS, evidence.yaml, and the governed artifacts are written directly under the output directory, with no bundle child. Pointing the operative runtime at candidate-upgraded/bundle therefore makes the subsequent evidence check refuse the package as missing, so the rehearsal still cannot exercise the upgraded service even after using the correct packaging command. Set package.root to upgraded itself.

Useful? React with 👍 / 👎.

Merged via the queue into main with commit 9013bf7 Sep 27, 2026
58 of 59 checks passed
@jeremi
jeremi deleted the jeremi/first-adopter-wp1-wp2 branch September 27, 2026 15:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment