Files
agentic-mobile-control/docs/CLOUD_DEPLOYMENT.md
T
q792602257andClaude Opus 4.6 2169bb03d9 feat(cloud-console): task listing, attempt history, CORS, and console SPA
Implements the cloud-console OpenSpec change: adds GET /v1/tasks (filterable,
bounded pagination, tasks:read) and GET /v1/tasks/{id}/attempts (404 on unknown
task) to the platform SDK, with matching CloudClient methods and a closed-by-
default CLOUD_CONSOLE_CORS_ORIGINS allow-list wired through CloudControlConfig.
Ships an independent Vue 3 + Vite SPA at cloud-console/ that authenticates with
an operator-supplied bearer token held in sessionStorage, renders tasks with
attempt history, device pool, host registry, and the plugin registry with a
registration form.

Backend test suite: 438 passed (-m "not integration"); cloud-console typecheck
and production build both succeed. PostgreSQL-backed repository tests and
manual end-to-end verification remain pending external infrastructure.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-07-13 14:00:23 +08:00

14 KiB

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:

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.

$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:

$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.

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:

$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:

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 resulting .env file outside version control. The Compose stack contains PostgreSQL, the Cloud API, and one Host Agent:

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:

GET http://127.0.0.1:8001/health/ready

Deploying A Jenkins-Built Image

docker compose up --build above builds the image from source on the target host. For environments that pull a pre-built image instead, Jenkinsfile runs the pytest suite (-m "not integration"), builds the image from the same Dockerfile, smoke-tests both console scripts (device-cloud-api --help, device-host-agent --help), and pushes <REGISTRY>/<IMAGE_NAME>:<BUILD_NUMBER>-<git short sha> plus :latest to the configured registry.

compose.deploy.yaml is the same three-service stack as compose.yaml except cloud-api and host-agent reference image: instead of build:. Set REGISTRY, IMAGE_NAME, and IMAGE_TAG (see .env.example) to the tag Jenkins published, then deploy without a local build step:

docker compose -f compose.deploy.yaml pull
docker compose -f compose.deploy.yaml up -d
docker compose -f compose.deploy.yaml ps

For a deployment that manages processes outside Compose, apply migrations before starting the new Cloud API version:

$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.

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:

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

Cloud Console (Web UI)

The repository ships an independent Vue 3 + Vite SPA at cloud-console/ that renders the task queue/history, device pool, host registry, and plugin registry, and exposes the existing plugin-registration action. It authenticates the same way CloudClient does: by attaching a pre-issued bearer token to every request. There is no login or session system.

Provision an operator bearer token

Add a CLOUD_PUBLIC_CREDENTIALS_JSON entry whose scopes cover what the console operators need to do. The least-privilege set for read-only dashboards is tasks:read, pool:read, and plugins:read. Add tasks:submit only if operators should submit ad-hoc tasks from the same tab, and plugins:admin only if operators should register plugins:

[
  {
    "principal_id": "console-operator",
    "token": "replace-with-a-long-random-opaque-token",
    "scopes": ["tasks:read", "pool:read", "plugins:read", "plugins:admin"]
  }
]

Rotate the token the same way as any other credential entry: deploy the updated Cloud API credential set and instruct operators to paste the new token into the console. The console keeps the token only in browser sessionStorage for that tab; closing the tab discards it.

Configure the CORS allow-list

The Cloud API has no CORS middleware by default. Before a browser can call it cross-origin, set CLOUD_CONSOLE_CORS_ORIGINS to a comma-separated allow-list that includes the exact origin (scheme + host + port, no trailing slash) the operator's browser will load the console from:

# Allow a local Vite dev server
export CLOUD_CONSOLE_CORS_ORIGINS="http://127.0.0.1:5173"
# Or a deployed origin
export CLOUD_CONSOLE_CORS_ORIGINS="https://console.example.com"

Restart the Cloud API after changing this env. The middleware is added only when the allow-list is non-empty — existing deployments see no behavior change until an operator opts in. Blanket allow_origins=["*"] is intentionally not supported because every console request carries a bearer token.

Run the console

cd cloud-console
cp .env.example .env.local
# Edit .env.local if your Cloud API is not at http://127.0.0.1:8001
npm install
npm run dev

Vite prints a local URL (default http://127.0.0.1:5173). That exact origin must be in CLOUD_CONSOLE_CORS_ORIGINS on the Cloud API. Open the dev URL, paste the operator token, and the dashboards become available.

For a production build, run npm run build and serve the resulting dist/ behind any static file server or CDN, with VITE_CLOUD_API_BASE_URL baked in at build time. The deployed origin must be in CLOUD_CONSOLE_CORS_ORIGINS.

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:

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.

Operational Limitations

Run exactly one scheduler-enabled Cloud API process. SQLite supports only the documented single-control-plane development mode. PostgreSQL row locking makes assignment and claim transactions safe if requests overlap, but this release does not implement scheduler leader election or claim active-active scheduler operation. Starting multiple Cloud API replicas would start one scheduler loop per replica and is outside the supported deployment topology.

Device execution provides at-least-once side-effect semantics, not exactly-once semantics. A device action can succeed immediately before the Host Agent loses its lease or its result response, after which the control plane may retry the task. Lease renewal and cooperative stop checks prevent later interruptible actions where possible, but they cannot roll back an action already sent to a device or safely terminate an in-progress synchronous driver call.

Use bounded attempts, inspect task attempt and failure metadata, and design device workflows to tolerate repeated actions when the target operation allows it. Do not use this release for operations that require a transactional exactly-once guarantee across the cloud database and an external device.

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:

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. 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:

    uv run alembic -c packages/cloud-platform/cloud/migrations/alembic.ini downgrade -1
    
  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.