建議 model:Opus,effort:medium——動的是 session_scope 這條每個 @transaction 必經的路,錯了是靜默壞(查不到而不是報錯)。本卡屬 FR-094(母卡見兄弟卡段),.1 前置與套件的第 1 棒。無前置棒,但它是 CM-1559 與 .2c 的硬前置。
系統有兩支半夜自動跑、沒人登入的清理程式:03:10 掃全部客戶的任務執行紀錄找孤兒綁定並刪除;01:00 清超過 7 天的法規框架解析暫存工作。它們現在掃得到全部客戶的資料,靠的是 jedi-common session_scope() 裡「沒有使用者身分就當最高權限」這條規則——而這條規則正是 CM-1559 要收緊的洞。收緊之後這兩支會「安靜地什麼都掃不到」,不報錯、孤兒資料一直堆。所以要先給它們一個明確宣告的系統身分,收緊那條規則時才不會連帶弄斷。
首腦核對(2026-09-13):
core/scheduler.py:202 框架解析清理 tick、:386 孤兒清理 tick——兩處註解自己寫明「走系統 session_scope(無 user context → super-admin RLS bypass)」。jedi_common/session/database/db.py:100-104 is_super = (not user_ctx.tenant_id) or _session_paths_have_root(...);:115-119 elif is_pg: 無 context 直接 SET LOCAL app.is_super_admin = 't'。framework_parse_job_cleanup 與 job_binding_orphan_cleanup 當時判「不壞」是因為兩張表當時無 RLS——本案 .2c 要開,所以它們變成會壞的那類。CM-1559 建議的修法是「新增明確的系統身分進入點顯式宣告要繞 RLS——繞 RLS 應該是一個動作,不是一個預設」。is_system_context 於 jedi-common/jedi-iam:0 命中——目前沒有這個欄位,要新開;但 CM-1559 卡也預定要開同名欄位,本棒與 1559 必須用同一個欄位(見怎麼修①)。jedi_iam/infra/elevated_session.py:38 elevated_readonly_session()(唯讀、離開必 rollback、鎖單次查詢)——但排程要寫(軟刪/刪除),唯讀版不夠,見怎麼修②。~/Projects/Billows/Audit-Manager/compliance-manager-be/core/scheduler.py:195-230 _framework_parse_job_cleanup_tick(01:00,掃 oscal.framework_parse_jobs)
~/Projects/Billows/Audit-Manager/compliance-manager-be/core/scheduler.py:380-420 _job_binding_orphan_cleanup_tick(03:10,掃 compliance.job_executions)
~/Projects/Jedicogy/module/jedi-python-package/jedi-common/jedi_common/session/database/db.py:87-119 session_scope 內 SET LOCAL 段(is_super 判定+無 context 分支)
~/Projects/Jedicogy/module/jedi-python-package/jedi-common/jedi_common/session/auth/auth_context.py get_user_context/set_user_context 與 UserContextDTO(先查欄位)
~/Projects/Jedicogy/module/jedi-python-package/jedi-iam/jedi_iam/infra/elevated_session.py:38 elevated_readonly_session(唯讀樣板,參考不照抄)
is_system_context 欄位或 system principal,直接用它;若還沒,本棒在 UserContextDTO 開 is_system_context: bool = False(先 grep 確認沒有同義欄位),並在 db.py 的 is_super 判定加 user_ctx.is_system_context or ...——不動 not tenant_id 與 elif is_pg 那兩條(那是 1559 的活,本棒只加正向訊號不拆舊路)。做完回寫本卡與 1559 卡「欄位已開,1559 第二步請沿用」。system_context(name: str) context manager(落點與 elevated_session.py 同層或 jedi-common session/auth/,先查有無同義物):進入時 set_user_context(UserContextDTO(id=None, tenant_id=None, is_system_context=True, ...)),離開必還原原 context;並 logger.warning('system context entered: %s', name) 留痕(1559 收尾要求「fail-closed 分支要留 WARNING log」同精神——每次繞 RLS 都要有名字)。with system_context('framework_parse_job_cleanup'): / with system_context('job_binding_orphan_cleanup'): 包住 service 呼叫;把兩處「無 user context → super-admin RLS bypass」的施工日誌型註解收掉,換成一行「為什麼要顯式系統身分」。其餘 6 支排程(drive worker/license 到期/detection 逾時/webhook renewer/tamper/integrity)本棒不動——那是 1559 第二步的範圍,本棒只把 .2c 要開 RLS 的兩支先接好;但要在回寫時列出這 6 支的位置給 1559 棒。core/scheduler.py 加一個 tick 註冊點的斷言(或 test/test_scheduler_system_context.py)——「每支排程 tick 進 session_scope 時 get_user_context() 不得為 None」。先只對本棒改的兩支生效(白名單其餘 6 支,附 CM-1559 卡號),1559 收完再拿掉白名單。is_system_context=True 進 session_scope → app.is_super_admin 為 't';is_system_context=False+有 tenant 路徑非 root → 'f'。system_context() 離開後 get_user_context() 還原成進入前的值(含原本是 None 的情況)。with system_context(...) 拿掉 → 守衛④ 要紅;把 db.py 的 is_system_context 分支拿掉 → 第一個測試要紅。