Skip to content

feat(evidence): compose BReg lookups as native Evidence sources - #865

Merged
jeremi merged 35 commits into
mainfrom
dx/evidence-breg-native-split
Sep 6, 2026
Merged

feat(evidence): compose BReg lookups as native Evidence sources#865
jeremi merged 35 commits into
mainfrom
dx/evidence-breg-native-split

Conversation

@jeremi

@jeremi jeremi commented Sep 6, 2026

Copy link
Copy Markdown
Member

Summary

Native BReg to Evidence composition: a governed BReg lookup becomes an Evidence source without hand-written adapters. Five single-area commits carry the feature (Evidence runtime and contracts, evidencectl, the BReg exporter, the bregctl dev lifecycle, docs and CI), followed by the fixes from the staff-engineer review of the original branch. Thirty-five linear commits, no merge commits, each signed off: twenty-six from the split, then five from the first review round (three Codex findings fixed, two first-run CI failures fixed, the answered findings recorded below), then one from the second round (evidence-grade targets were refused before compilation; fixed with its pinning tests), then two from the third round (the supervisor reads a whole control command before matching it; a starter that ships no source has its next step deferred to its README), then one from the fourth round (target explain inventories the public keys before it compiles, so a missing key reaches the report instead of ending inside the compile error).

  • Evidence gains named source connections and selector profiles: a bundle names the connection once, a question selects the profile, and the runtime binds the two under the same contract as http-json-source (the source-connection schema now carries the same allOf, so the published contract and the loaded-bundle validator refuse the same documents).
  • evidencectl imports a BReg source export (bounded manifest, artifact count and sizes, stem and provenance shapes, exactly one sources artifact, explicit resolution before an edited artifact is replaced) and scaffolds deployment targets whose settings are validated through the same closed governance type every consumer reads.
  • BReg exports governed lookups as Evidence sources: one route per selector alternative, the extraction adapter that checks the returned identity against the selector, and refusals for the shapes it must not export (change-request lifecycle entities, profiles without a lookup grant, grants without a compiled lookup route, profile names past 64 bytes, composite selectors past 8192 bytes).
  • bregctl gains a retained local development lifecycle (bregctl dev start | stop | status) on a Docker-owned PostgreSQL with a named data volume, dev stop --remove to reclaim container and volume, output-format aware reports, doctor classification that only treats an unready database as "needs activation", and role passwords redacted at the diagnostics boundary; bregctl generate evidence-source writes the export.
  • Docs and CI: two tutorials (evidence from BReg; custody and cutover) with the public evidence check command and admonitions at the irreversible steps, reference pages, products/breg/AGENTS.md, the README link to the native development guide, a starter archive generated by the docs pipeline instead of committed, and the composition proof in its own non-Docker breg-evidence-composition job so breg-tutorial keeps its previous scope.

Generated outputs travel with their source change: the two commits that change the bregctl surface carry the regenerated CLI reference pages and cli-reference.json; the schema commit carries the regenerated evidence-configuration.json.

Checks

Local, on the branch head, rustup cargo 1.95.0 with CARGO_INCREMENTAL=0 CARGO_PROFILE_DEV_DEBUG=0 CARGO_PROFILE_TEST_DEBUG=0:

  • cargo fmt --check and cargo clippy --locked --profile ci --all-targets -- -D warnings for registry-evidence, registry-evidence-authoring, registry-language-server, registry-evidencectl, registry-breg, registry-bregctl and registry-cli-docs: clean.
  • cargo test --locked --profile ci for the same seven crates (registry-cli-docs and registry-language-server are the developer-tools job's pair): all passing (registry-breg lib 229, http_read_only 68, registry-bregctl lib 80 and cli 83 among them). One pre-existing flaky test, glibc_at_the_floor_installs_both_commands in registry-breg (unchanged from main), fails when its two Linux fixtures are stamped in the same microsecond and one removes the other's assets; it passes alone and on rerun, and is listed in the follow-ups. One whole-crate run of registry-evidencectl during the second round failed interrupted_replacement_and_baseline_advance_restore_the_complete_prior_state (a test this branch adds) on its final lock acquire, once; 0 failures in 50 reruns of the lib tests (20 alone, 30 as two concurrent loops), and the assertion hides the error it got, so it is listed in the follow-ups with that diagnostic gap.
  • Live retained lifecycle: cargo test --locked --profile ci -p registry-bregctl --test dev_lifecycle -- --ignored after building breg (runtime), bregctl and mint in the ci profile: 1 passed in 25 s; the test authors a selector profile and a lookup access profile, exports the source, drives the live registry from the export (route, method, identity field, scalar type, projection, access profile, fixed headers), and reclaims container and volume.
  • Composition proof in the job's shape: verify-composition.py against the ci-profile bregctl, evidencectl and evidence: 8 export artifacts, 22 fixture cases, 2 questions, provenance-only revisions unchanged, consumed-change revisions both changed, native source update passed.
  • products/breg/scripts/test-postgres.sh against a throwaway PostGIS container at the digest CI pins: all 52 invocations passing. products/breg/scripts/check-contracts.sh (103 tests), check-client-contract.sh and products/evidence/scripts/check-contracts.sh (generated contracts reproduce exactly): passing.
  • Docs: npm run check (26245 links, check-llms, anchors, docset, archive lock, frontmatter, notary, Vale, SVG, SEO, accessibility), npm test (490), npm run check:cli-reference, and npm run generate leaving no tracked drift.
  • python3 .github/scripts/test_ci_changes.py: 75 OK, including the new breg_evidence_composition routing test and the narrowed breg_tutorial one.
  • shellcheck and shfmt on touched scripts (only pre-existing findings remain); cargo check --locked for every one of the 35 commits in a detached worktree; gitleaks dir --config .gitleaks.toml on the tree: no leaks found.
  • Not run locally: test-postgres-tls.sh (needs the CI service container); nothing on the branch touches that path.

Pinning tests per review item are named in each fix commit. The ones that pin a trust boundary: source_connections_are_constrained_alike_by_the_bundle_schema_and_the_runtime (registry-evidence config), an_exported_adapter_refuses_a_returned_identity_that_is_not_the_selector (registry-evidence Rhai runtime), target_new_rejects_unknown_governance_field and target_new_rejects_assurance_profile_outside_enum (evidencectl), only_an_unready_database_classifies_a_doctor_refusal_as_not_activated and role_password_bytes_cannot_reach_diagnostics_or_errors (bregctl dev), lookup_route_conceals_a_substituted_access_profile_selection (registry-breg read API). The review round adds refuses_a_string_selector_field_that_accepts_the_empty_value (registry-breg exporter), extends the live installed_dev_preserves_edits_and_recovers_failed_start_without_reseeding with a container removed by hand, and extends the source-connection schema pin with an unauthenticated loopback connection under both non-local assurance profiles. The second round adds evidence_grade_target_is_held_to_the_production_checks, evidence_grade_target_compiles_through_the_production_path and an_unknown_assurance_profile_is_refused_by_name (evidencectl). The third round adds a_control_command_split_across_writes_is_read_whole and a_control_command_stops_at_the_newline_or_the_size_bound (bregctl dev supervisor) and a_starter_without_sources_defers_the_fixture_run_to_its_readme (evidencectl scaffold). The fourth round adds a_missing_target_public_key_is_reported_by_explain_before_compilation and explain_reports_the_fixture_paths_when_every_public_key_is_present (evidencectl target).

Notes

Security review notes for the changed trust boundaries:

  • Source-connection contract. Threat: an operator follows the published contract, points an unauthenticated source connection at a public origin or pairs one with a private-CA trust profile, and believes the contract sanctions it. Enforcement point: SourceConnectionConfig::validate and validate_local_unauthenticated_source_origin for a loaded bundle, with the schema now refusing the same documents ahead of it, authentication.kind: none on a source connection under production or evidence-grade assurance included. Negative test: source_connections_are_constrained_alike_by_the_bundle_schema_and_the_runtime.
  • Returned-record identity. Threat: an adapter returns a record other than the selected one and the runtime treats it as the answer for the selector. Enforcement point: the extraction adapter the BReg exporter generates for every alternative (the runtime enforces arity, no-batch pairing, the fresh authorized selector subset and FactSet validation, and inspects nothing inside the script); the security matrix rows V1-I08 and V1-I11 now say so. Negative test: an_exported_adapter_refuses_a_returned_identity_that_is_not_the_selector; the emitted comparison is pinned verbatim by emitted_extract_checks_the_returned_identity_by_exact_value_and_scalar_type.
  • Target settings. Threat: a settings file target new accepts but every consumer refuses, leaving a create-only directory with no in-place repair. Enforcement point: target::validate_settings_documents delegating to build::TargetGovernance::into_bundle. Negative tests: target_new_rejects_unknown_governance_field, target_new_rejects_assurance_profile_outside_enum.
  • Doctor classification. Threat: a doctor failure of any other cause is read as "needs activation" and dev start proceeds on a broken database. Enforcement point: activation() in the bregctl dev module, the single classification site. Negative test: the Unrelated arm of only_an_unready_database_classifies_a_doctor_refusal_as_not_activated.
  • Role password at the diagnostics boundary. Threat: the plaintext role password reaches the private log, stdout or a returned error through libpq's statement echo. Enforcement point: redaction inside the command wrapper for the call that supplies the secret, before any byte is logged or wrapped into an error, windowed because libpq echoes a truncated statement. Negative test: role_password_bytes_cannot_reach_diagnostics_or_errors.
  • Evidence-grade targets. Threat: an evidence-grade deployment compiled under the local rules would accept unauthenticated or plaintext sources; the branch's first shape of compile_target_project instead refused every evidence-grade target before compilation, a regression from main's unconditional production checks once the local profile was added. Enforcement point: the assurance match in compile_target_project, which selects the production arm for production and evidence-grade (input validation under the production rules, disposable local identifiers rejected, every source held to the production transport conditions) and refuses any other value naming all three profiles. Negative test: evidence_grade_target_is_held_to_the_production_checks (an unauthenticated source is refused under evidence-grade with the same message as under production); evidence_grade_target_compiles_through_the_production_path pins the positive path.
  • Request-supplied access profile. Threat: an Evidence deployment with a substituted adapter names any profile on the exported lookup route and learns whether the entity or profile exists, or reads through a profile the token does not satisfy or that grants no lookup. Enforcement point: authorize_direct_route_base (route's compiled profile list, token scopes, purposes and row-boundary claims) and resolve_lookup_selector (lookup grant for the selector) in the BReg read API, both answering with the concealed not-found problem. Negative test: lookup_route_conceals_a_substituted_access_profile_selection.

Compatibility: the bregctl dev start --detach flag from the original branch is gone before it ever shipped; dev stop keeps container and data unless --remove is given. No released surface changes.

Follow-ups deferred from the review, none addressed here:

  • R1-3 behaviorRevision carries no engine or protocol version.
  • R1-5 the derived-view cardinality guard fires only on reads that reach the relation.
  • R1-6 the offline-fixture constructor exempts a connection-named source from the shared-resources requirement.
  • R1-7 traceability: one test mapped under sec-source-request-immutable does not assert it; the V1-I09 no-dedup clause is pinned only indirectly.
  • R2-4 five let _ = sites on cleanup and permission-sealing paths.
  • R2-5 the compiler bounds selector-profile combinations per question while the runtime bounds grants per profile (answered in the review thread: the runtime refuses the aggregate with the range named; a pre-render bound per generated profile is the follow-up).
  • R2-6 question.schema.json cannot express selector/profiles exclusivity (answered in the review thread: the derived schema describes shape only and the compiler and language server refuse the combination).
  • R2-7 source diff --target runs a not-yet-accepted export's adapter Rhai through fixtures.
  • R2-8 per-file read bound of 64 MiB before the aggregate check; write reads up to 512 MiB to validate the destination.
  • R2-9 a non-string role becomes an empty string (unreachable today).
  • R3-F8 six dependency edges added to registry-bregctl without a vetting record; p256 hand-rolls ES256.
  • R3-F9 connection URLs use localhost while the container publishes on 127.0.0.1.
  • R3-F10 three fixed ports in the live test.
  • R3-F14 dev stop in a never-started directory reports stopped.
  • R3-F15 starter seeds use registrationNumber while journeys use registration-number, alias unexplained.
  • R3-F17 the 120 s child and 45 s pg_isready deadlines are undocumented.
  • R3-F18 verify_outputs after a partial start is still untested.
  • R3-F19 executable() keeps a non-canonical PATH hit with no version recorded in state.
  • R3-F20 the pinned postgres:17.11 digest is not stated in DEV.md.
  • R4-8 "should contain" wording for deterministic output; R4-9 five "above/below" in-page references; R4-10 draft tutorials' cross-links are not checked by the built link check; R4-11 five steps lack failure-mode guidance.
  • New while fixing: both source definitions accept non-canonical loopback literals when authenticated; no fixture under products/evidence/fixtures/ instantiates sourceConnections; stale installed breg/mint binaries surface only as "package refused" because StartupError::PackageRefused discards its cause; bregctl init's example project ships no selector profile or lookup grant, so it cannot feed generate evidence-source unmodified; the SCRAM verifier alternative for the password redaction needs crypto not in bregctl's dependency closure; nothing pins ci-result's needs list; registry-mint links registry-evidence so Evidence changes still select breg-tutorial; no gates-inventory marker for the new composition job; an astro check ts(80007) warning on the starter generator's await; 12 pre-existing redocly security-defined warnings on the Evidence OpenAPI document; pre-existing shfmt hunks in check-evidence-tutorials.sh and test-postgres.sh.
  • From the review round: target new validates the runtime settings document for shape only, since evidencectl has no closed mirror of the runtime's RuntimeConfig and does not link the runtime crate (the runtime refuses unknown keys at startup with the field named); a published runtime settings contract or an evidencectl-local closed mirror is the follow-up. bregctl init's example code field declares no minLength, so adding a selector profile on it is refused by the exporter with the fix named; the template should declare minLength: 1. The registry-breg installer test names its fixture directory by process id and wall-clock nanoseconds, which macOS reports at microsecond resolution, so two tests building Linux fixtures in the same microsecond share a directory and the first to finish removes the other's assets (reproduced 5 times in 30 runs of the two tests together, 0 in 30 alone, 0 in 20 serial runs; file unchanged on this branch). interrupted_replacement_and_baseline_advance_restore_the_complete_prior_state (evidencectl source import, added by this branch) failed its final ProjectLock::acquire once in a whole-crate run and not in 50 reruns; the assertion is is_ok(), so the error was not captured, and the next step is to make it report the error and rerun under contention. From the third round: the BReg exporter maps a UUID or reference field to a 36-byte bounded string selector and a vocabulary code to a bounded string, since Evidence's selector contract has no UUID lexical type and controlled-code needs a codelist artifact the export does not carry; a same-length value outside the domain passes Evidence's selector validation and BReg refuses it with the audited request.invalid problem before any read (the decision queued for the maintainers: refuse these field types as not losslessly representable, or carry a codelist artifact for vocabularies and document the bounded-string limit for UUID and reference). The sourceConnections.*.baseUrl patterns mirror main's sources.*.baseUrl patterns, which admit user information and an out-of-range loopback port that the runtime refuses at startup; the next step tightens both branches together, regenerates the JSON and extends source_connections_are_constrained_alike_by_the_bundle_schema_and_the_runtime with those denied shapes. From the fourth round: a profiled --subject role@profile:field=value on evidence request prepare is parsed by the progressive selector rule (true, false and integer literals become booleans and integers, a double-quoted literal stays a string), so a string selector field whose value looks like an integer is refused by the runtime; the unprofiled role:field=value form keeps its value lexical. The queued shape parses the value by the declared field type when the selector is compiled, which threads the field types through the persisted CompiledSelector and the dev state; until then the double-quoted form is the escape. From the fifth round: a UUID or reference field exported as an output fact is a 36-character bounded string, since Evidence's Version 1 fact schema subset has no pattern and only the date and date-time string formats (the same decision as the selector item, now covering facts; a uuid format in the Evidence subset is the queued option). bregctl dev start resolves Docker from PATH for the rehearsal database and dev stop has no --docker-bin, while database and stop_database use the resolved flag. The evidencectl workflow reference still says build rejects local assurance inputs; on this branch build compiles an assuranceProfile: local target under the local checks and the bundle records the profile, so the build row and paragraph need rewriting. Client tokens are minted before the rehearsal, activation, BReg start and readiness with a 300 s lifetime, so a first start that exhausts its deadlines reaches seeding with expired tokens; minting after BReg readiness is the queued fix.

DCO

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

Bundles name the connections a source may use so admission, single-flight
and credentials stay scoped per connection, and questions declare selector
profiles so a governed BReg lookup composes without a caller value becoming
Evidence authority. The extract/3 adapter ABI, the source contract and the
invariant matrix change together; the authoring model and the language
server index accept profiles.

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

`source import|diff|update|detach` accepts a bregctl export, compares it
three ways against the previously imported revision and local edits, and
replaces files atomically under a project lock with a rollback journal.
`target new|explain` scaffolds and explains deployment targets; builds prune
unused source alternatives and verify target-owned connection slots.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
`evidence_source` turns a reviewed lookup grant into Evidence source,
selector, schema and adapter files with a behaviour fingerprint, refusing
profiles that do not grant lookup and selector types Evidence cannot
represent exactly. Record reads join only the derived relations a request
reaches, pinned by a test-only query-plan probe and dependency coverage.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
`bregctl dev start|stop` runs a digest-pinned PostgreSQL container and a
loopback registry with private state under `.breg/dev`, role separation
over TLS and a control socket; `bregctl generate evidence-source` wraps the
BReg exporter. Six workspace dependency edges back the supervisor.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Two draft tutorials cover composing and deploying an Evidence project from
a BReg export; generated CLI reference pages, configuration data and the
sidebar follow the new commands; a generator publishes the starter project
tarball; the breg tutorial job builds the Evidence toolset and verifies the
retained dev lifecycle and the composition.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
A named `sourceConnections` entry carried none of the conditional
constraints an inline `http-json-source` carries, so the published
Version 1 contract described bundles the runtime refuses to load: an
unauthenticated connection at an `https://` origin, an unauthenticated
loopback connection also naming a private-CA trust profile, and loose
loopback spellings such as a default, zero, out-of-range, or padded
port. Adopters read the contract before they meet the runtime, and a
contract that publishes a shape only the runtime denies teaches the
wrong lesson about where the unauthenticated exception ends.

Mirror the `http-json-source` conditional onto `source-connection` so
both documents describe the same narrow exception, and say in the two
affected descriptions what that exception costs.

Review notes:

- Threat: an operator follows the published contract, points an
  unauthenticated source connection at a public origin or pairs one
  with a private-CA trust profile, and believes the contract sanctions
  it. Two documents disagreeing is also how a later relaxation of one
  side passes review unnoticed.
- Enforcement point: `SourceConnectionConfig::validate` and
  `validate_local_unauthenticated_source_origin` in
  `crates/registry-evidence/src/config.rs` remain the enforcement point
  for a loaded bundle. The schema now refuses the same documents ahead
  of it, for every tool that validates against the contract alone.
- Focused negative test:
  `source_connections_are_constrained_alike_by_the_bundle_schema_and_the_runtime`
  in `crates/registry-evidence/src/config.rs` asserts that the runtime
  parser and `bundle_contract_validator()` both refuse each of the seven
  denied connection shapes, and that both accept an authenticated
  connection and the local loopback exception.

The generated Evidence configuration reference data is regenerated with
the owning generator so it carries the narrowed connection rules.

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

Matrix rows V1-I08 and V1-I11 and the adapter reference described the
returned-record identity check as if the runtime performed it. It does
not. Rust enforces the `extract` entry-point arity, the refusal to pair
`extract/3` with `batch`, the fresh authorized selector subset handed to
the script, and the fixed FactSet validation of what comes back, and it
inspects nothing inside the script. The identity comparison lives in the
extraction adapter, which a source exporter generates for every
alternative it exports and which a hand-written adapter must carry
itself.

Say that plainly in both rows and in the reference, and state the
obligation an author writing `extract/3` by hand takes on. Add a
negative test that runs an exporter-shaped adapter against source
responses whose returned identity differs from the selected profile by
value and by scalar type, so the enforcement point the matrix names has
an executable proof, and map it from both rows. Headings in
`ADAPTER-API.md` are unchanged because the documentation site pins
anchors into them.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
CI denies two nonminimal_bool clippy warnings in target.rs's hand-rolled
settings validator (lines 201 and 223). That validator also accepted any
nonempty assuranceProfile and any unknown governance field, while every
consumer (target explain, build --target, fixtures run --target) opens
the same governance.yaml through the closed TargetGovernance type, which
denies unknown fields and restricts assuranceProfile to local, production,
or evidence-grade. Because a target directory is create-only, a settings
file that `target new` accepted but a consumer later refused left behind
a directory with no in-place repair: the operator had to remove it and
start over.

Fix `target new` to validate governance by deserializing through the same
build::TargetGovernance type the consumers use (bumped to pub(crate) for
this), before creating anything, so it refuses exactly what consumers
refuse and a refusal leaves nothing on disk. This also removes the two
hand-rolled boolean checks the clippy lint was flagging; the one
remaining nonminimal_bool site (runtime.bundleDirectory, which has no
closed consumer type to delegate to) is fixed in place with is_none_or.

Security invariant review:
- threat: an operator-supplied settings file that target new's validator
  accepts but every reader of the same file (target explain, build,
  fixtures run) refuses, producing a create-only directory that cannot
  be repaired in place;
- Rust enforcement point: target::validate_settings_documents in
  target.rs, now delegating governance validation to
  build::TargetGovernance::into_bundle, the same closed type
  build::read_target_documents deserializes governance.yaml with;
- focused negative tests: target_new_rejects_unknown_governance_field and
  target_new_rejects_assurance_profile_outside_enum in target.rs assert
  new(..) is refused and the target directory is never created;
  target_new_accepts_a_valid_settings_file pins that a settings file
  matching the closed shape is still accepted.

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

mod.rs and files.rs enforce the SOURCE-EXPORT.md bounds on untrusted
export material (manifest and per-artifact size, artifact count,
aggregate size, provenance shape, manifest-is-a-mapping, exactly one
sources/<id>.yaml, and lowercase bounded artifact stems), but none of
the existing import tests exercised any of these refusals. Add one
focused test per bound:

- import_refuses_manifest_over_one_mebibyte
- import_refuses_more_than_256_artifacts
- import_refuses_artifact_over_one_mebibyte
- import_refuses_export_over_sixteen_mebibytes_aggregate
- import_refuses_provenance_outside_its_bounds
- import_refuses_yaml_artifact_that_is_not_a_mapping
- import_refuses_export_without_exactly_one_sources_artifact
- import_refuses_artifact_stem_outside_its_bounds

Each asserts the exact refusal message load_export emits, not a
paraphrase.

Also add deleting_obsolete_artifact_with_a_local_edit_requires_an_explicit_resolution:
the existing test for this conflict path
(customization_conflicts_finish_with_keep_adopt_and_resolved_file,
deleting_obsolete_artifacts_preserves_customization_and_authored_references)
always supplies a resolution for the edited, upstream-obsolete artifact,
so the unresolved-conflict refusal in Candidate::validate was never
exercised. The new test edits a file that the next export drops, gives
no resolution, and asserts source update refuses with its documented
message, the edited file is untouched, and no journal or partial
state replacement remains.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
The deployment tutorial told the reader to run `evidence bundle-check`, a
hidden internal seam that carries no compatibility promise, and described
output it does not produce. The public `evidence check` reads the candidate
through its runtime file, compiles the bundle and validates the mounted secret
material exactly as startup does.

That command resolves the runtime file's absolute bundleDirectory and secret
root, which this page sets to the final deployment location, so the step now
states the precondition instead of implying any build host can run it.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
The page carried three warnings as ordinary sentences a reader skims past:
private Transit key custody, development material reaching the operated host,
and a migration that constrains reversal. Each one loses a secret or forecloses
a return to the previous state, which is what the style guide asks a caution or
danger block to mark.

Each block now also says what the loss costs, since the sentence it replaced
gave the instruction without the consequence.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
The bregctl development supervisor always detaches, and the flag is being
removed from the binary. Both `bregctl dev` and `bregctl dev start` in this
page carried it. No sentence explained the flag, so the surrounding prose
already describes the behavior that remains.

The evidencectl invocations keep their own flag: that is a separate binary
with its own argument.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
`npm run generate` rebuilds docs/site/public/examples/breg-evidence-starter.tar.gz
on every run, so the tracked copy was a build artifact that reappeared in
`git status` after any generate and invited a hand-edited commit of generated
bytes. The generator and its unit test stay; only the output leaves the index.

The ignore entry names that one path, so the reviewed
public/examples/evidence/opencrvs-events-search.openapi.yaml fixture beside it
stays tracked. The archive path also leaves the tutorial gate's CI input list
with the gate scope revert.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
products/breg/evidence/tests/verify-composition.py drives bregctl, evidencectl
and the Evidence runtime over the reviewed teaching inputs and starts no
container, so it does not need the Docker-backed tutorial replay it was riding
in. Running it there also widened the replay to build and route two Evidence
crates the tutorial never executes, which selected the whole Docker job for
changes it cannot prove.

The proof now has a non-Docker ubuntu-24.04 job that builds only the three
binaries it passes on the command line, and a breg_evidence_composition
classifier flag that selects it from registry-breg, registry-bregctl,
registry-evidence, registry-evidencectl and their reverse dependencies, or from
products/breg/evidence/**. The flag is wired through the changes-job outputs and
into the ci-result aggregate, whose check is generic over its needs list.

breg-tutorial returns to its previous package set and inputs and keeps the
dev_lifecycle test, which owns a PostgreSQL container of its own.

Every step this commit adds or touches sets shell: bash with set -euo pipefail,
and the PyYAML pin follows the one the repository already used, 6.0.2.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
The root AGENTS.md routes registry-breg, bregctl and BREG configuration and
PostgreSQL behavior to products/breg/AGENTS.md, which did not exist, so a
contributor following the routing table landed nowhere and the product's own
gates had to be rediscovered from CI each time.

The entry states the boundary, including that the Evidence export is a governed
lookup export and never Evidence authority, points at the contracts, the
configuration references, DEV.md and EVIDENCE.md, and lists the checks per
change kind with the commands and environment the CI jobs use. It closes with
the generated outputs and their owning generators and the security review-note
expectation from CONTRIBUTING.md.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
The README described the scripted quickstart and the pilot lifecycle but never
linked DEV.md, so the retained `bregctl dev` loop, the loop the tutorials and
the composition proof use, was reachable only by listing the directory.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Three entries added to the two exclusion lists ended a column short of the
comment alignment shfmt keeps in those arrays, so `shfmt -d` reported them.
Only those three lines change; the pre-existing diffs elsewhere in
check-evidence-tutorials.sh are left as they are on main.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
The pinned surface list did not include the dev command, so the explicit
surface test failed as soon as dev shipped. Record dev in the pin at its
enum position instead of relaxing the assertion, keeping the list the
authority on which commands are public.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
dev start parsed --detach and never read it: the supervisor detaches
unconditionally, so the flag promised a choice the command does not offer.
Remove it from the CLI, from the documented invocations and from the
installed-binary proof, and say in the guide what detaching actually means.

The generated CLI reference pages and docs data are regenerated with the
owning generator so they stop documenting the removed flag.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
dev printed pretty JSON on success whatever --format asked for, so the
default human output of every other command became machine output as soon
as a reader ran dev. Render the report the way sibling commands do: keyed
lines for a human, the same pretty JSON document for --format json.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Threat: dev start read any doctor failure as "the package was never
activated" and answered it with apply --initial. A refused package, a
rejected audit profile or an unreachable database therefore reached the
activation authority with a hidden diagnostic, and the operator saw an
apply failure instead of the prerequisite that actually broke.

Enforcement point: activation() in the dev supervisor, a pure classifier
over the doctor report that runs before apply is spawned. Only a refusal
whose diagnostics are all startup.database.unready means not activated;
every other refusal aborts the start carrying doctor's own code, path and
message, and a report that is unparseable or names no diagnostic aborts
too. startup.database.unready remains broader than "no package activated"
because BReg reports several unready conditions under that one code, but
it is the only signal doctor exposes and it no longer admits unrelated
failures.

Focused negative test: only_an_unready_database_classifies_a_doctor_refusal
_as_not_activated in dev/tests.rs covers activated, not activated, an
unrelated refusal whose message must reach the error, a malformed report
and an empty diagnostic list.

command() now wraps a new output() that returns the exit outcome with the
captured stdout, so the classifier reads the report doctor printed.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Threat: dev interpolates the migration and runtime role passwords into
CREATE ROLE statements sent to psql, while every owned prerequisite pumps
its child's stderr straight into a retained 0600 log and copies a failed
child's stdout into a second log. A refused statement makes the database
client echo what it could not run, so the plaintext password reached
persistent local files, the returned bytes and any message built from
them. The client reports a truncated window around the error position, so
a whole-value match is not enough.

Enforcement point: the child boundary in dev's command runner. Standard
input now carries the secret substring it contains, and for such a call
the runner captures the child's stderr instead of streaming it, redacts
both that capture and the child's stdout, and only then writes the log,
the failure report and the returned bytes. Calls that carry no secret
keep streaming as before. redact() hides every run of at least eight
bytes that also occurs in the secret, which covers truncated echoes as
well as whole occurrences, and leaves the surrounding diagnostics
readable. A client-side SCRAM-SHA-256 verifier would avoid sending the
plaintext at all, but neither an HMAC nor a PBKDF2 implementation is
available to this crate without adding a dependency.

Focused negative test: role_password_bytes_cannot_reach_diagnostics_or
_errors in dev/tests.rs drives the real runner with a child that echoes
its statement whole and as a 30 byte window, then asserts no log file and
no error text contains the password or that window, while both logs still
carry their surrounding text. redaction_hides_whole_and_truncated_secret
_runs pins the pure function, including that unrelated text is untouched.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
The local lifecycle created a Docker container with an anonymous volume and
never removed either, so every project that was started once left storage
behind with no name a person could recognize or reclaim.

Bind the database to a named volume derived from the ownership identifier and
add `dev stop --remove`, which stops as before, then removes the owned
container with its volumes and the named volume itself, tolerating whatever an
earlier reclamation already took. Reclamation clears the container facts from
the private state so the next start builds an empty database from the retained
keys, credentials, ports, clients and package. Plain `dev stop` still keeps
everything.

The live lifecycle test reclaims from `Drop`, so a panicking assertion cannot
leak the container or the volume it created either.

The generated CLI reference pages and docs data are regenerated with the
owning generator so they document the flag.

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

The live lifecycle test started a registry and exercised it through hand
written requests, so nothing proved that `generate evidence-source` describes
the registry a person actually runs. A wrong route, access profile, identity
field or unresolved-problem shape would have shipped unnoticed.

Author one exact selector and one narrow request-origin lookup profile in the
test project, export it, then drive the exported route and access profile
twice: a seeded identity echoes its own value with the scalar type the response
schema declares, and an unknown identity answers exactly the status, type and
code the export names for an unresolved lookup.

Both assertions were confirmed to bite by mutating the expected seed value and
by looking up the seeded identity in place of the absent one. The added work is
two loopback requests and one offline export.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
The Evidence source exporter refuses five input shapes it must never
export: change-request lifecycle entities, a profile that grants no
lookup, a lookup grant without a compiled lookup route, selector names
that push the Evidence profile name past 64 bytes, and a composite
selector whose union crosses the 8192-byte selector bound while each
field stays inside its own. Each refusal now has a test asserting the
diagnostic code and the exact message an author sees.

The extraction adapter the exporter emits is the only place the
returned-record identity is compared with the selector. Pin the emitted
comparison verbatim for a single-field and a multi-field selector, so a
change to that script cannot land unnoticed.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
The exported Evidence prepare script carries the access profile as a
query parameter, so a replaced adapter can name any profile on the
lookup route. Selection is a request, never an authority: BReg resolves
the named profile against the route's compiled profile list, the
token's scopes, purposes and row-boundary claims, and the profile's
lookup grants before any read runs.

Threat: an Evidence deployment with a substituted adapter names a
profile the token does not satisfy, a profile that grants no lookup, or
a profile that does not exist, and learns from the answer whether the
entity or profile exists or reads through it.

Enforcement point: authorize_direct_route_base in the read API refuses a
profile outside the route's compiled list or unsatisfied by the token,
and resolve_lookup_selector refuses a profile without a lookup grant for
the requested selector; every refusal is the concealed not-found
problem.

Negative test: lookup_route_conceals_a_substituted_access_profile_selection
sends the exporter's lookup request with each of the three profile
kinds, asserts the identical concealed problem the unknown-entity route
returns, that the record service is never called, and that the profile
lacking lookup still serves the read it is granted.

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

chatgpt-codex-connector Bot commented Sep 6, 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-06T14:08:20.495051Z 6827339 New commits
ℹ️ 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: 16a230a5d6

ℹ️ 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 thread crates/registry-evidencectl/src/target.rs
Comment thread crates/registry-evidence-authoring/src/model.rs
Comment thread crates/registry-bregctl/src/dev/mod.rs Outdated
Comment thread products/evidence/contracts/bundle.schema.yaml
Comment thread crates/registry-breg/src/evidence_source.rs
Comment thread crates/registry-evidencectl/src/authoring.rs
…assurance in the schema

The published bundle contract and the runtime must refuse the same
documents: an unused `sourceConnections` entry using
`authentication.kind: none` at a canonical loopback origin previously
passed schema validation under `production` and `evidence-grade`
assurance even though `SourceConnectionConfig::validate` already
refuses it outside local assurance. Extend the assurance-profile
conditional to constrain `sourceConnections.*.authentication` the same
way it already constrains `sources.*.authentication`, pinned by
`source_connections_are_constrained_alike_by_the_bundle_schema_and_the_runtime`.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
The catalog tests pin the artifact kinds `bregctl generate` accepts and
the authoring inputs `evidencectl new` requires exactly one of. Both
shapes changed with the Evidence source exporter and the offline
starter, and the regenerated reference data already carries them; the
pins now hold the published shape so the developer-tools test job
proves it.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
The role password test binds a fixed hex value to a local named
`password`, which the secret scan reads as a leaked credential and
refuses. The value is a canary the test plants to prove redaction, so
the local now says so; the assertions and the redaction path are
unchanged.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
`dev stop --remove` is the documented recovery for a retained database
container already taken by hand: `reclaim` verifies ownership of whatever
Docker still lists and forgets the rest. But `stop` inspected the container
unconditionally before reaching `reclaim`, and that inspection refuses an
absent listing whenever the journal still names a container. Remove mode
never got past that refusal, so a container removed by hand could not be
reclaimed even though its named volume was still there to clean up.

Skip the inspection (and the stop it guards) in remove mode when nothing is
listed under the container's name, and let `reclaim` handle it as documented.
Plain `dev stop` without `--remove` keeps refusing a missing retained
container exactly as before, since that refusal protects retained data.

Pinned by installed_dev_preserves_edits_and_recovers_failed_start_without_reseeding
in crates/registry-bregctl/tests/dev_lifecycle.rs, extended to remove a
recreated container by hand and assert both that plain `dev stop` still
refuses it and that `dev stop --remove` reclaims the remaining volume and
resets the retained container identity.

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

BReg accepts an empty lookup value on a string field whose `minLength` is
zero, whether declared or defaulted, but the Evidence source exporter always
emitted `minimumBytes: 1` for a string selector. A caller relying on the
exported selector schema could never express the empty value BReg itself
still accepts, silently narrowing the field's real domain the same way the
existing unrestricted `int64` refusal already guards against.

Refuse this shape in `selector_schema` with the same style of message as the
int64 case, naming the fix: declare `minLength` of at least 1 on the field,
or use a custom adapter.

Pinned by refuses_a_string_selector_field_that_accepts_the_empty_value in
crates/registry-breg/src/evidence_source/tests.rs, both for the schema
function directly and through a compiled project whose `code` field omits
`minLength`.

The live dev lifecycle test authors its own selector field at generate time
via bregctl's init template, which does not declare `minLength` on `code`
(a separate, intentionally unchanged follow-up); update the test's authored
copy to declare `minLength: 1` so it keeps exporting under the new refusal.
Documented the requirement in products/breg/EVIDENCE.md alongside the int64
refusal it mirrors.

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

@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: b2b4f0b1c4

ℹ️ 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 thread crates/registry-evidencectl/src/authoring.rs Outdated
… checks

An evidence-grade target passes governance parsing, which accepts local,
production and evidence-grade, and then fell into the wildcard refusal in
compile_target_project, which named only local and production. Every
evidence-grade target was refused before compilation by build, target
explanation and target-backed fixture validation, a regression from the
unconditional production checks the function ran before the local profile
was added.

Treat evidence-grade as production: validate the deployment inputs under
the production rules, reject disposable local identifiers and hold every
source to the production transport conditions. The refusal for any other
value names all three accepted profiles.

Security review. Threat: an evidence-grade deployment compiled under the
local rules would accept unauthenticated or plaintext sources. Enforcement
point: the assurance match in compile_target_project selecting the
production arm. Negative test:
evidence_grade_target_is_held_to_the_production_checks pins that an
unauthenticated source is refused under evidence-grade with the same
message as under production; evidence_grade_target_compiles_through_the_production_path
pins the positive path and an_unknown_assurance_profile_is_refused_by_name
pins the refusal naming all three profiles.

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

@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: e63373dffa

ℹ️ 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 thread crates/registry-breg/src/evidence_source.rs
Comment thread crates/registry-bregctl/src/dev/mod.rs Outdated
Comment thread products/evidence/contracts/bundle.schema.yaml
Comment thread crates/registry-evidencectl/src/scaffold.rs Outdated
The dev supervisor answered its owner-only control socket from a single
read into a fixed buffer and compared the bytes it happened to receive
against the two known commands. A Unix stream socket may deliver a
client's write in parts, in which case the partial command counted as
unknown, the connection was dropped, and `dev stop` reported that the
supervisor had not stopped.

The supervisor now reads until the newline, the end of the stream, or
the 16-byte bound, whichever comes first, and only then compares the
command. The per-read timeout is unchanged, so a client that stalls
mid-command can hold the accept loop for at most sixteen seconds on a
socket only the owning user can open.

Pinned by `a_control_command_split_across_writes_is_read_whole` (both
commands split across two writes with a pause between them, both
failing before the change) and
`a_control_command_stops_at_the_newline_or_the_size_bound` (no read
past the newline, the 16-byte cap, end of stream without a newline).
No trust boundary changes: the socket's ownership and mode checks and
the command set are the same.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
`evidencectl new --starter` always told the reader to run `fixtures run`
next. The reviewed BReg starter ships questions, derivations, fixtures
and target settings but no source on purpose: its README asks for a
`bregctl generate evidence-source` export to be imported first, so the
printed next command failed straight after creation.

When the copied starter carries no file under `sources/`, the next step
now points at the project's README and says a source has to be imported
or authored before `fixtures run`; a starter that ships a source keeps
the previous line.

Pinned by `a_starter_without_sources_defers_the_fixture_run_to_its_readme`
(a starter with a README, questions, derivations, schemas and fixtures
but no source; failing before the change) alongside the unchanged
`local_starter_creates_offline_project_and_target_settings_example` for
a starter that ships one.

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

@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: e9b8516307

ℹ️ 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 thread crates/registry-evidencectl/src/request.rs
Comment thread crates/registry-evidencectl/src/target.rs Outdated
`target explain` compiled the project with the deployment target before it
read the target documents and listed the public key files the governance
references. The compile copies those keys from the target and fails on a
missing one, so a deleted or never-copied active or published key ended
the command inside the compile error and the `missingPublicKeyFiles`
report, which exists for that case, was never printed.

The command now parses governance and runtime, inventories the expected
and missing public key files and collects the secret references first.
When a key is missing it prints the report with no fixture paths and exits
with a failure; otherwise it compiles and prints the full report as before.

Pinned by `a_missing_target_public_key_is_reported_by_explain_before_compilation`
(the target key removed; failing before the change inside the compile with
no report) and `explain_reports_the_fixture_paths_when_every_public_key_is_present`.
No trust boundary changes: the report carries paths and logical secret
references only, as before.

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

@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: 682733901b

ℹ️ 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 thread crates/registry-breg/src/evidence_source.rs
Comment thread crates/registry-bregctl/src/dev/mod.rs
Comment thread crates/registry-evidencectl/src/authoring.rs
Comment thread crates/registry-bregctl/src/dev/mod.rs
@jeremi
jeremi merged commit 6a59f1d into main Sep 6, 2026
92 of 95 checks passed
@jeremi
jeremi deleted the dx/evidence-breg-native-split branch September 6, 2026 14:42
This was referenced Sep 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant