盤點日期: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)。
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 factoryapp.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 註冊進 registryapp.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)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/*)。
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 無聲消失」。
di_containers/ 的規模與 port adapter 盤di_containers/ 下 40 支非 __init__ 的 .py(含 dashboard_apis/ 12 支);總 5554 行。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)。config/di_modules.py 動態 rglob 掃描(release 模式改讀 build 期凍結的 config/di_modules_static.py)。實測全 repo 含 @inject 的模組 73 支。宿主接線盤的實際長度: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 支套件。
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。
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 一支。
| 位置 | 用途 |
|---|---|
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 通知,沒有佇列、沒有重試、沒有死信。拆服務時這是「非同步邊界已存在但沒有基礎設施」的訊號。
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(套件)| # | 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) | 通知推播、問卷共編 |
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。
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 跨疆界又寫入,是最難拆的。
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,避免兩份連線設定各自演化)。
是——而且是兩種不同的 agent,都是獨立 process、獨立 repo:
(a) evidence-agent(檔案 agent) — 獨立 repo ~/Projects/Billows/Audit-Manager/evidence-agent/:
CLAUDE.md):「把 jedi-file-upload 套件包成一個獨立可跑的極小 REST 服務 + Docker」,裝在**客戶自有設備(Linux)**上,binary 不存雲端磁碟。upload_files 一張表。「雲端 ↔︎ agent 只透過 REST 講話」。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。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)、雙向認證原語。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_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 連不上」)。\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 啟用。不只是「簽好的 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 附近)。
這個 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 並靠中介軟體通訊」的既有形態。
override_providers 補循環依賴,deepcopy 需 5000 recursion frames。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)。前兩者已被正名隔離,第三批沒有。threading.Thread fire-and-forget 通知)都在 api 模式的 gunicorn master 內,沒有佇列、沒有重試、沒有死信。*_route.py 檔)。di_containers/dashboard_apis/ 12 支檔的角色未逐支讀(從命名推測是 AI Dashboard 的資料源註冊,非 port adapter)。docker/production/docker-compose.yml 與 build_bundle.sh 為據)。.build/bundle/guidant-ai-1.14.0/docker-compose.yml 是舊版產物快照,未與 canonical 逐行 diff。