feat(breg): accept an optional reason on approve_request and apply_request - #1112
Merged
Merged
Conversation
A favourable decision had nowhere to put remarks: approve_request and apply_request refused a reason, so a reviewer could only record text on rejection or return. Approve and apply bodies now accept the same optional bounded reason reject and request-revision already accept. The approve reason is recorded on the decision entry exactly as a rejection's is. The apply reason is recorded on the application receipt, since apply is an execution step with no decision entry: it is disclosed in data.request.application under the same readableRequestFields reason rule, with reasonPresent always present, and request-detail erasure removes the text while keeping presence. The retention report gains an applicationReasons counter beside decisionReasons. The reason joins the canonical idempotency binding, so replay under one key must repeat the same text. Lifecycle webhook events carry reasonPresent and the reason on approve and apply transitions, and an approve that applies in the same transaction keeps its reason on the terminal applied event. BRegLifecycleAction::with_reason accepts approve and apply, recovery replays a reasoned body verbatim, and the Casework BReg adapter forwards reasons instead of answering ReasonUnsupported; the problem code remains registered for sources that do refuse one. The strict client metadata gate now expects the reason property on all four action schemas the server publishes, so clients and servers of one release stay in lockstep and an older client refuses newer metadata rather than guessing. Closes #1109. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
Allow a reason on an approve that leaves the request submitted. The request
lifecycle outbox projection guard accepted an approve reason only on
("approve", "approved"), so a reasoned approve at a non-final stage, or one
that did not complete its stage quorum, failed the whole action with HTTP 400
request.invalid.
Carry the applier's reason presence into erased terminal request metadata.
After operator_erase on an applied request,
data.request.application.reasonPresent came back null instead of true, so
erasure dropped the presence flag the retention rule preserves.
Cover both through the real PostgreSQL HTTP router, including the first
coverage for RequestDetailErasure::application_reasons.
Correct the decisions CHECK-constraint test, which still asserted the database
refuses an approve carrying a reason after this branch widened the constraint.
Update the change-request examples, the events-and-webhooks reference, and the
API action table, which still named only reject and request_revision.
Security review notes (data minimization): the applier reason keeps the same
disclosure gate as a decision reason. may_disclose_decision_reasons still
governs the text, reasonPresent is disclosed unconditionally, and erasure nulls
the retained text while leaving the flag set. The idempotency binding hashes
the reason into canonical_request_digest, so no reason text is stored in
cleartext there.
Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #1109.
What changes
A favourable decision had nowhere to put remarks:
approve_requestandapply_requestrefused areason, so remarks could only be captured on rejection or return. Both actions now accept the same optional, bounded reason (≤ 4096 Unicode characters, NUL refused, empty string allowed) thatreject_requestandrequest_revisionalready accept. A body withoutreasonstays valid, so nothing changes for callers that send none.registry-breg):RequestActionBody::Approve/Applycarryreason: Option<String>, parsed byparse_reasoned_proposal_action. The approve reason is recorded on the decision entry exactly as a rejection's is. The apply reason is recorded on the application receipt (ApplicationReceiptgainsreason/reason_present,registry_request_applicationsgains the columns) — apply is an execution step with no decision entry, and an application only ever exists for the current proposal version, sodata.request.applicationis the complete read surface for it.data.request.applicationcarriesreasonPresentalways andreasonunder the samereadableRequestFields: [reason]rule; anonymous profiles never see text. Request-detail erasure removes apply-reason text while keeping presence, and the retention report gainsapplicationReasonsbesidedecisionReasons.reasonPresent/reasonon approve and apply transitions — including an approve whose proposal applies in the same transaction (emitted on the terminalappliedstate).registry-breg-client+ Node/Python bindings):with_reasonaccepts approve and apply; recovery (recovery_action_body) replays a reasoned body verbatim; the retained/erased application decoders exposereason_present/reason. The byte-exact metadata gate now expects thereasonproperty on all four action schemas the server publishes — same-release clients and servers stay in lockstep, and an older client refuses newer metadata (fail closed) rather than guessing.SourceAdapterError::ReasonUnsupported. The problem code stays registered for source adapters that do refuse reasons; its published row andproducts/casework/README.mdno longer claim BReg refuses it.check-generated.shmatches all nine baselines.breg-api.mdxaction section, the Casework decision explanation, thereview-breg-changes-in-caseworktutorial (records a remark on the favourable decision), and the client READMEs.Decisions on the issue's open questions
review.stages[].requireReason? Not added — optional covers the stated institutional need; required can follow a concrete ask.reasonon the wire; the docs describe it as the approver's/applier's recorded remark, not only a refusal explanation.Security-sensitive notes
This touches disclosure and data minimization: reason text on favourable decisions is guarded by the existing
readableRequestFieldsprofile rule (presence always, text only when permitted, never for anonymous profiles) and erased byoperator_eraseretention with presence retained — verified by new assertions for the hidden-profile and erasure paths. The DB CHECK constraints enforce the bounds and the reason/presence pairing in both tables. No new audit surface: the reason was already covered for reject/return; approve/apply reuse it.Verification
cargo fmt --check;cargo clippy --all-targets -D warningsfor the changed crates; full-workspacecargo check --all-targets.cargo testforregistry-breg,registry-breg-client,registry-casework-breg, plusregistry-casework*,registry-bregctl— all pass.postgres_change_requests(including reworked reviewer-reason journeys: reasoned approve decision read-back, reasoned apply with disclosure/hidden/erasure checks and idempotent replay, event payloads),postgres_request_events,postgres_request_read_retention,postgres_request_upgrade_retention,postgres_request_receipts,postgres_mutation,postgres_webhook_outbox— all pass. The DB CHECK-constraint change is exercised by these suites (an in-place upgrade re-adds the constraint at startup).npm ci && npm run build:debug && npm test && npm run check:types(33 pass). Python binding: extension-module build + 27 unittests pass.products/breg/scripts/check-contracts.sh,check-client-contract.sh,check-generated.sh(9/9 baselines match),products/casework/scripts/check-checkpoint.sh.npm run generate,npm test(601 pass),npm run check.