feat(cloud): add edge host enrollment

This commit is contained in:
2026-07-13 13:54:16 +08:00
parent cd56facbbf
commit e61dcca801
40 changed files with 2302 additions and 48 deletions
@@ -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