ADR-039: The gate's signal is a named authority delta; thresholds are data, not code
Status: Proposed. This is the one substrate decision that can silently pre-empt Sergey's use-case selection, which is why it must be locked before code.
Context
Three candidate signals exist and only one is safe. The rehearsal recommendation word
(LOW_CONCERN / REVIEW / RECOMMEND_DENY) is decided partly by a connector-side substring classifier —
severity would depend on whether the customer names their buckets well. The verdict engine's Approve is
a ~12-way conjunction across seven graded signals and four clocks
(src/services/verdict-engine.ts:888-927) that returns Unknown on real data almost always. The third is
the named authority delta the rehearsal already computes: what appears in production that was not in
the validated non-production cone, keyed on resource_key ?? resource_id
(src/rehearsal/engine.ts:1299-1315).
All four candidate use cases — the two on the recommended shortlist (agent promotion;
authority-adding deployment) and the two recommended against, with reasons, which remain Sergey's call at
the working session — sit on identical substrate: the same route GET /api/v1/rehearsal/deployment
(src/api/routes/rehearsal.ts:340), the same UI page, the same unsealed RehearsalDeploymentVerdict
shape. So engineering can start now at zero rework risk only if the thresholds are not hardcoded to
one candidate.
Decision
- The gate's signal is the named authority delta. Approve / Constrain / Reject / Unknown remain the presentation vocabulary (they are already the product's vocabulary, #1776/#1778), produced by a small deterministic mapper over the delta.
- The mapper's thresholds live in a per-tenant, per-use-case policy table stored as data. No thresholds in code. A new use case is a row, not a deploy.
- The declared-delta artefact (an input the "authority-adding deployment" candidate would want from the customer's pipeline) is an optional payload enrichment, never a required field. Requiring it would pre-empt the agent-promotion candidate, which needs no declaration.
- The mapper is pure and deterministic — no ML, no scoring, no heuristics — and its inputs, the
policy row id,
policy_versionandmapper_versionare stamped into the sealed record and into theApprovalPayload, insideapproval_content_hash(ADR-036 Decision 2). This is what closes the too-narrow half of the hash defect: because the same rehearsal may yield a different outcome under a different policy row, a policy or mapper change between the status report and the human's confirmation requires a new confirmation rather than passing silently (ADR-034 item 3). - What the mapper produces is a computed verdict. What a human may then do with it is a separate question, and it is open — see below. Do not read Decision 1 as also deciding the disposition set.
Open for the working session — do NOT settle these in this document
Two product decisions were being decided implicitly by the substrate. Both are surfaced here, unfilled, so the working session decides them explicitly.
(A) Which human dispositions are reachable from which computed verdict. The demo's Ending B has the
approver release a run on which a customer-data destination newly appeared — while this family elsewhere
states that Constrain, Reject and Unknown stay held with no release affordance. Those two cannot
both be true. This matrix is the canonical home of that decision — it lives here, and every other
document in this family references it by name rather than reproducing it or settling it by omission.
Every cell is deliberately empty; the working session fills them in.
| Computed verdict ↓ / Human disposition → | Release | Request changes | Refuse | Accept exception |
|---|---|---|---|---|
Approve | ||||
Constrain | ||||
Reject | ||||
Unknown |
Constraints the session inherits and cannot vote away:
- The brief's human-in-the-loop rule stands: nothing auto-releases at a holding tier, in any cell.
- If any exception cell is opened, it is an explicit
accept_exceptionaction — never a plain release. It carries stronger authorization than a normal confirmation, a written reason, an explicit scope (this deployment only, or this delta for a named window), and an expiry. It is its own disposition on thegate_decisionslifecycle and its own sealed row, so "we released anyway" is never indistinguishable from "the verdict was clean". - Whatever the session picks, the demo narrative must match it. Today it does not.
(B) What is actually being gated — authority creation or authority activation. Wherever an assumable production role already exists by the time the cutover is gated, the gate holds activation, and the production authority existed before any human confirmed. Three options; the recommendation is ours, the decision is the session's:
In our own lab the authority comes from neither pipeline phase. The promotion pipeline creates no identity and no grant — both of its deployment roles deny every IAM write — and the agent's authority-bearing production role is created by the estate Terraform in
sv0-demo-labs, ungated, long before a promotion runs. The question above still stands for a partner pipeline; what does not hold is attributing the creation to a pipeline phase.
| Option | What changes | Cost | What we may then claim |
|---|---|---|---|
| (i) Phase 1 creates a disabled / non-assumable draft identity; the gated step enables it | Partner-side pipeline change | Highest — needs the design partner's IaC to cooperate | "No production authority becomes usable before a human approved it" |
| (ii) The authority-bearing apply itself is gated, using a declared-state rehearsal over the planned change | Requires the declared-delta artefact from the pipeline — which Decision 3 keeps optional precisely so it cannot be made mandatory here | Medium, and it re-opens Decision 3 if made a requirement | "The authority-creating step is what we hold" |
| (iii) Narrow the claim to a cutover gate | Nothing technical; wording only | Lowest | "We hold the activation of authority, not its creation" — and we say exactly that, everywhere |
Recommendation: (iii) for the pilot, with (i) as the target state — it is the only option that is true on day one with no partner-side prerequisite, and the honest claim is still a real one. This is a recommendation, not a decision. Note that the existing consequence below (gate phase 2 of a two-phase promotion) is option (iii) in practice; it must not be treated as having already settled (B).
Both items go on the working-session agenda alongside the use-case selection.
Alternatives considered
- Gate on the recommendation word. Rejected: it inherits a connector-side substring classifier into an enforcement decision.
- Gate on the verdict engine's
Approve. Rejected as the trigger: it is the right integrity object and the wrong sensitivity — near-permanentUnknownmakes the gate a coin flip on evidence coverage rather than on the change being deployed. It remains the record, per ADR-036. - Hardcode the pilot thresholds and refactor later. Rejected — this is exactly the quiet pre-emption the brief forbids.
Consequences
- The working session can select any of the four candidates without changing a line of gate substrate. A different candidate can still change the demo estate, the connector refresh set and the narrative — that is last-mile work, not substrate rework.
- The disposition matrix (A) is substrate-shaped, unlike the use-case selection. Opening any exception
cell adds a disposition to the
gate_decisionslifecycle, a stronger authorization check on the confirmation route (ADR-036 Decision 5), and a scope/expiry field set — so it must be answered before the lifecycle is frozen, not after. It is not in the current estimate; treat that estimate as an undercount until (A) is answered. - Because the policy row and mapper version are inside
approval_content_hash, a policy edit is a re-confirmation event. That is intended, and it has an operational cost: editing a threshold table while holds are open invalidates every open hold's confirmation and re-presents them. Policy edits are therefore an announced operation with a runbook, not a settings tweak. - One partner-side precondition is exposed by this decision and must be stated in the use-case pack: the
promotion engine compares UAT-now against prod-now, so a single-phase pipeline that creates the
production twin inside the gated job examines the pre-deploy twin and the first promotion returns 422.
The fix is pipeline shape — gate phase 2 (alias publish / traffic cutover) of a two-phase promotion.
Our own prior research already records that the webhook fires before the job, "so a two-phase
staging→production promotion pattern is required"
(
docs/architecture/research/2026-02-27-pre-deployment-assurance-research.md§3.6 / Appendix A).