FR-080 宿主側盤點:今天這個系統實際是怎麼組裝/部署/跑的

盤點日期:2026-09-09|branch feature/FR-075|HEAD c2bf55d6 盤點範圍:主專案(宿主)compliance-manager-be。套件本身只在「宿主怎麼呼叫它」的範圍內查證。 所有查證皆唯讀(Read / grep / SELECT)。DB 查詢對象:DEV 192.168.50.188:25432/guidant_ai_dev(cm_app)。


§1

A. 組裝(接線盤)

A1. core/app_factory.py 的 register_* 逐條

create_app(enable_socketio, enable_scheduler, integrity_context) 共 608 行,是唯一組裝根。 七支套件插件在此註冊(另有 10 支走 api/<mod>/__init__.py 的 create_blueprint,見 A1-b):

# 呼叫 行號 套件函式 傳入的 adapters/ports mount_api
1 register_identity core/app_factory.py:301-311 jedi_iam.plugin.register build_identity_api_adapters(container, build_identity_adapters(notification_service))+IdentityConfig(excel_upload_dir=...) True
2 register_file_upload :328-333 jedi_file_upload.plugin.register build_upload_file_adapters(container)、build_upload_file_config() True
3 register_integrity :352-357 jedi_integrity.plugin.register build_integrity_adapters()、context=integrity_context or build_integrity_context()、db_event_writer=_write_tamper_event_to_db 無此參數(不掛 route)
4 register_notification :375 jedi_notification.plugin.register build_notification_adapters(container) True
5 register_system_menu :390 jedi_system_menu.plugin.register build_system_menu_adapters(container) True
6 register_issue :404 jedi_issue.plugin.register build_issue_adapters(container) True
7 register_api_log :421 jedi_api_log.plugin.register build_api_log_adapters(container) True
8 register_survey :471-475(只在 enable_socketio=True) jedi_survey.plugin.register api.survey.build_adapters() / build_config() False(library 模式,一條 route 都不掛,只為寫 app.extensions["jedi_survey"] 供 socket handler 的 ctx();沿革見 :442-466 的 CM-1482 註解)

同一函式內的其他組裝動作(非 register_* 但屬接線):

  • configure_logging()(:71-73)— jedi-common,掛 DBLogHandler 到 7 個頂層 logger(系統 log 寫 DB system_logs 是功能不是除錯)
  • init_database(db_uri)(:131)— jedi-common,建 engine + session factory
  • app.config.update(get_runtime_config())(:144-146)— 從 DB system_configs / ROOT / RUNTIME_CONFIG 覆寫營運參數
  • start_forwarding_chain(LogForwardingConfig(...))(:155-165)— jedi-log-forwarding,syslog/GELF 轉發,起 listener+watcher 執行緒
  • redis_client.init_app(app)(:168)/socketio.init_app(..., message_queue=redis_url)(:179-186,僅 socketio 模式)
  • register_error_handlers(app)(:235,jedi-common)+ _register_local_error_handlers(app)(:236,本地三支:RequestEntityTooLarge/BpmnTopologyError/OtpResendTooFrequentError)
  • init_request_context_isolation(app)(:251)+ _make_reject_oversized_request_hook(:256)+ init_app_interceptor(app)(:259)
  • container.wire(modules=get_di_wire_modules())(:275-281)— DI wiring,全部後續接線的時序紅線
  • register_stage_hooks_to_registry(...)(:425-426)— flow_control 的 stage handler/precondition 註冊進 registry
  • app.extensions['di_container'] = container(:431)— 背景 job / adapter 延遲解析的唯一入口
  • configure_socket_identity(_user_service_provider, _login_token_service_provider)(:437-440)— jedi-iam socket 身分
  • _register_socketio_namespaces()(:481-482,僅 socketio 模式)
  • init_license_readonly_gate(app)(:488-489)— license readonly/locked 全域攔寫入
  • init_scheduler(app, worker_pool_size)(:493-499,僅 enable_scheduler=True)

A1-b. 走 api/<模組>/__init__.py::create_module() 的第二批插件接線

main.py:136-157 迴圈 import api.<name> 並 register_blueprint(create_module())。以下模組的 create_module() 實際上是「組 adapters → 向套件要 blueprint」:

模組 檔:行 套件
ai api/ai/__init__.py:27,46 jedi_ai_bot.plugin.create_blueprint(adapters: history_store=RedisChatHistoryStore(), current_user_id, auth_required, response_builder)
ai_dashboard api/ai_dashboard/__init__.py:19,100 jedi_ai_dashboard.plugin(services/auth_required/license_guard/locale_provider)
detection_tools api/detection_tools/__init__.py:22,127 jedi_detection.plugin(11 個 adapter:license_guard/capability_guard/identity_guard/crypto/evidence_sink/notify_config/agent_directory/platform_admin_check/agent_auth_settings_provider…)
evidence_classification api/evidence_classification/__init__.py:16,92 jedi_evidence_classification.plugin(5 adapter:project_directory/project_role_guard/evidence_source/control_catalog/document_converter)
flow_control api/flow_control/__init__.py:30 jedi_compliance_audit.plugin(兩組 blueprint:task/audit,各帶 license_guard/project_role_guard/identity_guard)
license api/license/__init__.py:44,89 jedi_license_runtime.plugin(notifier/tenant_directory/is_platform_admin/licensed_job_types/integrity_hook)
log_forwarding api/log_forwarding/__init__.py:51,86 jedi_log_forwarding.plugin
participant api/participant/__init__.py:14,130 jedi_participant.plugin(6 service + project_role_guard + user_directory)
remote_agent api/remote_agent/__init__.py:36,87 jedi_remote_agent.plugin.create_blueprint(admin_required/current_user/settings_provider/payload_provider/lifecycle_listener/recon_query/audit)+一支 route 留主專案(GET /agents/files/<uid>,binary 串流,api/remote_agent/routes/agent_file_route.py)
survey / task_survey api/survey/__init__.py:30、api/task_survey/__init__.py:25 jedi_survey.plugin.create_blueprints(12 service + license_guard/project_role_guard/presence_store/audit_nickname_enricher)

合計:18 支套件在宿主有顯式接線點(8 在 app_factory + 10 在 api/*)。

A1-c. config/app_modules.py 還登記哪些主專案模組

REGISTERED_APPS(config/app_modules.py:7-84)現有 31 個條目(另 8 個已註解退役:auth/captcha/resource/system_menu/notification/upload_file/log/issue):

bulletin, system_config, notify_config, survey, flow_engine, ai, task_survey, device,
report, subtask_status_history, user_auth_provider, label, feedback,
project_summary_report, module_frame, translate, oscal, project, participant,
ai_dashboard, flow_control, information_system, cloud_integration,
evidence_classification, remote_agent, version, detection_tools, license, setup,
log_forwarding

(實測 ls api/ 有 33 個目錄;find api -name '*_route.py' 有 72 支 route 檔。)

載入採 fail-fast:任一模組 import 失敗或缺 create_module() 直接 RuntimeError 終止啟動(main.py:140-154)——原本是 warning-only,症狀是「整組 API 無聲消失」。

A2. di_containers/ 的規模與 port adapter 盤

  • 檔案:di_containers/ 下 40 支非 __init__ 的 .py(含 dashboard_apis/ 12 支);總 5554 行。
  • Container 類別:39 個子 Container class + 1 個根 Containers(di_containers/containers.py:76),根內 providers.Container( 出現 39 次。
  • 最大三支:flow_control/flow_control_containers.py 720 行、oscal/oscal_containers.py 507 行、module_frame/module_frame_containers.py 258 行。
  • 循環依賴以 override_providers 事後補:containers.py 內共 11 處(:156, :167, :247, :250, :287, :292, :304, :311, :333, :341, :346, :378, :383, :401),把「宣告順序解不了的雙向依賴」補回去。這是接線盤的複雜度來源,也是 sys.setrecursionlimit(5000) 的成因(di_containers/containers.py:51-73 有完整說明:deepcopy 整張 provider 圖需 ~2000 frames,Python 預設 1000,額度不足時錯誤被 dependency_injector 重包成誤導性的 NonCopyableArgumentError)。
  • DI wire 模組數:config/di_modules.py 動態 rglob 掃描(release 模式改讀 build 期凍結的 config/di_modules_static.py)。實測全 repo 含 @inject 的模組 73 支。

「替套件實作 port adapter」的 container / 檔案完整清單

宿主接線盤的實際長度:15 支專職 wiring/adapter 檔(合計 2004 行):

檔 行數 服務哪支套件 port 名 → adapter 類 → 內部 import
common/iam_ports.py 100 jedi-iam ISettingsReader→EnvSettingsReader(os.getenv);INotifier→NotificationServiceNotifier(包 DI 的 notification_service,threading.Thread 非同步送,:59);OTP 文案→OtpMailBuilder(flask_babel _())
core/iam_wiring.py 210 jedi-iam(route 層) IdentityServices 名冊 17 個 provider(:51-74:user/role/tenant/org_unit/capability/ui_route/import_user/mfa_factory/login_token/login/user_auth_provider/user_change_password + 5 個宿主 wrapper + login_orchestrator);IdentityHooks 四件(_ui_routes_postprocess 用 common.authz.platform+viewer_licensed_modules、_web_menu_postprocess 用 common.authz.menu_license_filter、_password_policy 用 infra.system_config.system_config_root_reader、forget_password_masks_missing_email=True);auth_required=jwt_required()、capability_required=common.authz.require_capability、response_builder=jedi_common.return_response;login_orchestrator 的三張 port 實作在 app/auth/service/login_adapters.py(TurnstileCaptchaVerifier/EmailMfaDispatcher/UserScopeResolver)
core/upload_file_wiring.py 161 jedi-file-upload IUserNameResolver→AuthUserNameResolver(@transaction,延遲解析 container.auth_container.user_domain_service(),:81-106);ISettingsReader→_EnvSettingsReader(只問 LIBREOFFICE_CMD);file_access_guard=common.authz.signed_token_or_jwt;signed_token_issuer=issue_signed_token;services.file_upload_service=宿主的 ManagedFileUploadService(讀 tenant STORAGE_CONFIG);extra_object_storage_types=("remote_agent",)
common/integrity/adapters.py 357 jedi-integrity verifier→LicenseEngineVerifier;fingerprint→MachineIdFingerprint;locator→ProjectResourceLocator;trigger_context_collector→collect_host_trigger_context;IntegrityConfig 帶 lc_base_url/lc_api_token(:191-192,即 LC 座標)
infra/api_log/api_log_plugin_wiring.py 56 jedi-log(jedi_api_log) services.api_log_service(DI provider)/auth_required/platform_admin_required=common.authz.require_platform_admin_route/response_builder
infra/issue/issue_plugin_wiring.py 45 jedi-issue services.member_service(DI provider)/auth_required
infra/notification/notification_plugin_wiring.py 52 jedi-notification services.test_mail_service(DI provider,service 本體留宿主)/auth_required/capability_required/response_builder
infra/system_menu/system_menu_plugin_wiring.py 58 jedi-system-menu services.system_menu_service(DI provider,service 在套件內,名冊化是因為「怎麼建它」是宿主知識)/四件守門
infra/detection_tools/adapters.py 147 jedi-detection EvidenceSinkAdapter/NotifyConfigAdapter/AgentDirectoryAdapter/CryptoAdapter/LicenseGuardAdapter/CapabilityGuardAdapter/IdentityGuardAdapter/platform_admin_check(8 支,全部透過 _di() 從 current_app.extensions["di_container"] 延遲解析)
infra/evidence_classification/adapters.py 148 jedi-evidence-classification IProjectDirectory→ProjectDirectoryAdapter(import jedi_task_platform.domain.entity.project_query_entity);IProjectRoleGuard→ProjectRoleGuardAdapter(import jedi_participant.domain.entity.project_participant_query_entity);IEvidenceSource→DriveEvidenceSourceAdapter;IControlCatalog→LivingSspControlCatalogAdapter;IDocumentConverter→LibreOfficeDocumentConverterAdapter
infra/survey/adapters.py 69 jedi-survey IAuditNicknameEnricher→AuditNicknameEnricherAdapter(用 common.util.audit_nickname);IPresenceStore→RedisPresenceStoreAdapter(Redis list,common.util.redis_client_util.RedisClient);IProjectRoleGuard→ProjectRoleGuardAdapter(common.authz.project.assert_project_manager)
infra/participant/user_directory_adapter.py 93 jedi-participant user directory
infra/remote_agent/recon_query_adapter.py 34 jedi-remote-agent IReconQuery→RemoteAgentReconQuery(import jedi_file_upload.infra.models.upload_file.UploadFile,:11)
infra/upload_file/remote_agent_adapter.py 273 jedi-file-upload(storage backend) remote_agent 儲存後端:httpx 打客戶端 agent(見 B5)
app/auth/service/login_adapters.py 100 jedi-iam(login orchestrator) 三張 port

DI container 內把 adapter 註冊成 provider 的位置(樣本): di_containers/evidence_classification/evidence_classification_containers.py:68-72(五支 Singleton)、 di_containers/detection_tools/detection_orchestration_containers.py:44,104、 di_containers/survey/survey_containers.py:80、 di_containers/remote_agent/remote_agent_containers.py:32-36(DetectionLifecycleListener/DetectionTaskPayloadProvider/RemoteAgentReconQuery)、 di_containers/agent_task/agent_task_containers.py:14。

接線盤總結:15 支 wiring/adapter 檔(2004 行)+ 5554 行 DI container = 約 7,558 行純組裝碼,服務 18 支套件。

A3. 宿主直接 import 套件 infra 層(繞過套件 API 直摸表)

app/ 層 6 處(違反 DDD 分層規範最明顯的一批):

檔:行 import 什麼
app/feedback/dto/feedback_issue_dto.py:5 jedi_issue.infra.issue.adapter.dto.issue_dto.IssueDTO
app/feedback/service/issue_service.py:8-9 jedi_issue.infra...UpdateIssueDTO, CreateIssueDTO、IssueResponseDTO
app/module_frame/service/ssp_import_template_app_service.py:381 jedi_compliance_audit.infra.repository.project_extension_repo_impl(repo_impl 直呼)
app/oscal/service/ssp_document_pool_service.py:30 jedi_compliance_audit.infra.repository.ssp_document_pool_query.SspDocumentPoolQuery(query 物件直呼)
app/upload_file/service/managed_file_upload_service.py:10 jedi_file_upload.infra.repository.upload_file_repo_impl.UploadFileRepoImpl(repo_impl 直呼)

common/ 層:0 處(test/test_module_boundaries.py 有守衛測試擋反向 import)。

infra/ 層 47 處 import 套件 ORM model / mapper(宿主 model 直接繼承/外鍵指向套件表),是「拆成服務後會斷掉」的第一層:

宿主檔 指向的套件表/model
infra/flow_engine/models/ext_workflow_execution.py:2-8 jedi_flow_engine.infra.models(ElementVariable/JobExecution/WorkflowExecution as BaseWorkflowExecution=繼承)、jedi_participant.infra.model(ProcessParticipant/ProjectParticipant/TaskAssignee)
infra/bulletin/models/bulletin.py:1-4 jedi_iam.infra.models(OrgUnit/User)、jedi_bulletin.infra.models.bulletin.Bulletin as BaseBulletin(繼承)
infra/associations/model/*.py jedi_task_platform.infra.model.project.Project(4 檔)、jedi_flow_engine...JobExecution、jedi_device...Device、jedi_iam.infra.models.OrgUnit
infra/feedback/model/feedback_issue.py:1,7 jedi_iam...User、jedi_issue.infra.issue.models.issue.Issues
infra/module_frame/models/module_frame.py:1 jedi_iam...User
infra/project_summary_report/models/project_summary_report.py:2 jedi_task_platform...Project
infra/oscal/clone/ssp_versioning_cloner_impl.py:19-20 jedi_compliance_audit.infra.model.SspReferenceDocument(+Mapping)
infra/readmodel/*(見 B7) 5 支套件的 model

按檔案計數(grep -c 前十):infra/flow_control/repository/flow_control_job_repo_impl.py 16 次、di_containers/flow_control/flow_control_containers.py 14、di_containers/auth/auth_containers.py 14、infra/flow_control/repository/flow_control_project_repo_impl.py 12、di_containers/detection_tools/detection_tools_containers.py 11、infra/readmodel/audit/flow_control_dashboard_repo_impl.py 7、infra/flow_control/repository/job_export_query.py 7。


§2

B. 執行期形狀

B4. main.py 的 RUN_MODE 與 process 形態

只有一支入口(Nuitka 編譯目標只能有一個,main.py:1-5)。RUN_MODE 環境變數切換,非法值直接 SystemExit(:67-70)。

模式裁剪表(main.py:9-18,逐條與程式碼對得上):

載入項 api(預設) socketio
REST blueprints(31 個 REGISTERED_APPS) 全載(main.py:136-157) 不載,只留 GET /healthz(:162-164,不需認證,不掛 /api/1.0)
socketio.init_app + Redis message queue 不載 載(app_factory.py:179-186)
APScheduler 業務 jobs 載(唯一持有者) 不載
Integrity 抽查 job 載(掛主 scheduler,scheduler.py:420-422) 載(獨立 integrity-only scheduler,main.py:173→init_integrity_scheduler)
eventlet monkey_patch 不執行("eventlet" not in sys.modules) 執行(main.py:74-77,monkey_patch(all=False, socket=True))
DI container 全量 全量(main.py:16 明載「第一版求穩,瘦身留 FR-063.4」——即 socketio process 也吃整張 39-container 圖)
SocketIO namespaces 不註冊 註冊(2 個,見下)
承載 內嵌 gunicorn(正式)/Flask dev server(DEBUG) socketio.run(eventlet),port SOCKET_PORT=8002

gunicorn worker 數:GUNICORN_WORKERS 環境變數,預設 4(main.py:258);timeout GUNICORN_TIMEOUT 預設 120s(:260)。gunicorn 是內嵌的(BaseApplication 子類 _EmbeddedGunicorn,main.py:239-269),不是外部 gunicorn main:app——因為編譯後是密封 binary。 post_fork 做三件事(main.py:200-237):① engine.dispose(close=False) 丟掉繼承的 DB 連線池(不做會撞 DuplicatePreparedStatement,實測每 3 個請求炸 1 個 500);② register_gunicorn_master(server.pid)(tamper 終止路徑);③ restart_after_fork() 重建 log 轉發鏈的 listener/watcher 執行緒。

第三種 process:沒有。 沒有 celery、沒有獨立 worker 進程、沒有 cron 容器。所有背景工作都在 api 模式的 gunicorn master process 內,靠 APScheduler BackgroundScheduler(thread pool)跑。

全部背景排程(core/scheduler.py)

主 scheduler init_scheduler(app, worker_pool_size=DRIVE_SYNC_WORKER_POOL_SIZE 預設 4),ThreadPoolExecutor(max_workers=pool),UTC:

job id 觸發 做什麼 屬誰 行號
drive_sync_worker interval 5s cloud_integration_container.drive_sync_worker().run_once(batch_size=10) — 處理 pending drive_sync_jobs 宿主 app/cloud_integration/ :136-163
webhook_channel_renewer interval 6h Google Drive webhook channel 續期(24h 內到期者) 宿主 cloud_integration :168-197
framework_parse_job_cleanup cron 01:00 UTC 軟刪 7 天前的 framework_parse_jobs(oscal_container.framework_parse_job_domain_service()) 宿主 oscal :202-231
license_expiry_state_machine cron 02:00 UTC app.extensions[jedi_license_runtime.HANDLE_KEY].license_expiry_notification_service.run_daily_tick() — valid→notice→grace→readonly→locked + 寄信 jedi-license-runtime(不經 DI) :240-286
tamper_fs_db_sync date(啟動後一次) TamperSyncService(TamperEventRepo(), config).sync_fs_marker_to_db() + sync_unlock_receipt_to_db() jedi-integrity(不經 DI) :294-328
detection_execution_timeout interval 15min detection_orchestration_container.detection_orchestration_service().converge_timed_out_executions() jedi-detection(經宿主 DI) :341-372
job_binding_orphan_cleanup cron 03:10 UTC flow_control_container.job_binding_orphan_cleanup_service().run_once() — 四張綁定表孤兒清理(CM-1490 軟參照後的第三道防線) 宿主 flow_control :386-417
integrity_spot_check date 自排,4h + 每輪重抽 jitter 0-60min checker.run_spot_check();偵測 tamper 直接終止 process jedi-integrity :39-63, 420-422

socketio 模式的獨立 scheduler init_integrity_scheduler(:85-113):ThreadPoolExecutor(max_workers=1),只掛 integrity_spot_check 一支。

非 scheduler 的長駐/臨時 thread

位置 用途
jedi-log-forwarding/jedi_log_forwarding/common/forwarder.py:143,408 長駐 watcher thread(監看設定變更,重建 forwarding chain);gunicorn fork 後由 _post_fork 重建
common/iam_ports.py:59 OTP 信 fire-and-forget Thread
app/flow_control/service/project_service.py:789,795,801 通知三管道(mail/telegram/discord)各起一條 Thread
app/flow_control/service/job_batch_complete_service.py:178,184,190 同上形狀
app/flow_engine/service/workflow_execution_service.py:1285,1296,1302,1351,1362,1368,1441,1452,1458 同上形狀(9 處)
app/cloud_integration/service/handlers/init_project_folders_handler.py:238 ThreadPoolExecutor 平行建 Drive 資料夾
套件側:jedi-detection/.../detection_orchestration_service.py:2215-2234、detection_profile_extraction_service.py:173、jedi-survey/.../task_survey_service.py:526-543、jedi-evidence-classification/.../evidence_classification_service.py:244 同「通知/解析 fire-and-forget」形狀

這批 threading.Thread(...).start() 是 fire-and-forget 通知,沒有佇列、沒有重試、沒有死信。拆服務時這是「非同步邊界已存在但沒有基礎設施」的訊號。

SocketIO namespace(僅 socketio 模式)

config/socketio_namespaces.py:41-52,共 2 個:

  • /socket/notification → app.notification.handler.notification_socketio_handler.NotificationSocketioHandler(宿主)
  • /socket/fill-survey → jedi_survey.app.handler.fill_survey_socketio_handler.FillSurveySocketioHandler(套件)

B5. 對外 I/O 全清單

# I/O 誰 檔:行 做什麼
1 SMTP jedi-notification jedi-notification/jedi_notification/infra/smtp_mail/smtp_mail_adapter.py(全 repo 唯一 smtplib;宿主 app/ infra/ common/ core/ api/ domain/ 零命中) 寄信;設定從宿主 system_configs 讀(TestMailService 留宿主)
2 HTTP → Google OAuth/Drive 宿主 infra/cloud_integration/google_drive/google_oauth_client.py:13-16,52,70,97,109(requests) accounts.google.com / oauth2.googleapis.com / googleapis.com/oauth2/v2/userinfo / revoke
3 HTTP → 客戶端檔案 agent 宿主 infra/upload_file/remote_agent_adapter.py:17,176-204(httpx) remote_agent 儲存後端:save/get/delete/convert_to_pdf;mode=full 走 mTLS + 短效 JWT
4 HTTP → 檢測 agent jedi-remote-agent jedi_remote_agent/app/service/remote_agent_service.py:59-62,229-245(httpx + build_cloud_mtls_context) health probe、/blob/* 資料面(mTLS client cert + Bearer <RS256 短效 JWT>)
5 HTTP → 檢測工具 connector jedi-detection jedi_detection/infra/detection_tools/connector/agent_probe_client.py、agent_cancel_client.py、app/service/detection_result_handler.py、common/safe_http_fetch.py 探測/取消/取結果
6 HTTP → License Center jedi-license-runtime license_verification_service.py:355(POST {LC}/api/activation/activate)、tenant_license_admin_service.py:244,283(POST/GET /api/internal/*,帶 X-API-Token) 線上開通、請照、方案清單
7 HTTP → Cloudflare Turnstile jedi-iam jedi-iam/jedi_iam/turnstile/verifier.py 人機驗證(登入)
8 HTTP → Discord / Telegram jedi-notification infra/discord/discord_adapter.py、infra/telegram/telegram_adapter.py 通知管道
9 HTTP → OpenAI 宿主 api/translate/routes/translate_route.py:17,26-34,47(from openai import OpenAI,chat.completions.create) 翻譯(唯一直接在 route 層打 LLM 的地方)
10 HTTP(build 期,非 runtime) 宿主腳本 scripts/build/sign_manifest.sh:108(POST {LC}/api/internal/sign-manifest)、build_release.sh:943-955 產物 manifest 送 LC 簽章
11 Redis — SocketIO message queue 宿主 core/app_factory.py:172-186 socketio.init_app(message_queue=redis://...);僅 socketio 模式
12 Redis — flask-redis extension 宿主 core/extensions.py、app_factory.py:168 redis_client.init_app(app, decode_responses=True)
13 Redis — 問卷共編線上名單 宿主 adapter(供 jedi-survey) infra/survey/adapters.py:34-56(RedisPresenceStoreAdapter:lpush/lrem/get_all_from_list) list 結構,presence
14 Redis — AI 對話歷史 宿主 infra/ai/redis_chat_history_store.py(8 處 redis 呼叫) jedi-ai-bot 的 history_store port
15 Redis — Drive 整合 宿主 app/cloud_integration/service/google_drive_integration_service.py(4 處) OAuth state / 暫存
16 Redis — MFA / OTP 碼 jedi-iam jedi-iam/jedi_iam/common/utils/redis_client_util.py;用它的有 mfa/infra/email/adapter/email_adapter.py、mfa/app/service/totp_service.py、email_service.py OTP 碼存 redis(圖形驗證碼已退役,app_modules.py:11-13)
17 Redis — jedi-common 基礎連線 jedi-common jedi_common/session/redis/redis.py 連線工具
— JWT blocklist:不存在。common/middleware/jwt_mw.py(46 行)只註冊兩個 provider 給 jedi_iam.middleware;token 撤銷靠 DB 的 login_tokens 表(jedi_iam/infra/repository/login_token_repo_impl.py + infra/models/login_token.py),不是 Redis
18 檔案系統 宿主 compose 掛四個 bind:/app/static(上傳落地)、/app/log、/home/guidant(.cm-jobs 背景 job 狀態 + LibreOffice profile)、/opt/guidant/pki(tamper 標記);rootfs read_only: true,/tmp 與 /run 是 tmpfs docker/production/docker-compose.yml:136-155
19 物件儲存 S3(SeaweedFS / MinIO) jedi-file-upload jedi-file-upload/jedi_file_upload/infra/adapter/minio/minio_adapter.py(全 monorepo 唯一 minio/boto3 命中);宿主只做選型與設定(common/code/file_storage_type.py、app/upload_file/service/managed_file_upload_service.py、infra/upload_file/system_storage_config_reader.py、app/system_config/service/tenant_storage_config_seeder.py) 出貨走 SeaweedFS S3 gateway(guidant-seaweedfs:8333),憑證與 endpoint 存 DB system_configs group=STORAGE_CONFIG(per-tenant)
20 subprocess 宿主 app/oscal/service/export/ssp_libreoffice_converter.py:92(LibreOffice docx→pdf/odt);common/util/app_version.py:61(取版號) 全 repo 只有這 2 處
21 socket.io 宿主 + jedi-survey 2 namespace(見 B4) 通知推播、問卷共編

B6. DB:RLS 注入機制、schema、policy、view

RLS session 變數注入點:jedi-common/jedi_common/session/database/db.py::session_scope()(:85-127)。 逐條 SET LOCAL:

SET LOCAL row_security = on                       (:93)
SET LOCAL app.user_id = '<user_ctx.id>'           (:94-96)
SET LOCAL app.can_manage_orgs = 't'               (:97-99)
SET LOCAL app.is_super_admin = 't'/'f'            (:103-105)
SET LOCAL app.allowed_tenant_paths = '...'        (:107-109)  # 有值時
SET LOCAL app.allowed_org_paths = '...'           (:112-114)  # 有值時
--- 無 user context(背景 job / worker)分支 ---
SET LOCAL app.is_super_admin = 't'                (:116-118)  # RLS bypass

另有 :58-66 的 app.is_super_admin / app.can_read_all_orgs 設定路徑。 :88 註明「非 PostgreSQL(如 evidence-agent 的 SQLite)沒有 RLS、也不吃 SET LOCAL」——即這條鏈已經有一個非 PG 消費者。

DEV 實測(guidant_ai_dev):

schema BASE TABLE 數 RLS 啟用表數 policy 數
oscal 58 4 10
public 58 10 44
compliance 44 7 30
survey 15 2 8
config 14 9 18
合計 189 32 110

RLS 啟用的 32 張表逐張: compliance.{agent_tasks, detection_execution_groups, detection_executions, flow_templates, module_frames, remote_agent_enroll_tokens, remote_agents}; config.{detection_profile_versions, detection_profiles, detection_tool_profiles_deprecated_20260803, job_execution_detection_tool_agents, job_execution_detection_tools, tenant_detection_tool_configs, tenant_license_events, tenant_license_suspensions, tenant_licenses}; oscal.{ap_docx_parse_jobs, ar_xlsx_parse_jobs, ssp_docx_parse_jobs, ssp_excel_parse_jobs}; public.{bulletins, devices, drive_folder_mappings, drive_sync_jobs, feedback_issues, org_units, roles, system_configs, tenants, users}; survey.{survey_folders, surveys}。

觀察:compliance.projects、compliance.job_executions、oscal.system_security_plans 等核心業務表沒有 RLS——租戶隔離在那些表上靠應用層。

DB view 共 4 張(information_schema.views,實測依賴以 pg_depend/pg_rewrite 反查):

view 參照的表 跨疆界?
public.v_role_routes public.role_capabilities, public.route_capabilities 單疆界(IAM)
public.v_user_capabilities public.capabilities, public.role_capabilities, public.user_roles 單疆界(IAM)
public.v_user_routes public.user_roles, public.v_role_routes(view of view) 單疆界(IAM)
public.vw_user_job_queue 12 個關聯、跨 3 schema/6 疆界:compliance.{job_executions, project_audit_rounds, projects, task_assignees, workflow_executions}、oscal.{catalog_control_parts, catalog_controls, catalog_groups, profile_imports, system_security_plans}、public.{users, workflow_execution_control_mapping} 是——跨疆界的最硬一塊

vw_user_job_queue 的 Python 側 view model 在 infra/readmodel/tasks/vw_user_job_queue.py(94 行);消費者是 my_grc_jobs_query.py 與 flow_control_dashboard_repo_impl.py。

B7. 宿主 readmodel/跨疆界聚合查詢

infra/readmodel/(1753 行,9 支查詢 + 5 支 __init__)是專為此設立的「不隨套件走」目錄,檔頭(infra/readmodel/__init__.py:1-67)明寫動機:這些 SQL 字串裡藏著對其他模組資料表的依賴,grep 守衛掃不到(表名在字串裡不是 import)、harness 也測不出。

檔 行 JOIN 數 跨到哪些疆界的表(實測 SQL 抽取)
oscal/ssp_control_implementation_query.py 232 22 oscal.{assessment_findings, assessment_finding_risks, assessment_risks}/compliance.{workflow_templates(+_trans), workflow_executions, job_executions, job_evidences, job_execution_devices, job_execution_org_units, task_assignees, review_marks}/config.{detection_tools, job_execution_detection_tools, job_execution_detection_tool_agents}/survey.{surveys(+_trans), task_surveys}/public.{users, devices, org_units, workflow_execution_control_mapping} — 7 疆界(檔頭自述),13 支方法皆 .mappings().all() 直出 RowMapping
detection/detection_profile_usage_query.py 150 13 compliance.{agent_tasks, detection_executions, job_executions, projects, task_assignees}/config.job_execution_detection_tools — 檢測/派工/任務平台/participant/project
audit/flow_control_dashboard_repo_impl.py 254 8 compliance.{job_executions, projects, workflow_executions, workflow_templates}/oscal.{catalog_control_parts, catalog_controls, profile_imports, system_security_plans}/public.vw_user_job_queue;另 import jedi_compliance_audit.infra、jedi_participant.infra、jedi_task_platform.infra(7 次 model import)
oscal/resource_library_query.py 146 8 oscal.{catalog_control_parts, catalog_controls, catalog_groups, framework_versions, frameworks, profile_imports, profiles, system_security_plans}/compliance.module_frames(+_trans)/public.users — oscal/module_frame/upload_file/iam
tasks/job_batch_complete_query.py 284 2(ORM,import 6 個套件 model) jedi_iam...User、jedi_flow_engine...JobExecution、jedi_task_platform...Project、jedi_participant...{ProjectParticipant, TaskAssignee}、jedi_survey...TaskSurvey — 5 支套件
detection/detection_job_notify_query.py 161 2(ORM,import 4 個套件 model) jedi_iam...User、jedi_task_platform...Project、jedi_participant...{ProjectParticipant, TaskAssignee}
audit/auditor_dashboard_query.py 118 2 compliance.{project_audit_rounds, project_participants, projects}/oscal.assessment_findings
tasks/my_grc_jobs_query.py 198 2 走 vw_user_job_queue(六疆界濃縮成一張 view)
tasks/vw_user_job_queue.py 94 — view model(定義本身橫跨六疆界,見 B6)

readmodel 之外,仍有跨疆界 JOIN 留在各模組(grep -c '\.join(' 全 infra 排名): infra/flow_control/repository/job_export_query.py(7,含 7 個套件 infra import)、 flow_control_job_repo_impl.py(6 join/16 套件 import)、 infra/associations/repository/job_execution_device_mapping_repo_impl.py(4)、 infra/flow_control/repository/job_import_lookup_query.py(3/5 import)、 infra/project_summary_report/repository/project_summary_report_repo_impl.py(1)。

「拆成服務後會斷掉的查詢」清單=上表 9 支 + vw_user_job_queue view + flow_control 那 4 支寫讀混合的 repo_impl。 readmodel 已把純讀的部分正名收攏,但 infra/flow_control/repository/ 底下的寫讀混合 repo(16 次套件 infra import)尚未拆——那批既 JOIN 跨疆界又寫入,是最難拆的。


§3

C. 部署形狀

C8. 出貨的容器組成

canonical 是 docker/production/docker-compose.yml(478 行,含大量踩坑註解),scripts/build/build_bundle.sh 把它連同 image tar 封成 guidant-ai-<版號>.tar.gz。

六個服務 + 一個 one-shot:

容器 image 對外 內網 port 跑什麼
guidant-db postgres:16 不開 5432 PostgreSQL;POSTGRES_INITDB_ARGS="--encoding=UTF8 --locale=en_US.utf8";named volume guidant-pgdata;healthcheck pg_isready
guidant-redis redis:7-alpine 不開 6379 ACL user 模式(不是只有 requirepass——BE 連線串是 redis://<user>:<pwd>@host);--appendonly no --save ""(純快取,不是真相來源)
guidant-api guidant-ai-be:<ver> expose only 8000 BE REST(RUN_MODE 預設 api);read_only: true rootfs;tmpfs /tmp:2g /run
guidant-socketio 同一顆 image expose only 8002 RUN_MODE: socketio;掛載與 api 完全同組(integrity 抽查兩模式都跑)
guidant-fe guidant-ai-fe:<ver> 80:80 / 443:443(整組唯一對外映射) — nginx:SPA 靜態檔 + 同源反向代理(nginx.onprem.conf build 進 image):/api/1.0→guidant-api:8000、/socket.io→guidant-socketio:8002;HTTPS 憑證唯讀 bind(certs/server.crt/.key),缺檔直接 emerg 不退回 http
guidant-seaweedfs chrislusf/seaweedfs:3.99 不開 8333(S3 gateway) 單機一體模式(master+volume+filer+S3 同進程);-s3.config=/etc/seaweedfs/s3.json(不帶=匿名全開);named volume guidant-seaweedfs-data
guidant-db-init guidant-ai-init:<ver> — — one-shot,profiles: ["init"] 不隨 up 啟動;MODE=init(建庫)或 MODE=migrate(升級套 migration);退出碼契約 0/1/2/3/4;三組 DB 密碼只存在此容器,跑完隨 run --rm 消失

有 nginx/gateway 嗎:有,就是 guidant-fe(nginx)。它同時是靜態檔伺服器與 API gateway,是整套系統唯一對外入口。沒有獨立的 API gateway 元件。

容器互連:同一個 compose project(name: guidant),走 compose 預設 bridge network,以 container_name 當 DNS(guidant-db、guidant-redis、guidant-seaweedfs:8333、guidant-api:8000、guidant-socketio:8002)。啟動順序靠 depends_on: condition: service_healthy(x-guidant-common:126-133:BE 等 db+redis+seaweedfs 三者 healthy)——註解明寫「這條鏈只在同 project 內成立,那正是 DB/Redis 與 BE 刻意同 project 的原因」。

bundle 收哪些 image(build_bundle.sh:84-94):guidant-ai-be、guidant-ai-fe、guidant-ai-init、postgres:16、redis:7-alpine、chrislusf/seaweedfs:3.99 — 6 顆,air-gap 可裝。

build 管線(scripts/build/README.md):build_all.sh --all(⓪ assert_db_current.sh DB 前置斷言 → Nuitka 編譯 15-20 分 → smoke → 三顆 image)→ build_bundle.sh 封包。build 機是 188 的 /opt/guidant-ai-be(git clone,更新走 git pull)。smoke 連的是安裝版 stack 的 guidant-db 容器、用 cm_app(受 RLS 的帳號,=出貨實況)。

建庫 vs 升級(scripts/init/README.md):scripts/init/ 管「從無到有」(02-schema.sql 產生檔:5 schema/176 表/103 policy/82 function/18 trigger),scripts/sql/ 管增量 migration;同一顆 init image 換 MODE=migrate 即為 migration 容器(刻意不分兩個 service,避免兩份連線設定各自演化)。

C9. remote-agent 是不是獨立部署

是——而且是兩種不同的 agent,都是獨立 process、獨立 repo:

(a) evidence-agent(檔案 agent) — 獨立 repo ~/Projects/Billows/Audit-Manager/evidence-agent/:

  • 定位(該 repo CLAUDE.md):「把 jedi-file-upload 套件包成一個獨立可跑的極小 REST 服務 + Docker」,裝在**客戶自有設備(Linux)**上,binary 不存雲端磁碟。
  • 不連雲端 DB:自帶本地單檔 SQLite,只有 upload_files 一張表。「雲端 ↔︎ agent 只透過 REST 講話」。
  • 單租戶、無 RLS。出貨走 deploy/Dockerfile.runtime + build_compose_bundle.sh(compose 形態,自己的交付包)。
  • 協定定義處:宿主側 infra/upload_file/remote_agent_adapter.py(273 行)——save_file/get_file/delete_file(s)/convert_to_pdf;_request()(:176-204)在 mode=full 下 httpx.Client(verify=build_cloud_mtls_context(...)) + Authorization: Bearer <JWT>,走 https;_verify_integrity(:113)用 SHA-256 對實際 bytes 重算,信任根是雲端 upload_files.sha256。
  • 它是「宿主的 storage backend」:走 jedi-file-upload 的 storage type remote_agent(common/code/file_storage_type.py,宿主定義因為「客戶端檔案 agent 是雲端部署概念,屬 caller 業務知識」)。

(b) 檢測 agent(detection scan agent) — 協定在 jedi-remote-agent 套件:

  • 套件職責(jedi_remote_agent/__init__.py:1-28):enroll(一次性 token 自我註冊 → 內部 CA 簽 server 憑證回給它)、heartbeat(更新 last_seen_at,順手把待辦派工夾帶回去=polling 派工)、agent task 狀態機(pending→dispatched→running→succeeded/failed/cancelled)、雙向認證原語。
  • 協定:REST(blueprint 在套件內,plugin.py:300-303 依 ROUTE_TABLE 掛,宿主提供 admin_required 守門)+ mTLS + RS256 短效 JWT(common/agent_auth/tls.py:14 build_cloud_mtls_context、jwt_util.mint;AGENT_JWT_TTL_SEC 預設 60s)。
  • agent 主動撥出(agent_enrollment_service.py:3):「bootstrap 用 token + TLS;穩態 heartbeat 走 mTLS,不帶 user JWT」;指紋變動自動更新 + 留痕。
  • 宿主設定:config/config.py:261-271 的 11 個 AGENT_*(AGENT_AUTH_MODE 預設 none=demo 形態、CA cert/key、JWT key pair、cloud client cert/key、AGENT_HEARTBEAT_INTERVAL_SEC=300、AGENT_CERT_VALID_DAYS=3650、AGENT_JWT_TTL_SEC=60)。憑證以唯讀 bind mount 內外同路徑掛進容器(compose :177,註解明寫「掛在別處等於沒掛,症狀是服務全綠、只有 agent 連不上」)。
  • DB 側(DEV 實測 \d compliance.remote_agents):agent_type 預設 'file_storage'、capabilities jsonb 預設 ["file_storage"]、base_url、device_fingerprint、status、last_seen_at、agent_version、hardware_info——一張 registry 表同時承載兩種 agent。RLS 啟用。

C10. License Center 的交互

不只是「簽好的 License 檔」——有三條 runtime HTTP 通道,全在 jedi-license-runtime 套件內:

通道 方向 端點 認證 何時
① 線上開通 BE → LC POST {LICENSE_ACTIVATION_SERVER_URL}/api/activation/activate,body {activation_code, machine_fingerprint} 無 token(公開 API,LC 端自己有 15 分鐘 5 次失敗鎖定) 使用者在開通頁輸入序號時(license_verification_service.py:337-375)
② 請照/延期(root 後台) BE → LC POST {LC}/api/internal/* X-API-Token: {LICENSE_CENTER_API_TOKEN} root 後台請照(tenant_license_admin_service.py:239-277)
③ 方案清單透傳 BE → LC GET {LC}/api/internal/plans 同上 請照表單下拉(:281-300,無落地)
④ 產物簽章(build 期,非 runtime) build 機 → LC POST {LC}/api/internal/sign-manifest X-API-Token scripts/build/sign_manifest.sh:108

設定來源:config/config.py:303-313(LICENSE_ACTIVATION_SERVER_URL 預設 http://127.0.0.1:5062、LICENSE_CENTER_API_TOKEN 預設空);經 api/license/__init__.py:82-87 進 LicenseConfig,並經 common/integrity/adapters.py:49,191-192 進 IntegrityConfig(integrity 也認得 LC)。

信任邊界仍然清楚(license_verification_service.py:344-347 明寫):「兩 repo 零依賴——這裡只打 HTTP,不 import license_center 源碼。開通伺服器已核對序號合法性,但收到照後仍走 activate_license 完整驗章+(host 版)指紋核對,不因來源是開通伺服器就信任(設計裁示:BE 是唯一信任邊界)」。無 DB 層共用(LC 獨立 DB license_center_{dev,stg},migration 走 LC 自己的 scripts/sql/,不進主專案 schema_migrations)。 離線路徑仍在:LC 主機上跑 license-center sign-manifest(build_release.sh:955 的 air-gapped 保底)+ 匯入授權檔。 公鑰表在 jedi_license_runtime.common.public_keys(build_bundle.sh 出貨前檢查有無 PROD 鑰,:940-960 附近)。


§4

D. 已有的服務化樣板

這個 codebase 已經有 5 個「已經是網路 API 邊界」的內部元件:

# 元件 協定 誰認證/怎麼認證 資料怎麼同步 可照抄什麼
1 License Center(獨立 repo+獨立 DB+獨立服務) HTTPS/HTTP REST,JSON ① 開通 API 公開無 token(LC 側限流);②③ X-API-Token header(LC api_tokens 表白名單) 無資料同步——只傳「簽好的照」(含簽章 + kid + 機器指紋),BE 收到後完整驗章不信任來源;LC 的客戶名單/訂單/私鑰不進主產品 最乾淨的樣板:獨立 repo/DB/部署,零 DB 共用,非對稱簽章解決信任,離線路徑(人工搬檔)與線上路徑並存
2 **evidence-agent(檔案 agent) REST over HTTPS mode=full:mTLS + RS256 短效 JWT**(60s);mode=none:demo 無認證 agent 自帶 SQLite(upload_files 一張表);信任根是雲端的 upload_files.sha256,agent 那份只供比對;save_file_name 內嵌 uid 作災難復原安全網 「把一個 jedi 套件包成獨立 REST 服務」的已完成先例——套件邏輯完全重用不 fork,service 只剩 route+DI+config(該 repo CLAUDE.md 明寫「不建會變空殼的 domain/infra/app 三層」)
3 **檢測 agent(jedi-remote-agent 協定) REST,agent 主動撥出+polling** enroll:一次性 token+TLS;穩態:mTLS(build_cloud_mtls_context)+ RS256 短效 JWT;指紋比對+自動更新留痕 heartbeat 更新 last_seen_at(在線判定唯一依據)並夾帶待辦派工回去;agent task 狀態機(pending→dispatched→running→succeeded/failed/cancelled)記在雲端 compliance.agent_tasks 派工信封 vs 派工內容的分離:套件只管「誰的單、什麼狀態、什麼時候可領」,內容走 IAgentTaskPayloadProvider 由宿主注入、完成後動作走 IAgentTaskLifecycleListener。這是「服務邊界該切在哪」的現成答案
4 **AI 呼叫(OpenAI) HTTPS,OpenAI SDK api_key(env) 無狀態;對話歷史走 RedisChatHistoryStore(宿主實作 jedi-ai-bot 的 history_store port) port 化外部 AI 的形狀;但這支在 route 層直接建 client**(api/translate/routes/translate_route.py:26-34),是反例不是正例
5 **log 轉發(syslog/GELF) UDP/TCP syslog、GELF 無(網路層信任) 單向 fire-and-forget,設定變更由 watcher thread 熱重載;轉發失敗絕不影響服務**(app_factory.py:163-165 明寫 fail-open) 旁路服務的降級紀律:「掛不起來絕不可讓服務起不來」

補充:BE 內部本身也已有一道「準服務邊界」——guidant-api 與 guidant-socketio 是同一顆 image 的兩個 process,靠 Redis message queue 做 socket 的跨進程訊息傳遞(app_factory.py:179-186)。這是唯一一處「同一份程式碼跑成兩個 process 並靠中介軟體通訊」的既有形態。


§5

附:與首腦判斷相關的三個硬數字

  1. 宿主接線碼總量約 7,558 行(15 支 wiring/adapter 檔 2,004 行 + di_containers 5,554 行),服務 18 支已插件化的套件;根 container 有 39 個子 container + 11 處 override_providers 補循環依賴,deepcopy 需 5000 recursion frames。
  2. 跨疆界查詢:infra/readmodel/ 9 支 1,753 行(最大一支 22 個 JOIN、7 疆界)+ public.vw_user_job_queue 一張跨 3 schema/12 關聯的 view + infra/flow_control/repository/ 4 支寫讀混合 repo(單檔 16 次套件 infra import)。前兩者已被正名隔離,第三批沒有。
  3. 執行期只有兩個 process 形態(api / socketio)、零獨立 worker;全部背景工作(8 支 APScheduler job + 約 20 處 threading.Thread fire-and-forget 通知)都在 api 模式的 gunicorn master 內,沒有佇列、沒有重試、沒有死信。
§6

未查證事項

  • 未實測啟動 BE 驗證 blueprint 實際掛出的 route 總數(只數了 72 支 *_route.py 檔)。
  • di_containers/dashboard_apis/ 12 支檔的角色未逐支讀(從命名推測是 AI Dashboard 的資料源註冊,非 port adapter)。
  • STG/POC 的 live compose 實況未 ssh 查證(本報告的容器組成以 repo 內 docker/production/docker-compose.yml 與 build_bundle.sh 為據)。
  • 各套件內部有多少自己的跨疆界 SQL 未盤(本次只盤宿主側)。
  • .build/bundle/guidant-ai-1.14.0/docker-compose.yml 是舊版產物快照,未與 canonical 逐行 diff。