Add a runtime shape guard for the duplicate-role-map check - #414
Merged
Conversation
The existing test walks the AST for ast.Dict literals, so a copy of _ROLE_PRIORITY written as a list of pairs, a dict comprehension, dict(zip(...)), an enum.IntEnum, or a runtime-assembled dict never produces an ast.Dict node and passes unnoticed. Add a second guard that imports every module under api, core, services, tasks, integrations, notifications, and schemas and judges the value each module-level name resolves to, not the statement that built it. core.security._ROLE_PRIORITY is exempted by object identity so a second object holding the same values still fails. Both guards are kept, and each is verified to work independently: five synthetic copies (one per evasion shape) are confirmed to slip past the AST guard and be caught by the runtime one, and a plain dict literal is confirmed to still be caught by the AST guard on its own. Closes #387
haksungjang
force-pushed
the
fix-role-priority-runtime-shape-guard-387
branch
from
September 6, 2026 16:10
ef260c9 to
3f6747f
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The test protecting
core.security._ROLE_PRIORITYfrom a second copy (test_role_priority_has_one_home.py) walks the AST forast.Dictliterals. A copy written as a list of(role, grade)pairs, a dict comprehension,dict(zip(...)), anenum.IntEnum, or a dict assembled by a helper function at runtime never produces anast.Dictnode, so none of those shapes trip it.This adds a second guard (
test_role_priority_runtime_shape_guard.py) that imports every module underapi,core,services,tasks,integrations,notifications, andschemas, and judges the value each module-level name resolves to after import rather than the statement that produced it.core.security._ROLE_PRIORITYis exempted by object identity (is), not by name or path, so a second object holding identical values still fails.Design notes
test_ast_guard_alone_misses_every_evasion_shapeproves the AST guard is blind to all five evasion shapes, andtest_runtime_guard_alone_is_not_needed_to_catch_a_plain_dict_literalproves the AST guard still catches a plain literal on its own.dict(zip(...)),enum.IntEnum, runtime-assembled dict) are built as synthetic modules and confirmed, by breaking, to trip the new guard (hardening rule feat(releases): surface the branch a snapshot came from, and filter by it #7).test_a_second_object_with_the_same_values_still_failsproves the exemption is identity-based, not value-based: adict(_ROLE_PRIORITY)copy is still flagged.core/readiness.py, confirmed the new guard failed with the expected message, then reverted it (not part of this diff) — see verification below.DATABASE_URL/REDIS_URL/SECRET_KEYset at all, so no skip-list entries were needed. The skip mechanism (SKIPPED_MODULES: dict[str, str]) is still in place, with a test enforcing every entry states a reason, for future modules that grow import-time side effects.Verification
pytest tests/unit/test_role_priority_runtime_shape_guard.py— 15 passedpytest tests/unit/test_role_priority_has_one_home.py tests/unit/test_closed_status_vocabulary.py— unaffected, still greenruff check/mypyon the new file — cleannode tools/em-dash/lint.mjs— clean_ROLE_ORDER_DUPLICATE_FOR_MUTATION_TEST = {"viewer": 1, "developer": 2}intocore/readiness.pymakestest_role_priority_has_no_lookalike_anywhere_elsefail with the expected message; reverted before committing.pytest tests/unit(full suite, local, against a Postgres/Redis instance shared with other concurrent agents) — 3 pre-existing failures intest_admin_user_service.py/test_dashboard_service.pyunrelated to this change (row-count assertions sensitive to concurrent writes from other agents' test runs); all 3 pass in isolation, confirming shared-DB contention rather than a regression from this change.Closes #387