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:
@@ -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
|
||||
|
||||
@@ -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`。这两个变量
|
||||
|
||||
Reference in New Issue
Block a user