FR-166 · 設計文件 v2 · 2026-10-28(D1~D23 定案;v1 的 41 張+手測追加 29 張)
直接打 Agent 網址登入(三層身分)、八支掃描工具離線手動執行、網路除錯、斷線續傳、多據點報告回總部;誰能登與能掃誰是兩道門
老闆要 Agent 在與主系統斷線時仍能獨立作業。本案給 Agent 主控台加一頁登入與四個分頁:登入分三層(安裝時設的本機管理員、主系統授權的離線操作者、主系統帳號跳轉),掃描範圍一律照主系統下發的清冊、不因離線放寬;網路除錯以總連線數計噪音、超門檻先確認;結果先存 Agent、連回主系統自動補傳;LDAP 連線抽成獨立套件讓主系統與 Agent 共用,憑證改首次信任釘住。離線操作者的權限跟主系統同一份,名單只管能不能離線登入。本文件把 23 項決策展開到「照做就能交」;v1 的 41 張卡 9 批之外,手測又追加 29 張(§10)。
狀態: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 |
老闆 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():摘要與報告參照回主系統(任何終態冪等)
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]
整案: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 裁全做,無可砍項。
全文與選項推演在討論稿 §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 看到要查得到理由)
safe_http_fetch 拒內網,而除錯對象就是內網。用 httpx 直連但套同樣限制(不跟隨跨 host 轉址、最多 5 跳、大小上限、只取標頭)。docs/security-report/requirements/ 註記。每列都開檔確認過(討論稿 §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 |
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。
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。
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。
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」欄。
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。
secret_form.py:通用「欄位定義 → 驗證 → 祕密欄進 credential_store、其餘進設定檔 → 測試連線 hook → 套用 hook」。工具帳密(既有)、儲存設定、LDAP 自設三者各一份欄位定義。儲存設定的表單只有一個選項「系統內建 SeaweedFS」(D22),GET 不回任何端點、帳號、金鑰,只回 current_type 說明現在存哪;沒有內建儲存的機器(原生安裝、源碼模式)套用與測試回 409 並指向 CLI;「改回安裝時設定」保留。套用後 storage 模組 reload,不重啟。FE SecretForm.vue 同理三頁共用。
全表在討論稿 §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)。
批(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 任務總表。
| # | 步驟 | 期望 |
|---|---|---|
| 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 路由守衛綠 |
| 風險 | 怎麼擋 |
|---|---|
| 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」 |
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 卡尾。
| 你想知道 | 看 |
|---|---|
| 老闆說什麼、五種登入比較、§13 死結與既有機制實查、§14 全量工作清單 | 討論稿 |
| 畫面長什麼樣、流程怎麼走 | 互動雛形(「跟著走」) |
| 每個決策為什麼 | 本文件 §3 |
| v1 之後手測追加了什麼 | 本文件 §10 |
| 開工要改哪些檔、哪張卡、哪一批 | 本文件 §4、§6;底表 handoff/cards.py |