Implement edge-host-self-enrollment
Tests / Test passed: 581

Host Agent:
- One-time local operator account bootstrap (PBKDF2-HMAC-SHA256, atomic
  0600-permission write) gating the daemon's first unattended start via a
  new `setup` CLI subcommand.
- Default control-plane URL now https://amcp.home.jerryyan.top (env var
  override unchanged).
- Enrollment no longer requires a pre-issued token; falls back to
  zero-token self-service enrollment when none is configured.

Cloud control plane:
- CLOUD_SELF_SERVICE_ENROLLMENT_ENABLED (default false) opt-in flag.
- SelfServiceEnrollmentAuthProvider + ChainedEnrollmentAuthProvider:
  configured tokens still take priority; self-service only applies when
  no token matches, preserving edge-host-enrollment's token-bound path.
- Fixed a latent bug in sql_repository.py::enroll_host: the token-conflict
  lookup used `== enrollment_token_digest`, which SQLAlchemy compiles to
  `IS NULL` when the value is None, so every self-service enrollment after
  the first would have falsely collided with an existing NULL-digest host.
  Skipped that lookup entirely when the digest is None.

Docs/deploy: .env.example, compose.yaml, compose.deploy.yaml,
CLOUD_DEPLOYMENT.md, MACOS_IPHONE_SETUP.md updated for the new flag,
URL default, and required `device-host-agent setup` step.

Verification: 494 non-integration tests pass; openspec validate --strict
passes. PostgreSQL-backed contract tests and full manual end-to-end
verification were not run (no Postgres/Docker or reachable cloud-api in
this environment); noted as unchecked in tasks.md 7.2/7.4.
This commit is contained in:
2026-07-13 18:30:49 +08:00
parent a2802c6320
commit efeb3eb926
24 changed files with 838 additions and 68 deletions
+44
View File
@@ -84,6 +84,46 @@ 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.
## Self-Service Edge Enrollment (Zero-Token)
The Host Agent's default `HOST_AGENT_CONTROL_PLANE_URL` is
`https://amcp.home.jerryyan.top`. This is a single-operator home deployment
default; override the environment variable for local/dev/test runs pointed at
a different Cloud API.
When `HOST_AGENT_ENROLLMENT_TOKEN` is not set and no cached identity exists,
the Host Agent enrolls with no bearer credential at all. The Cloud API only
accepts that request when `CLOUD_SELF_SERVICE_ENROLLMENT_ENABLED=true`
(default `false`); a configured enrollment token, if presented, always takes
priority over self-service. This trades away any approval step: **any caller
that can reach the control-plane URL can register itself as a new Host.**
There is no rate limiting or throttling on this path by design — the intended
mitigation is network-perimeter control (firewall/reverse-proxy access to the
URL), not an in-process limiter. Only enable the flag on a deployment where
that network boundary is enforced.
Before the Host Agent's first unattended start, create the one-time local
operator account interactively:
```bash
uv run --package device-host-agent device-host-agent setup
```
This prompts for a username/password and writes a PBKDF2-hashed credential
file to `HOST_AGENT_LOCAL_ACCOUNT_PATH` (default
`tasks/host_local_account.json`), gating only this first-run bootstrap step —
it is not re-checked on subsequent unattended restarts. Running the daemon's
default command without a controlling terminal before this file exists fails
fast with a message naming the `setup` step, instead of hanging on a prompt
no one can answer.
Running under Compose, create the account once before `docker compose up`:
```bash
docker compose run --rm host-agent device-host-agent setup
docker compose up -d
```
## PostgreSQL Deployment
Start from `.env.example`, replace every `change-me-*` value, and keep the
@@ -154,6 +194,10 @@ renewal, and result operations for that host.
pool, or operate as a Host; they can only create one durable Host binding.
Use high-entropy values generated by the deployment secret manager.
`CLOUD_SELF_SERVICE_ENROLLMENT_ENABLED` (default `false`) additionally accepts
Host enrollment requests with no bearer token at all — see
[Self-Service Edge Enrollment](#self-service-edge-enrollment-zero-token).
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
+20 -6
View File
@@ -367,11 +367,21 @@ DeviceConfigStore("tasks/device_config.sqlite3").add(
PY
```
从云端管理员获取一枚未使用的一次性 enrollment token,然后启动:
首次启动前,先在交互式终端创建一次性本地操作账号(仅用于门禁"首次启动"这个
动作本身,后续无人值守重启不会再次要求):
```bash
uv run --package device-host-agent device-host-agent setup
```
`HOST_AGENT_CONTROL_PLANE_URL` 默认已固定为 `https://amcp.home.jerryyan.top`
仅在连接其他云端环境(如本地/测试用的 `cloud-api`)时才需要覆盖该变量。未设置
`HOST_AGENT_ENROLLMENT_TOKEN` 时,Host Agent 会直接向云端发起零 token 自助注册
(需要云端 `CLOUD_SELF_SERVICE_ENROLLMENT_ENABLED=true`);若仍持有云端管理员
签发的一次性 enrollment token,也可以继续设置 `HOST_AGENT_ENROLLMENT_TOKEN`
走原有的 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"
@@ -384,9 +394,13 @@ 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 不监听入站端口。若使用了一次性 enrollment token,成功后可以将其从
边缘环境移除;但必须保留并保护 `tasks/host_identity.json`
`tasks/device_config.sqlite3`;前者等同于 Host bearer credential。
零 token 自助注册意味着任何能访问该云端地址的设备都能自行注册成为 Host,没有
审批环节,也没有限流保护;这一取舍依赖网络边界(防火墙/反向代理)而非应用层
限制,仅应在受控网络中开启 `CLOUD_SELF_SERVICE_ENROLLMENT_ENABLED`
需要回滚到静态模式时,停止 Host Agent,由云端管理员配置匹配的静态 Host
credential,然后显式设置 `HOST_AGENT_HOST_ID``HOST_AGENT_TOKEN`。这两个变量