FR-181 · 需求討論稿 v1 · 2026-11-07(D1~D8 已裁,定案在 design.md)

Agent 只出站,客戶一個埠都不用開

直推 8443 退役;Agent 用長輪詢向主系統取指令,同步問答(測試連線、證據檔、查日誌)也走同一條;心跳維持只表存活

決策者 11-07 確認:客戶網路不可能讓主系統連進 Agent。FR-171(11-02 合回)把派工、重啟、升級、取消、換證、設定通知全改成主系統直推 Agent 8443,而證據檔與遠端查日誌從 FR-039 起就是直推——在這種客戶,主系統叫 Agent 做事的功能一個都送不到。本稿提出:主系統不再主動連 Agent,Agent 用長輪詢(一個請求吊著最多 30 秒,有事立刻回)向主系統取指令,八種用途全部改走這條;直推整個退役,不留兩套。這不是回到 7 月的「心跳夾帶工單」——心跳維持 11-01 裁定的只表存活,指令是獨立端點。

決策者已裁:客戶不開入站埠,確定 決策者已裁:直推退役、不留兩套 決策者已裁:同步問答(證據檔、查日誌)也走輪詢 D1~D8 已裁,定案見 design.md 心跳只表存活(11-01 裁示)不翻 跨 4 repo:BE/jedi-packages/agent/FE

0. 分工概述(30 秒版)

整案一句話:主系統不再連 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。


1. 為什麼要做:直推的前提被推翻了

1.1 時間軸:三個月內換了方向,前提卻是錯的

日期 事件 主系統 → 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 以來的檔案與同步問答路徑,從來沒在單向網路跑過。

1.2 現在單向網路下壞掉的八種功能

主系統主動連 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)。

1.3 心跳補推為什麼救不了

FR-171 D3 設計「直推打不通就等心跳回來補推」,很多人以為這代表單向網路可以用。實查 AgentCommandService.redispatch_pending():補推走的是同一支 AgentDataPlaneClient.command(),同樣連 Agent 8443。單向網路下每一跳的補推都失敗,單永遠 pending,管理頁「連線」欄永遠顯示「僅單向」。設計當時給的解法是 hover 提示「請確認 Agent 主機的 8443 埠對主系統開放」——也就是把問題交給客戶開埠,而客戶不會開。


2. 可以選的路,為什麼選長輪詢

做法 怎麼運作 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。長輪詢是「客戶不開埠」這個前提下最少元件的正規做法。


3. 設計:一條長輪詢、一個等回答插槽

3.1 端到端:指令怎麼到 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 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 立刻再掛
圖 1 — 長輪詢取指令。Agent 永遠是發起方;主系統沒事就吊著,有事立刻回。

正本仍在主系統 DB(FR-171 D3 的原則不變):工單表、restart_requested、rollout 表。Redis 只是「有新東西,醒來查表」的鈴,鈴沒響(Redis 重啟、publish 漏了)最壞 30 秒逾時後 Agent 再掛一條時查表也會查到。沒有第二個佇列。

三道防撞沿用 FR-171:task uid 冪等、ack 即 dispatched、Agent 本機單執行緒佇列。長輪詢只有一個觸發點(不像直推+補推兩個),反而更簡單。

3.2 端到端:同步問答怎麼走(7、8)

%%{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, …}
圖 2 — 等回答插槽。瀏覽器那條請求吊著等,Agent 的結果一進來就喚醒它。測試連線、證據檔、查日誌都用這同一個插槽。

為什麼要 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 待裁)。

3.3 八種用途的歸宿

# 用途 長輪詢回應的 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 不搬進去(它們的狀態機各自更複雜,硬併等於重寫)。

3.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 秒沒事)
  • 一次最多回 20 條(沿用 REDISPATCH_LIMIT),剩下的 Agent 下一條請求再拿。
  • task 的 payload 可能帶明文工具帳密(現況直推 body 就是),長輪詢回應走的是同一條 TLS+通行證+DPoP 的 Agent 出站連線,守門強度不低於直推(直推是 mTLS+JWT;出站是主系統 HTTPS 憑證指紋釘死+通行證+DPoP,FR-171 D 已定)。
  • Agent 拿到 probe/credentials_set/query_logs 等要回答的,先回 taken 再做(POST /agents/replies/<request_id> 帶 status: taken),主系統據此分辨「Agent 沒拿到」與「Agent 拿到了在做」。

3.5 主系統側的「等回答插槽」是共用元件

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 注入)。

3.6 Agent 側的長輪詢元件

guidant_agent/sync/app/command_pull.py(新,照 config_pull.py 的 WorkerComponent 形狀):

  • 註冊成功後起一條背景執行緒,無限迴圈 GET /agents/commands?wait=30;逾時 204 立刻再掛;連線錯誤退避(5 秒起、最多 60 秒)後再掛。
  • 拿到 commands 逐條分派到 §3.3 的既有函式;分派表是模組內一張 dict,不是 if 鏈。
  • 要回答的:先 taken,做完 done;做失敗回 failed 帶原因(主系統照樣喚醒瀏覽器回錯誤,不讓它等到逾時)。
  • 升級中、重啟已排定:拿到 task 回 failed reason=upgrading(現況直推回 409 的語意搬過來),主系統單留 pending。
  • 🔴 長輪詢與心跳是兩條獨立的出站連線:心跳照舊每 300 秒一跳只表存活;長輪詢掛著不算心跳。Agent 離線判定仍看心跳 last_seen(不動 offline_threshold_sec)。

3.7 退役清單

退什麼 在哪
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 常數)

4. 待決策

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)全部保留不動。


5. 現況接入點:哪些 reuse、哪些 extend、哪些 new

元件 現況 本案動作
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:等回答插槽,三個方法

6. 分棒草案

批=可同時跑的一組;登記型共用檔(契約 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 驗批。


7. 不做什麼

  • WebSocket/gRPC 長連線:長輪詢做到八成功能、零新元件;真有上百台 Agent 再評估(反悔條件:單一主系統 Agent 數 > 100 或 gthread 執行緒常態吃滿)。
  • 心跳改變:心跳維持 11-01 裁示只表存活,長輪詢是獨立連線;離線判定不動。
  • SaaS 版「binary 不落雲端磁碟」:D4 接受暫存;SaaS 到時另案。
  • Agent 主控台(8444):不動。
  • FR-171 的身分、換證、金鑰登記簿、設定通道、事件通道、DPoP:全部保留,本案只換「指令怎麼到 Agent」與「同步問答怎麼走」。
  • gunicorn 不驗直接上:批 0(P-0)先驗 sync 卡死與 gthread+配額,結論決定 D2。
  • 舊版 Agent 升級:決策者 11-07 裁「舊的先不管,全部重新安裝」——已裝的 Agent(123、POC)本案出貨後重裝,不走「主系統先收長輪詢再刪直推」的兩段升級,不留相容路徑。