evidence-agent 從「主系統的手腳」變成「同一個產品的第二個宿主」。49 支平鋪檔收成八個模組(模組在外、層在內);先解 jedi-common 版本斷層讓 Agent 能裝 kernel 庫,搭宿主殼與守衛,再分三期搬遷並抽出主系統 ↔︎ Agent 的契約套件。P0~P3 共 33 張卡,2026-10-31 全部合回主線;對外行為不變。
狀態:P0~P3 完成,已合回三 repo
feature/FR-139(Agent3ab1253、jedifc67330e、主系統18642c6cf)|日期:2026-10-29~10-31|母卡 P0 CM-3232(17 張)、P1~P3 CM-3324(16 張)|取捨全文docs/analysis/2026-10-29-agent-architecture-direction.md|架構圖architecture.html開發 branch:Agent 與 jedi-python-package 2026-10-29 皆在feature/FR-139;runner 開工git branch --show-current再確認,不對就停下問,不切。
| 日期 | 變更 | 對應 |
|---|---|---|
| 2026-10-29 | 初版:定位改判、七模組、四條裁決、命名 guidant-agent、P0 八張卡四批 | 母卡 CM-3232,子卡 CM-3233~CM-3240 |
| 2026-10-29 | 改成無人值守可跑完:套件單卡發 beta、結案卡轉正式版(D10);系統身分改每請求/每執行緒裝(D8);守衛排在宿主殼之後、背景元件與顯示名拆批;十張卡八批 | 新增 CM-3288、CM-3289 |
| 2026-10-30 | P0 全部合回主線;P1 五張卡四批(批 12~15)建卡不放行:shared 收攏、identity 搬遷+接 jedi-password、sync 通道與 observer 掛點、sync 搬遷、console 退聚合 | CM-3319、CM-3320、CM-3321、CM-3322、CM-3323 |
| 2026-10-30 | P2 五張(CM-3325,批 16~19)與 P3 四張(CM-3331,批 20~22)建卡不放行;三期母卡獨立;P2 真機驗證留測試期 | CM-3325、CM-3331、CM-3326~3330、CM-3332~3335 |
| 2026-10-31 | P1~P3 與收口清理全部完成(P2、P3 併入 CM-3324 單一工作坊無人值守跑完);總驗收 2014 綠、49 條路由與重構前相同、reviewer 結論只搬不改;合回三 repo 主線;主系統接 jedi-agent-protocol、Agent 改釘正式版;§8 改終局 | CM-3324 全部子卡 Done;Agent 3ab1253/c99da8c、jedi fc67330e、主系統 18642c6cf/198074868 |
Agent 要有自己的架構與守衛:模組在外、層在內,五個業務模組彼此只走 port,共用的只有不碰 DB 的 kernel 庫與通訊契約。
guidant_agent/
platform/ 宿主殼:啟動、DI 根、SQLite、migration 串接、背景元件 start/stop、完整性閘、錯誤回報
identity/ 本機帳號、離線操作者、LDAP、主控台 session
sync/ 註冊、心跳、outbox、profile、漏洞庫同步
scan/ 任務執行、connectors、工具帳密
netdiag/ 網路診斷
files/ 證據檔:blob API、儲存設定、完整性、瀏覽器直傳
console/ 主控台 API 聚合層,不碰 DB
shared/ 事件碼、錯誤碼、時間、路徑、archive_safety(零相依)
每個業務模組內部:domain/ app/ ports/ adapters/ api/ migrations.py
依賴方向:console → 業務模組 → platform → shared,箭頭只能往右。五個業務模組彼此不直接 import,要跨模組走 platform 註冊的 port。模組內 domain 零 import SQLAlchemy/Flask/requests;SQL 字串只准在 adapters/;模組層級不准有可變全域。
| # | 題目 | 定案 |
|---|---|---|
| D1 | 架構形狀 | 模組在外、層在內(jedi 插件的垂直切片形狀);不照抄主系統四層在外 |
| D2 | DB 通用範圍 | Postgres+SQLite;不做其他 DB。jedi-common 核心方言中立、jsonb_extras 明標 Postgres 專屬、psycopg 改 extras |
| D3 | files 獨立 |
獨立模組;scan→files 走 EvidenceStore port |
| D4 | 工具帳密 | 歸 scan;加密保管底層抽成 platform.SecretStore port |
| D5 | 四支檔歸屬 | browser_plane_server→files;task_sinks→scan(雲端 sink 走 sync port);archive_safety→shared;console/files 查詢→files/app |
| D6 | 模組間 import | 五個業務模組彼此禁止直接 import;platform、shared 可被任何模組 import |
| D7 | 命名 | guidant-agent/guidant_agent。畫面產品名走設定值 product.display_name;systemd/路徑/指令/repo/套件名與品牌脫鉤不再動 |
| D8 | 單租戶身分 | 固定系統身分(tenant_id 常數 1)以 before_request 每請求裝、背景執行緒入口各自包裝飾器——jedi-common 的身分是 ContextVar,每請求每執行緒各一份,啟動時設一次請求裡拿不到;upload_files 以 local_db v7 補欄 |
| D9 | 搬遷方式 | 絞殺式:一卡一模組、git mv、舊路徑留 re-export 殼一到兩週、功能卡與重構卡碰同模組就不同批 |
| D10 | 套件發版節奏 | 開發卡只發 beta(PEP 440 bN),正式版集中在結案卡 3-1;決策者按下結案批即視為確認套件清單。無人值守時單卡不得停下等人確認發版 |
主系統與 Agent 之間有兩條路:Agent 出站的心跳(每 60 秒,拉派工單與各種同步狀態),以及主系統連進 Agent 資料面 8443 的直推(mTLS+綁定 agent 身分的短效 JWT,測試連線、取證據檔、設定/清除工具帳密都走它,當場回結果)。落地版主系統與 Agent 同在客戶內網,直推一定通;SaaS 版主系統在雲端,要客戶開放 8443 進來才通,這是直推的唯一前提。心跳仍把狀態全量重送(實測 123 一跳:請求 5 KB,其中 profiles 3.3 KB、configured_tools 1 KB,沒變也送;回應 3 KB)。決策者方向:狀態類改帶版本號有變才送;SaaS 不想要求客戶開 8443 時,才需要 Agent 出站建 WebSocket 常駐連線讓主系統往下推。本案 P0 不含;開卡時沿用 BE 既有 SocketIO 8002。
| 元件 | 現況 | 本案動作 | 卡 |
|---|---|---|---|
| jedi-common pin | Agent 0.0.31,主系統 1.3.6;sql_exception.py:3-4 頂層 import psycopg |
lazy 化、psycopg 進 [postgres] extras、發 beta 1.3.7b1;主系統自己列了 psycopg,不動 |
1-1、3-1 |
| jedi-file-upload pin | Agent 0.0.22,主系統 1.5.0;DTO 搬到 domain/ports.py;upload_files.tenant_id NOT NULL |
config/config.py:5-7 改 import;每請求/每執行緒裝單租戶身分;core/local_db/migrations.py 加 v7 |
1-2、1-3 |
| 密碼政策 | jedi-iam common/utils/common_util.py:32 validate_password_policy;Agent core/local_credentials.py:68 另寫一份+argon2 |
抽 jedi-password(零資料表零 Flask),jedi-iam 改 re-export;Agent 改接在 P1 |
1-4、3-1 |
| 守衛測試 | Agent 無;主系統 test/test_module_boundaries.py 為範本 |
六支守衛(import 方向、全域、SQL 位置、禁新檔進舊 core、行數、背景元件) | 2-1 |
| 宿主殼 | core/app_factory.py 一支做完所有接線;di_containers/containers.py 39 行 |
guidant_agent/platform/ 新開:bootstrap、根容器、模組 register 掛點、migration 串接 |
2-2 |
| 背景執行緒 | core/enroll.py:355-370 心跳迴圈內逐一呼叫 observers;9 支各自開 thread |
BackgroundComponent 協定(start/stop/health),platform 統一起停 |
2-3 |
| 顯示名 | 寫死「evidence-agent」三處(main_app.py:1、core/app_factory.py:88、core/local_state_cli.py:150) |
product.display_name 設定值,預設「Guidant AI Agent」 |
2-4 |
批=可同時派的一組;一批一到兩張是刻意的,同批的卡不能改同一個檔。登記型檔只讓一張卡碰:Agent pyproject.toml(1-3、3-2 不同批)、core/local_db/migrations.py(1-2)、jedi-common pyproject.toml(1-1、3-1 不同批)、jedi-iam pyproject.toml(1-4、3-1 不同批)。guidant_agent/platform/bootstrap.py 由 2-2 建、2-3 與 2-4 各改一次,故三張各自一批。
| 批 | 卡 | repo | 做什麼 | 依賴 | reuse/extend/new |
|---|---|---|---|---|---|
| 1 | 1-1(CM-3233) | jedi-packages | jedi-common psycopg 改選配、sql_exception lazy 化、SQLite dialect 測試、發 beta 1.3.7b1 | — | extend 三處既有檔;new 一支測試 |
| 1 | 1-2(CM-3234) | agent | 系統身分 before_request+背景執行緒裝飾器;local_db v7(upload_files 補 tenant_id/owner_user_id) |
— | new core/agent_identity.py;extend migrations 尾巴加 v7 |
| 2 | 1-3(CM-3235) | agent | 升 jedi-common==1.3.7b1、jedi-file-upload==1.5.0;config.py 三行 import;全量測試;188 agent.db 複本升級排練 |
1-1、1-2 | extend pin 與 import |
| 2 | 1-4(CM-3236) | jedi-packages | 抽 jedi-password 0.1.0b1;jedi-iam 1.11.0b1 改 re-export;兩支發 beta |
— | new 套件;extend jedi-iam 一支檔 |
| 3 | 2-2(CM-3238) | agent | guidant_agent/platform/:bootstrap、根容器、register 掛點、migration 串接;core/local_db 搬入;core/app_factory.py 退成一行 |
1-3 | new platform 四檔;extend 兩支退成 re-export |
| 4 | 2-1(CM-3237) | agent | docs/ARCHITECTURE.md 範圍憲章+六支守衛測試+超標白名單(凍結清單以 2-2 搬完後 HEAD 盤) |
2-2 | new 八檔 |
| 5 | 2-3(CM-3239) | agent | BackgroundComponent 協定;心跳、outbox、profile_sync、vulndb_sync 改元件由 platform 起停;三種時序全用測試模擬 |
2-1 | new background.py;extend 四支既有檔 |
| 6 | 2-4(CM-3240) | agent | product.display_name 設定值;三處寫死改讀設定;Agent CLAUDE.md 改寫 |
2-3 | new product.py;extend 五支既有檔 |
| 7 | 3-1(CM-3288) | jedi-packages | jedi-common 1.3.7、jedi-password 0.1.0、jedi-iam 1.11.0 由 beta 轉正式版發 Nexus | 2-4 | 只改版號 |
| 8 | 3-2(CM-3289) | agent | Agent pin 改釘正式版 jedi-common==1.3.7;全量測試 |
3-1 | 只改 pin |
前置已成立:FR-166.1(CM-3199)、FR-169(CM-3223)2026-10-29 皆 Done。本案期間 Agent repo 不開功能卡;FR-167 的 Agent 側兩張(P4-5、P4-3)會改 core/enroll.py,不與本案批 5 同時派。主系統改釘 jedi-common 1.3.7/jedi-iam 1.11.0 不在本案,P0 驗收後另開主 repo 卡。
無人值守原則:單卡不得停下等人——不 sudo、不拔網路、不開瀏覽器、不等人確認發版;遇到要人決定的事(版號撞號、前置產物不在、測試紅找不到原因)commit 已完成部分、回寫問題→建議→理由、狀態留 In progress。真機情境(拔網路、真 188 Agent 升級)留測試期由首腦操作。
P1 母卡 CM-3324(獨立於 P0 母卡 CM-3232:P0 工作坊結案要求子卡全 Done,P1 掛同一張會擋結案)。要跑 P1 時新開工作坊選 CM-3324。
前提:發版與線上更新用 P0 版;P0 若要修先合主線再放行批 12。搬前先剪三條線:audit() 與 LOCAL_ADMIN_SUB 搬 shared(1-1);對主系統的 HTTP 通道從 task_executor 抽成 sync 的 adapter、心跳 observer 改走 registry 掛點(1-3,P1 最危險的一張)。identity 這次順便改接 jedi-password,舊雜湊必須仍可登入、驗過即 rehash。identity↔︎sync 四條互依邊在 1-4 用 port 與 platform/health.py 解。殼只在 1-5 刪零命中的,其餘 P2。
| 批 | 卡 | repo | 做什麼 | 依賴 | model/effort |
|---|---|---|---|---|---|
| 12 | P1-1(CM-3319) | agent | shared 收攏 | — | sonnet/medium |
| 12 | P1-2(CM-3320) | agent | identity 模組 | — | opus/high |
| 13 | P1-3(CM-3321) | agent | sync 通道與 observer 掛點 | P1-1、P1-2 | opus/high |
| 14 | P1-4(CM-3322) | agent | sync 模組搬遷 | P1-3 | opus/high |
| 15 | P1-5(CM-3323) | agent | console 退聚合+P1 收尾 | P1-4 | sonnet/medium |
拆卡底表 handoff/cards_p1.py;三條線的分析依據(AST 掃出的跨模組邊、各邊實際用到的符號)在 handoff/FR-170-LOG.md 第 4 棒。
AST 掃出的事實:scan → sync 13 條邊全是 allowed_targets/tool_schemas/outbox/heartbeat_status 四個 → 抽 TargetAuthorizer/ToolSchemaCatalog/ResultOutbox 三個 port 由 sync 註冊(P2-1);scan → console 7 條全是重啟旗標(歸 platform)與表單驗證(歸 scan);console 的 84 條出邊是退聚合不是剪線(只准 import 對方 app/ 與 ports/)。agent.db 建表步驟從 legacy 一張清單拆回各模組 migrations.py 申報,版本號不變、攤平順序用測試鎖死。Fernet 加密保管下沉 platform/secret_store.py(D4 落實)。sonarqube.py、inspec.py 超標只搬不拆。無人值守:真機出包與主系統派工留測試期首腦做。
| 批 | 卡 | repo | 做什麼 | 依賴 | model/effort |
|---|---|---|---|---|---|
| 16 | P2-1(CM-3326) | agent | scan 的三個 port 與歸位 | — | opus/high |
| 17 | P2-2(CM-3327) | agent | scan 模組搬遷 | P2-1 | opus/high |
| 17 | P2-3(CM-3328) | agent | netdiag + files 搬遷 | P2-1 | opus/medium |
| 18 | P2-4(CM-3329) | agent | console 退聚合 | P2-2、P2-3 | opus/high |
| 19 | P2-5(CM-3330) | agent | 刪舊 core/ | P2-4 | sonnet/medium |
jedi-agent-protocol)——批 20~22;收口清理 P3-5 CM-3357 批 23三邊鏡像現況:主系統 common/enum/event_code.py 52 個 AGENT_*、jedi-remote-agent/common/code.py 44 個、Agent event_codes.py 40 個;心跳請求 7 鍵、回應 24 鍵散在各 observer 的 data.get。P3 收成零相依 kernel 套件(事件碼、錯誤碼元組、HeartbeatRequest/HeartbeatResponse TypedDict、派工單鍵),三邊 import;數值與鍵名一字不變,三邊既有不一致只列不修。套件卡只發 beta,結案卡轉正式;主系統改釘與 event_code.py 改 import 是 P3 後另開的主 repo 卡。
| 批 | 卡 | repo | 做什麼 | 依賴 | model/effort |
|---|---|---|---|---|---|
| 20 | P3-1(CM-3332) | jedi-packages | 抽 jedi-agent-protocol 0.1.0b1 | — | opus/medium |
| 21 | P3-2(CM-3333) | agent | Agent 接 jedi-agent-protocol | P3-1 | sonnet/medium |
| 21 | P3-3(CM-3334) | jedi-packages | jedi-remote-agent 1.11.0b1 接 jedi-agent-protocol | P3-1 | sonnet/medium |
| 22 | P3-4(CM-3335) | jedi-packages | 結案(套件側) | P3-2、P3-3 | sonnet/low |
拆卡底表 handoff/cards_p2.py、handoff/cards_p3.py。三期母卡各自獨立(CM-3324 → CM-3325 → CM-3331):工作坊結案要求子卡全 Done,掛同一張會擋前一期結案。
pip show jedi-common 為 1.3.7(3-2 後不帶 b),pip show psycopg 不存在(Agent 不裝 Postgres 驅動);Nexus 上三支正式版存在。.backup 抓),跑 local_db.upgrade 到 v7 後 upload_files 每列 tenant_id 非 NULL;請求內與背景執行緒內上傳各一檔成功。pytest test/ 全綠含六支守衛;故意在 guidant_agent/identity/domain/ 加一行 import sqlalchemy 守衛紅。python main_app.py 起得來,log 第一行顯示「Guidant AI Agent app ready」;改 product.display_name 後重啟字串跟著變。core/app_factory.py 行數 ≤ 30;guidant_agent/platform/ 存在且 di_containers/containers.py 內容搬入根容器。platform 的元件清單裡,stop() 後 thread 數歸零;SIGTERM 子行程 5 秒內結束(測試斷言)。測試期首腦在 DEV Agent 真機拔網路一次。python -c 單執行緒測會過、真 app 上傳照樣撞 NOT NULL。1-2 驗收要求請求內與新 thread 內都斷言。1.3.7b1 排在 1.3.7 之前,pin 要寫完整版號不用範圍;jedi-iam 相依 jedi-password 的下限要含 b1 才解得到。本文件描述 P0~P3 的定案與拆法,四期已全部完成。終局結構以 Agent repo docs/ARCHITECTURE.md 為準(守衛測試即憲章的機器可讀版);取捨理由、被排除的方向、反悔條件在 docs/analysis/2026-10-29-agent-architecture-direction.md,無人值守派工與合回主線的教訓在 docs/analysis/2026-10-31-fr170-unattended-workshop-and-merge-lessons.md。下一期 P4(心跳瘦身、指令類改直推 8443)決策者裁暫緩,分析在 handoff/FR-170-LOG.md 第 4 棒。