盤點日期:2026-09-09。證據路徑相對根:
~/Projects/Jedicogy/module/jedi-python-package/(以下簡稱 M)~/Projects/Billows/Audit-Manager/compliance-manager-be/(以下簡稱 H)192.168.50.188:25432 / guidant_ai_dev(唯讀 SELECT)Q1 它是什麼
把「一個檔案存到某個後端、記一列 metadata、之後能下載/轉 PDF 預覽/刪除」這件事包起來的插件。裝上 register() 就長出六支 HTTP 檔案端點;mount_api=False 則退化成純 library(jedi-issue 就這樣用)。
README 與程式碼一致,且是我讀過最誠實的一份:
M jedi-file-upload/jedi_file_upload/api/__init__.py:104-110 的 mount_routes() 確實掛六條,與 README 表格逐條吻合。plugin.py:97-98 的 FileUploadServices 兩個欄位都是 Optional[Callable[[], Any]] provider,套件端零解析。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 — 業務端用 serviceH infra/remote_agent/recon_query_adapter.py:12、H infra/flow_engine/mapper/job_evidence_mapper.py — 直接查 UploadFile ORM modelH di_containers/upload_file/upload_file_containers.py — DI wiring(b) 其他 jedi 套件(依賴方向:這些套件 → file-upload):
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.12M 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 執行期形狀
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>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)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(),寫兩邊。次糾纏:jedi-remote-agent(雙向繞了一圈,但都經 port / 宿主)
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 注入。看起來像但不是一件事:
UploadFile model 做一支唯讀 id→uid 批次查(M jedi-detection/.../detection_report_file_query.py:14),是 consumer 不是同一件事。integrity_tamper_events 表與 upload_files 零關係),file-upload 的 sha256 欄是使用者上傳檔的錨點。同一個演算法、完全不同的資料。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。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)。
對別人的表的參照:
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 不一致。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。issues.project 只存字串(issue.py:18),宿主傳 self.project_name。產品詞彙洩漏:無(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、LabelDomainServiceH infra/feedback/model/feedback_issue.py:7 — import Issues 建 relationshipH app/feedback/dto/feedback_issue_dto.py:5 — import IssueDTOH 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 執行期形狀
GET /api/1.0/issue/get_members,blueprint 名 issue(plugin.py:97)infra/gitlab.py、infra/github.py,經 python-gitlab / PyGithub SDK);經 jedi-file-upload 間接碰檔案系統/S3os.getenv 4 處,全在 GitLab/GitHub 檔內(infra/github.py 讀 GITHUB_PRIVATE_TOKEN;infra/gitlab.py 讀 GITLAB_URL / GITLAB_PRIVATE_TOKEN / GITLAB_API_VERSION)——這是全批八支唯一還在直讀 env 的套件Q6 通用性
(a) 能直接用——工單系統裝它反而是最貼題的 consumer。
(b) 寫死的產品知識:
plugin.py:97 DEFAULT_BLUEPRINT_NAME = "issue" — 可覆寫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 都被迫背這兩個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 的一半用例住在宿主」。
看起來像但不是一件事:
members 表 vs jedi-iam 的 users 表:members(infra/member/models/member.py)是套件自己的名冊,與 public.users 零 FK、零 join,是兩份平行的人員資料。issue_assignee_mapping.member_uid → members(uid)(DB 查證),不是指向 users。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):
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 的表)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這個 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 BaseBulletinH app/bulletin/dto/bulletin.py:4 — BulletinDTO as BaseBulletinDTOH 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 執行期形狀
plugin.py:182-184)。對外端點是宿主的 H api/bulletin/__init__.py:20-24 三條/healthzQ6 通用性
(a) 能直接用——它就是一張表的 CRUD,任何產品都能裝。
(b) 寫死的產品知識:
bulletins.category default "others"(infra/models/bulletin.py:41-43)——類別集合本身沒定義,是自由字串,不算寫死plugin.py:74 DEFAULT_BLUEPRINT_NAME = "jedi_bulletin"(避開主專案同名 blueprint,可覆寫)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)。所以「公告發給哪些部門」這個關聯的擁有者是宿主,不是兩支套件任一方。
看起來像但不是一件事:
INotifier port 讓人以為兩者該接起來,實際上零接線,且宿主也不寄公告通知。Q1 它是什麼
一台實體/虛擬機器的基本資料(名稱、IP、OS、製造商、用途)的 CRUD 與 picker 選單查詢。README:3 標「D11 已定:本套件歸資產盤點語境」。
README 與程式碼大致一致,但有一處明確錯誤:
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 照抄漏改的。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 匯入匯出的設備 pickerH app/associations/service/job_execution_device_mapping_service.py:5-6、H app/associations/dto/job_execution_device_mapping_dto.py:4H infra/flow_control/repository/{job_export_query,flow_control_job_repo_impl,job_import_lookup_query}.py — 直接 import Device model 做 joinH infra/associations/{model,repository,mapper}/job_execution_device_mapping*.pyH di_containers/device/device_containers.py:4-6H 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 執行期形狀
Q6 通用性
(a) 能直接用。 (b) 寫死的產品知識:
status 用 magic number 1正常/0異常(device.py:71-77),沒有 enum 類別plugin.py:78 DEFAULT_BLUEPRINT_NAME = "jedi_device"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——決策者已裁定合併,以下是支撐證據(也是反面證據):
支持「同一件事」的事實:
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 值。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。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 表。Flask>=3.0,<4 + SQLAlchemy==2.0.37 + jedi-common>=0.0.23,一字不差。ISettingsReader / INotifier 兩張零使用 port、空 blueprint、同樣的 EXTENSION_KEY 命名法)。反對「同一件事」的事實(合併時會撞到的):
devices 在 public,information_systems 在 compliance(M .../information_system.py:21-22 "schema": "compliance")。合併後兩張表要不要搬 schema 是 breaking change(devices 有三條 FK 指著)。tenant_id nullable 不同:devices YES / information_systems NO。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 是自由字串)。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 完全沒有。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 字串)軟參照。oscal.ssp_inventory_items / compliance.module_frame_inventory_item_defaults),而那張表對兩者都是軟參照(ref_id 存 uid 字串,無 FK)。看起來像但其實不是:
remote_agents 也是「一台機器」,但存的是 agent endpoint(base_url / capabilities / 心跳),與 devices 零 FK、零 join、零 import。agent_tasks 沒有 device_id 欄位。兩者在系統中從未被關聯過。Q1 它是什麼
「被列入稽核範圍的資訊系統」的基本資料 CRUD——名稱、縮寫、FIPS-199 三軸安全等級、系統狀態、部署模型、授權邊界、系統負責人。README:3 標「資產盤點的主體之一」,且是 FR-032 G 抽套件的先例。
README 與程式碼大致一致,兩處錯誤:
is_delete=1)」——表裡沒有 is_delete(全包 grep 零命中)。實際的停用機制是 is_active boolean,domain service 那支叫 deactivate()(M .../domain/service/information_system_domain_service.py:84)。與 device README 是同一個照抄漏改。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。)
對別人的表的參照:
information_system.py:131-135 有註解明文記錄「FR-032 G 移除對 jedi_auth.User 的 infra 層 ORM join」,改走 IUserLookup 批次 enrichsystem_owner → public.users.id、created_by → public.users.id(欄位 comment 自陳)反向:DB 上零 FK 指向 information_systems(DEV pg_constraint 查證)。
產品詞彙洩漏:有,而且是本批最重的一支。
information_system.py:23)common/enum/information_system_enum.py:4-28):fips-199-low / operational / under-major-modification / disposition — 這些是 NIST SP 800-18 / FedRAMP 的系統生命週期詞,不是通用 IT 資產詞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 的資料格式焊進「通用資產套件」。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-7H 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 執行期形狀
plugin.py:175-198)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(不重複),要點:
asset_type discriminator 統一兩者H common/util/system_asset_snapshot.py 三對姊妹函式並列處理兩者H app/module_frame/service/ssp_import_template_app_service.py:161-162 同一建構子並列注入兩個 domain servicename / 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,與「合併成資產套件」的方向不同。
看起來像但其實不是:
Q1 它是什麼
「裝在客戶機房裡的代理程式」的註冊、心跳、任務派送、身分驗證(mTLS + 內部 CA + 短效 JWT) 四件事。README:6-8 白話講得很準:「那台機器怎麼被系統認得、怎麼證明還活著、伺服器怎麼把工作交給它、雙方怎麼互相驗證身分;工作內容本身留給產品自己決定」。
README 與程式碼一致,且是本批最完整的一份(282 行,含端點表、port 表、常見坑對照表)。程式碼確認:
ROUTE_TABLE 第三欄顯式宣告」→ M jedi-remote-agent/jedi_remote_agent/api/remote_agent_route.py:216-233 確實是 (path, Resource, needs_admin_bool) 三元組,agent 側四支明確標 Falseplugin.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,確認軟參照。
產品詞彙洩漏:有,兩處
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 筆全有值。remote_agents.capabilities 欄位 comment 寫死「派掃描任務只給含 detection_scan 者」(remote_agent.py:44-47;migration:47 同樣的 COMMENT)。FILE_AGENT_*(common/code.py:26-38)——「file agent」是 FR-039 的歷史命名,現在 agent 已多能力,但字串凍結為對外契約不能改(檔頭:3-10 說明)。6082 / 6083(common/code.py:57-58),與主專案 common/enum/event_code.py 同值。capabilities 的實際值域(題目特別問的):
capabilities 是自由 JSONB list,套件只做 capability in (a.capabilities or []) 的成員判定(domain/remote_agent/service/remote_agent_domain_service.py:50,111)'["file_storage"]'(migration:37)SELECT DISTINCT capabilities FROM compliance.remote_agents → 只有一種值 ["file_storage", "detection_scan"]file_storage 一個,且兩者實務上總是成對出現。所謂「多能力」目前只是二選一的旗標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,三支方法各一行轉呼叫 DetectionOrchestrationServiceH 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 執行期形狀
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 自報)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)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 讓它等效於必要相依)。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看起來像但其實不是:
remote_agents 與 devices 零 FK、零 join、零 import,agent_tasks 也沒有 device 欄位。系統中從沒把「這台 agent 裝在哪台 device 上」關聯起來。device_fingerprint 是客戶機器的識別碼(防 VM 複製撞號),integrity 的 machine_fingerprint 是本機自己的(unlock token 綁定)。兩者零共用程式碼(remote-agent 的指紋是 agent 送上來的,integrity 的是自己算的)。Q1 它是什麼
落地版產品的防篡改偵測:開機時逐檔比對簽章 manifest,不符就拒啟+起一個只回 503 的鎖定殼+留 FS/DB 雙落點證據;運行中每 4h 隨機抽查;只有原廠簽的一次性 unlock token 能解。
README 與程式碼一致,且是本批唯一「README 主動聲明自己不是 REST 插件」的(README:193-201)。程式碼確認:
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)pyproject.toml:11-21 註解明說執法核心零 runtime 相依,唯一宣告的 jedi-common>=0.0.30 只給 app/ 與 infra/ 兩層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 存在之前。
H main.py:112,跑在 validate_required_env() 之後、create_app() 之前。此時 DB / DI / Flask app 全部不存在——這正是套件 stdlib-only 紅線的原因。竄改時此處不返回(起 lockdown 殼或 SystemExit)。H core/app_factory.py:350-352,必須在 container.wire() 之後(writer 被呼叫時要開 DB session)。H core/scheduler.py 掛抽查 job,用 date trigger 自排下一輪(interval 的 jitter 中心點仍可預測,README:151-163)。api / socketio 兩模式都跑——是唯一兩模式都掛的 job(main.py:166)。handle.checker.hotpath_verify("upload-license") 由宿主在低頻高價值操作入口呼叫。不是中介層——沒有 before_request / after_request hook。
Q5 執行期形狀
mount_api 參數只為簽名一致而保留)runtime_check.py:49,73 用 threading.Lock 保護熱路徑;抽查 job 的排程由宿主的 APScheduler 掛(套件只提供 run_spot_check() 與 next_spot_check_delay_seconds())lockdown.py:51,234-235 用 stdlib ThreadingHTTPServer 接管原 port 回 503(服務起不來時才由它接管)lc_report.py:30-31,69,78 best-effort 回報 License Center,單次嘗試不重試)、process 訊號(termination.py 對 gunicorn master 送 SIGQUIT / os._exit(1))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 說明「維運查詢與簽發端都認這個名字」)core / resources / thirdparty(README:245-252)是套件約定的分層語意,consumer 的 build 產線要照這個結構產 manifest — 算契約不算產品知識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_fingerprintintegrity_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 明說「不自創新名」)。
看起來像但其實不是:
sha256 欄是使用者上傳檔的錨點(給 remote-agent 對帳用)。零共用程式碼、零資料交集。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 — 兩處:
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 解析)。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 執行期形狀
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)不需要分散式鎖app/service/tenant_license_admin_service.py:244,283 請照/展延/查方案;app/service/license_verification_service.py:355 線上開通)、檔案系統讀 /etc/machine-id(指紋)、寄信經 INotifier port(套件不直接碰 SMTP)--migrate / --issue 兩模式)啟動時機(題目特別問的):沒有啟動閘門。與 integrity 不同——license 是「app 起來之後掛 blueprint + 中介層 + 每日排程」,不會阻止服務啟動。三個生效點:① blueprint 掛載(create_app() 內)② 每個 request 過中介層 license_readonly_mw(宿主的,不是套件的)③ 每日 02:00 排程 tick。
Q6 通用性
(a) 不能直接用。 三個阻斷點:
public.tenants(含 path 格式)、public.users / user_roles / roles / capabilities / v_user_capabilitiesSET LOCAL app.is_super_admin 這個 RLS 慣例(license_tenant_resolver.py:148)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 的硬耦合)
LicenseTenantResolver.resolve() 每次都退回 fail-safe 分支(:69-72 查無 path 就回自己),TenantAdminNotifyQuery 直接 SQL 錯誤ITenantDirectory port 專門處理「租戶模型是宿主的」,卻被同一個套件內的 raw SQL 繞過次糾纏:jedi-integrity(雙向互開 port,見 integrity Q8,不重複)
integrity_hook port 接 integrity 的 hotpath_verifyH common/integrity/adapters.py:64-70)看起來像但其實不是:
H ../license_center/):名字幾乎一樣、講同一套授權體系,但刻意分離的獨立系統獨立 DB(BE CLAUDE.md 跨 repo 表已載明:私鑰、客戶名單、訂單、抽佣不進主產品)。兩者只透過「簽好的 License 檔」與幾支 HTTP API 交互,零 DB 共用。這是「看起來該合併但絕不能合併」的典型。INotifier port,但那是窄到只有一支 notify() 的投遞 port,不是通知領域本身。方向讀法:列 → 欄(列依賴欄)。
| ↓依賴者 \ 被依賴→ | 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」或「無關係」。
| 邊 | 形式 | 證據 |
|---|---|---|
| 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 零命中 |
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。
| 套件 | 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」。
to_oscal_dict() / from_oscal_dict() + 表 comment 寫「稽核範圍」agent_tasks.detection_tool_id NOT NULL(硬阻斷非檢測產品)+ capabilities comment 寫死 detection_scan + port 方法名 on_scan_succeeded/failedSET LOCAL app.is_super_admin RLS 慣例 + 硬編 tenant.update capability 名cm_app GRANT 假設,接近零洩漏file_access scope 名已 config 化;LibreOffice 是多餘職責不是產品洩漏)| 類別 | 套件 | 理由 |
|---|---|---|
| 真能獨立服務化 | 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 呼叫 |
| 問題 | 答案 |
|---|---|
| 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) |
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),本盤點證實該質疑成立。
device / info-system 兩支 README 都有同一個照抄錯誤:兩支都在「常見坑」寫「刪除是軟刪除(is_delete=1),資料列仍在」,但兩張表都沒有 is_delete 欄位(全包 grep 零命中 + DEV DB 欄位清單確認)。device 走實刪、info-system 走 is_active boolean(deactivate())。合併時若照 README 設計會設計錯。
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 套件。
feedback_issues.gitlab_issue_uid 有無非空值未查。agent_tasks 79 筆全有 detection_tool_id 是 DEV 的樣本,未驗 POC。common/enum/code.py 死碼我只用 grep 確認套件內零使用,未確認是否有外部 consumer import 它。issue_upload_file_mapping.file_uid FK 在 ORM 沒宣告但 DB 有這個不一致,我沒查是哪支 migration 建的(主專案 scripts/sql/ grep 未命中,可能是更早期的手動 DDL 或 create_all() 時代遺留)。