The running app logs in as its own role (D58): SELECT/INSERT/UPDATE on the live tables, SELECT on history and the migration record. It owns nothing, so it can't change the schema or switch off a trigger; it has no DELETE (only Charon's purge deletes). The change-tracking function now runs as its owner (SECURITY DEFINER, fixed search_path), so it alone writes history - the app can't forge a version. Grants are alembic_utils entities (src/thoth/sql/grants.py), columns from the models, so autogenerate keeps them in step. tests/test_roles.py holds its own copy of the matrix (a new table fails until it's decided) and proves the refusals: history insert, delete, trigger switch-off, drop, function replace, create table. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MdS1Hfahkh27nfbtRVZ4CR
pyproject configuration, based on the generic configuration.