@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-07-13
|
||||
@@ -0,0 +1,178 @@
|
||||
## Context
|
||||
|
||||
`cloud-planner-proxy` centralizes an LLM call for Hosts that opt into
|
||||
`AI_PLANNER_TRANSPORT=cloud`, but `cloud.planner_config` currently resolves
|
||||
provider, model, and credentials from Cloud API environment variables. The
|
||||
proxy route builds a client for every decision request, so it already has a
|
||||
natural per-request configuration boundary. The Cloud Control Plane also has a
|
||||
durable SQLAlchemy repository, Alembic migrations, session-authenticated
|
||||
administrator Console, CSRF protection, and auth audit records that this
|
||||
change can reuse.
|
||||
|
||||
The existing runtime clients instantiate provider SDKs with no arguments and
|
||||
therefore read keys from their environment. Database-backed credentials and
|
||||
an OpenAI-compatible base URL require those clients to accept explicit values
|
||||
while preserving the unchanged direct-transport default.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- Let administrators manage a Cloud-wide catalog of `anthropic` and
|
||||
`openai-compatible` planner profiles from the Cloud Console.
|
||||
- Encrypt every stored Provider API key and never return or log its plaintext.
|
||||
- Switch the provider/model used by all cloud-transport Host Agents without a
|
||||
Host configuration change, Cloud API restart, or local credential.
|
||||
- Support OpenAI-compatible endpoints through a configurable base URL and
|
||||
API key using the existing Chat Completions tool-calling implementation.
|
||||
- Remove Cloud API environment-based planner Provider configuration so every
|
||||
cloud-transport decision is governed by the active database profile.
|
||||
|
||||
**Non-Goals:**
|
||||
- Per-Host, per-user, per-task, weighted, or failover Provider selection.
|
||||
- Provider usage billing, connection-test endpoints, model discovery, or
|
||||
support for protocols other than Anthropic native tool use and OpenAI Chat
|
||||
Completions tool calling.
|
||||
- Arbitrary Anthropic-compatible endpoints, custom request headers, or
|
||||
different OpenAI-compatible parameter dialects.
|
||||
- Automatic encryption-master-key rotation. Provider API key rotation is
|
||||
supported by updating a profile; master-key rotation remains an operational
|
||||
migration until a key-ring design is separately proposed.
|
||||
|
||||
## Decisions
|
||||
|
||||
### D1: Persist a profile catalog and a singleton active-profile setting
|
||||
|
||||
Add `cloud_llm_provider_profiles` with immutable id, unique normalized name,
|
||||
provider type, model, optional base URL, timeout, encrypted API key, enabled
|
||||
state, revision, and timestamps. Add a one-row
|
||||
`cloud_llm_provider_settings` record with `database_management_enabled`,
|
||||
`active_profile_id`, revision, and timestamp. The profile table represents
|
||||
the operator's choices; the singleton holds the one Cloud-wide choice that is
|
||||
active.
|
||||
|
||||
Activating an enabled profile updates the singleton in the same transaction
|
||||
and sets `database_management_enabled=true`. An active profile cannot be
|
||||
disabled or deleted before another enabled profile is activated. This avoids
|
||||
the ambiguity and cross-database portability problems of a partial unique
|
||||
"one active row" index, and gives PostgreSQL and SQLite one repository
|
||||
contract.
|
||||
|
||||
Alternative considered: an `is_active` column on profiles. Rejected because
|
||||
atomic replacement and uniqueness semantics differ between SQLite and
|
||||
PostgreSQL, while a singleton pointer is explicit and makes initial
|
||||
environment-fallback state representable.
|
||||
|
||||
### D2: Resolve the active profile on every planner decision
|
||||
|
||||
The internal planner endpoint resolves the active profile from the repository
|
||||
at the start of each request, decrypts its API key, and builds the existing
|
||||
tool-calling client with the resolved values. It uses that same resolved
|
||||
profile for token-usage metadata, preventing a model switch during the call
|
||||
from recording a mismatched provider/model. No application-level cache is
|
||||
used; the one indexed settings lookup and symmetric decryption are negligible
|
||||
relative to an LLM request and make a successful activation effective on the
|
||||
next decision.
|
||||
|
||||
The resolver fails a planner request with the existing structured
|
||||
`planner_unavailable` response when the active profile is missing, disabled,
|
||||
corrupt, or undecryptable. It never reads legacy `AI_PLANNER_*` or Provider
|
||||
API-key environment variables.
|
||||
|
||||
Alternative considered: move profile configuration into Host Agent heartbeat
|
||||
responses. Rejected because it would replicate API keys to edge hosts, delay
|
||||
changes until heartbeat, and duplicate the Cloud proxy's existing decision
|
||||
boundary.
|
||||
|
||||
### D3: Use Fernet encryption with an environment-held master key
|
||||
|
||||
`cryptography` is added to `device-cloud-platform`. A small secret-box port
|
||||
uses `cryptography.fernet.Fernet` with
|
||||
`CLOUD_LLM_PROVIDER_ENCRYPTION_KEY`; only Fernet ciphertext is stored in the
|
||||
database. The key remains environment or secret-manager configuration and is
|
||||
not stored in the database. Profile write requests accept a plaintext API key
|
||||
only to encrypt it; profile read responses expose `has_api_key` and
|
||||
`key_last_rotated_at` rather than a key value.
|
||||
|
||||
The Cloud API can list profile metadata without the encryption key, but
|
||||
creating, rotating, activating, or resolving a profile without a valid key
|
||||
fails with a controlled configuration error that excludes the supplied secret.
|
||||
Provider mutation audit events retain profile id and action only.
|
||||
|
||||
Alternative considered: plaintext database keys. Rejected because database
|
||||
backup or read access would expose external-provider credentials. Alternative
|
||||
considered: a database-held master key. Rejected because it does not create a
|
||||
separate protection boundary.
|
||||
|
||||
### D4: Treat OpenAI-compatible as a concrete wire-protocol contract
|
||||
|
||||
Profiles have `provider_type` of `anthropic` or `openai-compatible`. An
|
||||
`openai-compatible` profile may omit `base_url` to use the official OpenAI
|
||||
endpoint, or supply an absolute HTTP(S) base URL for a compatible service.
|
||||
The runtime `OpenAIToolCallingClient` gains optional `api_key` and `base_url`
|
||||
constructor arguments and constructs `OpenAI` explicitly when supplied.
|
||||
`AnthropicToolCallingClient` gains an optional explicit API key for cloud-held
|
||||
credentials; absent optional arguments retain current SDK environment
|
||||
behavior for direct transport.
|
||||
|
||||
Compatibility means the endpoint accepts OpenAI Chat Completions requests
|
||||
with the tool/function-calling fields emitted by the existing client and
|
||||
returns a single compatible tool call. It does not claim compatibility with
|
||||
providers requiring a different request schema or custom authentication.
|
||||
|
||||
Alternative considered: create a new cloud-only HTTP adapter. Rejected because
|
||||
it duplicates response parsing and would drift from the direct provider path.
|
||||
|
||||
### D5: Expose a dedicated administrator API and Console view
|
||||
|
||||
Add a public `/v1/planner/providers` router composed by the Cloud API. It
|
||||
requires a new `llm-providers:admin` scope, which the existing admin role
|
||||
receives via its wildcard. All writes use the existing session CSRF validator
|
||||
and record non-secret auth audit events. The Console shows only for principals
|
||||
with that scope and provides profile creation, editing/key rotation,
|
||||
enable/disable, activation, and deletion of inactive profiles. Responses use
|
||||
revisions for optimistic-concurrency conflicts rather than overwriting an
|
||||
administrator's newer edit.
|
||||
|
||||
Alternative considered: reuse a generic configuration endpoint. Rejected
|
||||
because Provider records carry secrets and need substantially stricter output,
|
||||
audit, and activation invariants.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- [Risk] The Cloud database and encryption key become part of the planner hot
|
||||
path. -> Mitigation: resolver failures use the established fail-fast
|
||||
response, and readiness/configuration documentation makes the required
|
||||
active profile explicit.
|
||||
- [Risk] An administrator can direct Cloud traffic to an arbitrary compatible
|
||||
base URL. -> Mitigation: only `llm-providers:admin` can write a profile;
|
||||
deployments must treat that role as privileged network-configuration access.
|
||||
- [Risk] Losing the master key makes stored Provider credentials unusable. ->
|
||||
Mitigation: document secret-manager backup and keep a legacy deployment
|
||||
rollback path while adopting the feature.
|
||||
- [Risk] Some services marketed as OpenAI-compatible diverge on parameters or
|
||||
tools. -> Mitigation: document the exact Chat Completions tool-calling
|
||||
contract and surface provider failures instead of retrying against another
|
||||
profile.
|
||||
- [Risk] Concurrent Console administrators can race to edit or activate a
|
||||
profile. -> Mitigation: revision checks on profile/settings writes and a
|
||||
transaction for activation.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
1. Apply the migration and provision `CLOUD_LLM_PROVIDER_ENCRYPTION_KEY` from
|
||||
the deployment secret manager before routing cloud-transport planner work
|
||||
to the new Cloud API version.
|
||||
2. Create and activate a profile through the administrator Console. Until an
|
||||
active profile exists, cloud planner requests fail closed.
|
||||
3. Verify profile metadata and token-usage provider/model labels, then remove
|
||||
`AI_PLANNER_*`, `ANTHROPIC_API_KEY`, and `OPENAI_API_KEY` from the Cloud API
|
||||
deployment configuration.
|
||||
4. To roll back, restore a previous Cloud API version together with its legacy
|
||||
environment configuration. Do not remove the encryption key or database
|
||||
rows before a planned migration.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- None for the initial Cloud-wide selection. Per-Host assignment, cost-aware
|
||||
routing, and master-key rotation need separate designs because they change
|
||||
the selection and operational contracts.
|
||||
@@ -0,0 +1,56 @@
|
||||
## Why
|
||||
|
||||
The Cloud Control Plane proxy currently reads its LLM provider, model, and
|
||||
credentials from process environment variables. Changing a model therefore
|
||||
requires a deployment change and cannot be audited or operated from the Cloud
|
||||
Console, even though every cloud-transport Host Agent already depends on the
|
||||
control plane for its planner decisions.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Add a durable, Cloud-wide catalog of planner Provider profiles. Each profile
|
||||
records its supported provider kind (`anthropic` or `openai-compatible`),
|
||||
model, timeout, optional OpenAI-compatible base URL, enabled state, and an
|
||||
encrypted API key.
|
||||
- Add an administrator-only Cloud API and Cloud Console view to create, list,
|
||||
update, activate, disable, and retire Provider profiles. Read responses
|
||||
never expose an API key; writes can rotate a key without returning it.
|
||||
- Make the cloud planner-decision endpoint resolve the active profile from the
|
||||
database for each request, so a newly activated profile applies to all
|
||||
cloud-transport Host Agents without changing their configuration or
|
||||
restarting them.
|
||||
- Remove Cloud API environment-variable configuration for the planner
|
||||
provider, model, timeout, and Provider API keys. An absent active database
|
||||
profile fails closed instead of silently using a stale environment
|
||||
credential.
|
||||
- Add an encryption-key configuration for protecting Provider API keys at rest
|
||||
and document deployment, rotation, and migration behavior.
|
||||
- Extend the existing provider client construction so cloud-managed
|
||||
OpenAI-compatible profiles pass their encrypted API key and configured base
|
||||
URL explicitly to the OpenAI SDK, while Host Agent direct transport continues
|
||||
to use its existing environment-based credentials.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `llm-provider-management`: Durable, administrator-managed Cloud-wide LLM
|
||||
Provider profiles, encrypted credential handling, and Console/API operations.
|
||||
|
||||
### Modified Capabilities
|
||||
- `cloud-planner-proxy`: Cloud planner decisions select their provider,
|
||||
model, timeout, and credential from the active database profile once
|
||||
database management is enabled, rather than requiring a Cloud API restart
|
||||
and environment-variable change.
|
||||
|
||||
## Impact
|
||||
|
||||
- `packages/cloud-platform/cloud`: SQLAlchemy models, repository contract and
|
||||
implementation, Alembic migration, encrypted-secret service, and planner
|
||||
client construction from a resolved profile.
|
||||
- `apps/cloud-api` and `cloud.sdk`: an authenticated management router and
|
||||
application composition for the new service.
|
||||
- `cloud-console`: an administrator-only Provider management view, client API,
|
||||
and types.
|
||||
- Cloud deployment configuration: the encryption key remains deployment-held,
|
||||
while planner Provider/model/timeout/API-key environment variables are
|
||||
removed.
|
||||
@@ -0,0 +1,28 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Endpoint resolves exactly one tool-call decision using cloud-held provider configuration
|
||||
The Cloud Control Plane SHALL use its own configured LLM provider, model, and
|
||||
credentials -- not any value supplied by the requesting Host Agent -- to
|
||||
resolve a planner-decision request to exactly one tool name and one arguments
|
||||
object, within the request's timeout. The endpoint SHALL resolve the active
|
||||
database-managed Provider profile for every request and use its provider,
|
||||
model, timeout, OpenAI-compatible base URL when applicable, and encrypted
|
||||
cloud-held credential. It SHALL NOT read Cloud API planner Provider/model/
|
||||
timeout/API-key environment variables.
|
||||
|
||||
#### Scenario: Provider returns a usable decision
|
||||
- **WHEN** the configured provider responds to a planner-decision request
|
||||
with a tool call
|
||||
- **THEN** the Cloud Control Plane returns exactly one resolved tool name
|
||||
and arguments object to the requesting Host Agent
|
||||
|
||||
#### Scenario: Configured provider is unreachable or misconfigured
|
||||
- **WHEN** the Cloud Control Plane's configured provider call fails (for
|
||||
example, invalid credentials, provider error, or timeout)
|
||||
- **THEN** the endpoint returns a structured failure response rather than a
|
||||
fabricated decision, and does not crash the Cloud Control Plane process
|
||||
|
||||
#### Scenario: Database profile is the only planner configuration source
|
||||
- **WHEN** the Cloud API process has legacy planner environment variables
|
||||
- **THEN** subsequent planner-decision requests use only the active database
|
||||
profile and do not read a legacy Provider credential for that decision
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Cloud-wide LLM Provider profiles are durable and validated
|
||||
The Cloud Control Plane SHALL persist administrator-managed LLM Provider
|
||||
profiles with a unique name, provider type, model, timeout, enabled state,
|
||||
revision, and timestamps. A profile's provider type SHALL be either
|
||||
`anthropic` or `openai-compatible`; an OpenAI-compatible profile MAY specify
|
||||
an absolute HTTP(S) base URL and SHALL use the official OpenAI endpoint when
|
||||
it does not.
|
||||
|
||||
#### Scenario: Administrator creates an OpenAI-compatible profile
|
||||
- **WHEN** an authorized administrator submits a unique profile name,
|
||||
`openai-compatible` provider type, model, valid timeout, API key, and an
|
||||
optional valid base URL
|
||||
- **THEN** the Cloud Control Plane persists an enabled profile with a new
|
||||
revision and returns its non-secret metadata
|
||||
|
||||
#### Scenario: Invalid profile configuration is rejected
|
||||
- **WHEN** an administrator submits an unsupported provider type, blank model,
|
||||
non-positive timeout, duplicate name, or invalid base URL
|
||||
- **THEN** the Cloud Control Plane rejects the write without creating or
|
||||
changing a profile
|
||||
|
||||
### Requirement: Provider API keys are encrypted and never disclosed
|
||||
The Cloud Control Plane SHALL encrypt Provider API keys before persistence
|
||||
using a deployment-held encryption key, and SHALL not expose plaintext keys in
|
||||
read responses, validation errors, audit records, or application logs.
|
||||
|
||||
#### Scenario: Provider profile is listed after creation
|
||||
- **WHEN** an authorized administrator lists Provider profiles after creating
|
||||
one with an API key
|
||||
- **THEN** every response reports only key-presence and rotation metadata and
|
||||
does not contain the submitted API key or its ciphertext
|
||||
|
||||
#### Scenario: Encryption configuration is unavailable
|
||||
- **WHEN** a database-managed profile is created, rotated, activated, or
|
||||
resolved without a valid deployment encryption key
|
||||
- **THEN** the operation fails with a controlled configuration error that does
|
||||
not reveal an API key
|
||||
|
||||
### Requirement: Administrators can manage and activate Provider profiles
|
||||
The Cloud Control Plane SHALL expose session-CSRF-protected and scope-guarded
|
||||
operations to list, create, update, rotate a key, enable, disable, activate,
|
||||
and delete inactive LLM Provider profiles. Mutating operations SHALL require
|
||||
the `llm-providers:admin` scope and record a non-secret audit event.
|
||||
|
||||
#### Scenario: Non-administrator attempts to modify a profile
|
||||
- **WHEN** a principal without `llm-providers:admin` invokes a Provider
|
||||
mutation endpoint
|
||||
- **THEN** the Cloud Control Plane rejects the request before decrypting or
|
||||
modifying a Provider credential
|
||||
|
||||
#### Scenario: Administrator switches the active profile
|
||||
- **WHEN** an authorized administrator activates an enabled profile
|
||||
- **THEN** the Cloud Control Plane atomically selects that profile as the one
|
||||
Cloud-wide active profile and records the activation without retaining an
|
||||
API key in the audit event
|
||||
|
||||
#### Scenario: Administrator attempts to retire the active profile
|
||||
- **WHEN** an administrator attempts to disable or delete the active profile
|
||||
before activating a replacement
|
||||
- **THEN** the Cloud Control Plane rejects the operation and preserves the
|
||||
active profile selection
|
||||
|
||||
### Requirement: Activation dynamically selects one Provider for cloud transport
|
||||
The Cloud Control Plane SHALL resolve the active database-managed profile for
|
||||
each Cloud planner decision and SHALL apply an activation to subsequent
|
||||
requests without requiring a Cloud API restart or any Host Agent
|
||||
reconfiguration.
|
||||
|
||||
#### Scenario: A Host requests a decision after a model switch
|
||||
- **WHEN** an administrator activates a different enabled profile and a
|
||||
cloud-transport Host Agent submits its next planner-decision request
|
||||
- **THEN** the Cloud Control Plane calls that profile's provider, model,
|
||||
timeout, base URL, and API key while the Host Agent continues using the same
|
||||
Cloud Control Plane endpoint
|
||||
|
||||
#### Scenario: Active OpenAI-compatible profile is used
|
||||
- **WHEN** the active profile is OpenAI-compatible and includes a base URL
|
||||
- **THEN** the Cloud Control Plane makes the existing OpenAI Chat Completions
|
||||
tool-calling request to that base URL using the profile's decrypted API key
|
||||
and returns the resulting single tool-call decision
|
||||
|
||||
### Requirement: Cloud planner Provider configuration is database-only
|
||||
The Cloud Control Plane SHALL resolve Cloud planner Provider, model, timeout,
|
||||
base URL, and API key from the active database profile and SHALL NOT read
|
||||
planner Provider/model/timeout or Provider API-key environment variables.
|
||||
When no usable active profile exists, it SHALL fail a planner request with a
|
||||
structured unavailable response.
|
||||
|
||||
#### Scenario: Active profile is unavailable
|
||||
- **WHEN** the active database Provider profile is missing, disabled, or
|
||||
cannot be decrypted
|
||||
- **THEN** the planner decision fails without invoking a legacy
|
||||
environment-configured Provider
|
||||
|
||||
#### Scenario: Legacy environment variables are present
|
||||
- **WHEN** the Cloud API process has legacy planner Provider or API-key
|
||||
environment variables but an active database profile exists
|
||||
- **THEN** the planner decision uses only the active database profile
|
||||
@@ -0,0 +1,28 @@
|
||||
## 1. Secure persistence
|
||||
|
||||
- [x] 1.1 Add the encryption dependency and a testable Cloud Provider secret-box configuration backed by `CLOUD_LLM_PROVIDER_ENCRYPTION_KEY`.
|
||||
- [x] 1.2 Add SQLAlchemy profile/settings rows and an Alembic migration after the current Cloud schema revision.
|
||||
- [x] 1.3 Extend the Cloud repository contract and SQL implementation with profile CRUD, atomic activation, revision checks, and active-profile resolution.
|
||||
|
||||
## 2. Administrator management API
|
||||
|
||||
- [x] 2.1 Add non-secret Pydantic SDK request/response models and a dedicated `llm-providers:admin` scope.
|
||||
- [x] 2.2 Add the authenticated, CSRF-protected Provider profile router with audit records and compose it into the Cloud API.
|
||||
- [x] 2.3 Add focused repository and API tests for validation, encryption redaction, authorization, CSRF, revisions, activation, and retirement invariants.
|
||||
|
||||
## 3. Cloud planner resolution
|
||||
|
||||
- [x] 3.1 Allow runtime Anthropic and OpenAI clients to receive explicit API keys and an OpenAI-compatible base URL without changing direct transport behavior.
|
||||
- [x] 3.2 Resolve the active database profile per planner request, remove Cloud API planner environment configuration, and use the resolved metadata for token accounting.
|
||||
- [x] 3.3 Add planner route and client tests for database activation, fail-closed resolution, and OpenAI-compatible client construction.
|
||||
|
||||
## 4. Cloud Console
|
||||
|
||||
- [x] 4.1 Add Provider profile types and CSRF-aware client methods to the Cloud Console API layer.
|
||||
- [x] 4.2 Add an administrator-only Provider management view with create, edit/key rotation, enable/disable, activate, and inactive-profile deletion workflows.
|
||||
- [x] 4.3 Add Console tests for Provider API methods and permission-gated navigation/view behavior.
|
||||
|
||||
## 5. Documentation and validation
|
||||
|
||||
- [x] 5.1 Document encryption-key provisioning, required active-profile cutover, OpenAI-compatible configuration, removed planner environment variables, and rollback in Cloud deployment documentation.
|
||||
- [x] 5.2 Run relevant backend tests, Console tests/build, format/lint, compile checks, and strict OpenSpec validation; resolve failures.
|
||||
Reference in New Issue
Block a user