FR-166 · 設計文件 v2 · 2026-10-28(D1~D23 定案;v1 的 41 張+手測追加 29 張)

主系統斷線,現場照樣能登入、能掃、能查線路,連回來自動補傳

直接打 Agent 網址登入(三層身分)、八支掃描工具離線手動執行、網路除錯、斷線續傳、多據點報告回總部;誰能登與能掃誰是兩道門

老闆要 Agent 在與主系統斷線時仍能獨立作業。本案給 Agent 主控台加一頁登入與四個分頁:登入分三層(安裝時設的本機管理員、主系統授權的離線操作者、主系統帳號跳轉),掃描範圍一律照主系統下發的清冊、不因離線放寬;網路除錯以總連線數計噪音、超門檻先確認;結果先存 Agent、連回主系統自動補傳;LDAP 連線抽成獨立套件讓主系統與 Agent 共用,憑證改首次信任釘住。離線操作者的權限跟主系統同一份,名單只管能不能離線登入。本文件把 23 項決策展開到「照做就能交」;v1 的 41 張卡 9 批之外,手測又追加 29 張(§10)。

D1~D23 定案 v1 41 張 9 批+手測追加 29 張 跨 5 repo:BE/FE/jedi-python-package/evidence-agent/test 推翻 CM-2239:LDAP 憑證改首次信任釘住 陷阱:誰能登≠能掃誰,掃描範圍永遠過清冊 陷阱:登記型檔先佔位,後面的卡不碰

狀態:v2:全部實作合回 feature/FR-139,決策者手測中|日期:2026-10-28|母卡 CM-3109、子卡 CM-3110~CM-3168(v1)、CM-3170~CM-3196(手測追加,§10)|前作:FR-157(主控台殼、入場券、救援碼、工具帳密)、FR-156(8444 門、第三方元件掃描在 Agent)、FR-056(派工骨架)、FR-129(清冊) 討論稿(選項推演、§13 全案重新檢視、§14 全量工作清單):discussion.html|雛形:prototype/index.html(十二幕互動,上方「跟著走」四條流程) 開發 branch:五個 repo 2026-10-26 查皆在 feature/FR-139;runner 開工 git branch --show-current 再確認,不對就停下問,不切

🔴 這份是設計決策,不是現況。 決策(§3)與行為規則(§5)已更新到 v2;§4 現況盤點與 §6 拆卡表的行號與座標停在 v1 定稿當時。上線後的畫面與 API 現況看 SPEC 頁。

變更紀錄

日期 變更 對應
2026-10-26 初版設計定案(D1~D22),41 張卡 9 批;同日開卡 母卡 CM-3109,子卡 CM-3110~CM-3150
2026-10-28 v2:D22 只剩內建儲存、新增 D23;AD 登入改填 AD 帳號反查名單;離線登入天數改租戶設定;§5.1/§5.5/§5.6/§8 同步;新增 §10 手測追加卡 CM-3170~CM-3196

1. 需求背景與端到端

老闆 2026-10-26:Agent 與主系統斷線時仍能獨立作業——直接打 Agent 網址登入、不經任務按八支掃描工具、網路除錯(ping、traceroute 這類,使用者勾選,有些公司會告警)、斷線不停工復線補傳、多據點(高雄、台南、日本、新加坡)各自作業報告回台北;不能變跳板或駭客工具。決策者追加:登入方式比較業界模式、LDAP(老闆會要)、重啟 Agent、總覽像儀表板、登入頁與殼照主系統改 Agent 模式。

現況(FR-157 後):Agent 8444 有主控台三頁(總覽、檢測工具、檔案),只能從主系統簽入場券跳進去;斷線時沒有任何登入方式。八支工具全由主系統心跳領任務執行,Agent 自己沒有「選工具、填目標、按執行」的入口。結果回報失敗就丟。沒有網路除錯。

做完之後

sequenceDiagram
    participant U as 現場使用者
    participant AG as Agent 8444
    participant M as 主系統
    M-->>AG: 心跳回應:離線操作者名單(誰、範圍、到期)+授權清冊+CIDR
    Note over AG: 名單與清冊存本機 SQLite;Agent 端沒有「新增使用者」
    U->>AG: 直接打網址,登入頁依連線狀態給分頁
    alt 在線
        U->>M: 「主系統登入」跳回主系統驗,帶入場券回來
    else 離線
        U->>AG: 帳號+離線密碼(或 AD 密碼);本機管理員 local-admin
        AG->>AG: 名單內?未到期?密碼對?沒被鎖?四種失敗同一回應
    end
    AG-->>U: 既有 30 分鐘 session(scope 照名單範圍)
    U->>AG: 掃描工具:選工具→選目標→執行
    AG->>AG: 目標過 allowed_targets.check()(與派工同一支);未同步=一律拒
    AG->>AG: 走 _run_one 同一條,result 走本機 sink 寫 local_runs
    AG-->>M: 心跳後 outbox.drain():摘要與報告參照回主系統(任何終態冪等)
圖 1 — 三層登入與兩道門:誰能登由身分決定,能掃誰由主系統下發的清冊決定
flowchart LR
  R[任務/手動執行完成] --> S{主系統連得到?}
  S -->|是| M[(主系統 agent_tasks/agent_local_runs)]
  S -->|否| O[(Agent SQLite outbox)]
  O -->|心跳成功後 drain 50 筆/次,送成功才刪| M
  M -->|同 uid 任何終態重送| I[回 200 不重做]
  T[主系統派工 Agent 離線] -->|7 天內| W[顯示「等待回傳」不計失敗]
  T -->|超過 7 天| F[reaper 標 failed agent_unreachable]
圖 2 — 離線續傳:結果先存 Agent,連回自動補傳;主系統派工 Agent 離線 7 天才收尾

2. 分工概述(30 秒版)

整案:Agent 主控台多一頁登入與四個分頁(掃描工具、網路除錯、執行紀錄、服務與重啟),主系統連得上照舊跳轉,連不上用事先授權的離線密碼或本機管理員;能掃誰一律照主系統清冊;結果先存 Agent、連回自動補傳;LDAP 抽套件共用。

階段 做什麼 做完之後客戶多了什麼 停在這裡會怎樣 批
P0 底層 LDAP 連線抽成 jedi-ldap、Agent 本機 DB 版本管理、主系統兩張新表與心跳欄位、補主系統 LDAP 三個資安洞 看不到新東西;主系統 LDAP 登入變嚴(連錯會鎖、憑證換了會擋) 不會壞 1~3
P1 登入 Agent 登入頁、三種身分、主系統勾離線操作者、安裝設本機管理員密碼、主控台連入白名單 直接打 Agent 網址能登入;管理員在主系統勾離線操作者 登進去只有既有的三頁(總覽、檢測工具、檔案) 1~5
P2 離線能做事 手動掃描、網路除錯、執行紀錄、重啟、儲存設定 主系統連不到也能掃、能查線路、能重啟 結果只在 Agent 上 2~6
P3 接回主系統 outbox 續傳、主系統收紀錄頁、任務頁「等待回傳」、7 天收尾、主系統要求重啟 連回自動補上;台北看各區做了什麼;任務不永遠卡執行中 不會壞,結果留 Agent 5~8
P4 LDAP 與收尾 Agent 接客戶 AD 三選一、手冊、SPEC、守衛測試、資安例外登記、e2e 分公司用自己的 AD 登 Agent 不會壞 7~9

決策者檢查法(每階段對照雛形幕):P1 幕 1、10、12;P2 幕 2~6;P3 幕 5 回傳欄、幕 10 下半;P4 幕 7、11。PM 裁全做,無可砍項。


3. 決策定案(D1~D23)

全文與選項推演在討論稿 §9;這裡列定案與一句理由。

# 決策 定案 理由 被排除
D1 離線登入模式 三層:安裝時強制設 local-admin(既有救援碼 sub 加密碼、永不過期、仍過清冊)+主系統授權離線操作者(心跳下發名單、本人在線時設離線密碼、到期日;權限見 D23)+入場券跳轉;救援碼保留;Agent 端沒有新增使用者;本機管理員不由主系統遠端停用或重設 名單只在主系統一處管;主系統密碼不離開主系統;業界(Nessus/FortiGate/UniFi)皆安裝時建本機 admin 純本機多帳號
D2 有效期與鎖定 授權預設 90 天可選、上限一年;離線登入天數由租戶在主系統設定(預設 7、1~30 天),心跳下發,Agent 本機只能再收緊(可關閉離線登入);超過天數停用離線密碼登入與獨立掃描,AD 登入不受天數限制;已登入的離線操作者每個請求重驗;5 次鎖 15 分(單調時鐘,整台重開才解);密碼規則同主系統(12 碼+大小寫+數字);local-admin 永不過期,登入時通知租戶管理員、密碼 180 天提醒 清冊可能早過期;搬走的 Agent 不能永遠獨立運作;天數是租戶的資安政策 不限期/Agent 端自訂天數
D3 獨立掃描目標範圍 仍限清冊,離線用快取不放寬;手打限授權網段;八支全開,ZAP 網址解析成 IP 後過同一張清冊(Agent allowed_targets.py 補網址欄位) 決策者:所有工具都要能在 Agent 獨立執行;不拿 FR-129 當理由砍 離線放寬/拔 ZAP
D4 結果落點 主系統新表 agent_local_runs+Agent 管理頁「自主執行紀錄」分頁;報告本體留 Agent;第三方元件掃描走既有 source_scans 不進此表 與任務證據分開、多台彙整一頁 塞 detection_executions
D5 除錯工具 本機儀表板(資訊與連線狀態分開)+十項診斷全進第一版;結果=路徑圖+判讀卡(確定性規則) 老闆要 ping/traceroute;判讀要多項合起來 分期
D6 除錯目標 清冊+主系統+閘道+DNS 四類;手打限授權網段;未同步只准後三項 不能變跳板 自由輸入
D7 離線續傳 Agent outbox(任務結果永不丟)+心跳 drain;主系統任何終態冪等;「等待回傳」顯示;離線判定遲滯 3 敗/2 成 實查回報只對 cancelled 冪等,succeeded 重送會重產證據 主系統主動拉
D8 多據點 不加據點欄,靠 Agent 名稱;手冊寫命名建議 決策者:「不就是 Agent Name 就好」 據點欄
D9 8444 對誰開 安裝時設底線 CIDR、主系統只能收緊取交集;隱含 loopback;交集非空驗證;CLI reset 設錯不能把自己鎖外面 預設只內網
D10 明確不包 命令列、SSH 跳轉、埠範圍、nmap 探測、ARP 掃描、tcpdump、iperf、刪證據、Agent 端新增使用者——守衛測試 駭客工具線 —
D11 本機狀態存放 不起 Postgres;四張新表進既有 SQLite 單機幾千筆無並發 起 Postgres
D12 未同步狀態 沒收過清冊或名單=只有 local-admin 可登、只准主系統連線診斷 決策者:沒同步就預設沒完成設定 放寬
D13 安裝與升級 新裝強制設 local-admin 密碼;升級互動照問、無人值守不強制改紅字;加 reset-admin-password/wipe/console-allow-cidr reset;re-enroll 擴充清本機 無人值守升級不能卡 升級也強制
D14 重啟 Agent 主控台「服務與重啟」頁+主系統心跳旗標 when_idle/now;Agent 優雅結束由 systemd/compose 重拉 決策者:從主系統發動=跳轉進主控台按 Agent 呼叫 systemctl
D15 永久離線任務收尾 主系統派工 Agent 離線 7 天自動 failed(agent_unreachable),具名 system_context 排程 實查主系統沒有派工逾時 永遠等
D16 Agent 本機 DB 版本 最小 schema_version+有序 migration list;既有 upload_files 當 v1、本案四張表 v2 收斂「每表各自 heal」 alembic
D17 Agent LDAP 來源 三選一:跟主系統同步(預設)/這台自己設/不接;名單仍主系統決定,LDAP 只驗密碼。AD 分頁帳號欄填 AD 帳號(跟主系統登入一樣),Agent 用名單上主系統下發的 ad_account 反查是誰;未同步不准 AD 登入 各區可能不同 AD;使用者在主系統就是填 AD 帳號,不能要他記第二個 只同步/填主系統帳號
D18 LDAP 憑證與加密 首次信任釘住、拿掉「驗證憑證」勾選框、預設 STARTTLS;推翻 CM-2239;主系統設定頁與 Agent 同步改 客戶沒憑證可貼;釘住零操作擋中間人 維持勾選框
D19 LDAP 實作共用 抽 jedi-ldap 零相依套件(只靠 ldap3),jedi-iam 瘦身呼叫,Agent 裝它不裝 jedi-iam 不綁表、不綁 Flask 才能復用;jedi-common 版本線衝突 複製一份
D20 除錯噪音怎麼算 以這次總連線數計,執行前算給使用者看;超過 10 次或含中高項目跳確認框列原因;埠一次上限 5、連線間隔 1 秒;門檻寫死 決策者:「全勾不就一堆」 只能勾一項
D21 主控台檔案頁權限 維持 FR-157 D7 租戶層;授權頁與手冊寫明「勾了=看得到這台全部檔案」;專案層另開 FR 離線操作者逐人勾可控;專案層是獨立工項 本案提前做
D22 安裝題目搬 UI 只搬證據檔儲存位置進「服務與重啟」頁,且主控台只能選內建 SeaweedFS;自有 SeaweedFS/MinIO/本機磁碟只能用 CLI configure-storage;重新註冊與升級留 CLI 客戶不能從畫面把證據指到別處;重新註冊影響大且罕見 主控台四型全開/全搬/不搬
D23 離線操作者的權限從哪來 跟主系統同一份:名單只管「能不能離線登入、到期日」,不另勾範圍;每次心跳依該使用者當下的 Agent 能力點算出範圍下發(restart 不下放,D14);能力點被拿光的人照列、標撤銷,Agent 踢掉他的 session;主系統沒有任何 Agent 能力點的人不能加入 同一個人在線跳轉與離線登入只有一份權限,主系統改角色下一跳就生效 名單逐人勾範圍(上限為本人能力點)

畫面用字(決策者兩輪審雛形後定)

來源欄一律「主系統任務」「Agent 手動」(不寫「本機」);「身分來源」頁叫「LDAP 設定」;總覽只放當下(狀態燈、這台 Agent、正在執行),歷史在執行紀錄;狀態燈每盞只回答正常/注意/異常加一句原因,Agent 事實另開區塊不上色。回傳三狀態:已回傳(附時間)/待回傳/回傳失敗(附原因),在線可「立即重試」。登入失敗四種同一句(SR-01.1)。

資安例外登記(兩條,reviewer 看到要查得到理由)

  • SR-08.2 逐項回結果:規則寫給「目標自由填」的情境。本案目標限清冊+三固定例外、單目標序列、速率上限、噪音總量門檻;回詳細結果(通不通、斷在哪跳)是功能本體。
  • SR-04.9 內網直連:safe_http_fetch 拒內網,而除錯對象就是內網。用 httpx 直連但套同樣限制(不跟隨跨 host 轉址、最多 5 跳、大小上限、只取標頭)。
  • 兩條在 P4-5 寫進 docs/security-report/requirements/ 註記。

4. 現況接入點盤點

每列都開檔確認過(討論稿 §13.1 實查)。行數=2026-10-26 現況。

元件 現況座標 現況 本案動作 卡
主控台 session Agent core/console/session.py 75:記憶體 SessionStore、滑動 30 分、token_urlsafe(32) 跳轉與救援碼發 session 離線登入、local-admin 走同一個 store;加 revoke_by_sub P1-3
路由守門 core/console/routes.py 212 require_session(scope):無 session 401、scope 不在票上 403 既有三頁 新路由全掛它;新 scope run-local-scan/net-diag/restart 加進 agent_auth.CONSOLE_SCOPES;routes.py 由 P1-4 一次掛四支 blueprint P1-4
本機管理員身分 core/console/local_login.py 188:LOCAL_ADMIN_SUB="local-admin"、_create_local_session 發全 scope 救援碼登入用 不是新身分:同 sub 加密碼路徑,進同一個 _create_local_session P1-3
本機稽核與回傳 core/console/audit.py 138:JSONL、游標、REPORTED_ACTIONS 白名單;core/console/heartbeat.py 76 隨心跳帶 四個動作 REPORTED_ACTIONS 加 local_login/local_scan_started/net_diag_run/restart_requested/report_exported;主系統事件碼對應加 P1-4、P0-6
授權目標清冊 core/allowed_targets.py 203:心跳下發、check(params) 從 hosts/targets 抽目標、從未收到=一律拒;網址刻意不在 _CONNECT_FIELDS(檔頭明寫) 派工 _run_one 整包 check 獨立掃描與除錯呼叫同一支;加網址欄位:urlsplit 取 hostname → getaddrinfo → 過同一張清冊 P2-1
任務執行骨架 core/task_executor.py 369 _run_one:ack → check → 帳密 → connector.run → 上傳 → result;_RUNNING_TASKS 取消 雲端一條路徑 抽 sink:ack/result/report-sink 成可注入介面,雲端 sink=現況、本機 sink 寫 local_runs;不複製 _run_one P2-1
心跳迴圈 core/enroll.py 411 _heartbeat_loop:依序 observers(skew、allowed_targets、error_tracking、vulndb、tool_schemas、console)、dispatch_tasks;401/403/404 已分路 六個 observer P1-3 一次掛三個新 observer(offline_operators/restart_flag/outbox.drain,後兩支先 no-op);heartbeat_status 露 last_error_kind P1-3、P2-9、P3-1
本機帳密 core/credential_store.py 172(Fernet、secrets.key 0600) 工具帳密 手動掃描 credential_resolver 同一份;LDAP 服務帳密、儲存設定祕鑰也存這裡 P2-2、P2-8a、P4-1
工具表單定義 core/tool_schemas.py 95 心跳下發快取 主控台工具頁 掃描工具參數表單從這裡長 P2-2、P2-3
工具頁表單儲存 core/console/tools.py 213 工具帳密專用 抽 secret_form.py:工具帳密、儲存設定、LDAP 自設三者共用;tools.py 淨減 P2-8a
檔案索引與建表 Agent SQLite upload_files;core/app_factory.py 208 create(checkfirst)+_heal_upload_files_columns 沒有 migration 機制 D16:core/local_db/ 最小版本表;既有建表+heal 收成 v1,四張表 v2 P0-4
瀏覽器門 core/browser_plane_server.py 218 綁 0.0.0.0 不限來源 連入檢查:安裝 CIDR 底線∩主系統下發,隱含 loopback P1-5
維運 CLI scripts/install/install.sh 3426、deploy/install.sh 2894(兩份都超標);case 子命令;do_re_enroll;confirm_cloud_certificate 已有「核對指紋 y/N」 無 lib 目錄 開 scripts/install/lib/(本案第一支);新子命令進 lib、case 一行呼叫;新裝加兩題;升級分互動;re-enroll 擴充清本機 P1-5、P1-6
主控台 FE compliance-manager-fe/console/:App.vue 149 phase、api.js 74 401→onSessionLost、Blocked.vue、三 view TabView 殼換 Shell.vue(側欄+頂欄+狀態列);選單清單常數一次寫全九項,各分頁卡只新開 view;Login.vue 取代 Blocked.vue 為 phase='login';Tools.vue 467 表單段抽 SecretForm.vue P1-7a、P1-7b、P2-8b
入場券 jedi-remote-agent agent_ticket_service(purpose=console_login、scope、jti) 兩 scope 三新 scope 走同一張票 P1-1
心跳契約(主系統端) jedi-remote-agent agent_enrollment_service.py 608 _heartbeat;heartbeat_console.py 白名單收法 收 configured_tools/storage/console_events 收 outbox_size/local_admin_set/uptime_sec/last_restart_reason/console_allow_cidr_installed/sync_state/ldap_source;回 offline_operators[]/console_allow_cidr/restart/ldap_config P0-6
結果回報冪等 agent_task_service.py 170 receive_result:只 cancelled 短路 succeeded 重送重做 短路條件擴成任何終態 P0-6
派工逾時 沒有:dispatched_at 只是記錄欄位 永久離線任務永遠執行中 run_daily_tick() reaper(套件)+BE 排程接線(抽 scheduler_agent.py,scheduler.py 714 只加一行) P0-6、P3-5
套件 migration jedi-remote-agent migrations/010-remote-agent-console.sql 為最新 010 011:兩張新表、remote_agents 六新欄、RLS P0-5
能力點 FR-157 scripts/sql/2026-10-22-fr157-agent-console-capabilities.sql(configure/browse-files) 六顆 加 run-local-scan/net-diag/restart,照同形狀一支主線 migration P1-1
主系統接線 BE core/plugins/remote_agent.py 289、_agent_vulndb.py 先例 adapters+_ADMIN_CAPABILITIES 新邏輯各開 _agent_offline_operators.py/_agent_local_runs.py/_agent_ldap_config.py;remote_agent.py 只接線 P1-1、P3-2、P4-2
Agent 管理頁 FE src/views/remote-agent/RemoteAgentManage.vue 766 逼近上限 只加欄與一行掛子元件:OfflineOperatorsDialog.vue、RestartAgentDialog.vue、AgentOfflineBanner.vue、新 view AgentLocalRunsView.vue P1-2、P3-3b、P3-4b
LDAP 登入 jedi-iam ldap_adapter.py 295(目錄段 ~180 行+綁定段 ~30 行,繼承 BaseRepository);LdapConfigForm.vue 279 三個洞:失敗不計鎖定、加密預設無、憑證預設不驗 目錄段抽 jedi-ldap;adapter 瘦成 30 行;LDAP_VERIFY_CERT 退役改 LDAP_TRUSTED_FINGERPRINT;FE 拿掉勾選框 P0-1、P0-2、P0-3、P0-7
離線任務顯示 jedi-detection detection_orchestration_service.py 2375(只讀不加) — 狀態欄位在 jedi-remote-agent _enrich_online 補 P3-4a
第三方元件掃描 FR-156 後執行全在 Agent;上傳 PUT /scan-input 驗主系統簽的票;結果 source_scans 只有主系統入口 scan_input.py 多接受本機 session;結果照走 source_scans 標來源 Agent 手動,不進 agent_local_runs P2-2

5. 詳細設計

5.1 登入與身分(P1)

Agent 端資料(SQLite v2,P0-4):

表 欄 說明
local_credentials sub PK、kind(local_admin/offline/ldap_service)、hash(argon2id)、updated_at、failed_count、locked_until_mono 鎖定計時用 time.monotonic 的偏移存法(重啟即清,可接受)
offline_operators sub、display_name、ad_account、scope JSON、expires_at、revoked、received_at 心跳下發整份覆蓋;撤銷→SessionStore.revoke_by_sub;scope 是主系統當下算好的(D23),Agent 不判斷
local_runs uid、kind(scan/diag)、tool、targets JSON、params JSON(不含帳密)、started_by、status、summary JSON、report_file_uid、created_at、finished_at、synced_at、cloud_uid、sync_error 報告本體走既有 upload_files
outbox id、kind(task_result/local_run/audit)、payload、idempotency_key、attempts、next_retry_at、created_at 上限 10,000;丟棄順序 audit → local_run;task_result 永不丟

登入 API(P1-4):

GET  /console/state                 無需 session;回 {net: online|offline|unsynced|disabled, offline_days, agent:{name,ip,version}, ldap_source}
POST /console/local-session         body ≤1KB {sub, password, method: password|ldap};所有失敗同 401 同 body {error_code:"CONSOLE_LOGIN_FAILED"};成功發既有 session
POST /console/api/me/password       {old, new};只能改自己(local-admin 或本人離線密碼);12 碼

判定順序(local_auth.verify):①sub == LOCAL_ADMIN_SUB → 查 local_credentials,不受到期與離線天數影響。②找人:method=ldap 且目錄是 AD → sub 是 AD 帳號,用 offline_operators.get_by_ad_account 反查名單(網域與帳號各自不分大小寫比對,命中不是恰好一人就當找不到,不猜);其餘情況 sub 就是主系統帳號。找不到、已撤銷、到期 → 失敗。③method=password 另受離線天數限制、本機關閉離線登入時一律拒;method=ldap 不受天數限制,只問 AD、不退回離線密碼,沒綁 AD 的人 AD 分頁一律失敗。④鎖定:failed_count >= 5 且 locked_until 未到 → 失敗。所有失敗同一回應、反查不到也照樣跑一次雜湊(回應時間不洩漏有沒有人綁);原因只進 log 與稽核(not_listed/method_blocked/credential_rejected)。之後 session、稽核、鎖定計數一律記主系統帳號;已登入的離線操作者每個請求都重驗名單與天數。

未同步判定:allowed_targets.received and offline_operators.received_at is not None。未同步時 state.net = unsynced,掃描與除錯(主系統連線診斷除外)入口 403。

主系統端(P0-5/P0-6/P1-1/P1-2):remote_agent_offline_operators(agent_id、user_id、scope JSONB、expires_at、local_password_set_at、revoked_at、tenant_id、RLS FOR ALL)。授權 API 三核:agent 屬本租戶、user 屬本租戶、user 在主系統至少持有一顆 Agent 能力點;授權只收 {user_id, expires_at},不收範圍(D23)。心跳回應 offline_operators[](sub、display_name、ad_account、scope、expires_at、revoked),其中 scope 由 _scopes_for(user_id) 依當下能力點即時算、ad_account 取 jedi-iam user_auth_providers.provider_login(使用者用 AD 登過主系統一次才有)。表上的 scope 欄只是授權當下的快照。remote_agents 加 console_allow_cidr、outbox_size、local_admin_set、uptime_sec、restart_requested、ldap_source。能力點 detection.remote-agent.{run-local-scan,net-diag,restart},路由綁 remote-agent-manage ANY。

CIDR(P1-5):AGENT_CONSOLE_ALLOW_CIDR(安裝時問,空=不限);心跳收 console_allow_cidr 取交集,空交集忽略+稽核;127.0.0.0/8/::1 永遠放行;browser_plane_server 在 accept 後第一件事檢查。主系統端存前驗交集非空(Agent 回報 console_allow_cidr_installed)。

安裝與升級(P1-6,雛形幕 12):新裝題目 4/5 本機管理員密碼(12 碼、二次輸入、argon2id 寫 local_credentials)、5/5 白名單;完成畫面保管框。升級:[ -t 0 ] 真照新裝問(Enter 跳過),假只紅字。新子命令 reset-admin-password/wipe/console-allow-cidr reset 進 scripts/install/lib/,case 一行 source。do_re_enroll 擴充:outbox 非空先問,清名單、清冊、local_runs、outbox。cmd_upgrade 前查執行中任務,互動問、非互動標 cancelled。

5.2 手動掃描(P2-1~P2-3)

sink 抽取(P2-1):

class TaskSink(Protocol):
    def ack(self, task) -> None
    def result(self, task, outcome) -> None       # 失敗可拋,由 outbox 接
    def report(self, task, file) -> str           # 回 file_uid
CloudSink  = 現況三個函式搬進來,行為不變
LocalSink  = 寫 local_runs;report 走既有 file_upload_service;result 同時入 outbox(kind=local_run)
_run_one(task, sink)  # 既有呼叫改傳 CloudSink();手動執行傳 LocalSink()

並發:_RUNNING_TASKS 既有;手動執行加計數,掃描 >2 或診斷 >1 進佇列。取消走既有 cancel_event。離線 30 天/未同步在 start() 擋。

ZAP 網址納清冊:allowed_targets._target_networks 加 _URL_FIELDS(ZAP 的 target_url/urls),urlsplit().hostname → socket.getaddrinfo → 每個 IP 過 _is_allowed_ip;解不到=拒。主系統派的 ZAP 一併受管。

API(P2-2):GET /console/api/scan/tools(八支:code、name、configured、capabilities_ok);GET .../targets(清冊:devices[]、networks[]、version、received_at);GET .../schema/{tool}(既有 tool_schemas);POST .../runs({tool, targets[], params} → 清冊 check → credential_resolver → LocalSink → 回 local_uid);GET .../runs/{uid};POST .../runs/{uid}/cancel。scope run-local-scan。第三方元件:PUT /scan-input/<uid> 多認本機 session(scan_input.py 驗票處加分支),執行走既有 sbom_scan connector,結果走 source_scans 既有收件並標 source=agent_manual。

5.3 網路除錯(P2-4~P2-6)

core/netdiag/:dashboard.py 讀 ip -j addr/ip -j route/resolvectl/ss -ltn/ethtool/nmcli,容器形態(/.dockerenv 或無 NetworkManager)回 unavailable;回 {info:{...}, lamps:[{name,state,why}]} 資訊與燈分開。

十項 probe 共同規則:目標先過 allowed_targets.is_allowed(ip) 或三固定例外(主系統 host、預設閘道、DNS,由 Agent 自己算不收 body);subprocess.run([...], shell=False, timeout=固定);輸出長度上限 64KB、固定 regex 解析;連線之間 sleep(1);同目標每分鐘 3 輪(記憶體 token bucket)。各項逾時:ping 10s/DNS 5s/埠每個 3s(上限 5 個)/TLS 10s/HTTP 10s/SSH 10s/MTU 10s/cloud 序列 60s/traceroute 60s(無 CAP_NET_RAW 回 unavailable)/mtr 式 60s。TLS 用 ssl.create_default_context(),捕 SSLCertVerificationError 當結果;程式不得出現 CERT_NONE。HTTP 用 httpx:不跟隨跨 host 轉址、最多 5 跳、只取標頭、大小上限。

limits.estimate(selection)(D20):連線數 = ping 4 + DNS 1 + 埠 N + TLS 1 + HTTP 1 + SSH 1 + MTU 1 + cloud 5 + traceroute 30 + mtr 60;秒數 = 連線數 + 各項逾時估;等級:含 mtr=高,含 traceroute 或連線數 >10=中,否則安靜。POST runs 等級非安靜需 confirm=true。

verdict.py:確定性規則表(fixture 驅動),輸出結論/依據/建議動作三句;主系統連線診斷固定序列 DNS → 閘道 → 主系統 → 443 → TLS(describe_tls_failure)→ 心跳端點 → 時鐘(clock_skew)→ 代理,三種結論:連不到/連得到但身分被拒(401/403)/憑證驗不過。

報告:JSON+文字+PDF(既有 WeasyPrint 路徑,html.escape);存 upload_files;local_runs.kind=diag。

5.4 續傳與主系統端(P3)

outbox(P3-1):CloudSink.result 失敗 → 入列(idempotency_key = f"{kind}:{uid}:{status}");心跳成功後 drain():依 created_at 送最多 50 筆,退避 1/2/4/8 分上限 1 小時,送成功才刪;heartbeat_status 加遲滯(3 敗→offline、2 成→online);磁碟低於門檻停止入列 audit 類。心跳 payload 帶 outbox_size。

主系統冪等(P0-6):receive_result 短路條件 task.status in TERMINAL → 回 200 不重寫;POST /agents/local-runs 批次 ≤50、agent_id 從通行證取、(agent_id, local_uid) 唯一衝突視為已收;已停用/撤銷 Agent 拒收。

reaper(P0-6 套件+P3-5 接線):run_daily_tick():agent_tasks.status in (dispatched, running) 且該 Agent last_heartbeat_at < now - 7d → failed、fail_reason=agent_unreachable;0 筆不寫 log。BE core/scheduler_agent.py 註冊 @_job_scoped("agent_task_offline_reaper"),scheduler.py 只加一行。

離線顯示(P3-4a/P3-4b):_enrich_online 加 agent_offline_waiting、agent_last_heartbeat_at;FE 任務頁橫幅、清單「待傳」「LDAP」欄。

5.5 LDAP(P0-1/P0-2/P0-3/P0-7/P4-1~P4-3)

jedi-ldap(新套件,只靠 ldap3):

@dataclass class LdapConfig: host, port=389, encryption="starttls"|"ssl"|"none", base_dn, is_ad, bind_user, bind_password, ca_pem=None
class FingerprintStore(Protocol): get(host) -> str|None; set(host, fp, meta)
class LdapDirectory:
    def __init__(cfg, store: FingerprintStore)
    def test_connect() -> ConnectResult(ok, reason: "ok"|"unreachable"|"cert_changed"|"cert_invalid"|"bind_failed"|"plaintext_warning", fingerprint, cert_meta)
    def verify_user(username, password) -> DirectoryUser(uid, name, email, groups) | raises LdapAuthError(reason)

首次連線 store.get(host) is None → 記指紋;之後不同 → cert_changed,不連。encryption="none" 回 plaintext_warning。不 import Flask、不碰任何表。

jedi-iam 瘦身(P0-2):LdapAdapter 只留「DirectoryUser.uid → user_auth_providers → users」綁定段;LDAP bind 失敗走既有 login_failed_count 同一路徑;LDAP_VERIFY_CERT 退役,LDAP_TRUSTED_FINGERPRINT/LDAP_TRUSTED_AT 由 system-core 設定組提供(P0-3)並當 FingerprintStore;/ldap/connect-test 換 host 不帶舊密碼。

Agent 三選一(P4-1):ldap_source ∈ synced/local/none。synced:心跳 ldap_config 帶主系統設定與服務帳密(主系統解密後經 mTLS 心跳下發;Agent 收到後用自己的 credential_store key 加密保存、不落明文、不進 log),指紋由主系統下發;local:主控台表單(secret_form.py)存 credential_store,指紋存 local_credentials(kind=ldap_fingerprint);none:略過。登入分支見 5.1 ③。Nuitka --include-package=ldap3,jedi_ldap。

5.6 儲存設定搬 UI(P2-8a/P2-8b)

secret_form.py:通用「欄位定義 → 驗證 → 祕密欄進 credential_store、其餘進設定檔 → 測試連線 hook → 套用 hook」。工具帳密(既有)、儲存設定、LDAP 自設三者各一份欄位定義。儲存設定的表單只有一個選項「系統內建 SeaweedFS」(D22),GET 不回任何端點、帳號、金鑰,只回 current_type 說明現在存哪;沒有內建儲存的機器(原生安裝、源碼模式)套用與測試回 409 並指向 CLI;「改回安裝時設定」保留。套用後 storage 模組 reload,不重啟。FE SecretForm.vue 同理三頁共用。

5.7 安全規則對照

全表在討論稿 §13.6;每張卡的紀律段帶「資安規範」行,runner 回寫列對到哪幾條。本案特別要守:四種登入失敗同回應(SR-01.1)、body 上限在 before_request(SR-04.1)、機器身分只認通行證且只動自己名下(SR-01.6/01.7)、新表 RLS FOR ALL(SR-02.1)、授權三核(SR-02.4)、argon2id 且 API 不回雜湊(SR-06.1)、密碼欄進遮罩(SR-06.2)、subprocess 不經 shell(SR-04.8/04.10)、CERT_NONE 禁用(SR-03.2)、兩條例外登記(§3 callout)。


6. 拆分(41 張實作卡、9 批)

批(Stage)=工作坊同時開幾個小弟一起跑的那一組:同一批不改同一個既有檔、不依賴對方產出、不搶同一外部資源。階段(P0~P4)只是分組標籤。底表在 handoff/cards.py,兩個斷言已過:依賴都在更早的批;同批同 repo 無兩張卡改同一既有檔。

登記型檔先佔位(解掉 7 組撞檔):core/console/routes.py 由 P1-4 一次掛四支 blueprint(後三支先空殼);core/enroll.py 由 P1-3 一次掛三個 observer(後兩支先 no-op);console/Shell.vue 由 P1-7a 一次寫全九項選單(view 懶載入指向佔位)並佔好 i18n key。後面的卡只填自己的檔。

repo 分布:evidence-agent 15、FE 11、BE 8、jedi 套件 6、test 1。套件異動(發版前統一確認):jedi-ldap 新建、jedi-iam、jedi-system-core、jedi-remote-agent;開發期走 path dependency,發版等令。

批 階段 卡 repo reuse/extend/new+理由 改哪些既有檔 新開哪些檔 依賴 驗收(對照雛形)
1 P0 P0-1 抽 jedi-ldap 零相依套件:目錄連線、首次信任釘住、結構化失敗 jedi 套件 new(抽出):從 jedi-iam ldap_adapter.py 搬目錄連線段,不是重寫;套件只靠 ldap3 jedi-ldap/(整支:LdapDirectory、LdapConfig、DirectoryUser、FingerprintStore protocol、tests) — LdapDirectory(cfg).test_connect() 對 DEV 的 LDAP 回結構化結果;指紋第一次記、第二次比對、變了回 CertChanged;grep -r flask jedi-ldap/ 為 0;tests 綠且含突變(把比對拿掉要紅)
1 P0 P0-4 Agent 本機 DB 最小版本機制:schema_version+有序 migration list(D16) evidence-agent extend:把 app_factory.py 的 create(checkfirst)+_heal_upload_files_columns 收成 v1,不另寫第二套建表 core/app_factory.py 208 core/local_db/__init__.py、core/local_db/migrations.py — 空 DB 啟動→schema_version=2、四張表在;舊 DB(只有 upload_files)啟動→升到 2 且既有列不動;重啟不重套;test 含突變(把 v2 拿掉要紅)
1 P0 P0-5 jedi-remote-agent 兩張新表、remote_agents 新欄、三個能力點、domain/repo/service jedi 套件 extend:照套件既有 domain→repo→service 形狀加兩個 aggregate;能力點進既有 plugin/contract.py 清單 jedi_remote_agent/plugin/contract.py、infra/remote_agent/model/remote_agent.py 87、domain/.../remote_agent_domain_service.py 171、migrations/011-remote-agent-offline-operators.sql(新號) domain/offline_operator/*、domain/local_run/*、infra/offline_operator/*、infra/local_run/*、app/service/offline_operator_service.py、app/service/local_run_service.py — DEV 套 011 後 \d compliance.remote_agent_offline_operators/agent_local_runs 有 RLS 四指令;test_rls_all_tenant_tables.py 綠;service 單元測試綠
1 P1 P1-7a FE console 殼改側欄+頂欄+狀態列、登入頁骨架、選單清單一次寫全(照雛形幕 1~2,先接假 state) FE 主站/console extend:App.vue 的 phase 機制保留,TabView 換 Shell.vue;登入頁從主站 Login.vue 複製版面刪分流。登記型檔集中:Shell.vue 的選單是一份清單常數(九項全寫、view 用 defineAsyncComponent 懶載入、檔不存在先指向佔位),之後各分頁卡只新開自己的 view 檔、不碰 Shell.vue;i18n agent-console.json 各頁的 key 也在這張卡一次佔好(值先放中文占位) console/App.vue 149、console/main.js 51、console/console.scss 91、i18n agent-console.json console/Shell.vue、console/Login.vue、console/state.js — npm run build:CONSOLE 過;開 dist 看到側欄 14rem teal、頂欄、狀態列三態切換;登入頁三分頁依 state 變;grep border-left console/ 為 0
2 P0 P0-2 jedi-iam 瘦身接 jedi-ldap:失敗計入鎖定、憑證釘住欄、換主機重填密碼 jedi 套件 extend:LdapAdapter 刪目錄段改呼叫 jedi-ldap,只留綁定 30 行;鎖定計數走既有 login_failed_count 路徑,不另開計數器 jedi_iam/.../ldap_adapter.py 295、login_config_assembler.py 111、login_service.py 147、user_auth_provider_service.py 199、login_error_code.py P0-1 LDAP 密碼錯 5 次→鎖(與本地同一計數);LDAP_VERIFY_CERT 不再被讀;連線 test 換 host 不帶舊密碼;既有 jedi-iam tests 綠+新增三條突變
2 P0 P0-6 jedi-remote-agent 心跳契約擴充、終態冪等、local-runs 批次端點、restart 端點、7 天 reaper、事件碼 jedi 套件 extend:心跳收發照 heartbeat_console.py 白名單收法加欄位;receive_result 短路條件從 cancelled 擴成任何終態(改既有判斷,不另開路徑);reaper 照 run_daily_tick 既有排程形狀 app/service/agent_enrollment_service.py 608、app/service/agent_task_service.py 170、api/routing.py 142、app/service/heartbeat_console.py、common/code.py(事件碼) app/service/heartbeat_offline.py、app/service/agent_offline_reaper.py、api/routes/local_run_route.py、api/routes/agent_restart_route.py P0-5 同 uid 重送 succeeded 回 200 且 write_success 只跑一次(test);心跳回應含 offline_operators[];POST /agents/local-runs 51 筆回 400、重複 (agent_id,local_uid) 去重;reaper 對離線 8 天的 dispatched 標 failed、7 天不動
2 P1 P1-3 Agent 離線身分:名單快取 observer、local-admin/離線密碼 argon2id、鎖定、有效期、30 天停用、未同步判定;一次掛三個心跳 observer evidence-agent extend:local_login.py 既有 LOCAL_ADMIN_SUB+_create_local_session 加密碼路徑,不新身分;session 走既有 SessionStore 加 revoke_by_sub;observer 照 _observe_allowed_targets 形狀。登記型檔集中:enroll.py 這張卡一次掛 offline_operators/restart_flag/outbox.drain 三個 observer(後兩支先放 no-op 檔),P2-9/P3-1 只填自己的檔、不碰 enroll.py core/enroll.py 411(掛三個 observer)、core/console/session.py 75、core/console/local_login.py 188、core/heartbeat_status.py core/console/local_auth.py、core/offline_operators.py、core/local_credentials.py、core/restart_flag.py(no-op 殼)、core/outbox.py(no-op 殼) P0-4 local_auth.verify(sub, pw) 對名單外/密碼錯/到期/被鎖回同一個結果物件;5 次錯鎖 15 分(monotonic);撤銷→該 sub session 全消;30 天停用旗標對;未同步判定=allowed_targets.received and operators.received
2 P2 P2-4 網路除錯執行器 core/netdiag/:儀表板讀取、十項診斷、estimate 噪音、判讀規則、報告 evidence-agent new(新模組)+reuse:TLS 用既有 cloud_trust.describe_tls_failure、時鐘用 clock_skew、目標守門用既有 allowed_targets.is_allowed();PDF 走既有 WeasyPrint 路徑 core/netdiag/__init__.py、dashboard.py、probes/{ping,dns,port,tls,http,ssh,mtu,traceroute,mtr,cloud}.py、verdict.py、report.py、limits.py — 每支 probe shell=False、參數陣列、逾時固定(grep 守衛);limits.estimate() 對全勾回 >100 次、等級高;埠 >5 拒;verdict 對固定 fixture 回確定結論;grep CERT_NONE 為 0;未同步只放 cloud 序列
3 P0 P0-3 jedi-system-core LDAP 設定組加釘住指紋欄、服務帳密改 Fernet jedi 套件 extend:THIRD_PARTY_LOGIN 設定組加兩鍵;Fernet 用既有 jedi_common 加密 helper(先 grep),不另寫 jedi_system_core 設定 schema、migrations(套件) P0-2 設定存取回 trusted_fingerprint/trusted_at;明文密碼升級後讀回是密文、解得開;既有設定頁不壞
3 P0 P0-7 主站 LDAP 設定頁:拿掉驗證憑證勾選、已信任憑證+重新信任、預設 STARTTLS(雛形幕 11) FE 主站/console extend:改 LdapConfigForm.vue 既有欄位,不新開頁;錯誤碼同步 error-code.json src/views/ldap-config/LdapConfigForm.vue 279、src/config/locales/.../error-code.json、src/config/api/api.js P0-2 幕 11 三變化;grep verify_cert 為 0;加密下拉預設 STARTTLS、「無」在末尾標僅限測試;憑證變了顯示紅帶+重新信任按鈕
3 P1 P1-1 主系統接線:plugins/remote_agent 接 P0-5/P0-6 port、三端點能力點、能力點 migration、CIDR 交集驗證 BE extend:core/plugins/remote_agent.py 既有 adapters 加 port,新邏輯進 _agent_offline_operators.py(照 _agent_vulndb.py 先例);能力點 migration 照 FR-157 2026-10-22-fr157-agent-console-capabilities.sql 形狀 core/plugins/remote_agent.py 289、core/host_capabilities.py 275 core/plugins/_agent_offline_operators.py、scripts/sql/2026-xx-xx-fr166-agent-standalone-capabilities.sql P0-5、P0-6 DEV 套 migration 後三能力點在;_ADMIN_CAPABILITIES 三端點登記;CIDR 無交集回 400;BE 重啟實打三端點 200/403
3 P1 P1-4 Agent 登入 API:local-session、me/password、state;稽核五個新動作;一次登記四支 console blueprint evidence-agent extend:守門用既有 require_session(scope);稽核用既有 audit.record+REPORTED_ACTIONS 加五值。登記型檔集中:routes.py 這張卡一次掛 local_auth_routes/scan_routes/netdiag_routes/ops_routes(後三支先放空 blueprint 檔),之後 P2-2/P2-5/P2-8a 只填自己的檔、不碰 routes.py core/console/routes.py 212(掛四支 blueprint)、core/console/audit.py 138(REPORTED_ACTIONS 加五值) core/console/local_auth_routes.py、core/console/scan_routes.py(空殼)、core/console/netdiag_routes.py(空殼)、core/console/ops_routes.py(空殼) P1-3 四種失敗同 401 同 body(test);body >1KB 413;GET /console/state 無 session 可打、回四態;改密碼要舊密碼;稽核 jsonl 有 local_login 無密碼欄
3 P1 P1-5 主控台連入 CIDR 白名單:安裝底線、心跳交集、隱含 loopback、連入檢查、CLI reset(開 scripts/install/lib/) evidence-agent new(小模組)+extend:browser_plane_server.py 連入檢查加一處;CLI 子命令進新開 lib/(install.sh 超標只准拆),case 一行呼叫 core/browser_plane_server.py 218、config/config.py 162、scripts/install/install.sh 3426(case 一行)、deploy/install.sh 2894(case 一行) core/console_allow_cidr.py、scripts/install/lib/console_allow_cidr.sh — 底線 192.168.10.0/24 時 10.0.0.1 連 8444 被拒、127.0.0.1 放行;心跳下發更寬的被忽略+稽核;console-allow-cidr reset 後不限;兩份 install.sh 都只多一行
3 P2 P2-1 Agent 手動執行骨架:_run_one 抽 sink、local_runs 寫入、並發上限、ZAP 網址納清冊 evidence-agent extend(抽共用):_run_one 的 ack/result/report-sink 抽成可注入介面,雲端 sink=現況、本機 sink 新增;不複製 _run_one;allowed_targets.py 加網址欄位解析(改既有 _target_networks) core/task_executor.py 369(淨減)、core/allowed_targets.py 203 core/task_sinks.py、core/local_runs.py P0-4、P1-3 主系統派工行為不變(既有 tests 綠);本機 sink 寫 local_runs;第 3 個掃描排隊;ZAP https://x/ 解成清冊外 IP 被拒、清冊內放行;離線 30 天/未同步時 start() 拒
4 P1 P1-2 主站離線操作者授權對話框+清單欄(雛形幕 10 上半) FE 主站/console new(子元件):RemoteAgentManage.vue 766 只加一行掛+一欄,不往裡加;表單元件 reuse 主站既有 Dialog/DataTable src/views/remote-agent/RemoteAgentManage.vue 766(一行+一欄)、src/config/api/api.js、i18n src/views/remote-agent/OfflineOperatorsDialog.vue P1-1 幕 10 離線操作者段;沒能力點的格反灰;「Agent 檔案」欄標整台;0 位紅字;撤銷後「有待同步的撤銷」
4 P1 P1-6 安裝與升級流程:新裝強制設 local-admin、升級分互動/無人值守、reset-admin-password/wipe、re-enroll 清本機、upgrade 前問執行中(雛形幕 12) evidence-agent extend:兩份 install.sh 各只在 case 加行、安裝題目段加兩題;新子命令進 P1-5 開好的 lib/;re-enroll 擴充既有 do_re_enroll scripts/install/install.sh 3426、deploy/install.sh 2894 scripts/install/lib/local_admin.sh、scripts/install/lib/wipe.sh P1-3、P1-5 新裝不設密碼裝不完;upgrade </dev/null 不問只紅字;reset-admin-password 後舊密碼失效;wipe 先清 SQLite 再刪目錄;re-enroll 有 outbox 先問;shellcheck 綠
4 P1 P1-7b FE console 登入頁接真端點:三態、四種登入、設離線密碼對話框、我的離線密碼、改密碼(雛形幕 1 五變化) FE 主站/console extend:接 P1-7a 殼與 P1-4 端點;api.js 401 走既有 onSessionLost console/Login.vue、console/Shell.vue、console/state.js、console/api.js 74 console/SetOfflinePasswordDialog.vue P1-7a、P1-4 幕 1 五變化實打:離線密碼登入進總覽;失敗同一句;斷線 30 天只剩本機管理員;跳轉首次自動跳設密碼框;改密碼成功
4 P2 P2-2 手動掃描 API:tools/targets/schema/runs/cancel;第三方元件走既有 scan-input 與 source_scans evidence-agent extend:scan_input.py 多接受本機 session(不另開上傳口);帳密走既有 credential_resolver;表單定義走既有 tool_schemas core/console/scan_routes.py(P1-4 已掛的空殼,填內容)、core/scan_input.py P2-1 GET tools 回八支含帳密狀態;POST runs 清冊外 403;未同步 403;sbom 走 PUT /scan-input 本機 session 成功;取消走既有 _RUNNING_TASKS
4 P2 P2-5 網路除錯 API:dashboard/targets/runs/report;未同步只放 cloud 序列 evidence-agent extend:掛既有 blueprint;守門 require_session('net-diag');寫入走 P2-1 的 local_runs core/console/netdiag_routes.py(P1-4 已掛的空殼,填內容) P2-1、P2-4 POST runs 勾全部回 estimate 超門檻需 confirm=true;未同步只准 cloud;報告 txt/pdf 下載過 scope;local_runs kind=diag
4 P2 P2-8a 維運 API:重啟(優雅結束)、uptime、支援診斷包、儲存設定表單 API(抽 secret_form 與工具帳密共用) evidence-agent extend(抽共用):tools.py 213 的表單儲存段抽成 secret_form.py,工具帳密與儲存設定、P4-1 的 LDAP 都用它;重啟掛 main_app.py 結束鉤子 main_app.py 31、core/console/tools.py 213(抽出)、core/console/ops_routes.py(P1-4 已掛的空殼,填內容;儲存設定路由也放這支) core/console/ops.py、core/console/secret_form.py、core/support_bundle.py P1-3 重啟 when_idle 等任務完才 exit 0、now 立即且任務標 cancelled;診斷包 zip 無帳密無清冊;儲存表單四型別存讀測;tools.py 行數淨減且既有工具帳密 tests 綠
5 P1 P1-8 總覽 API:狀態燈、這台 Agent 資訊、正在執行(雛形幕 2 三區塊) evidence-agent extend:overview.py 33 加三組組裝,燈與資訊分開回;資料來源全 reuse(heartbeat_status、disk_space、local_runs、credential_store) core/console/overview.py 33 P1-3、P2-1 回應三區:lamps[](name/state/why)、info{}、running[];未同步回 registration{} 且其他三區空
5 P2 P2-9 心跳旗標重啟 observer:when_idle/now evidence-agent extend:observer 照既有形狀掛 enroll.py 一行;重啟動作 reuse P2-8a 的 ops core/restart_flag.py(P1-3 已掛的 no-op 殼,填內容) P1-3、P0-6、P2-8a 心跳帶 restart: now→任務 cancelled 後 exit;when_idle→等;回報 last_restart_reason
5 P3 P3-1 Agent outbox:表、入列、心跳後 drain、退避、丟棄順序、離線遲滯、磁碟門檻 evidence-agent extend:_result 走 P2-1 抽好的 sink,失敗入列;drain 掛 enroll.py observer;idempotency_key 配 P0-6 終態冪等 core/outbox.py(P1-3 已掛的 no-op 殼,填內容)、core/task_sinks.py(雲端 sink 失敗→入列) P0-4、P1-3、P2-1 斷網跑完任務→outbox 1 筆;復線一次心跳後 0 筆且主系統收到一次;滿 10,000 先丟 audit 再 local_run、task_result 不丟;3 敗切離線 2 成切在線
5 P3 P3-2 主系統收自主執行紀錄:local-runs 端點接線、落庫、報告下載走資料面 JWT、nickname enrich BE extend:接 P0-6 端點到 plugins;下載 reuse 既有 data_plane_auth;enrich 用 common/util/audit_nickname.py core/plugins/remote_agent.py core/plugins/_agent_local_runs.py P0-6、P1-1 Agent 送 2 筆→agent_local_runs 2 列含 started_by_name;重送不增;下載 URL 帶資料面 JWT 能拿到報告
5 P3 P3-5 7 天 reaper 接線:抽 core/scheduler_agent.py(scheduler.py 714 只准拆) BE extend(拆檔):scheduler.py 加一行呼叫,邏輯進新檔;reuse P0-6 run_daily_tick() core/scheduler.py 714(一行) core/scheduler_agent.py P0-6 排程具名 agent_task_offline_reaper;DEV 造一筆離線 8 天 dispatched→失敗 agent_unreachable;0 筆不寫 log
5 P3 P3-7 守衛測試(Agent 側七條):入口過清冊、無 shell=True、無 nmap/tcpdump/iperf、local-admin 清冊外 403、未同步 403、無新增使用者路由、無 CERT_NONE evidence-agent new(test):照 BE test/test_module_boundaries.py 形狀寫 grep 型守衛 test/test_fr166_guards.py P2-2、P2-5 七條綠;每條有「故意違規一次會紅」的突變證明寫在 docstring
6 P2 P2-3 FE console 掃描工具分頁:步驟條、工具卡、清冊目標、tool_schemas 表單、執行中、ZAP 網址、第三方元件上傳(雛形幕 3 四變化) FE 主站/console reuse:參數表單用主站 DetectionConfigField;步驟條照主站派任務精靈;上傳走既有 scan-input 元件 console/Shell.vue(加選單項) console/views/Scan.vue、console/scanApi.js P1-7b、P2-2 幕 3 四變化實打;手打清冊外 IP 當場擋;ZAP 網址解析顯示;sbom 上傳到這台;執行中進度輪詢、取消
6 P2 P2-6 FE console 網路除錯分頁:資訊區+連線狀態燈、目標、勾選、噪音計算條、超門檻確認框、路徑圖、判讀卡(雛形幕 4) FE 主站/console new(元件):PathFigure/VerdictCard 新開;燈元件與總覽共用(P1-8 同形) console/views/NetDiag.vue、console/components/PathFigure.vue、console/components/VerdictCard.vue、console/netdiagApi.js P1-7b、P2-5 勾項目即時打 estimate 更新噪音條;超門檻跳框列原因;確認後結果路徑圖+判讀卡;匯出 txt/pdf
6 P2 P2-7 FE console 執行紀錄分頁:同一表、來源欄、回傳三狀態、立即重試、報告下載(雛形幕 5) FE 主站/console reuse:主站 DataTable 慣例;重試打 P3-1 的 resend console/views/Runs.vue P1-7b、P2-2、P3-1 幕 5;離線重試反灰;在線重試後變已回傳附時間;拒絕列有
6 P2 P2-8b FE console 服務與重啟分頁+總覽改版:狀態卡、重啟確認框、本機設定、儲存位置表單、診斷包;總覽三區(雛形幕 6、幕 2) FE 主站/console reuse:儲存表單與工具帳密表單同一個元件(Tools.vue 467 的表單段抽成 SecretForm.vue,兩頁共用) console/views/Overview.vue 141、console/views/Tools.vue 467(抽出) console/views/Ops.vue、console/components/SecretForm.vue、console/components/LampGrid.vue P1-7b、P1-8、P2-8a 幕 6 兩變化、幕 2 三態;Tools.vue 行數淨減且既有工具帳密頁不壞;燈元件總覽與除錯頁同一支
6 P3 P3-3 主站自主執行紀錄頁 BE 側:ui_routes+route_capabilities migration(FR-132 守衛) BE extend:照既有 ui_routes migration 形狀一支 scripts/sql/2026-xx-xx-fr166-agent-local-runs-route.sql P1-1 DEV 套後選單有「Agent 自主執行紀錄」且綁 read 或 net-diag;FR-132 守衛測試綠
6 P3 P3-4a jedi-remote-agent _enrich_online 補離線等待回傳狀態欄(detection_orchestration_service 只讀不加) jedi 套件 extend:在既有 _enrich_online 加兩欄;不碰 jedi-detection 超標檔 jedi_remote_agent/.../remote_agent_service.py _enrich_online`` P0-6 任務序列化回 agent_offline_waiting: true+agent_last_heartbeat_at 當 Agent 離線且任務 dispatched/running
7 P3 P3-3b 主站自主執行紀錄頁 FE:篩 Agent/種類/工具/人/日期、報告下載、離線標示(雛形幕 10 中段) FE 主站/console new(view):主站 DataTable 慣例;篩選 SegmentButtons src/config/api/api.js、i18n、路由表 src/views/remote-agent/AgentLocalRunsView.vue P3-2、P3-3 幕 10 自主執行紀錄段;Agent 離線那筆顯示「報告在離線的 Agent 上」;下載走資料面
8 P3 P3-4b 主站 Agent 清單待傳/LDAP 欄+要求重啟對話框+任務頁離線橫幅(雛形幕 10 清單與下半) FE 主站/console new(兩個子元件)RestartAgentDialog.vue、AgentOfflineBanner.vue;RemoteAgentManage.vue 766 只加兩欄一顆按鈕,不往裡加邏輯 src/views/remote-agent/RemoteAgentManage.vue(兩欄+一行按鈕)、任務頁元件、i18n src/views/remote-agent/RestartAgentDialog.vue、src/components/remote-agent/AgentOfflineBanner.vue P3-4a、P3-6 幕 10 清單三欄、要求重啟段、任務頁顯示段;送出重啟後 Agent 下次心跳重啟
7 P3 P3-6 主系統要求重啟接線(BE) BE extend:接 P0-6 端點到 _ADMIN_CAPABILITIES core/plugins/remote_agent.py P0-6、P1-1、P2-9 POST /remote-agents/{uid}/restart 回 200 且下次心跳回應帶 restart;無 restart 能力點 403
7 P4 P4-1 Agent LDAP 三選一:裝 jedi-ldap、ldap_source、同步收心跳 ldap_config、登入多一分支、Nuitka include evidence-agent extend:登入分支加在 P1-3 的 local_auth.py;設定表單用 P2-8a 的 secret_form.py;指紋存既有 local_credentials core/console/local_auth.py、scripts/build/build_agent_release.sh、pyproject.toml(加 jedi-ldap) core/ldap_source.py P0-1、P1-3、P2-8a synced 模式收心跳設定後 AD 密碼登得進;名單外 AD 對仍 401;local 模式自設可連;none 退回離線密碼;Nuitka build 含 ldap3
8 P4 P4-2 主系統 LDAP 設定下發心跳:ldap_config 組裝(服務帳密密文下發、Agent 轉存)、清單 LDAP 欄值 BE extend:照 _agent_vulndb.py 先例新開 _agent_ldap_config.py;設定讀 P0-3 的 system-core core/plugins/remote_agent.py core/plugins/_agent_ldap_config.py P0-3、P0-6、P4-1 心跳回應含 ldap_config(密文);Agent 端轉存後能用;清單 LDAP 欄顯示 synced/local/none
8 P4 P4-3 FE console LDAP 設定分頁:三選一、同步唯讀、自設表單(雛形幕 7) FE 主站/console reuse:表單用 P2-8b 的 SecretForm.vue console/views/LdapSettings.vue P2-8b、P4-1 幕 7;同步模式唯讀;自設測試連線;指紋顯示
9 P4 P4-6 e2e:主站 LDAP 登入鎖定與釘住、Agent 管理頁三個新動作 test reuse:site-regression 既有 page object site-regression features/steps(LDAP、agent-manage) P0-7、P1-2、P3-3b、P3-4b headless 綠;Agent 主控台不在範圍
9 P4 P4-4 SPEC:Agent 主控台頁改版、Agent 管理頁、LDAP 設定頁(writing-feature-specs) BE extend:改既有 spec 頁 docs/spec-site/current/(三頁) 全部實作卡 三頁變更紀錄各一行;文件衛生 grep 0
9 P4 P4-5 手冊與安裝文件:Agent 獨立作業一章、安裝需求、資安例外登記兩條、出貨基線待重產回報 BE extend:手冊既有章節結構;例外登記進 docs/security-report/requirements/ docs/user-manual/、docs/security-report/requirements/ 手冊章節 全部實作卡 章節齊;SR-08.2/SR-04.9 例外登記可查;文件衛生 grep 0;回寫明講出貨基線待重產。Agent repo deploy/README.md 的安裝需求段由 P1-6 順手改,本卡只引用

決策者功課(開工前):批 1 派出前確認五個 repo 都在 feature/FR-139。卡號對照見 handoff/FR-166-STATE.md 任務總表。


7. 端到端驗收

# 步驟 期望
1 主系統 Agent 管理頁勾王小明為高雄 Agent-01 離線操作者(四範圍、90 天) 下次心跳 Agent SQLite offline_operators 有一列;清單欄「1 位」
2 王小明從主系統「開啟主控台」跳進去 自動跳「設定這台的離線密碼」;設完側欄底部「離線登入」可見
3 拔 Agent 對主系統的線;直接打 https://<agent>:8444/console 登入頁三分頁(AD/離線密碼/本機管理員);橫幅「主系統連線中斷」
4 用離線密碼登入 總覽:紅帶斷線第 N 天;狀態燈「主系統連線」異常;正在執行
5 掃描工具:OpenSCAP → 勾 web-01 → 參數 → 開始 執行中進度;完成後執行紀錄一列「Agent 手動」、回傳「待回傳」
6 手打 192.168.77.5(清冊外) 當場擋;執行紀錄一列「拒絕」;稽核有
7 網路除錯:目標 db-01,全勾,開始 噪音條「高」、連線數 >100;確認框列原因;確認後路徑圖+判讀卡
8 只勾 ping 噪音「安靜」,不跳框
9 接回線 兩次心跳內:執行紀錄「已回傳」附時間;主系統自主執行紀錄頁看到第 5 步那筆;下載報告拿到同一份
10 主系統任務頁,Agent 離線期間派一筆到它 顯示「Agent 離線中,等待回傳」;不計失敗;接回線後跑完
11 用 local-admin 登入(在線) 全部分頁可進;「服務與重啟」有「變更」密碼;掃清冊外仍 403
12 斷線 31 天(改 Agent 時鐘或改 last_heartbeat_ok_at) 登入頁只剩本機管理員;離線密碼 401;手動掃描 403;主系統派的任務復線照跑
13 全新安裝 問本機管理員密碼與白名單,不設不給裝完;完成畫面保管框;裝完未註冊打網址只有本機管理員能按,登入後只有主系統連線診斷
14 upgrade </dev/null 不問密碼、只紅字;主控台總覽與主系統清單紅字「本機管理員密碼未設定」
15 主系統 LDAP 設定頁 無「驗證憑證」勾選;第一次測試連線後顯示「已信任」指紋;換憑證後登入被擋、紅帶、重新信任後恢復
16 LDAP 密碼連錯 5 次 鎖 15 分(與本地同計數)
17 Agent LDAP 設定選「跟主系統同步」 心跳後 AD 密碼登得進;名單外 AD 密碼對仍 401
18 主系統「要求重啟」when_idle Agent 跑完任務後重啟;uptime 歸零;清單看到「上次重啟原因」
19 主控台「服務與重啟」改儲存位置為自有 SeaweedFS,測試連線,儲存並套用 不重啟生效;舊檔警語;CLI configure-storage 仍可用
20 守衛測試 Agent test_fr166_guards.py 七條綠;BE test_rls_all_tenant_tables.py、FR-132 路由守衛綠

8. 風險與陷阱

風險 怎麼擋
local-admin 密碼被現場共用、稽核分不出誰 手冊:平常走離線操作者;稽核記來源 IP;主系統「0 位」紅字
離線期間撤銷不了 有效期+30 天停用;主系統標「有待同步的撤銷」
CIDR 設錯鎖自己 隱含 loopback;交集非空驗證;CLI reset;手冊 SSH port-forward
重啟中 drain 到一半重送 idempotency_key+主系統終態冪等
outbox 塞爆 10,000 上限、丟 audit → local_run、task_result 不丟
除錯被客戶 IDS 當攻擊 D20 總量門檻+確認框;單目標序列;間隔 1 秒;手冊「先跟網管講」
兩份 install.sh 超標 新邏輯進 lib/,case 只加行;shellcheck
RemoteAgentManage.vue 766 四個新 UI 全子元件,只加欄與一行
_run_one 抽 sink 動到派工核心 既有 tests 綠為硬條件;CloudSink 行為逐字搬
登記型檔撞批 先佔位給 owner 卡;cards.py 斷言
雛形與 FE 用了左側粗邊框 交稿前 grep 邊框樣式為 0(CLAUDE.md UI 禁令)
jedi-common 版本線(Agent 0.0.31 vs jedi-iam ≥1.3.4) jedi-ldap 零相依,Agent 不裝 jedi-iam
心跳 ldap_config 帶服務帳密 走 mTLS;Agent 收到即用自己 key 加密保存;不落明文、不進 log
AD 帳號撞名(不同網域同帳號、或同一個 AD 帳號綁給兩人) 反查命中不是恰好一人就當找不到;輸入帶網域才比網域
主系統改了角色,離線名單還是舊權限 D23:範圍每次心跳即時算,能力點拿光就標撤銷
下載弱點資料失敗後要等 30 分鐘才重試 主控台「立即檢查更新」清冷卻並喚醒心跳;10 秒去抖、下載中不重複觸發
無人值守升級沒設密碼 紅字三處;不擋升級
判讀卡講錯 確定性 fixture 表;報告註明「規則判讀非 AI」

10. 手測後追加的功能

v1 的 41 張卡合回後,決策者手測再追加 29 張(含首腦直接修的一批)。決策層的變動已併入 §3(D2、D17、D22、D23)與 §5;這裡列每張卡做了什麼。

登入與身分

卡 內容 repo
CM-3170/3171/3172 離線登入天數由租戶設定(預設 7、1~30),心跳下發;Agent 套用並可本機關閉;主系統 Agent 管理頁設定天數、顯示本機管理員最近登入 BE+套件/Agent/FE
CM-3173 AD 登入只問 AD、不受離線天數限制;已登入的離線操作者每個請求重驗 Agent
CM-3174/3175 本機管理員密碼設定時間回報主系統、180 天提醒;本機管理員登入時通知租戶管理員 Agent/BE+套件+FE
CM-3176 主系統與主控台閒置 15 分鐘提醒、30 分鐘登出(共用元件) FE
CM-3177 離線密碼規則同主系統;登入頁 AD 與離線密碼分開送 FE
CM-3178 安裝時主系統位址只收 https(源碼模式明確設定才放 http) Agent
CM-3179 Agent 名稱在主控台與主系統兩邊可改,以修改時間較新者為準 四 repo
CM-3183/3184/3192 名單下發 ad_account;AD 分頁填 AD 帳號、反查名單;失敗原因分三種記 log BE+套件/Agent
CM-3189 離線操作者對話框顯示綁定的 AD 帳號;登入頁提示 FE
CM-3193/3194 D23:離線權限跟主系統同一份;對話框拿掉範圍勾選,改顯示主系統權限 BE+套件/FE

主控台

卡 內容 repo
CM-3185/3186 網址照主系統用一般路徑(/console/runs),重新整理停原頁,不存在的頁 404 Agent/FE
CM-3187/3188 報告線上預覽(/preview 子路由),沿用主系統預覽對話框 Agent/FE
CM-3190/3191 D22:儲存位置只剩內建 SeaweedFS Agent/FE
CM-3195/3196 總覽「弱點資料」卡「立即檢查更新」:清失敗冷卻、喚醒心跳立刻跳一次 Agent/FE

主系統

卡 內容 repo
CM-3180/3182 「Agent 掃描執行紀錄」併入第三方元件的 Agent 手動掃描,選單搬到「合規與稽核」;版面與主控台一致、失敗可看原因 BE+套件/FE
CM-3181 背景工作排程去重 BE

首腦直接修(未開卡):主控台贅述刪除、執行紀錄頁重做(今日/自訂區間、⋮ 操作、重新執行、失敗原因可複製)、錯誤訊息遮罩、麵包屑、版本號加 v;主系統 Agent 管理頁「連線至 Agent」、離線操作者對話框改版、角色權限頁補漏翻譯、主系統「檢測工具」頁先隱藏。施工細節在各 commit 與 CM-3177 卡尾。


9. 這份文件的定位

你想知道 看
老闆說什麼、五種登入比較、§13 死結與既有機制實查、§14 全量工作清單 討論稿
畫面長什麼樣、流程怎麼走 互動雛形(「跟著走」)
每個決策為什麼 本文件 §3
v1 之後手測追加了什麼 本文件 §10
開工要改哪些檔、哪張卡、哪一批 本文件 §4、§6;底表 handoff/cards.py