FR-080 批次 B 套件盤點(8 支)

盤點日期:2026-09-09。證據路徑相對根:

  • 套件 monorepo ~/Projects/Jedicogy/module/jedi-python-package/(以下簡稱 M)
  • 主專案 ~/Projects/Billows/Audit-Manager/compliance-manager-be/(以下簡稱 H)
  • DB 查證:DEV 192.168.50.188:25432 / guidant_ai_dev(唯讀 SELECT)

§1

jedi-file-upload

Q1 它是什麼

把「一個檔案存到某個後端、記一列 metadata、之後能下載/轉 PDF 預覽/刪除」這件事包起來的插件。裝上 register() 就長出六支 HTTP 檔案端點;mount_api=False 則退化成純 library(jedi-issue 就這樣用)。

README 與程式碼一致,且是我讀過最誠實的一份:

  • README 說「六支端點、URL 凍結」→ M jedi-file-upload/jedi_file_upload/api/__init__.py:104-110 的 mount_routes() 確實掛六條,與 README 表格逐條吻合。
  • README 說「套件不解析儲存設定,宿主交一個已知道怎麼挑後端的 service」→ plugin.py:97-98 的 FileUploadServices 兩個欄位都是 Optional[Callable[[], Any]] provider,套件端零解析。
  • README 說「守門三件沒給就拒絕掛載」→ plugin.py:282-300 _assert_api_wiring() 實作確認,missing 非空即 RuntimeError。

唯一與「純通用」有落差的地方(README 自己也寫了):common/utils/libreoffice_util.py 把 LibreOffice 轉檔當成套件知識內建,plugin.py:65-67 硬編 DEFAULT_PDF_CONVERTIBLE_EXTS 十種副檔名。這是「辦公文件轉 PDF」耦合進檔案上傳套件,不是 Guidant 產品知識,但確實是「多做了一件事」。

Q2 資料

自己擁有的表:只有一張 | 表 | 存什麼 | 關鍵欄位 | |---|---|---| | public.upload_files(M .../infra/models/upload_file.py:16) | 一列=一個已落地的檔案實體 | uid(對外識別)、storage_type(local/minio/seaweedfs/宿主自有)、path(目錄或 bucket)、save_file_name、sha256(防竄改錨點)、storage_scope(system/customer) |

本表無 tenant_id(M .../migrations/001-upload-file-tables.sql:24-27 明文說明,ORM 繼承 BaseModel 而非 TenantScopedMixinModel),故刻意不掛 RLS。

對別人的表的參照:零。套件端無任何 FK、無 relationship、無 raw SQL 打別人的表。

反向:別人對它建了三條真 FK(DEV DB 查證):

  • compliance.job_evidences.file_id → upload_files(id)(H infra/flow_engine/models/job_evidence.py:41)
  • oscal.ssp_reference_documents.file_id → upload_files(id)(H scripts/sql/2026-06-15-fr038-b3-ssp-reference-documents.sql:15)
  • issue_upload_file_mapping.file_uid → upload_files(uid) ON DELETE CASCADE(jedi-issue 的表,DB pg_constraint 查證)

產品詞彙洩漏:無。表名 / 欄位名全是通用檔案語彙。StorageTypeCode(M .../common/code/storage_type_code.py)只有 local/minio/seaweedfs/s3/azure/gcs,remote_agent 刻意留在宿主(H common/code/file_storage_type.py:9)。

Q3 Port

需要宿主提供(M .../plugin.py:124-154 FileUploadAdapters): | Port | 簽名 | 必填 | 用在哪 | |---|---|---|---| | services.file_upload_service | () -> FileUploadService 或子類 | mount_api 時是 | route 的上傳/刪除/轉檔 | | services.upload_file_service | () -> UploadFileService | 同上 | metadata 查詢 | | auth_required | (fn) -> fn | 是 | 全部端點 | | file_access_guard | (scope, bind_uid_arg) -> decorator | 是 | download / pdf-preview | | signed_token_issuer | (scope, resource_uid) -> str | 是 | /file/access-token/<uid> | | identity | IdentityContext(jedi-common D8 標準件) | 否 | 審計欄位補 nickname(app/service/identity_enricher.py:28) | | settings | ISettingsReader.get(key, default) | 否 | 只問一個 key:LIBREOFFICE_CMD | | response_builder | (ok, data) -> dict | 否 | 回應信封 |

提供給別人的入口:FileUploadService / UploadFileService / UploadFileDomainService / UploadFileRepoImpl / UploadFileEntity / UploadFileMapper / UploadProviderFactory / IUploadFileProvider(ports/upload_file_provider/upload_file_provider.py:13)/ 四支 config DTO。README:180-188 明文說 service 那一面是「兩個 consumer 共用的契約」。

樣板殘留 port:無。八張 port 全部有實際使用點(settings 只服務一個 key,但確實在用)。

Q4 消費者

(a) 主專案:22 個檔案 import(grep -rln jedi_file_upload --include=*.py),分佈:

  • H core/upload_file_wiring.py + core/app_factory.py:326-333 — 插件註冊(唯一組裝點)
  • H app/upload_file/service/managed_file_upload_service.py — 繼承套件的 FileUploadService,加上讀 system_config STORAGE_CONFIG 的租戶維度(8 處 import)
  • H infra/upload_file/remote_agent_adapter.py — 實作套件的 IUploadFileProvider,第四種後端
  • H app/flow_engine/service/job_evidence_service.py、app/oscal/service/framework_parse_job_service.py、app/oscal/service/ssp_document_pool_service.py、app/cloud_integration/.../import_drive_file_handler.py — 業務端用 service
  • H infra/remote_agent/recon_query_adapter.py:12、H infra/flow_engine/mapper/job_evidence_mapper.py — 直接查 UploadFile ORM model
  • H di_containers/upload_file/upload_file_containers.py — DI wiring

(b) 其他 jedi 套件(依賴方向:這些套件 → file-upload):

  • jedi-issue:9 處 import(M jedi-issue/.../local_issue_attachment.py:4-8、local_issue_adapter.py:3-5、ports/dto/upload_file_dto.py:3),且 M jedi-issue/pyproject.toml:45 顯式宣告 jedi-file-upload>=0.0.12
  • jedi-detection:1 處(M jedi-detection/.../detection_report_file_query.py:14 直接 import UploadFile ORM model 做 id→uid 批次解析)

它對 jedi-iam 的依賴:import 零處、pyproject 零宣告。 全包 grep jedi_iam|jedi_auth 只命中 pyproject 註解與 docstring 文字(M jedi-file-upload/pyproject.toml:30,37,42、plugin.py:1、common/settings.py:15)。身分名冊走 jedi_common.identity.IdentityContext 這條標準 port,實作在宿主 H core/upload_file_wiring.py:53-106(那支才 import jedi_iam)。

Q5 執行期形狀

  • Route 6 條,blueprint 名 upload_file,prefix /api/1.0(plugin.py:55-58):POST /file/upload、DELETE /file/upload/<uid>、POST /file/uploads、GET /file/access-token/<uid>、GET /file/download/<uid>、GET /file/pdf-preview/<uid>
  • 背景排程 / worker / 長駐 thread:無
  • 對外 I/O:檔案系統(local adapter)、S3 協議 HTTP(minio SDK,infra/adapter/minio/minio_adapter.py)、subprocess(LibreOffice 轉 PDF,local_file_adapter.py:135、minio_adapter.py:263、app/service/upload_file_service.py:2)
  • 快取 / Redis:無(pyproject 已在 P3.2 清掉虛掛的 redis)
  • 單獨起 process:可以——自帶 api 層、自帶 harness(M jedi-file-upload/harness/dev_app.py 244 行 + docker-compose)

Q6 通用性

(a) 能直接用。 一個工單系統裝它,只要提供守門三件 + 兩支 service provider,六支端點就能用。

(b) 寫死的產品知識:極少,且都不是 Guidant 專屬

  • plugin.py:61 DEFAULT_FILE_ACCESS_SCOPE = "file_access" — 註解明說「Guidant AI 抽出前用的就是這個字串」,但已 config 化可覆寫
  • plugin.py:58 DEFAULT_BLUEPRINT_NAME = "upload_file" — 同上,可覆寫
  • migrations/002-upload-file-grants.sql GRANT 對象假設 cm_app(README:151 已標「不吃這套生態的 consumer 可只套 001」)
  • plugin.py:65-67 LibreOffice 十種副檔名硬編 — 這是「Office 轉檔」領域知識塞進檔案套件,不是 Guidant 知識,但屬多餘職責

Q7 插件完整度

項目 有/無
① api 層隨包 ✅ jedi_file_upload/api/(__init__.py + routes/upload_file_route.py 262 行 + serializers)
② 註冊入口 ✅ register(app, adapters=None, config=None, schema_extensions=None, mount_api=True) -> PluginHandle(plugin.py:303)+ create_blueprint(adapters, config, schema_extensions)(:253)
③ migrations 隨包 ✅ 2 支(001 建表、002 GRANT),iter_migrations() (plugin.py:241),pyproject:58-60 有 include
④ DI container 預設 ❌ 無(刻意:provider 名冊交宿主,plugin.py:31-36)
⑤ 獨立 harness ✅ harness/dev_app.py 244 行 + harness/docker-compose.yml(port 5498)
⑥ 接入 README ✅ 188 行,含 quickstart / port 表 / 宿主前提表 / 常見坑

Q8 糾纏對象

最糾纏:jedi-issue(單向,issue → file-upload)

  • issue_upload_file_mapping.file_uid → upload_files(uid) ON DELETE CASCADE — DB 層真 FK(DEV pg_constraint 查證)。這是本批唯一「兩支套件的表之間有真 FK」的案例。
  • M jedi-issue/.../local_issue_attachment.py:60-65:同一交易內先 file_upload_service.upload_file() 再 add_issue_file_mapping(),寫兩邊。
  • 但方向是明確的上下游(issue 沒有 file-upload 就沒有附件;file-upload 沒有 issue 完全活得下去)。file-upload 對 issue 零認知。

次糾纏:jedi-remote-agent(雙向繞了一圈,但都經 port / 宿主)

  • remote-agent 的 IRemoteAgentReconQuery 對帳需要「這台 agent 上有哪些檔」,實作在宿主 H infra/remote_agent/recon_query_adapter.py:12 直接 query UploadFile 篩 storage_type='remote_agent'。
  • 反向:upload_files.storage_type 的第四種值 remote_agent 由宿主 H core/upload_file_wiring.py:50 經 extra_object_storage_types 注入。
  • 兩邊都不互相 import,耦合點全在宿主的 adapter。

看起來像但不是一件事:

  • jedi-detection:只 import UploadFile model 做一支唯讀 id→uid 批次查(M jedi-detection/.../detection_report_file_query.py:14),是 consumer 不是同一件事。
  • jedi-integrity:兩者都算 sha256、都講「防竄改」,但 integrity 算的是程式碼產物的 hash(integrity_tamper_events 表與 upload_files 零關係),file-upload 的 sha256 欄是使用者上傳檔的錨點。同一個演算法、完全不同的資料。

§2

jedi-issue

Q1 它是什麼

「一張工單 + 誰負責 + 貼哪些標籤 + 掛哪些附件」的儲存與查詢,並帶一套「issue 來源可以是本地 DB / GitLab / GitHub」的 adapter 架構。

README 與程式碼一致,且 README 自己就是最誠實的那份(M jedi-issue/README.md:9-27):README 開頭紅字說「約 58% 是待裁決的死碼」,只有成員管理是活的。程式碼對照確認:

  • M jedi-issue/jedi_issue/api/__init__.py:110 FROZEN_URLS = frozenset({"/api/1.0/issue/get_members"}) — 整支套件只有一條 route。
  • GitLab/GitHub adapter 確實存在且完整(infra/issue/adapter/gitlab/gitlab_issue_adapter.py 197 行、github/github_issue_adapter.py 163 行)。

但「58% 死碼」這個說法對主專案而言不成立(這正是 FR-080 開卡時說的那句「檔頭那句是錯的」):H app/feedback/service/issue_service.py 有 24 處呼叫 JediIssueService,且 :116 用 IssueProviderCode.GITLAB、:141 用 IssueProviderCode.GITHUB——主專案的意見回饋模組是把 issue 同步推到 GitLab/GitHub 的。「產品沒在用」的判斷與主專案程式碼衝突。

Q2 資料

自己擁有的表:六張 | 表 | 存什麼 | 關鍵欄位 | |---|---|---| | issues(infra/issue/models/issue.py:10) | 一張工單 | uid、project(字串非 FK)、title、description、state | | members(infra/member/models/member.py:8) | 可被指派的成員名冊 | uid、username、email、role、type、enabled | | labels(infra/label/models/label.py:9) | 標籤 | uid、name、color、scope、is_deleted | | issue_assignee_mapping(:8) | issue↔︎成員 | issue_uid + member_uid 複合 PK | | issue_label_mapping(:8) | issue↔︎標籤 | issue_uid + label_uid 複合 PK | | issue_upload_file_mapping(infra/issue_upload_files/models/issue_upload_file.py:11) | issue↔︎檔案 | issue_uid + file_uid 複合 PK |

六張表全無 tenant_id(皆繼承裸 Base 而非 TenantScopedMixinModel)。

對別人的表的參照:

  • (a) 真 FK:issue_upload_file_mapping.file_uid → upload_files(uid) ON DELETE CASCADE — 跨套件真 FK。ORM 上 issue_upload_file.py:16 只宣告 String(50) 沒寫 ForeignKey,但 DB 上實際有這條 constraint(DEV pg_constraint 查得 issue_upload_file_mapping_file_uid_fkey)。ORM 與 DB 不一致。
  • (b) ORM relationship 跨包:套件端無。宿主端有:H infra/feedback/model/feedback_issue.py:30-37 的 FeedbackIssue.issue relationship 直接指向套件的 Issues model(viewonly=True,primaryjoin 用 foreign(Issues.uid)),且 :74-77 的 title hybrid_property 對 Issues.title 發 scalar_subquery。
  • (c) 軟參照:issues.project 只存字串(issue.py:18),宿主傳 self.project_name。
  • (d) raw SQL 打別人的表:無。

產品詞彙洩漏:無(issue/label/member 都是通用軟體工程語彙)。

Q3 Port

需要宿主提供(M jedi-issue/jedi_issue/plugin.py:133-148 IssueAdapters): | Port | 簽名 | 必填 | 用在哪 | |---|---|---|---| | services.member_service | () -> MemberService | mount_api 時是 | 唯一那條 route | | auth_required | (fn) -> fn | 是(_assert_api_wiring :216-234) | 同上 | | response_builder | (ok, data) -> dict | 否 | 零使用(plugin.py:40-42 明說 route 直接回 service 結果不包信封) |

套件內部的 adapter 註冊表(不是對宿主的 port,是內部依賴反轉):ports/issue_provider/issue_factory.py:83 TYPE: Dict[str, Type[IIssueProvider]] = {} + register_adapter(),由 plugin.py:83-84 import 觸發填入。這條線很脆:get_issue_adapter() 若註冊表空會 RuntimeError(issue_factory.py:100-107),而註冊的唯一觸發點是 import jedi_issue.plugin。

提供給別人:MemberService、IssueService(app/issue/service/issue_service.py:10)、IssueAttachmentService、LabelDomainService、Issues model、IssueDTO / CreateIssueDTO / UpdateIssueDTO / IssueResponseDTO。

樣板殘留:response_builder 有欄位但零使用(plugin.py 自己註記)。SchemaExtensions(:152-163)插槽開齊零使用。

Q4 消費者

(a) 主專案:

  • H app/feedback/service/issue_service.py — 24 處呼叫,是最大 consumer。用 JediIssueService(LOCAL/GITLAB/GITHUB 三 provider)、IssueAttachmentService、LabelDomainService
  • H infra/feedback/model/feedback_issue.py:7 — import Issues 建 relationship
  • H app/feedback/dto/feedback_issue_dto.py:5 — import IssueDTO
  • H infra/issue/issue_plugin_wiring.py + core/app_factory.py:402-404 — 插件註冊
  • 對外端點是主專案的 H api/feedback/__init__.py:21-24 四條 /feedback*,不是套件那條

(b) 其他 jedi 套件:零。沒有任何 jedi-* 套件 import jedi-issue。

反向依賴(issue → 別人):jedi-file-upload(9 處,pyproject 顯式宣告)、jedi-common(29 處,P3.6 才補上宣告)。守衛測試 M jedi-issue/tests/unittest/test_plugin_contract.py:32 把允許清單焊死成 ("jedi_common", "jedi_file_upload", "jedi_issue")。

Q5 執行期形狀

  • Route 1 條:GET /api/1.0/issue/get_members,blueprint 名 issue(plugin.py:97)
  • 背景排程 / worker / thread:無
  • 對外 I/O:HTTP 到 GitLab / GitHub(infra/gitlab.py、infra/github.py,經 python-gitlab / PyGithub SDK);經 jedi-file-upload 間接碰檔案系統/S3
  • os.getenv 4 處,全在 GitLab/GitHub 檔內(infra/github.py 讀 GITHUB_PRIVATE_TOKEN;infra/gitlab.py 讀 GITLAB_URL / GITLAB_PRIVATE_TOKEN / GITLAB_API_VERSION)——這是全批八支唯一還在直讀 env 的套件
  • 快取 / Redis:無(P3.5 已清掉虛掛的 redis 宣告)
  • 單獨起 process:理論可以但意義不大——只有一條 route,且沒有 harness(README:121 明說「本套件沒有 harness」)

Q6 通用性

(a) 能直接用——工單系統裝它反而是最貼題的 consumer。

(b) 寫死的產品知識:

  • plugin.py:97 DEFAULT_BLUEPRINT_NAME = "issue" — 可覆寫
  • 四個 GitLab/GitHub env 變數名寫死在 infra/gitlab.py / infra/github.py(未 port 化,plugin.py:56-59 明文記錄「動了等於替產品決定要保留第三方整合」)
  • python-gitlab==5.6.0 / PyGithub==2.6.1 在 runtime 相依(pyproject:43-44),任何 consumer 都被迫背這兩個
  • 零 Guidant 產品詞彙(沒有稽核 / CMMC / SSP / round / detection)

Q7 插件完整度

項目 有/無
① api 層隨包 ✅ 但只有一條 route(jedi_issue/api/)
② 註冊入口 ✅ register(app, adapters=None, config=None, schema_extensions=None, mount_api=True)(plugin.py:266)+ create_blueprint(:237)
③ migrations 隨包 ❌ 無 migrations/ 目錄(六張表全靠宿主自己建)
④ DI container 預設 ❌ 無
⑤ 獨立 harness ❌ 無(README:121 明說沒有;驗收改用「在真宿主上拔掉註冊行」)
⑥ 接入 README ✅ 144 行

Q8 糾纏對象

最糾纏:jedi-file-upload(單向下游,證據見上:真 FK + 同交易寫兩邊 + pyproject 顯式相依 + 9 處 import)。issue 沒有 file-upload 就沒有附件功能;反過來完全成立。

次糾纏:主專案的 feedback 模組(不是套件,但耦合度比任何套件對都高):H infra/feedback/model/feedback_issue.py 的 feedback_issues 表以 issue_uid 軟參照 issues.uid,且用 ORM relationship + hybrid_property 跨包 join。等於「issue 的一半用例住在宿主」。

看起來像但不是一件事:

  • jedi-bulletin:兩者都是「一段文字 + 狀態 + 時間」的 CRUD,形狀很像,但 issue 有 assignee/label/attachment 三個關聯維度與外部同步,bulletin 是單表無關聯。兩邊零 import、零 FK。
  • jedi-issue 的 members 表 vs jedi-iam 的 users 表:members(infra/member/models/member.py)是套件自己的名冊,與 public.users 零 FK、零 join,是兩份平行的人員資料。issue_assignee_mapping.member_uid → members(uid)(DB 查證),不是指向 users。

§3

jedi-bulletin

Q1 它是什麼

系統公告的 CRUD 與分頁查詢 library(單表、軟刪除)。

README 與程式碼一致,且 README 的驗收邊界誠實聲明是準的(M jedi-bulletin/README.md:11-24):明說「D6 五件套只做到三項」「沒有 api 層」「拔掉測試做不到」。程式碼確認:M jedi-bulletin/jedi_bulletin/plugin.py:171-193 的 create_blueprint() 建出來的 blueprint 不掛任何 resource,docstring:182-184 明文「這是刻意的,且有測試焊死——不是還沒寫完」。

README:39-45 的「系統裡有兩層公告 service」也屬實:宿主 H app/bulletin/service/bulletin_service.py 是產品用例層,套件的 BulletinService 只做 CRUD。

Q2 資料

自己擁有的表:一張 | 表 | 存什麼 | 關鍵欄位 | |---|---|---| | bulletins(M .../infra/models/bulletin.py:16) | 一則系統公告 | uid、title、content、category(default "others")、release_time / expire_time、enable(1/0)、is_delete(1/0 軟刪除) |

繼承 BaseModel, TenantScopedMixinModel(:14)→ 多租戶模式下有 tenant_id / org_unit_id。

對別人的表的參照:套件端零。跨包 import 0 處(README:98 宣稱,我逐檔 grep 確認:全包只 import jedi_common + sqlalchemy + 自己)。

反向,宿主對它做了三件事(H infra/bulletin/models/bulletin.py):

  • (a) 真 FK:bulletin_org_units.bulletin_id → bulletins(id) ON DELETE CASCADE(:16-20,DEV DB 查證 bulletin_org_units_bulletin_id_fkey);同表另一欄 org_unit_id → org_units(id)(jedi-iam 的表)
  • (b) ORM relationship 跨包:ExtendedBulletin(BaseBulletin, TenantScopedMixinModel)(:29)用 __table_args__ = {"extend_existing": True} 就地擴充套件的 model,加上 org_units many-to-many(:41-45)與 created_user_info / updated_user_info 對 jedi_iam.User 的 relationship(:47-57、:75-85)+ 兩個 hybrid_property 發 scalar_subquery 查 User.nickname
  • (d) raw SQL:無

這個 extend_existing 擴充模式是本批唯一一例——宿主不是「用」套件的 model,是改寫它。

產品詞彙洩漏:無。

Q3 Port

需要宿主提供(M .../plugin.py:115-127 BulletinAdapters): | Port | 簽名 | 現況 | |---|---|---| | settings | ISettingsReader.get(key, default)(:79-85) | 零使用 | | notifier | INotifier.send(*, to, subject, body)(:89-97) | 零使用 |

兩張 port 都是樣板殘留——README:93-102 自己就這樣寫(「插槽開齊是為了日後真需要時簽名不用改,不是現在有東西被 port 化了」),且有測試焊死。

提供給別人:BulletinService、BulletinDomainService、BulletinRepoImpl、BulletinEntity / BulletinQueryEntity、BulletinDTO、Bulletin model。

另一處樣板殘留:M .../common/enum/code.py 整支 37 行是從別支套件照抄的死碼——UserStatus / LogTypeCode / RoleStatusCode / ChangePasswordTypeCode / ChangePasswordStatusCode 全部與公告無關,且全包 grep 零使用(我查過,只有定義處那一行命中)。

Q4 消費者

(a) 主專案:只有 3 處(全是「繼承/別名」而非呼叫)

  • H infra/bulletin/models/bulletin.py:4 — from jedi_bulletin.infra.models.bulletin import Bulletin as BaseBulletin
  • H app/bulletin/dto/bulletin.py:4 — BulletinDTO as BaseBulletinDTO
  • H domain/bulletin/entities/bulletin_entity.py:1 — BulletinEntity as BaseBulletinEntity

也就是說:主專案沒有用套件的 service / domain service / repo,只繼承了 model / DTO / entity 三個資料形狀,業務全部自己寫(H app/bulletin/service/bulletin_service.py 181 行 + H domain/bulletin/ 整層 + H infra/bulletin/repository/)。README:44 說「被上者間接使用」——不成立,宿主的 BulletinDomainService 是自己的(H domain/bulletin/service/bulletin_domain_service.py),不是套件的。

(b) 其他 jedi 套件:零。

(c) 註冊狀態:grep jedi_bulletin.plugin 在主專案 零命中——register() 從沒被呼叫過。套件的插件機制在本產品上是完全未使用的。

Q5 執行期形狀

  • Route 0 條(blueprint 是空的,plugin.py:182-184)。對外端點是宿主的 H api/bulletin/__init__.py:20-24 三條
  • 背景排程 / worker / thread:無
  • 對外 I/O:只有 DB。零 subprocess、零 HTTP、零檔案系統、零 socket
  • 快取 / Redis:無
  • 單獨起 process:沒有意義——零 route。harness 起來只有 /healthz

Q6 通用性

(a) 能直接用——它就是一張表的 CRUD,任何產品都能裝。

(b) 寫死的產品知識:

  • bulletins.category default "others"(infra/models/bulletin.py:41-43)——類別集合本身沒定義,是自由字串,不算寫死
  • plugin.py:74 DEFAULT_BLUEPRINT_NAME = "jedi_bulletin"(避開主專案同名 blueprint,可覆寫)
  • 零 Guidant 產品詞彙
  • 唯一的「不通用」是 README:113-116 那兩道宿主前提:必須有 tenants / org_units 在 SQLAlchemy metadata 裡(TenantScopedMixinModel 的 FK 在 mapper 設定期就要解析),這把套件綁死在「jedi 多租戶生態」上

對 身分名冊/通知 的依賴形式(題目特別問的):

  • 身分名冊:套件端零依賴。公告的 created_user / updated_user 只存 login_name 字串(BaseModel 給的),補 nickname 這件事完全在宿主(H infra/bulletin/models/bulletin.py:47-100 用 ORM relationship + hybrid_property 直接 join jedi_iam.User)——注意這違反了主專案自己的「審計欄位在 app service 層批次查、不在 infra 層 JOIN」規範。
  • 通知:INotifier port 宣告了但零使用(plugin.py:89-97、126-127)。宿主的 H app/bulletin/service/bulletin_service.py grep notif|mail|send 零命中——公告發布目前根本不寄通知。「公告 → 通知」這條線在整個系統裡不存在。
  • 公告發給哪些部門:在宿主 H domain/bulletin/service/bulletin_org_unit_domain_service.py,README:153-154 明文說不在套件。

Q7 插件完整度

項目 有/無
① api 層隨包 ❌ 無(blueprint 空的)
② 註冊入口 ✅ 簽名齊全 register(app, adapters=None, config=None, schema_extensions=None, mount_api=True)(plugin.py:196)——但註冊了什麼都不會發生
③ migrations 隨包 ❌ 無 migrations/ 目錄
④ DI container 預設 ❌ 無
⑤ 獨立 harness ✅ harness/dev_app.py 200 行 + docker-compose(port 5491),會實跑建表→CRUD→軟刪除逐步斷言
⑥ 接入 README ✅ 190 行

Q8 糾纏對象

本批八支裡最不糾纏的一支。 它跟任何一支 jedi 套件都沒有 FK、沒有 import、沒有共同交易。

唯一的資料層牽連是與 jedi-iam:bulletin_org_units 這張橋表一頭接 bulletins、一頭接 org_units(jedi-iam 的表),且這張表定義在宿主而非任一套件(H infra/bulletin/models/bulletin.py:13-26)。所以「公告發給哪些部門」這個關聯的擁有者是宿主,不是兩支套件任一方。

看起來像但不是一件事:

  • jedi-notification(不在本批,但相關):bulletin 宣告了 INotifier port 讓人以為兩者該接起來,實際上零接線,且宿主也不寄公告通知。
  • jedi-issue:形狀相似(文字 + 狀態 + 時間 + 軟刪除)但零關聯,見 issue 的 Q8。

§4

jedi-device

Q1 它是什麼

一台實體/虛擬機器的基本資料(名稱、IP、OS、製造商、用途)的 CRUD 與 picker 選單查詢。README:3 標「D11 已定:本套件歸資產盤點語境」。

README 與程式碼大致一致,但有一處明確錯誤:

  • README:145 常見坑 4 寫「刪除是軟刪除(is_delete=1),資料列仍在」——程式碼裡 devices 表根本沒有 is_delete 欄位(M jedi-device/jedi_device/infra/models/device.py 全檔無此欄;全包 grep is_delete 零命中;DEV DB devices 欄位清單也沒有)。DeviceRepoImpl 走的是 BaseRepositoryImpl 的實刪。這條是從 jedi-bulletin README 照抄漏改的。
  • README:11-19 的「驗收邊界誠實聲明」(沒有 api 層、拔掉測試做不到)屬實:plugin.py:174-197 的 blueprint 不掛任何 resource。

Q2 資料(決策者要的表欄位全列)

自己擁有的表:一張

欄位 型別 Null 說明
id integer NO PK(BaseModel)
uid varchar(50) NO UUID,unique
name varchar(255) NO 設備名稱
description text YES 描述
os varchar(255) YES 作業系統
os_version varchar(100) YES OS 版本
hostname varchar(255) YES 主機名
ip varchar(50) NO IP 位址
device_type varchar(255) YES 設備類型(自由字串,無 enum)
manufacture varchar(255) YES 製造商
manufacture_at date YES 製造日期
status integer NO 1正常 / 0異常(整數,非 enum)
purpose text YES 用途
created_at / updated_at timestamp NO BaseModel
created_user / updated_user varchar(50) YES login_name(BaseModel)
tenant_id integer YES TenantScopedMixinModel
org_unit_id integer YES 同上

(表定義 M .../infra/models/device.py:11-85;欄位型別/nullable 以 DEV DB information_schema.columns 實查為準。schema 是 public,不是 compliance。)

⚠️ devices.tenant_id 是 nullable,information_systems.tenant_id 是 NOT NULL——這是兩支合併時的第一個 schema 落差。

對別人的表的參照:套件端零(跨包 import 0 處,README:92 宣稱,逐檔 grep 確認)。

反向,三張表對 devices 建了真 FK(DEV DB pg_constraint 查證):

  • compliance.job_execution_device_mapping.device_id → devices(id)(H infra/associations/model/job_execution_device_mapping.py:78,ondelete="CASCADE")
  • compliance.job_execution_devices.device_id → devices(id)
  • compliance.project_job_execution_device_mapping.device_id → devices(id)

還有一支跨套件 ORM relationship:M jedi-survey/jedi_survey/infra/models/task_survey.py:1,136-145 — TaskSurvey.device_id + device relationship + device_uid / device_name association_proxy。jedi-survey 直接 import 並 join jedi-device 的 model。

產品詞彙洩漏:無(devices 是通用 IT 資產語彙)。

Q3 Port

需要宿主提供(M .../plugin.py:118-130 DeviceAdapters):settings(ISettingsReader)與 notifier(INotifier)——兩張都零使用,README:81-84 與 plugin.py:54-55 都明文承認是樣板殘留。

提供給別人:DeviceService(app 層,9 支方法)、DeviceDomainService(12 支)、DeviceRepoImpl、DeviceEntity / DeviceQueryEntity / DeviceMenuEntity、DeviceDTO / DeviceMenuDTO、Device model、DeviceMapper、ErrorCode。

Q4 消費者

(a) 主專案:11 個檔案

  • H app/device/service/device_app_service.py:10-12 — wrapper app service(98 行),加審計 nickname enrich 與刪除前引用計數
  • H common/util/system_asset_snapshot.py:170 — 與 information-system 並列使用的共用檔(見 Q8)
  • H app/module_frame/service/ssp_import_template_app_service.py:62-63,161,197,757,855,973,1409 — Excel 匯入匯出的設備 picker
  • H app/associations/service/job_execution_device_mapping_service.py:5-6、H app/associations/dto/job_execution_device_mapping_dto.py:4
  • H infra/flow_control/repository/{job_export_query,flow_control_job_repo_impl,job_import_lookup_query}.py — 直接 import Device model 做 join
  • H infra/associations/{model,repository,mapper}/job_execution_device_mapping*.py
  • H di_containers/device/device_containers.py:4-6
  • 對外端點是宿主的 H api/device/__init__.py:22-27 5 條(/devices、/devices/menu、/device、/device/<uid>、/device/<uid>/references)

(b) 其他 jedi 套件:jedi-survey 1 處(task_survey.py:1,ORM relationship,見 Q2)。

(c) 註冊狀態:grep jedi_device.plugin 主專案 零命中——register() 從沒被呼叫。

Q5 執行期形狀

  • Route 0 條(blueprint 空的)
  • 背景排程 / worker / thread:無
  • 對外 I/O:只有 DB
  • 快取 / Redis:無
  • 單獨起 process:沒有意義(零 route)

Q6 通用性

(a) 能直接用。 (b) 寫死的產品知識:

  • status 用 magic number 1正常/0異常(device.py:71-77),沒有 enum 類別
  • plugin.py:78 DEFAULT_BLUEPRINT_NAME = "jedi_device"
  • 零 Guidant 產品詞彙
  • 同 bulletin:綁死 tenants / org_units 在 metadata 裡這道前提

Q7 插件完整度

項目 有/無
① api 層隨包 ❌ 無
② 註冊入口 ✅ 簽名齊全(plugin.py:199)+ create_blueprint(:174)
③ migrations 隨包 ❌ 無
④ DI container 預設 ❌ 無
⑤ 獨立 harness ✅ 192 行 + docker-compose(port 5493)
⑥ 接入 README ✅ 175 行(但常見坑 4 是錯的,見 Q1)

Q8 糾纏對象

最糾纏:jedi-information-system——決策者已裁定合併,以下是支撐證據(也是反面證據):

支持「同一件事」的事實:

  1. 宿主已經用一張表把兩者統一了:compliance.module_frame_inventory_item_defaults 與 oscal.ssp_inventory_items 兩張表都有 asset_type / ref_type / ref_id 三欄(H scripts/sql/2026-06-04-fr032-inventory-asset-type.sql:19-38),asset_type 的值域就是 'hardware'(=device)與 'information_system',ref_type 是 'device' / 'information_system'。資料模型上兩者已經是同一張表的兩個 discriminator 值。
  2. 同一支檔案同時操作兩者:H common/util/system_asset_snapshot.py 整支 197 行就是「把 device master 或 information_system master snapshot 成 inventory props」——snapshot_information_system()(:39)與 snapshot_device()(:177)是姊妹函式,_match_information_system_by_name()(:106)與 _match_device_by_name()(:167)是姊妹函式,resolve_system_asset_fields()(:54)與 resolve_device_inventory_fields()(:137)是姊妹函式。檔頭常數 ASSET_TYPE_HARDWARE / ASSET_TYPE_INFORMATION_SYSTEM / REF_TYPE_DEVICE 三個並列在 :17-19。
  3. 同一支 app service 同時注入兩個 domain service:H app/module_frame/service/ssp_import_template_app_service.py:161-162 建構子並列收 device_domain_service 與 information_system_domain_service;:197-198 存成 self._device_domain_service / self._is_domain_service。Excel 匯入時兩張 sheet 各走一條,落到同一張 inventory 表。
  4. 兩支套件的 pyproject 相依完全相同:Flask>=3.0,<4 + SQLAlchemy==2.0.37 + jedi-common>=0.0.23,一字不差。
  5. 兩支的 plugin.py 結構完全相同(同批 P3.5 補殼,ISettingsReader / INotifier 兩張零使用 port、空 blueprint、同樣的 EXTENSION_KEY 命名法)。
  6. 兩支的 README 有相同的照抄錯誤(都寫「刪除是軟刪除 is_delete=1」,兩支都沒這欄位——info-system README:145、device README:145)。

反對「同一件事」的事實(合併時會撞到的):

  1. schema 不同:devices 在 public,information_systems 在 compliance(M .../information_system.py:21-22 "schema": "compliance")。合併後兩張表要不要搬 schema 是 breaking change(devices 有三條 FK 指著)。
  2. tenant_id nullable 不同:devices YES / information_systems NO。
  3. enum 使用完全不同:information-system 有 4 個 PostgreSQL ENUM 型別(security_sensitivity_level_enum / security_objective_level_enum / system_status_enum / deployment_model_enum,M .../common/enum/information_system_enum.py,值域全是 OSCAL / FIPS-199 詞彙:fips-199-low、operational、under-major-modification、on-premise…);device 一個 enum 都沒有(status 是裸 integer、device_type 是自由字串)。
  4. information-system 有 OSCAL 序列化:M .../domain/entity/information_system_entity.py:57-114 的 to_oscal_dict() / from_oscal_dict() 直接吐 OSCAL SSP system-characteristics 結構(system-id / security-sensitivity-level / authorization-boundary…)。這是產品詞彙洩漏(見該支 Q2),device 完全沒有。
  5. owner 欄位形狀不同:information-system 有 system_owner(int,users.id 軟參照)+ created_by(int)+ 一張 IUserLookup port;device 完全沒有 owner 概念,只有 BaseModel 的 created_user(login_name 字串)。

共同欄位盤點(合併後資料模型的交集):id / uid / name / description / created_at / updated_at / created_user / updated_user / tenant_id / org_unit_id。也就是說——兩張表的交集就是 BaseModel + TenantScopedMixinModel 給的那些,加上 name 與 description 兩個業務欄位。 其餘 11 個 device 欄位與 12 個 information-system 欄位零重疊。

誰在 JOIN 它們:

  • devices:compliance.job_execution_device_mapping / job_execution_devices / project_job_execution_device_mapping 三張表以真 FK join;jedi-survey 的 task_survey 以 ORM relationship join。
  • information_systems:DB 上零 FK 指向它(DEV 查證,pg_constraint 無任何 confrelid = compliance.information_systems)。曾有 compliance.project_information_systems 橋表但已於 2026-05-23 DROP(H scripts/sql/2026-05-23-c3-pr2-drop-project-mapping-tables.sql:24)。現在只靠 inventory 表的 ref_id(存 uid 字串)軟參照。
  • 兩者共同的下游是 inventory 表(oscal.ssp_inventory_items / compliance.module_frame_inventory_item_defaults),而那張表對兩者都是軟參照(ref_id 存 uid 字串,無 FK)。

看起來像但其實不是:

  • jedi-remote-agent:remote_agents 也是「一台機器」,但存的是 agent endpoint(base_url / capabilities / 心跳),與 devices 零 FK、零 join、零 import。agent_tasks 沒有 device_id 欄位。兩者在系統中從未被關聯過。

§5

jedi-information-system

Q1 它是什麼

「被列入稽核範圍的資訊系統」的基本資料 CRUD——名稱、縮寫、FIPS-199 三軸安全等級、系統狀態、部署模型、授權邊界、系統負責人。README:3 標「資產盤點的主體之一」,且是 FR-032 G 抽套件的先例。

README 與程式碼大致一致,兩處錯誤:

  • README:145 同樣寫「刪除是軟刪除(is_delete=1)」——表裡沒有 is_delete(全包 grep 零命中)。實際的停用機制是 is_active boolean,domain service 那支叫 deactivate()(M .../domain/service/information_system_domain_service.py:84)。與 device README 是同一個照抄漏改。
  • README:89 說「跨 jedi 套件 import:0 處」屬實。

README:83-85 說「本套件已有一張自己的 port IUserLookup,D16 精神在這支已經實現了」——屬實(M .../domain/repository/user_lookup.py:13),是本批八支裡唯一在 FR-069 之前就自己做過 dependency inversion 的。

Q2 資料(決策者要的表欄位全列)

自己擁有的表:一張

欄位 型別 Null 說明
id integer NO PK
uid varchar(50) NO UUID,unique,對應 OSCAL system-id
name varchar(255) NO 系統全名,對應 OSCAL system-name
abbreviation varchar(100) YES 縮寫,對應 OSCAL system-name-short
description text YES 描述
security_sensitivity_level ENUM compliance.security_sensitivity_level_enum YES low / moderate / high
security_objective_confidentiality ENUM security_objective_level_enum YES fips-199-low/moderate/high
security_objective_integrity ENUM 同上 YES 同上
security_objective_availability ENUM 同上 YES 同上
system_status ENUM system_status_enum YES operational / under-development / under-major-modification / disposition / other
deployment_model ENUM deployment_model_enum YES on-premise / cloud / hybrid / co-location
authorization_boundary text YES 對應 OSCAL authorization-boundary.description
system_owner integer YES 軟參照 public.users.id(無 FK)
is_active boolean NO 停用後不出現在專案選單
created_by integer YES 軟參照 public.users.id(無 FK)
created_at / updated_at timestamp NO BaseModel
created_user / updated_user varchar(50) YES login_name
tenant_id integer NO TenantScopedMixinModel
org_unit_id integer YES 同上

(M .../infra/models/information_system.py:17-137;型別/nullable 以 DEV DB 實查為準。schema 是 compliance。)

對別人的表的參照:

  • (a) 真 FK:零
  • (b) ORM relationship 跨包:零——information_system.py:131-135 有註解明文記錄「FR-032 G 移除對 jedi_auth.User 的 infra 層 ORM join」,改走 IUserLookup 批次 enrich
  • (c) 軟參照:system_owner → public.users.id、created_by → public.users.id(欄位 comment 自陳)
  • (d) raw SQL:無

反向:DB 上零 FK 指向 information_systems(DEV pg_constraint 查證)。

產品詞彙洩漏:有,而且是本批最重的一支。

  • 表 comment 直接寫「儲存可被列入稽核範圍的系統基本資料」(information_system.py:23)
  • 四個 enum 全是 OSCAL / FIPS-199 合規標準詞彙(common/enum/information_system_enum.py:4-28):fips-199-low / operational / under-major-modification / disposition — 這些是 NIST SP 800-18 / FedRAMP 的系統生命週期詞,不是通用 IT 資產詞
  • entity 帶 OSCAL 序列化:domain/entity/information_system_entity.py:57-79 to_oscal_dict() 吐 {"system-id": {"identifier-type": "https://ietf.org/rfc/rfc4122", ...}, "security-objective-confidentiality": ..., "authorization-boundary": {...}};:81-114 from_oscal_dict() 反向解析。這是把 OSCAL SSP 的資料格式焊進「通用資產套件」。
  • 欄位 comment 六處寫「對應 OSCAL xxx」

Q3 Port

需要宿主提供: | Port | 簽名 | 現況 | |---|---|---| | IUserLookup.get_id_by_uid(uid) -> Optional[int](domain/repository/user_lookup.py:17) | user uid → users.id | 真的在用——InformationSystemService._resolve_system_owner()(app/service/information_system_service.py:31)寫入時轉換 | | IUserLookup.get_display_by_ids(ids) -> dict[int, tuple[uid, nickname]](:23) | 批次 id → 顯示名 | 真的在用——InformationSystemDomainService._enrich_owner()(domain/service/...:25)讀取時 enrich | | settings(plugin.py:84) | ISettingsReader | 零使用(樣板) | | notifier(:94) | INotifier | 零使用(樣板) |

宿主實作在 H infra/information_system/jedi_auth_user_lookup.py:13(JediAuthUserLookup implements IUserLookup)。

提供給別人:InformationSystemService(app,10 支方法含 upsert_by_name)、InformationSystemDomainService(8 支)、InformationSystemRepoImpl、InformationSystemEntity / InformationSystemQueryEntity / InformationSystemMenuEntity、DTO、InformationSystem model、四個 enum、IUserLookup ABC。

Q4 消費者

(a) 主專案:8 個檔案(不含測試/docs)

  • H api/information_system/routes/information_system_route.py:22 — route 層直接 import 套件的 app service(是本批唯一一支 route 直接吃套件 service 的)
  • H infra/information_system/jedi_auth_user_lookup.py:13 — IUserLookup 實作
  • H common/util/system_asset_snapshot.py:111 — 與 device 並列(見 device Q8)
  • H app/module_frame/service/ssp_import_template_app_service.py:64-67,162,198 — Excel 匯入
  • H app/oscal/service/ssp_resources_context_service.py:153 — SSP 匯出時取系統資料
  • H app/module_frame/excel_template/sheet_definitions.py:253 — 註解說「直接用 OSCAL 值」
  • H di_containers/information_system/information_system_container.py:5-7
  • 對外端點是宿主的 H api/information_system/__init__.py:23-26 4 條(menu / list / create / detail),其中 list 那條吃的是宿主的 InformationSystemAppService(做 nickname enrich),其餘三條吃套件的 service(README:26-33 說明)

(b) 其他 jedi 套件:零。

(c) 註冊狀態:register() 從沒被呼叫(grep jedi_information_system.plugin 主專案零命中)。

Q5 執行期形狀

  • Route 0 條(blueprint 空的,plugin.py:175-198)
  • 背景排程 / worker / thread:無
  • 對外 I/O:只有 DB
  • 快取 / Redis:無
  • 單獨起 process:沒有意義(零 route)

Q6 通用性

(a) 不能直接用。 一個工單系統裝它,會得到一張帶四個 FIPS-199 / OSCAL enum 的表——那些欄位對非合規產品毫無意義,且 to_oscal_dict() 這支 API 完全用不上。要用只能忽略掉一半的欄位。

(b) 寫死的產品知識(本批最多): | 位置 | 內容 | |---|---| | common/enum/information_system_enum.py:10-13 | SecurityObjectiveLevel = fips-199-low / moderate / high(FIPS 199 標準) | | :23-28 | SystemStatus = operational / under-development / under-major-modification / disposition / other(OSCAL system-status) | | :16-20 | DeploymentModel = on-premise / cloud / hybrid / co-location(OSCAL deployment-model) | | domain/entity/information_system_entity.py:57-114 | 整組 OSCAL SSP system-characteristics 序列化/反序列化 | | infra/models/information_system.py:23 | 表 comment「可被列入稽核範圍」 | | infra/models/information_system.py:21 | schema 硬寫 "compliance"(README:106 列為宿主前提:庫必須先有這個 schema) |

Q7 插件完整度

項目 有/無
① api 層隨包 ❌ 無
② 註冊入口 ✅ 簽名齊全(plugin.py:200)+ create_blueprint(:175)
③ migrations 隨包 ❌ 無
④ DI container 預設 ❌ 無
⑤ 獨立 harness ✅ 198 行 + docker-compose(port 5494)
⑥ 接入 README ✅ 173 行(常見坑 5 是錯的,見 Q1)

Q8 糾纏對象

最糾纏:jedi-device——完整證據見 device 的 Q8(不重複),要點:

  • 宿主已用單一 inventory 表 + asset_type discriminator 統一兩者
  • H common/util/system_asset_snapshot.py 三對姊妹函式並列處理兩者
  • H app/module_frame/service/ssp_import_template_app_service.py:161-162 同一建構子並列注入兩個 domain service
  • 但欄位交集只有 name / description,其餘全部不重疊;schema 不同、tenant_id nullable 不同、enum 有無不同、owner 模型不同

與 jedi-oscal-v2 的關係(本批外,但值得記錄):InformationSystemEntity.to_oscal_dict() 產出的是 OSCAL SSP system-characteristics 區塊,而 SSP 的擁有者是 jedi-oscal-v2。宿主 H app/oscal/service/ssp_resources_context_service.py:153 在 SSP 匯出鏈路上 import 本套件。「資訊系統是 SSP 的一部分」這個關係目前靠宿主膠水維繫——但要注意這條線指向的是 oscal 而不是 device,與「合併成資產套件」的方向不同。

看起來像但其實不是:

  • jedi-remote-agent:都有「系統/機器 + 狀態」,但零關聯(見 device Q8 同一段)。

§6

jedi-remote-agent

Q1 它是什麼

「裝在客戶機房裡的代理程式」的註冊、心跳、任務派送、身分驗證(mTLS + 內部 CA + 短效 JWT) 四件事。README:6-8 白話講得很準:「那台機器怎麼被系統認得、怎麼證明還活著、伺服器怎麼把工作交給它、雙方怎麼互相驗證身分;工作內容本身留給產品自己決定」。

README 與程式碼一致,且是本批最完整的一份(282 行,含端點表、port 表、常見坑對照表)。程式碼確認:

  • README:265 說「每支端點的認證意圖在 ROUTE_TABLE 第三欄顯式宣告」→ M jedi-remote-agent/jedi_remote_agent/api/remote_agent_route.py:216-233 確實是 (path, Resource, needs_admin_bool) 三元組,agent 側四支明確標 False
  • README:180-182 三張 port「未注入時優雅降級」→ plugin.py:122-124 三個欄位皆 = None 預設

Q2 資料

自己擁有的表:三張 | 表 | 存什麼 | 關鍵欄位 | |---|---|---| | compliance.remote_agents(infra/remote_agent/model/remote_agent.py:16) | 一台已註冊的 agent | uid、base_url、agent_type(default file_storage)、device_fingerprint(SHA-256)、status(active/revoked)、last_seen_at、capabilities(JSONB)、hardware_info(JSONB) | | compliance.remote_agent_enroll_tokens(.../remote_agent_enroll_token.py:14) | per-tenant 一次性註冊 token | token_hash(sha256,不存明文)、label、enabled | | compliance.agent_tasks(infra/agent_task/model/agent_task.py:16) | 派工單 + 狀態機 | agent_id、detection_tool_id、job_execution_uid、params(JSONB)、status、result_ref(JSONB)、scheduled_at |

三張全繼承 TenantScopedMixinModel。

對別人的表的參照:全是 (c) 軟參照,零真 FK(M .../migrations/001-remote-agent-tables.sql:94-96 三條 COMMENT 自陳):

  • agent_tasks.agent_id → compliance.remote_agents.id(BIGINT,連自己套件內的表都沒建 FK)
  • agent_tasks.job_execution_uid → 宿主的執行紀錄(VARCHAR(50),指向 compliance.job_executions)
  • agent_tasks.detection_tool_id → config.detection_tools.id(指向 jedi-detection 的表)

DEV DB 查證:compliance.agent_tasks 上零 FK constraint,確認軟參照。

產品詞彙洩漏:有,兩處

  1. agent_tasks.detection_tool_id 是 NOT NULL(agent_task.py:31-33 nullable=False;migration:79 BIGINT NOT NULL;DEV DB is_nullable=NO 確認)。這是最硬的一處洩漏——套件宣稱「工作內容留給產品決定」,但每張派工單都強制要有一個「檢測工具 id」。一個不做檢測的產品(例如備份 agent、監控 agent)建不出一張合法的派工單。DEV 實查 79 筆 agent_tasks,detection_tool_id 79 筆全有值。
  2. remote_agents.capabilities 欄位 comment 寫死「派掃描任務只給含 detection_scan 者」(remote_agent.py:44-47;migration:47 同樣的 COMMENT)。
  3. 錯誤碼前綴 FILE_AGENT_*(common/code.py:26-38)——「file agent」是 FR-039 的歷史命名,現在 agent 已多能力,但字串凍結為對外契約不能改(檔頭:3-10 說明)。
  4. 事件碼硬編整數 6082 / 6083(common/code.py:57-58),與主專案 common/enum/event_code.py 同值。

capabilities 的實際值域(題目特別問的):

  • 套件內沒有 enum 類別——capabilities 是自由 JSONB list,套件只做 capability in (a.capabilities or []) 的成員判定(domain/remote_agent/service/remote_agent_domain_service.py:50,111)
  • DB default 是 '["file_storage"]'(migration:37)
  • DEV DB 實查:SELECT DISTINCT capabilities FROM compliance.remote_agents → 只有一種值 ["file_storage", "detection_scan"]
  • 也就是說:detection 以外只有 file_storage 一個,且兩者實務上總是成對出現。所謂「多能力」目前只是二選一的旗標
  • 三態語意(README:277):心跳沒帶 / 帶空清單 / 帶非 list 都回 None 不動 DB 現值,只有非空 list 才寫入(app/service/agent_enrollment_service.py:50-70)

Q3 Port

需要宿主提供(M .../plugin.py:118-127 RemoteAgentAdapters): | Port | 簽名 | 必填 | 未給時 | |---|---|---|---| | admin_required | (fn) -> fn | 是(無預設值,dataclass 前兩欄無 default) | — | | current_user | () -> str | 是 | — | | settings_provider | () -> AgentAuthSettings | 否 | 退回 config 內的靜態值 | | payload_provider | IAgentTaskPayloadProvider.build_pending_payloads(tasks, tenant_id) -> list[dict] | 否 | 待辦只回 {"uid": ...} 空信封 | | lifecycle_listener | IAgentTaskLifecycleListener:on_task_claimed(task) / on_scan_succeeded(task, summary) / on_scan_failed(task, error_message) | 否 | 三個掛鉤不呼叫 | | recon_query | IRemoteAgentReconQuery.list_files_for_base_url(base_url) -> List[dict](domain/remote_agent/repository/recon_query.py:22) | 否 | reconcile 回空盤點 | | audit | AuditHook | 否 | 套件自帶同格式版本 | | response_builder | (ok, data) -> dict | 否 | return_response |

⚠️ IAgentTaskLifecycleListener 的方法名 on_scan_succeeded / on_scan_failed 帶 "scan" 這個檢測詞彙——這是 port 命名層的產品知識洩漏(一個備份 agent 完成任務不叫 "scan succeeded")。

提供給別人:RemoteAgentService / AgentEnrollmentService / AgentEnrollTokenService / AgentTaskService(app 層 4 支)、三個 domain service、AgentAuthSettings、jwt_util、build_cloud_mtls_context、is_online、RemoteAgentErrorCode / AgentTaskErrorCode、三個 ORM model。

樣板殘留:SchemaExtensions(plugin.py:132-150)插槽開齊零使用。

Q4 消費者

(a) 主專案 app/remote_agent 做什麼(題目特別問的):整個目錄只有 3 支 adapter,212 行,全部是「把套件的 port 接到 detection 的編排」:

  • H app/remote_agent/adapter/detection_task_payload_provider.py(125 行)— 實作 IAgentTaskPayloadProvider。每張單補上 ① detection_tool_code(查 detection_tools 目錄)② 解密後的 params(走 jedi_detection.common.detection_secret_params)③ 該租戶該工具的憑證(查 tenant_detection_tool_configs 解密)④ source file 限額。檔頭:3-8 明說「套件管信封、這裡管內容」
  • H app/remote_agent/adapter/detection_lifecycle_listener.py(32 行)— 實作 IAgentTaskLifecycleListener,三支方法各一行轉呼叫 DetectionOrchestrationService
  • H app/remote_agent/adapter/lazy.py(55 行)— 三個 port 的延遲解析代理(create_module() 跑在 blueprint 載入迴圈裡,此時 DI 拿不到實例)

主專案其他接觸點:

  • H api/remote_agent/__init__.py(99 行)— 組 adapters 呼叫 create_blueprint();另外單掛一條留在主專案的 route GET /agents/files/<uid>(:96,回 binary 串流不是 JSON 信封,授權走 detection resolver)
  • H infra/remote_agent/recon_query_adapter.py:12 — 實作 IRemoteAgentReconQuery,query UploadFile 篩 storage_type='remote_agent'
  • H infra/upload_file/remote_agent_adapter.py:25,27 — 反向:實作 file-upload 的 IUploadFileProvider,借用 remote-agent 的 jwt_util 與 build_cloud_mtls_context 打 agent 取檔
  • H app/flow_control/service/job_service.py:19、H app/oscal/service/ssp_control_implementation_service.py:29 — 只借 is_online() 判在線
  • H common/util/agent_auth_settings.py:15、H di_containers/remote_agent/、H di_containers/agent_task/

(b) 其他 jedi 套件:jedi-detection 3 處(M jedi-detection/.../agent_probe_client.py:18,20、agent_cancel_client.py:16,18、app/service/detection_result_handler.py:36,38)——全都是借用 common.agent_auth.jwt_util 與 tls.build_cloud_mtls_context 這兩支認證原語去打 agent。

Q5 執行期形狀

  • Route 15 條(api/remote_agent_route.py:216-233 ROUTE_TABLE),blueprint 名 remote-agent,prefix /api/1.0。11 條管理端(套 admin_required)+ 4 條 agent 側(/agents/register、/agents/heartbeat、/agents/tasks/<uid>/ack、/agents/tasks/<uid>/result,刻意不掛守門,身分靠 mTLS + payload 自報)
  • 背景排程 / worker / thread:套件內無(心跳是 agent 主動打進來的 pull 模型,不是雲端 push)
  • 對外 I/O:httpx HTTP/mTLS 打 agent(app/service/remote_agent_service.py:62-66,236,244 健康檢查與對帳)、檔案系統讀憑證(CA cert/key、JWT 金鑰對、client cert,common/agent_auth/settings.py)、cryptography 簽 CSR(common/agent_auth/ca.py)
  • 快取 / Redis:無
  • 單獨起 process:可以——自帶 15 條 route + 自帶 migration + harness(harness/dev_app.py 208 行,有 --migrate / --seed 兩個模式)

Q6 通用性

(a) 不能直接用。 阻斷點是 agent_tasks.detection_tool_id NOT NULL:一個備份 agent 產品裝上它,建不出一張合法派工單,除非塞假值。

(b) 寫死的產品知識: | 位置 | 內容 | |---|---| | infra/agent_task/model/agent_task.py:31-33 + migrations/001:79 | detection_tool_id BIGINT NOT NULL(最硬的一處) | | infra/remote_agent/model/remote_agent.py:46 + migrations/001:47 | capabilities comment「派掃描任務只給含 detection_scan 者」 | | domain/agent_task/repository/lifecycle.py(port 方法名) | on_scan_succeeded / on_scan_failed 帶檢測詞 | | common/code.py:26-38 | 全部錯誤碼前綴 FILE_AGENT_*(歷史命名,凍結為 FE i18n key) | | common/code.py:57-58 | 事件碼硬編 6082 / 6083 | | infra/remote_agent/model/remote_agent.py:31 | agent_type default "file_storage" | | 三張表 schema 硬寫 compliance | migration:23,53,72 |

Q7 插件完整度

項目 有/無
① api 層隨包 ✅ 15 條 route + serializers(86 行)
② 註冊入口 ✅ 兩個:register(app, adapters, config=None, schema_extensions=None, mount_api=True)(plugin.py:318)與 create_blueprint(adapters, config, schema_extensions, handle)(:270)。主專案走後者(main.py 的載入迴圈是 app.register_blueprint(mod.create_module()))。另有 build_services(adapters, config)(:203)供 mount_api=False
③ migrations 隨包 ✅ 2 支(001 建三表+索引+COMMENT、002 RLS+GRANT),FR-069 的首例(README:194),pyproject:48-50 有 include
④ DI container 預設 ⚠️ 半有 — build_services() 自己組完整的 repo→domain→app 三層(plugin.py:203-268),不需要外部 DI
⑤ 獨立 harness ✅ 208 行 + docker-compose(port 55432),有完整的 register→heartbeat curl 劇本
⑥ 接入 README ✅ 282 行,本批最完整

Q8 糾纏對象

最糾纏:jedi-detection(雙向,且是本批最深的耦合)

  • 資料層:agent_tasks.detection_tool_id NOT NULL → config.detection_tools.id(jedi-detection 的表)。這是「一邊沒有另一邊就沒意義」的教科書案例——沒有 detection_tools 表,agent_tasks 一列都寫不進去(雖然是軟參照不是真 FK,但 NOT NULL 讓它等效於必要相依)。
  • 程式碼層雙向:detection → remote-agent 3 處 import 認證原語;remote-agent → detection 零 import(走 port),但兩張 port(payload_provider / lifecycle_listener)的實作100% 是 detection 邏輯(H app/remote_agent/adapter/detection_task_payload_provider.py import 了 jedi_detection.common.detection_source_file 與 detection_secret_params)。
  • 同一交易寫兩邊:on_scan_succeeded 在派工單狀態落地後同步呼叫 detection 的轉證據編排(H app/remote_agent/adapter/detection_lifecycle_listener.py:24-27)。

次糾纏:jedi-file-upload(間接,經宿主)

  • IRemoteAgentReconQuery 的唯一實作 query upload_files(H infra/remote_agent/recon_query_adapter.py:12,26)
  • 反向:upload_files.storage_type = 'remote_agent' 是第四種後端,adapter 在 H infra/upload_file/remote_agent_adapter.py,該檔 import remote-agent 的 jwt_util 與 mTLS builder
  • 兩支套件互相不 import,全部經宿主的兩支 adapter

看起來像但其實不是:

  • jedi-device:都是「一台機器」。但 remote_agents 與 devices 零 FK、零 join、零 import,agent_tasks 也沒有 device 欄位。系統中從沒把「這台 agent 裝在哪台 device 上」關聯起來。
  • jedi-integrity:都算 SHA-256 指紋、都有「machine fingerprint」概念。但 remote-agent 的 device_fingerprint 是客戶機器的識別碼(防 VM 複製撞號),integrity 的 machine_fingerprint 是本機自己的(unlock token 綁定)。兩者零共用程式碼(remote-agent 的指紋是 agent 送上來的,integrity 的是自己算的)。

§7

jedi-integrity

Q1 它是什麼

落地版產品的防篡改偵測:開機時逐檔比對簽章 manifest,不符就拒啟+起一個只回 503 的鎖定殼+留 FS/DB 雙落點證據;運行中每 4h 隨機抽查;只有原廠簽的一次性 unlock token 能解。

README 與程式碼一致,且是本批唯一「README 主動聲明自己不是 REST 插件」的(README:193-201)。程式碼確認:

  • README:26-34 的能力表對得上模組:startup_gate.py(242 行)/ lockdown.py(258)/ runtime_check.py(358)/ tamper_marker.py(159)/ unlock.py(363)/ forensic.py(136)/ lc_report.py(91)
  • README:212-214 說「stdlib-only 紅線」→ pyproject.toml:11-21 註解明說執法核心零 runtime 相依,唯一宣告的 jedi-common>=0.0.30 只給 app/ 與 infra/ 兩層
  • README:36-38 的「邊界誠實聲明」(這是拉高成本不是不可破解)— 這種自我設限在套件 README 裡很少見

Q2 資料

自己擁有的表:一張 | 表 | 存什麼 | 關鍵欄位 | |---|---|---| | public.integrity_tamper_events(migrations/001-integrity-tamper-events-tables.sql:25;表名寫死在 infra/tamper_event_repo.py:28 TABLE_NAME) | 一次被偵測到的竄改事件 | event_uid(UNIQUE,冪等基礎 + unlock token 綁定對象)、detected_at、detected_by(CHECK IN boot/scheduler/hook)、machine_fingerprint、detail(JSONB)、unlocked_at(NULL=仍鎖定)、unlock_nonce(防重放) |

刻意不掛 RLS(README:319-323):竄改是機器級現象與租戶無關,且閘門在無使用者脈絡下執行。有測試焊死「不得出現 RLS」。表無 tenant_id。

對別人的表的參照:零。無 FK、無 relationship。

infra/tamper_event_repo.py 用 raw SQL 打自己的表(:60 INSERT、:88 UPDATE、:98 SELECT,f-string 拼 TABLE_NAME)——不是打別人的表。

產品詞彙洩漏:零。 表名 / 欄位名 / 錯誤訊息全部是通用防竄改語彙。

Q3 Port

需要宿主提供(M jedi-integrity/jedi_integrity/ports.py): | Port | 簽名 | 必填 | |---|---|---| | ISignatureVerifier.verify(envelope: dict, expected_type: str) -> dict(:57) | 離線驗章,manifest 與 unlock token 共用 | 是(無預設) | | IMachineFingerprint.compute() -> str(:80) | 跨重開機穩定的機器識別 | 是 | | IResourceLocator.resource_root() -> Path / is_packaged() -> bool(:96,:104) | 產物根目錄 + 打包判定 | 是 | | TamperEventWriter(型別別名,:117) | (event_uid, detected_at, detected_by, machine_fingerprint, detail) -> bool | 否,未給 fail-open | | TriggerContextCollector(:124) | (detected_by) -> dict 宿主側旁證 | 否 | | ProcessTerminator(:128) | () -> None | 否(README:216 明說「不建議覆寫」) |

ports.py:15-19 有一張很清楚的對照表記錄「抽出前這三件事是直接 import 主專案哪三個模組」。

SignatureVerificationError(:39)是套件定義、要求宿主 adapter 把自己的例外轉成它——README:87-88 紅字警告「漏轉會在啟動路徑上炸成未預期例外」。

樣板殘留:schema_extensions 插槽(plugin.py)有開但 README:190 說「宿主往 tamper 事件多塞欄位」,屬選填未使用。

提供給別人:run_startup_gate(context)、build_context(adapters, config)、register(app, adapters, context, db_event_writer)、handle.checker(run_spot_check() / hotpath_verify(op) / next_spot_check_delay_seconds())、register_gunicorn_master(pid)、TamperEventRepo、TamperSyncService、iter_migrations()、IntegrityConfig / IntegrityContext。

Q4 消費者

(a) 主專案:6 個檔案

  • H main.py:106,112 — run_startup_gate(INTEGRITY_CONTEXT) 在 create_app() 之前(:110 建 context,:112 跑閘門,:121 才 from core.app_factory import create_app)
  • H main.py:223 — gunicorn post_fork 掛 register_gunicorn_master(server.pid)
  • H core/app_factory.py:342-352 — 第二段接線:_write_tamper_event_to_db writer + register_integrity(...)
  • H core/scheduler.py:72,301-304 — 抽查排程(init_integrity_scheduler)+ FS→DB 補同步(TamperSyncService)
  • H common/integrity/adapters.py — 三支 port adapter(LicenseEngineVerifier 包 jedi_license_runtime.common.engine、MachineIdFingerprint 包 jedi_license_runtime.common.machine_fingerprint、ProjectResourceLocator)
  • H common/integrity/__init__.py — 對照表 + build_integrity_context()

(b) 其他 jedi 套件:零(無任何套件 import jedi_integrity)。

啟動時機(題目特別問的):兩段式,第一段在 app 存在之前。

  1. 第一段(閘門):H main.py:112,跑在 validate_required_env() 之後、create_app() 之前。此時 DB / DI / Flask app 全部不存在——這正是套件 stdlib-only 紅線的原因。竄改時此處不返回(起 lockdown 殼或 SystemExit)。
  2. 第二段(DB 落點 + checker):H core/app_factory.py:350-352,必須在 container.wire() 之後(writer 被呼叫時要開 DB session)。
  3. 第三段(排程):H core/scheduler.py 掛抽查 job,用 date trigger 自排下一輪(interval 的 jitter 中心點仍可預測,README:151-163)。api / socketio 兩模式都跑——是唯一兩模式都掛的 job(main.py:166)。
  4. 業務埋點:handle.checker.hotpath_verify("upload-license") 由宿主在低頻高價值操作入口呼叫。

不是中介層——沒有 before_request / after_request hook。

Q5 執行期形狀

  • Route 0 條(README:193-201 明說沒有 api 層,mount_api 參數只為簽名一致而保留)
  • 背景 thread:有 — runtime_check.py:49,73 用 threading.Lock 保護熱路徑;抽查 job 的排程由宿主的 APScheduler 掛(套件只提供 run_spot_check() 與 next_spot_check_delay_seconds())
  • 獨立 HTTP server:有 — lockdown.py:51,234-235 用 stdlib ThreadingHTTPServer 接管原 port 回 503(服務起不來時才由它接管)
  • 對外 I/O:檔案系統(逐檔讀算 sha256、寫 FS tamper 標記、讀 unlock token)、stdlib urllib HTTP(lc_report.py:30-31,69,78 best-effort 回報 License Center,單次嘗試不重試)、process 訊號(termination.py 對 gunicorn master 送 SIGQUIT / os._exit(1))
  • 快取 / Redis:無
  • 單獨起 process:不適用——它的形狀就不是 service,是「在別人的 process 起來之前先跑一段」。harness(410 行)跑的是四個劇本(pass / tamper / unlock / lockdown),不是起服務

Q6 通用性

(a) 能直接用。 這是本批八支裡通用性最好的一支——任何「編譯成 binary 裝在客戶機器上」的產品都能裝,提供三支十行等級的 adapter 即可。

(b) 寫死的產品知識:幾乎沒有

  • migrations/001 的 GRANT 對象假設 cm_app,但包在 pg_roles 存在判斷內(README:302-303),單帳號 consumer 不會整支失敗
  • TABLE_NAME = "public.integrity_tamper_events" 硬寫(infra/tamper_event_repo.py:28,README:322 說明「維運查詢與簽發端都認這個名字」)
  • manifest 三層 core / resources / thirdparty(README:245-252)是套件約定的分層語意,consumer 的 build 產線要照這個結構產 manifest — 算契約不算產品知識
  • 零 Guidant 詞彙、零 os.getenv(有測試守著,README:212-217)

Q7 插件完整度

項目 有/無
① api 層隨包 ❌ 無,且是刻意的(README:193-201 給了理由:對外表面是啟動閘門與鎖定殼,不是 REST 端點;解鎖走檔案系統正因為「要解鎖的機器服務起不來」)
② 註冊入口 ✅ register(app, adapters, config=None, schema_extensions=None, mount_api=True)(plugin.py)+ build_context(adapters, config)(閘門用,不需要 app)
③ migrations 隨包 ✅ 1 支(001 建表;README:296-298 說明「沒有 002-rls 是刻意的,本表不掛 RLS」),pyproject:42-44 有 include
④ DI container 預設 ❌ 無(不需要——它不吃 DI)
⑤ 獨立 harness ✅ 410 行,不需要 DB / Redis / 主產品,會在 tmp 造假 dist + 真 Ed25519 簽章 manifest 跑四個劇本
⑥ 接入 README ✅ 429 行,本批最長。含「宿主要做的四件事」逐步範例、與簽章產線的檔案座標約定、離線解鎖五步流程、七條踩過的坑(含 CM-1241 容器內 killpg 打不到 PID 1 這種只有實測才知道的事)

Q8 糾纏對象

最糾纏:jedi-license-runtime(單向借用,且方向反直覺)

  • 主專案的 ISignatureVerifier 實作包的是 license-runtime 的驗章引擎:H common/integrity/adapters.py:33 from jedi_license_runtime.common.engine import ...;:39 from jedi_license_runtime.common.machine_fingerprint import compute_machine_fingerprint
  • 也就是說:integrity 的兩支必填 port,宿主是拿 license-runtime 的東西去實作的。兩套 Ed25519 驗章共用同一個引擎、同一個公鑰列表機制、同一個機器指紋算法。
  • 反向:license-runtime 有一張 integrity_hook port(M jedi-license-runtime/.../plugin.py:167),宿主接的是 integrity 的 hotpath_verify(README:98)。兩支套件互相在對方身上開了 port,但都不直接 import 對方——全部經宿主的 H common/integrity/adapters.py。
  • H common/integrity/adapters.py:64-70 那段 Nuitka 產物路徑清單同時列 jedi_integrity/ 與 jedi_license_runtime/,兩者在 build 產線上也是綁在一起的。
  • 但兩者的資料完全不共用:integrity_tamper_events(無 tenant,機器級)與 config.tenant_licenses(有 tenant,租戶級)零 FK、零 join。

次糾纏:License Center(外部系統,非套件):lc_report.py best-effort POST tamper 事件到 LC;license-runtime 也打 LC 請照。兩者共用 LICENSE_ACTIVATION_SERVER_URL / LICENSE_CENTER_API_TOKEN 兩個設定值(lc_report.py:24-27 明說「不自創新名」)。

看起來像但其實不是:

  • jedi-file-upload:都算 sha256。但 integrity 算的是程式碼產物的 hash(比對 manifest),file-upload 的 sha256 欄是使用者上傳檔的錨點(給 remote-agent 對帳用)。零共用程式碼、零資料交集。
  • jedi-remote-agent:都有 machine fingerprint。但一個是「本機自己的」、一個是「agent 回報的」,零共用(見 remote-agent Q8)。

§8

jedi-license-runtime

Q1 它是什麼

授權體系的消費端:讀客戶的授權照、Ed25519 驗簽、綁機器指紋、推進到期狀態機(valid→notice→grace→readonly→locked)、到期前後寄通知、提供 root 後台的租戶授權管理端點。簽發那一端在獨立的 License Center 系統。

README 與程式碼一致,且 README:11-24 那張「做 / 不做」表是全批最清楚的疆界宣告:「套件只管驗出狀態,執法留宿主」。程式碼確認:套件內確實沒有任何「擋哪些端點」的邏輯;宿主的兩個執法點在 H common/middleware/license_readonly_mw.py(唯讀 gate)與 H common/authz/license.py(軸⑥模組守門)。

README:26-28 的紅字「宿主的唯讀 gate 必須豁免整段 /license/ 前綴,攔了就是永久死鎖」→ H common/middleware/license_readonly_mw.py:55,69 _EXEMPT_PREFIXES 內確實有 "/license/"。

Q2 資料

自己擁有的表:四張(README:146 說三張,實際四張——tenant_license_suspensions 是 CM-1579 後加的,README 未更新) | 表 | 存什麼 | 關鍵欄位 | |---|---|---| | config.tenant_licenses(infra/model/tenant_license.py:18) | 一列=一張落地的照(replace 制,換發新增列不覆寫) | license_raw(照原文 JSON,逐次驗章依據)、license_id、expires_at、expiry_policy(JSONB)、modules(JSONB)、limits(JSONB)、deployment_mode(saas/host)、machine_fingerprint、kid、status、is_current | | config.tenant_license_events(raw SQL 操作,infra/license_event_repo.py:33,40) | 生命週期事件時間線 | — | | config.tenant_license_suspensions(infra/model/tenant_license_suspension.py:32) | 一列=一個租戶目前人為停權中 | tenant_id(UNIQUE)、suspended_at、suspend_reason、actor | | public.license_clock_watermark(raw SQL,infra/clock_watermark_repo.py:22,30) | 單列(id=1)的時鐘回撥防護浮水印 | max_seen_issued_at |

對別人的表的參照:這是本批最嚴重的一支。

(d) raw SQL 直打別人的表/view — 兩處:

  1. infra/license_tenant_resolver.py 直打 public.tenants(jedi-iam 的表)四次:

    • :66 SELECT path FROM public.tenants WHERE id = :tid
    • :97 SELECT name FROM public.tenants WHERE id = :tid
    • :128 SELECT path FROM public.tenants WHERE id = :tid
    • :133 SELECT count(*) FROM public.tenants WHERE path LIKE :prefix AND id <> :tid 而且全部走「獨立 session + SET LOCAL app.is_super_admin = 't' 繞 RLS」(:148)。檔頭:22-29 說明理由(子租戶對祖先租戶的 SELECT 被 RLS 前綴比對擋住)。 ⚠️ 這條線把「租戶樹的 path 是 /1/102/152/ 這種格式」這個 jedi-iam 的內部資料結構寫死進 license 套件(:74-86 逐段 split 解析)。
  2. infra/tenant_admin_notify_query.py:19-37 一支 raw SQL 同時 JOIN 四張 jedi-iam 的表 + 一個 view:

    FROM public.users u
      ... public.user_roles ur JOIN public.roles r ...
      ... public.v_user_capabilities vc JOIN public.capabilities c ...

    v_user_capabilities 就是題目問的「raw SQL 打 iam 的 view」——確認有,在這裡。 且條件裡硬編了 c.name = 'tenant.update' 這個能力點名稱、r.is_admin = 1、u.status >= -1(檔頭:14 自陳「比照 jedi-auth UserRepoImpl._gen_filters 的既有慣例」)。

(c) 軟參照:tenant_licenses.tenant_id / tenant_license_suspensions.tenant_id 都是「頂層持照租戶 id」,指向 public.tenants.id,無 FK。

(a) 真 FK:零。(b) ORM relationship 跨包:零。

產品詞彙洩漏:無 Guidant 專屬詞。 但有生態綁定:config schema、public.tenants 的 path 格式、v_user_capabilities view 名、tenant.update capability 名——這四樣是「jedi 生態知識」,換一個不用 jedi-iam 的產品全部失效。

Q3 Port

需要宿主提供(M .../plugin.py:160-169 LicenseAdapters): | Port | 簽名 | 必填 | 未給時 | |---|---|---|---| | admin_required | (fn) -> fn | 是(無 default) | — | | login_required | (fn) -> fn | 是 | —(README:112 註「必須比 admin 寬,否則自救路徑死鎖」) | | notifier | INotifier.notify(recipients: List[str], subject: str, body: str)(domain/repository/ports.py:26) | 否 | 不寄信但狀態機照常推進(記 warning) | | tenant_directory | ITenantDirectory.list_tenants() / get_tenant_by_uid(uid)(:56) | 否 | root 後台端點回「查無租戶」 | | is_platform_admin | () -> bool | 否 | 恆 False | | licensed_job_types | () -> list[str] | 否 | 回空清單 | | integrity_hook | (op: str) -> None | 否 | no-op | | response_builder | (bool, Any) -> dict | 否 | 套件自帶信封 |

ports.py:1-18 的檔頭寫得很好:「抽取時把照怎麼驗、狀態怎麼推全部搬進套件,留在宿主的只剩三類產品知識」。

⚠️ 但 ports.py 的宣告與實際不符:它說「其餘(驗章、指紋、狀態機、事件稽核、DB 讀寫)全部在套件內,宿主不必提供」——卻沒提到套件自己繞過 port 直接 raw SQL 打 public.tenants 與 v_user_capabilities。ITenantDirectory 這張 port 存在的意義(「租戶模型是宿主的」)被 license_tenant_resolver 那四條 SQL 直接繞掉了。同一件事(讀租戶)有兩條路:一條走 port、一條走 raw SQL 繞 RLS。

提供給別人:LicenseVerificationService / LicenseStatusService / TenantLicenseAdminService / LicenseExpiryNotificationService、三個 domain service、LicenseErrorCode、common.engine(驗章)、common.machine_fingerprint、LicenseTenantResolver、TenantLicenseEntity、iter_migrations()。

Q4 消費者

(a) 主專案:

  • H api/license/__init__.py:44 — create_blueprint(adapters, config) 組裝(route 全在套件內)
  • H core/scheduler.py:244,269 — 每日 02:00 UTC 跑 run_daily_tick()(從 app.extensions[HANDLE_KEY] 取 handle,不經 DI container)
  • H common/middleware/license_readonly_mw.py:47 — 執法點①唯讀 gate(Flask 中介層)
  • H common/authz/license.py:60 — 執法點②軸⑥模組守門
  • H common/integrity/adapters.py:33,39 — 借驗章引擎與指紋算法給 jedi-integrity 用(見 integrity Q8)
  • H app/auth/service/role_app_service.py:17、H app/setup/service/setup_wizard_service.py:61 — 借 error code / 指紋
  • H di_containers/license/license_containers.py:20-28 — DI wiring

(b) 其他 jedi 套件:零。

Q5 執行期形狀

  • Route 14 條(api/routes.py:41-56 ROUTE_TABLE),blueprint 名 license,prefix /api/1.0。9 條 admin + 5 條 login(/license/status、/license/activation/machine-code、/license/activation/upload、/license/activation/online——README:221-223 紅字「這幾支不可以升級成 admin,開通當下該租戶通常還沒有照」)
  • 背景排程:不自帶,但要求宿主每日跑一次 run_daily_tick(curr_user="system")(README:132-141)。必須在 super-admin RLS bypass 的 session_scope 內,冪等靠 conditional UPDATE(CAS)不需要分散式鎖
  • 對外 I/O:httpx 打 License Center(app/service/tenant_license_admin_service.py:244,283 請照/展延/查方案;app/service/license_verification_service.py:355 線上開通)、檔案系統讀 /etc/machine-id(指紋)、寄信經 INotifier port(套件不直接碰 SMTP)
  • 快取 / Redis:無
  • 單獨起 process:可以——14 條 route + 3 支 migration + harness(355 行,含 --migrate / --issue 兩模式)

啟動時機(題目特別問的):沒有啟動閘門。與 integrity 不同——license 是「app 起來之後掛 blueprint + 中介層 + 每日排程」,不會阻止服務啟動。三個生效點:① blueprint 掛載(create_app() 內)② 每個 request 過中介層 license_readonly_mw(宿主的,不是套件的)③ 每日 02:00 排程 tick。

Q6 通用性

(a) 不能直接用。 三個阻斷點:

  1. raw SQL 綁死 jedi-iam 的 schema:public.tenants(含 path 格式)、public.users / user_roles / roles / capabilities / v_user_capabilities
  2. 綁死 SET LOCAL app.is_super_admin 這個 RLS 慣例(license_tenant_resolver.py:148)
  3. 綁死 config schema

(b) 寫死的產品知識/生態知識: | 位置 | 內容 | |---|---| | infra/license_tenant_resolver.py:66,97,128,133 | 四條 raw SQL 打 public.tenants | | infra/license_tenant_resolver.py:74-86 | 租戶 path /1/102/152/ 格式的逐段解析 | | infra/license_tenant_resolver.py:148 | SET LOCAL app.is_super_admin = 't' | | infra/tenant_admin_notify_query.py:19-37 | JOIN 四表 + v_user_capabilities view | | infra/tenant_admin_notify_query.py:33 | 硬編 capability 名 'tenant.update' | | infra/clock_watermark_repo.py:22 | 硬編 WHERE id = 1(README:162-164 紅字說這列不可省,沒有它整道時鐘回撥防護靜默失效) | | common/public_keys.py | 編譯進程式的公鑰列表(README:170 「不放設定檔、不放 DB——可設定就等於可被替換」)。換環境要改這個檔並重新發版套件 | | common/constant.py DEFAULT_ROOT_TENANT_ID | 預設 1(但已 config 化可覆寫,plugin.py:101) | | 四張表 schema 硬寫 config / public | migrations 001/002/003 |

Q7 插件完整度

項目 有/無
① api 層隨包 ✅ 14 條 route(api/license_route.py 140 行 + license_admin_route.py 143 行 + serializers 109 行)
② 註冊入口 ✅ register(app, adapters, config=None, schema_extensions=None, mount_api=True)(plugin.py)+ create_blueprint(adapters, config)(主專案走這條)
③ migrations 隨包 ✅ 3 支(001 建表+索引+浮水印 seed、002 RLS+GRANT、003 suspensions 表)
④ DI container 預設 ⚠️ 半有 — plugin.py 內自組 service 樹(:300-353),宿主 DI container 只補幾支
⑤ 獨立 harness ✅ 355 行 + docker-compose(port 55433),有 --migrate / --issue 自簽照
⑥ 接入 README ✅ 229 行,含 port 表 / 三道宿主前提 / 公鑰更新四步 / 安全敏感區警告 / 端點表

Q8 糾纏對象

最糾纏:jedi-iam(單向,且是繞過 port 的硬耦合)

  • 兩支 infra 檔用 raw SQL 直打 iam 的五張表 + 一個 view(證據見 Q2)
  • 這是「一邊沒有另一邊就沒意義」的實例:拿掉 jedi-iam,LicenseTenantResolver.resolve() 每次都退回 fail-safe 分支(:69-72 查無 path 就回自己),TenantAdminNotifyQuery 直接 SQL 錯誤
  • 諷刺的是套件已經有一張 ITenantDirectory port 專門處理「租戶模型是宿主的」,卻被同一個套件內的 raw SQL 繞過

次糾纏:jedi-integrity(雙向互開 port,見 integrity Q8,不重複)

  • integrity 的兩支必填 port 用 license-runtime 的引擎實作
  • license-runtime 的 integrity_hook port 接 integrity 的 hotpath_verify
  • 兩者在 Nuitka build 產線清單上並列(H common/integrity/adapters.py:64-70)

看起來像但其實不是:

  • License Center(H ../license_center/):名字幾乎一樣、講同一套授權體系,但刻意分離的獨立系統獨立 DB(BE CLAUDE.md 跨 repo 表已載明:私鑰、客戶名單、訂單、抽佣不進主產品)。兩者只透過「簽好的 License 檔」與幾支 HTTP API 交互,零 DB 共用。這是「看起來該合併但絕不能合併」的典型。
  • jedi-notification(不在本批):license 有 INotifier port,但那是窄到只有一支 notify() 的投遞 port,不是通知領域本身。

§9

跨套件觀察

A. 本批八支之間的關係矩陣

方向讀法:列 → 欄(列依賴欄)。

↓依賴者 \ 被依賴→ file-upload issue bulletin device info-sys remote-agent integrity license-rt
file-upload — — — — — — — —
issue import 9 處+pyproject 宣告+DB 真 FK — — — — — — —
bulletin — — — — — — — —
device — — — — — — — —
info-sys — — — — — — — —
remote-agent 經宿主 adapter(recon_query 讀 upload_files) — — — — — — —
integrity — — — — — — — 經宿主 adapter(驗章引擎+指紋)
license-rt — — — — — — 經宿主(integrity_hook) —

唯一的套件對套件直接 import:issue → file-upload(9 處)。其餘全是「經宿主 adapter」或「無關係」。

B. 本批八支對外(批次外套件)的關係

邊 形式 證據
jedi-detection → file-upload import ORM model 做唯讀查詢 M jedi-detection/.../detection_report_file_query.py:14
jedi-detection → remote-agent import 認證原語(jwt_util / mTLS builder)3 處 M jedi-detection/.../agent_probe_client.py:18,20 等
remote-agent → jedi-detection 資料層軟參照且 NOT NULL:agent_tasks.detection_tool_id → config.detection_tools.id M .../agent_task.py:31-33、migration:79、DEV DB is_nullable=NO
jedi-survey → device import ORM model + relationship + association_proxy M jedi-survey/.../task_survey.py:1,136-145
license-rt → jedi-iam raw SQL 直打 5 表 + 1 view,繞 RLS infra/license_tenant_resolver.py:66,97,128,133,148、infra/tenant_admin_notify_query.py:19-37
bulletin → jedi-iam 經宿主:bulletin_org_units.org_unit_id → org_units(id) 真 FK(DEV DB) H infra/bulletin/models/bulletin.py:23
info-sys → jedi-iam 走 IUserLookup port(正解,零直接依賴) M .../domain/repository/user_lookup.py:13
file-upload → jedi-iam 零(走 jedi_common.identity 標準 port) 全包 grep 零命中

C. DB 層 FK 事實(DEV guidant_ai_dev 實查 pg_constraint)

指向本批套件的表的所有真 FK:

bulletin_org_units.bulletin_id                        → bulletins            [宿主表 → bulletin 套件表]
compliance.job_execution_device_mapping.device_id     → devices              [宿主表 → device 套件表]
compliance.job_execution_devices.device_id            → devices              [同上]
compliance.project_job_execution_device_mapping.device_id → devices          [同上]
issue_assignee_mapping.issue_uid / member_uid         → issues / members     [issue 套件內部]
issue_label_mapping.issue_uid / label_uid             → issues / labels      [issue 套件內部]
issue_upload_file_mapping.issue_uid                   → issues               [issue 套件內部]
issue_upload_file_mapping.file_uid                    → upload_files         [★ 跨套件真 FK:issue → file-upload]
compliance.job_evidences.file_id                      → upload_files         [宿主表 → file-upload 套件表]
oscal.ssp_reference_documents.file_id                 → upload_files         [宿主表 → file-upload 套件表]

零 FK 指向:compliance.information_systems、compliance.remote_agents、compliance.agent_tasks、config.tenant_licenses、public.integrity_tamper_events。

D. 插件完整度總表

套件 api 層 register migrations harness README 宿主有沒有真的呼叫 register()
file-upload ✅ 6 route ✅ ✅ 2 支 ✅ ✅ 188 行 ✅ H core/app_factory.py:328
issue ✅ 1 route ✅ ❌ ❌ ✅ 144 行 ✅ H core/app_factory.py:404
bulletin ❌ 0 route ✅ 空殼 ❌ ✅ ✅ 190 行 ❌ 零命中
device ❌ 0 route ✅ 空殼 ❌ ✅ ✅ 175 行 ❌ 零命中
info-sys ❌ 0 route ✅ 空殼 ❌ ✅ ✅ 173 行 ❌ 零命中
remote-agent ✅ 15 route ✅ + create_blueprint ✅ 2 支 ✅ ✅ 282 行 ✅ H api/remote_agent/__init__.py:97(走 create_blueprint)
integrity ❌(刻意) ✅ + build_context ✅ 1 支 ✅ ✅ 429 行 ✅ H main.py:112 + H core/app_factory.py:352
license-rt ✅ 14 route ✅ + create_blueprint ✅ 3 支 ✅ ✅ 229 行 ✅ H api/license/__init__.py:44(走 create_blueprint)

三支空殼插件(bulletin / device / info-sys)的共同特徵:同一批 P3.5 補殼、同樣的兩張零使用 port(ISettingsReader / INotifier)、同樣的空 blueprint、同樣的 pyproject 三行相依、同樣的 README 照抄錯誤(都寫「軟刪除 is_delete=1」但兩支沒這欄位)、register() 在主專案從未被呼叫過。它們現在的實質身分是「三個被繼承/被 import 的資料形狀 library」。

E. 產品知識洩漏排行(由重到輕)

  1. info-system:四個 OSCAL/FIPS-199 enum + entity 帶 to_oscal_dict() / from_oscal_dict() + 表 comment 寫「稽核範圍」
  2. remote-agent:agent_tasks.detection_tool_id NOT NULL(硬阻斷非檢測產品)+ capabilities comment 寫死 detection_scan + port 方法名 on_scan_succeeded/failed
  3. license-runtime:raw SQL 綁死 jedi-iam schema(5 表 + 1 view)+ SET LOCAL app.is_super_admin RLS 慣例 + 硬編 tenant.update capability 名
  4. issue:4 個 GitLab/GitHub env var 硬編 + 兩個第三方 SDK 進 runtime 相依
  5. device / bulletin:只有 blueprint 預設名與 cm_app GRANT 假設,接近零洩漏
  6. file-upload:接近零(file_access scope 名已 config 化;LibreOffice 是多餘職責不是產品洩漏)
  7. integrity:零(stdlib-only + 零 os.getenv,有測試守著)

F. 執行期形狀分類(能不能單獨起 process)

類別 套件 理由
真能獨立服務化 remote-agent(15 route + 自帶 migration + agent 側 mTLS 端點)、license-runtime(14 route + 3 migration + 打 LC 的 HTTP)、file-upload(6 route + 2 migration + S3/FS I/O) 有完整 api 層、有自己的表、有對外 I/O
形狀上不適用 integrity 它是「在別人 process 起來之前先跑一段」+ 起 lockdown 殼,不是 service
服務化沒有意義 bulletin / device / info-sys(零 route)、issue(1 route,且真正的用例在宿主 feedback 模組) 沒有對外表面,拆出去只是把 DB 呼叫變成 HTTP 呼叫

G. 題目點名事項的直接回答

問題 答案
file-upload 對 iam 的依賴是 import 還是 pyproject? 兩者皆無。全包零 import、pyproject 零宣告。身分名冊走 jedi_common.identity.IdentityContext port,實作在宿主 H core/upload_file_wiring.py:53-106(那支才 import jedi_iam)
storage_type 有沒有被宿主擴成 REMOTE_AGENT? 有。套件的 StorageTypeCode 不含它;宿主定義在 H common/code/file_storage_type.py:9,經 FileUploadConfig.extra_object_storage_types=("remote_agent",) 注入(H core/upload_file_wiring.py:50、plugin.py:118)
issue 用 file-upload 的哪些東西? FileUploadService、UploadFileEntity、UploadFileDomainService、UploadProviderFactory、UploadFileRepoImpl — 5 個類別、9 處 import,全在 local_issue_attachment.py / local_issue_adapter.py / ports/dto/upload_file_dto.py 三檔
remote-agent capabilities 有哪些值?detection 以外還有誰? 沒有 enum 類別,是自由 JSONB list。DB default ["file_storage"];DEV 實查只有一種值 ["file_storage", "detection_scan"]。detection 以外只有 file_storage 一個
agent_tasks.detection_tool_id 是否 NOT NULL? 是。ORM nullable=False、migration BIGINT NOT NULL、DEV DB is_nullable=NO、79 筆資料全有值
宿主 app/remote_agent 做什麼? 只有 3 支 adapter 共 212 行:payload provider(補檢測工具 code/參數解密/憑證/限額)、lifecycle listener(3 行轉呼叫 detection 編排)、lazy 代理
integrity 啟動時機? 兩段式,第一段在 create_app() 之前(H main.py:112,DB/DI/app 皆不存在,故 stdlib-only)。第二段在 app_factory.py:352 接 DB 落點。排程由宿主掛,api/socketio 兩模式都跑
license-runtime 啟動時機? 無啟動閘門。blueprint 掛載 + 宿主中介層 + 每日 02:00 排程 tick
license-runtime 是否 raw SQL 打 iam 的 view? 是。infra/tenant_admin_notify_query.py:19-37 JOIN public.v_user_capabilities + capabilities + users + user_roles + roles,並硬編 c.name = 'tenant.update'。另有 license_tenant_resolver.py 四條 SQL 打 public.tenants 且繞 RLS
bulletin 對身分名冊/通知的依賴形式? 身分名冊:套件端零依賴;補 nickname 在宿主 H infra/bulletin/models/bulletin.py:47-100 用 ORM relationship 直接 join jedi_iam.User(違反主專案自己的「不在 infra 層 JOIN」規範)。通知:INotifier port 宣告了但零使用,且宿主的公告 service grep `notif
device 與 info-system 合併後資料模型長怎樣? 欄位交集只有 name / description 加上 BaseModel/TenantMixin 給的那些。device 11 個業務欄位(os/ip/hostname/device_type/manufacture/status/purpose…)與 info-sys 12 個(4 個 OSCAL enum/authorization_boundary/system_owner/is_active…)零重疊。schema 不同(public vs compliance)、tenant_id nullable 不同(YES vs NO)、enum 有無不同(0 個 vs 4 個)、owner 模型不同(無 vs int 軟參照+IUserLookup port)
誰在 JOIN 它們? device:3 張 compliance 表以真 FK + jedi-survey 以 ORM relationship。info-system:DB 上零 FK(橋表 project_information_systems 已於 2026-05-23 DROP),只靠 inventory 表的 ref_id 軟參照
主專案哪裡同時用到兩支? 兩處,且都是姊妹函式並列形式:① H common/util/system_asset_snapshot.py 整支 197 行(三對姊妹函式 + 檔頭三個並列常數)② H app/module_frame/service/ssp_import_template_app_service.py:161-162 同一建構子並列注入兩個 domain service。更關鍵的是資料層已經合併了:oscal.ssp_inventory_items 與 compliance.module_frame_inventory_item_defaults 兩張表以 asset_type ∈ {hardware, information_system} 統一承載兩者(H scripts/sql/2026-06-04-fr032-inventory-asset-type.sql:19-38)

H. 三件與既有文件衝突、值得首腦注意的事實

  1. jedi-issue README 的「58% 死碼」對主專案不成立:README:15 說 GitLab/GitHub 整合「產品目前沒在用」,但 H app/feedback/service/issue_service.py 有 24 處呼叫,其中 :116 IssueProviderCode.GITLAB、:141 IssueProviderCode.GITHUB 明確走那兩條 path。FR-080 已為此開了掃描 arc(commit 2ffdbe7c),本盤點證實該質疑成立。

  2. device / info-system 兩支 README 都有同一個照抄錯誤:兩支都在「常見坑」寫「刪除是軟刪除(is_delete=1),資料列仍在」,但兩張表都沒有 is_delete 欄位(全包 grep 零命中 + DEV DB 欄位清單確認)。device 走實刪、info-system 走 is_active boolean(deactivate())。合併時若照 README 設計會設計錯。

  3. license-runtime 的 ITenantDirectory port 被自己套件內的 raw SQL 繞過:ports.py:52-56 宣告「租戶模型是宿主的,走 port」,但 infra/license_tenant_resolver.py 四條 raw SQL 直接讀 public.tenants 且繞 RLS。同一件事兩條路,且 raw SQL 那條把 jedi-iam 的 path 格式寫死進 license 套件。


§10

未查證事項(誠實列出)

  • 未跑任何套件的測試(唯讀盤點紀律)。README 宣稱的「契約測試 N 條全綠」「守衛測試焊死」我只讀了測試檔名與少量測試碼,未實跑驗證。
  • STG / POC 的 DB schema 未查(只查 DEV)。device/info-system 的 tenant_id nullable 落差在其他環境是否一致,未驗。
  • jedi-issue 的 GitLab/GitHub path 在 POC/STG 是否真的有流量未查(我只證明了主專案程式碼會走那條,沒證明實際被觸發過)。系統日誌 / feedback_issues.gitlab_issue_uid 有無非空值未查。
  • agent_tasks 79 筆全有 detection_tool_id 是 DEV 的樣本,未驗 POC。
  • bulletin 套件的 common/enum/code.py 死碼我只用 grep 確認套件內零使用,未確認是否有外部 consumer import 它。
  • 各套件 harness 未實跑——「能不能單獨起 process」的結論是從 route 數 + migration + harness 檔存在性推的,不是實測。
  • jedi-issue 的 issue_upload_file_mapping.file_uid FK 在 ORM 沒宣告但 DB 有這個不一致,我沒查是哪支 migration 建的(主專案 scripts/sql/ grep 未命中,可能是更早期的手動 DDL 或 create_all() 時代遺留)。