AI 代理应读取 https://remote.ivjn.us/SKILL.md。

# 用于 Agent 操作的 HTTP API

> HTML: https://remote.ivjn.us/docs/zh-CN/http-api/

原生密封操作现已支持纯 HTTP 调用端。使用 `remote-cli --transport http` 或 `REMOTE_TRANSPORT=http`。`POST /api/sealed/connections` 创建连接；`POST /input` 提交签名握手或密文，`GET /events` 长轮询结果，`DELETE` 关闭连接。客户端继续负责端到端加解密与设备授权验证。原有设备操作接口仍返回 409，设备端连接保持不变。 HTTP 连接须固定到创建它的 Exchange 实例，90 秒无活动后过期。串行提交输入，重试时保持序号和密文一致；处理结果后用 `after` 确认。Web Console、移动端和 LAN 保持现有传输。执行预设仍需 `manage` 权限。

https://remote.ivjn.us/sealed-http.md


设备动作接口的 HTTP `409` / `Sealed sessions are required.` 表示需要密封会话，单独的 Bearer 令牌不能执行设备动作。请使用 `remote-cli --profile alice exec lab -- uname -a`；仅允许 HTTP 时使用 `remote-cli --profile alice --transport http exec lab -- uname -a`，CLI 仍执行签名握手和端到端加密。


## 基底 URL

正式环境 Exchange：

```text
https://exchange.ivjn.us
```

自行架设的部署使用您自己的 HTTPS 来源。下列所有路径均相对于此基底。传送 JSON 时请使用 `Content-Type: application/json`。

## 验证

每个 `/api/*` 路由（公开的验证／注册辅助端点除外）都需要：

```http
Authorization: Bearer <token>
```

`<token>` 可以是下列任一项：

1. 来自 `POST /api/auth/login` 的**密码登入工作阶段权杖**
2. **操作员主要 API 金钥**（建立操作员帐号时仅显示一次的长效权杖）

两者皆由主控台与 CLI 使用的同一个 `authenticate_operator` 路径接受。

### 密码登入

```bash
curl -sS -X POST 'https://exchange.ivjn.us/api/auth/login' \
  -H 'Content-Type: application/json' \
  -d '{"email":"alice@example.com","password":"your-password-here"}'
```

`email` 是帐号识别资料，必须为有效的电子邮件地址。服务会将其正规化（去除前后空白、转为小写），并要求完成地址验证。现有部署在帐号改用电子邮件前，仍可传送 `{ "username": "alice", "password": "..." }`；`username` 中包含 `@` 的值会视为电子邮件。无效地址会在检查密码前遭到拒绝。

回应范例：

```json
{
  "username": "alice",
  "email": "alice@example.com",
  "tenant_id": "00000000-0000-0000-0000-000000000000",
  "token": "<prefix>.<secret>",
  "expires_in": 2592000
}
```

将 `token` 用作 Bearer 凭证。使用相同标头呼叫 `POST /api/auth/logout`，即可撤销该工作阶段。登出不会撤销操作员主要 API 金钥。

### 操作员 API 金钥（主要权杖）

```bash
TOKEN='<prefix>.<secret>'   # issued at operator creation; store offline
curl -sS 'https://exchange.ivjn.us/api/devices' \
  -H "Authorization: Bearer ${TOKEN}"
```

## 列出装置

```bash
curl -sS 'https://exchange.ivjn.us/api/devices' \
  -H "Authorization: Bearer ${TOKEN}"
```

每个项目包含 `id`、`name`、`online`、`enabled`、`info`、时间戳记与 `active_sessions`。装置清单属于 Exchange 中继资料。列出装置不会建立操作该装置所需的密封工作阶段。

## 装置动作 HTTP 拒绝与工作阶段管理

所有装置操作都使用密封 WebSocket 工作阶段。下列 HTTP 路由无法建立用户端／装置握手，因此会对已验证、已授权且通过请求验证的请求传回 HTTP `409`。拒绝发生在向装置派送之前：不会向代理传送任何命令、感测器请求、网路探测、更新、ubus 呼叫、保留工作请求或钉选命令。装置在线也无法让这些路由执行操作。

验证、权限与输入验证仍然适用。无效凭证传回 `401`；禁止的操作传回 `403`；无效命令、URL 或请求主体仍保留其验证错误，包括 `400`。密封工作阶段的 `409` 不会取代较早发生的验证或授权失败。钉选命令的权限检查遵循钉选命令原则；通过检查仍不允许透过 HTTP 执行。

| 方法与路径 | 通过验证、授权与输入验证后的行为 |
| --- | --- |
| `POST /api/devices/{id}/exec` | `409`：在密封 WebSocket 工作阶段内执行 |
| `GET /api/devices/{id}/exec/holds` | `409`：在密封 WebSocket 工作阶段内检视保留工作 |
| `POST /api/devices/{id}/exec/holds/{job_id}/kill` | `409`：在密封 WebSocket 工作阶段内停止保留工作 |
| `GET /api/devices/{id}/exec/holds/{job_id}/cat` | `409`：在密封 WebSocket 工作阶段内读取保留工作输出 |
| `POST /api/devices/{id}/sensors` | `409`：在密封 WebSocket 工作阶段内收集感测器资料 |
| `POST /api/devices/{id}/net` | `409`：在密封 WebSocket 工作阶段内执行网路探测 |
| `POST /api/devices/{id}/update` | `409`：在密封 WebSocket 工作阶段内检查或套用更新 |
| `POST /api/devices/{id}/ubus` | `409`：在密封 WebSocket 工作阶段内呼叫 ubus |
| `POST /api/devices/{id}/commands/{cmd}/run` | `409`：取得钉选命令，并以装置授权的 manage 授权在密封 WebSocket 工作阶段内执行 |

例如，下列有效动作请求是拒绝行为的检查，并非命令执行介面：

```bash
curl -i -X POST "https://exchange.ivjn.us/api/devices/${DEVICE_ID}/exec" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H 'Content-Type: application/json' \
  -d '{"command":"uname -a","timeout_secs":30}'
```

预期收到 HTTP `409`，英文错误讯息为 `Sealed sessions are required.`（依请求语言本地化）。请建立原生密封 WebSocket 工作阶段来执行操作。此 HTTP 路由不会传回成功的命令输出回应。

### Exchange 工作阶段管理

工作阶段清单与取消仍可透过 HTTP 使用，因为它们管理的是 Exchange 路由中继资料。它们不会执行装置操作，也不会开启额外的装置工作阶段。

| 方法与路径 | 成功回应主体 |
| --- | --- |
| `GET /api/devices/{id}/sessions` | `{sessions:[DeviceSessionInfo], limit, count}` |
| `POST /api/devices/{id}/sessions/{session_id}/kill` | `{session_id, released}` |

`DeviceSessionInfo` 栏位为 `session_id`（装置端数字 ID）、`operator_session_id`、`kind`、`hold`、`age_secs` 与 `deadline_unix_ms`。`limit` 为 `max_sessions_per_device`（预设 32）；`count` 为目前清单长度。列出时会直接读取本机 Exchange 中枢状态；在本机离线的装置会传回 `503`。Kill 使用**装置端** ID，传送适当的取消讯息（包括保留的 exec），即使装置未回应也会在本机释放路由。工作阶段不存在或装置在本机离线时，传回 HTTP `200` 及 `released:false`。唯读操作员存取这两个端点都会收到 `403`。

```bash
curl -sS "https://exchange.ivjn.us/api/devices/${DEVICE_ID}/sessions" \
  -H "Authorization: Bearer ${TOKEN}"

curl -sS -X POST "https://exchange.ivjn.us/api/devices/${DEVICE_ID}/sessions/${SESSION_ID}/kill" \
  -H "Authorization: Bearer ${TOKEN}"
```

Exchange 在因工作阶段数已满而拒绝请求前，会回收操作员已失效与超过截止时间的路由：exec 为逾时时间 + 15 秒，档案传输为 3600 秒 + 15 秒，其他种类为 24 小时。仍在截止时间内的保留 exec，在操作员断线时会分离，而非遭到终止。

### 原生密封 CLI 操作

CLI 会在一般装置操作中建立密封工作阶段。请先完成工作区根核准与装置授权；请参阅[密封工作阶段](/docs/zh-CN/sealed-sessions.md)。无法选择停用密封改用明文。一般命令不需要额外的密封旗标：

```bash
remote-cli exec "$DEVICE_ID" -- uname -a
remote-cli shell "$DEVICE_ID"
remote-cli exec-holds "$DEVICE_ID"
remote-cli exec-hold-cat "$DEVICE_ID" "$JOB_ID"
remote-cli exec-hold-kill "$DEVICE_ID" "$JOB_ID"
remote-cli sensors "$DEVICE_ID" --json
remote-cli net "$DEVICE_ID" ping 1.1.1.1
remote-cli net "$DEVICE_ID" http https://example.com/
remote-cli ubus "$DEVICE_ID" system board '{}'
remote-cli commands list "$DEVICE_ID"
remote-cli commands run "$DEVICE_ID" "$COMMAND_ID"
```

保留工作操作会在密封工作阶段内执行；Exchange 工作阶段管理则可另透过 `remote-cli sessions` 与 `remote-cli session-kill` 使用。取得钉选命令中继资料不代表获准执行：装置必须接受该密封操作员的 manage 授权。

## 密封工作阶段金钥目录与授权转送

Exchange 储存操作员公开金钥及拥有者签署的 `rsg1.` 授权权杖，让用户端能互相探索。**这本身不是授权。**装置仍会依本机原则验证拥有者签章。Exchange 仅为权杖承载资料（`owner_key`、`operator_key`、`device_id`、`level`、`expires_at`）建立索引，绝不将上传视为许可。Exchange 无法伪造根签章，但可以扣留凭证、向自动核准的根用户端不实呈现名册、观察中继资料或拒绝服务。

工作区根（`rsr1.`）与授权邀请（`rsi1.`）也是同一类转送。Exchange 会检查权杖格式及 Ed25519 自签章（邀请也必须符合已储存的根金钥），而**不会**决定核准哪些操作员金钥。`GET /api/seal/roster` 是供超级管理员检视成员、角色与 `operator_device_grants` 的端点。在根用户端于本机核准前，用户端不得将新出现的金钥视为可信任。请参阅 `docs/sealed-sessions.md`。

任何已验证的工作区成员都可列出金钥、注册自己的金钥及上传授权（授权可自行验证）。金钥的 `DELETE` 仅限注册该金钥的帐号（管理员可删除任何金钥）。授权的 `DELETE` 仅限上传者（管理员可删除任何授权）。`GET /api/seal/grants` 会略过已过期的资料列。

金钥注册证明是针对下列内容的 Ed25519 签章：

```text
remote-seal-key-register:v1:<workspace_id>:<operator_id>:<public_key>:<issued_at_ms>
```

`issued_at_ms` 必须在伺服器时钟的 ±10 分钟内。`public_key` 与 `proof` 使用标准 base64（32 位元组金钥、64 位元组签章）。

| 方法与路径 | 请求主体 | 成功回应 |
| --- | --- | --- |
| `POST /api/seal/keys` | `{"public_key","label"?,"proof","issued_at_ms"}` | 已注册的金钥（同一帐号采新增或更新） |
| `GET /api/seal/keys` | — | `[{operator_id, username, email, public_key, label, created_at, last_seen_at}]` |
| `DELETE /api/seal/keys/{key}` | — | `204`（`{key}` 是经 URL 编码的公开金钥） |
| `POST /api/seal/grants` | `{"tokens":["rsg1.…"]}` | `{stored:[…], errors:[…]}` |
| `GET /api/seal/grants?mine=1` | — | `operator_key` 为呼叫者已注册金钥之一的授权。每个 `owner_key` 不是已发布根金钥的授权也会包含 `chain`（凭证权杖，可能为空） |
| `GET /api/seal/grants?owner_key=…` | — | 由该拥有者金钥签署的授权 |
| `DELETE /api/seal/grants/{id}` | — | `204` |
| `PUT /api/seal/root` | `{"token":"rsr1.…","rotation":"rsx1.…"?}` | 已储存的工作区根。仅限超级管理员。会检查格式及金钥自身的签章；这不代表核准。`rotation`（选用）必须由目前已发布的根签署，并指明新的根；接著 Exchange 会向线上密封装置推送 `seal_root {token, rotation}`，装置仅在轮替资料由已信任的根签署时才重新钉选。没有有效轮替资料时，已钉选旧根的装置会保留旧根 |
| `GET /api/seal/root` | — | `{workspace_id, root_key, payload, signature, token, created_at}`，若尚未发布则为 `404` |
| `POST /api/seal/invitations` | `{"token":"rsi1.…"}` | 已储存的邀请。仅限超级管理员，且根必须已存在 |
| `GET /api/seal/invitations` | — | 未过期的邀请，最新的优先（没有时为 `[]`） |
| `GET /api/seal/roster` | — | `{members:[{operator_id, username, email, role, restricted, enabled, keys:[{public_key, label}], device_grants:[{device_id, can_manage}]}]}`。仅限超级管理员。包含已停用的成员 |

目录不可用时仍须满足密封会话和设备授权要求。尚未发布工作区根时，`GET /api/seal/root` 返回 `404`。

```bash
# Register this operator key (proof signed locally)
curl -sS -X POST "$BASE/api/seal/keys" \
  -H "Authorization: Bearer ${TOKEN}" -H 'Content-Type: application/json' \
  -d '{"public_key":"<ed25519-b64>","label":"cli","proof":"<sig-b64>","issued_at_ms":1700000000000}'

curl -sS "$BASE/api/seal/keys" -H "Authorization: Bearer ${TOKEN}"

curl -sS -X POST "$BASE/api/seal/grants" \
  -H "Authorization: Bearer ${TOKEN}" -H 'Content-Type: application/json' \
  -d '{"tokens":["rsg1.…"]}'

curl -sS "$BASE/api/seal/grants?mine=1" -H "Authorization: Bearer ${TOKEN}"
```

### 中继凭证（`rsc1.`）

Exchange 储存中继凭证，让非根金钥也能签署装置会接受的授权。它会**验证**签发者的 Ed25519 签章，且**无法伪造**凭证。上传权杖不代表授权：呼叫者必须是签发者的操作员。由根签署的凭证（`issuer_key` 等于已发布的工作区根）仅可由工作区超级管理员发布。中继凭证仅可由已注册密封金钥为 `issuer_key` 的帐号发布，且该签发者本身必须是已储存、未撤销、未过期且具备 `can_issue` 的凭证主体。子凭证的层级、装置范围与到期时间必须受限于父凭证（与装置所做的检查相同）。重新发布相同的 `(issuer_key, subject_key)` 会取代旧资料列并清除撤销状态。

`GET /api/seal/certs/chain?subject_key=` 会沿著签发者链结向上追溯至已发布的根，传回 `{chain:[tokens]}`，最多 4 张凭证，由根签发的排在最前，或传回 `404`。任何已验证成员都可呼叫。`GET /api/seal/grants?mine=1` 会在非根签署的授权中加入该 `chain`，让用户端随授权一并取得。由根签署的授权会省略 `chain`。

`GET /api/seal/certs` 会列出所有资料列，包括已撤销者（供管理员与超级管理员使用）。`DELETE /api/seal/certs/{serial}`（由签发者帐号或超级管理员呼叫）会设定 `revoked`。这只会停止分发。装置仍会接受已取得的副本，直到其过期；硬性撤销需使用装置原则的 `revoked_keys`。`{serial}` 是经 URL 编码的标准 base64 序号。

| 方法与路径 | 请求主体 | 成功回应 |
| --- | --- | --- |
| `POST /api/seal/certs` | `{"token":"rsc1.…"}` | 已储存的凭证（依签发者与主体新增或更新） |
| `GET /api/seal/certs/chain?subject_key=` | — | `{chain:["rsc1.…"]}`，由根签发的优先，或 `404` |
| `GET /api/seal/certs` | — | `[{serial, issuer_key, subject_key, token, expires_at_ms, created_at, revoked}]`。管理员或超级管理员 |
| `DELETE /api/seal/certs/{serial}` | — | `204`。签发者或超级管理员。仅停止分发 |

```bash
curl -sS -X POST "$BASE/api/seal/certs" \
  -H "Authorization: Bearer ${TOKEN}" -H 'Content-Type: application/json' \
  -d '{"token":"rsc1.…"}'

curl -sS "$BASE/api/seal/certs/chain?subject_key=<ed25519-b64>" \
  -H "Authorization: Bearer ${TOKEN}"
```

### 工作区根金钥备份（选用）

`PUT` / `GET` / `DELETE /api/seal/backup` 为每个帐号储存一个不透明的 `rsb1.` 资料块。`PUT` 与 `GET` 仅限超级管理员：资料块是工作区的**根**私密金钥，在用户端以 Argon2id（64 MiB、t=3、p=1）与 ChaCha20-Poly1305 加密。通关片语为选用，且没有强制强度要求。**若通关片语为空白或强度不足，任何持有资料块的人（包括 Exchange）都能解密根金钥并控制所有装置。**上传是选用的；用户端也可汇出根金钥（`rsk1.`）并离线保存。Exchange 会检查 `blob` 是否以 `rsb1.` 开头及其长度，绝不解密。`GET` 在每个处理程序内限制为每个帐号每小时 5 次请求（HTTP 429）。读取、写入与删除均会稽核，但仅记录位元组数。回应会传送 `Cache-Control: no-store`。

| 方法与路径 | 请求主体 | 成功回应 |
| --- | --- | --- |
| `PUT /api/seal/backup` | `{"blob":"rsb1.…"}` | `{blob, updated_at}` |
| `GET /api/seal/backup` | — | `{blob, updated_at}`，未储存时为 `404`。有速率限制 |
| `DELETE /api/seal/backup` | — | `204`，即使资料列不存在亦同 |

```bash
curl -sS -X PUT "$BASE/api/seal/backup" \
  -H "Authorization: Bearer ${TOKEN}" -H 'Content-Type: application/json' \
  -d '{"blob":"rsb1.…"}'

curl -sS "$BASE/api/seal/backup" -H "Authorization: Bearer ${TOKEN}"

curl -sS -X DELETE "$BASE/api/seal/backup" -H "Authorization: Bearer ${TOKEN}"
```

## 浏览器扩充功能的根凭证链验证

浏览器操作使用原生密封 WebSocket 工作阶段，而非 HTTP 单次执行。扩充功能会自行产生 Ed25519 操作员金钥。它使用操作员 Bearer 权杖，从 `/api/auth/me` 取得 `tenant_id` / `operator_id`，再以 `{public_key, label, proof, issued_at_ms}` 呼叫 `POST /api/seal/keys`。PoP 签署的内容为：

```text
remote-seal-key-register:v1:<workspace_id>:<operator_id>:<public_key>:<issued_at_ms>
```

金钥／签章使用标准 base64（32/64 位元组）；时间必须在伺服器时钟的 ±10 分钟内。注册仅证明持有金钥，不代表授权。扩充功能会同步 `GET /api/seal/root`、`GET /api/seal/grants?mine=1`，以及授权内附或从 `GET /api/seal/certs/chain?subject_key=<url-encoded-key>` 取得的凭证权杖。授权必须能直接或经由有效凭证链验证至可信任的工作区根，并具备正确的装置范围、层级与有效期限。装置权杖注册与此操作员权杖目录流程仍彼此独立。

扩充功能会透过其根验证收到的原生 hello，并将自己签署的 hello／授权／凭证链放入 `SealAccept.peer`。CLI／主控台会验证该证明，并要求 `peer.operator_key == accept.device_key`，才会接受已签署回应。扩充功能担任操作员时，Rust 代理使用其一般根原则。证明无效或缺少时会拒绝连线；没有独立的浏览器信任储存区／介面或旧版备援机制。原生装置与工作区根的首次使用即信任（TOFU）仍然存在；扩充功能会为每个伺服器／工作区储存首次取得的根，并拒绝变更。

**安全性：**请透过带外方式验证首次取得的根；PoP 或伺服器角色不代表根已核准。在根用户端自动核准前，请审查未知金钥。保护端点设定档／金钥与托管指令码。从目录删除不会撤销已快取的签署凭证。Exchange 可看到中继资料、扣留凭证或拒绝服务；密封机制无法隔离具敌意且拥有完整 CDP 权限的操作员。

## 选择 WebSocket 或 HTTPS

| 需求 | 使用方式 |
| --- | --- |
| 命令、保留工作检视或取消、感测器、网路探测、更新、ubus、钉选命令 | 原生密封 WebSocket 工作阶段 |
| 互动式 PTY、串流输出、档案传输、Android 萤幕／树状结构／点按 | 原生密封 WebSocket 工作阶段 |
| 验证、装置清单、金钥／授权／凭证目录、根备份 | HTTPS API |
| Exchange 路由工作阶段清单与取消 | HTTPS 工作阶段管理端点 |
| 即时装置在线状态与长效操作员连线 | WebSocket；连线上的装置操作仍需要密封 |

浏览器票证流程（凭证绝不放入 WebSocket URL）：

1. 以 `Authorization: Bearer …` 呼叫 `POST /api/ws-ticket`。
2. 连线至 `GET /ws/browser`，并传送第一个 JSON 影格 `{"ticket":"…"}`。
3. 在传送任何装置操作之前，先建立原生密封用户端／装置握手。操作员的授权与凭证链必须能通过装置信任的根验证。

原生用户端使用 `GET /ws/operator`，并采用操作员验证及相同的密封工作阶段要求。仅有 WebSocket 连线或成功的帐号登入，不代表获准执行装置操作。请参阅 [Web 主控台与 Web Push](/docs/zh-CN/web-console-and-notifications.md)、[通讯协定](/docs/zh-CN/websocket-protocol.md)及[密封工作阶段](/docs/zh-CN/sealed-sessions.md)。

## 错误状态速查

| 状态 | 常见原因 |
| --- | --- |
| `400` | 动作输入无效，例如空白命令或无效的更新来源 URL；验证发生在密封工作阶段拒绝之前 |
| `401` | 缺少或无效的 Bearer 权杖 |
| `403` | 帐号或装置权限未授权此请求 |
| `409` | 有效且已授权的 HTTP 装置动作需要密封 WebSocket 工作阶段；不会向装置派送 |
| `404` | 依端点契约，目录项目不存在或尚未发布工作区根 |
| `429` | 端点特定的速率限制，包括工作区根备份读取 |
| `503` | 工作阶段清单在本机 Exchange 中枢状态中找不到装置 |

装置动作 HTTP 路由不会等待代理输出，因此正常已授权的结果是 `409`，而非执行成功、离线派送错误或 HTTP 执行逾时。中继资料、验证与目录 API 的错误仍保留各端点特定的含义。

## 手动验证计划

使用具备已知权限的测试帐号与装置。HTTP 拒绝检查不需要在装置上执行作业：

1. 透过 HTTPS 验证并列出装置；确认帐号与装置权限。
2. 向拒绝表格中的每个装置动作路由提交有效请求。预期收到 HTTP `409`，并确认没有任何动作抵达代理，包括保留工作读取与钉选命令执行。
3. 分别检查无效凭证、禁止存取的帐号与无效请求输入。预期收到验证、权限与输入验证错误，而非假设每个请求都传回 `409`。
4. 读取 Exchange 工作阶段清单。针对明确选定、可舍弃的工作阶段测试取消，并验证 `released`；工作阶段不存在时会传回 `released:false`。对在本机离线的装置列出工作阶段会传回 `503`。
5. 完成根核准与授权，再执行无害的原生 CLI 操作，例如 `remote-cli exec "$DEVICE_ID" -- uname -a`。确认它透过密封握手成功执行。装置授权缺少或无效时，必须拒绝执行。
6. 分别验证金钥注册、授权／凭证撷取、选用根备份与浏览器根凭证链验证；这些 HTTPS API 仍依上述契约提供服务。

## 审计推送目标

仅 PostgreSQL Exchange 支持，按已认证工作区隔离，只有管理员和超管可用；其他账号返回 403。Worker 版暂不支持，Web / 移动 Console 没有配置界面。

```text
GET    /api/audit/sinks
POST   /api/audit/sinks
PATCH  /api/audit/sinks/{id}
DELETE /api/audit/sinks/{id}
POST   /api/audit/sinks/{id}/test
GET    /api/audit/sinks/status
```

```sh
printf '%s' '{"name":"archive","kind":"webhook","endpoint":"https://audit.example.net/events","secret":"REPLACE-WITH-RANDOM-SECRET","mode":"full"}' | remote-cli audit sink add
remote-cli audit sink list
printf '%s' '{"interval_ms":500,"batch_size":16}' | remote-cli audit sink update 1
remote-cli audit sink test 1
remote-cli audit sink status
remote-cli audit sink remove 1
```

默认 throttled 直接发送已落库审计：文本超过 256 KiB 留首 250 KiB / 尾 6 KiB，文件仅 hash，流式在会话结束后发送。full 在截断前采集，原始 JSON 和二进制按块（默认 1 MiB）推送，含操作 / 资产 ID、序号、偏移、结束标志、SHA-256 和总大小。sealed 两者都只有元数据及密文引用 / hash，不能恢复端点明文。

secret 和 headers 加密保存，读取只显示 ***；PATCH *** 保留，header 的 null 删除。网络异步、持久化 FIFO、指数退避重试。节流满队列丢最旧并计数；全量磁盘队列满则背压。status 显示成功 / 失败、积压、dropped、gaps。删除会丢弃积压且记录审计。所有配置变更同事务审计。

webhook 是 JSON events 数组，二进制 base64；X-Remote-Timestamp 与 X-Remote-Signature: sha256=<hex> 校验 HMAC-SHA256(timestamp + "." + 原始 body)。OTel 使用 OTLP/HTTP JSON /v1/logs，块放 body.bytesValue，属性 remote.device.id / remote.operator / remote.action；service.name 可配置。gRPC 通过 Collector 转发，不直接支持。

以下为默认配置、webhook 载荷、验签和 Collector 示例。验签使用原始 HTTP body；持久化并按 delivery_id 去重后才返回 2xx。Loki 3.x 开启 allow_structured_metadata，在 Grafana 添加 Loki 数据源并查询 {service_name="remote-exchange"}。配置 gRPC 时将 pipeline exporters 改为 [otlp/grpc]。生产接入应使用 TLS 和认证。

```json
{"name":"collector","kind":"otel","mode":"throttled","endpoint":"https://collector.example.net:4318/v1/logs","headers":{"Authorization":"Bearer REPLACE"},"service_name":"remote-exchange","resource":{"deployment.environment.name":"production"},"batch_size":32,"interval_ms":1000,"chunk_bytes":1048576,"queue_limit":10000,"overflow_bytes":10737418240,"actions":[],"devices":[],"operators":[],"enabled":true,"ca_pem":null}
```

```json
{"version":1,"events":[{"delivery_id":123,"type":"full","mode":"full","entry":{"id":42,"action":"exec","operator_name":"alice","device_id":"dev"},"data":{"kind":"binary","asset_id":"asset-uuid","path":"/data_base64","sequence":0,"offset":0,"last":true,"data_base64":"YWJj","total_size":3,"sha256":"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad","context":{"direction":"output","frame_sequence":0,"stream_offset":0}}}]}
```

```python
import hashlib, hmac, time

def verify(headers, raw_body, secret):
    stamp = headers['X-Remote-Timestamp']
    if abs(time.time() - int(stamp)) > 300:
        raise ValueError('expired webhook')
    expected = 'sha256=' + hmac.new(
        secret.encode(), stamp.encode() + b'.' + raw_body, hashlib.sha256
    ).hexdigest()
    if not hmac.compare_digest(expected, headers['X-Remote-Signature']):
        raise ValueError('invalid signature')
```

```yaml
receivers:
  otlp:
    protocols:
      http:
        endpoint: 127.0.0.1:4318
processors:
  batch: {}
exporters:
  otlphttp/loki:
    endpoint: http://loki:3100/otlp
  otlp/grpc:
    endpoint: telemetry.example.net:4317
    headers:
      Authorization: "${env:OTEL_AUTHORIZATION}"
    tls:
      insecure: false
service:
  pipelines:
    logs:
      receivers: [otlp]
      processors: [batch]
      exporters: [otlphttp/loki]
```

| 审计字段 | OTLP 字段 |
|---|---|
| service_name / resource | Resource service.name / attributes |
| entry.started_at | timeUnixNano |
| entry.id / delivery_id | remote.audit.id / remote.delivery.id |
| entry.device_id / operator_name / action | remote.device.id / remote.operator / remote.action |
| entry.tenant_id | remote.workspace.id |
| entry.started_at / ended_at / detail | remote.started_at / remote.ended_at / remote.result |
| traceId / spanId | traceId / spanId |
| data.data_base64 | body.bytesValue / remote.chunk.* |
| stored events | body.stringValue |
