fix(members): offload deny-path SEL audits off the event loop (#8523) - #8604
Conversation
Design Review (Fable 5) — ✅ PASSDesign-level review of Design-Verdict: PASS Copies the repo's established offload-with-guard audit shape onto four missed deny sites; convention-consistent, minimal, and regression-pinned by an AST ratchet. Suggestions
[DESIGN-REVIEWED] 61ca6e3 |
Opus 4.8 Review — ✅ no blocking findingsReviewed Verdict parsed from the review's SHA-scoped output markers for commit False positive or not applicable? A repository writer can comment: |
GPT 5.6 Review — ✅ no blocking findingsGPT 5.6 completed its review of This comment is updated in place on each push. Review detailsNo findings. False positive or not applicable? A repository writer can comment: |
First Principles Review (Fable 5) — ✅ PASSPremise-level review of All the evidence is in. The fix matches verified in-repo precedents ( First-Principles-Verdict: PASS Every item is the fix or its guard, matches a recorded repo convention, and adds zero public surface — the only excess is three byte-identical inline blocks. What this change shipsIntent: stop deny-path audits in the members endpoints from freezing the gateway on a fresh start — a FIX.
No config key, flag, or exported symbol is added; WatchThe declared "Pattern harvest" understates the scale: grepping SubtractionsFold the three member-pin blocks in [FIRST-PRINCIPLES-REVIEWED] 61ca6e3 |
|
Disposition — ACCEPT-AND-DEFER (advisory CONCERNS; the cause-level fix is extracted to issue #8608).
The premise holds as stated: What this PR does not do, deliberately, is make that startup change here. It fixes the four sites #8523 names, in the shape the module's 14 in-repo precedents already use; moving SEL's first-touch init onto the gateway boot path is precisely what the AUTOSDE
|
_deny_app_caller and the three member-pin denial sites in dashboard/handlers/members.py called _sel().log_api_access(...) synchronously from async handlers. On a fresh gateway the first _sel() touch performs synchronous filesystem initialization (HMAC key load/create, chain-head read) on the event loop, stalling every gateway task (AUTOSDE no-blocking-call-on-event-loop). Each site now offloads the whole call -- the _sel() accessor INCLUDED -- via await asyncio.to_thread(lambda: ...), the shape _shared.py's non-owner denial and autonudge's authorization audit already use. The helper's fast allow path (no app token) stays on-loop with no thread hop. Tests: thread-ident assertions for the app-denial helper and a member-pin site, a real first-touch SEL initialization exercised through the deny path (fails if the accessor is hoisted out of the lambda), and an AST guard pinning every _sel().log_api_access in the module inside a to_thread lambda. Closes #8523
0307213 to
61ca6e3
Compare
Closes #8523
Problem
_deny_app_callerand the three member-pin denial sites insrc/kiro_crew/dashboard/handlers/members.pycalled_sel().log_api_access(...)synchronously from async handlers. On a fresh gateway the first_sel()touch performs synchronous filesystem initialization — HMAC key load/create and the chain-head read — ON the event loop, stalling every gateway task (AUTOSDEno-blocking-call-on-event-loop).Fix
Every deny-path audit in the module now offloads the whole call — the
_sel()accessor INCLUDED — viaawait asyncio.to_thread(lambda: ...), the shape_shared.py's non-owner denial andautonudge.py's authorization audit already use, and each site is wrapped in the sameexcept Exceptionguard those precedents carry ("audit must never change the outcome"): an executor shut down mid-teardown or an audit write failure degrades to a debug log, never to a 500 replacing the 404/409. The helper's fast allow path (no app token) stays on-loop with no thread hop._deny_app_callerbecameasync; its three call sites await it. No status code, body, or ordering of any response changed: every new await sits immediately before itsreturn, with no state read across the suspension point.Tests
TestDenialAuditOffload::test_app_denial_audit_runs_off_the_event_loop— thread-ident assertion on the app-token denial (helper site), plus kwargs contract.TestDenialAuditOffload::test_member_pin_denial_audit_runs_off_the_event_loop— same for the pin-mismatch denial (409 path), and asserts no slot was minted.TestDenialAuditOffload::test_first_sel_touch_initializes_off_the_event_loop— a realSecurityEventLogfirst touch (HMAC key create verified on disk) driven through the deny path, recording the thread_init_lockedruns on; catches the accessor being hoisted out of the lambda. Displaces then RESTORES the prior process singleton per thesel_private_rootconvention (the fixture itself would pre-consume the first touch).TestDenialAuditOffload::test_every_members_sel_audit_is_offloaded— AST guard: everylog_api_accesscall in the module, regardless of receiver spelling, must sit inside anasyncio.to_threadlambda.Mutation-verified: reverting each of the four sites to a sync call, and hoisting the accessor out of the lambda, are each caught by a distinct test. Local gates green (isort 6.0.0 / flake8 7.1.0 / mypy 1.14.1 / black 26.3.1 CI pins, black-baseline, brand, subprocess-encoding, harness parity). Full pytest: 86341 passed; the 89 failures reproduce identically on clean main (host-environment:
/local/homeuid ownership, AF_UNIX path length, userns EPERM) — none touch this diff.Pre-push review lanes: GPT (gpt-5.6-sol) 1 BLOCKING (singleton restore-to-None in the first-touch test) and Opus (claude-opus-5) 1 BLOCKING + 3 CONCERNS (black gate offender, same singleton restore, missing audit exception guard vs the three in-repo precedents, AST guard pinning the spelling rather than the operation) — all five verified real on-source and fixed in this commit.
Pattern harvest
Class: deny-path audit added before the offload convention landed keeps running on-loop while new success-path audits in sibling files offload.
Rule candidate: any
log_api_accessreachable from an async handler must go throughasyncio.to_thread/run_in_executorwith the SEL accessor inside the offloaded callable, wrapped so an audit failure never changes the response.