From 989fdbf878689226fbe8ffcfbf00fa4cf9eaef85 Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Tue, 14 Jul 2026 21:01:47 +0800 Subject: [PATCH] chore(openspec): archive edge-host-enrollment 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 --- .../.openspec.yaml | 0 .../design.md | 0 .../proposal.md | 0 .../specs/cloud-control-plane/spec.md | 0 .../specs/device-pool/spec.md | 0 .../specs/edge-host-enrollment/spec.md | 0 .../specs/host-agent-protocol/spec.md | 0 .../2026-07-14-edge-host-enrollment}/tasks.md | 0 openspec/specs/cloud-control-plane/spec.md | 29 ++++++-- openspec/specs/device-pool/spec.md | 38 ++++++++-- openspec/specs/edge-host-enrollment/spec.md | 71 +++++++++++++++++++ openspec/specs/host-agent-protocol/spec.md | 41 ++++++++--- 12 files changed, 159 insertions(+), 20 deletions(-) rename openspec/changes/{edge-host-enrollment => archive/2026-07-14-edge-host-enrollment}/.openspec.yaml (100%) rename openspec/changes/{edge-host-enrollment => archive/2026-07-14-edge-host-enrollment}/design.md (100%) rename openspec/changes/{edge-host-enrollment => archive/2026-07-14-edge-host-enrollment}/proposal.md (100%) rename openspec/changes/{edge-host-enrollment => archive/2026-07-14-edge-host-enrollment}/specs/cloud-control-plane/spec.md (100%) rename openspec/changes/{edge-host-enrollment => archive/2026-07-14-edge-host-enrollment}/specs/device-pool/spec.md (100%) rename openspec/changes/{edge-host-enrollment => archive/2026-07-14-edge-host-enrollment}/specs/edge-host-enrollment/spec.md (100%) rename openspec/changes/{edge-host-enrollment => archive/2026-07-14-edge-host-enrollment}/specs/host-agent-protocol/spec.md (100%) rename openspec/changes/{edge-host-enrollment => archive/2026-07-14-edge-host-enrollment}/tasks.md (100%) create mode 100644 openspec/specs/edge-host-enrollment/spec.md diff --git a/openspec/changes/edge-host-enrollment/.openspec.yaml b/openspec/changes/archive/2026-07-14-edge-host-enrollment/.openspec.yaml similarity index 100% rename from openspec/changes/edge-host-enrollment/.openspec.yaml rename to openspec/changes/archive/2026-07-14-edge-host-enrollment/.openspec.yaml diff --git a/openspec/changes/edge-host-enrollment/design.md b/openspec/changes/archive/2026-07-14-edge-host-enrollment/design.md similarity index 100% rename from openspec/changes/edge-host-enrollment/design.md rename to openspec/changes/archive/2026-07-14-edge-host-enrollment/design.md diff --git a/openspec/changes/edge-host-enrollment/proposal.md b/openspec/changes/archive/2026-07-14-edge-host-enrollment/proposal.md similarity index 100% rename from openspec/changes/edge-host-enrollment/proposal.md rename to openspec/changes/archive/2026-07-14-edge-host-enrollment/proposal.md diff --git a/openspec/changes/edge-host-enrollment/specs/cloud-control-plane/spec.md b/openspec/changes/archive/2026-07-14-edge-host-enrollment/specs/cloud-control-plane/spec.md similarity index 100% rename from openspec/changes/edge-host-enrollment/specs/cloud-control-plane/spec.md rename to openspec/changes/archive/2026-07-14-edge-host-enrollment/specs/cloud-control-plane/spec.md diff --git a/openspec/changes/edge-host-enrollment/specs/device-pool/spec.md b/openspec/changes/archive/2026-07-14-edge-host-enrollment/specs/device-pool/spec.md similarity index 100% rename from openspec/changes/edge-host-enrollment/specs/device-pool/spec.md rename to openspec/changes/archive/2026-07-14-edge-host-enrollment/specs/device-pool/spec.md diff --git a/openspec/changes/edge-host-enrollment/specs/edge-host-enrollment/spec.md b/openspec/changes/archive/2026-07-14-edge-host-enrollment/specs/edge-host-enrollment/spec.md similarity index 100% rename from openspec/changes/edge-host-enrollment/specs/edge-host-enrollment/spec.md rename to openspec/changes/archive/2026-07-14-edge-host-enrollment/specs/edge-host-enrollment/spec.md diff --git a/openspec/changes/edge-host-enrollment/specs/host-agent-protocol/spec.md b/openspec/changes/archive/2026-07-14-edge-host-enrollment/specs/host-agent-protocol/spec.md similarity index 100% rename from openspec/changes/edge-host-enrollment/specs/host-agent-protocol/spec.md rename to openspec/changes/archive/2026-07-14-edge-host-enrollment/specs/host-agent-protocol/spec.md diff --git a/openspec/changes/edge-host-enrollment/tasks.md b/openspec/changes/archive/2026-07-14-edge-host-enrollment/tasks.md similarity index 100% rename from openspec/changes/edge-host-enrollment/tasks.md rename to openspec/changes/archive/2026-07-14-edge-host-enrollment/tasks.md diff --git a/openspec/specs/cloud-control-plane/spec.md b/openspec/specs/cloud-control-plane/spec.md index 1f1e8ec..8a3a4b4 100644 --- a/openspec/specs/cloud-control-plane/spec.md +++ b/openspec/specs/cloud-control-plane/spec.md @@ -33,22 +33,26 @@ The Cloud Control Plane SHALL run configurable scheduler and expired-lease maint - **THEN** its lifecycle workers stop accepting new iterations and terminate without abandoning an in-process database transaction ### 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, devices, tasks, leases, attempts, and plugins. +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 all cloud state and transactional assignment operations +- **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 is available with the documented single-control-plane concurrency limitation +- **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 release's migrations to a supported previous schema -- **THEN** existing hosts, devices, tasks, and plugins are retained and the new lease fields become available +- **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 @@ -75,3 +79,18 @@ The application SHALL expose separate liveness and readiness endpoints and SHALL #### Scenario: Task lifecycle is logged - **WHEN** a task is assigned, claimed, renewed, completed, retried, or failed - **THEN** the log event includes available task, host, device, attempt, lease, and correlation identifiers but excludes bearer tokens, screenshots, UI trees, and typed text + +### 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 diff --git a/openspec/specs/device-pool/spec.md b/openspec/specs/device-pool/spec.md index b975780..bc279c1 100644 --- a/openspec/specs/device-pool/spec.md +++ b/openspec/specs/device-pool/spec.md @@ -48,15 +48,19 @@ The system SHALL allow looking up a single `PooledDevice` by `device_id` regardl - **THEN** the pool returns a not-found result (e.g. `None`) rather than raising an unhandled exception ### Requirement: Authenticated network synchronization feeds the device pool -The system SHALL expose an authenticated Host Agent operation that validates a host device snapshot and delegates it to the existing device-pool synchronization behavior. +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 remote snapshot -- **WHEN** an authenticated Host Agent submits a valid complete snapshot for its bound host id -- **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 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 device identifiers, driver types, statuses, or capability tags -- **THEN** the control plane rejects the snapshot without partially replacing the host's previously stored devices +- **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 ### Requirement: Device identity ownership conflicts are explicit The device pool SHALL reject a snapshot that claims a `device_id` actively owned by a different non-stale host, rather than silently transferring ownership. @@ -68,3 +72,25 @@ The device pool SHALL reject a snapshot that claims a `device_id` actively owned #### Scenario: Previous owner is stale - **WHEN** a configured ownership-recovery policy permits takeover and the prior owning host is stale beyond the recovery threshold - **THEN** the new host may claim the device id and the ownership transition is recorded + +### 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 diff --git a/openspec/specs/edge-host-enrollment/spec.md b/openspec/specs/edge-host-enrollment/spec.md new file mode 100644 index 0000000..7a78f21 --- /dev/null +++ b/openspec/specs/edge-host-enrollment/spec.md @@ -0,0 +1,71 @@ +# edge-host-enrollment Specification + +## Purpose +Define how an edge Host Agent bootstraps its identity, enrolls local devices +with cloud-assigned identifiers, and maintains durable credentials and device +mappings across restarts without requiring static configuration. + +## 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 diff --git a/openspec/specs/host-agent-protocol/spec.md b/openspec/specs/host-agent-protocol/spec.md index ee05317..18256e1 100644 --- a/openspec/specs/host-agent-protocol/spec.md +++ b/openspec/specs/host-agent-protocol/spec.md @@ -7,26 +7,38 @@ synchronize devices, claim leased work, execute it locally, and report results. ## Requirements ### Requirement: Host identity is authenticated and bound to one host id -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. +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 credentials bound to its requested `host_id` -- **THEN** the internal API authorizes permitted heartbeat, claim, renewal, and result operations +- **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 -### 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. +#### 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 -#### Scenario: Host reports devices -- **WHEN** a Host Agent submits a valid heartbeat containing its current devices +### 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 device records are removed while devices owned by other hosts remain unchanged +- **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 ### Requirement: Host Agent receives work through outbound long-polling The Host Agent SHALL request assigned work for its own host through a configurable long-poll endpoint, and the control plane SHALL return at most one atomically claimable assignment per response or an empty timeout response. @@ -82,3 +94,14 @@ The Host Agent SHALL perform synchronization, work retrieval, lease renewal, and #### Scenario: Host is behind NAT - **WHEN** the Host Agent can reach the control-plane URL but exposes no inbound listener - **THEN** it can register devices and execute cloud assignments through the complete protocol + +### 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