# 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`）。

---

## 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。

---

## 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 跨疆界又寫入，是最難拆的。

---

## 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` 附近）。

---

## 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 並靠中介軟體通訊」的既有形態。

---

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

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 內，**沒有佇列、沒有重試、沒有死信**。

## 未查證事項

- 未實測啟動 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。
