FR-181 · 需求討論稿 v1 · 2026-11-07(D1~D8 已裁,定案在 design.md)
直推 8443 退役;Agent 用長輪詢向主系統取指令,同步問答(測試連線、證據檔、查日誌)也走同一條;心跳維持只表存活
決策者 11-07 確認:客戶網路不可能讓主系統連進 Agent。FR-171(11-02 合回)把派工、重啟、升級、取消、換證、設定通知全改成主系統直推 Agent 8443,而證據檔與遠端查日誌從 FR-039 起就是直推——在這種客戶,主系統叫 Agent 做事的功能一個都送不到。本稿提出:主系統不再主動連 Agent,Agent 用長輪詢(一個請求吊著最多 30 秒,有事立刻回)向主系統取指令,八種用途全部改走這條;直推整個退役,不留兩套。這不是回到 7 月的「心跳夾帶工單」——心跳維持 11-01 裁定的只表存活,指令是獨立端點。
整案一句話:主系統不再連 Agent;Agent 開一條長輪詢去主系統問「有事嗎」,有事主系統立刻回、Agent 做完把結果送回來;原本八種「主系統連 Agent」的用途全部改走這條;直推與 Agent 8443 的指令端點退役。
| 階段 | 做什麼 | 產出 | 完成怎麼判定(決策者檢查法) |
|---|---|---|---|
| P1 地基 | 契約定「取指令」與「回結果」的形狀;主系統開 GET /agents/commands 長輪詢端點與「等回答插槽」;Agent 開長輪詢背景元件 |
jedi-agent-protocol 0.3.0、jedi-remote-agent、Agent command_pull.py |
在 123 Agent 主機把防火牆設成只准出站(iptables 擋 8443 入站),主系統派一張掃描單,Agent 主控台 3 秒內看到開跑 |
| P2 指令類搬家 | 派工、重啟、升級、取消檢測、立即換證、設定變更通知六種改成從長輪詢回應觸發 | 主系統側 _agent_commands.py 改寫、Agent 側六個 handler 改接 |
同上防火牆設定,管理頁按重啟/升級/取消/立即換證/改授權清冊,Agent 各在 3 秒內反應 |
| P3 同步問答搬家 | 測試連線、工具帳密、證據檔上傳下載轉 PDF、遠端查日誌改成「排請求 → Agent 取走做 → 結果回來 → 喚醒等著的瀏覽器請求」 | 等回答插槽接上四種用途 | 同上防火牆設定,按「測試連線」2 秒內看到結果;證據存 Agent 側的租戶上傳一支檔、預覽、下載都正常;支援頁查 Agent 日誌查得到 |
| P4 退役 | 刪 AgentDataPlaneClient 直推路徑、Agent 8443 的六個指令 blueprint、last_command_status、FE「連線」欄的「僅單向」狀態 |
守衛測試:主系統程式碼沒有任何地方連 Agent 8443 | grep -rn "8443|data_plane_client|AgentDataPlaneClient" 主系統與套件為 0(8444 瀏覽器直傳除外);Agent 8443 只剩 /health、/agent-info |
依賴一句:P1 是全部的前提;P2 與 P3 可平行;P4 等 P2、P3 全部驗完才做。決策者功課:D1~D8。
| 日期 | 事件 | 主系統 → Agent 的連線方向 |
|---|---|---|
| 2026-07-26 | FR-056 T-3.2:心跳回應夾帶 pending_tasks,Agent 每 60 秒來拿 |
只有 Agent 出站 |
| 2026-06~10 | FR-039 證據檔 Agent、FR-056.5 測試連線、FR-058 工具帳密:主系統直連 Agent 8443 | 檔案與同步問答要主系統連進來;指令仍出站 |
| 2026-10-29 | FR-170 §3.x 決策者裁:「落地版主系統與 Agent 同在客戶內網,直推一定通;SaaS 版才需要 WebSocket」 | 以「同內網一定通」為前提 |
| 2026-11-01 | 決策者裁「心跳不再帶任何訊息」 | — |
| 2026-11-02 | FR-171 E-0b(844b273c2):指令全改直推 8443,心跳指令鍵拔掉、套件加守衛「宿主不得借 extras 加回來」 |
六種指令全部要主系統連進來 |
| 2026-11-07 | 決策者確認:客戶網路不可能讓主系統連進 Agent | 前提推翻 |
結論:FR-171 把一個本來在單向網路能跑的機制,換成了不能跑的。而且 FR-039 以來的檔案與同步問答路徑,從來沒在單向網路跑過。
主系統主動連 Agent 8443 的用途,開檔逐支盤出來共八種:
| # | 用途 | 主系統側出口 | Agent 側入口 | 性質 |
|---|---|---|---|---|
| 1 | 派工 | core/plugins/_agent_commands.py → AgentCommandService.dispatch_new_tasks |
sync/api/cmd_tasks.py POST /tasks |
指令,主系統有正本(agent_tasks pending) |
| 2 | 重啟 | push_restart_after |
cmd_restart.py POST /restart |
指令,有正本(restart_requested) |
| 3 | 升級 | push_update_soon |
cmd_update.py POST /update |
指令,有正本(rollout 表) |
| 4 | 設定變更通知 | _agent_config_notify.py |
cmd_config.py POST /config/notify |
通知,Agent 本來就會拉 GET /agents/config |
| 5 | 立即換證 | _agent_keys.py:118 直接 send |
cmd_credentials.py POST /credentials/renew |
指令,沒有正本(只發不記) |
| 6 | 取消檢測 | jedi-detection agent_cancel_client.py |
scan/api POST /detection/cancel |
指令,沒有正本 |
| 7 | 測試連線、工具帳密 | jedi-detection agent_probe_client.py、agent_credential_client.py |
/detection/probe、/detection/credentials |
同步問答:管理員按鈕要當下看到結果 |
| 8 | 證據檔上傳/下載/轉 PDF、遠端查日誌 | infra/upload_file/remote_agent_adapter.py、FR-178 /logs |
files/api/blob.py、sync/api/cmd_logs.py |
同步資料往返:瀏覽器要當下拿到 bytes |
1~6 改成「Agent 定時來拉」就通;7、8 要「主系統當下拿到回答」,固定間隔的輪詢不夠——這就是為什麼本稿提長輪詢而不是定時輪詢(§3)。
FR-171 D3 設計「直推打不通就等心跳回來補推」,很多人以為這代表單向網路可以用。實查 AgentCommandService.redispatch_pending():補推走的是同一支 AgentDataPlaneClient.command(),同樣連 Agent 8443。單向網路下每一跳的補推都失敗,單永遠 pending,管理頁「連線」欄永遠顯示「僅單向」。設計當時給的解法是 hover 提示「請確認 Agent 主機的 8443 埠對主系統開放」——也就是把問題交給客戶開埠,而客戶不會開。
| 做法 | 怎麼運作 | 7、8 同步問答 | 新元件 | 判斷 |
|---|---|---|---|---|
| A 固定間隔輪詢 | Agent 每 N 秒問一次 | ❌ 最壞等 N 秒,使用者早以為壞了 | 無 | 只解 1~6,決策者已裁 7、8 也要通,不採 |
| B 長輪詢 | Agent 打 GET /agents/commands,主系統沒事就吊著最多 30 秒、有事立刻回;Agent 處理完馬上再打下一個 |
✅ 1 秒內送達 | 主系統一個「等回答插槽」(Redis pub/sub) | 採用。HTTP 一條請求、穿 proxy 沒問題、Flask/gunicorn 直接撐、Agent 側 config_pull.py 骨架可延伸 |
| C WebSocket 出站長連線 | Agent 連出來掛著,主系統往下推 | ✅ | BE 要多一個連線管理元件(FR-170 列第二期)、Agent 要自建保活與重連、穿 proxy 常斷 | 比 B 多一點即時、多很多元件;B 能做到 C 八成功能,不採 |
| D 回到 7 月的心跳夾帶 | 把 pending_tasks 塞回心跳回應 |
❌ 同 A | 無 | 違反 11-01 裁示「心跳只表存活」,且當時 8 KB 全量重送的病會回來,不採 |
| E MQTT/message broker | 客戶機上多跑一個 broker | ✅ | broker 行程、身分體系另接 | 沒有 IT 的客戶多一個會壞的東西,不採(FR-171 §3.1 已排除) |
業界對照:Datadog、CrowdStrike、Tailscale 的 agent 全部只出站;GitHub Actions self-hosted runner 就是長輪詢(runner 打 GitHub 問「有 job 嗎」,吊 50 秒);Chef/Puppet 定時 pull。長輪詢是「客戶不開埠」這個前提下最少元件的正規做法。
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E2F0F1','primaryTextColor':'#14201F','primaryBorderColor':'#0E7C86','secondaryColor':'#EEF2F3','secondaryTextColor':'#14201F','tertiaryColor':'#FBFCFC','tertiaryTextColor':'#14201F','lineColor':'#4A5A5C','textColor':'#14201F','mainBkg':'#E2F0F1','nodeBorder':'#0E7C86','nodeTextColor':'#14201F','edgeLabelBackground':'#FBFCFC','titleColor':'#14201F','clusterBkg':'#FBFCFC','clusterBorder':'#E4EAEB','actorBkg':'#E2F0F1','actorTextColor':'#14201F','actorBorder':'#0E7C86','actorLineColor':'#C7D1D2','signalColor':'#4A5A5C','signalTextColor':'#14201F','labelBoxBkgColor':'#EEF2F3','labelBoxBorderColor':'#C7D1D2','labelTextColor':'#14201F','loopTextColor':'#14201F','noteBkgColor':'#F6EBD5','noteTextColor':'#14201F','noteBorderColor':'#9C6B12','activationBkgColor':'#EEF2F3','activationBorderColor':'#C7D1D2','sequenceNumberColor':'#FFFFFF'}}}%%
sequenceDiagram
autonumber
participant A as Agent(長輪詢元件)
participant M as 主系統 API
participant Q as 指令佇列(DB 正本+Redis 喚醒)
participant U as 管理員瀏覽器
A->>M: GET /agents/commands?wait=30(通行證+DPoP)
M->>Q: 查這台有沒有待辦
Note over M: 沒有 → 訂閱 Redis 頻道 agent:<uid>,吊著
U->>M: 按「派工」(或重啟/升級/取消/換證/改設定)
M->>Q: 寫 DB 正本(工單 pending/restart_requested/…)+ publish agent:<uid>
Q-->>M: 喚醒
M-->>A: 200 {commands:[{kind:"task", task_uid, payload}, …]}
Note over A: 逐條交給既有 handler(cmd_tasks/cmd_restart/…的 app 層函式)
A->>M: POST /agents/tasks/<uid>/ack(既有)
M->>Q: status=dispatched
A->>M: GET /agents/commands?wait=30(立刻再掛一條)
Note over M,A: 30 秒沒事 → 204,Agent 立刻再掛
正本仍在主系統 DB(FR-171 D3 的原則不變):工單表、restart_requested、rollout 表。Redis 只是「有新東西,醒來查表」的鈴,鈴沒響(Redis 重啟、publish 漏了)最壞 30 秒逾時後 Agent 再掛一條時查表也會查到。沒有第二個佇列。
三道防撞沿用 FR-171:task uid 冪等、ack 即 dispatched、Agent 本機單執行緒佇列。長輪詢只有一個觸發點(不像直推+補推兩個),反而更簡單。
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E2F0F1','primaryTextColor':'#14201F','primaryBorderColor':'#0E7C86','secondaryColor':'#EEF2F3','secondaryTextColor':'#14201F','tertiaryColor':'#FBFCFC','tertiaryTextColor':'#14201F','lineColor':'#4A5A5C','textColor':'#14201F','mainBkg':'#E2F0F1','nodeBorder':'#0E7C86','nodeTextColor':'#14201F','edgeLabelBackground':'#FBFCFC','titleColor':'#14201F','clusterBkg':'#FBFCFC','clusterBorder':'#E4EAEB','actorBkg':'#E2F0F1','actorTextColor':'#14201F','actorBorder':'#0E7C86','actorLineColor':'#C7D1D2','signalColor':'#4A5A5C','signalTextColor':'#14201F','labelBoxBkgColor':'#EEF2F3','labelBoxBorderColor':'#C7D1D2','labelTextColor':'#14201F','loopTextColor':'#14201F','noteBkgColor':'#F6EBD5','noteTextColor':'#14201F','noteBorderColor':'#9C6B12','activationBkgColor':'#EEF2F3','activationBorderColor':'#C7D1D2','sequenceNumberColor':'#FFFFFF'}}}%%
sequenceDiagram
autonumber
participant U as 管理員瀏覽器
participant M as 主系統 API(worker X)
participant R as Redis
participant M2 as 主系統 API(worker Y)
participant A as Agent
U->>M: POST 測試連線 {agent_uid, target}
M->>M: 寫 agent_requests(kind=probe, status=waiting, request_id)
M->>R: publish agent:<uid>;subscribe reply:<request_id>
Note over M: 吊著等,最多 30 秒
A->>M2: GET /agents/commands(剛好掛在 worker Y)
M2-->>A: {commands:[{kind:"probe", request_id, target}]}
A->>A: 跑 probe
A->>M2: POST /agents/replies/<request_id> {result}
M2->>M2: agent_requests status=done, result 落 DB
M2->>R: publish reply:<request_id>
R-->>M: 喚醒
M->>M: 讀 agent_requests.result
M-->>U: 200 {reachable: true, …}
為什麼要 Redis:gunicorn 四個 worker 是獨立進程,瀏覽器的請求吊在 worker X、Agent 的回覆打到 worker Y,進程內的 Event 喚不醒對方。專案已有 Redis(core/extensions.py redis_client),用它的 pub/sub 跨進程喚醒。結果本身落 DB(agent_requests.result),Redis 只傳「好了」——Redis 掉訊息最壞 30 秒逾時時再查一次 DB。
逾時:瀏覽器那條吊 30 秒沒等到就回 504「Agent 無回應」,agent_requests 留 waiting,Agent 晚到的回覆照樣落 DB(可查,不浪費)。
證據檔(8a)的特例:bytes 不能走 JSON 回覆。
| 操作 | 現況(主系統直連) | 改法 |
|---|---|---|
| 上傳到 Agent | 主系統收檔 → POST /blob 推給 Agent |
主系統收檔暫存本機(/tmp/agent-outbound/<request_id>)→ 排 fetch_file 指令 → Agent 取走(GET /agents/files/<request_id>,既有通行證)→ 回覆已存(含 agent file uid、sha256)→ 主系統寫參照 row、刪暫存 |
| 下載/預覽 | 主系統 GET /blob/<uid> 拿回 bytes 串給瀏覽器 |
排 send_file 指令 → Agent POST /agents/replies/<request_id>(multipart)把檔上傳 → 主系統落暫存 → 喚醒 → 串給瀏覽器 → 刪暫存 |
| 轉 PDF | 主系統 GET /blob/<uid>/pdf |
同下載,Agent 側先轉再送 |
| 刪除 | DELETE /blob/<uid> |
排 delete_file 指令,不等回答(fire-and-forget,Agent 回覆只記 log) |
暫存落主系統磁碟是 FR-039「binary 不落雲端」的例外——那是 SaaS 的要求,落地版主系統本來就在客戶內網(D4 待裁)。
| # | 用途 | 長輪詢回應的 kind |
Agent 側接哪支既有函式(extend,不新寫) | 要不要回答 |
|---|---|---|---|---|
| 1 | 派工 | task |
cmd_tasks._parse 後的 task_queue.enqueue |
ack(既有端點) |
| 2 | 重啟 | restart |
ops.request_restart |
不用 |
| 3 | 升級 | update |
update_sync.trigger |
升級事件(既有) |
| 4 | 設定變更 | config_changed |
config_pull.pull_now |
不用 |
| 5 | 立即換證 | credentials_renew |
cert_renew.run_now |
不用(換證結果走既有事件) |
| 6 | 取消檢測 | detection_cancel |
既有 cancel route 的 app 層函式 | 不用 |
| 7a | 測試連線 | probe |
既有 probe route 的 app 層函式 | 要,agent_requests |
| 7b | 工具帳密 | credentials_set |
既有 credentials route 的 app 層函式 | 要(存好才算) |
| 8a | 證據檔四操作 | fetch_file/send_file/send_pdf/delete_file |
jedi-file-upload 的 service(blob route 現在呼叫的那幾支) |
要(刪除除外) |
| 8b | 遠端查日誌 | query_logs |
audit_log.list_logs |
要 |
主系統側的正本:1~4 用既有表;5、6 沒有正本,併進新表 agent_requests(kind、agent_id、payload、status waiting/taken/done/expired、result、request_id、建立時間)——7、8 也用這張表。也就是說只新增一張表,五種「沒正本的指令」與「要回答的請求」共用;1~4 不搬進去(它們的狀態機各自更複雜,硬併等於重寫)。
GET /api/1.0/agents/commands?wait=30
→ 200 {
"commands": [
{"kind": "task", "task_uid": "…", "payload": {…}},
{"kind": "probe", "request_id": "…", "payload": {"target": "…"}},
{"kind": "config_changed"}
],
"server_time_utc": "…"
}
→ 204 (30 秒沒事)REDISPATCH_LIMIT),剩下的 Agent 下一條請求再拿。task 的 payload 可能帶明文工具帳密(現況直推 body 就是),長輪詢回應走的是同一條 TLS+通行證+DPoP 的 Agent 出站連線,守門強度不低於直推(直推是 mTLS+JWT;出站是主系統 HTTPS 憑證指紋釘死+通行證+DPoP,FR-171 D 已定)。probe/credentials_set/query_logs 等要回答的,先回 taken 再做(POST /agents/replies/<request_id> 帶 status: taken),主系統據此分辨「Agent 沒拿到」與「Agent 拿到了在做」。app/remote_agent/service/agent_request_service.py(新):
ask(agent, kind, payload, timeout=30) -> result:寫 agent_requests、publish 喚醒 Agent 的長輪詢、subscribe 回覆頻道、吊著等、回結果或 raise 逾時。fire(agent, kind, payload):寫表+喚醒,不等。reply(request_id, status, result):Agent 回覆端點呼叫,落 DB+publish。7a、7b、8a、8b 都只呼叫 ask,不各寫一套等待邏輯。jedi-detection 的 agent_probe_client/agent_credential_client/agent_cancel_client 三支 client 改成呼叫主系統注入的 ask/fire(套件不認識 Redis,透過 port 注入)。
guidant_agent/sync/app/command_pull.py(新,照 config_pull.py 的 WorkerComponent 形狀):
GET /agents/commands?wait=30;逾時 204 立刻再掛;連線錯誤退避(5 秒起、最多 60 秒)後再掛。taken,做完 done;做失敗回 failed 帶原因(主系統照樣喚醒瀏覽器回錯誤,不讓它等到逾時)。task 回 failed reason=upgrading(現況直推回 409 的語意搬過來),主系統單留 pending。last_seen(不動 offline_threshold_sec)。| 退什麼 | 在哪 |
|---|---|
AgentDataPlaneClient.command() 直推路徑(保留 class 給 8444 上傳票簽發?→ D6) |
jedi-remote-agent |
AgentCommandService(直推服務整支)、_agent_commands.py 的推送與補推、with_command_push |
BE app/remote_agent/、core/plugins/ |
last_command_status/last_command_at 欄與 FE「連線」欄三態 |
jedi-remote-agent、FE RemoteAgentManage.vue |
agent_probe_client/agent_credential_client/agent_cancel_client 的 HTTP 部分 |
jedi-detection |
remote_agent_adapter._via_data_plane |
BE infra/upload_file/ |
Agent 8443 六個 cmd_* blueprint、/detection/probe/cancel/credentials、/blob 的主系統入口、/logs |
Agent sync/api/、scan/api/、files/api/blob.py |
Agent 8443 的 mTLS client 憑證驗證(verify_mode=CERT_REQUIRED) |
8443 只剩 /health、/agent-info,要不要連埠一起收 → D7 |
FR-171 契約 commands.py 的直推路徑常數 |
jedi-agent-protocol(改成長輪詢 kind 常數) |
D1 長輪詢的吊住時間(決策者 11-07 裁 30 秒)
Agent 一條請求最多吊多久。太短=Agent 一直重掛、主系統 access log 塞滿;太長=客戶的 proxy/負載平衡可能把閒置連線切掉(常見 60 秒)。
建議:30 秒。GitHub runner 用 50 秒、AWS SQS 長輪詢上限 20 秒,30 秒在中間,低於多數 proxy 的 60 秒閒置上限。gunicorn timeout 現在是 120 秒,不用動。
D2 長輪詢佔 gunicorn worker 的問題——批 0 驗過才定(決策者 11-07 裁)
出貨主機走 gunicorn 預設 sync worker(main.py 未設 worker_class),worker 數=nproc 封頂 4(install.sh:2667);2 核客戶機就是 2 個 worker,一個 worker 同時只服務一個請求。長輪詢吊著的請求在 gunicorn 眼裡就是「還沒回完的請求」——1 台 Agent 佔掉 1 個 worker,使用者按「測試連線」再佔 1 個,2 核機主系統在那 2 秒對所有人不回應;第 2 台 Agent 上線就整個吊死。這不是規模問題,是上線第一天的問題。現在沒事是因為沒有任何請求會故意吊著(心跳幾十毫秒就回、直推是往外打佔背景執行緒)。本機 DEV 看不出來——DEBUG=true 走 Flask dev server 本來就多執行緒(main.py:313 threaded=True)。
三道防線一起上:①gthread(main.py gunicorn options 加 worker_class="gthread"、threads=16,2 核 32 條、4 核 64 條;不動 installer/compose);②配額——長輪詢與等回答各設上限(各 8 條),超過立刻回 204/503 不吊,保證一般 API 永遠有一半執行緒;③每台 Agent 同時只准一條,第二條進來踢掉第一條。①是容量、②是防線、③是衛生。
批 0 先獨立驗(決策者 11-07 裁):DEV 用 USE_GUNICORN=true GUNICORN_WORKERS=2 起,一支假端點吊 30 秒,腳本開 20 條吊它、同時打一般 API 量回應時間。sync 要重現卡死(證明問題存在);gthread+配額要一般 API 回應時間與沒吊時無差,並跑一輪既有測試確認多執行緒沒炸 DI/session。通過→長輪詢;不通過→退回定時輪詢:同一支 GET /agents/commands 不帶 wait、每 10~15 秒問一次,Agent 側不改一行;代價是同步問答最壞多等一個間隔。兩條路其餘全部相同,批 0 驗完再定不會白做。
建議:批 0 通過就 ①+②+③ 全上;D1 的 30 秒不動。
D3 指令與請求的過期(決策者 11-07 裁照建議)
Agent 離線一週回來,長輪詢拿到一週前的「測試連線」「查日誌」請求沒有意義;但「派工」「升級」要保留。
建議:agent_requests(5、6、7、8)一律帶 expires_at,預設 5 分鐘,過期的 Agent 拿不到、主系統標 expired;工單(1)、重啟(2)、升級(3)照既有邏輯不過期。取消檢測(6)過期沒關係——檢測本身 5 分鐘內要嘛跑完要嘛還在跑,還在跑的話管理員再按一次。
D4 證據檔怎麼拿:經主系統中轉(決策者 11-07 裁 A)
證據存 Agent 的租戶,主系統只有參照(upload_files.storage_type=remote_agent、path=Agent 位址、save_file_name=Agent 側檔案 uid),bytes 全在 Agent。現況下載是主系統主動連 Agent 8443 GET /blob/<uid> 串給瀏覽器——單向網路下斷在這。
兩個選項:A 經主系統中轉(排 send_file → Agent 上傳到主系統暫存 → 串給瀏覽器 → 刪暫存;任何網路都通、多等一趟、暫存短暫落主系統磁碟);B 瀏覽器拿票直連 Agent 8444 取檔(零暫存、單趟串流,但瀏覽器要連得到 Agent 主機,與 D5 同前提)。
定案 A。暫存目錄 /tmp/agent-outbound/<request_id>、容器重啟清空、取走即刪、孤兒 1 小時清。代價兩點記在案:①大檔(百 MB)等待約翻倍(Agent 整檔傳完主系統才開始回瀏覽器);②該租戶當初選「證據存 Agent」是不想讓主系統碰檔,若客戶在意「主系統磁碟曾經有過」,暫存要加密(一次性鑰加密落盤、串出去時解)——到時加一張卡,本案先不做。B 等 D5 有答案再看。
D5 瀏覽器直傳 8444(源碼掃描上傳)——擱置(決策者 11-07 裁先不管)
源碼包上傳是使用者瀏覽器憑主系統簽的票直連 Agent 8444,不是主系統連 Agent,防火牆擋的是主系統↔︎Agent 之間。本案不動。若日後客戶回報瀏覽器也連不到 Agent 主機,把源碼包改成「瀏覽器上傳主系統暫存 → Agent 取走」(與 D4 上傳同一條路)另開卡。
D6/D7 直推程式留著、用開關關掉;8443 照起、客戶不用開埠(決策者 11-07 裁「怕未來又說要開」)
兩項合併裁。Agent 側:8443 照舊起、照舊 mTLS,六個指令 blueprint(cmd_*、/detection/*、/blob 主系統入口、/logs)程式碼留著、預設不掛——register_command_blueprints 加一個設定開關 data_plane_commands_enabled(預設 false),開了才掛;8443 常態只剩 /health、/agent-info。主系統側:AgentDataPlaneClient 與 mTLS client 憑證簽發流程留著(不然未來要開時 Agent 認不得主系統),AgentCommandService 直推路徑留著但沒有任何呼叫者,同樣加開關 AGENT_DIRECT_PUSH_ENABLED(預設 false)。客戶防火牆不用開 8443:本案之後主系統不會主動連 Agent,8443 對外通不通功能完全不受影響;安裝手冊刪掉「8443 要對主系統開放」那句;資安報告 8443 仍列對外埠,註明「預設不提供指令功能,只有健檢」。
未來要開=Agent 改一個設定值、主系統改一個設定值、客戶開埠,程式不用改。代價:守衛測試不能寫「主系統不連 8443」,改寫「兩個開關預設關;開關關閉時沒有任何路徑會呼叫 AgentDataPlaneClient.command()」。實查補充:8444 上傳票簽發在 agent_ticket_service.py,與 AgentDataPlaneClient 無關。
D8 FR-171 討論稿與 design 補指路行(決策者 11-07 裁照建議)
FR-171 design v1.0 的 A 指令通道、D3、D11 都被本案推翻;FR-170 §3.x「落地版直推一定通」也是。
建議:兩份各開頭加一行指向本案(照「定稿後不改 v1.0」規範,只加指路行不改內文);FR-171 其餘(B 設定通道、C 換證、K 金鑰登記簿、E 事件通道、DPoP)全部保留不動。
| 元件 | 現況 | 本案動作 |
|---|---|---|
Agent config_pull.py(WorkerComponent 背景拉、版本快取) |
設定通道的 Agent 側 | reuse 形狀:command_pull.py 照它的 WorkerComponent 骨架寫 |
Agent cmd_tasks/cmd_restart/cmd_update/cmd_config/cmd_credentials 的 app 層函式 |
直推 handler 呼叫的本體 | reuse:長輪詢分派表直接呼叫同一支;只刪 blueprint 殼 |
Agent event_sender.py+outbox.py |
事件送不到進 SQLite outbox 補送 | reuse:回覆(POST /agents/replies)送不到也進 outbox |
Agent task_queue.py |
本機單執行緒工單佇列 | reuse 不動 |
BE AgentCommandService.redispatch_pending()、_due() |
心跳回來查 pending 單 | extend:查單邏輯搬進長輪詢端點的「這台有什麼待辦」組裝;推送部分刪 |
BE _agent_update.rollout_service().update_for_agent() |
算這台該不該升、升哪版 | reuse:長輪詢組裝時呼叫 |
BE agent_offline_reaper、心跳 last_seen |
離線判定 | reuse 不動(長輪詢不算心跳) |
jedi-remote-agent AgentHeartbeatRoute 的守門(通行證+DPoP) |
Agent 出站端點的認證 | reuse:/agents/commands、/agents/replies、/agents/files 掛同一組 AUTH_AGENT |
core/extensions.redis_client |
已有 | reuse:pub/sub 喚醒 |
| jedi-detection 三支 agent client | 直推 HTTP | extend:改成呼叫注入的 ask/fire port,套件不認識 Redis |
remote_agent_adapter |
直連 Agent blob | extend:四個操作改走 ask,錯誤碼不變(FE 依碼顯示) |
agent_requests 表 |
無 | new:一張表承接 5、6、7、8 |
agent_request_service.py |
無 | new:等回答插槽,三個方法 |
批=可同時跑的一組;登記型共用檔(契約 commands.py、routing.py、Agent registry.py、事件碼表)由批 1 一張卡當 owner 一次掛齊。
| 批 | 卡 | repo | 做什麼 | 依賴 |
|---|---|---|---|---|
| 1 | F-1 契約 | jedi-packages | jedi-agent-protocol 0.3.0:commands.py 改成長輪詢 kind 常數與各 kind 的 payload dataclass、CommandsResponse、ReplyRequest;刪直推路徑常數 |
— |
| 1 | F-2 Agent 長輪詢殼 | agent | command_pull.py WorkerComponent、分派表(先只接 task/restart/update/config_changed 四個既有函式)、registry.py 登記、退避 |
— |
| 1 | F-3 主系統端點殼 | jedi-packages | jedi-remote-agent:GET /agents/commands 長輪詢(組裝 pending 單/restart/update/config 版本)、POST /agents/replies/<id>、GET /agents/files/<id>,routing.py 一次掛齊;Redis 喚醒走注入的 port |
— |
| 2 | E-1 環境卡 | main | 主專案改走三支套件 path dependency、gunicorn 改 gthread、agent_requests migration |
F-1 F-3 |
| 2 | A-1 等回答插槽 | main | agent_request_service.py(ask/fire/reply)、Redis pub/sub、逾時、過期 reaper;接上 F-3 的 port |
F-3 |
| 3 | A-2 指令六種改接 | main | _agent_commands.py 改成「寫正本+publish」;5、6 寫 agent_requests;刪 AgentCommandService 推送 |
A-1 |
| 3 | A-3 Agent 回答類接線 | agent | 分派表接 probe/credentials_set/detection_cancel/credentials_renew/query_logs/證據檔四種;taken/done/failed 回覆;回覆進 outbox |
F-2 |
| 3 | A-4 jedi-detection 三 client 改 port | jedi-packages | probe/credential/cancel 改呼叫 ask/fire |
A-1 |
| 4 | B-1 證據檔四操作 | main | remote_agent_adapter 改走 ask+暫存目錄+孤兒清理 |
A-1 A-3 |
| 4 | B-2 遠端查日誌 | main | FR-178 的 /logs 直連改走 ask(query_logs) |
A-1 A-3 |
| 4 | B-3 FE 連線欄 | fe | 「連線」欄三態改二態(在線/離線),刪「僅單向」與 hover | A-2 |
| 5 | C-1 退役 | main+jedi-packages | §3.7 清單主系統側;守衛測試「主系統不連 8443」 | 批 4 全驗 |
| 5 | C-2 退役 | agent | 8443 六 blueprint、probe/cancel/credentials/blob 主系統入口/logs 刪;8443 改聽 localhost(D7) | 批 4 全驗 |
| 5 | C-3 文件 | main | FR-170/171 指路行(D8)、資安報告連線清單更新、手冊「Agent 只出站」 | C-1 C-2 |
批 1 三張互不依賴可同跑(契約是登記型檔,F-2、F-3 開工時契約還沒發版,各自先用本機 path 指 F-1 工作區——這正是環境卡 E-1 要在批 2 收的)。design.md 會用 handoff/cards.py 驗批。
gthread 執行緒常態吃滿)。sync 卡死與 gthread+配額,結論決定 D2。