feat(cloud): add edge host enrollment
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-07-13
|
||||
@@ -0,0 +1,111 @@
|
||||
## 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_id` and `device_id` values.
|
||||
- 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_ID` and `HOST_AGENT_TOKEN` configuration 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
|
||||
|
||||
1. Deploy the schema migration while existing Cloud API and Host Agent versions are stopped or compatible with the additive schema.
|
||||
2. Deploy the Cloud API with `CLOUD_ENROLLMENT_TOKENS_JSON`; retain existing `CLOUD_HOST_CREDENTIALS_JSON` during migration.
|
||||
3. Verify readiness and enrollment API tests, then deploy new Host Agents.
|
||||
4. 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.
|
||||
5. 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.
|
||||
@@ -0,0 +1,31 @@
|
||||
## Why
|
||||
|
||||
Deploying a Device Host Agent currently requires an operator to pre-coordinate both `host_id` and every `device_id` with the Cloud Control Plane. This makes edge installation brittle and prevents a cloud-authoritative onboarding flow where a trusted control plane assigns stable identities after a host proves possession of a bootstrap credential.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Add an authenticated Host enrollment operation that consumes a configured one-time enrollment token and returns a cloud-generated `host_id`.
|
||||
- Let the enrolling Host Agent generate its long-lived bearer secret locally, so retries remain idempotent without the control plane storing or returning plaintext credentials.
|
||||
- Persist dynamically enrolled host credential digests in the cloud repository while retaining existing statically configured Host credentials for compatibility.
|
||||
- Add an authenticated device enrollment operation that maps a host-scoped opaque local device reference to a cloud-generated `device_id`.
|
||||
- Persist the cloud device mapping separately from transient heartbeat state and require heartbeat snapshots to use device IDs assigned to the authenticated host.
|
||||
- Add Host Agent bootstrap and identity persistence so an edge installation can start with only the control-plane URL and an enrollment token, recover safely after response loss, and reuse assigned IDs after restart.
|
||||
- Extend local device configuration with an optional cloud device mapping while keeping existing local Runtime device identifiers compatible.
|
||||
- Update deployment configuration and operator documentation for enrollment-token provisioning, identity-state protection, migration, revocation, and static-credential fallback.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `edge-host-enrollment`: Secure, idempotent Host and device onboarding with cloud-assigned identities and edge-persisted credential/mapping state.
|
||||
|
||||
### Modified Capabilities
|
||||
- `host-agent-protocol`: Permit bootstrap enrollment before the normal authenticated heartbeat/claim protocol and require enrolled cloud device IDs in later Host Agent traffic.
|
||||
- `device-pool`: Separate durable device enrollment identity from transient heartbeat state and accept only device identities assigned to the reporting host.
|
||||
- `cloud-control-plane`: Persist dynamic Host credentials and enrollment records through the shared PostgreSQL/SQLite repository and schema migration contract.
|
||||
|
||||
## Impact
|
||||
|
||||
- Cloud authentication, internal Host Agent API models/routes, repository protocol, SQLAlchemy models, migrations, and application composition.
|
||||
- Device Host Agent configuration, startup/bootstrap client, local identity persistence, device configuration mapping, heartbeat construction, and assignment execution lookup.
|
||||
- Environment variables, Compose wiring, deployment documentation, repository/HTTP/Host Agent tests, migration tests, and OpenSpec main capability contracts after archive.
|
||||
- Existing deployments using `CLOUD_HOST_CREDENTIALS_JSON` with explicit `HOST_AGENT_HOST_ID` and `HOST_AGENT_TOKEN` remain supported during migration.
|
||||
@@ -0,0 +1,44 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Deployment and local persistence modes share one contract
|
||||
The cloud repository SHALL support PostgreSQL for deployed operation and SQLite for local development and tests through the same behavioral contract, including hosts, dynamic Host credential bindings, device enrollments, pooled devices, tasks, leases, attempts, and plugins.
|
||||
|
||||
#### Scenario: Start with PostgreSQL
|
||||
- **WHEN** the configured database URL selects PostgreSQL and the schema is current
|
||||
- **THEN** the control plane uses PostgreSQL for cloud state, enrollment idempotency, authentication lookup, and transactional assignment operations
|
||||
|
||||
#### Scenario: Start in local SQLite mode
|
||||
- **WHEN** the configured database URL selects SQLite in a local or test environment
|
||||
- **THEN** the same repository contract, including Host and device enrollment, is available with the documented single-control-plane concurrency limitation
|
||||
|
||||
### Requirement: Cloud schema is versioned with migrations
|
||||
The system SHALL provide versioned forward and downgrade database migrations and SHALL refuse readiness when the database schema is incompatible with the running application.
|
||||
|
||||
#### Scenario: Upgrade an existing cloud database
|
||||
- **WHEN** an operator applies the enrollment release migration to a database at revision 0001
|
||||
- **THEN** existing hosts, pooled devices, tasks, attempts, and plugins are retained while nullable Host enrollment fields and durable device enrollment storage are added
|
||||
|
||||
#### Scenario: Downgrade the enrollment schema
|
||||
- **WHEN** an operator downgrades revision 0002 while no enrollment-capable application process is connected
|
||||
- **THEN** dynamic Host credential and device enrollment storage is removed while legacy cloud state from revision 0001 remains available
|
||||
|
||||
#### Scenario: Schema is behind at startup
|
||||
- **WHEN** the application connects to a database whose schema version is not accepted by the running release
|
||||
- **THEN** readiness fails with a diagnostic that does not expose credentials
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Dynamic Host authentication uses durable credential digests
|
||||
The Cloud Control Plane SHALL authenticate dynamically enrolled Host bearer credentials through indexed repository lookup of a cryptographic token digest and SHALL compose that lookup with existing configured credentials.
|
||||
|
||||
#### Scenario: Enrolled Host authenticates after Cloud API restart
|
||||
- **WHEN** a non-revoked enrolled Host presents its bearer credential after the Cloud API restarts
|
||||
- **THEN** the repository-backed authentication provider resolves the stored Host binding and authorizes only Host-scoped internal operations
|
||||
|
||||
#### Scenario: Dynamic Host credential calls a public route
|
||||
- **WHEN** a dynamically enrolled Host credential is presented to a public SDK operation requiring a scope
|
||||
- **THEN** the request is rejected for missing scope rather than inheriting public operator privileges
|
||||
|
||||
#### Scenario: Static credential deployment remains active
|
||||
- **WHEN** an operator continues to configure a Host-bound credential through the existing environment configuration
|
||||
- **THEN** that Host can use the existing operational protocol without performing bootstrap enrollment
|
||||
@@ -0,0 +1,40 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Authenticated network synchronization feeds the device pool
|
||||
The system SHALL expose an authenticated Host Agent operation that validates a host device snapshot against its authentication mode and durable device enrollments before delegating it to the existing device-pool synchronization behavior.
|
||||
|
||||
#### Scenario: Valid managed remote snapshot
|
||||
- **WHEN** an authenticated enrollment-managed Host submits a complete snapshot containing only non-revoked device IDs enrolled to that Host with matching driver types
|
||||
- **THEN** the device pool refreshes that Host and its devices with the same replacement and staleness semantics as an in-process synchronization call
|
||||
|
||||
#### Scenario: Valid legacy remote snapshot
|
||||
- **WHEN** an authenticated statically configured Host submits a valid complete snapshot
|
||||
- **THEN** the device pool preserves the existing compatible synchronization and ownership-conflict behavior
|
||||
|
||||
#### Scenario: Invalid snapshot is rejected atomically
|
||||
- **WHEN** a Host Agent snapshot contains invalid identifiers, unowned cloud device IDs, conflicting driver metadata, statuses, or capability tags
|
||||
- **THEN** the control plane rejects the snapshot without partially replacing the Host's previous pooled devices or heartbeat timestamp
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Durable device enrollment identity is independent of pool presence
|
||||
The cloud repository SHALL retain a Host-scoped device enrollment and its assigned `device_id` independently of whether the device appears in the Host's latest heartbeat snapshot.
|
||||
|
||||
#### Scenario: Enrolled device disconnects
|
||||
- **WHEN** a Host submits a heartbeat that no longer includes a previously enrolled device
|
||||
- **THEN** the pooled-device projection removes that device while its durable enrollment remains available for later idempotent re-enrollment
|
||||
|
||||
#### Scenario: Enrolled device reconnects
|
||||
- **WHEN** the Host later enrolls or reports the same local device reference again
|
||||
- **THEN** the control plane reuses the existing cloud `device_id`
|
||||
|
||||
### Requirement: Managed device identity cannot be claimed by another Host
|
||||
The device pool SHALL derive managed device ownership from durable enrollment rather than accepting a caller-selected cloud device ID.
|
||||
|
||||
#### Scenario: Host reports another Host's managed device
|
||||
- **WHEN** Host B includes a cloud device ID enrolled to Host A in its heartbeat
|
||||
- **THEN** the control plane rejects Host B's snapshot and Host A retains ownership
|
||||
|
||||
#### Scenario: Prior Host becomes stale
|
||||
- **WHEN** Host A becomes stale and Host B presents Host A's cloud device ID
|
||||
- **THEN** the control plane still rejects implicit takeover because managed device transfer requires a future explicit operation
|
||||
@@ -0,0 +1,64 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Host bootstrap uses a one-time enrollment credential
|
||||
The Cloud Control Plane SHALL provide a Host enrollment operation that authenticates a configured bootstrap credential, generates the authoritative `host_id`, and binds it to an edge-generated long-lived Host credential without storing the plaintext Host credential.
|
||||
|
||||
#### Scenario: New edge instance enrolls
|
||||
- **WHEN** an edge instance presents a valid unused enrollment token, a new instance identifier, and a high-entropy candidate Host credential
|
||||
- **THEN** the control plane returns a generated `host_id` and durably stores only the credential digest and enrollment binding
|
||||
|
||||
#### Scenario: Enrollment token is invalid
|
||||
- **WHEN** an edge instance presents an unknown enrollment token
|
||||
- **THEN** the control plane rejects enrollment without creating a Host identity or consuming any configured token
|
||||
|
||||
#### Scenario: Enrollment token is reused by another instance
|
||||
- **WHEN** a consumed enrollment token is presented with a different instance identifier
|
||||
- **THEN** the control plane returns a conflict and preserves the original Host binding
|
||||
|
||||
### Requirement: Host enrollment is idempotent across response loss
|
||||
The Host enrollment operation SHALL return the existing cloud Host identity when the same instance repeats enrollment with the same bootstrap-token binding and Host credential digest.
|
||||
|
||||
#### Scenario: Identical enrollment is retried
|
||||
- **WHEN** an edge instance repeats a successful enrollment request after losing the response
|
||||
- **THEN** the control plane returns the original `host_id` without creating another Host or rotating the submitted Host credential
|
||||
|
||||
#### Scenario: Instance retries with a different Host credential
|
||||
- **WHEN** an enrolled instance repeats enrollment with a different candidate Host credential
|
||||
- **THEN** the control plane rejects the request and leaves the original Host credential binding unchanged
|
||||
|
||||
### Requirement: Cloud assigns Host-scoped device identities
|
||||
An authenticated Host SHALL enroll each local device through an opaque Host-scoped local reference, and the control plane SHALL generate and durably return the `device_id` used by scheduling, heartbeat, leases, and assignment execution.
|
||||
|
||||
#### Scenario: Host enrolls a local device
|
||||
- **WHEN** an authenticated Host submits a previously unknown local device reference and valid driver metadata
|
||||
- **THEN** the control plane creates a device enrollment owned by that Host and returns a generated `device_id`
|
||||
|
||||
#### Scenario: Device enrollment is repeated
|
||||
- **WHEN** the same Host repeats enrollment for the same local device reference
|
||||
- **THEN** the control plane returns the existing `device_id` and does not create a duplicate enrollment
|
||||
|
||||
#### Scenario: Same local reference appears on another Host
|
||||
- **WHEN** a different Host enrolls an identical local device reference
|
||||
- **THEN** the control plane creates a distinct Host-scoped device enrollment rather than silently transferring ownership
|
||||
|
||||
### Requirement: Edge identity and device mappings survive restart
|
||||
The Host Agent SHALL persist its generated instance identifier, long-lived Host credential, assigned Host ID, and local-to-cloud device mappings outside process memory and SHALL reuse them on later starts.
|
||||
|
||||
#### Scenario: Host Agent restarts after enrollment
|
||||
- **WHEN** a managed Host Agent restarts with intact identity state
|
||||
- **THEN** it authenticates with the previously assigned `host_id` and credential without consuming another enrollment token
|
||||
|
||||
#### Scenario: Device mapping is missing but Host identity remains
|
||||
- **WHEN** a managed Host Agent has its Host identity but lacks a cached mapping for a configured local device
|
||||
- **THEN** it repeats idempotent device enrollment and restores the original cloud `device_id`
|
||||
|
||||
#### Scenario: Enrollment response is lost before Host ID persistence
|
||||
- **WHEN** the Host Agent persisted its candidate credential but did not persist the successful response
|
||||
- **THEN** its next start retries the identical enrollment request and recovers the original `host_id`
|
||||
|
||||
### Requirement: Enrolled Host credentials are revocable
|
||||
The cloud repository SHALL support revoking a dynamically enrolled Host credential, and authentication SHALL reject revoked credentials without deleting task or attempt history.
|
||||
|
||||
#### Scenario: Revoked Host sends heartbeat
|
||||
- **WHEN** a Host presents a credential whose enrollment has been revoked
|
||||
- **THEN** the internal API rejects the request and preserves existing cloud history for that Host and its devices
|
||||
@@ -0,0 +1,48 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Host identity is authenticated and bound to one host id
|
||||
Except for the bootstrap enrollment operation authenticated by a configured one-time enrollment credential, the internal Host Agent API SHALL require a host-scoped bearer principal and SHALL reject any request that attempts to act for a `host_id` different from the authenticated principal's bound host.
|
||||
|
||||
#### Scenario: Host authenticates as itself
|
||||
- **WHEN** a Host Agent presents valid static or dynamically enrolled credentials bound to its requested `host_id`
|
||||
- **THEN** the internal API authorizes permitted device enrollment, heartbeat, claim, renewal, and result operations
|
||||
|
||||
#### Scenario: Host attempts to impersonate another host
|
||||
- **WHEN** valid credentials bound to host A are used on a request for host B
|
||||
- **THEN** the internal API rejects the request without reading or modifying host B's state
|
||||
|
||||
#### Scenario: Unknown Host credential is presented
|
||||
- **WHEN** a caller presents a bearer credential that is neither statically configured nor bound to a non-revoked enrolled Host
|
||||
- **THEN** the internal API rejects the request without exposing whether a Host ID exists
|
||||
|
||||
### Requirement: Host Agent synchronizes heartbeat and complete device snapshots
|
||||
The Host Agent SHALL periodically submit its complete local device snapshot to the control plane, and the control plane SHALL atomically refresh the host heartbeat and replace only that host's pooled-device records. Enrollment-managed Hosts SHALL report the cloud device IDs assigned through device enrollment.
|
||||
|
||||
#### Scenario: Managed Host reports enrolled devices
|
||||
- **WHEN** an enrollment-managed Host Agent submits a valid heartbeat containing cloud device IDs enrolled to that Host
|
||||
- **THEN** the control plane updates the host's last-seen time and exposes the submitted devices through the aggregated pool
|
||||
|
||||
#### Scenario: Managed Host reports an unknown device ID
|
||||
- **WHEN** an enrollment-managed Host reports a device ID not enrolled to that Host or a driver type that conflicts with its enrollment
|
||||
- **THEN** the control plane rejects the entire snapshot without changing the previous heartbeat or pooled-device state
|
||||
|
||||
#### Scenario: Legacy Host reports devices
|
||||
- **WHEN** a statically configured legacy Host submits a valid device snapshot
|
||||
- **THEN** the control plane retains the existing compatible snapshot and ownership-conflict behavior
|
||||
|
||||
#### Scenario: Host reports no devices
|
||||
- **WHEN** a previously populated Host submits an empty device snapshot
|
||||
- **THEN** only that Host's prior pooled-device records are removed while durable enrollment records and devices owned by other Hosts remain unchanged
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Managed Host startup enrolls before normal protocol loops
|
||||
The Host Agent SHALL resolve its Host identity and cloud device mappings before starting heartbeat synchronization or assignment long-polling.
|
||||
|
||||
#### Scenario: New managed Host starts
|
||||
- **WHEN** the Host Agent has an enrollment token but no completed local Host identity
|
||||
- **THEN** it performs idempotent Host enrollment, enrolls configured local devices, constructs its `DeviceManager` with cloud device IDs, and only then starts heartbeat and claim loops
|
||||
|
||||
#### Scenario: Explicit legacy credentials are configured
|
||||
- **WHEN** both `HOST_AGENT_HOST_ID` and `HOST_AGENT_TOKEN` are explicitly supplied
|
||||
- **THEN** the Host Agent skips bootstrap enrollment and preserves existing local device ID behavior
|
||||
@@ -0,0 +1,40 @@
|
||||
## 1. Durable Enrollment Repository
|
||||
|
||||
- [x] 1.1 Add Host and device enrollment records plus repository protocol operations for atomic enrollment, credential lookup/revocation, device lookup, and managed-Host detection.
|
||||
- [x] 1.2 Extend SQLAlchemy models and add Alembic revision 0002 with forward/downgrade support, indexes, uniqueness constraints, and the updated schema head.
|
||||
- [x] 1.3 Implement atomic Host enrollment idempotency and one-time enrollment-token consumption in the SQL repository for SQLite and PostgreSQL.
|
||||
- [x] 1.4 Implement dynamic credential lookup/revocation and idempotent Host-scoped device enrollment in the SQL repository.
|
||||
- [x] 1.5 Add repository contract and migration tests covering success, retries, token reuse conflicts, revocation, per-Host device identity, and schema preservation.
|
||||
|
||||
## 2. Cloud Authentication And Internal API
|
||||
|
||||
- [x] 2.1 Parse and validate `CLOUD_ENROLLMENT_TOKENS_JSON` without exposing token values, while retaining existing configured bearer credentials.
|
||||
- [x] 2.2 Add enrollment-token verification, repository-backed dynamic Host authentication, and composed authentication providers.
|
||||
- [x] 2.3 Add Host/device enrollment request-response models and authenticated internal API routes with generated cloud IDs and conflict handling.
|
||||
- [x] 2.4 Require enrollment-managed Host heartbeat snapshots to contain only enrolled, non-revoked device IDs with matching driver types while preserving legacy snapshot behavior.
|
||||
- [x] 2.5 Compose enrollment services into the deployable Cloud API and add HTTP tests for bootstrap, idempotency, authorization, device enrollment, heartbeat validation, restart authentication, and public-scope isolation.
|
||||
|
||||
## 3. Edge Identity And Device Mapping Storage
|
||||
|
||||
- [x] 3.1 Add an atomic, restrictive-permission Host identity store that persists pending and completed enrollment state across response loss and restart.
|
||||
- [x] 3.2 Extend Host Agent configuration to support explicit legacy credentials or managed enrollment with configurable token and identity-state path.
|
||||
- [x] 3.3 Extend `DeviceConfigStore` with nullable cloud device mappings and backward-compatible schema upgrade/read/write operations.
|
||||
- [x] 3.4 Add focused tests for identity-state recovery, configuration mode validation, file secrecy behavior, and device mapping persistence.
|
||||
|
||||
## 4. Host Agent Enrollment Startup
|
||||
|
||||
- [x] 4.1 Extend the Host Agent client with synchronous bootstrap/device-enrollment operations that preserve existing retry and typed-error behavior.
|
||||
- [x] 4.2 Resolve managed Host identity before application construction, persisting the candidate secret before the request and assigned Host ID after success.
|
||||
- [x] 4.3 Enroll configured local devices before building the managed `DeviceManager`, persist mappings, and register drivers under cloud device IDs while leaving legacy mode unchanged.
|
||||
- [x] 4.4 Update Host Agent application and heartbeat/assignment tests for first enrollment, response-loss retry, restart reuse, device mapping recovery, legacy compatibility, and startup ordering.
|
||||
|
||||
## 5. Deployment And Operations
|
||||
|
||||
- [x] 5.1 Add enrollment-token and identity-path variables to example/Compose deployment configuration without overwriting existing registry/image customizations.
|
||||
- [x] 5.2 Update cloud and macOS edge deployment documentation for managed enrollment, secret/state handling, static fallback, revocation, migration, rollback, and outbound-only networking.
|
||||
|
||||
## 6. Verification
|
||||
|
||||
- [x] 6.1 Run formatting/static checks and focused repository, migration, Cloud API, Host Agent, deployment-contract, and end-to-end tests.
|
||||
- [x] 6.2 Run the complete non-integration workspace test suite and resolve regressions without modifying unrelated cloud-console work.
|
||||
- [x] 6.3 Run strict OpenSpec validation, review the final diff for credential leakage and architecture-boundary violations, and record verified limitations.
|
||||
Reference in New Issue
Block a user