Skip to content

Enable ruff security, logging and debugging rules (S, T20, LOG, G, DTZ, ERA, TD, FIX, EXE) - #3915

Merged
anth-volk merged 19 commits into
masterfrom
ruff-security-logging
Oct 19, 2026
Merged

anth-volk merged 19 commits into
masterfrom
ruff-security-logging

Conversation

@anth-volk

@anth-volk anth-volk commented Oct 13, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #3914

Also fixes #3909: the country validator's print("Validating country") is removed.

Summary

Phase 1 step 1.8, group 6: security, logging and debugging leftovers. The bottom of its stack since #3913 merged.

  • Rules: select gains S, T20, LOG, G, DTZ, ERA, TD, FIX and EXE. Every finding at the base (262, plus 12 in Stage 12A: Sign-in identity and v1-anchored user associations #3891's code after the restack) is fixed, kept with an inline reason, or, in tests, CI/CD scripts and command-line scripts, covered by a file-level exception. One commit per family.
  • File-level exceptions (new):
    • Where they're allowed. A ruff per-file-ignores entry is now allowed only for:
      • tests (a tests or authenticated_tests directory);
      • CI/CD scripts (.github);
      • command-line scripts (a scripts directory, or a module with a top-level if __name__ == "__main__":).
    • How it's checked. The rules guard expands each entry's pattern the way ruff does: * and ? also match /, and a pattern also matches a file by its name. It fails when the pattern covers any other file, is negated, or matches nothing. scripts/rules/file_exceptions.py holds the definitions, docs/engineering/skills/rules-registry.md#file-level-exceptions documents them, and six rules-tool tests prove them.
    • The entries. Each lists exactly the group 6 codes those files had suppressed inline, not whole families, and each has a registered per-file-ignore rule:
      • tests (**/tests/**, extending api.quality.tests-ruff-exemptions): S105, S106, S603, S607, S608, DTZ001, DTZ011, T201;
      • CI/CD scripts (.github/**): S310, T201;
      • command-line scripts (scripts/**): T201, S603, S607, S310, S311, DTZ011;
      • the five modules that also run as programs (catalog initialization, the three migration qualifications, worker_spm_release): T201.
    • What it removed. 120 inline suppressions: 74 in tests, 28 in scripts, 10 in the program modules and 8 in .github.
    • Alembic revisions. Alembic revisions aren't any of those kinds of file, so their "migrations/*/versions/*.py" = ["ALL"] entry moves to [tool.ruff.lint] exclude. ruff check . still skips them, and ruff format still checks them. api.quality.alembic-revisions-unused-imports now describes the exclusion.
  • Behaviour changes (three kinds, all deliberate):
    • Debug prints removed (T201): the country validator (validate_country prints "Validating country" on every request #3909), plus the create, get and update prints in the report output and simulation routes. The two update prints become logger.debug calls on a module logger, because their ids come from the request body.
    • Logger name (LOG015): the household calculation routes log their three errors on a module logger instead of the root logger. The message text is unchanged, and production records still reach the root observability handler.
    • Nothing else: error responses, message text and published schemas are unchanged.
  • Asserts outside tests (S101, 11): each type-narrowing assert in the v2 household, policy, user-household and user-policy services, and two in the rules tool, becomes an explicit check that raises TypeError. None runs inside a Pydantic validator, and nothing catches AssertionError. Every check was already guaranteed by an earlier one, so none is expected to fire; the v2 routes and the mirror paths treat either type the same way. No production command runs Python with -O, so these asserts were live.
  • Prints in commands (T201): command-line scripts, CI/CD scripts and the modules that run as programs (including the migration qualification commands' JSON evidence) keep their output under the file-level exceptions. capture_migration_baseline.py, smoke_test_local_boot.py and the rules tool's CLI route output through one _report helper. Application code keeps the api.py startup timings and the gateway-auth warning on stderr, each with an inline reason.
  • Datetimes (DTZ, 21): no value switches between naive and aware. The catalog extraction builds its aware UTC value directly; the other 20 stay naive. Those in tests and scripts, such as the rules tool's calendar-day review dates and test values, are covered by the file-level exceptions. The 4 in application code (naive database columns, the local calendar year) keep an inline reason.
  • Security (S): no vulnerability found.
    • Fixed: hashlib.sha1(..., usedforsecurity=False) (same digest).
    • Covered by file-level exceptions:
      • in tests: fake secrets, runs of this repository's scripts and git, and table names from a fixed tuple;
      • in scripts and .github: fixed https and loopback URLs, and a seeded load-test mix.
    • Kept inline with a reason:
    • S110: the five try/except Exception: pass blocks group 5 left (two in the ASGI factory, and the user routes' failure logging, the GCP log handler and request-context binding) are now with contextlib.suppress(Exception): blocks with their reason in a comment above, like the eighteen Enable ruff exception rules (TRY, EM, BLE) #3913 converted. They need neither BLE001 nor S110, and behaviour is unchanged.
  • Leftovers: 32 lines of commented-out code are deleted (29 in tests/to_refactor/python/test_policy.py). The two TODOs become plain notes; the user-profile one keeps its link to Utilize a technology like Marshmallow to validate inbound requests #2054 and says it was closed. Seven Python scripts with a shebang and a __main__ entry point are now executable.
  • Also in this PR:
    • The GPT-4 dashboard experiment is removed. dashboard/experiments/gpt4_api_user.py and its requirements file were a Streamlit experiment nothing imports or runs. Removing it also removes its eval and its three S113 suppressions.
    • A doc-only rule, api.process.agents-suppress-as-last-resort: agents add an inline ruff suppression only when neither a code change nor a ruff setting removes the finding, and a PR lists every inline suppression it adds, with the reason. docs/engineering/skills/rules-registry.md#inline-suppressions gives the order to try, and the GitHub PRs skill cites the rule.
  • New inline suppressions (12), as that rule asks:
    • S105 (6): environment-variable names that contain SECRET, in data/v2/settings.py, libs/auth0_management.py (2) and libs/gateway_auth.py (2), and the WWW-Authenticate challenge in fastapi_routes/v2/errors.py. Ruff's hard-coded-password check matches on the names; none is a secret.
    • T201 (2): the startup timings in api.py and the unauthenticated-client warning in libs/simulation_entrypoint.py, both written straight to stderr. Routing them through logging would change their output.
    • DTZ005 (2), DTZ011 (1), DTZ901 (1): the local calendar year in constants.py and services/household_calculation_service.py, and naive start_time values in services/economy_service.py and runtime_cache/reform_impacts.py that must match a naive database column. Making them aware would change stored values.
    • It also removes five BLE001 suppressions from master (the contextlib.suppress blocks above).
  • Registry:

Testing

At this branch's tip (3f7deea, on master at 43e6c7f):

  • ruff check and ruff format --check (0.15.12) are clean repo-wide, and adding RUF100 finds no unused suppression other than six E402 ones in a test file that master also has.
  • The quality guards pass: migration contracts, rules registry and CI scripts.
  • The 16 unit test files that cover the four rewritten modules: 351 passed, on Python 3.12 with the uv.lock dependencies.
  • CI: this PR is now the bottom of its stack, so it runs the full PR workflow.

Before this update, on the earlier tip:

  • tests/unit (2,685 passed) and the to_refactor, contract and dedupe set (97 passed).
  • Every Pydantic model schema (230 models, 100 adapters), the FastAPI OpenAPI with every native router enabled, the /v2/openapi.json preview and the legacy spec were byte-identical to the base.
  • mypy was clean (167 files).
  • CI/CD: no workflow changed. Four .github scripts changed only in comments, output helpers or file mode, and the environment variables they read are unchanged.

🤖 Generated with Claude Code

@anth-volk
anth-volk added this pull request to stack #3888 October 13, 2026 17:53
@anth-volk
anth-volk removed this pull request from stack #3888 October 13, 2026 19:26
@anth-volk
anth-volk added this pull request to stack #3917 October 13, 2026 19:26
@anth-volk
anth-volk force-pushed the ruff-security-logging branch from f973af3 to 6c8d2b6 Compare October 14, 2026 13:52
@anth-volk
anth-volk force-pushed the ruff-security-logging branch 2 times, most recently from 747e269 to 899ad06 Compare October 14, 2026 16:59
@anth-volk
anth-volk force-pushed the ruff-security-logging branch 3 times, most recently from c67b555 to 1d13dbe Compare October 14, 2026 20:04
@anth-volk
anth-volk force-pushed the ruff-security-logging branch 7 times, most recently from 509c02c to dfd8c98 Compare October 15, 2026 15:21
@anth-volk
anth-volk force-pushed the ruff-security-logging branch from dfd8c98 to 1f2e57c Compare October 15, 2026 15:45
@anth-volk
anth-volk force-pushed the ruff-security-logging branch from 1f2e57c to 795091f Compare October 15, 2026 17:21
@anth-volk
anth-volk force-pushed the ruff-security-logging branch 2 times, most recently from 7a6c495 to 7008825 Compare October 16, 2026 06:45
anth-volk and others added 18 commits October 19, 2026 23:05
Each of these six scripts has a python3 shebang and a __main__ entry
point, so it is meant to be run directly. Git now records them as
executable; running them through python is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The user-profile update note keeps its link to issue #2054, which was
closed as not planned, and now says so. The simulation serializer note
explains that its JSON-in-a-string projection serves only the v1
contract.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The three logging.exception calls in the household routes used the root
logger. They now use a module logger, so the records are named after
policyengine_api.routes.household_routes; the message text and traceback
are unchanged. The module keeps its existing structured logger for
log_struct.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The catalog extractor builds a date's midnight with tzinfo=UTC directly
instead of attaching UTC afterwards; the result is the same aware value.

Every other naive value stays naive, with an inline reason: reform
impact start_time and report run timestamps are naive columns (and the
runtime cache's datetime.min fallback must compare with them), economy
start_time has always held local time, the default year and household
period follow the host's local calendar, and the rules registry checks
review and expiry dates against the local date.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The eight asserts in the v2 household, policy and user-household
services and the two in the rules ci-scripts guard now raise TypeError
when the narrowed value has the wrong type. None runs inside a Pydantic
validator and nothing catches AssertionError: the v2 routes return their
generic 500 and the mirror paths report an "unexpected" failure for
either type, and the rules callers check for a missing [ci_scripts]
table first. The checks also hold under python -O, which strips asserts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
validate_country no longer prints "Validating country" on every request
(#3909). The simulation and report routes no longer print on create and
get, where the URL already carries the country and id; their update
messages, whose id comes from the request body, become debug records on
a module logger with the same text.

The remaining prints are output by design and carry a reason: the CLI
entry points' JSON evidence and results, the startup timings and the
unauthenticated simulation client warning on stderr.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Printing is these scripts' job. The four scripts with more than three
prints (the version bump, the migration baseline capture, the local boot
smoke test and the rules CLI) route their output through one _report
helper. Three of those helpers' prints carry the suppression; the version
bump's helper passes its file argument through, which T201 does not flag,
and its stderr messages still go to stderr. Every other print carries an
inline reason, as does the catalog publication qualification test, whose
printed evidence is its report.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Behaviour-identical fixes: the runtime cache version's sha1 is marked
usedforsecurity=False (the digest is unchanged), and the GPT-4
experiment parses its embedding strings with ast.literal_eval.

Test modules reuse one constant per fake credential or claim token
instead of repeating the literal, and the Cloud Run deploy-script tests
run bash through one helper.

Every other finding keeps its code with an inline reason: try/except/pass
on the deliberate telemetry and logging catch-alls (S110, alongside their
BLE001 reason), environment-variable and Secret Manager names and test
sentinels (S105, S106), fixed repository commands (S603, S607), fixed
https and loopback URLs (S310), the experiment's untimed requests
(S113), the load test's seeded random mix (S311) and fixed table names
in a test query (S608).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Select flake8-datetimez (DTZ), eradicate (ERA), flake8-executable (EXE),
flake8-fixme (FIX), flake8-logging-format (G), flake8-logging (LOG),
flake8-bandit (S), flake8-print (T20) and flake8-todos (TD). Tests
follow all nine: their exemptions stay limited to S101, D1, ANN and
PLR2004.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
api.quality.ruff-lint now lists DTZ, ERA, EXE, FIX, G, LOG, S, T20 and
TD. The rendered rules page also picks up the rules tool's new hash.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…S101)

The user-household and user-policy patch methods asserted that a
supplied legacy id is not None before checking it in v1. They now raise
TypeError instead, as the other v2 services do. The update models
already reject an explicit null legacy id, so neither check can fail for
a validated request; if one did, nothing catches either type and the
route returns its generic 500. The checks also hold under python -O.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- S105: the Auth0 Management secret environment-variable names and the
  invalid-token WWW-Authenticate challenge keep their literals with a
  reason, as do the Auth0 test's Secret Manager path and the route-auth
  test's leak sentinel.
- S106: the Cloud Run deploy-script tests name the Auth0 Management
  secret resource once, as TEST_AUTH0_SECRET_RESOURCE, instead of
  repeating the literal three times.
- S110: the user routes' failure logging keeps its try/except/pass with
  the BLE001 reason.
- T201: the association qualification command's two prints are its JSON
  evidence and keep a reason.
- EXE001: scripts/qualify_v2_user_association_migration.py has a
  python3 shebang and a __main__ entry point like its two siblings, so
  git now records it as executable.

These were the other 10 ruff group 6 findings in the code #3891 added.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…r file

Per-file-ignores are now only for tests, CI/CD scripts and command-line
scripts, so the "migrations/*/versions/*.py" = ["ALL"] entry moves to
[tool.ruff.lint] exclude. The revisions must stay exactly as Alembic
generates them (api.migration.alembic-autogenerated-only), so ruff still
doesn't lint them when it walks the repository, and ruff format still
checks them as before. api.quality.alembic-revisions-unused-imports now
describes the exclusion, with no ruff enforcer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e scripts

A ruff per-file-ignores entry switches rules off for whole files. That is
now acceptable where it's the norm: tests, CI/CD scripts under .github,
and command-line scripts (under a scripts directory, or modules run
through if __name__ == "__main__"). Everywhere else a kept finding still
needs an inline suppression with its reason.

The rules guard already requires a per-file-ignore rule for every entry.
It now also expands each entry's pattern the way ruff does (globset
syntax, where * and ? also match "/", and a pattern also matches a file
by its name alone) and fails when the pattern covers any other file, is
negated, or matches nothing.
scripts/rules/file_exceptions.py holds the definitions, and the registry
doc describes the policy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Group 6's rules flag patterns that are the norm in tests, CI/CD scripts
and command-line programs: fake credentials, subprocess calls to
repository scripts, prints, naive datetimes, fixed URLs. Those files now
take file-level exceptions, each registered as a per-file-ignore rule,
and the 120 inline suppressions they cover are removed:

- tests (**/tests/**, extending api.quality.tests-ruff-exemptions):
  S105 30, S603 14, DTZ001 13, S607 11, S106 2, S608 2, DTZ011 1, T201 1;
- CI/CD scripts (.github/**): T201 6, S310 2;
- command-line scripts (scripts/**): T201 17, S603 4, S607 2, DTZ011 2,
  S310 2, S311 1;
- modules that also run as programs (catalog initialization, the three
  migration qualifications and worker_spm_release): T201 10.

Each entry lists exactly the codes these files suppressed, not whole
families, so other security rules still apply to them. Suppressions in
application code are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dashboard/experiments held only gpt4_api_user.py, a Streamlit experiment
that nothing imports or runs, and the requirements file for it. Removing
it also removes its three S113 suppressions (requests without a timeout).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Five try/except Exception: pass blocks still carried BLE001 and S110
suppressions: two in the ASGI factory and one each in the user routes'
failure logging, the GCP log handler and request-context binding. Ruff's
SIM105 doesn't flag them because their try bodies hold several
statements, so #3913 left them as they were.

Each is now a with contextlib.suppress(Exception) block with its reason
in the comment above, like the eighteen #3913 converted. Behaviour is
unchanged: each block still ignores any exception from its body. This
removes five BLE001 and five S110 codes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…st resort

api.process.agents-suppress-as-last-resort: agents add an inline ruff
suppression only when neither a code change nor a ruff setting removes
the finding, and a PR lists every inline suppression it adds, with the
reason. Inline suppressions grew from 72 on master to 157 at the top of
this stack, while the complexity PRs, told to add none, fixed 225
findings by refactoring.

The rules-registry skill gains an "Inline suppressions" section with the
order to try (code change, ruff setting, allowed file-level exception)
and the contextlib.suppress form for deliberate best-effort calls. The
GitHub PRs skill cites the rule for PR descriptions. The rule is
judgment-tier and doc-only, so nothing checks it yet; a shrink-only
count of suppressions could enforce it later. The rendered rules page
and the AGENTS.md Rules block are regenerated, and the changelog
fragment describes this PR's other two additions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@anth-volk
anth-volk force-pushed the ruff-security-logging branch from af985e1 to 3f7deea Compare October 19, 2026 19:10
@anth-volk
anth-volk marked this pull request as ready for review October 19, 2026 21:13
@anth-volk
anth-volk requested a review from a team as a code owner October 19, 2026 21:13
@anth-volk
anth-volk merged commit c39ae36 into master Oct 19, 2026
15 checks passed
@anth-volk
anth-volk deleted the ruff-security-logging branch October 19, 2026 21:14
anth-volk added a commit that referenced this pull request Oct 20, 2026
#3915 removed dashboard/experiments, so ruff no longer needs to treat it
as a namespace package.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk added a commit that referenced this pull request Oct 20, 2026
#3915 removed dashboard/experiments, so ruff no longer needs to treat it
as a namespace package.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk added a commit that referenced this pull request Oct 20, 2026
#3915 removed dashboard/experiments, so ruff no longer needs to treat it
as a namespace package.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk added a commit that referenced this pull request Oct 20, 2026
#3915 removed dashboard/experiments, so ruff no longer needs to treat it
as a namespace package.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch was previously deployed

1 inactive deployment
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant