主系統不再連 Agent;Agent 常駐一條長輪詢向主系統取指令,同步問答(測試連線、證據檔、查日誌)走同一條加「等回答插槽」;直推程式留著用開關關掉、客戶不用開 8443;心跳維持只表存活。
| 項目 | 內容 |
|---|---|
| 狀態 | 已開卡 CM-3595,子卡 CM-3596~3608,13 張 6 批,無人值守一路跑到 C-1 |
| 日期 | 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 張 6 批,python cards.py 驗批) |
| 上一版 | design.md(v1.0,凍結) |
| 版本 | 日期 | 內容 |
|---|---|---|
| v1.0 | 2026-11-07 | 定案 |
| v1.1 | 2026-11-07 | 無人值守:批次 7→6、P-0 判據寫死、驗收改本機模擬單向網路、A-4 釘版併進 B-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 永遠是發起方;主系統有事就讓掛著的那條請求立刻回去。
| 批 | 做什麼 | 卡 | 完成怎麼判定(決策者檢查法) |
|---|---|---|---|
| 1 | gunicorn 併發驗證+契約+Agent 長輪詢元件(三 repo 互不依賴) | P-0 F-1 F-2 | P-0 證據檔三組數字與結論(判據寫死:gthread 吊 20 條 p95 比基準慢 < 100 ms 且無新紅 → 長輪詢);本機 Agent 對 DEV 註冊、DB 插一張 pending 單 3 秒內 ack |
| 2 | 主系統長輪詢端點+環境卡(都只讀 P-0 證據檔) | F-3 E-1 | USE_GUNICORN=true 起 log 印 gthread(或證據檔說退則不翻);\d compliance.agent_requests 有表 |
| 3 | 等回答插槽(跨 worker 喚醒、配額、逾時、過期) | A-1 | gunicorn 2 worker 實打:兩條 ask 各在自己的 worker 醒來;殺 Redis → 30 秒 504 不 500 |
| 4 | 指令六種(主系統側)、回答九種(Agent 側)、jedi-detection 三 client 換底 | A-2 A-3 A-4 | 本機 Agent 模擬單向網路(DEV DB 把該 Agent base_url 改成不存在位址,直推必失敗):派工、重啟、升級、改授權清冊、立即換證、取消檢測各 3 秒內反應;按「測試連線」2 秒內看到結果 |
| 5 | 證據檔四操作、遠端查日誌、連線欄(FE+套件)、工具帳密 | B-1 B-2 B-3 B-4 B-5 | 同上模擬:證據存 Agent 的租戶上傳/預覽/下載/轉 PDF/刪除都正常、/tmp/agent-outbound 取走後為空;支援頁查 Agent 日誌查得到;連線欄只剩在線/離線 |
| 6 | 釘 1.17.1、守衛測試 | C-1 | 守衛三支綠 |
| 7 | 文件收口(暫緩) | C-2 | 真機驗收過後派;手冊 grep 不到「8443 要對主系統開放」 |
| 8~10 | 對帳改走插槽(補卡) | B-6a→B-6b+B-6c→B-6d | 證據存 Agent 的租戶上傳 3 檔按「對帳」matched 3;竄改一檔 mismatched 1;Agent 停掉 error 3 不 500 |
依賴:P-0 結論(判據寫死)決定 E-1 翻不翻 gunicorn 預設、F-3 端點吊不吊;批 1 三張與批 4、5 各三張分屬不同 repo。無人值守:全程沒有要決策者裁的點;真機(123 擋 8443 入站)驗證在母卡收口時由首腦做;母卡 Done 由決策者收。
| # | 決策 | 定案 | 理由 | 被排除 |
|---|---|---|---|---|
| D1 | 長輪詢一條請求吊多久 | 30 秒 | 低於多數 proxy/LB 的 60 秒閒置上限;GitHub runner 50 秒、SQS 上限 20 秒,取中間;gunicorn timeout 120 秒不用動 |
5 秒(Agent 一直重掛、log 塞滿);60 秒(撞 proxy 閒置切線) |
| D2 | 長輪詢吊住 gunicorn worker | P-0 驗過才定,判據寫死不等人:gthread 吊 20 條時一般 API p95 比基準慢 < 100 ms 且 pytest 無基準以外新紅 → 長輪詢;否則退定時輪詢。通過→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 升→再關直推(多一棒) |
%%{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 立刻再掛
正本仍在 DB:工單表、restart_requested、rollout 表、agent_requests。Redis 只是「醒來查表」的鈴;鈴沒響最壞 30 秒逾時後 Agent 再掛一條時查表也會查到。沒有第二個佇列(FR-171 D3 原則不變)。
防撞:task uid 冪等、ack 即 dispatched、Agent 本機單執行緒佇列(沿用 FR-171 三道);restart 被 Agent 取走時主系統立刻清 restart_requested(不然每條長輪詢都回 restart)。
%%{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}
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=,主系統立刻喚醒瀏覽器回錯誤,不讓它等到逾時。
| 操作 | 流程 |
|---|---|
| 上傳到 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 依碼顯示)。
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),檔頭註解改「直推預設關,長輪詢是主路徑」。
套件只做端點與組裝,不認識 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。
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 每小時。
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 → 出貨基線待重產。
為什麼只加一張表:取消、換證(沒正本)與測試連線、帳密、證據檔、查日誌(要回答)性質相同——「一次性、短命、要麼做完要麼過期」;工單/重啟/升級各有自己更複雜的狀態機,硬併等於重寫。
guidant_agent/sync/app/command_pull.py,照 config_pull.py 的 WorkerComponent 骨架:
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 殼改薄、開關開時仍呼叫同一支,不複製。
main.py gunicorn options 加 worker_class(env GUNICORN_WORKER_CLASS)、threads(GUNICORN_THREADS);P-0 只加入口不翻預設,E-1 依 P-0 結論翻成 gthread/16。install.sh 寫 .env 補這兩個變數。
| 側 | 變數 | 預設 | 擋在哪 |
|---|---|---|---|
| 主系統 | 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,跑派工/重啟/升級/換證/設定變更五條全綠。
| 元件 | 現況 | 本案動作 |
|---|---|---|
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 |
批=可同時跑的一組(6 批,無人值守一路跑);登記型檔各只讓一張卡碰(契約 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 |
| 1 | F-1 | jedi-packages | 契約 0.3.0:長輪詢取指令與回答的形狀 | extend commands.py(登記型 owner) |
— | opus/medium |
| 1 | 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 |
| 2 | E-1 | main | 環境卡:釘新版套件、gunicorn 翻預設、agent_requests migration |
extend pyproject/main.py/install.sh;new migration |
F-1 P-0 | sonnet/medium |
| 3 | A-1 | main | 等回答插槽:ask/fire/reply、Redis 跨 worker、配額、過期;實作四 port | new service+entity/repo/waiter/spool;extend DI/plugin mount/排程 | E-1 F-3 | opus/high |
| 4 | A-2 | main | 指令六種改「寫正本+喚醒」,取消與換證寫 agent_requests,直推加開關 |
extend _agent_commands/_agent_config_notify/_agent_keys/AgentCommandService |
A-1 | opus/medium |
| 4 | A-3 | agent | 回答九種接線、taken→done/failed、抽 app 層 | extend 分派表;new scan/app/cancel/probe、files/app/blob_ops |
F-2 | opus/high |
| 4 | A-4 | jedi-packages | jedi-detection 三 client 改 channel port | extend 三 client;new port | A-1 F-1 | sonnet/medium |
| 5 | B-1 | main | 證據檔四操作改走插槽;開工先釘 A-4 的 jedi-detection 版 | extend remote_agent_adapter、pyproject |
A-1 A-3 | opus/medium |
| 5 | B-2 | main | 遠端查日誌改 ask | extend agent_log_query_app_service |
A-1 A-3 | sonnet/low |
| 5 | B-3 | fe | 連線欄三態改二態 | extend RemoteAgentManage.vue |
A-2 | sonnet/low |
| 5 | B-4 | main | Agent 工具帳密設定/清除改走插槽(補卡,CM-3619) | extend agent_credential_client/_agent_tool_credentials,拆交易 |
A-1 A-3 | opus/medium |
| 5 | B-5 | jedi-packages | 套件連線欄算法改二態、發 1.17.1(補卡,CM-3631) | extend heartbeat.connection_state |
— | sonnet/medium |
| 6 | C-1 | main | 釘 remote-agent 1.17.1、守衛測試「直推開關預設關」 | extend pyproject;new 守衛 | A-2 B-1 B-2 B-3 B-5 | sonnet/medium |
| 7 | C-2 | main | 文件收口:FR-170/171 指路行、手冊、資安報告、README(暫緩,真機驗收過後派) | extend 文件 | C-1、真機驗收 | sonnet/low |
| 8 | B-6a | jedi-packages | 契約加 KIND_VERIFY_FILE、發 0.5.2(補卡,CM-3634) |
extend commands.py |
— | sonnet/low |
| 9 | B-6b | agent | Agent 接 verify_file 回 sha256(補卡,CM-3635) |
extend blob_ops/files/api/commands |
B-6a | sonnet/low |
| 9 | B-6c | jedi-packages | 套件 reconcile 改走 file_verifier port、發 1.17.2(補卡,CM-3636) |
extend contract/remote_agent_service |
B-6a | sonnet/medium |
| 10 | B-6d | main | 主系統注入 file_verifier=每檔 ask(verify_file);釘 0.5.2/1.17.2(補卡,CM-3637) |
extend _agent_channel |
B-6b B-6c | sonnet/low |
各卡的改哪些檔、新開哪些檔、驗收條件以 cards.py 為準(Notion 卡從它產)。
runner 各卡用本機 Agent 模擬單向網路(DEV DB 把 Agent base_url 改成不存在位址)做;母卡收口時首腦在 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 | 連線欄 | 只剩在線/離線 |
gthread 執行緒常態吃滿)。決策者 11-07 要求工作坊半夜放著跑、中途不被打斷。v1.0 有三處要人介入,全改機械化;批次隨之重排。
| 項 | v1.0 | v1.1 | 為什麼 |
|---|---|---|---|
| 批次 | 7 批:P-0 獨立批 1、F-1~F-3 批 2、E-1 批 3… | 6 批:P-0+F-1+F-2 批 1、F-3+E-1 批 2、A-1 批 3、A-2~A-4 批 4、B-1~B-3 批 5、C-1 批 6 | F-1 契約與 F-2 Agent 元件不吃 P-0 結論(兩條路同一支端點、差在加不加 wait),不必等;只有 F-3(端點吊不吊)與 E-1(翻不翻預設)要讀結論 |
| D2 判定 | P-0 回寫後決策者看數字拍板 | 判據寫死:gthread 吊 20 條 p95 比基準慢 < 100 ms 且 pytest 無新紅 → 長輪詢,否則退定時輪詢;P-0 自己寫結論,E-1/F-3 開工讀證據檔照做 |
可機械判定的就不停下問 |
| 批 4、5 驗收環境 | 123 Agent 主機擋 8443 入站(要人去做) | runner 用本機 Agent、DEV DB 把 base_url 改成不存在位址模擬直推失敗;真機擋埠在母卡收口時首腦做 |
runner 不碰 123、不動防火牆;模擬驗的是「長輪詢路徑通」,真機驗的是「防火牆下整張都通」,兩者分層 |
| A-4 發版後主系統釘版 | 首腦合回時做 | B-1 開工第一步做 | 批 5 第一張開工就釘,不留給人 |
| E-1 依賴 | F-1 F-3 P-0 | F-1 P-0(F-3 同批,1.13.0 釘不到就先釘契約、A-1 開工再釘) | F-3 挪進同批後不能互依 |
| A-1 依賴 | E-1 | E-1 F-3 | 補上 F-3(原本經 E-1 間接依賴) |
| 補卡 B-6a~d | — | 批 8~10:契約加 verify_file kind → Agent handler+套件 reconcile 改 file_verifier port → 主系統注入 ask |
首腦驗收抓到 Agent 管理頁「對帳」仍走直推 GET /blob/<uid>/sha256(§1.2 盤點時併進證據檔沒單獨列,B-1 只改四操作),開關關著每檔 error;影響範圍只有「證據存 Agent 側」租戶 |
| 拆卡 C-2 | C-1 含釘版+守衛+四項文件 | C-1 只留釘版+守衛;文件拆到 C-2(CM-3632)批 7 暫緩 | 決策者 11-07 裁「文件不用現在寫,太早」——文件描述終局,真機驗收退回程式就不用重寫文件;首腦驗收過後改回 Not started 派 |
| 補卡 B-5 | — | 批 5 加 B-5(CM-3631):套件 connection_state 改只看心跳、發 1.17.1;C-1 開工釘 |
連線欄的值是 jedi-remote-agent 算的,直推關掉後每台在線 Agent 都會被算成 one_way(語意錯),B-3 只改 FE 碰不到套件——首腦拆卡時漏了套件那一半;工作坊首腦驗 B-3 時補開 |
| 補卡 B-4 | — | 批 5 加 B-4(CM-3619):Agent 工具帳密設定/清除改走插槽 | A-4 卡列的 agent_credential_client.py 實際在主系統 infra/remote_agent/、不在 jedi-detection(首腦拆卡時位置寫錯),A-4 動不到;工作坊首腦驗 A-4 時依補卡六條規則開,排最早可放的批 5 |
全程剩下唯一要決策者的動作:母卡 Done。中途 runner 停下(契約對不上、測試紅修不好)屬意外,不是設計上的裁決點。