feat(cloud): remove static credentials and add host console
Tests / Test No test results found

This commit is contained in:
2026-07-13 19:45:53 +08:00
parent efeb3eb926
commit c162c2501b
61 changed files with 3118 additions and 1221 deletions
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-07-13
@@ -0,0 +1,118 @@
## Context
The Cloud API currently chains configured bearer credentials, browser user
sessions, and repository-backed Host credentials. Three JSON environment
variables populate the first provider and a second provider protects first Host
enrollment. The deployment image already contains the Console bundle, but
Compose separately supplies its fixed location. The Console also retains a
bearer-token compatibility screen and an administrator-only Users view.
The intended deployment has one trusted Cloud endpoint. A human operator signs
in with a Cloud account created or recovered through `device-cloud-admin`; a
new Host Agent registers itself once, generates its own high-entropy Host
secret, and persists the returned Host identity locally. Subsequent Host
requests continue to authenticate against the stored digest.
## Goals / Non-Goals
**Goals:**
- Start Cloud production without any static credential JSON or startup
requirement for one.
- Authorize browser operations by the existing user session and preserve its
role, CSRF, expiry, audit, and interactive CLI controls.
- Accept direct first Host registration, then strictly bind all later Host
operations to the registered Host identity and persisted secret.
- Remove obsolete Console and Compose configuration paths while keeping the
Jenkins-built Console available at `/console/`.
**Non-Goals:**
- No personal-access-token, service-account, or replacement SDK credential
scheme in this change.
- No user-directory view in the Console. Account provisioning and recovery
remain administration-CLI operations.
- No Host enrollment approval, rate limiting, or multi-tenant trust policy.
Network reachability to the trusted Cloud endpoint remains the boundary
explicitly chosen for this deployment.
- No change to durable task, lease, device, or user database schema.
## Decisions
### D1. Remove configured bearer providers instead of leaving empty compatibility configuration
`CloudControlConfig` will no longer parse the three credential JSON variables
or require credentials in production. The Cloud application will compose public
authorization from `UserSessionAuthProvider` only, while Host operational
authorization remains `RepositoryHostAuthProvider`. An empty production user
table is allowed so an administrator can execute the interactive bootstrap CLI
after migrations complete.
Leaving the variables optional was rejected because it preserves two
authentication modes and makes operators believe a deployed secret is needed.
Adding user-issued API tokens was rejected because the requested outcome is no
manual token configuration and it introduces a separate credential lifecycle.
### D2. Direct enrollment is a single unauthenticated bootstrap operation
The enrollment endpoint accepts a fresh Host's generated instance identifier
and candidate secret without an `Authorization` header. It stores only the
candidate secret digest and returns a generated `host_id`; the Host persists
that result atomically. The configured enrollment-token provider and static
Host configuration are removed. Heartbeat, device enrollment, claim, renewal,
and result endpoints remain Host-bound and require the persisted bearer secret.
An opt-in enrollment flag was rejected because the target deployment always
uses direct trust and the flag is another operational switch that can silently
block first-run setup. Keeping token enrollment as a fallback was rejected for
the same reason as D1.
### D3. Keep user administration off the Console, not out of the control plane
The Console retains login, logout, current-account display, and password
change. It removes bearer-token controls, token storage, and all Users routes,
navigation, client calls, and components. The authenticated user administration
API and `device-cloud-admin` CLI remain available for controlled provisioning
and recovery; the CLI is the documented initial-admin path.
Removing the administration API entirely was rejected because it would make
recovery tooling less complete and is unrelated to eliminating deployment
tokens.
### D4. Treat packaged static assets as an image contract
The Dockerfile sets `CLOUD_CONSOLE_STATIC_DIR=/app/console-static` after copying
the built SPA. Compose does not repeat this invariant. Application code keeps
the variable optional for source-based local development, where no bundled
directory exists.
## Risks / Trade-offs
- **[Risk] Any network caller can create a Host identity.** → The accepted
mitigation is reverse-proxy/firewall control of the trusted Cloud endpoint;
every operation after enrollment still requires the Host-generated secret.
- **[Risk] Existing integrations using static bearer tokens stop working.** →
This is intentional; create Cloud user accounts before deployment and migrate
human workflows to session login.
- **[Risk] An upgrade before an administrator exists leaves no human API
access.** → Run the documented interactive `device-cloud-admin users create`
command immediately after migration and before exposing the Console.
- **[Risk] Existing static or token-enrolled Hosts cannot rely on removed
configuration after upgrade.** → Preserve their already persisted dynamic
identities where present; otherwise perform a fresh direct enrollment.
## Migration Plan
1. Build and deploy the image with the schema migration already included.
2. Start the Cloud API using only database, production, and network settings.
3. Run `device-cloud-admin users create` interactively to bootstrap an
administrator, then verify Console login over HTTPS.
4. Start each new Host Agent with its persistent identity path; it directly
enrolls once and then uses its durable secret for future starts.
5. Remove the legacy credential JSON values from the deployment secret store.
Rollback requires restoring an earlier image and its matching static
credentials before restarting any Host that lacks a durable enrolled identity.
## Open Questions
- None. The user explicitly accepts direct Host enrollment behind the network
perimeter and does not require a Console user-directory surface.
@@ -0,0 +1,49 @@
## Why
The Cloud deployment currently requires operators to hand-maintain public API,
Host, and enrollment bearer tokens in Compose environment variables. Cloud
operator accounts now provide the human authentication boundary, while a Host
Agent can establish its own durable identity by registering directly with the
trusted control plane. Retaining both models makes deployment error-prone and
leaves secrets in configuration without serving the intended workflow.
## What Changes
- **BREAKING** Remove configured public, static Host, and enrollment bearer
credentials from the Cloud Control Plane configuration and deployment
examples.
- Authorize human Cloud API access exclusively through persistent Cloud user
sessions; remove the Console's bearer-token path and user-directory UI.
Interactive `device-cloud-admin` commands remain the account provisioning and
recovery surface.
- Enable a fresh Host Agent to register directly with the trusted Cloud API
without a pre-shared enrollment token, then use its persisted, cloud-bound
secret for all later Host operations.
- Bake the packaged Console static directory into the production image rather
than repeating it in Compose, and remove Compose entries whose values merely
duplicate application defaults or have no runtime effect.
- Update deployment documentation and tests to describe and enforce the
tokenless production flow.
## Capabilities
### New Capabilities
- `cloud-operator-authentication`: Cloud operator sign-in, CLI account
provisioning, and a token-free Console experience.
- `credentialless-host-bootstrap`: Direct Host registration and durable
post-registration identity without static deployment credentials.
### Modified Capabilities
- `platform-sdk`: Replace configured bearer credentials as the production
authorization prerequisite with Cloud user-session authorization.
- `host-agent-protocol`: Add unauthenticated first registration while retaining
host-bound authentication for every subsequent Host operation.
## Impact
Affected areas include the Cloud control configuration and authentication
composition, public and internal Cloud routers, Host Agent configuration and
enrollment client, Console UI, Docker/Compose deployment assets, tests, and
`docs/CLOUD_DEPLOYMENT.md`. Existing deployments using the removed static token
variables must create an administrator and re-enroll Hosts through the new
flow before upgrading.
@@ -0,0 +1,43 @@
## ADDED Requirements
### Requirement: Cloud operator sessions are the only human API authentication path
The system SHALL authorize human Cloud API operations through persistent Cloud
user sessions and SHALL not require configured static bearer credentials for
production startup or normal Console operation.
#### Scenario: Production starts without credential JSON
- **WHEN** the Cloud API starts in production with a current database schema
and no configured public, Host, or enrollment credential JSON
- **THEN** the application starts, exposes health and authentication routes,
and rejects unauthenticated protected API requests
#### Scenario: Signed-in operator uses a protected API route
- **WHEN** an enabled Cloud user has a valid session and calls an operation
allowed by the user's role scopes
- **THEN** the operation is authorized without an Authorization bearer header
### Requirement: The Console contains no token or user-directory management surface
The Console SHALL offer login, logout, current-account display, and password
change, and SHALL not render bearer-token controls, token persistence, Users
navigation, or user-management forms.
#### Scenario: Unauthenticated operator opens the Console
- **WHEN** no valid user session exists
- **THEN** the Console presents the username/password login flow without an API
token alternative
#### Scenario: Administrator opens the Console
- **WHEN** an administrator signs in
- **THEN** the Console presents normal authorized operational views but no
user-directory navigation or account lifecycle form
### Requirement: Account provisioning and recovery remain interactive administration operations
The system SHALL retain interactive, non-echoed administration CLI commands to
create, reset, enable, and revoke Cloud user accounts without accepting
passwords through Compose configuration or command-line arguments.
#### Scenario: Initial administrator is created after deployment
- **WHEN** an operator runs `device-cloud-admin users create` against the
migrated Cloud database and completes the password prompts
- **THEN** an enabled administrator account is created without a deployment
token or plaintext password in process arguments
@@ -0,0 +1,43 @@
## ADDED Requirements
### Requirement: Fresh Hosts register directly with the trusted Cloud API
The Cloud API SHALL accept a first Host registration without a configured
enrollment token or Authorization header, assign a durable `host_id`, and store
only the digest of the Host-generated high-entropy secret.
#### Scenario: Fresh Host registers without static configuration
- **WHEN** a Host Agent with no cached identity sends a valid instance
identifier and generated Host secret to the enrollment endpoint
- **THEN** the Cloud API creates or returns the durable Host identity and does
not require a pre-shared deployment credential
#### Scenario: Registration is retried by the same Host
- **WHEN** the same Host retries registration with its original instance
identifier and candidate secret after losing the response
- **THEN** the Cloud API returns the existing Host identity without creating a
duplicate Host row
### Requirement: Registered Hosts authenticate all later operational requests
The system SHALL require the persisted Host secret for device enrollment,
heartbeat, claim, renewal, and result operations after first registration, and
SHALL bind each accepted request to its registered `host_id`.
#### Scenario: Registered Host sends a heartbeat
- **WHEN** a Host presents its persisted secret for its own Host identity
- **THEN** the Cloud API accepts the heartbeat subject to normal validation
#### Scenario: Caller attempts a Host operation without its secret
- **WHEN** a caller accesses any post-registration Host operation without a
valid secret bound to the path Host identity
- **THEN** the Cloud API rejects the operation without changing Host state
### Requirement: Host Agent configuration contains no static Cloud credential
The Host Agent SHALL use its cached identity when present and otherwise perform
direct registration, without `HOST_AGENT_HOST_ID`, `HOST_AGENT_TOKEN`, or
`HOST_AGENT_ENROLLMENT_TOKEN` configuration.
#### Scenario: Fresh Host starts with only its local state path
- **WHEN** a Host Agent starts with no cached Cloud identity and no static Host
credential environment values
- **THEN** it generates and persists an identity through direct registration
before starting heartbeat or assignment polling
@@ -0,0 +1,39 @@
## ADDED Requirements
### Requirement: Host identity can be established by direct registration
The internal Host Agent API SHALL accept a fresh Host registration without a
pre-shared deployment credential, SHALL assign a durable Host identifier, and
SHALL store only the digest of the Host-generated secret for subsequent
host-scoped authentication.
#### Scenario: Fresh Host establishes an identity
- **WHEN** a fresh Host Agent submits a valid registration request containing
its instance identifier and generated secret
- **THEN** the control plane returns a durable Host identifier and stores only
the secret digest bound to that Host
#### Scenario: Registration request is retried
- **WHEN** the same Host Agent repeats registration with its original instance
identifier and secret after a lost response
- **THEN** the control plane returns the existing Host identifier without
creating a second Host identity
## MODIFIED Requirements
### Requirement: Host identity is authenticated and bound to one host id
After direct registration, the internal Host Agent API SHALL require a
host-scoped bearer principal for all Host operational requests and SHALL reject
any request that attempts to act for a `host_id` different from the
authenticated principal's bound host. The initial registration endpoint is the
only exception and establishes that bound principal.
#### Scenario: Host authenticates as itself after registration
- **WHEN** a Host Agent presents the persisted generated secret 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
@@ -0,0 +1,36 @@
## 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. The deployable Cloud Control Plane SHALL
reject anonymous access outside an explicit insecure-development override, but
production startup SHALL not require configured static bearer credentials.
#### Scenario: Production starts without configured bearer credentials
- **WHEN** the Cloud Control Plane is configured as production with a usable
database and no static bearer credential configuration
- **THEN** startup succeeds and protected platform routes reject unauthenticated
requests
#### 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
## REMOVED Requirements
### Requirement: Python SDK supports authenticated requests
**Reason**: The deployment no longer provisions or accepts static bearer
credentials for public platform access; human operations use Cloud user
sessions.
**Migration**: Replace bearer-token SDK workflows with authenticated Console
user-session workflows. A non-human service credential model is outside this
change and must be designed separately before reintroducing SDK automation.
@@ -0,0 +1,30 @@
## 1. Cloud authentication configuration
- [x] 1.1 Remove configured public and static-Host bearer credential parsing and production credential validation from `CloudControlConfig`, while retaining the optional trusted-proxy setting used by login throttling.
- [x] 1.2 Compose public authorization from Cloud user sessions and dynamic repository-backed Host credentials only; remove configured enrollment-token authentication.
- [x] 1.3 Update Cloud configuration and application tests to prove production starts without JSON credentials, protected API routes reject anonymous requests, and user sessions retain scoped authorization.
## 2. Direct Host bootstrap
- [x] 2.1 Remove static Host and enrollment-token settings from Host Agent configuration while retaining persistent identity-file validation and control-plane URL overrides.
- [x] 2.2 Change the Host enrollment client and identity resolution to perform unauthenticated first registration and persist the returned Host identity and generated secret.
- [x] 2.3 Change the Cloud enrollment route to accept direct registration, preserve idempotency for the same instance, and require the persisted Host secret for all later Host operations.
- [x] 2.4 Add Cloud and Host Agent tests for direct registration, idempotent retry, missing/invalid post-registration Host credentials, and absence of static credential settings.
## 3. Console token and user-directory removal
- [x] 3.1 Remove bearer-token compatibility state, controls, client behavior, and related tests from the Cloud Console.
- [x] 3.2 Remove Console Users navigation, views, client calls, and tests while retaining login, logout, current-user, and password-change behavior.
- [x] 3.3 Add or update Console tests covering session-only login and the absence of token and user-directory UI paths.
## 4. Deployment contract and documentation
- [x] 4.1 Bake the Console static directory into the Docker image environment and remove it, credential JSON, static Host credentials, and redundant defaults from Compose and `.env.example`.
- [x] 4.2 Update deployment documentation for user-account bootstrap, direct Host registration, required runtime configuration, rollback, and the actual deploy Compose topology.
- [x] 4.3 Update deployment-configuration tests to enforce the reduced environment contract and image-provided Console path.
## 5. Verification and change validation
- [x] 5.1 Run focused Cloud API, Cloud platform, Host Agent, deployment, and Console test suites; fix failures caused by the removed credential paths.
- [ ] 5.2 Render `compose.deploy.yaml` with representative non-secret settings and verify the Cloud API command, health check, and environment contract.
- [ ] 5.3 Run the full non-integration workspace suite, Console build, and strict OpenSpec validation; record any environment-gated checks that cannot run locally.