Skip to content

Update CHANGELOG through PR #8#9

Merged
cmeans merged 1 commit into
mainfrom
changelog-update
Mar 21, 2026
Merged

Update CHANGELOG through PR #8#9
cmeans merged 1 commit into
mainfrom
changelog-update

Conversation

@cmeans
Copy link
Copy Markdown
Owner

@cmeans cmeans commented Mar 21, 2026

Summary

CHANGELOG was stale — only covered the early unreleased changes. Now includes everything merged since v0.2.0 (PRs #2-8).

🤖 Generated with Claude Code

Covers PRs #2-8: note entry type, remember/update_entry/get_stats/
get_tags tools, latency instrumentation, /health endpoint, changelog
rename, README refresh, Vision section, memory prompts doc, Docker
health check fix, branch protection, 148 tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@cmeans cmeans merged commit 1ebdf91 into main Mar 21, 2026
5 checks passed
@cmeans cmeans deleted the changelog-update branch March 21, 2026 22:33
cmeans pushed a commit that referenced this pull request Apr 21, 2026
#9

Round-3 CodeQL fix. The _format_ip_header_chain helper from round 1
didn't break CodeQL's dataflow taint — the analyzer still tracked
`header_chain` through the helper call into logger.info. CodeQL #9
(new) duplicated #8's flag on the helper-call form.

New approach: split the log statement by whether defaults are used.

  - Default path (ip_headers is None): log the literal display string
    constant _DEFAULT_IP_HEADER_DISPLAY. No contributor-controlled
    value flows into the log sink — the names are source-literal.
    Operators reading the log can verify their default config.
  - Override path (env-configured ip_headers): log only the count of
    custom entries + a pointer to AWARENESS_OAUTH_PROXY_IP_HEADERS
    so operators can inspect the env var directly to see names.
    The names themselves never reach a log sink.

This removes the taint-flow path entirely rather than trying to wrap
it through a helper the analyzer sees through. Closes CodeQL #8 and
#9 without UI dismissal.

Trade-off: operators with a non-default header chain don't see the
names in logs — they check the env var instead. The extra pointer
message makes the indirection explicit.

Removed the _format_ip_header_chain helper — no longer needed. Also
dropped CHANGELOG wording that suggested #8 might need re-scan
adjudication; the split closes both cleanly.

60 existing oauth_proxy tests pass. ruff / ruff format / mypy clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cmeans-claude-dev Bot added a commit that referenced this pull request Apr 21, 2026
…ng flags (#5/#6/#7/#8) (#352)

Closes CodeQL alerts **#5, #6, #7, #8, #9**
(`py/clear-text-logging-sensitive-data` in `oauth.py` +
`oauth_proxy.py`).

## Round 3 — IP-header-chain redesign

The round-1 `_format_ip_header_chain` helper (see description below in
§"The fix") didn't break CodeQL's dataflow — the analyzer tracked
`header_chain` through the helper call and CodeQL #9 duplicated #8's
flag on the new form. Round-3 commit `e31561c` **removes the helper**
and instead splits the log by config path: the default case logs a
module-level string literal (no taint flow), the override case logs only
the count + env-var pointer (names never reach the sink). Both #8 and #9
close through code change — no UI dismissal.

This is the code-improvement path per the user preference for a
"reasonable wording change that would make CodeQL happy without
sacrificing log usefulness" rather than inline suppression.

## The problem

CodeQL flagged four log statements in the OAuth paths:

| Alert | File | Line | What it logs |
|-------|------|------|--------------|
| #5 | `oauth.py` | 81 | `discovery_url` + exception when OIDC discovery
fails |
| #6 | `oauth.py` | 85 | `self.issuer` when falling back to default JWKS
path |
| #7 | `oauth_proxy.py` | 225 | `discovery_url` + exception in the
proxy's discovery path |
| #8 | `oauth_proxy.py` | 312 | IP-header-name chain config |

CodeQL's `py/clear-text-logging-sensitive-data` uses dataflow from
"sensitive sources" (values tracked as credential-adjacent) to log
sinks. In our case, all four sites log values that are **not actually
secrets today** — `self.issuer` is a config URL, header names aren't
credentials — but CodeQL taints conservatively because the values flow
from constructor parameters in auth-adjacent contexts.

## The fix — defensive URL redaction, not suppression

Two new helpers:

### `safe_url_for_log(url)` (in `helpers.py`)

Strips credential-carrying URL components before logging:

- **`userinfo`** — RFC 3986 lets URLs embed `user:password@host`. An
OAuth issuer URL misconfigured that way would leak credentials through
every log line that mentioned it.
- **`query string`** — OAuth authorization-code flow passes `code`,
`state`, and PKCE verifiers here. Anything on the query string is a
candidate to be secret.
- **`fragment`** — OAuth implicit flow (deprecated but still
occasionally in the wild) puts access tokens in the fragment.

Returns `scheme://host[:port]/path` — preserves the operator-meaningful
portion so an operator reading a log can still identify the endpoint,
while removing anything that could leak a credential.

Defensive rather than currently-exploitable: **our issuer URLs today are
plain `https://provider/...` strings with no userinfo/query/fragment**.
The helper ensures a future misconfiguration, provider change, or
redirect can't change that. Unparseable or empty inputs return
`"<redacted url>"` — a log helper should never itself become a failure
source.

**Applied at every URL-logging site in the OAuth paths**, not just the
four CodeQL flagged — consistency:

- `oauth.py:_discover_oidc_config` — 4 log statements (discovered JWKS
URI, discovered userinfo endpoint, discovery-request failure,
discovery-fallback warning)
- `oauth_proxy.py:discover_oidc_endpoints` — 2 log statements (discovery
failure, discovered-endpoints summary)

### IP-header-chain log split by config path (in `oauth_proxy.py`)

The round-1 approach wrapped the IP-header-chain log in a
`_format_ip_header_chain` helper to try to break CodeQL's taint-flow
through function indirection. That didn't work — CodeQL #9 duplicated
#8's flag on the helper-call form. Round 3 takes a different approach:
**split the log statement by whether defaults are in use, and remove the
taint-flow path entirely**:

- **Default branch** (`ip_headers is None`): log a module-level literal
`_DEFAULT_IP_HEADER_DISPLAY = "CF-Connecting-IP → X-Real-IP → ASGI
client"`. Source-literal string; no contributor-controlled value flows
into the sink.
- **Override branch** (env-configured `ip_headers`): log only the
*count* of custom entries and a pointer to
`AWARENESS_OAUTH_PROXY_IP_HEADERS` so operators can inspect the env var
directly. The names themselves never reach a log sink.

Trade-off: operators running with a non-default header chain don't see
the names in logs — they see the count + env-var name. Small loss of log
convenience for the minority-override case; full operator clarity for
the default case (which most deployments run). Closes CodeQL #8 and #9
cleanly without UI dismissal.

## Testing

11 new unit tests in `tests/test_helpers.py::TestSafeUrlForLog`:

- `test_plain_https_preserved` — unaltered URLs round-trip
- `test_strips_userinfo` — `user:password@host` → `host`
- `test_strips_query_string` — `?code=secret&state=xyz` → `` (empty)
- `test_strips_fragment` — `#access_token=secret` → `` (empty)
- `test_strips_all_credential_carrying_parts_at_once` — composite test
- `test_preserves_non_default_port` — `:8443` survives
- `test_empty_string_returns_placeholder` — `""` → `"<redacted url>"`
- `test_unparseable_string_returns_placeholder` — `"not a url"` →
`"<redacted url>"`
- `test_no_netloc_returns_placeholder` — `"/path/only"` → `"<redacted
url>"`
- `test_path_preserved_exactly` — deep paths with dots/dashes unchanged

**All existing 120 OAuth tests (`tests/test_oauth.py` +
`tests/test_oauth_proxy.py`) continue to pass** against the refactored
code — no behavior regression, only log-output change.

## Scope

- `src/mcp_awareness/helpers.py` — `+42` (new `safe_url_for_log` helper
+ docstring)
- `src/mcp_awareness/oauth.py` — `+10, -4` (import + 4 log-site changes)
- `src/mcp_awareness/oauth_proxy.py` — `+34, -10` (import +
`_DEFAULT_IP_HEADER_DISPLAY` module constant + 3 log-site changes,
including the split-by-config-path IP-header-chain log added in round 3)
- `tests/test_helpers.py` — `+69` (import + 11 new test cases in
`TestSafeUrlForLog` class)
- `CHANGELOG.md` — `+3` (new `### Security` entry under `[Unreleased]`)

No migrations. No source API surface changed. No log-message semantics
changed beyond the URL-component redaction.

## References

- Closes CodeQL alerts #5, #6, #7 (direct — URL now redacted before
flowing to log sink)
- #8 and #9 also closed — the round-3 split-by-config-path log statement
removes taint-flow entirely; no UI dismissal required
- Session context: #334 hardening cascade and ci.yml permissions closed
alerts #1/#2/#3 via PR #351. This PR addresses Group B of yesterday's
CodeQL audit.

## QA

### Prerequisites

None. Pure code changes, covered by new unit tests and existing
integration tests.

### Automated checks

- `lint` / `typecheck` — clean locally
- `test (3.10/3.11/3.12)` — `tests/test_helpers.py::TestSafeUrlForLog`
adds 11 cases; existing OAuth tests continue to pass
- `CodeQL (python)` — expected outcome: #5, #6, #7, #8, #9 all flip Open
→ Fixed on merge.
- `codecov/patch` — new helper is covered by the 11 new test cases;
should be 100% on touched lines
- `docker-smoke` — **not triggered** (no `Dockerfile` / `pyproject.toml`
/ `uv.lock` / `.dockerignore` change)

### Manual tests

1. - [x] **All tests pass locally.**
     ```
python -m pytest tests/test_helpers.py tests/test_oauth.py
tests/test_oauth_proxy.py -q
     ```
     Expected: all pass, including the 11 new `TestSafeUrlForLog` cases.

2. - [x] **`safe_url_for_log` correctly strips each sensitive
component.**
     ```
     python -c "
     from mcp_awareness.helpers import safe_url_for_log as f
assert f('https://user:secret@host/path') == 'https://host/path',
'userinfo'
assert f('https://host/path?code=secret') == 'https://host/path',
'query'
assert f('https://host/path#token=secret') == 'https://host/path',
'fragment'
assert f('https://u:p@host:8443/oauth?c=1#t=2') ==
'https://host:8443/oauth', 'composite'
     assert f('') == '<redacted url>', 'empty'
     assert f('not a url') == '<redacted url>', 'unparseable'
     print('all manual assertions pass')
     "
     ```
     Expected: `all manual assertions pass`.

3. - [x] **Log-output semantics unchanged for normal URLs.** For any
sensible OIDC issuer URL
(`https://api.workos.com/user_management/client_X`), the log now reads
identically to before — same host, same path. Verify by eyeballing an
OAuth auth flow locally and confirming `Discovered JWKS URI: <url>` /
`OAuth proxy: discovered endpoints — authorize=<url>, …` log lines
render as before when URLs don't contain credentials.

4. - [x] **No contributor-controlled inputs are now logged.** Grep for
any remaining `%s` + URL arg that isn't routed through the helper in the
OAuth paths:
     ```
grep -nE
'logger\.(info|warning|error).*%s.*(discovery_url|self\.issuer|userinfo_endpoint|authorization_endpoint|token_endpoint|registration_endpoint|header_chain|jwks)'
src/mcp_awareness/oauth.py src/mcp_awareness/oauth_proxy.py | grep -v
safe_url_for_log | grep -v _format_ip_header_chain || echo "(none —
good)"
     ```
Expected: `(none — good)`. Every URL-logging site has been routed
through a redactor.

5. - [x] **CHANGELOG entry documents the defensive rationale.**
     ```
     grep -nE '(credential|redact)' CHANGELOG.md | head -3
     ```
Expected: the new `### Security` entry mentions credential-carrying
components and redaction.

6. - [x] **Diff review.**
     ```
     git diff --stat origin/main
     ```
Expected: 5 files changed. `CHANGELOG.md` (+3),
`src/mcp_awareness/helpers.py` (+42), `src/mcp_awareness/oauth.py` (+10,
-4), `src/mcp_awareness/oauth_proxy.py` (+34, -10),
`tests/test_helpers.py` (+69). Nothing else.

7. - [ ] **(Post-merge) Confirm CodeQL alerts flip.** After merge,
`Analyze (python)` re-runs on `main`. At
https://github.com/cmeans/mcp-awareness/security/code-scanning:
- Alerts #5, #6, #7 flip Open → Fixed (each was
`clear-text-logging-sensitive-data` in an `oauth.py`/`oauth_proxy.py`
file — the URL arg is now routed through `safe_url_for_log`, which
CodeQL should see as a sanitizer).
- Alerts #8 and #9 both flip Open → Fixed — the round-3
split-by-config-path log statement removes the taint-flow path entirely.

### Acceptance

- ✅ CodeQL #5 — `oauth.py:82` discovery-request-failed log: URL now
`safe_url_for_log(discovery_url)`.
- ✅ CodeQL #6 — `oauth.py:91` default-JWKS-path log: URL now
`safe_url_for_log(self.issuer)`.
- ✅ CodeQL #7 — `oauth_proxy.py:229` discovery-failed log: URL now
`safe_url_for_log(discovery_url)`.
- ✅ CodeQL #8 — `oauth_proxy.py:330-345` header-chain log: split into
default (logs literal `_DEFAULT_IP_HEADER_DISPLAY` constant — no taint
flow) and override (logs count + env-var pointer — header names never
reach the log sink).
- ✅ CodeQL #9 — same fix. The round-1 `_format_ip_header_chain` helper
triggered a duplicate flag; removing the helper and splitting by config
path resolves both #8 and #9.

---------

Co-authored-by: cmeans-claude-dev[bot] <3223881+cmeans-claude-dev[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@cmeans-claude-dev cmeans-claude-dev Bot mentioned this pull request Apr 22, 2026
cmeans-claude-dev Bot added a commit that referenced this pull request Apr 22, 2026
Patch release stamping six PRs merged to `main` since v0.18.1 on
2026-04-20.

## Summary

Two-file diff:

- `pyproject.toml` — `version` bump `0.18.1` → `0.18.2`
- `CHANGELOG.md` — `[Unreleased]` renamed to `[0.18.2] - 2026-04-21`;
new empty `[Unreleased]` section seeded; comparison-link footer updated

## Why patch

- No new MCP tools, no changed tool signatures, no resource changes.
- No breaking config, no migration, no data-format change.
- `requires-python = ">=3.10"` floor unchanged in `pyproject.toml`.
- Dockerfile base bump (3.12 → 3.13) is runtime-transparent to image
consumers; CI matrix widening (3.13, 3.14) is pure infra.
- OAuth log-redaction is security-hardening with no behavior change on
the happy path.
- `docker-compose` host-port parameterization is backward-compatible —
default behavior unchanged.

Textbook patch bump for a 0.x project.

## Included PRs

| PR | Title | Kind |
|---|---|---|
| [#351](#351) | ci: cascade
env-routing to `pr-labels.yml` + workflow permissions | Security |
| [#352](#352) | fix(oauth):
redact URLs in log output (CodeQL #5-#9) | Security |
| [#350](#350) | ci: add
`docker-smoke` workflow — build + import smoke on Dockerfile PRs | Added
|
| [#353](#353) |
chore(compose): parameterize host port in `docker-compose.yaml` |
Changed |
| [#354](#354) | ci: extend
Python test matrix to include 3.13 and 3.14 | Added |
| [#355](#355) |
chore(docker): bump base image from `python:3.12-slim` to `3.13-slim` |
Changed |

All six merged via their own QA-Approved cycles — nothing in this
release bypasses the standard pipeline.

## What's unchanged

- `docker-compose.yaml` — uses `:latest`, no version bump needed
- `README.md` — tool count (32) and text-mode content unchanged; no
update needed
- `uv.lock` — no dep changes in any of the six PRs

## QA

Lightweight per project convention — all substantive code was tested in
its own PR. Review-only checks:

1. - [x] **`pyproject.toml` version** is `0.18.2`. Verify line 3:
`version = "0.18.2"`.
2. - [x] **CHANGELOG** — `[0.18.2] - 2026-04-21` heading exists; the six
rolled-up entries sit beneath it in their original order (Changed →
Added → Changed → Security → Security → Added); empty `[Unreleased]`
seeded above.
3. - [x] **Comparison links** — `[0.18.2]: …v0.18.1...v0.18.2` added;
`[Unreleased]` now points at `v0.18.2...HEAD`.
4. - [ ] **Scope** — `git diff --stat origin/main` shows exactly
`CHANGELOG.md` (+4, -1) and `pyproject.toml` (+1, -1). Nothing else.
5. - [x] **No accidental content drift in rolled-up entries** — diff
between this branch's `[0.18.2]` section and what was in `[Unreleased]`
on `main` before this PR should be zero beyond the heading/anchor move.

### Acceptance

- ✅ CI green
- ☐ Merge + tag (Dev authorization, executed post-merge)

## Merge + tag (Dev post-merge action)

After merge, Dev runs:

\`\`\`
git checkout main && git pull --ff-only origin main
git tag -a v0.18.2 -m "v0.18.2 — CI matrix widening (3.13/3.14),
Dockerfile to python:3.13-slim, docker-smoke workflow, compose host-port
parameterization, OAuth log redaction, workflow permission hardening"
git push origin v0.18.2
\`\`\`

The tag triggers \`docker-publish.yml\` to build and publish the
\`:v0.18.2\` + \`:latest\` images.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: cmeans-claude-dev[bot] <3223881+cmeans-claude-dev[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cmeans-claude-dev Bot added a commit that referenced this pull request Apr 22, 2026
#377)

Closes R2 of #359 (RLS harness coverage extension tracking). Closes
#362.

## Summary

Complements R1 (#372) and R3 (merged in #373) by covering the execution
contexts the request-path `rls_store` fixture doesn't reach: the
`_do_cleanup` daemon thread, the `upsert_embedding` pool path used by
`server._embedding_pool`, and Postgres's transaction-local `set_config`
semantics plus the combined pool/Postgres contract.

Test-only change — no production code modified.

**What the tests actually catch (honest framing, per QA round-1
feedback):** the background-thread and pool tests verify that
regressions dropping *all* of the layered cross-tenant defenses (SQL
`WHERE owner_id = %s`, `_set_rls_context`, and the pool role's
`BYPASSRLS` / fixture `NOBYPASSRLS` re-entry) are caught in aggregate.
They do **not** each isolate one layer — that's defense-in-depth doing
its job, and the module docstring now says so explicitly. The two
`test_set_config_is_local_…` tests are the ones that isolate a single
guarantee (Postgres's transaction-local `set_config` semantic) from
everything else, using a raw `psycopg.connect` with no pool involved.

## Scope

**2 files changed, +518, -0** relative to origin/main (`git diff
--shortstat origin/main` → `2 files changed, 518 insertions(+)`).

| File | ± | Purpose |
|---|---|---|
| `tests/test_rls_background.py` | +517, -0 (new) | 11 tests across 3
classes |
| `CHANGELOG.md` | +1, -0 | `### Security` bullet under `[Unreleased]`
(pure append to existing section) |

## Test inventory (11 tests, 3 classes)

### `TestRLSBackgroundCleanup` (4 tests)

1. `test_cleanup_isolates_expired_deletions_per_owner` — alice opts in,
bob does not; both have expired entries. After `_do_cleanup`, alice's
expired entries are gone, bob's remain. Exercises the full cleanup call
path (owner enumeration + per-owner DELETE) under a `NOBYPASSRLS`
request-path fixture.
2. `test_cleanup_skips_owners_without_preference` — cleanup is a no-op
for owners who haven't set `auto_cleanup=true`.
3. `test_cleanup_preserves_non_expired_entries_for_opted_in_owner` —
only rows with `expires <= now` are deleted; future-dated entries
survive.
4. `test_cleanup_expired_background_thread_preserves_isolation` — runs
cleanup through the spawned daemon thread (via `_cleanup_expired()`) and
verifies isolation holds. Exercises the real threaded path rather than
the synchronous call.

### `TestRLSBackgroundEmbedding` (2 tests)

5. `test_upsert_embedding_respects_owner_isolation` — alice's embedding
is not visible to bob via `get_entries_without_embeddings`. Covers the
full upsert call path including both the `WHERE owner_id = %s` SQL
filter and RLS policies.
6. `test_upsert_embedding_from_worker_thread_preserves_isolation` —
submits `upsert_embedding` via a `ThreadPoolExecutor`, same pattern as
`server._embedding_pool`.

### `TestRLSPoolGuarantees` (5 tests)

7.
**`test_set_config_is_local_true_does_not_persist_across_transactions`**
— direct Postgres check on a raw `psycopg.connect` (no pool):
`set_config(..., true)` is reverted at COMMIT. This is the test that
isolates the transaction-local semantic from `psycopg_pool`'s `RESET
ALL` check-in reset.
8. **`test_set_config_is_local_false_persists_across_transactions`** —
direct Postgres check counterpart: `set_config(..., false)` does persist
across transactions. Together with #7, these prove the `is_local` flag
is what's producing the behavior, not some ambient reset.
9. `test_pool_checkout_does_not_see_prior_rls_context` — after a store
operation, a fresh pool checkout sees no `app.current_user`. Verifies
the *combined* pool+Postgres contract, not either layer alone.
10. `test_rls_context_cleared_after_exception_rollback` — an exception
inside a store-style transaction + pool check-in cleanup combine to
leave no residue. Same combined-contract pattern as #9.
11. `test_concurrent_owners_do_not_cross_contaminate` — two threads on
different owners; each lands writes correctly and cannot see the other's
data. Verifies the full call path under real concurrency (the pool
physically hands out distinct connections per thread, which makes
`app.current_user`-based cross-contamination impossible on its own; this
test proves the broader call path is also clean).

## What changed vs. round-1 reviewed head `263250e2`

1. Replaced the round-1
`test_rls_context_does_not_persist_between_transactions` with **two new
direct-Postgres tests** (#7 and #8 above). Those two actually fail when
you flip `true` ↔ `false` in the test — meta-verified in-session before
pushing. The old test is now renamed
`test_pool_checkout_does_not_see_prior_rls_context` and its docstring is
honest about being a combined-contract check.
2. Softened the docstrings of the remaining combined-contract tests
(#10, #11) so they no longer claim to isolate a layer they don't
isolate.
3. Module docstring rewritten to describe the defense-in-depth design
accurately: cleanup and embedding are doubly scoped (SQL `WHERE owner_id
= %s` + `_set_rls_context`), pool tests split into two camps
(direct-Postgres isolation vs. combined-contract).
4. CHANGELOG bullet updated to reflect the new test count (11, not 9)
and to describe what the tests verify without overclaiming.

## Runtime cost

11 tests, ~2.6 s against the shared testcontainers Postgres. Full pytest
suite went from 989 (pre-R2) to **1000 passing** on this branch; 7
skipped unchanged.

## References

- Parent tracking: #359
- Closes: #362
- Prior R-series: #372 (R1, merged), #373 (R3, merged 2026-04-22)
- R4 (hypothesis fuzz): #364 — remaining sub-PR
- Round-1 QA review: flagged three inaccurate meta-verifications; this
revision addresses each one

## QA

### Prerequisites

Docker (testcontainers spins up Postgres). Same as any other pytest run.
The test file is self-contained; uses the shared `pg_dsn` fixture from
`conftest.py`.

### Manual tests

1. - [ ] **Run the new test file in isolation:** `python -m pytest
tests/test_rls_background.py -v`. Expected: `11 passed in ~2.6s`.

2. - [ ] **Meta-verify #7 is load-bearing** — flip `true` to `false` in
`test_set_config_is_local_true_does_not_persist_across_transactions`.
Re-run the test. Expected: **FAIL** with `AssertionError:
set_config(..., true) survived COMMIT: got 'alice'`. Revert after
verification. (Verified by Dev in-session on `e1708d7`.)

3. - [ ] **Meta-verify #8 is load-bearing** — flip `false` to `true` in
`test_set_config_is_local_false_persists_across_transactions`. Expected:
**FAIL** with `AssertionError: assert '' == 'alice'`. Revert. (Verified
by Dev in-session on `e1708d7`.)

4. - [ ] **Defense-in-depth framing (not a failure scenario).** Per QA
round-1 finding: a single-layer regression (e.g., commenting out only
`self._set_rls_context(...)` inside `cleanup_expired`) does **not**
cause tests #1#6 to fail — the redundant `WHERE owner_id = %s` in the
SQL files and the pool role's `BYPASSRLS` still enforce isolation on
their own. That's the point: the tests verify the *aggregate* contract,
not any individual layer. To cause a failure you'd need to drop both the
SQL filter and `_set_rls_context`.

5. - [ ] **Scope** — `git diff --stat origin/main` shows exactly
`CHANGELOG.md` (+1, -0) and `tests/test_rls_background.py` (+517, -0);
net: 2 files, +518, -0.

### Acceptance

- ☐ CI green on all matrix entries
- ✅ 1000/1000 tests passing locally (989 → 1000, +11 new)
- ✅ `ruff check`, `ruff format --check`, `mypy` all clean
- ✅ Single-concern: background-thread + pool coverage only
- ✅ Module docstring accurately describes what tests catch
(defense-in-depth aggregate, not single-layer)
- ✅ Two new tests (`test_set_config_is_local_true/false_…`) directly
codify Postgres's `is_local` semantic independently of `psycopg_pool` —
meta-verified to fail when the `is_local` flag is flipped
- ✅ Deferred refactor explicitly noted (shared helpers module —
follow-up)
- ✅ Bot commit identity verified (`272174644+cmeans-claude-dev[bot]`,
author + committer); push attributed to bot (`e1708d7`, verified via `gh
api repos/.../activity`)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: cmeans-claude-dev[bot] <272174644+cmeans-claude-dev[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant