FR-181 Agent 只出站通道 — 設計定版 v1.0

主系統不再連 Agent;Agent 常駐一條長輪詢向主系統取指令,同步問答(測試連線、證據檔、查日誌)走同一條加「等回答插槽」;直推程式留著用開關關掉、客戶不用開 8443;心跳維持只表存活。

FR-181 Agent 只出站通道 — 設計定版 v1.0

🔴 本版已凍結。現行版本是 design-v1.1.md(無人值守調整:批次 7→6、P-0 判據寫死、驗收改本機模擬)。

項目 內容
狀態 定案(決策者 2026-11-07 逐項裁示),待開卡
日期 2026-11-07
討論稿 discussion.md(D1~D8 的推導與選項比較)
推翻 FR-170 §3.x「落地版同內網直推一定通」前提;FR-171 A 指令通道(D2、D3、D11)
保留 FR-171 B 設定通道、C 自動換證、K 金鑰登記簿、E 事件通道、DPoP;11-01「心跳只表存活」裁示
拆卡底表 handoff/cards.py(13 張 7 批,python cards.py 驗批)

變更紀錄

版本 日期 內容
v1.0 2026-11-07 定案

1. 這案在做什麼

客戶網路確定不讓主系統連進 Agent 主機。現況八種「主系統主動連 Agent 8443」的功能在這種客戶一個都送不到:派工、重啟、升級、設定變更通知、立即換證、取消檢測(六種指令);測試連線與工具帳密、證據檔四操作與遠端查日誌(同步問答)。心跳補推也是連 8443,救不了。

做完後的狀態:

項目 現況 目標
主系統 → Agent 的連線 直推 8443(mTLS+JWT) 零。主系統不主動連 Agent
Agent → 主系統 心跳(300 秒)、事件、設定拉取、ack/結果 同上,加一條常駐長輪詢 GET /agents/commands?wait=30
六種指令怎麼到 Agent 直推,打不通等心跳補推 主系統寫 DB 正本+敲 Redis 鈴 → 長輪詢立刻回 → Agent 做
同步問答(7、8) 主系統直連當場拿回答 主系統排請求吊著等 → Agent 長輪詢取走做 → 回覆上來 → Redis 喚醒等著的那條 → 回瀏覽器
證據檔 bytes 主系統直連 Agent 串流 經主系統暫存中轉(Agent 送上來/Agent 來拿),取走即刪
直推程式 主路徑 留著,主系統與 Agent 各一個開關預設關
Agent 8443 六個指令端點+health 照起、只剩 /health//agent-info;客戶不用開埠
心跳 只表存活 不變;長輪詢不算心跳,離線判定不動
gunicorn sync worker gthread 16 執行緒+長輪詢/等回答各配額 8 條(批 0 驗過才翻)
舊 Agent — 全部重裝,不留相容路徑

整案一句話:Agent 永遠是發起方;主系統有事就讓掛著的那條請求立刻回去。


2. 分工概述(30 秒版)

批 做什麼 卡 完成怎麼判定(決策者檢查法)
1 先驗 gunicorn 撐不撐得住:sync 重現卡死、gthread+配額證明一般 API 不受影響 P-0 看證據檔三組數字:sync 吊 2 條 health p95 > 25 秒;gthread 吊 20 條 p95 與沒吊時差 < 50 ms。通過走長輪詢,不通過退定時輪詢(其餘卡不變)
2 地基三張:契約、Agent 長輪詢元件、主系統長輪詢端點 F-1 F-2 F-3 123 Agent 裝 F-2 版、DEV 起 F-3 工作區的主系統:DB 插一張 pending 單,Agent 主控台 3 秒內看到開跑
3 環境卡:主系統釘新版套件、gunicorn 翻預設、agent_requests 表 E-1 USE_GUNICORN=true 起 log 印 gthread;\d compliance.agent_requests 有表
4 等回答插槽(跨 worker 喚醒、配額、逾時、過期) A-1 gunicorn 2 worker 實打:兩條 ask 各在自己的 worker 醒來;殺 Redis → 30 秒 504 不 500
5 指令六種(主系統側)、回答九種(Agent 側)、jedi-detection 三 client 換底 A-2 A-3 A-4 123 Agent 主機 iptables 擋 8443 入站:派工、重啟、升級、改授權清冊、立即換證、取消檢測各 3 秒內反應;按「測試連線」2 秒內看到結果
6 證據檔四操作、遠端查日誌、FE 連線欄 B-1 B-2 B-3 同上防火牆:證據存 Agent 的租戶上傳/預覽/下載/轉 PDF/刪除都正常、/tmp/agent-outbound 取走後為空;支援頁查 Agent 日誌查得到;連線欄只剩在線/離線
7 守衛測試、FR-170/171 指路行、手冊與資安報告 C-1 守衛三支綠;手冊 grep 不到「8443 要對主系統開放」

依賴:批 1 的結論決定 E-1 翻不翻 gunicorn 預設;批 2 三張互不依賴;批 5 三張分屬三個 repo。決策者功課:批 1 驗完看一眼數字拍板 D2;批 5 驗收前把 123 Agent 主機 8443 入站擋掉(或告訴首腦擋)。


3. 決策定案表

# 決策 定案 理由 被排除
D1 長輪詢一條請求吊多久 30 秒 低於多數 proxy/LB 的 60 秒閒置上限;GitHub runner 50 秒、SQS 上限 20 秒,取中間;gunicorn timeout 120 秒不用動 5 秒(Agent 一直重掛、log 塞滿);60 秒(撞 proxy 閒置切線)
D2 長輪詢吊住 gunicorn worker 批 0 驗過才定。通過→gthread 16 執行緒+配額(長輪詢 ≤ 8、等回答 ≤ 8,超過立刻回不吊)+每台 Agent 只准一條(第二條踢第一條) 出貨 sync worker 數=nproc 封頂 4,1 台 Agent 就佔 1 個 worker、按「測試連線」再佔 1 個,2 核機當場吊死。三道:容量/防線/衛生。本機 DEV 看不出來(dev server 本來多執行緒) 另起 RUN_MODE=agent-poll 獨立進程(多一個行程進 compose/installer/升級鏈,幾十台 Agent 不值得;上百台再換);不驗直接上(多執行緒下 DI/session 沒實打過)
D2 退路 批 0 不通過 退定時輪詢:同一支 GET /agents/commands 不帶 wait、每 10~15 秒問一次 兩條路只差端點吊不吊,Agent 側不改一行;代價同步問答最壞多等一個間隔 —
D3 指令與請求的過期 agent_requests 預設 5 分鐘過期,過期的 Agent 拿不到、主系統標 expired;工單/重啟/升級不過期 Agent 離線一週回來拿到一週前的「測試連線」沒意義;派工與升級要保留。取消檢測過期沒關係,還在跑就再按 全部不過期(Agent 回來做一堆沒人收的事);全部過期(派工會丟)
D4 證據檔怎麼拿 A 經主系統中轉:下載排 send_file Agent 上傳到主系統暫存再串給瀏覽器;上傳主系統收檔暫存排 fetch_file Agent 來拿;暫存 /tmp/agent-outbound/<request_id>、取走即刪、孤兒 1 小時清 任何網路都通。代價:大檔等待約翻倍;暫存短暫落主系統磁碟(FR-039「不落雲端」是 SaaS 承諾,落地版磁碟是客戶的)。若客戶在意「磁碟曾經有過」,暫存加密另開卡 B 瀏覽器拿票直連 Agent 8444(零暫存、單趟,但前提與 D5 同,等 D5 有答案再看)
D5 源碼包瀏覽器直傳 8444 擱置,本案不動 那是瀏覽器連 Agent,不是主系統連 Agent;防火牆擋的是主系統↔︎Agent —
D6 主系統直推程式 留著、開關關:AGENT_DIRECT_PUSH_ENABLED 預設 False;AgentCommandService.send() 開頭擋;AgentDataPlaneClient 與主系統 mTLS client 憑證簽發留著 決策者「怕未來又說要開」。未來要開=主系統改一值、Agent 改一值、客戶開埠,程式不改。實查 8444 上傳票簽發在 agent_ticket_service.py 與它無關 整支刪(留殼怕被當成還能直推——用守衛測試擋)
D7 Agent 8443 照起、六個指令 blueprint 用開關 data_plane_commands_enabled 預設不掛;只剩 /health、/agent-info;客戶不用開埠 同 D6。8443 對外通不通功能完全不受影響;安裝手冊刪「8443 要對主系統開放」;資安報告 8443 仍列、註明「預設只有健檢」 改聽 localhost(未來要開又得改);整個收掉
D8 FR-170/171 文件 各檔頭加一行指向本案,不改 v1.0 內文 「定稿後不改 v1.0」規範 改寫內文
— 舊 Agent 全部重裝,不留直推→長輪詢的兩段升級 決策者裁「舊的先不管」;123、POC 本案出貨後重裝 主系統先收長輪詢暫不刪直推→Agent 升→再關直推(多一棒)

4. 端到端流程

4.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 command_pull
  participant M as 主系統 /agents/commands
  participant DB as DB 正本
  participant R as Redis
  participant U as 管理員
  A->>M: GET /agents/commands?wait=30(通行證+DPoP)
  M->>DB: 查這台待辦(pending 單/restart/update/agent_requests waiting)
  Note over M: 沒有 → waiter.wait(uid, 30) 訂閱 agent:<uid>
  U->>M: 按「派工」
  M->>DB: 寫正本(after_commit)
  M->>R: publish agent:<uid>
  R-->>M: 喚醒
  M->>DB: 再查一次
  M-->>A: 200 {commands:[{kind:"task", task_uid, payload}]}
  A->>A: 分派表 → task_intake → task_queue.submit
  A->>M: POST /agents/tasks/<uid>/ack(既有)
  M->>DB: status=dispatched
  A->>M: GET /agents/commands?wait=30(立刻再掛)
  Note over M,A: 30 秒沒事 → 204 → Agent 立刻再掛
圖 1 — 長輪詢取指令。Agent 永遠是發起方;主系統沒事就吊著,有事立刻回。

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

防撞:task uid 冪等、ack 即 dispatched、Agent 本機單執行緒佇列(沿用 FR-171 三道);restart 被 Agent 取走時主系統立刻清 restart_requested(不然每條長輪詢都回 restart)。

4.2 同步問答怎麼走

%%{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 主系統 worker X
  participant R as Redis
  participant M2 as 主系統 worker Y
  participant A as Agent
  U->>M: POST 測試連線 {agent_uid, target}
  M->>M: ask():agent_requests(kind=probe, waiting, expires=+5m)
  M->>R: publish agent:<uid>;subscribe reply:<request_id>
  Note over M: 吊著等,最多 30 秒(配額 ≤ 8 條,滿了 503)
  A->>M2: GET /agents/commands(掛在 worker Y)
  M2-->>A: {commands:[{kind:"probe", request_id, payload}]}
  A->>M2: POST /agents/replies/<id> {status:"taken"}
  A->>A: 跑 probe
  A->>M2: POST /agents/replies/<id> {status:"done", result}
  M2->>M2: agent_requests 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 喚不醒對方。結果本身落 DB,Redis 只傳「好了」。

逾時:30 秒沒等到回 504 GRC_504_AGENT_NO_REPLY,agent_requests 留 waiting,Agent 晚到的回覆照樣落 DB。taken 與 failed:Agent 拿到要回答的先回 taken(主系統據此分辨「沒拿到」與「在做」);做失敗回 failed reason=,主系統立刻喚醒瀏覽器回錯誤,不讓它等到逾時。

4.3 證據檔四操作(D4 A 案)

操作 流程
上傳到 Agent 主系統收檔 → spool.save(request_id) → ask(fetch_file, {request_id, name, sha256}) → Agent GET /agents/files/<request_id> 下載、驗 sha、FileUploadService.save → reply done {file_uid, sha256} → 主系統寫參照 row、刪暫存
下載/預覽 ask(send_file, {file_uid}) → Agent 讀本機、POST /agents/replies/<id> multipart → 主系統 spool.save → 喚醒 → spool.open 串給瀏覽器 → 讀完刪
轉 PDF 同下載,kind send_pdf,Agent 側先走既有轉檔
刪除 fire(delete_file, {file_uid}),不等回答

錯誤碼不變:504→FILE_AGENT_UNREACHABLE、Agent failed→FILE_AGENT_BAD_RESPONSE(FE 依碼顯示)。


5. 詳細設計

5.1 契約(jedi-agent-protocol 0.3.0,F-1)

GET  /api/1.0/agents/commands?wait=30        → 200 CommandsResponse | 204
POST /api/1.0/agents/replies/<request_id>    body Reply(JSON)或 multipart(file + Reply 欄位)
GET  /api/1.0/agents/files/<request_id>      → bytes(Agent 來拿主系統暫存的檔)

Command        {kind, request_id?, task_uid?, payload, expires_at?}
CommandsResponse {commands: [Command], server_time_utc}
Reply          {status: taken|done|failed, result?, reason?}

十三個 kind:

kind 誰寫正本 要回答 Agent 接哪支
task agent_tasks(既有) ack(既有端點) task_intake → task_queue.submit
restart restart_requested(既有) 否 ops.request_restart
update rollout 表(既有) 否(升級事件既有) update_sync.trigger
config_changed agent_requests 否 config_pull.request()
credentials_renew agent_requests 否 cert_renew.request_now()
detection_cancel agent_requests 否 scan/app/cancel.py
probe agent_requests 是 scan/app/probe.py
credentials_set agent_requests 是 scan/app/credential_store
fetch_file agent_requests 是 files/app/blob_ops.fetch
send_file agent_requests 是(multipart) files/app/blob_ops.send
send_pdf agent_requests 是(multipart) files/app/blob_ops.send_pdf
delete_file agent_requests 否 files/app/blob_ops.delete
query_logs agent_requests 是 platform/audit_log.list_logs

既有直推路徑常數與 body dataclass 不刪(D6),檔頭註解改「直推預設關,長輪詢是主路徑」。

5.2 主系統長輪詢端點(jedi-remote-agent 1.13.0,F-3)

套件只做端點與組裝,不認識 Redis、不建表,四個 port 由宿主注入(app/ports/command_channel.py):

port 方法 主系統實作(A-1)
waiter wait(uid, timeout) -> bool/notify(uid)/preempt(uid) infra/remote_agent/redis_command_waiter.py:Redis pub/sub + 本地 threading.Event 鏡像;長輪詢配額 ≤ 8
spool save(request_id, stream)/open(request_id)/discard(request_id) infra/remote_agent/file_spool.py:/tmp/agent-outbound/
command_sources restart_for(agent) -> mode?/update_for(agent) -> info?/requests_for(agent) -> [Command] 包既有 restart_requested、rollout_service().update_for_agent、agent_requests waiting 未過期列
on_reply (request_id, status, result) agent_request_service.reply

端點組裝順序:pending 且 _due 的工單(搬 redispatch_pending 的查單邏輯)→ restart → update → agent_requests;一次最多 20 條;有東西立刻回,沒有 waiter.wait 後再查一次;同台第二條進來 preempt 讓第一條回 204。

5.3 等回答插槽(A-1)

app/remote_agent/service/agent_request_service.py 三個方法,7a、7b、8a、8b 都只呼叫這三個,不各寫等待邏輯:

  • ask(agent, kind, payload, timeout=30):寫表(expires_at=+5 分)→ notify → subscribe reply:<id> 吊著 → 喚醒讀 DB result → 回;配額滿 503 GRC_503_AGENT_BUSY;逾時 504 GRC_504_AGENT_NO_REPLY。
  • fire(agent, kind, payload):寫表+notify,不等。
  • reply(request_id, status, result):落 DB+publish reply:<id>;taken 只改狀態不喚醒。

背景排程(core/scheduler_agent.py):expire_requests 每分鐘、reap_orphans 每小時。

5.4 agent_requests 表(E-1)

compliance.agent_requests:id/uid/agent_id FK/tenant_id/kind/payload jsonb/status waiting|taken|done|failed|expired/result jsonb/reason/expires_at/taken_at/done_at/created 系列。索引 (agent_id, status, expires_at)。GRANT cm_app、RLS 照 agent_tasks。主線 phase=active → 出貨基線待重產。

為什麼只加一張表:取消、換證(沒正本)與測試連線、帳密、證據檔、查日誌(要回答)性質相同——「一次性、短命、要麼做完要麼過期」;工單/重啟/升級各有自己更複雜的狀態機,硬併等於重寫。

5.5 Agent 長輪詢元件(F-2、A-3)

guidant_agent/sync/app/command_pull.py,照 config_pull.py 的 WorkerComponent 骨架:

  • 註冊成功後起背景執行緒無限迴圈;204 立刻再掛;2xx 逐條分派;連線錯退避 5→10→20→40→60 秒。
  • 分派表 dict[kind -> handler],不是 if 鏈;F-2 接四個(task/restart/update/config_changed)、其餘留 no-op 給 A-3 填。
  • 要回答的先 taken 再做,做完 done,失敗 failed reason=。
  • 升級中/重啟已排定拿到 task → 不進佇列、reply failed reason=upgrading(現況直推 409 語意)。
  • 回覆走 reply_sender.py,送不到進 event_sender 同一條 outbox(新 kind reply)。
  • 🔴 長輪詢與心跳是兩條獨立出站連線;離線判定仍看心跳 last_seen。

抽 app 層:cmd_tasks._parse → task_intake.py;detection_route 的 cancel/probe → scan/app/cancel.py/probe.py;blob_route 四操作 → files/app/blob_ops.py。route 殼改薄、開關開時仍呼叫同一支,不複製。

5.6 gunicorn(P-0、E-1)

main.py gunicorn options 加 worker_class(env GUNICORN_WORKER_CLASS)、threads(GUNICORN_THREADS);P-0 只加入口不翻預設,E-1 依 P-0 結論翻成 gthread/16。install.sh 寫 .env 補這兩個變數。

5.7 開關(D6、D7)

側 變數 預設 擋在哪
主系統 AGENT_DIRECT_PUSH_ENABLED(config/config.py) False AgentCommandService.send() 開頭回 CommandResult.disabled()
Agent data_plane_commands_enabled(config/config.py AgentAuthConfig) False data_plane_server.register_command_blueprints 開了才掛六個 cmd_*

守衛(C-1):預設皆 False;主系統開關 False 時 patch AgentDataPlaneClient.command 成 raise,跑派工/重啟/升級/換證/設定變更五條全綠。


6. 現況接入點盤點

元件 現況 本案動作
Agent config_pull.py 設定通道 WorkerComponent reuse 形狀:command_pull.py 照它寫
Agent cmd_* 的 app 層函式、task_queue、event_sender+outbox、cert_renew.request_now、update_sync.trigger、ops.request_restart 直推 handler 呼叫的本體 reuse:分派表直接呼叫;route 殼留著用開關
BE AgentCommandService.redispatch_pending/_due 心跳回來查 pending 單 extend:查單邏輯搬進套件端點組裝;推送部分留著加開關
BE rollout_service().update_for_agent 算該不該升 reuse:command_sources.update_for
BE _agent_config_notify 直推 /config/notify extend:改寫 agent_requests(config_changed)+notify
jedi-remote-agent AgentHeartbeatRoute 守門 AUTH_AGENT reuse:三條新端點掛同一組
core/extensions.redis_client 已有 reuse:pub/sub
jedi-detection 三支 client 直推 HTTP extend:改收 channel port
remote_agent_adapter 直連 blob extend:四操作改 ask/fire,錯誤碼不變
agent_requests 表、agent_request_service、redis_command_waiter、file_spool、command_pull、reply_sender、task_intake 無 new

7. 拆分表

批=可同時跑的一組;登記型檔各只讓一張卡碰(契約 F-1、routing.py F-3、Agent registry.py/data_plane_server.py F-2、remote_agent.py plugin 掛點 A-1、main.py/migration E-1、jedi-detection 三 client A-4)。底表 handoff/cards.py,python cards.py 斷言:依賴在更早的批、同批同 repo 不改同一檔。

批 卡 repo 標題 reuse/extend/new 依賴 model/effort
1 P-0 main gunicorn 長輪詢併發驗證:sync 重現卡死、gthread+配額證明一般 API 不受影響 new 驗證腳本+假端點;extend main.py 加 worker_class/threads 入口 — opus/medium
2 F-1 jedi-packages 契約 0.3.0:長輪詢取指令與回答的形狀 extend commands.py(登記型 owner) — opus/medium
2 F-2 agent Agent 長輪詢元件 command_pull、分派表、退避、回覆 outbox、8443 指令開關 new command_pull/reply_sender/task_intake;extend registry.py/data_plane_server.py(登記型 owner) — opus/high
2 F-3 jedi-packages 主系統長輪詢端點、replies、files,四 port new route+ports;extend routing.py(登記型 owner) — opus/high
3 E-1 main 環境卡:釘新版套件、gunicorn 翻預設、agent_requests migration extend pyproject/main.py/install.sh;new migration F-1 F-3 P-0 sonnet/medium
4 A-1 main 等回答插槽:ask/fire/reply、Redis 跨 worker、配額、過期;實作四 port new service+entity/repo/waiter/spool;extend DI/plugin mount/排程 E-1 opus/high
5 A-2 main 指令六種改「寫正本+喚醒」,取消與換證寫 agent_requests,直推加開關 extend _agent_commands/_agent_config_notify/_agent_keys/AgentCommandService A-1 opus/medium
5 A-3 agent 回答九種接線、taken→done/failed、抽 app 層 extend 分派表;new scan/app/cancel/probe、files/app/blob_ops F-2 opus/high
5 A-4 jedi-packages jedi-detection 三 client 改 channel port extend 三 client;new port A-1 F-1 sonnet/medium
6 B-1 main 證據檔四操作改走插槽 extend remote_agent_adapter A-1 A-3 opus/medium
6 B-2 main 遠端查日誌改 ask extend agent_log_query_app_service A-1 A-3 sonnet/low
6 B-3 fe 連線欄三態改二態 extend RemoteAgentManage.vue A-2 sonnet/low
7 C-1 main 守衛測試、FR-170/171 指路行、手冊與資安報告 new 守衛;extend 文件 A-2 B-1 B-2 B-3 sonnet/medium

各卡的改哪些檔、新開哪些檔、驗收條件以 cards.py 為準(Notion 卡從它產)。


8. 端到端驗收

全部在 123 Agent 主機 iptables 擋 8443 入站(iptables -I INPUT -p tcp --dport 8443 ! -s 127.0.0.1 -j DROP)的狀態下做:

# 動作 預期
1 主系統派一張源碼掃描單 Agent 主控台 3 秒內看到開跑;完成後主系統看到結果
2 管理頁按重啟 Agent 重啟一次,不重複重啟
3 分批升級轉成下發 Agent 3 秒內開始下載、升級事件回主系統
4 改一條授權清冊 Agent 3 秒內拉到
5 按立即換證 Agent log 有 renew、agent_requests 該列 done
6 派一張檢測再按取消 Agent 3 秒內中止
7 檢測工具頁按「測試連線」 2 秒內看到結果
8 檢測工具頁存帳密 Agent 側帳密庫有該筆
9 證據存 Agent 的租戶:上傳/預覽/下載/轉 PDF/刪除 全部正常;/tmp/agent-outbound 取走後為空;100 MB 檔時間記錄
10 支援頁查 Agent 日誌 查得到
11 Agent 停掉再做 1、7、9 1 的單留 pending、Agent 回來 3 秒內開跑;7、9 30 秒內回「Agent 無回應」不 500
12 Redis 停掉再做 7 30 秒 504 不 500;Redis 回來後 7 正常
13 20 條假長輪詢吊著 一般頁面回應時間無差(P-0 腳本)
14 兩個開關 預設 False;grep 主系統 .send( 呼叫只剩開關後那一處;Agent 8443 POST /tasks 404
15 連線欄 只剩在線/離線

9. 不做什麼

  • WebSocket/gRPC 長連線(反悔條件:單一主系統 Agent 數 > 100 或 gthread 執行緒常態吃滿)。
  • 心跳改變、離線判定改變。
  • SaaS 版「binary 不落雲端磁碟」(D4 暫存加密另開卡)。
  • 源碼包瀏覽器直傳 8444(D5 擱置)。
  • 舊 Agent 兩段升級(全部重裝)。
  • FR-171 身分、換證、金鑰登記簿、設定通道、事件通道、DPoP 全部不動。