feat(cloud): add edge host enrollment
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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 启动脚本,建议另开变更实现:
|
||||
|
||||
|
||||
Reference in New Issue
Block a user