本卡屬 FR-090(母卡 CM-1682),第 7 棒:把最後三支還走舊式接法的套件(jedi-ai-bot、jedi-ai-dashboard、jedi-remote-agent)收進 core/plugins/。實作類,零行為變更,對外 URL 一字不改。建議 model:Sonnet 5,effort:medium——形狀已由 17 支現成範例定死,工作是機械搬遷加逐條實打驗證,不需要設計判斷;三支中 remote-agent 那支最重(adapter 多、有留在主專案的 binary 端點),做那支時放慢。
主專案接一支套件現在的標準是「三個地方」:pyproject 一行、core/plugins/<pkg>.py 一個檔(裡面固定三段:① 這支套件開的 port 主專案怎麼答、② 填表、③ 掛載)、core/plugins/__init__.py 的 PLUGINS 清單加一列。21 支套件裡有 18 支已經是這個形狀,剩下三支還是 FR-069 抽出時的舊形狀:接線寫在 api/ai/__init__.py、api/ai_dashboard/__init__.py、api/remote_agent/__init__.py 的 create_module() 裡,靠 config/app_modules.py 的 REGISTERED_APPS 字串驅動 main.py 的 blueprint 載入迴圈掛上去。這張卡把三支搬成標準形狀,搬完主專案側 21 支一模一樣,PM 頁「有三個零件的裝法還是舊式」那句就可以拿掉。
為什麼會漏到現在:CM-1689 那棒的範圍是當時已經走 PLUGINS 迴圈的 11 支,這三支走 REGISTERED_APPS 另一條路,不在那張卡的清單;CM-1700/1701/1726 各自順手收了 evidence-classification/license/detection 三支同形態的,這三支沒人順到。CM-1730 寫守衛掃 PLUGINS 對 seed 時才暴露,暫時用 _CAPABILITY_PACKAGES_OUTSIDE_PLUGINS 直讀三支的能力點清單兜住。
core/plugins/ 現有 17 個 .py(含 _host.py、__init__.py),沒有 ai_bot/ai_dashboard/remote_agent。config/app_modules.py:47 "ai"、:83 "ai_dashboard"、:98 "remote_agent" 三個註冊字串仍在,驅動 main.py:127 的 for app_name in REGISTERED_APPS 迴圈。api/ai/__init__.py(57 行):create_module() 內組 AiBotAdapters(history_store=infra/ai/redis_chat_history_store.py 的 RedisChatHistoryStore、current_user_id、auth_required、response_builder)+ AiBotConfig(api_key=os.getenv("ANTHROPIC_API_KEY")),回 create_blueprint()。RedisChatHistoryStore 全 repo 只有這一個 caller(AST 掃過),可以整支搬進 ① 段。api/ai_dashboard/__init__.py(117 行):已有 build_adapters()/build_config() 兩支獨立函式(形狀接近標準)+ _LicenseGuard(軸⑥ adapter)+ _lazy();build_config() 讀 di_containers/ai_dashboard/ai_dashboard_containers.py:45 的 _ai_api_keys()。無 infra adapter 要搬。api/remote_agent/__init__.py(99 行):create_module() 內定義 admin_required 三層守門(jwt → require_license("remote-agent-manage") → require_capability("remote-agent-manage.create"))、組 RemoteAgentAdapters(七個欄位,三個 port 走 infra/remote_agent/adapter/lazy.py 的 Lazy* 代理),另外自己在同一個 blueprint 上多掛一條主專案端點 Api(bp).add_resource(AgentFileDownloadRoute, "/agents/files/<string:uid>")(api/remote_agent/routes/agent_file_route.py,FR-058.7 的 binary 下載,刻意不進套件)。infra/remote_agent/adapter/lazy.py 三個 Lazy 代理只有 api/remote_agent/__init__.py 一個 caller(AST 掃過);infra/remote_agent/adapter/detection_lifecycle_listener.py、detection_task_payload_provider.py、infra/remote_agent/recon_query_adapter.py 三支是 di_containers/agent_task/ 與 di_containers/remote_agent/ 注入的真實作,別的層在用,留原地(§5.1 第 1 條例外)。di_containers/containers.py 有 10 處 remote_agent_container. 反向依賴(detection_tools/flow_control/oscal/upload_file/detection_orchestration 要直接用 domain service 挑機器),ai_dashboard_container 也在 containers.py:421。兩顆 container 都保留,本卡不動 DI(DI 瘦身是 CM-1696 的事)。register() 簽名都是標準的 register(app, adapters, config=None, schema_extensions=None, mount_api=True),內部自己 app.register_blueprint(bp),與 detection/license 那兩支一樣,不需要特殊掛法。api/routing.py 的 FROZEN_URLS:ai-bot 1 條(/api/1.0/ai-chatbot)、remote-agent 14 條、ai-dashboard 沒有 FROZEN_URLS 常數,URL 是 routing.py:13-14 的 HEALTH_URL/AUTO_GENERATE_URL 兩條。加上主專案自掛的 /api/1.0/agents/files/<uid> 共 18 條,搬完要逐條實打對照。test/test_module_boundaries.py:_CAPABILITY_EXEMPT_PACKAGES(:1376-1378)與 _CAPABILITY_PACKAGES_OUTSIDE_PLUGINS(:1385-1389)各列了這三支並寫明「未收進 core/plugins/」;_PORT_IMPL_ALLOWED_OUTSIDE_PLUGINS(:1167 infra/ai/redis_chat_history_store.py、:1171 infra/remote_agent/adapter/lazy.py)白名單有兩列是為這三支留的;_CONVERGED_PLUGIN_DIRS(:1139)記錄「舊落點不得長回來」。pyproject.toml:227 remote-agent、:231 ai-dashboard、:247 ai-bot),已指工作樹,不用再改。RUN_MODE=socketio)不跑 REGISTERED_APPS 迴圈但會跑 PLUGINS 迴圈——搬過去後三支在 socketio 模式也會被 mount()。detection/license/evidence-classification 三支同形態搬過去後就是這樣跑的,沒出事(它們的 route 掛在 app 上但 socketio 模式沒人打)。照那三支的做法,不要為 socketio 加條件。api/ai/__init__.py 整檔 → core/plugins/ai_bot.py(改完刪目錄)
api/ai_dashboard/__init__.py 整檔 → core/plugins/ai_dashboard.py(改完刪目錄)
api/remote_agent/__init__.py 接線段 → core/plugins/remote_agent.py(改完刪 __init__,routes/ 留)
api/remote_agent/routes/agent_file_route.py 留原地不動,由 core/plugins/remote_agent.py 的 mount() 掛
infra/ai/redis_chat_history_store.py → core/plugins/ai_bot.py ① 段(唯一 caller 是接線);搬完刪 infra/ai/
infra/remote_agent/adapter/lazy.py → core/plugins/remote_agent.py ① 段(唯一 caller 是接線);搬完刪該檔
config/app_modules.py:47,83,98 三個字串改成註解,寫明改由 core/plugins/<pkg>.py 掛(比照 :100-110 license 那段寫法)
core/plugins/__init__.py import 三支 + PLUGINS 加三列
test/test_module_boundaries.py:1139 _CONVERGED_PLUGIN_DIRS 加三列
test/test_module_boundaries.py:1162 _PORT_IMPL_ALLOWED_OUTSIDE_PLUGINS 拿掉 infra/ai/redis_chat_history_store.py 與 infra/remote_agent/adapter/lazy.py 兩列
test/test_module_boundaries.py:1376 _CAPABILITY_EXEMPT_PACKAGES 拿掉三列
test/test_module_boundaries.py:1385 _CAPABILITY_PACKAGES_OUTSIDE_PLUGINS 改成空 tuple(常數與 _declared_capabilities() 的讀取邏輯保留,讓下次再有這種漏網有地方登記)
scripts/deliverables/plugin_metrics.py HOST_WIRING_FILES 加三支(比照 :65 license 那列)