Skip to content

feat(citycore): Phase A hardening — Postgres default, write authz, audit, backup/restore (v0.4.0) - #9

Merged
scottconverse merged 2 commits into
mainfrom
feature/citycore-hardening
Jun 28, 2026
Merged

feat(citycore): Phase A hardening — Postgres default, write authz, audit, backup/restore (v0.4.0)#9
scottconverse merged 2 commits into
mainfrom
feature/citycore-hardening

Conversation

@scottconverse

Copy link
Copy Markdown
Contributor

Phase A of the CivicAccess → city-core integration plan (CivicSuite/civicsuite docs/roadmap/civicaccess-citycore-integration/). Module-repo work only — no umbrella/desktop/installer changes (those are Phases B–D). Mirrors the shipped CivicNotice no-AI city-core pattern.

Closes probe gaps (see PROBE-PROGRESS.md)

Persistence

  • Default to the shared CivicCore PostgreSQL: read supervisor DATABASE_URL (asyncpg) → derive sync psycopg2 URL. CIVICACCESS_REVIEW_DB_URL overrides; SQLite is now an explicit dev fallback.
  • psycopg2-binary moved to a runtime dependency. Migration id → civicaccess-windows-local-state-v1.
  • Mandatory Postgres release gate: verify-release.sh + CI require CIVICACCESS_POSTGRES_TEST_URL (postgres:16 service) so PG coverage can't be skipped.

Verification

  • Full verify-release.sh PASSED locally against a real PostgreSQL (34 passed incl. the PG persistence test; docs gate, placeholder gate, ruff, 0.4.0 wheel+sdist build).
  • Version bumped 0.3.0 → 0.4.0 across code, docs, and the gate. Pin to civiccore v1.2.0 wheel+SHA unchanged; not v1.0.0.

🤖 Generated with Claude Code

scottconverse and others added 2 commits June 28, 2026 15:46
…dit, backup/restore (v0.4.0)

Bring CivicAccess to city-core readiness in its own repo by mirroring the shipped
CivicNotice no-AI pattern. Closes probe gaps #2 (authz), #3 (audit), #4 (backup/restore)
and defaults persistence to the shared CivicCore PostgreSQL. Cuts v0.4.0.

Persistence (#Postgres default):
- Read the supervisor's DATABASE_URL and derive a sync psycopg2 URL (_sync_database_url);
  CIVICACCESS_REVIEW_DB_URL overrides; SQLite is now an explicit dev fallback, not the default.
- Move psycopg2-binary to a runtime dependency.
- Rename the migration id to civicaccess-windows-local-state-v1.
- Mandatory Postgres release gate: verify-release.sh + CI require CIVICACCESS_POSTGRES_TEST_URL
  (postgres:16 service); tests/test_postgres_persistence.py.

Authz (#2):
- Trusted-write guard on POST /review and POST /reviews/{id}/records-export
  (CIVICACCESS_TRUSTED_WRITE_TOKEN + X-CivicAccess-Write-Token): 403 missing/invalid, 503 fail-closed.
- New stateless public POST /analyze (no persistence, no token); public page uses it so the
  public surface can no longer write city records.
- Staff page carries the server-rendered token and sends it on save/export.

Audit (#3):
- audit_events table + record_audit_event; review.create written in the same transaction as the
  review, review.records_export on export.

Backup/restore (#4):
- Round-trip test proving review + audit data survive a Data-directory backup and restore.

Version bumped 0.3.0 -> 0.4.0 across code, docs, and the release gate. PROBE-PROGRESS.md added.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…obustness, coverage, honesty)

Adversarial verification panel (4 lenses) found 0 Blocker / 1 Critical / 2 Major / 6 Minor.
All resolved:

Critical — trusted-write secret was embedded in the staff page served over an unauthenticated GET:
- Removed server-rendered token from render_staff_page; the operator now pastes the token into a
  field kept in sessionStorage and sent only as X-CivicAccess-Write-Token on save/export.
- Added test_staff_page_never_leaks_the_write_token (asserts the env secret never appears in the page).

Major — Postgres-default selection and the async->sync conversion had zero coverage:
- Added tests/test_database_url_selection.py: _sync_database_url scheme flip + credential/marker
  preservation + sqlite passthrough; _review_database_url precedence (override > DATABASE_URL > SQLite).

Major — backup/restore proof only covered the SQLite dev fallback, not the Postgres default store:
- Added test_postgres_review_and_audit_survive_reconnect (default-store durability across a fresh
  engine); scoped/reworded the SQLite test (backup taken while live) and the gap #4 evidence.

Minor:
- Constant-time token comparison (hmac.compare_digest).
- _sync_database_url now rewrites only the URL scheme via sqlalchemy make_url (robust to passwords/
  db names containing scheme-marker substrings).
- CHANGELOG no longer overclaims the records_export audit is in the review transaction.
- Removed the discredited "v1.0.0" label from four module docstrings; verify-release.sh now greps
  civicaccess/*.py to keep it out.

Full verify-release.sh PASSED against real Postgres: 45 passed, docs/placeholder/ruff gates,
0.4.0 wheel+sdist build.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@scottconverse
scottconverse merged commit 7b24516 into main Jun 28, 2026
2 checks passed
@scottconverse
scottconverse deleted the feature/citycore-hardening branch June 28, 2026 22:00
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