feat(cloud): add edge host enrollment

This commit is contained in:
2026-07-13 13:54:16 +08:00
parent cd56facbbf
commit e61dcca801
40 changed files with 2302 additions and 48 deletions
+82 -3
View File
@@ -41,6 +41,49 @@ defaults to `./tasks`.
The Host Agent only initiates outbound HTTP requests. It does not expose an
inbound port.
## Managed Edge Enrollment
New edge installations do not need a pre-coordinated Host or device ID. The
Cloud API accepts configured one-time enrollment credentials:
```powershell
$env:CLOUD_ENROLLMENT_TOKENS_JSON = '[{"principal_id":"edge-installer","token":"replace-with-a-long-random-one-time-token"}]'
```
On the edge Host, omit `HOST_AGENT_HOST_ID` and `HOST_AGENT_TOKEN` and provide
the enrollment token only for the first successful enrollment:
```bash
export HOST_AGENT_CONTROL_PLANE_URL="https://cloud.example.com"
export HOST_AGENT_ENROLLMENT_TOKEN="replace-with-a-long-random-one-time-token"
export HOST_AGENT_IDENTITY_PATH="tasks/host_identity.json"
export HOST_AGENT_DISPLAY_NAME="Edge Mac 01"
uv run --package device-host-agent device-host-agent
```
Before its first request the Host Agent creates `HOST_AGENT_IDENTITY_PATH` with
an instance identifier and long-lived random Host secret. The cloud consumes
the enrollment token, assigns `host_id`, stores only credential digests, and
returns the assigned ID. The Host Agent then enrolls each record from
`tasks/device_config.sqlite3`, stores its cloud-assigned `device_id` in that
database, connects the resulting devices, and starts heartbeat/claim loops.
Keep the identity file and device configuration database on persistent edge
storage with permissions limited to the service account. The identity file is
a bearer secret: do not put it in an image, repository, log, or general backup.
After successful enrollment, remove `HOST_AGENT_ENROLLMENT_TOKEN` from the edge
environment. An intact identity file is sufficient for restart; if only a
device mapping is lost, device enrollment reconstructs the same cloud ID.
Enrollment tokens are one-time even when they remain in Cloud API environment
configuration: their consumed digest is stored in the database. Reusing a token
for another edge instance returns a conflict. Create a distinct token for every
edge installation.
Explicit `HOST_AGENT_HOST_ID` plus `HOST_AGENT_TOKEN` takes precedence and keeps
the previous legacy behavior, including locally selected device IDs. This is
the rollback and staged-migration path for existing deployments.
## PostgreSQL Deployment
Start from `.env.example`, replace every `change-me-*` value, and keep the
@@ -106,11 +149,40 @@ only the scopes required by each integration:
include exactly one `host_id`; its token is valid only for heartbeat, claim,
renewal, and result operations for that host.
`CLOUD_ENROLLMENT_TOKENS_JSON` contains bootstrap principals with only
`principal_id` and `token`. These credentials cannot submit tasks, read the
pool, or operate as a Host; they can only create one durable Host binding.
Use high-entropy values generated by the deployment secret manager.
Do not place bearer tokens in command history, image layers, Compose files, or
logs. Use environment injection or the deployment platform's secret manager.
Rotate a token by deploying the updated Cloud API credential set and Host Agent
configuration together.
Dynamically enrolled Host credentials are stored as digests in the cloud
database. This release exposes repository-level revocation rather than a public
administration endpoint. An operator with database deployment access can revoke
a Host without deleting its task history:
```bash
export HOST_ID="host-..."
uv run --package device-cloud-platform python - <<'PY'
import os
from cloud.database import CloudDatabase
from core.models import utc_now
database = CloudDatabase(os.environ["CLOUD_DATABASE_URL"], create_schema=False)
try:
changed = database.repository.revoke_enrolled_host(
os.environ["HOST_ID"],
revoked_at=utc_now(),
)
print("revoked" if changed else "not an enrolled host")
finally:
database.close()
PY
```
## Runtime AI Planner
The Host Agent reuses the local Runtime planner. AI planning is disabled by
@@ -167,17 +239,24 @@ For rollback:
1. Stop all Host Agents and the Cloud API.
2. Back up PostgreSQL or the SQLite database file.
3. If the previous application version cannot use the current schema, run the
3. Before rolling back to a release without enrollment support, provision
temporary static Host credentials for every managed edge that must continue
operating. Stop those Host Agents and set their explicit Host ID/token.
4. If the previous application version cannot use the current schema, run the
tested downgrade while no application process is connected:
```bash
uv run alembic -c packages/cloud-platform/cloud/migrations/alembic.ini downgrade -1
```
4. Restore the previous application image or checkout and start the Cloud API.
5. Verify `/health/ready`, then restart Host Agents with credentials compatible
5. Restore the previous application image or checkout and start the Cloud API.
6. Verify `/health/ready`, then restart Host Agents with credentials compatible
with the restored Cloud API.
Downgrading revision 0002 removes dynamic credential bindings and durable
device enrollment mappings. It retains the revision-0001 Host heartbeat rows,
pooled devices, queued tasks, attempts, and plugins.
Do not remove the PostgreSQL volume during an application rollback. Queued and
attempt history are durable database state and should remain available to the
restored or forward-deployed control plane.
+58 -4
View File
@@ -338,7 +338,60 @@ Console 默认连接 `http://127.0.0.1:8000`。已由上面启动脚本连接的
`iphone-1` 会出现在设备列表中。不要在 Console 中重复登记同一台设备;当前登记
操作只写入配置,不会自动 connect。
## 9. 多设备与端口
## 9. 启动云端受管 Host Agent
完成 Appium/WDA 真机验证后,可以把这台 Mac 作为只发起出站连接的边缘 Host。
先把设备连接参数写入本地配置;这里的 `device_id` 只是 Mac 内部引用,不需要与
云端协调,云端会在首次启动时下发正式 ID:
```bash
uv run --package device-agent-runtime python - <<'PY'
import os
from storage.device_config import DeviceConfigStore
DeviceConfigStore("tasks/device_config.sqlite3").add(
device_id="local-iphone-1",
name="Edge iPhone",
driver_type="wda",
connection_info={
"server_url": "http://127.0.0.1:4723",
"device_name": "iPhone",
"udid": os.environ["DEVICE_UDID"],
"wda_local_port": 8100,
"xcodeOrgId": os.environ["APPLE_TEAM_ID"],
"xcodeSigningId": "Apple Development",
"updatedWDABundleId": os.environ["WDA_BUNDLE_ID"],
},
)
PY
```
从云端管理员获取一枚未使用的一次性 enrollment token,然后启动:
```bash
export HOST_AGENT_CONTROL_PLANE_URL="https://cloud.example.com"
export HOST_AGENT_ENROLLMENT_TOKEN="<one-time enrollment token>"
export HOST_AGENT_IDENTITY_PATH="tasks/host_identity.json"
export HOST_AGENT_DISPLAY_NAME="Edge Mac 01"
export AI_PLANNER_ENABLED="true"
export AI_PLANNER_PROVIDER="anthropic"
export ANTHROPIC_API_KEY="<secret>"
uv run --package device-host-agent device-host-agent
```
首次启动顺序为:持久化候选 Host secret、向云端换取 `host_id`、为每个本地设备
换取 `device_id`、保存映射、连接 WDA、发送 heartbeat、开始 long-poll 领取任务。
Host Agent 不监听入站端口。成功后可以从边缘环境移除 enrollment token,但必须
保留并保护 `tasks/host_identity.json``tasks/device_config.sqlite3`;前者等同于
Host bearer credential。
需要回滚到静态模式时,停止 Host Agent,由云端管理员配置匹配的静态 Host
credential,然后显式设置 `HOST_AGENT_HOST_ID``HOST_AGENT_TOKEN`。这两个变量
同时存在时优先于 enrollment state。
## 10. 多设备与端口
同时连接多台 iPhone 时,每台设备至少需要:
@@ -364,7 +417,7 @@ Console 默认连接 `http://127.0.0.1:8000`。已由上面启动脚本连接的
`webDriverAgentUrl`。常规单机使用优先让 Appium 管理 WDA,不要一开始就引入
`iproxy` 或手工 WDA 生命周期。
## 10. 常见故障
## 11. 常见故障
### Appium 返回 `device offline`
@@ -420,7 +473,7 @@ Appium server 默认使用 4723;WDA 通常使用 8100。多设备必须为每
input、launch 和 UI tree 验证基础控制,再单独处理 PaddleOCR/PaddlePaddle 的 macOS
wheel 与 Apple Silicon 兼容性。
## 11. 完成检查表
## 12. 完成检查表
- [ ] Xcode 能看到已解锁的 iPhone。
- [ ] iPhone 已信任 Mac,并启用 Developer Mode。
@@ -433,8 +486,9 @@ wheel 与 Apple Silicon 兼容性。
- [ ] 实机 integration test 通过。
- [ ] Runtime API 返回 `iphone-1`,并能执行 screenshot/tap/launch。
- [ ] 如需 OCR,另行确认 PaddleOCR 在当前 Mac/Python 架构下可运行。
- [ ] 云端受管部署已保存 Host identity,并能在 `/v1/hosts``/v1/devices` 中看到。
## 12. 后续代码改进建议
## 13. 后续代码改进建议
为了让后续执行不再依赖内联 Python 启动脚本,建议另开变更实现: