179 lines
9.2 KiB
Markdown
179 lines
9.2 KiB
Markdown
## 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.
|