---
title: "主系統與 Agent 的通訊：指令直推、狀態帶版本、身分自動滾動 — 需求討論稿 (FR-171)"
brand: "Guidant AI · **FR-171** 主系統與 Agent 通訊重整"
eyebrow: "FR-171 · 需求討論稿 v1 · 2026-10-31（現況地圖＋目標設計＋D1～D12 待裁）"
h1: "指令不再等心跳，身分不再靠人換"
subtitle: "主系統要 Agent 做事就直接打過去；心跳只同步有變的設定；憑證到期自己換、金鑰可以輪；兩個方向同一套規則"
lede: "主系統現在要 Agent 做事（派掃描、重啟、升級）都把指令塞進心跳回應，Agent 每 60 秒來拿一次，平均等 30 秒。心跳回應已經堆到 24 個鍵、每跳全量重送 8 KB。另一邊，Agent 的憑證與通行證有效 10 年，到期要人去主機打指令換，而客戶沒有 IT。本稿先把**現在四條通訊路、每條怎麼證明身分、憑證從哪來到哪去**畫成一張地圖，對照業界做法找出哪些是半套，再提出**統一後的設計**：一條指令通道、一條同步通道、一套簽章規則、憑證自動滾動，六個月後除錯只要看兩支檔。"
chips: [
  {text: "決策者方向：等心跳太偷懶，要改雙向", kind: accent},
  {text: "D1～D12 待裁", kind: warn},
  {text: "現況骨架正確，五處半套", kind: warn},
  {text: "WebSocket 不做（FR-170 已裁，本稿不翻）", kind: plain},
  {text: "跨 4 repo：BE／jedi-python-package／evidence-agent／FE", kind: accent},
  {text: "陷阱：私鑰永遠不離開 Agent", kind: crit},
]
footer: "FR-171 需求討論稿 v1 · 首腦撰寫 · 2026-10-31 · 前作 FR-170（Agent 架構重整，P4 分析為本稿起點）"
---

## 0. 分工概述（30 秒版） {#overview nav="概述"}

**整案一句話**：主系統與 Agent 之間改成「指令直推、狀態帶版本號、憑證自動換」，而且兩個方向用同一套簽章規則，不再有四份長得像的 client 與三種半套的守門。

| 主題 | 做什麼 | 產出 | 完成怎麼判定（決策者檢查法） |
|---|---|---|---|
| A 指令通道 | 派工、重啟、升級、換證四種指令改成主系統直接打 Agent 8443；打不通退回心跳 | Agent 新端點、主系統統一的 client、契約 0.2.0 | 在主系統按「派工」，Agent 主控台 3 秒內看到任務開始（現在要等最多 60 秒）；把 Agent 網路線拔掉再派，插回去 60 秒內照樣開始 |
| B 設定同步瘦身 | 心跳請求帶各類設定的版本號，主系統只回有變的 | 心跳契約加 `config_versions` | 看 BE log 一跳的回應大小從 3 KB 降到 300 B 以內；改一條授權清冊，下一跳 Agent 就拿到 |
| C 憑證自動換 | Agent 到期前 90 天自己換證、換通行證；主系統可按鈕叫它立刻換 | Agent 換證元件、主系統換證端點 | 把 DEV 的憑證效期設成 3 天，等兩天看 Agent 管理頁「憑證到期日」自己往後推，過程零人工 |
| D 信任錨可輪換 | 主系統的 CA 與簽章公鑰改成清單下發，新舊並存 | 信任錨清單走 B 通道下發 | 主系統按「加一把新鑰」，等所有 Agent 心跳確認收到，再按「切換」，所有直推照常 |
| E 心跳持有證明 | Agent 打主系統時除了通行證，再帶一張用自己私鑰簽的 60 秒簽章 | 主系統驗簽中介層、Agent 簽章元件 | 把 Agent 的通行證檔複製到另一台機器打心跳，主系統回 401 並記稽核 |
| F 守門與 client 收斂 | 主系統四份 client 收成一份；Agent 守門一條規則、全部前綴綁用途、加防重放 | jedi-remote-agent 的 `AgentDataPlaneClient`、Agent `data_plane_auth` 改版 | 全 repo grep `jwt_util.mint` 只剩一處；同一張 JWT 重送第二次 Agent 回 401 |

**依賴一句**：A 與 B 是通道，C、D、E 是通道上的第一批使用者；F 是把通道做乾淨的前提。拆卡時 F 先做（它是地基），A 與 B 同時開，C、D、E 接在後面。**決策者功課**：裁 D1～D12，其中 D2（範圍）決定本案是 3 張卡還是 15 張。

---

## 1. 為什麼要做 {#why nav="為什麼"}

> 之前都是只能等心跳由 agent 發訊息給本體，後來才有了臨時 token 的模式讓本體可以主動跟 agent 溝通，所以現在應該算是可以雙向溝通？我想要把等待心跳的功能都改掉，除了原本的心跳功能，因為我覺得這太偷懶了而且越塞越多，不可能都只依靠心跳來處理事情。
> [— 決策者原話（2026-10-31）]{.attr}

> 反正都要重構了，我希望是好的設計，而不是像之前一樣都沒想過硬塞。mTLS 10 年過了要怎麼更新？你要想像我的客戶是沒有 IT 人員的一般人，叫他打指令根本是要他的命。目前看來很多機制沒錯，但都做一半或是沒有統一，這不對。要嘛就是機制統一幾個，不然兩個月後沒人記得，除錯要花兩三天。
> [— 決策者原話（2026-10-31）]{.attr}

兩段話對應兩個問題：

::: grid2
::: {.card .warn}
#### 指令走心跳：慢，而且越塞越多
主系統要 Agent 做事，只能把指令放進心跳回應等它來拿。心跳回應現在有 24 個鍵、Agent 端 10 個 observer 依序讀；每跳請求 5 KB、回應 3 KB，沒變也送。FR-167 為了讓升級快一點，做了一支「叫 Agent 立刻來心跳」的端點，等於承認心跳不夠用卻又多疊一層。
:::
::: {.card .crit}
#### 身分材料十年到期，靠人換
Agent 憑證、主系統 client 憑證、控制面通行證全部 10 年。到期前 60 天主系統發通知，然後要人到 Agent 主機上跑 `re-enroll`。客戶沒有 IT；原廠要飛去現場。主系統自己的 CA 與簽章私鑰則連「換」的機制都沒有，外洩就是所有 Agent 重新註冊。
:::
:::

---

## 2. 現況地圖：四條路、三個入口 {#map nav="現況地圖"}

這一節只描述現在是什麼，不評價。全部是從程式碼讀出來的（主系統 `jedi-remote-agent` 1.11.0、`jedi-agent-protocol` 0.1.0、Agent 主線 `3ab1253`）。

```{.mermaid cap="圖 1 — 主系統與 Agent 之間的四條路。A 只發生一次，是其他三條的信任起點。"}
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E2F0F1','primaryTextColor':'#14201F','primaryBorderColor':'#0E7C86','secondaryColor':'#EEF2F3','secondaryTextColor':'#14201F','tertiaryColor':'#FBFCFC','tertiaryTextColor':'#14201F','lineColor':'#4A5A5C','textColor':'#14201F','mainBkg':'#E2F0F1','nodeBorder':'#0E7C86','nodeTextColor':'#14201F','edgeLabelBackground':'#FBFCFC','titleColor':'#14201F','clusterBkg':'#FBFCFC','clusterBorder':'#E4EAEB','actorBkg':'#E2F0F1','actorTextColor':'#14201F','actorBorder':'#0E7C86','actorLineColor':'#C7D1D2','signalColor':'#4A5A5C','signalTextColor':'#14201F','labelBoxBkgColor':'#EEF2F3','labelBoxBorderColor':'#C7D1D2','labelTextColor':'#14201F','loopTextColor':'#14201F','noteBkgColor':'#F6EBD5','noteTextColor':'#14201F','noteBorderColor':'#9C6B12','activationBkgColor':'#EEF2F3','activationBorderColor':'#C7D1D2','sequenceNumberColor':'#FFFFFF'}}}%%
flowchart LR
  subgraph cloud["主系統（雲端或落地版）"]
    direction TB
    ck["持有：內部 CA 私鑰<br/>JWT 簽章私鑰（一把簽三種票）<br/>主系統 client 憑證<br/>各 Agent 的憑證與狀態"]
  end
  subgraph agent["Agent（客戶機器）"]
    direction TB
    p8443["8443 資料面<br/>mTLS，只給主系統"]
    p8444["8444 主控台＋直傳<br/>給人用，CIDR 白名單"]
    ak["持有：自己的私鑰＋憑證<br/>CA 憑證、JWT 公鑰<br/>控制面通行證"]
  end
  browser["使用者瀏覽器"]
  ak -- "A 註冊（一次）<br/>一次性 token＋CSR" --> ck
  ak -- "B 心跳／回報／下載<br/>每 60 秒，bearer 通行證" --> ck
  ck -- "C 直推<br/>mTLS＋60 秒 JWT" --> p8443
  ck -- "D 簽 60 秒一次性票" --> browser
  browser -- "帶票連" --> p8444
```

| 路 | 方向 | 什麼時候 | 身分怎麼證明 | 現在走這條的事 |
|---|---|---|---|---|
| **A 註冊** | Agent → 主系統 | 第一次啟動、被主系統刪掉後、通行證失效（401） | 一次性註冊 token ＋ CSR；重註冊另帶舊私鑰簽章 | 只有註冊 |
| **B 心跳** | Agent → 主系統 | 每 60 秒＋有事就打 | 長效通行證（bearer JWT，10 年） | 心跳、派工 ack／result、outbox 補送、下載 profile／漏洞庫／升級包／任務檔、本機掃描回報、工具 schema |
| **C 直推** | 主系統 → Agent | 使用者在主系統按了什麼 | mTLS ＋ 每請求現簽的 60 秒 JWT | 取消掃描、測試連線、設定／清除工具帳密、取證據檔、叫醒心跳、拉升級進度 |
| **D 瀏覽器票** | 瀏覽器 → Agent | 使用者開 Agent 主控台、直傳證據檔 | 主系統簽的 60 秒一次性票，換成 cookie session | 主控台登入、檔案直傳 |

### 2.1 路 A：註冊，信任的起點 {#flow-a}

```{.mermaid cap="圖 2 — 註冊流程。第 2 步的私鑰從此不離開 Agent；第 12 步的人工核准是擋「拿到 token 就能報到」的那道門。"}
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E2F0F1','primaryTextColor':'#14201F','primaryBorderColor':'#0E7C86','secondaryColor':'#EEF2F3','secondaryTextColor':'#14201F','tertiaryColor':'#FBFCFC','tertiaryTextColor':'#14201F','lineColor':'#4A5A5C','textColor':'#14201F','mainBkg':'#E2F0F1','nodeBorder':'#0E7C86','nodeTextColor':'#14201F','edgeLabelBackground':'#FBFCFC','titleColor':'#14201F','clusterBkg':'#FBFCFC','clusterBorder':'#E4EAEB','actorBkg':'#E2F0F1','actorTextColor':'#14201F','actorBorder':'#0E7C86','actorLineColor':'#C7D1D2','signalColor':'#4A5A5C','signalTextColor':'#14201F','labelBoxBkgColor':'#EEF2F3','labelBoxBorderColor':'#C7D1D2','labelTextColor':'#14201F','loopTextColor':'#14201F','noteBkgColor':'#F6EBD5','noteTextColor':'#14201F','noteBorderColor':'#9C6B12','activationBkgColor':'#EEF2F3','activationBorderColor':'#C7D1D2','sequenceNumberColor':'#FFFFFF'}}}%%
sequenceDiagram
  autonumber
  participant A as Agent
  participant M as 主系統
  participant U as 管理員
  Note over A: 算本機指紋 device_uuid（主機板序號＋machine-id）
  Note over A: 自己產金鑰對，私鑰落地、永不外送
  Note over A: 產 CSR，名稱＝device_uuid
  A->>M: POST /agents/register｛token, device_uuid, csr, base_url, 硬體資訊｝<br/>https，Agent 用安裝時確認過的指紋釘住主系統憑證
  Note over M: token 的 sha256 查 DB 找租戶（只認 token 查出的，body 說什麼都不信）
  Note over M: 驗 CSR 名稱＝device_uuid；指紋撞號就拒
  Note over M: 重註冊帶舊私鑰簽章 → 驗過就接管原列、保留 uid
  Note over M: 內部 CA 簽 Agent 憑證（10 年）
  Note over M: 簽控制面通行證（10 年、typ=agent-control）
  M-->>A: ｛agent_uid, 憑證, CA 憑證, JWT 公鑰, 通行證, 心跳間隔｝
  Note over A: 五樣落地 cert_dir；用自己憑證起 8443
  U->>M: 在 Agent 管理頁按「核准」
  Note over M: 核准前心跳回 403，什麼都不能做
```

要點：

- 註冊 token **一租戶共用、永久有效**（撤銷才失效），靠第 12 步人工核准擋住「拿到 token 就能報到」。
- 重註冊靠**舊私鑰簽新 CSR** 證明「我是原本那台」（`prior_key_proof`）。主系統驗這個簽章時**不看舊證有沒有過期**，只驗 CA 簽發、名稱、簽章。這是自動換證缺的那半塊，而且過期寬限在程式層已經成立。
- Agent 對主系統 https 憑證的信任是**安裝時釘指紋**，不靠系統 CA。換主系統主機要跑 `re-enroll` 重釘。

### 2.2 路 B：Agent → 主系統（心跳與所有回報） {#flow-b}

```{.mermaid cap="圖 3 — 心跳。身分只靠通行證；撤銷靠每次請求查 DB，所以通行證長效是刻意的。"}
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E2F0F1','primaryTextColor':'#14201F','primaryBorderColor':'#0E7C86','secondaryColor':'#EEF2F3','secondaryTextColor':'#14201F','tertiaryColor':'#FBFCFC','tertiaryTextColor':'#14201F','lineColor':'#4A5A5C','textColor':'#14201F','mainBkg':'#E2F0F1','nodeBorder':'#0E7C86','nodeTextColor':'#14201F','edgeLabelBackground':'#FBFCFC','titleColor':'#14201F','clusterBkg':'#FBFCFC','clusterBorder':'#E4EAEB','actorBkg':'#E2F0F1','actorTextColor':'#14201F','actorBorder':'#0E7C86','actorLineColor':'#C7D1D2','signalColor':'#4A5A5C','signalTextColor':'#14201F','labelBoxBkgColor':'#EEF2F3','labelBoxBorderColor':'#C7D1D2','labelTextColor':'#14201F','loopTextColor':'#14201F','noteBkgColor':'#F6EBD5','noteTextColor':'#14201F','noteBorderColor':'#9C6B12','activationBkgColor':'#EEF2F3','activationBorderColor':'#C7D1D2','sequenceNumberColor':'#FFFFFF'}}}%%
sequenceDiagram
  autonumber
  participant A as Agent
  participant M as 主系統
  loop 每 60 秒，或被 /update/notify 叫醒
    A->>M: POST /agents/heartbeat<br/>Authorization: Bearer 通行證<br/>body 44 欄約 5 KB：狀態、版本、能力、清冊版本、profile 狀態、升級結果、主控台事件…
    Note over M: 驗通行證簽章＋typ；查 remote_agents 狀態（撤銷→403）
    Note over M: 更新 last_seen、自報狀態入庫
    Note over M: 組回應 24 鍵約 3 KB：派工單、重啟旗標、升級指示、清冊、CIDR、操作者名單、設定…
    M-->>A: 回應
    Note over A: 10 個 observer 依序讀各自的鍵
    Note over A: 有派工單 → 丟給 scan 模組執行
  end
  Note over A,M: 同一條通道還走：ack、result、outbox 補送、下載 profile／漏洞庫／升級包／任務檔
```

心跳回應 24 鍵分三類（FR-170 第 4 棒分析）：

| 類 | 鍵 | 性質 | 現況問題 |
|---|---|---|---|
| **指令類** | `pending_tasks`、`restart`、`update` | 要 Agent 立刻做一件事 | 等 60 秒是純延遲 |
| **設定同步類** | `allowed_targets`、`console_allow_cidr`、`offline_operators`、`tool_schemas_version`、`profiles`、`vulndb`、`ldap_config`、`error_tracking`、`file_protect`、`offline_login_days`、`allowed_target_names`、`agent_name` | 要的是最終一致 | 每跳全量重送；只有 `allowed_targets` 與 `tool_schemas` 有版本比對、`profiles` 靠 sha256 逐筆比，其餘沒有 |
| **對帳類** | `accepted`／`rejected`、`console_events_ack`、`server_time_utc` | 回執 | 不動 |

取消掃描**已經是直推**（jedi_detection 的 `agent_cancel_client` 打 `/detection/cancel`，成功才標 cancelled），不在心跳裡。

### 2.3 路 C：主系統 → Agent（直推 8443） {#flow-c}

```{.mermaid cap="圖 4 — 直推。四道門由外到內；第 ④ 道目前只對 /update 前綴強制。"}
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E2F0F1','primaryTextColor':'#14201F','primaryBorderColor':'#0E7C86','secondaryColor':'#EEF2F3','secondaryTextColor':'#14201F','tertiaryColor':'#FBFCFC','tertiaryTextColor':'#14201F','lineColor':'#4A5A5C','textColor':'#14201F','mainBkg':'#E2F0F1','nodeBorder':'#0E7C86','nodeTextColor':'#14201F','edgeLabelBackground':'#FBFCFC','titleColor':'#14201F','clusterBkg':'#FBFCFC','clusterBorder':'#E4EAEB','actorBkg':'#E2F0F1','actorTextColor':'#14201F','actorBorder':'#0E7C86','actorLineColor':'#C7D1D2','signalColor':'#4A5A5C','signalTextColor':'#14201F','labelBoxBkgColor':'#EEF2F3','labelBoxBorderColor':'#C7D1D2','labelTextColor':'#14201F','loopTextColor':'#14201F','noteBkgColor':'#F6EBD5','noteTextColor':'#14201F','noteBorderColor':'#9C6B12','activationBkgColor':'#EEF2F3','activationBorderColor':'#C7D1D2','sequenceNumberColor':'#FFFFFF'}}}%%
sequenceDiagram
  autonumber
  participant M as 主系統
  participant A as Agent 8443
  Note over M: 從 DB 取 agent（base_url、uid、指紋）
  Note over M: 用 JWT 私鑰簽 60 秒 token：aud=agent_uid、op="POST /detection/cancel"、bound_fp=指紋、tenant_id
  M->>A: TLS 握手，出示 client 憑證（內部 CA 簽）
  Note over A: ① TLS 層 CERT_REQUIRED：沒 CA 簽的憑證在 TCP 層就拒，碰不到 Flask
  M->>A: HTTPS 請求 + Authorization: Bearer 60 秒 JWT
  Note over A: ② 驗簽章（註冊時拿的公鑰）、iss、aud、exp
  Note over A: ③ bound_fp ≠ 本機指紋 → 401（憑證被搬到別台）
  Note over A: ④ /update 前綴才驗：op 要等於「方法 路徑」
  A-->>M: 回應 + X-Agent-Fingerprint
  Note over M: ⑤ 回應指紋≠DB 存的 → 記稽核、當失敗
```

目前只開 9 個端點：`/blob*`、`/detection/probe`、`/detection/credentials`、`/detection/cancel`、`/scan-input`、`/update/notify`、`/update/progress`、`/health`、`/agent-info`。受保護前綴只有一份清單 `_PROTECTED_PREFIXES = ("/blob", "/detection", "/update")`，新端點掛在底下自動受守。

### 2.4 路 D：瀏覽器 → Agent 8444（給人用） {#flow-d}

使用者在主系統按「開 Agent 主控台」，主系統驗權限後簽一張 60 秒一次性票（`iss=guidant-cloud`、`aud=agent:<uid>`、`sub=使用者`、`scope=哪些操作`、`jti`），瀏覽器帶票連 Agent 8444。Agent 先過來源 IP 的 CIDR 白名單，再驗簽章、purpose、scope、jti 沒用過，換成 cookie session（只在記憶體，Agent 重啟全部失效）。直傳證據檔走同樣形狀，purpose 不同。**這條已經有 jti 防重放**（`TicketReplayGuard`），路 C 沒有。

### 2.5 憑證與金鑰清單 {#keys}

| 材料 | 誰產生 | 誰持有 | 效期 | 撤銷／換新 |
|---|---|---|---|---|
| 內部 CA 私鑰 | installer 用 openssl | 主系統 | 10 年 | **無機制** |
| JWT 簽章私鑰（一把簽路 C、通行證、路 D 三種票） | installer | 主系統 | 無到期 | **無機制** |
| 主系統 client 憑證（打 8443 用） | installer 用同一個 CA 簽 | 主系統 | 10 年 | **無機制** |
| Agent 私鑰 | Agent 自己 | 只在 Agent | 隨憑證 | 重註冊時換新 |
| Agent 憑證 | 主系統 CA 簽 | Agent（8443 伺服器證） | 10 年 | 到期前 60 天通知；**人工** `re-enroll` |
| 控制面通行證 | 主系統 JWT 私鑰 | Agent | 10 年 | 每請求查 DB 狀態 |
| 資料面 JWT（路 C） | 主系統 JWT 私鑰 | 每請求現簽 | 60 秒 | 不再簽即失效 |
| 瀏覽器票（路 D） | 主系統 JWT 私鑰 | 瀏覽器轉交 | 60 秒、一次性 | jti 燒掉 |
| 註冊 token | 管理員在畫面產 | DB 存 sha256 | 永久 | 畫面停用 |
| 主系統 https 憑證指紋 | 安裝 Agent 時人工確認 | Agent | 隨主系統憑證 | `re-enroll` 重釘 |

---

## 3. 業界對照：這套形狀對不對 {#industry nav="業界對照"}

結論先講：**形狀是「Kubernetes 節點模型」，對同內網的落地版是正確選擇，不用換模型。** 要做的是把形狀內的半套補成整套。

| 我們的路 | 業界對應 | 判定 |
|---|---|---|
| A 註冊：bootstrap token → 節點自產金鑰送 CSR → 控制面簽 → 人工核准 | Kubernetes kubelet TLS bootstrapping、RFC 7030（EST）、AWS IoT fleet provisioning，骨架一模一樣 | **正規**。差異只在 K8s 的 token 預設 24 小時用完即棄；我們永久共用靠人工核准補。對沒有 IT 的客戶，永久 token 加核准反而務實，但 token 應可設到期（D9） |
| B 心跳拉指令 | Chef／Puppet／Salt 的 agent 都定期 pull；Datadog remote config 也輪詢 | **常見但業界正在遠離**。問題不在 pull，在「所有指令只能等下一輪」。主流已改成 pull 保底、另有即時通道 |
| B 身分用長效 bearer | RFC 6750 允許，是最低標 | **合格但弱**。裝置身分主流是 mTLS（kubelet、AWS IoT、Azure IoT Hub X.509）或持有證明（RFC 9449 DPoP） |
| C 直推：mTLS＋短效 JWT 綁 aud／op／指紋 | Kubernetes API server 打 kubelet 10250 就是 mTLS；綁 op 與指紋接近 RFC 9449 的 htm／htu 精神 | **正規，比多數產品嚴**。缺 jti 防重放、op 綁定只套一個前綴 |
| D 瀏覽器一次性票 | S3 presigned URL、Teleport 一次性 session token | **正規** |
| 憑證 10 年、人工換 | 公用 TLS 上限 398 天；內部 PKI 無法定上限，NIST 800-57 的原則是「效期由輪換能力決定」；kubelet 預設一年且自動換 | **有自動換證就該縮短；沒有自動換證才被迫 10 年**。10 年不是設計，是把問題推給十年後 |
| CA 10 年無輪換 | 私有根 CA 普遍 10～20 年；NIST 要求「為補救外洩而能換」，不是排程換 | 長效本身**合規**；**缺的是「能換」** |
| 三種票共用一把私鑰、靠 typ／iss 區分 | RFC 8725 §3.11 明寫防跨用途混淆的正規做法就是明確 typ＋驗 iss＋驗 aud | **現況合規**。分鑰是縱深防禦不是修 bug |

### 3.1 其他做法，以及為什麼不換 {#alternatives}

| 做法 | 代表產品 | 優點 | 代價 | 判斷 |
|---|---|---|---|---|
| 純出站長連線，主系統透過連線反推 | Tailscale、Teleport、Cloudflare Tunnel | 客戶不開任何入站埠 | 認證只在握手、要自建保活與重連、穿 proxy 常斷、BE 要多一個連線管理元件 | SaaS 版才需要；落地版同內網不值得。FR-170 §3.x 已裁，本稿不翻 |
| Message broker（MQTT） | AWS IoT Core、Azure IoT Hub | 直推與離線暫存天生就有 | 客戶機上多一個要維運的行程、身分體系要另外接 | 沒有 IT 的客戶多一個元件就是多一個會壞的東西，不建議 |
| 工作負載身分框架 | SPIFFE／SPIRE | 短效憑證自動輪換是核心功能 | 整套框架很重，有自己的 server 與 agent | 不引入，但「短效＋自動換」的設計照它抄 |
| 內部 ACME | step-ca、smallstep | 換證協定標準化 | 又多一個 CA 服務 | 不引入，用現有註冊路徑做換證就夠 |
| gRPC 雙向串流 | 很多自家 agent | 一條連線雙向、契約強 | 與長連線同樣問題，且要換掉 Flask 那一面 | 不建議 |

---

## 4. 現況的洞：哪些是半套 {#gaps nav="現況的洞"}

每一條附規範依據，不是我覺得。

| # | 洞 | 現況 | 後果 | 依據 |
|---|---|---|---|---|
| G1 | 指令塞心跳 | 派工、重啟、升級走心跳回應 | 最慢等 60 秒；心跳回應越堆越多 | 業界主流 pull 保底＋即時通道 |
| G2 | 憑證無自動換證 | 10 年、60 天前通知、人工 `re-enroll` | 客戶沒 IT 做不到；原廠要出差 | RFC 7030 re-enroll、kubelet 自動 rotate |
| G3 | CA 與簽章金鑰不能換 | Agent 只存一張 CA、一把公鑰 | 外洩就是所有 Agent 重新註冊 | NIST 800-57 要求有補救外洩的能力；RFC 7517 JWKS 多鑰並存 |
| G4 | 直推無防重放 | 路 C 的 JWT 沒 jti，Agent 不記已用過的 | 60 秒內同一張票重送，重啟會執行兩次。本案要做的正是指令，這條變必修 | RFC 9449 §11.1 要求 jti＋伺服器端短期快取 |
| G5 | op 綁定只套一個前綴 | 只有 `/update` 驗 op | 一張簽給 `GET /blob/x` 的票 60 秒內能打 `/detection/probe` | RFC 9449 htm／htu 是全部請求都綁 |
| G6 | 簽發前不查撤銷 | 四份 client 拿 DB 載入的 agent 就簽，沒看 status | 撤銷後 60 秒內主系統仍會對它下令 | 短效 token 的撤銷模型前提是「簽發點一定查狀態」 |
| G7 | 心跳只靠 bearer | 通行證被讀走就能冒充 10 年 | 備份外洩、技術人員拷走憑證目錄 | RFC 8705／RFC 9449 持有者約束 |
| G8 | 主系統有四份 client | `agent_credential_client`、`agent_update_notify_client`、jedi_detection 的 cancel 與 probe client、`remote_agent_adapter` 各自簽 JWT、各自建 mTLS context | 改一條規則要改四處；G6 就是這樣漏的 | 不是規範，是決策者說的「兩個月後沒人記得」 |
| G9 | 設定同步全量重送 | 12 類設定只有 2 類有版本比對 | 每跳 8 KB；設定類再加就再長 | 不是資安，是效率與可維護性 |

其中 G6 是**真正的洞**（現在就有風險）；G4、G5、G7 是**縱深不足**（有別的門擋著但少一層）；G2、G3 是**沒想過**（把問題推給未來）；G1、G8、G9 是**可維護性**。

---

## 5. 目標設計：統一後的全貌 {#target nav="目標設計"}

### 5.1 一句話版 {#target-oneline}

- **一個信任根**：註冊那一次。之後所有材料都從它自動滾動。
- **一條指令通道**（路 C 擴大）：主系統 → Agent 8443。派工、重啟、升級、換證、取消全走它。打不通退回心跳，單一真相在主系統 DB。
- **一條同步通道**（路 B 瘦身）：心跳帶版本號，有變才送。清冊、信任錨、操作者名單、所有設定走它。
- **一套簽章規則**：兩個方向都是「每個請求帶一張 60 秒、綁方法與路徑、綁 jti、用自己私鑰簽的 JWT，對方用註冊時拿到的公鑰驗」。只是簽的人換邊。
- **一份守門**：Agent 端一支 `data_plane_auth`，所有前綴同一條規則。主系統端一支 `AgentDataPlaneClient`，所有呼叫同一條路。
- **一個換鑰規則**：Agent 憑證一年自動換；CA 長效但可換；全程 Agent 零人工。

```{.mermaid cap="圖 5 — 目標狀態。路 A 不變；路 B 加持有證明並瘦身；路 C 擴成指令通道；路 D 不變。"}
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E2F0F1','primaryTextColor':'#14201F','primaryBorderColor':'#0E7C86','secondaryColor':'#EEF2F3','secondaryTextColor':'#14201F','tertiaryColor':'#FBFCFC','tertiaryTextColor':'#14201F','lineColor':'#4A5A5C','textColor':'#14201F','mainBkg':'#E2F0F1','nodeBorder':'#0E7C86','nodeTextColor':'#14201F','edgeLabelBackground':'#FBFCFC','titleColor':'#14201F','clusterBkg':'#FBFCFC','clusterBorder':'#E4EAEB','actorBkg':'#E2F0F1','actorTextColor':'#14201F','actorBorder':'#0E7C86','actorLineColor':'#C7D1D2','signalColor':'#4A5A5C','signalTextColor':'#14201F','labelBoxBkgColor':'#EEF2F3','labelBoxBorderColor':'#C7D1D2','labelTextColor':'#14201F','loopTextColor':'#14201F','noteBkgColor':'#F6EBD5','noteTextColor':'#14201F','noteBorderColor':'#9C6B12','activationBkgColor':'#EEF2F3','activationBorderColor':'#C7D1D2','sequenceNumberColor':'#FFFFFF'}}}%%
flowchart LR
  subgraph cloud["主系統"]
    direction TB
    client["AgentDataPlaneClient（唯一出口）<br/>簽發前查狀態、mTLS、60 秒 JWT 含 jti 與 op"]
    anchors["信任錨清單<br/>CA 與簽章公鑰多把並存，各帶 kid"]
    verify["心跳驗簽中介層<br/>驗通行證＋驗持有證明"]
  end
  subgraph agent["Agent"]
    direction TB
    guard["data_plane_auth（唯一守門）<br/>所有前綴：驗簽、aud、指紋、op、jti"]
    cmd["指令端點<br/>/tasks /restart /update /credentials/renew /detection/cancel"]
    renew["換證元件（BackgroundComponent）<br/>到期前 90 天自動換"]
    sign["每請求簽章元件<br/>用自己私鑰簽 60 秒 JWT"]
  end
  client -- "C 指令直推<br/>打不通退回心跳" --> guard --> cmd
  sign -- "B 心跳：通行證＋持有證明<br/>帶 config_versions，只回有變的" --> verify
  anchors -. "走 B 下發" .-> agent
  renew -- "走 A 的重註冊路徑<br/>舊私鑰簽新 CSR" --> cloud
```

### 5.2 主題 A：指令通道 {#topic-a}

四種指令各一個端點，全部掛在受保護前綴下、op 綁定、冪等：

| 指令 | 端點 | body | Agent 端做什麼 | 冪等鍵 | 退回心跳怎麼接 |
|---|---|---|---|---|---|
| 派工 | `POST /tasks` | 一張派工單（同心跳 `pending_tasks[]` 的元素） | 丟給 `scan.dispatch_tasks`，與心跳路徑同一個入口；立刻 ack | task uid；已領過的回 200 不重做 | 單子本來就在 DB 等心跳撿；直推成功 Agent ack（pending → dispatched），下一跳不會再拿到 |
| 重啟 | `POST /restart` | `{mode}` | 同 `restart_flag.observe` | 60 秒內同 jti 拒；重啟本身冪等 | 主系統仍寫 `restart_requested` 旗標再直推；打通 Agent 當下處理並由心跳回報已清 |
| 升級 | `POST /update` | 同心跳 `update` 的內容 | 同 `update_sync.observe` | target_version 相同不重下載 | 主系統仍走 `update_extra`；直推只是提前 |
| 換證 | `POST /credentials/renew` | 空 | 觸發主題 C 的換證流程 | 進行中就回 202 不重跑 | 不需要備援，Agent 自己會到期前換 |
| 取消 | `POST /detection/cancel` | 既有 | 既有 | 既有 | 既有（失敗 409） |

**主系統端統一寫法**：所有「要 Agent 做事」的呼叫都是「先把事實寫進 DB（單子、旗標），再呼叫 `AgentDataPlaneClient.command(agent, op, body)`；回 True 記『直推成功』，回 False 記『退回心跳』」。直推失敗**不重試、不排隊**，交給心跳。這是「至少一次投遞＋冪等消費者」的標準模式。

**相容**：舊 Agent 沒有新端點回 404，主系統當直推失敗，退回心跳；舊主系統不打新端點，新 Agent 照舊從心跳拿。兩邊不綁版。

**`/update/notify` 退役**：它的「叫 Agent 立刻來心跳」在有 `POST /update` 之後沒有存在理由。契約 0.2.0 標 deprecated，主系統改打 `POST /update`；Agent 端留一個版本（舊主系統還會打），下一個 minor 拔。

### 5.3 主題 B：設定同步帶版本號 {#topic-b}

**通則**（取代七類各寫一套）：

- 心跳請求加一個鍵 `config_versions: {類別: 版本字串}`，Agent 回報手上每一類的版本。
- 主系統對每一類算一個版本（內容的 sha256 前 16 碼，或資料表的 updated_at），**只有不同時才回該類內容**，相同時回 `{版本}` 或不帶鍵。
- Agent 沒帶 `config_versions`（舊 Agent）→ 主系統全量回，行為同現在。
- 主系統沒回某類（舊主系統）→ Agent 沿用快取，行為同現在。

`allowed_targets` 已經這樣做，本主題是把它的做法套到全部 12 類。信任錨清單（主題 D）是第 13 類，也是這個通道的第一個新用戶。

**歸類**：`dispatch_gate` 留在心跳的派工回應裡（它是主系統自己的升級閘，決定「現在要不要發單」，與 Agent 無關）；`upgrade_result` 是對帳類不動；`update` 改走指令通道後，心跳的 `update` 鍵保留當備援。

### 5.4 主題 C：憑證自動換 {#topic-c}

```{.mermaid cap="圖 6 — 自動換證。走現有註冊路徑的重註冊分支，只差「到期前自己觸發」。"}
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E2F0F1','primaryTextColor':'#14201F','primaryBorderColor':'#0E7C86','secondaryColor':'#EEF2F3','secondaryTextColor':'#14201F','tertiaryColor':'#FBFCFC','tertiaryTextColor':'#14201F','lineColor':'#4A5A5C','textColor':'#14201F','mainBkg':'#E2F0F1','nodeBorder':'#0E7C86','nodeTextColor':'#14201F','edgeLabelBackground':'#FBFCFC','titleColor':'#14201F','clusterBkg':'#FBFCFC','clusterBorder':'#E4EAEB','actorBkg':'#E2F0F1','actorTextColor':'#14201F','actorBorder':'#0E7C86','actorLineColor':'#C7D1D2','signalColor':'#4A5A5C','signalTextColor':'#14201F','labelBoxBkgColor':'#EEF2F3','labelBoxBorderColor':'#C7D1D2','labelTextColor':'#14201F','loopTextColor':'#14201F','noteBkgColor':'#F6EBD5','noteTextColor':'#14201F','noteBorderColor':'#9C6B12','activationBkgColor':'#EEF2F3','activationBorderColor':'#C7D1D2','sequenceNumberColor':'#FFFFFF'}}}%%
sequenceDiagram
  autonumber
  participant A as Agent 換證元件
  participant M as 主系統
  Note over A: 每天看一次憑證到期日；剩 90 天開始，或收到 POST /credentials/renew
  Note over A: 產新金鑰對＋新 CSR（名稱＝device_uuid）
  Note over A: 用舊私鑰簽新 CSR（prior_key_proof）
  A->>M: POST /agents/register｛csr, 舊憑證, 舊私鑰簽章, agent_uid｝<br/>Authorization: Bearer 通行證＋持有證明
  Note over M: 驗通行證（查狀態，撤銷不簽）
  Note over M: 驗舊私鑰簽章（不看舊證是否過期，現況已如此）
  Note over M: 簽新憑證（1 年）、簽新通行證（1 年）；保留 uid
  M-->>A: ｛新憑證, 新通行證, 信任錨清單｝
  Note over A: 新私鑰、新證、新通行證落地；8443 熱換不重啟
  Note over A: 失敗明天再試，連試到過期後 90 天
  A->>M: 下一跳心跳回報 cert_not_after 與換證結果
```

**三條底線**：

1. **私鑰永遠在 Agent 上產生、永遠不經過網路。** 主系統「發動換證」只能是叫 Agent 自己去換，不可以幫它產一對鑰推過去。
2. **換證要同時過兩個因子**：通行證（我是誰）＋舊私鑰簽章（我真的持有鑰）。少一個就退化成「拿到通行證就能換到新證」。
3. **主系統簽發前查狀態**：撤銷、停用的 Agent 不簽。這是整條換證線的真正保護，比「過期後能不能換」重要。

**數字**：憑證 1 年、剩 90 天開始換、過期後寬限 90 天仍可換。離線容忍度 9 個月加寬限。1 年效期的意義是憑證被偷最多活一年，現在是十年。**剩下唯一要人碰的情況**：整機壞掉或憑證目錄被清空，那本來就是重裝。

### 5.5 主題 D：信任錨清單 {#topic-d}

Agent 現在只存一張 CA、一把 JWT 公鑰。改成清單：

```
trust_anchors:
  version: "a3f9…"
  ca_certs:  [{kid, pem, not_after}, …]          ← 驗主系統 client 憑證
  jwt_keys:  [{kid, use: data_plane|ticket, pem, not_after}, …]   ← 驗直推 JWT 與瀏覽器票
```

- 走主題 B 的版本號通道下發；Agent 收到就更新本機。
- 每張 JWT 帶 `kid`，Agent 用 kid 挑公鑰驗。沒帶 kid（舊主系統）就用清單第一把。
- **換鑰流程**：主系統加新鑰 → 清單下發（舊鑰仍有效）→ 管理頁顯示「N 台已收到」→ 全部收到後按「切換」→ 新簽發改用新鑰 → Agent 下次換證拿到新 CA 簽的證 → 舊鑰到期停用。全程主系統按兩個鈕，Agent 零碰。
- **CA 本身不排程換**。換 CA 是整套系統最危險的操作，每三年強迫做一次是製造事故。長效維持，只做「能換」，在外洩或演算法淘汰時用。
- 順手把三種票分鑰（資料面、通行證、瀏覽器票各一把），三把公鑰都進清單。這是縱深不是修 bug，但既然要做清單，分鑰便宜。

**仍需人的唯一情況**：主系統私鑰被偷（不是到期）。那時舊鑰不能信，等於全部 Agent 重新註冊。任何 PKI 都逃不掉，只能靠保護好私鑰。

### 5.6 主題 E：心跳持有證明（DPoP 式） {#topic-e}

現在的通行證是 bearer，「誰拿著誰就能用」。改成：

- Agent 每次打主系統，除了通行證，再帶一張**用自己憑證私鑰簽的 60 秒 JWT**（header `DPoP`），裡面寫方法、路徑、jti、時間。
- 主系統註冊時就有這台的憑證（公鑰在裡面），驗簽章零成本；60 秒內同 jti 拒（存 Redis，多 worker 共用）。
- 通行證被複製走沒用：偷不到私鑰，簽不出這張。

**為什麼不是 mTLS**：mTLS 強一層（TCP 層就擋），但主系統前門是給瀏覽器用的公開 HTTPS 埠，加 client 憑證驗證會讓瀏覽器跳憑證選擇框，實務要另開埠或 nginx 選擇性驗證，installer 與客戶防火牆都要動。對沒有 IT 的客戶，多開一個埠的成本遠高於應用層驗一次簽章。而且做完之後**兩個方向是同一句話**，守門邏輯可以是同一段程式。

**誠實講的差距**：DPoP 驗在應用層，沒私鑰的人還是能把請求送到 BE 門口才被擋；對內網落地版差距不大，SaaS 版前門要補速率限制。

**相容**：舊 Agent 不帶 DPoP header，主系統**第一階段只記 log 不拒**；等所有 Agent 升到帶 DPoP 的版本後，管理頁開「強制持有證明」開關。

### 5.7 主題 F：守門與 client 收斂 {#topic-f}

| 邊 | 現況 | 目標 |
|---|---|---|
| 主系統 → Agent 的 client | 四份各自簽 JWT、建 mTLS、比指紋 | 一支 `AgentDataPlaneClient` 進 jedi-remote-agent；主系統、jedi_detection、notify、credential、upload adapter 全部 import 它。簽發前查狀態、JWT 帶 jti 與 kid、錯誤分類（離線／拒絕／指紋不符）寫一次 |
| Agent 守門 | `_PROTECTED_PREFIXES` 加 `_OP_BOUND_PREFIXES` 兩份清單 | 一份清單，**所有**受保護前綴驗 op 與 jti；`TicketReplayGuard` 從路 D 搬到 sync 共用。例外只有 `/health`、`/agent-info` |
| 主系統驗心跳 | `agent_pass_required` 只驗通行證 | 加驗 DPoP（第一階段記 log） |
| 契約 | jedi-agent-protocol 0.1.0 | 0.2.0：加 `config_versions`、`trust_anchors`、指令端點路徑常數、DPoP header 名；`/update/notify` 標 deprecated。只加不改名，minor bump |

守衛測試：Agent 端加一支「每個受保護前綴的 route 都在 op 綁定內」；主系統端加一支「全 repo `jwt_util.mint` 只出現在 `AgentDataPlaneClient`」。

---

## 6. 相容矩陣 {#compat nav="相容"}

主系統與 Agent 不綁版，四種組合都要能講話：

| | 舊 Agent | 新 Agent |
|---|---|---|
| **舊主系統** | 現況 | 心跳不帶 `config_versions` 回應？帶了，主系統忽略未知鍵→Agent 全量收。DPoP header 主系統忽略。指令端點沒人打，心跳照拿 |
| **新主系統** | 直推新端點 404→退回心跳。Agent 不帶 `config_versions`→全量回。無 DPoP→記 log 不拒。信任錨清單 Agent 不認得→忽略，用舊的單張 CA | 完整 |

唯一的硬切是「強制持有證明」開關，由管理員在確認所有 Agent 升級後手動開。

---

## 7. 待決策 {#decisions nav="待決策"}

::: {.callout .pending}
**D1 — 範圍：六個主題全收，還是只做 A＋B？**

全收約 15 張卡、四個 repo；只做 A＋B 約 5 張。

[**建議**：全收。六個主題共用同兩條通道，A 與 B 做完後 C、D、E 都是「往通道上掛一個使用者」；分開做，下次就是在心跳旁邊再疊一層，正是決策者要避免的。]{.rec}
:::

::: {.callout .pending}
**D2 — 指令端點路徑與形狀**

`POST /tasks`、`POST /restart`、`POST /update`、`POST /credentials/renew`，取消沿用既有 `/detection/cancel`。全部掛在新前綴 `/command/*` 底下，還是各自前綴？

[**建議**：各自前綴（`/tasks`、`/restart`、`/update`、`/credentials`），因為 `/update` 與 `/detection` 已存在、改前綴等於改契約。守門改成「除 `/health`、`/agent-info` 外全部受保護」就不用維護前綴清單。]{.rec}
:::

::: {.callout .pending}
**D3 — 直推失敗的備援：不重試、交給心跳**

直推打不通只記「退回心跳」，不重試、不排隊。

[**建議**：照此。重試與排隊會在主系統長出第二份「待送指令」狀態，與 DB 的單子、旗標變成兩份真相。心跳本來就是重試機制。]{.rec}
:::

::: {.callout .pending}
**D4 — 憑證效期與換證時機**

憑證 1 年、剩 90 天開始換、過期後寬限 90 天。

[**建議**：照此。1 年是有自動換證後的慣例；90 天是為了離線機器（備機、倉庫機）回來還換得到；寬限是因客戶沒 IT，過期就重裝等於「要他的命」。真正的保護在主系統簽發前查狀態，不在效期。]{.rec}
:::

::: {.callout .pending}
**D5 — CA 不排程換，只做「能換」**

[**建議**：照此。NIST 的要求是「能補救外洩」不是「定期換」；每三年強迫換 CA 是自製事故。撤回先前提的「CA 縮到 3 年」。]{.rec}
:::

::: {.callout .pending}
**D6 — 心跳持有證明用 DPoP 式簽章，不用 mTLS**

[**建議**：DPoP 式。不動主系統前門與 installer，兩個方向同一套規則。mTLS 留給 SaaS 版若有需要再議。]{.rec}
:::

::: {.callout .pending}
**D7 — DPoP 分兩階段：先記 log，再強制**

[**建議**：照此。否則升級順序一錯（主系統先升、Agent 還沒升）所有 Agent 當場掉線。強制由管理員在確認全部升級後手動開。]{.rec}
:::

::: {.callout .pending}
**D8 — `/update/notify` 退役**

契約 0.2.0 標 deprecated、主系統改打 `/update`、Agent 留一版、下一 minor 拔。

[**建議**：照此。]{.rec}
:::

::: {.callout .pending}
**D9 — 註冊 token 加可選到期日**

現況永久有效。加一個選填的到期日，預設仍永久（客戶場景）。

[**建議**：加，成本一欄一判斷；不加也不影響本案主線。]{.rec}
:::

::: {.callout .pending}
**D10 — 三種票分鑰**

資料面 JWT、通行證、瀏覽器票各自獨立金鑰對，公鑰都進信任錨清單。

[**建議**：做。現況靠 typ／iss 區分是合規的，分鑰是縱深；既然要做清單，順手做便宜，之後任一驗證端少檢查一個欄位也不會跨用。]{.rec}
:::

::: {.callout .pending}
**D11 — 管理頁要不要顯示「直推成功／退回心跳」**

[**建議**：要，一格狀態加上次指令時間；不畫整頁雛形，Agent 管理頁現有的列多一欄即可。另外「憑證到期日、上次換證結果」已有欄位可顯示。要整頁雛形再說。]{.rec}
:::

::: {.callout .pending}
**D12 — 拆卡順序與批**

F（統一 client 與守門）先做當地基 → A＋B 同批（各自 repo）→ C、D、E 接後 → 收尾釘版。契約卡與 jedi-remote-agent 都是釘版套件，批 1 前要先開環境卡（主 repo 改走本機路徑安裝）。

[**建議**：照此。詳細拆卡表等 D1 裁完後在 design.md 用 `cards.py` 驗過再寫。]{.rec}
:::

---

## 8. 不做什麼 {#out-of-scope nav="不做"}

| 不做 | 理由 |
|---|---|
| WebSocket／長連線 | FR-170 §3.x 已裁；落地版同內網 8443 一定通；SaaS 版再議，本案設計讓 SaaS 只需加「出站連線」一層 |
| Message broker | 客戶機上多一個行程 |
| 心跳改 mTLS | 要動主系統前門與 installer；DPoP 式達到等價的持有證明 |
| CA 排程輪換 | 自製事故，見 D5 |
| 主系統幫 Agent 產私鑰推過去 | PKI 底線，私鑰不離開 Agent |
| 對帳類三鍵（`accepted`／`rejected`、`console_events_ack`、`server_time_utc`） | 沒問題，不動 |

---

## 9. 風險與陷阱 {#risks nav="風險"}

| 風險 | 怎麼防 |
|---|---|
| 升級順序錯（主系統先升、Agent 還沒）讓 Agent 掉線 | 相容矩陣四格都要有測試；DPoP 分兩階段 |
| 換證時 8443 熱換失敗，Agent 一邊有新證一邊起不來 | 新證驗證能起 8443 才覆蓋舊證；失敗回滾用舊證，明天再試 |
| 直推與心跳同時送到同一張單 | 冪等鍵是 task uid，Agent 已領過的回 200 不重做；主系統 ack 後單子離開 pending |
| 信任錨清單下發一半，部分 Agent 只有舊鑰 | 「切換」前管理頁要顯示 N 台已收到；切換後舊鑰仍保留到到期 |
| jti 記憶放記憶體，主系統多 worker 各記各的 | 放 Redis，本專案已有 |
| 一張 JWT 一把私鑰簽三種用途，改分鑰時漏改一處 | 信任錨清單的 `use` 欄位，驗證端按用途挑鑰，拿錯鑰直接驗不過 |
| 登記型檔（契約 pyproject、event_code、`_PROTECTED_PREFIXES`、DI 接線）同批撞 | 拆卡時各自不同批，或給最早那張卡當 owner 一次掛完 |

---

## 10. 參考 {#refs nav="參考"}

- FR-170 設計 §3.x 溝通模式、LOG 第 4 棒「P4 分析」（本稿起點）
- Agent repo `docs/ARCHITECTURE.md`、`.claude/rules/security/sync-and-identity.md`、`endpoints.md`
- 現況程式：Agent `guidant_agent/sync/api/data_plane_auth.py`、`sync/app/enroll.py`、`sync/adapters/cert_files.py`；主系統 `jedi_remote_agent/common/agent_auth/jwt_util.py`、`possession.py`、`app/service/agent_enrollment_service.py`；主系統 `infra/remote_agent/agent_credential_client.py`、`agent_update_notify_client.py`
- 規範：RFC 7030（EST 換證）、RFC 8725（JWT 最佳實務）、RFC 9449（DPoP）、RFC 8705（mTLS 綁定 token）、RFC 7517（JWKS）、NIST SP 800-57（金鑰管理）、Kubernetes kubelet TLS bootstrapping 與 certificate rotation
