chore(openspec): add cloud control plane proposal

This commit is contained in:
2026-07-12 14:30:29 +08:00
parent c94b3efe87
commit 99ea1509ef
9 changed files with 511 additions and 0 deletions
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-07-12
@@ -0,0 +1,121 @@
## Context
The archived cloud-runtime capabilities produced useful domain modules and tests, but they stop at an in-process boundary. `DevicePool.sync_host_devices()` is never exposed to a host process, `create_cloud_router()` is not mounted by a runnable cloud application, assignment is invoked manually, and `TaskDispatcher` rejects any assignment owned by another host. The completed `ai-planner-runtime` change now provides a production Planner path behind Runtime configuration, so a remote host can execute a cloud-submitted goal through the same `TaskRunner` used locally.
This change follows `uv-workspace-packaging`: the existing cloud domain remains in the outer `device-cloud-platform` distribution, while two thin application members provide composition and process lifecycle. Transport and persistence adapters remain outside `core`, `driver`, `device`, and `tools`.
## Goals / Non-Goals
**Goals:**
- Provide a deployable Cloud Control Plane process that exposes the public SDK API and owns scheduling, leases, persistence, and operational health.
- Provide a Device Host Agent process that makes outbound authenticated requests, synchronizes local device state, claims work for its own host, and executes through existing Runtime/workflow entry points.
- Prevent concurrent assignment of one device and recover work after host or control-plane restarts.
- Support PostgreSQL with migrations for deployment and SQLite for local development/tests through one repository contract.
- Require scoped authentication by default for public and host APIs.
- Preserve existing domain dependency direction and reuse existing scheduler, pool, plugin, Runtime, and workflow behavior where compatible.
**Non-Goals:**
- Exactly-once execution of device side effects.
- Multi-tenancy, billing, organization/user lifecycle, or a cloud administration frontend.
- A separate message broker, distributed scheduler cluster, or active-active control-plane deployment.
- Inbound network access to device hosts, WebSocket transport, or direct cloud access to WDA/Appium.
- Remote installation of arbitrary plugin packages; plugin registration only addresses already installed implementations.
- Task cancellation and live interactive device streaming in this change.
## Decisions
### D1: Two thin application members compose the existing packages
`apps/cloud-api` provides the `device-cloud-api` project and an app factory/CLI entry point. It depends on `device-cloud-platform` and composes the repository, pool, scheduler, plugin registry, auth providers, routers, and lifecycle workers.
`apps/device-host-agent` provides the `device-host-agent` project and CLI entry point. It depends on both `device-agent-runtime` and `device-cloud-platform`, because it adapts cloud protocol models to the local `DeviceManager`, `TaskRunner`, and `WorkflowRunner`.
The inner Runtime does not import either application or the cloud package. Alternative considered: mount cloud routes into `api/rest.py`. Rejected because that would make every local Runtime deployment own cloud persistence and scheduler lifecycle and would reverse the intended optional outer composition.
### D2: Host Agents use authenticated outbound long-polling
Each Host Agent periodically sends a heartbeat containing its address metadata and complete device snapshot, then long-polls an internal endpoint for work assigned to its `host_id`. The control plane never initiates a connection to the host.
Long-polling is selected over WebSockets because it works through common NAT/firewall configurations, is straightforward to recover after connection loss, and does not require connection-affinity infrastructure. Poll timeout and retry backoff are configurable. The protocol remains versioned under `/internal/v1` so a later streaming transport can coexist.
### D3: Assignments use database leases and at-least-once delivery
The existing task states remain externally recognizable: `queued`, `assigned`, `dispatched`, `done`, and `failed`. Assignment additionally stores `attempt_count`, `lease_id`, `lease_expires_at`, and result/failure metadata.
The scheduler atomically selects a queued task and eligible device, increments the attempt, creates a random lease, and marks the task `assigned`. An authenticated Host Agent for the owning host atomically claims that assignment and transitions it to `dispatched`; periodic renewal extends the same lease. Terminal result reporting succeeds only for the active lease and is idempotent when the same result is retried.
An expired `assigned` or `dispatched` lease is requeued while attempts remain, otherwise it becomes `failed`. Active assignments reserve their devices independently of possibly stale host snapshots, preventing a subsequent scheduler iteration from assigning the same apparently-idle device.
This is at-least-once execution. A host can perform a device side effect immediately before losing its lease, after which the task may be retried. Alternative considered: claim exactly-once semantics. Rejected because the control plane cannot transactionally coordinate its database with external device effects. Mitigation is short leases with renewal, stopping execution after renewal failure where possible, bounded attempts, and clear attempt/result history.
### D4: Host execution reuses existing TaskRunner and WorkflowRunner
For a goal assignment, the Host Agent creates a `Task` for the assigned device and invokes a locally composed `TaskRunner`. Runtime configuration determines whether the completed AI Planner implementation is enabled and which provider/model it uses. For a workflow assignment, it loads and invokes the existing `WorkflowRunner` contract.
The Host Agent reports normalized terminal status, failure reason, and execution metadata to the control plane. It does not reimplement planning, tool execution, retries, perception, or driver access.
### D5: One repository contract supports SQLite and PostgreSQL
The cloud package defines a repository protocol/facade covering hosts, devices, tasks, leases, plugins, and transactional assignment operations. A SQLAlchemy 2 implementation supports SQLite URLs for local/test use and PostgreSQL URLs for deployment. Alembic owns versioned schema migrations.
Synchronous SQLAlchemy sessions match the existing synchronous FastAPI handlers and Runtime execution model. Database-specific locking is isolated in repository methods: PostgreSQL uses row locking/skip-locked where appropriate; SQLite uses transactions suitable for the documented single-control-plane development mode.
Alternative considered: retain direct `sqlite3` and add a separate psycopg implementation. Rejected because duplicating schema and transactional logic would make lease correctness diverge between development and deployment.
### D6: Authentication is required and scope-aware
Public `/v1` routes use bearer principals with explicit scopes such as `tasks:submit`, `tasks:read`, `pool:read`, and `plugins:admin`. Host `/internal/v1` routes use host credentials bound to exactly one `host_id`; a host cannot synchronize or claim work for another identity.
The existing `AuthProvider` extension point is retained and expanded to return scopes. The default production composition uses configured bearer credentials and fails startup when none are configured. Anonymous access is available only through an explicit insecure-development flag and must be rejected when the environment is marked production.
Plugin listing may use a read scope, while plugin registration requires `plugins:admin` because resolving an installed entry point can load code. Credentials are compared in constant time and must not be emitted in logs.
### D7: Control-plane lifecycle owns scheduler and lease-reaper loops
The cloud app factory uses FastAPI lifespan to validate configuration, apply or verify migrations according to deployment policy, initialize dependencies, start a periodic scheduler loop and lease-reaper loop, and stop both cleanly. Each loop catches and records iteration failures without terminating the process or silently skipping future work.
Only one control-plane scheduler is supported for SQLite. PostgreSQL transactional assignment prevents duplicate claims if multiple API processes are later run, but active-active scheduler leadership is not claimed by this change and deployment documentation defaults to one scheduler-enabled process.
### D8: Health separates process liveness from readiness
`/health/live` reports that the process event loop is running and does not require database access. `/health/ready` verifies configuration, database connectivity/schema version, and lifecycle worker state. Readiness fails when the control plane cannot safely accept tasks.
The Host Agent exposes local process health only when explicitly configured; its primary observable state is structured heartbeat and execution logging.
### D9: Result and retry operations are idempotent
Heartbeat/snapshot replacement, assignment claim, lease renewal, and result reporting use stable host/task/lease identifiers. Retrying the same terminal result for the same lease returns the recorded outcome. A stale or foreign lease receives a conflict response and cannot overwrite a newer attempt.
### D10: Operational logging uses correlation identifiers
Structured logs include task id, host id, device id, attempt, and lease id where applicable, while excluding bearer credentials, screenshots, UI trees, and text input. Public and internal requests accept or generate a correlation id propagated into task lifecycle logs.
## Risks / Trade-offs
- [Risk] At-least-once retry can repeat a device action. -> Mitigation: lease renewal, bounded attempts, stop-on-lease-loss behavior, and visible attempt history; do not claim exactly-once guarantees.
- [Risk] Host snapshots can lag actual device state. -> Mitigation: active assignments reserve devices in the database and heartbeat staleness continues to mark unreachable hosts.
- [Risk] PostgreSQL and SQLite transaction semantics differ. -> Mitigation: centralize operations behind repository contract tests run against both engines, with concurrency tests required for PostgreSQL.
- [Risk] A mis-scoped token could expose plugin loading. -> Mitigation: deny anonymous access by default and require the separate `plugins:admin` scope.
- [Risk] Background loops inside the API process complicate horizontal scaling. -> Mitigation: document a single scheduler-enabled deployment and keep lifecycle services injectable so they can move to a dedicated worker later.
- [Trade-off] Long-poll adds request overhead compared with a broker or WebSocket. -> Accepted for the first deployable version because it is operationally simpler and NAT-friendly.
- [Risk] The Host Agent may be configured without AI provider credentials. -> Mitigation: readiness/config diagnostics report Planner configuration, and task failures preserve explicit Runtime failure reasons.
## Migration Plan
1. Apply and verify `uv-workspace-packaging` so cloud and application dependency ownership is stable.
2. Introduce the repository contract, SQLAlchemy models, and baseline migration while preserving existing `CloudStore` behavior through an adapter.
3. Add lease/reservation semantics and test scheduler/reaper transactions on SQLite and PostgreSQL.
4. Add scoped auth providers and the internal Host Agent protocol endpoints.
5. Add the `cloud-api` composition/lifespan application and health endpoints.
6. Add the `device-host-agent` sync, long-poll, renewal, execution, and result-reporting loop.
7. Extend the public SDK task status surface and update the Python client authentication support.
8. Run local single-host, multi-host fake-driver, restart-recovery, PostgreSQL concurrency, and full non-integration regression tests.
9. Publish container/deployment configuration and an upgrade procedure that runs database migrations before the new control plane accepts traffic.
Rollback requires stopping Host Agents first, then the new cloud API. Database migrations must include tested downgrade paths until the release is accepted; queued tasks can be exported or left in the database for a forward redeploy. The previous in-process cloud modules remain usable only for development and do not consume the new remote assignments.
## Open Questions
- The initial implementation should select concrete default lease, poll, and retry durations based on integration tests; all remain configuration values rather than protocol constants.
@@ -0,0 +1,36 @@
## Why
The existing cloud package models hosts, pooled devices, queued tasks, plugins, and an SDK, but it has no runnable control-plane composition, no network path for hosts to synchronize devices, and explicitly rejects assignments targeting another process. This change turns those isolated modules into a deployable single-control-plane, multi-host execution loop while reusing the existing Runtime and completed AI Planner integration.
## What Changes
- Add independently runnable `cloud-api` and `device-host-agent` uv workspace applications on top of the packaging boundaries established by `uv-workspace-packaging`.
- Add a Cloud Control Plane FastAPI application that composes the existing pool, scheduler, store, plugin registry, and public `/v1` SDK router and runs scheduler/lease-maintenance loops through application lifespan.
- Add an authenticated Host Agent protocol for host registration, heartbeat/device snapshot synchronization, long-poll task claiming, lease renewal, and terminal result reporting.
- Replace the local-only remote-dispatch rejection with lease-backed execution by the Host Agent, which invokes the existing `TaskRunner` or `WorkflowRunner` on the owning device host.
- Add atomic assignment/claim semantics, active-device reservation, lease expiry, retry/requeue policy, idempotent result reporting, and process-restart recovery.
- Add PostgreSQL-backed cloud persistence with migrations for deployed environments while retaining SQLite for local development and tests behind the same store contract.
- Replace anonymous-by-default public API access with configured bearer-token authentication; use separately scoped credentials for public integrators and Host Agents.
- Add health/readiness endpoints, structured operational logging, and configuration validation suitable for container deployment.
- **BREAKING**: the platform SDK API no longer allows anonymous access by default; startup requires an explicit development override or configured credentials.
## Capabilities
### New Capabilities
- `cloud-control-plane`: Runnable cloud application composition, lifecycle workers, deployable persistence, health checks, and restart recovery.
- `host-agent-protocol`: Authenticated outbound Host Agent synchronization, leased task delivery, execution reporting, and failure recovery.
### Modified Capabilities
- `device-pool`: Host registrations and device snapshots become reachable through the authenticated Host Agent protocol while retaining staleness behavior.
- `task-scheduler`: Remote assignments become lease-backed, remotely executable work with atomic claim, reservation, expiry, and retry semantics instead of being rejected.
- `platform-sdk`: Public cloud routes require configured authentication by default and expose stable task lifecycle outcomes from the distributed execution loop.
## Impact
- Depends on completion of `uv-workspace-packaging` and adds workspace members under `apps/cloud-api` and `apps/device-host-agent`.
- Extends the extracted cloud platform package, cloud database schema, configuration, SDK models/client, and deployment documentation.
- Adds PostgreSQL driver and migration dependencies for cloud deployments; SQLite remains supported for local/test operation.
- Host Agent execution composes existing `DeviceManager`, `TaskRunner`, `WorkflowRunner`, and the production Planner selected by Runtime configuration; it does not move transport concerns into `core`, `driver`, `device`, or `tools`.
- Does not add multi-tenancy, billing, a user-management UI, arbitrary inbound connections to device hosts, or a separate message broker in this change.
@@ -0,0 +1,71 @@
## ADDED Requirements
### Requirement: Runnable Cloud Control Plane application
The system SHALL provide an independently runnable Cloud Control Plane application that composes the cloud repository, device pool, task scheduler, plugin registry, authentication providers, public platform router, internal Host Agent router, and lifecycle services without modifying the local Runtime API application.
#### Scenario: Start the cloud application
- **WHEN** an operator starts the Cloud Control Plane with valid configuration and an available database
- **THEN** the application exposes its versioned public, internal, and health routes and starts its configured lifecycle workers
#### Scenario: Local Runtime remains separately runnable
- **WHEN** an operator starts the existing local Runtime API without the cloud application
- **THEN** local Runtime routes operate without initializing cloud persistence, scheduling, or Host Agent services
### Requirement: Scheduler and lease maintenance run through application lifecycle
The Cloud Control Plane SHALL run configurable scheduler and expired-lease maintenance loops after application startup and SHALL stop them cleanly during shutdown.
#### Scenario: Queued work becomes assigned
- **WHEN** a queued task has an eligible device and the scheduler loop runs
- **THEN** the task receives an atomic assignment and becomes available to the owning Host Agent
#### Scenario: Lifecycle iteration fails transiently
- **WHEN** one scheduler or lease-maintenance iteration raises an operational error
- **THEN** the error is recorded and subsequent configured iterations continue rather than permanently terminating the worker
#### Scenario: Application shuts down
- **WHEN** the Cloud Control Plane receives a graceful shutdown signal
- **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.
#### 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
#### 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
### 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
#### 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
### Requirement: Restart recovery preserves durable work
The Cloud Control Plane SHALL recover persisted queued tasks and SHALL requeue or fail expired assigned/dispatched attempts according to retry policy after process restart.
#### Scenario: Restart with queued tasks
- **WHEN** the control plane restarts while tasks are queued
- **THEN** those tasks remain queued and are considered by later scheduler iterations
#### Scenario: Restart after a lease expires
- **WHEN** the control plane restarts and finds an assigned or dispatched task with an expired lease
- **THEN** lease maintenance requeues it when attempts remain or marks it failed when the retry limit is exhausted
### Requirement: Liveness, readiness, and safe operational logging
The application SHALL expose separate liveness and readiness endpoints and SHALL emit structured lifecycle logs with correlation identifiers while excluding credentials and sensitive device payloads.
#### Scenario: Database is unavailable
- **WHEN** the process is running but cannot reach its configured database
- **THEN** liveness succeeds and readiness fails
#### 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
@@ -0,0 +1,23 @@
## ADDED Requirements
### 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.
#### 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: 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
### 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.
#### Scenario: Two live hosts report the same device id
- **WHEN** host B reports a device id currently owned by non-stale host A
- **THEN** host B's conflicting snapshot is rejected with an ownership-conflict response and host A retains ownership
#### 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
@@ -0,0 +1,78 @@
## ADDED 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.
#### 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
#### 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: Host reports devices
- **WHEN** a Host Agent submits a valid heartbeat containing its current devices
- **THEN** the control plane updates the host's last-seen time and exposes the submitted devices through the aggregated pool
#### 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
### 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.
#### Scenario: Assigned work is available
- **WHEN** a Host Agent long-polls and an unclaimed assignment exists for its host
- **THEN** the control plane atomically transitions the assignment to dispatched and returns its task, device, attempt, lease, and execution payload
#### Scenario: No work becomes available
- **WHEN** no assignment for the host becomes available before the configured poll timeout
- **THEN** the endpoint returns a normal empty response and the Host Agent may poll again with backoff
### Requirement: Active execution renews its lease
The Host Agent SHALL renew the active assignment lease before expiry while execution continues, and SHALL treat loss or rejection of the lease as a stop condition for further planned actions where interruption is possible.
#### Scenario: Lease renewal succeeds
- **WHEN** the owning Host Agent renews an unexpired active lease
- **THEN** the control plane extends its expiry without changing the task attempt or device assignment
#### Scenario: Lease is stale or foreign
- **WHEN** a Host Agent attempts to renew an expired, replaced, or differently owned lease
- **THEN** the control plane returns a conflict and does not revive or alter the current attempt
### Requirement: Host execution composes existing Runtime and workflow runners
The Host Agent SHALL execute goal assignments through the existing `TaskRunner` and workflow assignments through the existing `WorkflowRunner`, using its local `DeviceManager` and Runtime configuration rather than reimplementing execution behavior.
#### Scenario: Execute a goal assignment
- **WHEN** the Host Agent claims a goal-based assignment for a connected local device
- **THEN** it runs a `Task` through the configured Runtime Planner/Executor loop and captures the terminal status and failure reason
#### Scenario: Execute a workflow assignment
- **WHEN** the Host Agent claims an assignment referencing an available workflow definition
- **THEN** it invokes the existing workflow runner for the assigned device and captures the terminal workflow outcome
### Requirement: Terminal result reporting is idempotent
The Host Agent SHALL report a terminal result using the task, attempt, and lease identifiers, and repeating the same report SHALL return the already recorded outcome without duplicating state transitions.
#### Scenario: Report a successful result
- **WHEN** the active lease owner reports successful completion
- **THEN** the control plane marks the scheduled task done, releases the device reservation, and records the result metadata
#### Scenario: Retry a result after response loss
- **WHEN** the Host Agent repeats the identical terminal report for an already completed active lease
- **THEN** the control plane returns the recorded terminal result without creating a new attempt or error
#### Scenario: Stale attempt reports after requeue
- **WHEN** an expired earlier attempt reports after a newer attempt has been created
- **THEN** the control plane rejects the stale report and preserves the newer attempt's state
### Requirement: Host operation requires no inbound cloud connection
The Host Agent SHALL perform synchronization, work retrieval, lease renewal, and result reporting using outbound requests only.
#### 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
@@ -0,0 +1,51 @@
## MODIFIED Requirements
### Requirement: Pluggable authentication hook with a safe default
The system SHALL evaluate every platform SDK route through a configurable scope-aware `AuthProvider` hook, and the deployable Cloud Control Plane SHALL reject anonymous access unless an explicit insecure-development override is enabled outside production.
#### Scenario: Production starts without configured credentials
- **WHEN** the Cloud Control Plane is configured as production without a usable public authentication provider or credentials
- **THEN** startup or readiness fails rather than exposing anonymous platform routes
#### Scenario: Explicit local anonymous override
- **WHEN** a non-production operator explicitly enables the insecure anonymous-development override
- **THEN** platform routes may use an anonymous principal and the application records that insecure mode is active
#### Scenario: Custom AuthProvider is honored
- **WHEN** a caller configures a custom `AuthProvider` that rejects a request or omits its required scope
- **THEN** the platform SDK route returns an authentication or authorization error without executing its handler operation
## ADDED Requirements
### Requirement: Public API operations enforce scopes
The public platform API SHALL require operation-specific scopes, including task submission, task reading, pool reading, plugin reading, and plugin administration.
#### Scenario: Submit token has task scope
- **WHEN** a principal with `tasks:submit` calls the task-submission endpoint
- **THEN** the request is authorized subject to normal task validation
#### Scenario: Non-admin token attempts plugin registration
- **WHEN** an authenticated principal without `plugins:admin` calls plugin registration
- **THEN** the API rejects the request before resolving or loading the plugin target
### Requirement: Distributed task status exposes attempt outcomes
The task-status API SHALL expose the existing lifecycle status and SHALL include non-secret assignment, attempt, and terminal failure metadata needed to diagnose distributed execution.
#### Scenario: Query an active remote task
- **WHEN** an authorized caller queries an assigned or dispatched task
- **THEN** the response includes its status, assigned host/device, current attempt number, and lease expiry without exposing the lease credential
#### Scenario: Query a failed remote task
- **WHEN** an authorized caller queries a task that exhausted retries or failed during Runtime execution
- **THEN** the response includes the terminal failure reason and attempt count
### Requirement: Python SDK supports authenticated requests
The Python `CloudClient` SHALL accept bearer credentials or an injectable authentication mechanism and SHALL apply authentication consistently to every public API method.
#### Scenario: Client configured with bearer token
- **WHEN** a caller constructs `CloudClient` with a valid bearer token and invokes a permitted method
- **THEN** the client sends the authorization credential and returns the corresponding API result
#### Scenario: Client receives authorization failure
- **WHEN** the configured credential is missing, invalid, or lacks the required scope
- **THEN** the client raises a typed HTTP/API error that preserves the response status without exposing the credential
@@ -0,0 +1,52 @@
## ADDED Requirements
### Requirement: Assignment and device reservation are atomic
The scheduler SHALL atomically bind a queued task to one eligible device, create a bounded lease attempt, and reserve that device so no other active task can be assigned to it.
#### Scenario: Scheduler assigns an idle device
- **WHEN** a queued task matches an idle pooled device with no active reservation
- **THEN** one transaction records the assigned task, owning host/device, incremented attempt, lease identifier, lease expiry, and device reservation
#### Scenario: Later scheduler iteration sees stale idle snapshot
- **WHEN** the host snapshot still reports a device idle but that device has an active assignment lease
- **THEN** the scheduler excludes the device from candidates for every other queued task
### Requirement: Host claim transitions assigned work to dispatched
The scheduler repository SHALL allow only the authenticated owning host to atomically claim an unexpired assigned attempt and transition it to `dispatched`.
#### Scenario: Owning host claims once
- **WHEN** the owning host requests available work and an unexpired assigned attempt exists
- **THEN** exactly one request receives the assignment and its status becomes dispatched
#### Scenario: Concurrent claims race
- **WHEN** multiple requests concurrently attempt to claim the same assignment
- **THEN** at most one request succeeds and every other request receives no assignment or a conflict
### Requirement: Expired attempts follow bounded retry policy
The system SHALL detect expired assigned or dispatched leases and SHALL either requeue the task with its reservation released or mark it failed when the configured attempt limit is reached.
#### Scenario: Lease expires with attempts remaining
- **WHEN** an active lease expires before a terminal result and the task has remaining attempts
- **THEN** the task returns to queued, the previous device reservation is released, and the expired attempt remains auditable
#### Scenario: Lease expires at attempt limit
- **WHEN** an active lease expires and the task has reached its maximum attempts
- **THEN** the task becomes failed with a lease-expiry reason and its device reservation is released
### Requirement: Terminal transitions validate the active lease
The system SHALL accept a `done` or `failed` result only from the current active task attempt and lease and SHALL make repeated identical terminal reports idempotent.
#### Scenario: Active lease reports completion
- **WHEN** the active lease owner reports a terminal result
- **THEN** the task transitions once to done or failed and releases its device reservation
#### Scenario: Superseded lease reports completion
- **WHEN** a result references a lease superseded by expiry and retry
- **THEN** the result is rejected and cannot overwrite the current task attempt
## REMOVED Requirements
### Requirement: Remote assignments are rejected explicitly, not silently ignored
**Reason**: Remote execution is now performed by the authenticated Host Agent that owns the assigned device, so rejecting every non-local assignment prevents the intended cloud execution loop.
**Migration**: Direct callers may continue using `TaskDispatcher` for local assignments, but cross-host work SHALL be delivered through the Host Agent lease/claim protocol instead of calling a dispatcher in the control-plane process.
@@ -0,0 +1,77 @@
## 1. Preconditions And Application Members
- [ ] 1.1 Complete and verify every task in `uv-workspace-packaging` before changing cloud behavior.
- [ ] 1.2 Add `apps/cloud-api` as the `device-cloud-api` workspace project with an app factory and CLI/server entry point.
- [ ] 1.3 Add `apps/device-host-agent` as the `device-host-agent` workspace project with a CLI entry point and explicit Runtime/cloud dependencies.
- [ ] 1.4 Add configuration models that validate environment, database URL, scheduler intervals, lease durations, retry limits, poll settings, host identity, and insecure-development overrides.
## 2. Repository Contract And Migrations
- [ ] 2.1 Define the cloud repository contract for hosts, device snapshots, plugins, tasks, attempts, leases, reservations, and transactional assignment operations.
- [ ] 2.2 Implement SQLAlchemy models and a repository adapter that preserves existing `CloudStore` observable behavior.
- [ ] 2.3 Add PostgreSQL and SQLite database URL support with engine/session lifecycle owned by the cloud application.
- [ ] 2.4 Add Alembic configuration and a baseline migration that preserves existing host, device, task, and plugin data while adding lease/attempt/result fields.
- [ ] 2.5 Add forward/downgrade migration tests and schema-version readiness checks.
- [ ] 2.6 Run repository contract tests against SQLite and PostgreSQL, including rollback and process-restart cases.
## 3. Lease-Backed Scheduling
- [ ] 3.1 Extend scheduled-task persistence with attempt count, lease id/expiry, terminal result, failure reason, and auditable attempt records.
- [ ] 3.2 Implement atomic queued-task assignment and device reservation while excluding devices with active assignments even when snapshots report idle.
- [ ] 3.3 Implement owning-host claim that atomically transitions one assigned attempt to dispatched under its active lease.
- [ ] 3.4 Implement lease renewal with host/task/attempt ownership validation and conflict responses for stale leases.
- [ ] 3.5 Implement idempotent terminal result recording and reservation release for active leases.
- [ ] 3.6 Implement expired-lease requeue/failure behavior with bounded attempts and preserved attempt history.
- [ ] 3.7 Add PostgreSQL concurrency tests proving one assignment/claim winner and SQLite tests documenting single-control-plane behavior.
## 4. Authentication And Authorization
- [ ] 4.1 Extend authenticated principals with scopes and implement constant-time configured bearer-token verification without logging credentials.
- [ ] 4.2 Add public scopes for task submission/read, pool read, plugin read, and plugin administration and enforce them on every `/v1` route.
- [ ] 4.3 Add host principals bound to one `host_id` and reject cross-host heartbeat, claim, renewal, or result operations.
- [ ] 4.4 Make missing production credentials a startup/readiness failure and permit anonymous mode only through the explicit non-production override.
- [ ] 4.5 Add authentication tests covering invalid tokens, missing scopes, host impersonation, plugin administration, and secret redaction.
## 5. Host Agent Internal API
- [ ] 5.1 Add versioned `/internal/v1` request/response models for heartbeat snapshots, long-poll claim, lease renewal, and terminal result reporting.
- [ ] 5.2 Add atomic heartbeat/snapshot validation and device ownership-conflict handling before delegating to `DevicePool`.
- [ ] 5.3 Add long-poll assignment delivery that returns at most one claimed task and produces a normal empty timeout response.
- [ ] 5.4 Add lease-renewal and idempotent terminal-result endpoints with typed stale-lease conflicts.
- [ ] 5.5 Add internal API integration tests for multi-host isolation, duplicate device ids, timeout behavior, stale attempts, and repeated result reports.
## 6. Cloud Control Plane Composition
- [ ] 6.1 Compose repository, pool, scheduler, plugin registry, public router, internal router, and auth providers in the cloud app factory.
- [ ] 6.2 Implement FastAPI lifespan startup/shutdown for configuration validation, database checks, scheduler loop, and lease-reaper loop.
- [ ] 6.3 Ensure lifecycle iteration failures are logged and retried without terminating later iterations.
- [ ] 6.4 Add `/health/live` and `/health/ready` with separate process, database/schema, and worker-state semantics.
- [ ] 6.5 Add structured correlation-aware logging for requests and task lifecycle events with sensitive payload redaction.
- [ ] 6.6 Add app-level tests for startup failures, readiness transitions, graceful shutdown, persisted queue recovery, and expired-lease recovery.
## 7. Device Host Agent Execution Loop
- [ ] 7.1 Implement an authenticated Host Agent client for heartbeat, long-poll claim, renewal, and result operations with bounded retry/backoff.
- [ ] 7.2 Build complete device snapshots from the local `DeviceManager` and synchronize them at the configured interval.
- [ ] 7.3 Compose local `TaskRunner` and `WorkflowRunner` factories without importing cloud concerns into Runtime-owned packages.
- [ ] 7.4 Execute goal assignments through the configured Runtime Planner/Executor and workflow assignments through the existing workflow runner.
- [ ] 7.5 Run lease renewal alongside active execution and stop further interruptible actions after confirmed lease loss.
- [ ] 7.6 Normalize and report successful/failed terminal outcomes, including Runtime failure reasons, with idempotent retries after response loss.
- [ ] 7.7 Implement graceful shutdown that stops polling, finishes or interrupts current work according to lease policy, and performs a final heartbeat when possible.
- [ ] 7.8 Add fake-driver end-to-end tests for one host, multiple hosts, NAT-style outbound-only operation, control-plane restart, Host Agent restart, and lease loss.
## 8. Public SDK And Operational Delivery
- [ ] 8.1 Extend public task status models/routes with attempt count, lease expiry metadata, and terminal failure details without exposing lease credentials.
- [ ] 8.2 Add bearer authentication and typed authorization errors to `CloudClient` while preserving injectable HTTP clients for tests.
- [ ] 8.3 Add container definitions and example environment configuration for the cloud API, PostgreSQL, and Host Agent without committing secrets.
- [ ] 8.4 Document local SQLite startup, deployed PostgreSQL migration/startup, credential/scopes setup, Runtime AI Planner configuration, and shutdown/rollback procedures.
- [ ] 8.5 Document the single scheduler-enabled control-plane limitation and the at-least-once device-side-effect trade-off.
## 9. Verification And Project Records
- [ ] 9.1 Run formatting, static checks, all non-integration tests, and targeted PostgreSQL integration/concurrency tests.
- [ ] 9.2 Run an end-to-end cloud submission through a Host Agent and fake device until the public SDK reports done and a failure case until it reports failed.
- [ ] 9.3 Verify existing local REST/MCP/console behavior and dependency-boundary tests remain unchanged.
- [ ] 9.4 Run OpenSpec validation for `cloud-control-plane-integration` and map automated tests to every new or modified scenario.
- [ ] 9.5 Update the project index, architecture/deployment documentation, and runtime maturity memory after implementation verification.