Agent 架構定案

guidant-agent(現名 evidence-agent)從「主系統的手腳」變成「同一個產品的第二個宿主」之後,程式該長什麼樣。四條裁決 2026-10-29 已定案(第七節),檔案歸屬以本頁為準;P0~P3 共 33 張卡 2026-10-31 全部合回主線(第八節),本頁畫的終局結構已是現況;守衛測試是它的機器可讀版。

一、現在 vs 改完

現在 平鋪

evidence-agent/
  core/                 45 支檔直接躺同一層,7,378 行
    enroll.py  outbox.py  profile_sync.py
    local_credentials.py  ldap_source.py
    task_executor.py  running_tasks.py
    credential_store.py  storage_settings.py
    ... 36 支自己寫 SQL,27 處全域 dict / lock
    console/            3,652 行,自己查 DB、自己管狀態
    netdiag/            2,089 行
    task_executor_connectors/  5,984 行,有 port 抽象
    local_db/           migration 清單
  api/blob  api/detection
  di_containers/        39 行,只接 jedi-file-upload
  common/  config/

改完 模組在外、層在內

guidant_agent/          repo、資料夾、套件統一 guidant-agent
  platform/   宿主殼:啟動、DI 根、SQLite、migration 串接
  identity/   本機帳號、離線操作者、LDAP、主控台 session
  sync/       對雲端那一面:註冊、心跳、outbox、profile
  scan/       任務執行、connectors、工具帳密
  netdiag/    網路診斷(已自成一塊,搬進來即可)
  files/      證據檔:blob API、儲存設定、完整性
  console/    主控台 API 聚合層,不碰 DB
  shared/     事件碼、錯誤碼、時間、路徑等零相依工具

  每個業務模組內部固定四層:
  identity/
    domain/     純 Python 規則,零 import SQL / Flask
    app/        application service,@transaction
    ports/      抽象介面:Repo、Authenticator、Clock
    adapters/   SQLite repo、ldap3、HTTP client
    api/        blueprint,只解 request
    migrations.py

跟主系統的差別只有一個:主系統是「層在外、模組在內」(api/ app/ domain/ infra/ 四個大資料夾,一個功能散四處)。Agent 走「模組在外、層在內」,也就是 jedi 插件(jedi_iam、jedi_remote_agent)已經在用的垂直切片。分層規則、error code、@transaction、守衛測試、800/60 行上限全部沿用主系統,小弟兩邊切換紀律是同一套,只有目錄形狀不同。

二、45 支檔各去哪

行數是現況。歸屬依「這支檔在服務誰」判,不依名字、也不依實作巧合(都起 TLS 不代表同模組)。

platform/宿主殼,約 1,900 行
把 Agent 起起來、把各模組串在一起。沒有業務規則。背景執行緒統一在這裡 start / stop,不准模組被 import 時偷偷開 thread。
  • app_factory.py(拆成 bootstrap+各模組 register)
  • local_db/(migration 清單由各模組申報、這裡依序串)
  • di_containers/(擴成根容器)
  • audit_pipeline.py(logging 管線安裝)
  • error_tracking/
  • common/integrity/(啟動完整性閘)
  • local_ops.py(install.sh 呼叫的升級鉤子)
  • local_state_cli.py
  • restart_flag.py
  • disk_space.py
  • support_bundle.py
  • clock_skew.py
  • host_identity.py、fingerprint.py
  • config/
  • SecretStore port(加密保管底層,從 credential_store 抽出;LDAP 綁定密碼、雲端憑證日後同一條)
identity/約 1,500 行
誰可以登主控台、怎麼驗。密碼政策與 argon2 抽成 kernel 庫後這裡只剩呼叫。
  • local_credentials.py
  • offline_operators.py
  • offline_login_window.py
  • ldap_source.py
  • console/session.py
  • console/local_auth.py
  • console/local_login.py
  • console_allow_cidr.py
sync/約 3,200 行
Agent 對雲端的那一面。心跳、outbox、profile、漏洞庫同步都是「背景元件」,實作 start / stop 給 platform 管。task_sinks 的雲端 sink 由這裡實作、以 port 供 scan 呼叫。
  • enroll.py、agent_auth.py
  • heartbeat_status.py、heartbeat_wake.py
  • outbox.py
  • profile_cache.py、profile_sync.py
  • vulndb_sync.py、vulndb_bundle.py
  • tool_schemas.py
  • cloud_trust.py
  • allowed_targets.py
  • agent_name.py
  • data_plane_auth.py、data_plane_server.py
scan/約 7,500 行
派來的任務怎麼跑。connectors 已有 DetectionConnector port,直接變 adapters。工具帳密三支歸這裡(唯一消費者是掃描工具),加密保管底層走 platform 的 SecretStore port。
  • task_executor.py
  • task_executor_connectors/(→ adapters/)
  • running_tasks.py、local_runs.py
  • scan_input.py
  • probe_runner.py
  • credential_store.py
  • credential_resolver.py
  • credential_handover.py
  • task_sinks.py(雲端 sink 走 sync port)
netdiag/約 2,100 行
現有 core/netdiag/ 整包搬,內部已有 probes 抽象,只補 ports / adapters 的名字。
  • netdiag/verdict.py
  • netdiag/dashboard.py
  • netdiag/report.py
  • netdiag/probes/
  • netdiag/limits.py
files/約 900 行+套件
證據檔的門。真正的檔案邏輯在 jedi-file-upload,這裡是 Agent 側的設定、完整性與瀏覽器直傳。獨立於 scan:主控台檔案頁、直傳、下載加密都與掃描無關,scan 要存報告走 EvidenceStore port。
  • api/blob/
  • storage_settings.py
  • storage_trust.py
  • evidence_sha256.py
  • browser_plane_server.py
  • console/files.py 的查詢邏輯(→ files/app)
console/退成約 1,500 行
只剩路由與聚合。現在 overview、ops、files、audit 自己查 DB 的那些邏輯回到所屬模組的 app service。
  • console/routes.py
  • console/*_routes.py
  • console/overview.py(→ 呼叫各模組)
  • console/ops.py(→ 呼叫 platform)
  • console/stats.py
  • console/audit.py(→ shared 事件碼+platform 管線)
shared/約 500 行
零相依工具。這裡的檔不准 import 任何模組。
  • console/event_codes.py
  • common/code/*_error_code.py
  • common/util/
  • archive_safety.py
kernel 庫(套件側)新抽
沒有資料表、不假設 Flask 使用者與租戶,兩個宿主都能裝。
  • jedi-password(密碼政策+argon2,從 jedi-iam 抽)
  • jedi-ldap(已是)
  • jedi-masking(已是)
  • jedi-agent-protocol 0.1.0(P3 已抽:事件碼、錯誤碼、心跳 TypedDict、派工單鍵;主系統、jedi-remote-agent、Agent 三邊 import)

三、依賴方向與守衛

模組之間

console→identity / sync / scan / netdiag / files→platform→shared
  1. 箭頭只能往右。sync 不准 import console,platform 不准 import 任何業務模組。
  2. 五個業務模組彼此不直接 import。要跨模組,走 platform 註冊的 port(例如 scan 要上傳報告,拿的是 files 註冊的 EvidenceStore 介面)。
  3. shared 零相依,只放純函式與常數。

模組之內

api→app→domain←ports←adapters
  1. domain 零 import SQLAlchemy、Flask、requests。只有 Python 標準庫與 shared。
  2. api 不 import adapters,也不開 session。
  3. SQL 字串只准出現在 adapters/。
  4. 模組層級不准有可變全域(dict / set / Lock)。要保狀態的東西做成類別,由 DI 給單例。
守衛測試擋什麼對應現況
test_module_boundaries.py上面六條 import 方向,掃 AST現在 core/ 任何檔可 import 任何檔
test_no_module_globals.py模組層級 _x = {}、threading.Lock()27 處
test_sql_only_in_adapters.pytext(、execute( 出現在 adapters 以外36 支
test_no_new_flat_core.py禁止新檔進舊 core/ 平鋪層(搬遷期白名單遞減)45 支
test_file_and_function_size.py800 行/60 行,超標白名單只能變短5 支檔、22 支函式超標
test_background_components.py開 thread 的類別必須實作 start()/stop() 且由 platform 持有9 支各自開 thread

四、「功能可能搬到 Agent」怎麼保

最壞的結果是同一個功能在主系統和 Agent 各活一份。保法是把 FR-069 插件契約(D6)往前推一步:一支模組同一份 domain 與 app service,持久層與身分用 port 換。

同一支模組主系統宿主Agent 宿主
domain/app service同一份程式碼
persistence adapterPostgres,多租戶,RLSSQLite,單租戶,固定 tenant_id
身分脈絡 portJWT 使用者+租戶路徑本機操作者+固定系統身分
api 層掛主 Flask app掛主控台 8444 app
設定system_configs(D18 申報制)SQLite 設定表,同一份 schema 申報

不是每支模組一開始就做雙宿主,那是過度設計。規則是:在哪個宿主先出現就在那裡寫,但寫成上面的形狀;真的要搬時補一個 adapter,不重寫。先做雙宿主的只有老闆已說過「主控中心有、Agent 也要有」的:帳號密碼政策、稽核事件、LDAP、檔案清單。

五、名字分兩層,老闆只改一層

層用什麼誰能改
畫面與文件上的產品名設定值 product.display_name,預設「Guidant AI Agent」。主控台標題、log 開頭、guidant-agent-ctl 提示文字都從這裡讀老闆,改一個值
systemd 服務、安裝路徑、維運指令維持 guidant-agent、/opt/guidant-agent、guidant-agent-ctl,客戶機已是這組不改。改了四台部署機要走改名升級
repo、資料夾、Python 套件、pyprojectguidant-agent/guidant_agent,從骨架卡 CM-3238 建第一個目錄起就用新名,舊 core/ 空了即刪一次,之後不動

內部名跟品牌脫鉤是刻意的:import 路徑會寫在幾百處,品牌一換就是全 repo 替換。主系統的 jedi_* 套件就是同一個做法,產品改名過它們一行沒動。repo 改名與本機資料夾改名只能你本人做(涉及 GitLab remote 與其他 session 的路徑),改完 workshop.toml 的 agent = 跟一行。install.sh 升級路徑要認得舊 binary 檔名 current/agent/evidence-agent,否則部署機升級找不到舊版。

六、搬遷方式

原則做法
絞殺式,不大爆炸一張卡搬一個模組,當天合回。重構分支活不過幾天,跟開發分支的差距永遠小。
git mv 搬檔Git 追改名。開發卡後來改到舊路徑,合回時改動自動帶到新路徑。
舊路徑留轉接殼搬完的模組在舊路徑留一支只做 re-export 的檔撐一到兩週,守衛測試對「新寫進舊路徑」報警,到期一次刪。
不與功能卡交錯P0 期間 Agent repo 不開功能卡;FR-167 的 Agent 側兩張會改 core/enroll.py,不與本案批 5 同時派。P1 以後功能卡與重構卡碰同一個模組就不同批。FR-169 新增的兩支 core/ 檔已在、宿主殼卡又會搬走 local_db,守衛的凍結清單與白名單照開工當時 HEAD 盤,不照本頁數字。
無人值守套件開發卡只發 beta(1.3.7b1),正式版集中在結案卡,決策者按下結案批即確認套件清單;單卡不 sudo、不拔網路、不開瀏覽器、不等人確認;要人決定的事回寫問題→建議→理由、狀態留 In progress。

七、四條裁決 2026-10-29 定案

判準只有一句:老闆之後再加需求,會加在哪個格子,六個月後看得出來嗎。

題目定案理由沒選的
files 獨立或併 scan獨立主控台檔案頁、瀏覽器直傳、下載加密都與掃描無關;scan→files 是一條明確的 EvidenceStore port併入 scan:老闆再加檔案功能全塞進 scan
工具帳密三支歸處歸 scan;加密保管底層抽成 platform 的 SecretStore port現在唯一消費者是掃描工具;保管祕密是宿主能力,LDAP 綁定密碼、雲端憑證日後同一條獨立 credentials 模組:提前抽象,等第二個消費者出現再升格
四支歸屬未定的檔browser_plane_server→files;task_sinks→scan(雲端 sink 走 sync port);archive_safety→shared;console/files 查詢→files/app依服務誰判,不依實作巧合—
模組間能否直接 import五個業務模組彼此禁止,一律走 port;platform、shared 任何模組可 import模組可單獨測、可搬到主系統、依賴圖畫得出來守衛擋得住;現在跨模組呼叫約十來處,成本可控寬鬆版「可 import 對方 app 層」:console 直接查別人 DB 長出 3,600 行就是這條路,只是從 DB 層挪到 app 層

八、拆卡:P0 十張卡八批(CM-3232)、P1~P3 十六張批 12~23(CM-3324)全部完成

批=可同時派的一組;一批一到兩張是刻意的,同批不能改同一個檔。登記型檔只讓一張卡碰:Agent pyproject.toml(CM-3235、CM-3289 不同批)、local_db/migrations.py(CM-3234)、兩支套件的 pyproject.toml(開發卡與結案卡不同批)。platform/bootstrap.py 由 CM-3238 建、CM-3239 與 CM-3240 各改一次,故三張各自一批。

批卡repo做什麼依賴
1CM-3233jedi 套件jedi-common psycopg 改選配、sql_exception lazy 化、SQLite dialect 測試;發 beta 1.3.7b1—
1CM-3234agent系統身分以 before_request 每請求裝、背景執行緒入口各自包;local_db v7 給 upload_files 補 tenant_id/owner_user_id—
2CM-3235agent升 jedi-common 1.3.7b1、jedi-file-upload 1.5.0;config.py 三行 import;188 agent.db 複本升級排練3233、3234
2CM-3236jedi 套件抽 jedi-password 0.1.0b1(密碼政策+argon2id);jedi-iam 1.11.0b1 改 re-export;兩支發 beta—
3CM-3238agentguidant_agent/platform/ 宿主殼:bootstrap、根容器、模組 register 掛點、migration 串接;local_db 搬入;app_factory 退成一行3235
4CM-3237agentdocs/ARCHITECTURE.md 範圍憲章+第三節六支守衛測試+超標白名單(凍結清單以搬完後 HEAD 盤)3238
5CM-3239agentBackgroundComponent 協定;心跳、outbox、profile_sync、vulndb_sync 改由 platform 起停;三種時序全用測試模擬3237
6CM-3240agent顯示名設定值 product.display_name;三處寫死改讀;Agent CLAUDE.md 改寫3239
7CM-3288jedi 套件結案:jedi-common 1.3.7、jedi-password 0.1.0、jedi-iam 1.11.0 由 beta 轉正式版發 Nexus3240
8CM-3289agent結案:Agent pin 改釘正式版 jedi-common 1.3.7;全量測試3288

P1(搬 identity、sync)、P2(搬 scan、netdiag、files,console 退聚合,刪舊 core/)、P3(抽 jedi-agent-protocol)已於 2026-10-31 全部合回主線;主系統已改釘 jedi-common 1.3.7、jedi-iam 1.11.x、jedi-remote-agent 1.11.0 並接契約。P1~P3 拆卡表見 design.md §5.1~§5.3。下一期 P4(心跳瘦身、指令類改直推 8443)決策者裁暫緩。