Sync delta specs into main specs before archiving: modified cloud-control-plane, device-pool, and host-agent-protocol; created new edge-host-enrollment capability spec. openspec validate --specs reports 18/18 passing. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
11 KiB
Context
The current Cloud Control Plane authenticates Host Agents from a static environment-provided credential list. A Host Agent must start with a pre-agreed host_id, bearer token, and locally chosen device IDs; its first heartbeat implicitly creates the host and pooled-device rows. This works for controlled development but creates manual coordination, identity collisions, and unsafe retry behavior for repeatable edge deployment.
The change crosses Cloud API authentication, durable repository state, migrations, internal protocol models, Host Agent startup, and local device configuration. It must preserve the established outbound-only Host Agent protocol, PostgreSQL/SQLite parity, the existing static-credential deployment path, and the Runtime dependency direction: enrollment remains an outer cloud/application concern and does not enter core, driver, device, or tools.
Goals / Non-Goals
Goals:
- Allow a new edge installation to start with a control-plane URL and one-time enrollment token instead of pre-coordinated Host and device IDs.
- Make the Cloud Control Plane authoritative for generated
host_idanddevice_idvalues. - Keep enrollment retries idempotent across response loss and process restart without persisting plaintext long-lived Host secrets in cloud storage.
- Persist Host credentials and device enrollment mappings through the existing repository abstraction for both PostgreSQL and SQLite.
- Let dynamically enrolled Host Agents authenticate normal heartbeat, claim, renew, and result requests through the existing Host-bound authorization contract.
- Preserve explicit
HOST_AGENT_HOST_IDandHOST_AGENT_TOKENconfiguration as a compatible legacy mode. - Protect existing local Runtime device IDs by storing the cloud mapping separately and translating managed assignments at Host Agent composition time.
Non-Goals:
- Automatic discovery of iPhones from USB/Appium; operators still create local device configuration records.
- A public enrollment-management UI, tenant model, certificate authority, mTLS, or remote Host installation service.
- Silent transfer of an enrolled device between Hosts. Moving a phone creates a new Host-scoped device enrollment unless a future explicit transfer capability is added.
- Distribution of Appium/WDA signing configuration, LLM credentials, or workflow definitions from the cloud.
- Removal of static Host credentials in this change.
Decisions
D1. Separate bootstrap enrollment from the operational Host protocol
Add POST /internal/v1/enrollments authenticated by a configured enrollment token. The endpoint is the only internal route that does not require an existing Host-bound principal. Heartbeat, claim, renewal, result reporting, and device enrollment continue to require a Host-bound bearer credential.
Alternative considered: allow an unknown Host to create itself through heartbeat. Rejected because heartbeat would mix bootstrap authentication, identity creation, and state replacement, and an interrupted first heartbeat would be difficult to distinguish from credential misuse.
D2. The edge generates the long-lived Host secret; the cloud generates the Host ID
Before its first enrollment request, the Host Agent generates and durably stores an agent_instance_id and a high-entropy bearer token. The enrollment request sends the candidate Host token over TLS while authenticating with the one-time enrollment token. The cloud generates host_id, stores only the Host token digest, and binds it to the instance.
This makes response-loss retries safe: the same instance can resend the same candidate secret, and the server can verify its digest and return the same host_id. The cloud never needs to retain or re-return plaintext Host credentials.
Alternative considered: cloud-generated Host secret returned once. Rejected because a lost response would require either unrecoverable enrollment or plaintext/encrypted secret recovery state on the server.
D3. Enrollment tokens are configured but consumption is durable
CLOUD_ENROLLMENT_TOKENS_JSON supplies high-entropy bootstrap tokens to the Cloud API. Authentication compares token digests without logging token values. The Host enrollment transaction records the enrollment-token digest used by an instance, with a uniqueness constraint so a token cannot enroll a second instance after restart.
An identical retry for the same agent_instance_id and Host credential digest returns the existing Host identity. Reuse for another instance, or retry with a different candidate Host token, returns a conflict.
D4. Dynamic Host credentials compose with existing configured credentials
Add a repository-backed Host AuthProvider that hashes the presented bearer token and resolves a non-revoked enrolled Host. Compose it after the existing configured bearer provider. Public SDK scopes remain configuration-driven; dynamically enrolled credentials receive only a Host-bound principal and therefore cannot call public operator APIs.
Static Host credentials retain current behavior and local device IDs. This limits migration risk and permits staged deployment.
D5. Durable device enrollment is separate from transient pool state
Add a device_enrollments table with cloud-generated device_id, owning host_id, opaque local_device_id, driver metadata, enrollment timestamps, and revocation state. Enforce uniqueness for both device_id and (host_id, local_device_id).
POST /internal/v1/hosts/{host_id}/devices/enroll is Host-authenticated. Repeating the same Host/local-device pair returns the same cloud ID and may refresh non-identity metadata. A different Host receives a different ID even if it reports the same physical phone.
Pooled-device rows remain replaceable heartbeat projections. For enrollment-managed Hosts, a heartbeat may report only non-revoked device IDs enrolled to that Host, with matching driver type. Legacy statically authenticated Hosts retain the existing snapshot behavior.
D6. Local Runtime identity and cloud identity remain distinct
Extend DeviceConfigStore records with nullable cloud_device_id. Existing device_id remains the local Runtime/configuration key and is used as the opaque enrollment reference; no raw iPhone UDID must be sent solely for enrollment.
In managed mode the Host Agent enrolls every configured device before constructing its DeviceManager, persists returned mappings, and registers drivers under the cloud device_id. Assignment execution therefore continues to use the existing DeviceManager and tool contracts without adding translation logic to Runtime layers. Legacy mode registers the existing local IDs unchanged.
D7. Host identity state is written before and after network enrollment
Use an edge-local identity file under the mounted tasks/state path. Before the first request, atomically persist the generated instance ID and Host token; after a successful response, atomically add the assigned Host ID. Restrict file permissions to the current user where the operating system supports it. Environment-provided explicit Host credentials take precedence and do not overwrite managed identity state.
This state is a secret and must be backed up or deliberately revoked before replacement. Losing it causes a new enrollment rather than unsafe guessing of a prior identity.
D8. Schema revision 0002 carries enrollment state
Add a forward/downgrade Alembic revision after 0001_cloud_repository. Extend host_registrations with nullable instance, credential, enrollment-token, display-name, enrolled-at, and revoked-at fields, plus required uniqueness/indexes. Add device_enrollments. Fresh local/test databases continue to use SQLAlchemy metadata creation; production readiness requires revision 0002.
The repository owns atomic Host enrollment, credential lookup/revocation, device enrollment/lookup, and managed-snapshot validation queries. HTTP handlers do not assemble multi-step uniqueness checks outside the transaction.
Risks / Trade-offs
- [Identity file theft permits Host impersonation] -> Store only on the edge host, set restrictive permissions, keep it outside images/source control, use HTTPS, and support repository-level revocation.
- [Configured enrollment tokens remain present after consumption] -> Persist token-digest consumption with a unique constraint so application restart or unchanged environment configuration cannot reuse them.
- [Static and managed modes increase transitional complexity] -> Make the mode explicit in resolved Host Agent configuration and cover both paths with contract tests; do not silently convert a static deployment.
- [Device mapping is lost locally] -> Re-enrollment is idempotent by
(host_id, local_device_id)and reconstructs the same cloud ID when Host identity state remains available. - [Phone movement creates multiple historical device IDs] -> Treat enrollment as a Host attachment for this release; require future explicit transfer semantics before preserving identity across Hosts.
- [Database-backed auth adds a query to Host requests] -> Query by an indexed SHA-256 digest. Optimize with bounded caching only after measurement; revocation correctness takes priority.
- [Rollback cannot authenticate newly enrolled Hosts] -> Keep static credential support and require operators to provision temporary static credentials before rolling back application/schema.
Migration Plan
- Deploy the schema migration while existing Cloud API and Host Agent versions are stopped or compatible with the additive schema.
- Deploy the Cloud API with
CLOUD_ENROLLMENT_TOKENS_JSON; retain existingCLOUD_HOST_CREDENTIALS_JSONduring migration. - Verify readiness and enrollment API tests, then deploy new Host Agents.
- Existing explicitly configured Host Agents continue in legacy mode. New edge installations use enrollment mode and persist identity/device mappings under their tasks/state volume.
- After all managed Hosts are verified, rotate or remove no-longer-required static Host credentials independently; public SDK credentials remain configured.
Rollback requires stopping managed Host Agents, provisioning static Host credentials and IDs for any Host that must continue operating on the previous release, then downgrading the schema to revision 0001. The downgrade removes dynamic credentials and durable device enrollments but leaves legacy hosts, pooled devices, tasks, attempts, and plugins intact.
Open Questions
No blocking questions remain for the first implementation. Certificate-based Host identity, enrollment-token administration APIs, and cross-Host device transfer are intentionally deferred.
Verification Notes
- Repository, migration, authentication, Cloud API, Host Agent, deployment-contract, and existing end-to-end tests pass in the local SQLite/non-integration environment.
- PostgreSQL behavior is covered by the shared parameterized repository contract but was not executed without
TEST_POSTGRES_URL. - No real macOS/iPhone/Appium environment was available to validate first enrollment against physical hardware.
- Host revocation is implemented at the repository/operations layer; a public administrative revocation API and UI remain out of scope.
- Device discovery remains explicit local configuration, and device identity transfer between Hosts remains intentionally unsupported.