You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
docs(backlog): reconcile the two same-day #1317 amendments (BACKLOG #1317)
Isolated on purpose: this commit touches docs/BACKLOG.md and nothing else, so
the row merge can be read and merged as rows.
Row #1317 collected two amendments dated 2026-09-03 that were written blind to
each other. PR 760's re-measured all three library arms and left hvac as a
named residual gated on a CI change. This branch's installed the extra and
built the arm, which is that residual. Ordered 760's first, since it was
written and landed first, and marked what it supersedes in place rather than
deleting it -- the convention this row already uses a few paragraphs up.
Four claims in 760's amendment expired the same day and now say so: the
"hvac -- NOT BUILT" bullet, the trigger table's hvac row, the "TRIGGERS, not
guards" reading, and the residual in section 5. The two ODBC tests are still
triggers, and the text now says not to read the three as a set.
The banner reading looked like a contradiction and is not one. 760 measures the
strict positive allowlist as BUILT; this branch says it governs the operator
knob and does not reach an inherited default context. Build-state and reach are
different questions about one measurement. What is left is a scope question, no
builder may rule on it, and both amendments left the banner PARTIAL. It stays
PARTIAL.
Also disambiguates an earlier pointer that read "the 2026-09-03 amendment
below" while two now sit below it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: docs/BACKLOG.md
+95-8Lines changed: 95 additions & 8 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -5986,7 +5986,7 @@ complete result, not a stall.
5986
5986
5987
5987
## 1042. The `[vault]` key/secret/transit providers build a redirect-following HTTP client, so a diverted 3xx could carry `X-Vault-Token` off-path
5988
5988
5989
-
> ✅ **SHIPPED 2026-08-10 — all three `[vault]` clients now refuse redirects.** Value **4/10** · Difficulty **2/10** · _fill-in_. `allow_redirects=False` at both `hvac.Client` construction points (`store/keyprovider_vault.py` and `config/secretprovider_vault.py` `_build_client`); `store/crypto_transit.py` already reuses the first, so the Transit cipher inherits the policy rather than growing a second construction point. Verified against the library rather than assumed: hvac 2.4.0's `Client.__init__` takes `allow_redirects` (default `True`), stores it on the adapter, and the adapter passes `allow_redirects=self.allow_redirects` to `requests.Session.request` — recorded in the test module's docstring because CI never installs the `[vault]` extra and no test can reach it there. Four tests in `tests/test_vault_client_redirect_policy.py`: three watched RED (each reported the constructed kwargs as `dict_keys(['url', 'token'])`), and the fourth is a live positive control that the recording stand-in reports an absent policy rather than swallowing an unrecognised kwarg. The Transit client is driven end to end through `build_transit_cipher`, so a private client construction there would red rather than pass an identity check.
5989
+
> ✅ **SHIPPED 2026-08-10 — all three `[vault]` clients now refuse redirects.** Value **4/10** · Difficulty **2/10** · _fill-in_. `allow_redirects=False` at both `hvac.Client` construction points (`store/keyprovider_vault.py` and `config/secretprovider_vault.py` `_build_client`); `store/crypto_transit.py` already reuses the first, so the Transit cipher inherits the policy rather than growing a second construction point. Verified against the library rather than assumed: hvac 2.4.0's `Client.__init__` takes `allow_redirects` (default `True`), stores it on the adapter, and the adapter passes `allow_redirects=self.allow_redirects` to `requests.Session.request` — recorded in the test module's docstring because CI installed the `[vault]` extra nowhere and no test could reach it there. **That premise expired on 2026-09-03** ([#1317](#1317)): the `test` leg installs `[vault]` now, so a live assertion against hvac's real construction IS reachable in CI and a future revision of this policy should make one rather than inherit the docstring. Four tests in `tests/test_vault_client_redirect_policy.py`: three watched RED (each reported the constructed kwargs as `dict_keys(['url', 'token'])`), and the fourth is a live positive control that the recording stand-in reports an absent policy rather than swallowing an unrecognised kwarg. The Transit client is driven end to end through `build_transit_cipher`, so a private client construction there would red rather than pass an identity check.
5990
5990
5991
5991
**Cluster:** Egress / secret handling. **Priority:** P3. **Verdict:** build (small). **Severity:** conditional, not an exposure on the shipping config. The vault provider is behind an optional pip extra and off by default; when selected it points at operator-trusted infrastructure. On first deployment, an on-path 3xx (absent TLS integrity) or a spoofed Vault could divert the bearer token, while every default egress refuses redirects.
5992
5992
@@ -15800,8 +15800,8 @@ so a decoy call cannot satisfy it. Deleting the call from any one site reds that
15800
15800
for -- admitting only suites present on every current candidate list, rejecting anything unnamed -- is
15801
15801
not built. This records a call site gaining an assertion, not the allowlist changing shape.
15802
15802
15803
-
***THAT PARAGRAPH IS FALSE, AND IT WAS FALSE ON THE DAY IT WAS WRITTEN. Superseded by the 2026-09-03
15804
-
amendment below, which measures it.*** Left standing rather than deleted because it is a dated record
15803
+
***THAT PARAGRAPH IS FALSE, AND IT WAS FALSE ON THE DAY IT WAS WRITTEN. Superseded by the FIRST
15804
+
2026-09-03 amendment below, which measures it.*** Left standing rather than deleted because it is a dated record
15805
15805
of what a reader believed, and because the same reader's other claim on this row is sound. The
15806
15806
allowlist landed eight days EARLIER, on 2026-08-22 in `ae72f5828` (PR 519), and this row's own BUILT
15807
15807
paragraph says so a few paragraphs up. The banner is therefore not held open by the allowlist.
@@ -15850,7 +15850,10 @@ SAID SO.** Both rulings are re-measured here and both hold.
15850
15850
**The ADR measured `store/sqlserver.py` alone and generalised to "ODBC Driver 18".** The ODBC
15851
15851
*transport* -- the DATABASE destination and the ADR 0010 `db_lookup` hop, which is where message
15852
15852
content crosses -- was never named in it. It is measured now.
15853
-
- **`hvac` -- NOT BUILT, deliberately, and the ADR names its own trigger.** hvac delegates to
15853
+
- **`hvac` -- NOT BUILT, deliberately, and the ADR names its own trigger.** ***Superseded by the
15854
+
SECOND 2026-09-03 amendment below, which installed the extra and built it.*** Left standing because
15855
+
it is why the CI leg had to come first, and because its own trigger is what reported the change.
15856
+
hvac delegates to
15854
15857
requests, which delegates to urllib3, which builds and owns the context per connection, so the
15855
15858
engine holds no object -- the same shape as `ldap3`. Unlike `ldap3` the gap cannot be closed by
15856
15859
measurement: `urllib3` is absent from the interpreter and **no CI leg installs the `[vault]` extra**
@@ -15869,14 +15872,22 @@ control and each proved non-vacuous by mutation.
15869
15872
| `test_the_odbc_scope_out_premise_still_holds` | neither ODBC module reaches for an `ssl` context | appended an `ssl.create_default_context()` to `transports/database.py` |
15870
15873
| (its control) | the scan finds contexts where they exist (`store/postgres.py`) | pointed the control at a module with none |
15871
15874
| `test_the_sqlserver_hop_asserts_no_suites_and_pins_what_it_can_control` | the DSN path asserts nothing, and pins `Encrypt=yes` / `TrustServerCertificate=no`, which it CAN control | changed the emitted `Encrypt` value |
15872
-
| `test_the_hvac_scope_out_premise_still_holds` | `urllib3` absent AND no leg installs `[vault]` | made `urllib3` resolvable; separately added `vault` to a workflow's extras |
15875
+
| `test_the_hvac_scope_out_premise_still_holds` *(FIRED; converted 2026-09-04 to `test_the_hvac_arm_stays_built_and_stays_executable`)* | `urllib3` absent AND no leg installs `[vault]` | made `urllib3` resolvable; separately added `vault` to a workflow's extras |
15873
15876
| (its control) | the workflow scan reads real install lines | removed `webauthn` from both workflows that install it |
15874
15877
15875
15878
**These are TRIGGERS, not guards.** A red does not mean the engine got worse. It means the reason an
15876
15879
arm was left unasserted has stopped being true and the arm is now buildable -- build it and amend
15877
15880
ADR 0180, never delete the test.
15878
15881
15879
-
**5. THE RESIDUAL, and it is the only one left.** Install the `[vault]` extra on a CI leg; then
15882
+
***THE hvac TRIGGER FIRED, AND THAT SENTENCE IS WHAT WAS DONE WITH IT.*** The second 2026-09-03
15883
+
amendment below installed the extra and built the assertion, so both halves of that test went red by
15884
+
design. It was converted rather than deleted, and it now GUARDS the opposite failure: that the arm,
15885
+
once built, keeps being executed. **The two ODBC tests are still triggers and read exactly as written
15886
+
above.** They are no longer the same shape as the hvac one, so do not read them as a set.
15887
+
15888
+
**5. THE RESIDUAL, and it is the only one left.** ***MET on the same day, by the amendment below.***
15889
+
Preserved as the dated statement of the precondition, because naming it is what made it satisfiable.
15890
+
Install the `[vault]` extra on a CI leg; then
15880
15891
urllib3's own `urllib3.util.ssl_.create_urllib3_context()` -- the function urllib3 itself calls, not a
15881
15892
look-alike -- becomes executable by a test, and the assertion belongs in both `_build_client`
15882
15893
factories (`store/keyprovider_vault.py` and `config/secretprovider_vault.py`, which between them cover
@@ -15885,8 +15896,84 @@ none is allocated. **The trigger test above is what will report the day that pre
15885
15896
15886
15897
**PROPOSED CLOSURE, not taken here.** Every claim this row still carries as open is now either built
15887
15898
(the allowlist, the `ldap3` arm), permanently out with evidence (ODBC Driver 18), or a named residual
15888
-
gated on a CI change (`hvac`). A builder must not flip a banner while a residual stands, so the banner
15889
-
is left PARTIAL for the owner to rule on with the residual in view.
15899
+
gated on a CI change (`hvac`). *The `hvac` residual was met the same day -- see below -- so the
15900
+
banner is now held open by the allowlist SCOPE question alone.* A builder must not flip a banner while
15901
+
a residual stands, so the banner is left PARTIAL for the owner to rule on with the residual in view.
15902
+
15903
+
**AMENDMENT 2026-09-03 (SECOND, same day; written after the one above and landed after it) -- THE
15904
+
VAULT ARM IS BUILT, AND THE CI LEG IS WHAT MAKES IT HONEST.** Owner-ruled
15905
+
the same day: install the `[vault]` extra on a CI job, *then* build the hvac assertion. A Builder
15906
+
earlier that day had refused to build it alone and was right to -- hvac is absent from every local
15907
+
interpreter and no CI leg installed the extra, so the assertion could have shipped without ever being
15908
+
executed once. See [ADR 0180](adr/0180-asserting-tls-suites-on-a-library-that-exposes-no-sslcontext.md)
15909
+
Amendment A for the full measurement; what this row records is what it now measures.
15910
+
15911
+
**The order is the deliverable, not a preamble.** `[vault]` now installs on `ci.yml`'s `test` leg --
15912
+
the only leg that runs `tests/`, so the only install line that can exercise the control. Six small
15913
+
pure-Python wheels, every one already pinned in `requirements.lock` and `constraints.lock`, so **no
15914
+
re-lock was needed**. `tests/_extras_probe.py` gained a `vault` row, so an interpreter without the
15915
+
extra now announces the run as INCOMPLETE rather than reporting a quiet green over skipped security
15916
+
tests.
15917
+
15918
+
**IT IS ASSERTABLE, AND THE MEASUREMENT SAYS WHY A REPLICA IS THE ONLY INSTRUMENT.** Measured against
15919
+
the locked pins (hvac 2.4.0, requests 2.34.2, urllib3 2.7.0) by driving a real client at a real socket,
15920
+
not by reading source: `hvac.Client` carries zero `SSLContext` attributes, its `requests.Session`
15921
+
carries none, its `PoolManager` has `connection_pool_kw == {'maxsize': 10, 'block': False}` with no
15922
+
`ssl_context` key, and the module-level `requests.adapters._preloaded_ssl_context` that requests 2.32
15923
+
carried is **gone in 2.34**. urllib3 builds the context lazily, per connection, calling
15924
+
`create_urllib3_context` **once** and yielding **17 suites -- zero NULL, zero anonymous, zero
15925
+
non-forward-secret**. So `assert_hvac_tls_suites` replicates that construction with urllib3's OWN
15926
+
public constructor and refuses any `hvac.Client` argument it cannot replicate -- `session=` above all,
15927
+
since that is the documented way to hand this hop a different context.
15928
+
15929
+
**ONE GUESS THIS ROW WOULD OTHERWISE HAVE INHERITED, corrected by measurement.**
15930
+
`create_urllib3_context()` and stdlib `create_default_context()` produce the **identical** 17 suites on
15931
+
the current OpenSSL. The reason to use urllib3's own constructor is therefore *not* that a stdlib
15932
+
look-alike is measurably wrong today -- it is that only the real function tracks urllib3 if urllib3
15933
+
narrows its own defaults. The satisfying version of that argument would have been false.
15934
+
15935
+
**THE PROOF IT CARRIES, which is the shape this row demanded of its own first half.** Seven tests
15936
+
pinning the refusal AND a positive control -- the real 17-suite list is required to be non-empty and
15937
+
clean on all three shipped predicates, because a refusal pinned alone cannot tell a working control
15938
+
from one that refuses everything. Five mutations, each applied alone with the tree restored between,
15939
+
**every one RED**: delete the assertion from either construction point, make the unreplicable-argument
15940
+
refusal inert, drop `harden_cipher_suites` from the replica, and make the replica build a context that
15941
+
*differs* from urllib3's. The last is the one that matters for a replica -- it proves the equivalence
15942
+
test detects drift rather than merely passing.
15943
+
15944
+
**TWO CONSTRUCTION POINTS COVER THREE CLIENTS, and that is now pinned by identity rather than prose:**
15945
+
a test asserts `crypto_transit._build_client is keyprovider_vault._build_client`. `_build_client` also
15946
+
moved OUT of its callers' `try` blocks in all three callers, whose `except Exception` would have
15947
+
relabelled this configuration refusal as a connectivity failure -- the same mistake ADR 0180 records
15948
+
for the LDAPS site. Fail-closed is unchanged: both paths propagate and the subsystem refuses to start.
15949
+
15950
+
**THE TRIGGER THE AMENDMENT ABOVE LEFT BEHIND IS CONVERTED, NOT DELETED (2026-09-04).** That amendment
15951
+
shipped `test_the_hvac_scope_out_premise_still_holds`, asserting the two premises this one falsifies:
15952
+
`urllib3` absent, and no leg installing `[vault]`. Both halves went red on the merge, which is the
15953
+
trigger working rather than a defect, and that amendment's own text says what to do with it -- *"build
15954
+
it and amend ADR 0180, never delete the test."* It is now
15955
+
`test_the_hvac_arm_stays_built_and_stays_executable`, guarding the failure a fired trigger cannot see:
15956
+
that the built arm keeps being EXECUTED. Two halves again. A CI leg must still install `[vault]`, or
15957
+
all seven tests above skip and a suite reports green over a security control nothing ran; and both
15958
+
`_build_client` factories must still reach `assert_hvac_tls_suites`, checked at source level because
15959
+
on an interpreter without the extra those seven tests are exactly the ones that would not run.
15960
+
15961
+
**WHERE THE ROW NOW STANDS. Every library named in the residual is closed or recorded with evidence:**
15962
+
`ldap3` asserts (2026-08-30), `hvac` asserts (this amendment), and **ODBC Driver 18 is
15963
+
un-assertable with the reason in ADR 0180** -- TLS is terminated inside the native driver, the suite
15964
+
list belongs to the driver and the OS TLS stack rather than to the interpreter's OpenSSL, and no
15965
+
Python-side context exists to hold or replicate. That is a finding, not a deferral.
15966
+
15967
+
**THE BANNER STILL DOES NOT FLIP, AND THE TWO AMENDMENTS AGREE ON THAT WITHOUT AGREEING ON WHY. Read
15968
+
them together or you will read a contradiction that is not there.** The one above measures the strict
15969
+
positive allowlist as BUILT and finds nothing left to build. This one adds where it REACHES: it
15970
+
governs the operator KNOB (`validate_tls_ciphers`), and an inherited default context is still checked
15971
+
by the three property predicates alone. Build-state and reach are different questions about one
15972
+
measurement, so neither reading corrects the other. What is left is therefore a SCOPE question --
15973
+
should the positive allowlist extend to contexts no operator configured? -- and it is the row's only
15974
+
remaining ask. **Closure is proposed by both amendments and taken by neither.** The library half this
15975
+
row declared open is fully accounted for. A seat with the authority should rule on whether the
15976
+
allowlist scope survives as this row or as a new one.
15890
15977
15891
15978
## 1315. prose path:line citations carry no token, so nothing can verify them
0 commit comments