本卡屬 FR-181(母卡 FR-181 母案(同清單上一張)),卡 F-2,批 2。只改 evidence-agent。依賴:無。
Agent 這邊的核心:一條背景執行緒,註冊成功後就不停地打主系統 GET /agents/commands?wait=30——主系統有事立刻回一串指令、沒事 30 秒回 204,Agent 收到就馬上再打下一次,所以任何時刻都有一條請求掛在主系統那邊等。拿到指令逐條交給「既有的處理函式」(派工進既有佇列、重啟叫既有 ops.request_restart、升級叫既有 update_sync.trigger、設定變了叫既有 config_pull),不重寫邏輯,只是觸發點從「主系統打進來」換成「自己拉到」。連不上主系統就退避(5→10→20→40→60 秒)再試。另外兩件事一起做:①回覆主系統的小工具 reply_sender(之後 A-3 回答類指令要用),送不到進既有的 outbox 補送;②8443 上原本給主系統直推用的六個端點,加一個設定開關預設不掛(決策者裁「API 功能先關掉、程式留著怕未來要開」),8443 照起只剩 health。照 config_pull.py 的骨架寫,那支就是「背景拉東西」的現成範本。
new:guidant_agent/sync/app/command_pull.py(照 config_pull.py 的 WorkerComponent 骨架)——註冊成功後起背景執行緒無限迴圈 GET /agents/commands?wait=30(通行證+DPoP 同心跳);204 立刻再掛、2xx 逐條分派、連線錯退避 5→10→…→60 秒;分派表是模組內一張 dict[kind -> handler],本卡先接四個既有函式:task→cmd_tasks 的 _parse+task_queue.submit(把 _parse 從 route 檔抽到 app 層 task_intake.py,route 改呼叫它)、restart→ops.request_restart(mode, REASON_CLOUD, ACTOR_MAIN)、update→update_sync.trigger、config_changed→config_pull.request();其他九個 kind 先登記 no-op(A-3 填)。new:guidant_agent/sync/app/reply_sender.py——reply(request_id, status, result=None, reason=None, file=None) 打 POST /agents/replies/<id>,送不到走 event_sender 同一條 outbox(新 kind reply)。extend:registry.py 登記本元件(登記型檔,本卡 owner,一次把 A-3 要的 handler 槽也留好);data_plane_server.register_command_blueprints 加開關 cfg.data_plane_commands_enabled(預設 False)才掛六個 cmd_*(D7,本卡一併做,避免 C-2 再碰登記型檔)。🔴 升級中/重啟已排定拿到 task → 不進佇列、回 failed reason=upgrading(現況 409 語意搬過來)
repo:~/Projects/Billows/Audit-Manager/evidence-agent
改既有:guidant_agent/platform/registry.py
改既有:guidant_agent/sync/api/data_plane_server.py 61(register_command_blueprints 加開關)
改既有:guidant_agent/sync/api/cmd_tasks.py(_parse 抽到 app 層)
改既有:guidant_agent/sync/app/outbox.py(加 kind reply)
改既有:config/config.py(AgentAuthConfig 加 data_plane_commands_enabled 預設 False)
新開:guidant_agent/sync/app/command_pull.py
新開:guidant_agent/sync/app/task_intake.py
新開:guidant_agent/sync/app/reply_sender.py
新開:tests/sync/test_command_pull.py
pyproject.toml 暫改 path 指 F-1 的工作區(~/Projects/Jedicogy/module/jedi-python-package/.worktrees/<母卡號>/jedi-agent-protocol,沒有就指主資料夾),這個改動不 commit(git update-index --assume-unchanged pyproject.toml poetry.lock),回寫寫明;E-1 收回釘版。guidant_agent/sync/app/command_pull.py:照 config_pull.py 的 WorkerComponent(bind/start/stop/health);start 後背景執行緒 while not stopped: resp = GET COMMANDS_PATH?wait=30(通行證+DPoP 用 enroll.py 心跳同一支 client);204→立刻下一輪;200→CommandsResponse.from_dict 逐條 _dispatch(cmd);連線錯/5xx→backoff = min(backoff*2, 60) 起始 5;2xx 後 backoff 歸 5。health() 回 {last_poll_at, backoff, dispatched_total}。HANDLERS: dict[str, Callable[[Command], None]] 模組層定義;本卡填四個:KIND_TASK→task_intake.accept(cmd.payload);KIND_RESTART→ops.request_restart(cmd.payload["mode"], ops.REASON_CLOUD, ACTOR_MAIN)(RestartRefused 只記 log);KIND_UPDATE→update_sync.trigger(target, sha, size);KIND_CONFIG_CHANGED→config_pull.request()。其餘九個 kind 登記 _noop_until_a3(log info「未接線」),A-3 填。未知 kind→log warning 跳過。guidant_agent/sync/app/task_intake.py:把 cmd_tasks._parse(驗 uid 長度、payload 是 dict、uid 一致)與「升級中/重啟已排定→拒收」的判斷抽過來成 accept(payload) -> AcceptResult(ok, duplicate, reason);cmd_tasks.handle 改呼叫它(route 殼 ≤ 15 行)。長輪詢拿到 task 若 reason == "upgrading" → reply_sender.reply(request_id?, "failed", reason="upgrading")——注意 task 沒有 request_id,這種情況只記 log 不回覆(主系統單留 pending,下一輪再拿)。guidant_agent/sync/app/reply_sender.py:reply(request_id, status, result=None, reason=None, file_path=None) → POST REPLIES_PATH,有 file_path 走 multipart(欄位 file + reply JSON 字串);非 2xx 或連線錯→outbox.enqueue(kind="reply", …)(outbox.py 加 kind reply,補送時重放同一支)。🔴 outbox 裡還有沒補完的時新的也排隊(沿用 event_sender 的順序規則)。data_plane_server.register_command_blueprints:加 if not cfg.data_plane_commands_enabled: return(config/config.py AgentAuthConfig 加欄位,env AGENT_DATA_PLANE_COMMANDS_ENABLED 預設 false);docstring 改「預設不掛,FR-181 後主系統不直推」。registry.py 登記 command_pull 元件(放 config_pull 旁,順序在它之後)。tests/sync/test_command_pull.py:假 server(responses 或 httpx.MockTransport)——204 連續兩次間隔 < 100 ms;一條 task → task_intake.accept 被叫一次;未知 kind → warning 且其餘照分派;連線錯三次 → sleep 參數 5/10/20;升級旗標起 → accept 回 upgrading、佇列不變;開關 false → POST /tasks 404。INSERT compliance.agent_tasks(... status='pending') 一張 → Agent log 3 秒內出現 ack。回寫附 log 片段。pytest:假主系統回 204 → 立刻再掛(兩次請求間隔 < 100 ms);回一條 task → task_queue.submit 被叫一次且 ack 走既有路徑;回一條未知 kind → log warning、其餘照分派;連線錯三次 → 間隔 5/10/20 秒;升級旗標起時拿到 task → reply failed upgrading、佇列不變;data_plane_commands_enabled=False 時 8443 上 POST /tasks 回 404。本機對 DEV 主系統(F-3 工作區起的)實跑:主系統 DB 插一張 pending 單 → Agent 3 秒內 ack
~/Projects/Billows/Audit-Manager/compliance-manager-be/docs/features/FR-181-2611-agent-outbound-only-channel/design.md §3 決策定案(D1~D8)、§4 端到端流程兩張圖、§5 詳細設計本卡那節、§7 拆分表本卡那列;為什麼這樣定看 discussion.md