Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 52 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ jobs:
# contradictions, no duplicates — is satisfied just as easily by scanning a remnant of the corpus
# as the whole of it. Without a floor, a change that stopped the archive being read would go green.
# Raise the number when the total legitimately grows; it must never be lowered to make CI pass.
# ⚠️ THE FLOOR LIVES IN TWO PLACES AND NOTHING COMPARES THEM: here, and `_MIN_TOTAL_ITEMS` in
# WARNING: THE FLOOR LIVES IN TWO PLACES AND NOTHING COMPARES THEM: here, and `_MIN_TOTAL_ITEMS` in
# tests/test_backlog_status_check.py. Raise BOTH, or the lower one becomes the only floor that
# binds. Found on 2026-08-05 at 277 against a corpus of 300 — 23 items of accumulated slack, in a
# guard whose entire purpose is to notice the corpus shrinking.
Expand Down Expand Up @@ -167,7 +167,7 @@ jobs:
# docs-only PR pays a base editable install and ~5s of tests rather than the full extras install.
# The gated steps below are deliberately UNTOUCHED, so a code PR is byte-identical to before.
#
# ⚠️ 89 of these tests SKIP here and that is structural, not a gap to fix in this step:
# WARNING: 89 of these tests SKIP here and that is structural, not a gap to fix in this step:
# `tests/test_threat_model_doc_drift.py` asserts against `docs/security/THREAT-MODEL.md`, which is
# vault-only and absent from this tree. ADR 0156 records that class (six `*_doc_drift` modules
# assert against documents `git ls-files docs/security/` shows are not present) and ASVS 15.1.3 is
Expand Down Expand Up @@ -295,7 +295,7 @@ jobs:
# Step-level watchdog UNDER the job cap (#55): the windows-2022 leg intermittently
# wedges ~25% in (a Windows ProactorEventLoop listener-teardown / socket wait that the shared
# session event loop can't get past), emits no output for ~12 min, then the JOB cap CANCELS it
# — a red with no stack and no named test. Two belts make that fail FAST and NAMED instead:
# — a red X with no stack and no named test. Two belts make that fail FAST and NAMED instead:
# * `--timeout-method=thread` (the ONLY method on Windows — SIGALRM is POSIX-only) dumps ALL
# thread stacks at the per-test cap (matrix `pytest_timeout`, 120s on these Windows legs),
# naming the stuck frame.
Expand Down Expand Up @@ -834,7 +834,11 @@ jobs:
# A path appears in `changed` even if only deleted/renamed, so a pure doc rename still short-
# circuits, and a code deletion still runs.
# Allowlisted (docs-only) paths: any *.md anywhere, docs/**, top-level LICENSE/NOTICE/AUTHORS,
# .editorconfig, .gitignore/.gitattributes, .github/{ISSUE_TEMPLATE,PULL_REQUEST_TEMPLATE,...}.md.
# .editorconfig, .gitattributes, .github/{ISSUE_TEMPLATE,PULL_REQUEST_TEMPLATE,...}.md.
# NOT .gitignore -- #327 removed it from the regex and this line went on listing it for
# weeks, the THIRD instance in this file of a comment stating the opposite of its code.
# `.gitattributes` IS still listed here and is nonetheless CODE: `alwayscodepath` below is
# checked first and wins. Precedence, not deletion -- see the reasoning there.
# Be conservative: when in doubt a path is CODE. Empty diff (shouldn't happen on a PR) => code=true.
# `.gitignore` is NOT in this allowlist, deliberately (BACKLOG #327). Six of its rules are the
# sole control keeping maintainer-internal material out of a public commit, and
Expand All @@ -845,8 +849,52 @@ jobs:
# ungated backlog guards above were added for. Treating it as code costs one suite run on a
# rare PR; the alternative costs the publishing boundary, silently.
noncode='(\.md$|^docs/|^LICENSE$|^NOTICE$|^AUTHORS$|^\.editorconfig$|^\.gitattributes$|^\.github/(ISSUE_TEMPLATE/|PULL_REQUEST_TEMPLATE))'
# EXTENSIONLESS CONFIG THAT IS ALWAYS CODE (BACKLOG #1200). The extension rule below cannot
# reach a file with no extension, and `.gitattributes` is exactly that: it was still in the
# docs-only allowlist above, so a `.gitattributes`-only PR skipped lint, mypy and the whole
# suite. It is not cosmetic -- the vault's own asvs-scorecard.yml records that a change here
# "silently alters how the corpus is materialized, which is exactly what makes the digest
# differ", so it is an input to the ASVS corpus pin.
#
# Stated as a POSITIVE list and CHECKED FIRST, rather than by deleting the entry from
# `noncode`, for two reasons. "Is code" should not depend on the ABSENCE of a line somewhere
# else -- a future edit re-adding a path to `noncode` would silently undo this with nothing
# to say so. And keeping the historical `noncode` intact is what lets
# tests/test_ci_docs_only_detector.py assert the regression in BOTH directions: it can still
# reconstruct the old classification by disabling this rule alone. `.gitignore` is named for
# the first reason even though #327 already removed it -- it was code only by falling through.
alwayscodepath='^(\.gitattributes|\.gitignore)$'
# EXTENSION OVERRIDE, EVALUATED FIRST (BACKLOG #1200). An executable file is CODE wherever it
# lives, including under docs/.
#
# The paragraph above states the intent exactly -- "any *.py ... counts as CODE" -- and the
# regex did not implement it, because `^docs/` is an alternation branch that matches a .py
# under docs/ and short-circuits before the *.py rule is ever reached. An auditor reads the
# comment, agrees with it, and moves on. Measured on 2026-08-09:
# docs/security/asvs-apply-cells.py classified NON-CODE -- the tool that WRITES the ASVS
# record of record, able to silently un-close an owner-closed cell, exempt from lint, mypy
# and the entire pytest suite by virtue of its directory. Two mypy errors had been sitting in
# it since it was written; they could not have survived a single check.
#
# THE PRECEDENT IS FOUR LINES ABOVE THIS ONE. BACKLOG #327 fixed exactly this shape for
# `.gitignore` and wrote the lesson down -- and the identical defect for docs/**/*.py sat in
# the regex immediately below the paragraph explaining it. The instance was fixed and the
# class left open, with the reasoning that would have closed it preserved in place. Hence an
# EXTENSION rule rather than another one-path exception: the next executable file someone
# puts under docs/ must not need this discovered a third time.
#
# The docs-only optimisation is deliberately preserved for actual documents -- deleting
# `^docs/` outright would run the full suite on every prose edit, which is the cost this
# short-circuit exists to avoid. Order matters: this is checked BEFORE `noncode`.
alwayscode='\.(py|ps1|sh|ts|js|yml|yaml|toml|lock|cfg|ini)$'
if [ -z "$changed" ]; then
echo "code=true" >> "$GITHUB_OUTPUT"
elif echo "$changed" | grep -qE "$alwayscode"; then
# An executable/config file changed, wherever it lives -> run the full suite.
echo "code=true" >> "$GITHUB_OUTPUT"
elif echo "$changed" | grep -qE "$alwayscodepath"; then
# Extensionless config the extension rule cannot see -> run the full suite.
echo "code=true" >> "$GITHUB_OUTPUT"
elif echo "$changed" | grep -qvE "$noncode"; then
# At least one changed path is NOT docs-only -> run the full suite.
echo "code=true" >> "$GITHUB_OUTPUT"
Expand Down
58 changes: 58 additions & 0 deletions docs/BACKLOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8714,6 +8714,64 @@ filing.

**Source:** filed 2026-08-08 from the ASVS ledger-coverage sweep of the partial and fail cells that carried no backlog item at all; this cell was one of them. The scorecard is the record of record for the verdict; this item tracks the research toward changing it.

## 1200. the CI docs-only detector exempts EXECUTABLE files under `docs/` from the entire suite

> 🔢 **Filed 2026-08-09 - FIXED in the same change. Reproduced with the workflow's own regex under real `grep -E`, with a negative control.** Value **7/10** · Difficulty **2/10**. `ci.yml`'s `changes` job short-circuits the required `test` legs when every changed path is docs-only. `^docs/` is an alternation branch in that allowlist, so it matches a **`.py` under `docs/`** and short-circuits before the stated `*.py` rule is ever reached. A PR touching only such a file set `code=false` and skipped install, lint, type-check and the whole of pytest.

**Cluster:** CI correctness / gate blindness. **Priority:** P2. **Verdict:** build (done).
**Severity:** no product effect and no PHI effect. The cost is that a defect here does not fail loudly
- it REMOVES the thing that would have failed, which is the worst failure mode a gate has.

**Measured, not reasoned.** Extracting the live regex from `ci.yml` and running real `grep -E`:

```
PRE-FIX (noncode only):
docs/security/asvs-apply-cells.py -> NON-CODE (suite skipped)
docs/benchmarks/.../b5_microbench.py -> NON-CODE (suite skipped)
POST-FIX (alwayscode checked first):
docs/security/asvs-apply-cells.py -> code
docs/SECURITY.md -> NON-CODE (still short-circuits)
.gitignore -> code (via the noncode branch, BACKLOG #327)
```

**Blast radius.** Engine: 2 files, both benchmark scripts under
`docs/benchmarks/results/2026-07-04-adr0071-b5-executor-marshaling/` - low risk. Vault: 3 files,
including `docs/security/asvs-apply-cells.py`, the tool that WRITES the ASVS record of record and can
silently un-close an owner-closed cell. **Two mypy errors had been sitting in that file since it was
written; they could not have survived a single check.** That is the corroboration that the exemption
was real and not theoretical.

**TWO THINGS MAKE THIS WORSE THAN A MISSING TEST.**

**The comment and the regex disagree, and the comment is what people read.** `ci.yml` states the intent
in as many words: *"Anything outside the allowlist - any `*.py`, `ide/**`, config, lockfiles, OTHER
workflows, scripts, samples, harness - counts as CODE and runs the full suite."* The regex does not
implement that sentence. An auditor reads the comment, agrees with it, and moves on.

**The precedent sits four lines above the defect.** `#327` fixed exactly this shape for `.gitignore` -
allowlisted as docs-only, so a `.gitignore`-only PR skipped `tests/test_private_paths_stay_ignored.py`,
*"the one guard that would catch the rule being deleted DID NOT RUN, on exactly the PR shape it exists
to catch"* - and the lesson was written down in place. The identical defect for `docs/**/*.py` was in
the regex immediately below that paragraph. **The instance was fixed and the class was left open, with
the reasoning that would have closed it preserved alongside.** That is the recurring shape: a fix that
does not generalise is the one that comes back.

**The fix.** An `alwayscode` EXTENSION check evaluated BEFORE the `noncode` allowlist:
`\.(py|ps1|sh|ts|js|yml|yaml|toml|lock|cfg|ini)$`. An executable file is code wherever it lives. The
docs-only optimisation is deliberately preserved for actual documents - simply deleting `^docs/` would
have run the full suite on every prose edit, which is the cost the short-circuit exists to avoid.

**The test drives the DETECTOR, and reads its regexes OUT of `ci.yml`.** A test carrying its own copy
of the pattern passes forever while the workflow drifts underneath it, reproducing this very defect one
level up. It asserts the regression in BOTH directions in a single test - the pre-fix logic classifies
`docs/x.py` as non-code AND the post-fix logic does not - because asserting only the new behaviour
cannot distinguish a fixed detector from a deleted one (`return True` passes that). It carries a
negative control, so a regex that accidentally matched everything cannot make every assertion pass
vacuously.

**Source:** found 2026-08-09 while promoting the ASVS writer out of `docs/security/` (BACKLOG #1200's
sibling work), and escalated from instance to class by the parallel `asvs-tracking-rework` session,
which measured the blast radius in both repos and identified the `#327` precedent.
## 1201. `redacted_settings` served credential-bearing HTTP headers outside a five-name list

> 🔢 **Filed 2026-08-09 - FIXED IN THE SAME CHANGE, and the entry is published WITH the fix rather than ahead of it.** Value **8/10** · Difficulty **2/10**. Header redaction was `str(k).lower() in _SECRET_HEADER_NAMES` -- an exact-membership test against **five** strings (`authorization`, `proxy-authorization`, `x-api-key`, `api-key`, `cookie`). Header names are **operator-authored free text**, typed into `connections.toml` or a Handler, so an exhaustive list cannot exist even in principle. Measured against the shipped list: `X-Auth-Token`, `X-Amz-Security-Token` and `Private-Token` were all returned VERBATIM.
Expand Down
Loading
Loading