本卡屬 FR-181(母卡 FR-181 母案(同清單上一張)),卡 F-3,批 2。只改 jedi-python-package。依賴:無。
主系統這邊收長輪詢的端點,放在 jedi-remote-agent 套件裡(Agent 相關端點都在那)。GET /agents/commands:驗 Agent 身分(跟心跳同一套)→ 查「這台現在該做什麼」(沒人領的工單、要不要重啟、要不要升級、待回答的請求)→ 有就立刻回、沒有就吊著等鈴響(最多 30 秒)再查一次 → 一次最多 20 條。同一台 Agent 第二條請求進來,就讓第一條先回 204(避免 Agent 網路抖動重連堆兩條)。另外兩個端點:POST /agents/replies/<id> 收 Agent 的回覆(JSON 或帶檔案的 multipart)、GET /agents/files/<id> 讓 Agent 來拿主系統暫存的檔。🔴 套件不認識 Redis、不建表:「吊著等鈴」「存暫存檔」「查有什麼待辦」「收到回覆怎麼處理」四件事定義成四個 port(介面),主系統那邊實作(A-1)再注入進來;套件自己附一個 threading.Event 版的 waiter 給測試用。這張卡是 routing.py 的 owner,三條端點一次掛齊。做完發版 1.13.0。
new:jedi_remote_agent/api/routes/agent_commands_route.py——AgentCommandsRoute.get:守門同 AgentHeartbeatRoute(AUTH_AGENT);組裝「這台現在該做的事」:pending 且 _due 的工單(搬 AgentCommandService.redispatch_pending 的查單邏輯,推送部分不搬)、restart_requested 有值→restart、update_for_agent 有值→update(這兩個由宿主透過 command_sources port 注入 callable,套件不認識 rollout)、宿主注入的 agent_requests 未過期 waiting 列→各 kind;有東西立刻回、沒有就呼叫注入的 waiter.wait(agent_uid, timeout=wait)(port,宿主用 Redis 實作;套件內建一個 threading.Event 版給測試)後再查一次;一次最多 20 條;同一台第二條進來就讓第一條回 204(D2 ③,waiter.preempt(agent_uid))。AgentRepliesRoute.post:status taken|done|failed、JSON 或 multipart(檔案落宿主注入的 spool.save(request_id, stream))、呼叫注入的 on_reply(request_id, status, result)。AgentFilesRoute.get:宿主注入的 spool.open(request_id) 串出去。extend:routing.py 一次掛三條(登記型檔 owner);remote_agent_service 加 mark_restart_dispatched(uid)(Agent 拿到 restart 就清旗標,不然每條長輪詢都回 restart)。禁:套件內不 import redis、不建表——agent_requests 的讀寫全走 port
repo:~/Projects/Jedicogy/module/jedi-python-package
改既有:jedi-remote-agent/jedi_remote_agent/api/routing.py
改既有:jedi-remote-agent/jedi_remote_agent/app/service/remote_agent_service.py
改既有:jedi-remote-agent/jedi_remote_agent/app/service/agent_task_service.py(list_pending_for_agent 加 due 過濾)
改既有:jedi-remote-agent/pyproject.toml(1.13.0)
新開:jedi-remote-agent/jedi_remote_agent/api/routes/agent_commands_route.py
新開:jedi-remote-agent/jedi_remote_agent/app/ports/command_channel.py(waiter/spool/command_sources/on_reply 四個 port 的 Protocol)
新開:jedi-remote-agent/tests/test_agent_commands_route.py
pyproject.toml 暫 path 指 F-1 工作區,不 commit(assume-unchanged),回寫寫明。jedi_remote_agent/app/ports/command_channel.py:四個 Protocol——CommandWaiter(wait(agent_uid, timeout) -> bool、notify(agent_uid)、preempt(agent_uid))、FileSpool(save(request_id, stream) -> Path、open(request_id) -> IO、discard(request_id))、CommandSources(restart_for(agent) -> str|None、update_for(agent) -> dict|None、requests_for(agent) -> list[Command])、ReplySink(on_reply(request_id, status, result, file_path=None))。附 EventCommandWaiter(threading.Event per uid,給測試與無 Redis 的 dev)。api/routes/agent_commands_route.py:AgentCommandsRoute.get——wait = min(int(args.get(WAIT_PARAM, 0)), WAIT_MAX_SEC);waiter.preempt(uid);cmds = _collect(agent);空且 wait>0 → waiter.wait(uid, wait) 後再 _collect;仍空→204;否則 200 CommandsResponse(cmds[:COMMANDS_LIMIT])。_collect:pending 且 due 的工單(把 AgentCommandService.redispatch_pending 的查單搬過來:agent_task_service.list_pending_for_agent 加 due_only=True)→ Command(KIND_TASK, task_uid=…, payload=payload_builder(...))(payload_builder 由宿主注入,現況 DetectionTaskPayloadProvider);sources.restart_for → Command(KIND_RESTART, {mode}) 並 remote_agent_service.mark_restart_dispatched(uid)(清 restart_requested);sources.update_for → KIND_UPDATE;sources.requests_for 逐條。守門同 AgentHeartbeatRoute(AUTH_AGENT)。🔴 吊著期間不可持有 DB session(wait 前結束交易、醒來再開)。AgentRepliesRoute.post:request_id 路徑參數;Content-Type 是 multipart → spool.save(request_id, request.files["file"].stream)、Reply.from_dict(json.loads(form["reply"]));否則 JSON;sink.on_reply(request_id, reply.status, reply.result, file_path);回 200 {accepted: true}。AgentFilesRoute.get:spool.open(request_id) → send_file 串流,讀完 discard(宿主在 spool 實作裡做)。routing.py:一次掛 ("/agents/commands", AgentCommandsRoute, AUTH_AGENT)、("/agents/replies/<string:request_id>", …)、("/agents/files/<string:request_id>", …) 三條,AUTH_AGENT 清單(133 行附近)同步加。登記型檔 owner。mount() 的參數)加四個 port 與 payload_builder 的注入位;缺 waiter 時用 EventCommandWaiter;缺其他 port 時端點回 503 並 log(宿主沒接線)。tests/test_agent_commands_route.py(Event 版 waiter):無待辦 wait=1 → 1 秒後 204;吊著時另一執行緒 notify → 100 ms 內 200;同台第二條 → 第一條 204;pending 單+restart+一筆 request → 三條、kind 正確、restart_requested 被清;過期 request 不回;replies multipart → spool.save 被叫、on_reply 收到 file_path;routing.py 三條都在 AUTH_AGENT。1.13.0、全部 tests、poetry publish -r nexus,回寫版號、四個 port 的簽名(E-1/A-1 照這個接)。pytest(Event 版 waiter):無待辦 wait=1 → 1 秒後 204;吊著時 waiter.notify(uid) → 100 ms 內回 200 帶 commands;同台第二條進來 → 第一條 204、第二條吊著;pending 單 + restart + 一筆 waiting request → 回三條且 kind 正確;過期 request 不回;replies multipart → spool.save 被叫;routing.py 三條都在 AUTH_AGENT 清單。發版 1.13.0 推 Nexus,回寫版號與 port 介面
~/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