docs(cloud): add deployment runbook

This commit is contained in:
2026-07-13 07:58:24 +08:00
parent 90961bd0a4
commit e28705b0f2
6 changed files with 162 additions and 1 deletions
+7
View File
@@ -18,3 +18,10 @@ HOST_AGENT_POLL_TIMEOUT_SECONDS=20
HOST_AGENT_RETRY_BACKOFF_SECONDS=1
HOST_AGENT_MAX_RETRY_BACKOFF_SECONDS=30
HOST_AGENT_MAX_RETRY_ATTEMPTS=5
AI_PLANNER_ENABLED=false
AI_PLANNER_PROVIDER=anthropic
AI_PLANNER_MODEL=
AI_PLANNER_TIMEOUT_SECONDS=30
ANTHROPIC_API_KEY=
OPENAI_API_KEY=
+3
View File
@@ -59,6 +59,9 @@ invariants future changes must preserve are in
## Operator Guides
- [Cloud Control Plane deployment](docs/CLOUD_DEPLOYMENT.md): run local SQLite
or deployed PostgreSQL, configure credentials and Runtime AI planning, and
perform orderly shutdown or rollback.
- [macOS migration and real iPhone setup](docs/MACOS_IPHONE_SETUP.md): install
Xcode, Appium/XCUITest, sign WebDriverAgent, verify a real device, and start a
connected Runtime API.
+6
View File
@@ -63,6 +63,12 @@ services:
HOST_AGENT_RETRY_BACKOFF_SECONDS: ${HOST_AGENT_RETRY_BACKOFF_SECONDS:-1}
HOST_AGENT_MAX_RETRY_BACKOFF_SECONDS: ${HOST_AGENT_MAX_RETRY_BACKOFF_SECONDS:-30}
HOST_AGENT_MAX_RETRY_ATTEMPTS: ${HOST_AGENT_MAX_RETRY_ATTEMPTS:-5}
AI_PLANNER_ENABLED: ${AI_PLANNER_ENABLED:-false}
AI_PLANNER_PROVIDER: ${AI_PLANNER_PROVIDER:-anthropic}
AI_PLANNER_MODEL: ${AI_PLANNER_MODEL:-}
AI_PLANNER_TIMEOUT_SECONDS: ${AI_PLANNER_TIMEOUT_SECONDS:-30}
ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-}
OPENAI_API_KEY: ${OPENAI_API_KEY:-}
volumes:
- ${HOST_AGENT_TASKS_PATH:-./tasks}:/app/tasks
depends_on:
+142
View File
@@ -0,0 +1,142 @@
# Cloud Control Plane Deployment
This guide covers the deployable Cloud API and outbound Device Host Agent.
Run commands from the repository root after synchronizing the uv workspace:
```bash
uv sync --locked --all-packages
```
## Local SQLite
SQLite is intended for local development and tests with one Cloud API process.
Configure public and host credentials even in local mode because anonymous
development access cannot authorize a host identity.
```powershell
$env:CLOUD_ENVIRONMENT = "local"
$env:CLOUD_DATABASE_URL = "sqlite:///cloud/cloud.sqlite3"
$env:CLOUD_PUBLIC_CREDENTIALS_JSON = '[{"principal_id":"local-sdk","token":"replace-public-token","scopes":["tasks:submit","tasks:read","pool:read","plugins:read","plugins:admin"]}]'
$env:CLOUD_HOST_CREDENTIALS_JSON = '[{"principal_id":"local-host","token":"replace-host-token","scopes":[],"host_id":"host-local"}]'
uv run --package device-cloud-api device-cloud-api --host 127.0.0.1 --port 8001
```
In a second terminal, start the Host Agent with the matching host identity and
token:
```powershell
$env:HOST_AGENT_CONTROL_PLANE_URL = "http://127.0.0.1:8001"
$env:HOST_AGENT_HOST_ID = "host-local"
$env:HOST_AGENT_TOKEN = "replace-host-token"
uv run --package device-host-agent device-host-agent
```
At startup, the Host Agent loads device registrations from
`tasks/device_config.sqlite3`, the same `DeviceConfigStore` used by the local
Runtime console API. Register or update devices before starting the Host Agent,
then restart it to reload changes. In Compose,
`HOST_AGENT_TASKS_PATH` selects the host directory mounted at `/app/tasks`; it
defaults to `./tasks`.
The Host Agent only initiates outbound HTTP requests. It does not expose an
inbound port.
## PostgreSQL Deployment
Start from `.env.example`, replace every `change-me-*` value, and keep the
resulting `.env` file outside version control. The Compose stack contains
PostgreSQL, the Cloud API, and one Host Agent:
```bash
docker compose up --build -d
docker compose ps
```
The Cloud API container waits for PostgreSQL, runs the committed Alembic
migrations, then starts the API on port `8001`. Readiness is available at:
```text
GET http://127.0.0.1:8001/health/ready
```
For a deployment that manages processes outside Compose, apply migrations
before starting the new Cloud API version:
```powershell
$env:CLOUD_DATABASE_URL = "postgresql+psycopg://USER:PASSWORD@HOST:5432/device_cloud"
uv run alembic -c packages/cloud-platform/cloud/migrations/alembic.ini upgrade head
uv run --package device-cloud-api device-cloud-api --host 0.0.0.0 --port 8001
```
Production startup requires `CLOUD_ENVIRONMENT=production`, a current schema,
and at least one configured bearer credential.
## Credentials And Scopes
`CLOUD_PUBLIC_CREDENTIALS_JSON` is a JSON array of public API principals. Grant
only the scopes required by each integration:
- `tasks:submit`: submit tasks.
- `tasks:read`: read task status and failure metadata.
- `pool:read`: list hosts and devices.
- `plugins:read`: list installed plugin registrations.
- `plugins:admin`: register installed plugin entry points.
`CLOUD_HOST_CREDENTIALS_JSON` contains Host Agent principals. Every entry must
include exactly one `host_id`; its token is valid only for heartbeat, claim,
renewal, and result operations for that host.
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.
## Runtime AI Planner
The Host Agent reuses the local Runtime planner. AI planning is disabled by
default. Configure it in the Host Agent environment when goal assignments must
use a model:
```text
AI_PLANNER_ENABLED=true
AI_PLANNER_PROVIDER=anthropic
AI_PLANNER_MODEL=claude-sonnet-5
AI_PLANNER_TIMEOUT_SECONDS=30
ANTHROPIC_API_KEY=<secret manager reference>
```
For OpenAI, set `AI_PLANNER_PROVIDER=openai`, choose the deployed model through
`AI_PLANNER_MODEL`, and provide `OPENAI_API_KEY`. Provider credentials belong
only on the Host Agent; the Cloud API does not need them.
## Shutdown And Rollback
For a normal shutdown, stop Host Agents first so they stop polling, interrupt
later cooperative actions, finish terminal reporting where the lease remains
valid, and attempt a final heartbeat. Stop the Cloud API after Host Agents have
exited, then stop PostgreSQL only if the database itself is being maintained:
```bash
docker compose stop host-agent
docker compose stop cloud-api
docker compose stop postgres
```
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
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
with the restored Cloud API.
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.
@@ -66,7 +66,7 @@
- [x] 8.1 Extend public task status models/routes with attempt count, lease expiry metadata, and terminal failure details without exposing lease credentials.
- [x] 8.2 Add bearer authentication and typed authorization errors to `CloudClient` while preserving injectable HTTP clients for tests.
- [x] 8.3 Add container definitions and example environment configuration for the cloud API, PostgreSQL, and Host Agent without committing secrets.
- [ ] 8.4 Document local SQLite startup, deployed PostgreSQL migration/startup, credential/scopes setup, Runtime AI Planner configuration, and shutdown/rollback procedures.
- [x] 8.4 Document local SQLite startup, deployed PostgreSQL migration/startup, credential/scopes setup, Runtime AI Planner configuration, and shutdown/rollback procedures.
- [ ] 8.5 Document the single scheduler-enabled control-plane limitation and the at-least-once device-side-effect trade-off.
## 9. Verification And Project Records
+3
View File
@@ -28,6 +28,9 @@ def test_compose_defines_database_control_plane_and_outbound_host_agent() -> Non
assert services["host-agent"]["environment"][
"HOST_AGENT_CONTROL_PLANE_URL"
] == "http://cloud-api:8001"
assert services["host-agent"]["environment"]["AI_PLANNER_ENABLED"] == (
"${AI_PLANNER_ENABLED:-false}"
)
def test_container_uses_locked_workspace_install_and_migrations() -> None: