459 lines
23 KiB
Markdown
459 lines
23 KiB
Markdown
# Auth Secret Creation Flow — Tech Spec
|
|
|
|
## Problem
|
|
|
|
The cloud mode input V2 composing UI lets users select a non-Oz harness (e.g. Claude Code), but has no way to associate an auth secret with that harness. The server already supports `harnessAuthSecrets` (query) and `createManagedSecret` (mutation), and `AgentConfigSnapshot` already has a `harness_auth_secrets` field — but the client never populates it. We need to add the UI for selecting/creating auth secrets and wire the selection into the spawn config.
|
|
|
|
## Relevant Code
|
|
|
|
### Client (warp-internal)
|
|
|
|
- `app/src/ai/harness_availability.rs` — `HarnessAvailabilityModel` singleton. Fetches and caches harness availability from the server. **Will be extended** with per-harness auth secret state.
|
|
- `app/src/terminal/view/ambient_agent/harness_selector.rs` — `HarnessSelector` view (`ActionButton` + `Menu<A>`). The auth secret selector chip mirrors this pattern.
|
|
- `app/src/terminal/view/ambient_agent/model.rs` (165-221) — `AmbientAgentViewModel`. Tracks `harness`, `worker_host`, `harness_model_id`. **Will add** `harness_auth_secret_name`.
|
|
- `app/src/terminal/view/ambient_agent/model.rs` (841-861) — `build_default_spawn_config()`. Builds `AgentConfigSnapshot`. **Will add** `harness_auth_secrets` population.
|
|
- `app/src/ai/ambient_agents/task.rs` (27-65) — `AgentConfigSnapshot` struct. Already has `harness_auth_secrets: Option<HarnessAuthSecretsConfig>` with `claude_auth_secret_name`.
|
|
- `app/src/terminal/input/agent.rs` (507-525) — `render_cloud_mode_v2_content()`. Builds `top_row + input_container`. **Branch point** for FTUX vs normal rendering.
|
|
- `app/src/terminal/input/agent.rs` (539-556) — `render_cloud_mode_v2_top_row()`. **Will add** `AuthSecretSelector` chip conditionally.
|
|
- `app/src/server/server_api/managed_secrets.rs` — `ManagedSecretsClient` trait. Has `create_managed_secret` and `list_secrets`. **Will add** `list_harness_auth_secrets`.
|
|
|
|
### Server (warp-server) — read-only, no changes needed
|
|
|
|
- `graphql/v2/queries/managed_secrets.graphqls` — `harnessAuthSecrets` query, `ListHarnessAuthSecretsInput`, `HarnessAuthSecretsResult`.
|
|
- `graphql/v2/mutations/create_managed_secret.graphqls` — `createManagedSecret` mutation.
|
|
- `model/types/enums/managed_secret_type.go` (46-52) — `authSecretTypesByHarness` map. Claude Code maps to 3 types.
|
|
- `logic/managed_secrets/secret_type.go` — Secret value structs and their JSON field shapes.
|
|
|
|
### GraphQL client crate
|
|
|
|
- `crates/warp_graphql` — cynic-based GraphQL query modules. **Will add** `list_harness_auth_secrets` query module.
|
|
|
|
## Current State
|
|
|
|
- `HarnessAvailabilityModel` fetches harness metadata on auth/network/workspace changes and caches it. It has no awareness of auth secrets.
|
|
- `AmbientAgentViewModel` tracks harness selection and builds the spawn config, but never sets `harness_auth_secrets`.
|
|
- The cloud mode V2 composing UI renders a top row (host + harness selectors) and an input container below it. There is no auth secret UI.
|
|
- The oz web app (`client/packages/agents/src/components/HarnessAuthSecretSelector`) already has a React component and `useHarnessAuthSecrets` hook that query the same server endpoint.
|
|
|
|
## Proposed Changes
|
|
|
|
### 1. GraphQL query: `harnessAuthSecrets`
|
|
|
|
**New file**: `crates/warp_graphql/src/queries/list_harness_auth_secrets.rs`
|
|
|
|
Add a cynic query module for the server's `harnessAuthSecrets` query. The input takes an `AgentHarness` enum; the output reuses the existing `ManagedSecret` fragment.
|
|
|
|
```rust
|
|
#[derive(cynic::QueryVariables)]
|
|
pub struct ListHarnessAuthSecretsVariables {
|
|
pub request_context: RequestContext,
|
|
pub input: ListHarnessAuthSecretsInput,
|
|
}
|
|
|
|
#[derive(cynic::InputObject)]
|
|
pub struct ListHarnessAuthSecretsInput {
|
|
pub harness: AgentHarness,
|
|
}
|
|
|
|
#[derive(cynic::QueryFragment)]
|
|
#[cynic(graphql_type = "RootQuery", variables = "ListHarnessAuthSecretsVariables")]
|
|
pub struct ListHarnessAuthSecrets {
|
|
#[arguments(requestContext: $request_context, input: $input)]
|
|
pub harness_auth_secrets: HarnessAuthSecretsResult,
|
|
}
|
|
```
|
|
|
|
Extend `ManagedSecretsClient` in `app/src/server/server_api/managed_secrets.rs`:
|
|
|
|
```rust
|
|
async fn list_harness_auth_secrets(
|
|
&self,
|
|
harness: AgentHarness,
|
|
) -> Result<Vec<ManagedSecret>>;
|
|
```
|
|
|
|
### 2. Extend `HarnessAvailabilityModel` with auth secrets
|
|
|
|
**File**: `app/src/ai/harness_availability.rs`
|
|
|
|
Add a lazily-fetched per-harness auth secret map to the existing singleton, keeping all harness-related data in one place.
|
|
|
|
```rust
|
|
pub struct HarnessAvailabilityModel {
|
|
harnesses: Vec<HarnessAvailability>,
|
|
/// Lazily fetched when a non-Oz harness is selected.
|
|
auth_secrets: HashMap<Harness, AuthSecretFetchState>,
|
|
}
|
|
|
|
pub enum AuthSecretFetchState {
|
|
NotFetched,
|
|
Loading,
|
|
Loaded(Vec<AuthSecretEntry>),
|
|
/// Surfaced as "Unable to load secrets" in the dropdown; subsequent
|
|
/// `ensure_auth_secrets_fetched` retries.
|
|
Failed(String),
|
|
}
|
|
|
|
pub struct AuthSecretEntry {
|
|
pub name: String,
|
|
}
|
|
```
|
|
|
|
New events added to `HarnessAvailabilityEvent`:
|
|
|
|
```rust
|
|
pub enum HarnessAvailabilityEvent {
|
|
Changed,
|
|
AuthSecretsLoaded { harness: Harness },
|
|
AuthSecretCreated { harness: Harness, name: String },
|
|
AuthSecretCreationFailed { error: String },
|
|
}
|
|
```
|
|
|
|
Key new methods:
|
|
|
|
- `auth_secrets_for(&self, harness) -> &AuthSecretFetchState` — returns current state for the harness.
|
|
- `ensure_auth_secrets_fetched(&mut self, harness, ctx)` — if `NotFetched` or `Failed`, fires async `list_harness_auth_secrets` and transitions to `Loading`. On success, stores `Loaded(entries)` and emits `AuthSecretsLoaded`.
|
|
- `create_auth_secret(&mut self, harness, name, value, ctx)` where `value: ManagedSecretValue` — calls `ManagedSecretManager::create_secret(SecretOwner::CurrentUser, name, value, None)`. On success, appends a new `AuthSecretEntry` to the cached `Loaded` list (or transitions `NotFetched`/`Failed` directly to `Loaded(vec![entry])`) and emits `AuthSecretCreated { harness, name }`. On error, emits `AuthSecretCreationFailed { error }` and leaves the cache untouched.
|
|
- `invalidate_auth_secrets(&mut self, harness)` — resets to `NotFetched`. Called when the user signs out (subscription on `AuthManager`'s `AuthComplete` already exists; we extend it to clear the per-harness cache).
|
|
|
|
### 3. Auth secret type metadata
|
|
|
|
**New file**: `app/src/ai/auth_secret_types.rs`
|
|
|
|
Client-side mirror of the server's `authSecretTypesByHarness`. Maps each harness to its secret types and their input field schemas.
|
|
|
|
```rust
|
|
pub struct AuthSecretTypeField {
|
|
pub label: &'static str,
|
|
pub json_key: &'static str,
|
|
pub optional: bool,
|
|
}
|
|
|
|
pub struct AuthSecretTypeInfo {
|
|
pub display_name: &'static str,
|
|
pub secret_type: &'static str,
|
|
pub fields: &'static [AuthSecretTypeField],
|
|
}
|
|
|
|
pub fn auth_secret_types_for_harness(harness: Harness) -> &'static [AuthSecretTypeInfo] {
|
|
match harness {
|
|
Harness::Claude => &[
|
|
AuthSecretTypeInfo {
|
|
display_name: "Anthropic API Key",
|
|
secret_type: "anthropic_api_key",
|
|
fields: &[
|
|
AuthSecretTypeField { label: "ANTHROPIC_API_KEY", json_key: "api_key", optional: false },
|
|
],
|
|
},
|
|
AuthSecretTypeInfo {
|
|
display_name: "Bedrock API Key",
|
|
secret_type: "anthropic_bedrock_api_key",
|
|
fields: &[
|
|
AuthSecretTypeField { label: "AWS_BEARER_TOKEN_BEDROCK", json_key: "aws_bearer_token_bedrock", optional: false },
|
|
AuthSecretTypeField { label: "AWS_REGION", json_key: "aws_region", optional: false },
|
|
],
|
|
},
|
|
AuthSecretTypeInfo {
|
|
display_name: "Bedrock Access Key",
|
|
secret_type: "anthropic_bedrock_access_key",
|
|
fields: &[
|
|
AuthSecretTypeField { label: "AWS_ACCESS_KEY_ID", json_key: "aws_access_key_id", optional: false },
|
|
AuthSecretTypeField { label: "AWS_SECRET_ACCESS_KEY", json_key: "aws_secret_access_key", optional: false },
|
|
AuthSecretTypeField { label: "AWS_SESSION_TOKEN", json_key: "aws_session_token", optional: true },
|
|
AuthSecretTypeField { label: "AWS_REGION", json_key: "aws_region", optional: false },
|
|
],
|
|
},
|
|
],
|
|
_ => &[],
|
|
}
|
|
}
|
|
```
|
|
|
|
### 4. Per-harness FTUX setting
|
|
|
|
**File**: `app/src/ai/settings.rs` (or equivalent)
|
|
|
|
```rust
|
|
pub static HARNESS_AUTH_FTUX_COMPLETED: Setting<HashMap<String, bool>> = Setting::new(
|
|
"harness_auth_ftux_completed",
|
|
HashMap::new(),
|
|
);
|
|
|
|
pub fn is_harness_auth_ftux_completed(harness: Harness) -> bool { ... }
|
|
pub fn mark_harness_auth_ftux_completed(harness: Harness) { ... }
|
|
```
|
|
|
|
### 5. `AmbientAgentViewModel` changes
|
|
|
|
**File**: `app/src/terminal/view/ambient_agent/model.rs`
|
|
|
|
Add field:
|
|
|
|
```rust
|
|
harness_auth_secret_name: Option<String>,
|
|
```
|
|
|
|
Update `set_harness()` to clear `harness_auth_secret_name` when harness changes.
|
|
|
|
Update `build_default_spawn_config()` to populate `harness_auth_secrets`:
|
|
|
|
```rust
|
|
let harness_auth_secrets = self.harness_auth_secret_name.as_ref().map(|name| {
|
|
HarnessAuthSecretsConfig {
|
|
claude_auth_secret_name: Some(name.clone()),
|
|
}
|
|
});
|
|
```
|
|
|
|
Add getter/setter and new event `AuthSecretSelected`.
|
|
|
|
### 6. Auth Secret Selector — single shared dropdown with sidecar submenu
|
|
|
|
**File**: `app/src/terminal/view/ambient_agent/auth_secret_selector.rs` (already exists, will be extended).
|
|
|
|
The selector is the single source of truth for the dropdown UX. It is rendered both as the top-row chip (returning users) and as the FTUX dropdown (FTUX flow), so the menu items are defined in exactly one place.
|
|
|
|
#### Dropdown structure
|
|
|
|
The `Menu<AuthSecretSelectorAction>` shows:
|
|
|
|
1. A non-clickable header (`MenuItem::Header`) labeled "Auth secret".
|
|
2. The list of existing secrets for the harness, each as a `MenuItem::Item` with `SelectSecret(name)` as its on-select action. When the fetch state is `Loaded` and empty, this section is omitted (per the design decision that the empty-state dropdown shows only the "New" parent).
|
|
3. A `MenuItem::Item` labeled "New" with a `+` icon and a chevron-right indicator on the right side, with `OpenNewTypeSidecar` as its on-select action. Hovering this item opens the sidecar.
|
|
4. While `Loading`, replace the secrets list with a single disabled "Loading…" item; while `Failed`/`NotFetched`, show a disabled "Unable to load secrets" item but **still show the "New" item** so the user can always create a secret even when listing fails.
|
|
|
|
#### Sidecar pattern (for "New" expansion)
|
|
|
|
Following the precedent set by `app/src/terminal/profile_model_selector.rs` (`ModelSpecSidecar`) and the new-session menu in `workspace/view.rs` (`update_new_session_sidecar` / `configure_worktree_new_session_sidecar`), the "New" item is *not* a `MenuItem::Submenu` (which is deprecated and unused). Instead:
|
|
|
|
- The selector owns a second `Menu<AuthSecretSelectorAction>` view (`new_type_sidecar`).
|
|
- Hovering the "New" item triggers `OpenNewTypeSidecar`, which sets `is_new_type_sidecar_open = true` on the selector and calls `set_safe_zone_target` + `set_submenu_being_shown_for_item_index` on the main menu (mirroring the worktree-config sidecar in `view.rs:8800-8814`).
|
|
- The render method adds the sidecar as a positioned overlay child next to the main menu, using `OffsetPositioning::offset_from_save_position_element` with the main menu's save-position id (same approach as `profile_model_selector.rs:1972-1984`). When it would overflow the right window edge, it renders to the left.
|
|
- The sidecar menu items are built from `auth_secret_types_for_harness(harness)`: one `MenuItem::Item` per `AuthSecretTypeInfo`, with `SelectNewType(type_index)` as the on-select action and an icon (`Icon::Key`) on the left.
|
|
- Selecting a type fires `SelectNewType(type_index)`, which:
|
|
- Closes the sidecar and the main menu.
|
|
- Emits `AuthSecretSelectorEvent::NewTypeSelected { harness, type_index }` so the FTUX view can swap into creation mode.
|
|
|
|
#### Updated action and event enums
|
|
|
|
```rust
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
pub enum AuthSecretSelectorAction {
|
|
ToggleMenu,
|
|
SelectSecret(String),
|
|
/// Hovering the "New" parent item opens the sidecar.
|
|
OpenNewTypeSidecar,
|
|
/// User picked one of the new-secret types from the sidecar.
|
|
SelectNewType(usize),
|
|
}
|
|
|
|
pub enum AuthSecretSelectorEvent {
|
|
MenuVisibilityChanged { open: bool },
|
|
/// Emitted when the user picks "New {type}" from the sidecar.
|
|
NewTypeSelected {
|
|
harness: Harness,
|
|
type_index: usize,
|
|
},
|
|
}
|
|
```
|
|
|
|
#### Selector struct
|
|
|
|
```rust
|
|
pub struct AuthSecretSelector {
|
|
button: ViewHandle<ActionButton>,
|
|
menu: ViewHandle<Menu<AuthSecretSelectorAction>>,
|
|
new_type_sidecar: ViewHandle<Menu<AuthSecretSelectorAction>>,
|
|
is_menu_open: bool,
|
|
is_new_type_sidecar_open: bool,
|
|
menu_positioning_provider: Arc<dyn MenuPositioningProvider>,
|
|
ambient_agent_model: ModelHandle<AmbientAgentViewModel>,
|
|
}
|
|
```
|
|
|
|
Subscribes to `HarnessAvailabilityModel` for `AuthSecretsLoaded` / `AuthSecretCreated` (refresh both menus) and to `AmbientAgentViewModel` for `HarnessSelected` (rebuild menus when the harness changes).
|
|
|
|
### 7. Auth Secret FTUX View
|
|
|
|
**New file**: `app/src/terminal/view/ambient_agent/auth_secret_ftux_view.rs`
|
|
|
|
A `TypedActionView` that owns the FTUX content. The FTUX view *embeds* the existing `AuthSecretSelector` rather than building its own dropdown — this is the single-source-of-truth requirement.
|
|
|
|
```rust
|
|
pub struct AuthSecretFtuxView {
|
|
ambient_agent_model: ModelHandle<AmbientAgentViewModel>,
|
|
/// Shared dropdown view; same one rendered as the top-row chip when FTUX is
|
|
/// already completed. The FTUX view subscribes to its events.
|
|
auth_secret_selector: ViewHandle<AuthSecretSelector>,
|
|
/// Single-line editors, one per field of the currently-selected new type.
|
|
/// Empty when no "New {type}" has been selected.
|
|
field_editors: Vec<ViewHandle<EditorView>>,
|
|
creation_state: Option<SecretCreationState>,
|
|
/// Whether to show the "Click here to skip" link. Always true for the
|
|
/// initial FTUX entry; false when the FTUX is re-entered from the chip's
|
|
/// "New {type}" path (since the user has already completed FTUX once).
|
|
show_skip_link: bool,
|
|
}
|
|
|
|
pub struct SecretCreationState {
|
|
pub harness: Harness,
|
|
pub secret_type_index: usize,
|
|
/// Per-field name for the secret being created. Server requires this.
|
|
pub secret_name: String,
|
|
pub is_saving: bool,
|
|
}
|
|
```
|
|
|
|
#### Field input model
|
|
|
|
For each `AuthSecretTypeField` of the selected type, the FTUX view constructs a single-line `EditorView` (using `EditorView::single_line` with `SingleLineEditorOptions`, mirroring `model_selector.rs`'s search input). The editor's buffer text is the field value; the FTUX view reads them on Continue without needing per-keystroke `UpdateField` actions.
|
|
|
|
The view also constructs a name-of-secret editor (the user must name the saved secret); this is a required field that the server uses as the unique key.
|
|
|
|
#### Actions
|
|
|
|
```rust
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
pub enum AuthSecretFtuxAction {
|
|
Cancel,
|
|
Continue,
|
|
Skip,
|
|
}
|
|
```
|
|
|
|
No per-field `UpdateField` action is needed — the editors hold their own state and `Continue` reads them.
|
|
|
|
#### Behavior
|
|
|
|
- On construction: calls `HarnessAvailabilityModel::ensure_auth_secrets_fetched(harness, ctx)`.
|
|
- Subscribes to the embedded `auth_secret_selector` for `NewTypeSelected { harness, type_index }`. On that event, populates `creation_state` and rebuilds `field_editors` to match `auth_secret_types_for_harness(harness)[type_index].fields`.
|
|
- Subscribes to `HarnessAvailabilityModel` for `AuthSecretCreated` (transition out of saving state, set the secret as selected on `AmbientAgentViewModel`, mark FTUX completed) and `AuthSecretCreationFailed` (transition out of saving, surface error toast via `Input`'s existing toast subscription).
|
|
- Continue button:
|
|
- If a secret is already selected on the view model (from clicking an existing item in the dropdown), mark FTUX completed and `ctx.notify()`.
|
|
- If `creation_state` is `Some`, validate that all non-optional fields are filled, build the matching `ManagedSecretValue` (e.g. `anthropic_api_key`, `anthropic_bedrock_api_key`, `anthropic_bedrock_access_key`), set `is_saving = true`, and call `HarnessAvailabilityModel::create_auth_secret(harness, name, value, ctx)`. Disable the button while `is_saving`.
|
|
- Cancel button: switches harness back to Oz via `AmbientAgentViewModel::set_harness(Harness::Oz, ctx)`. (Same as the existing `CancelAuthSecretFtux` action.)
|
|
- Skip button: marks FTUX completed via `CloudAgentSettings::mark_harness_auth_ftux_completed(harness, ctx)`. (Same as the existing `SkipAuthSecretFtux` action.)
|
|
|
|
#### Wiring in `Input`
|
|
|
|
The FTUX view is constructed in `Input::new` alongside the existing `AuthSecretSelector` and stored on `AmbientAgentViewState`. The `agent.rs` rendering of `render_auth_secret_ftux_content` is replaced by `ChildView::new(&self.auth_secret_ftux_view)`.
|
|
|
|
The inline `SkipAuthSecretFtux` / `CancelAuthSecretFtux` `InputAction` variants and their handlers in `input.rs` are removed in favor of the FTUX view dispatching `AuthSecretFtuxAction` directly to itself.
|
|
|
|
### 8. Rendering tree integration
|
|
|
|
**File**: `app/src/terminal/input/agent.rs`
|
|
|
|
The branch point is `render_cloud_mode_v2_content` (line 507-525). Currently:
|
|
|
|
```
|
|
render_cloud_mode_v2_content()
|
|
└── Align (centered)
|
|
└── ConstrainedBox (max-width: 720px)
|
|
└── Flex::column (gap: 10px)
|
|
├── render_cloud_mode_v2_top_row() ← ALWAYS rendered
|
|
└── render_cloud_mode_v2_input_container() ← normal input
|
|
```
|
|
|
|
With FTUX active (non-Oz harness + FTUX not completed):
|
|
|
|
```
|
|
render_cloud_mode_v2_content()
|
|
└── Align (centered)
|
|
└── ConstrainedBox (max-width: 720px)
|
|
└── Flex::column (gap: 10px)
|
|
├── render_cloud_mode_v2_top_row() ← SAME
|
|
└── ChildView::new(&auth_secret_ftux_view) ← replaces input container
|
|
```
|
|
|
|
With FTUX completed (returning user):
|
|
|
|
```
|
|
render_cloud_mode_v2_content()
|
|
└── Align (centered)
|
|
└── ConstrainedBox (max-width: 720px)
|
|
└── Flex::column (gap: 10px)
|
|
├── render_cloud_mode_v2_top_row()
|
|
│ └── HostSelector | HarnessSelector | AuthSecretSelector | ...
|
|
└── render_cloud_mode_v2_input_container() ← normal input
|
|
```
|
|
|
|
The top row always renders. The second child swaps based on `should_show_auth_secret_ftux()`.
|
|
|
|
### 9. Toast notification
|
|
|
|
Subscribe to `HarnessAvailabilityEvent::AuthSecretCreated` in `Input` and show an ephemeral `DismissibleToast` via `ToastStack`: "API key saved." with a "Manage secrets" action button.
|
|
|
|
## End-to-End Flow
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant User
|
|
participant HarnessSelector
|
|
participant AmbientAgentVM as AmbientAgentViewModel
|
|
participant HarnessAvail as HarnessAvailabilityModel
|
|
participant FtuxView as AuthSecretFtuxView
|
|
participant Server as warp-server
|
|
|
|
User->>HarnessSelector: Select "Claude Code"
|
|
HarnessSelector->>AmbientAgentVM: set_harness(Claude)
|
|
Note over AmbientAgentVM: Emits HarnessSelected
|
|
Note over FtuxView: render_cloud_mode_v2_content checks FTUX setting
|
|
alt FTUX not completed
|
|
FtuxView->>HarnessAvail: ensure_auth_secrets_fetched(Claude)
|
|
HarnessAvail->>Server: harnessAuthSecrets(CLAUDE_CODE)
|
|
Server-->>HarnessAvail: [secret1, secret2, ...]
|
|
HarnessAvail-->>FtuxView: AuthSecretsLoaded
|
|
FtuxView->>FtuxView: Populate dropdown
|
|
alt User selects existing secret
|
|
User->>FtuxView: Select "secret1"
|
|
User->>FtuxView: Click Continue
|
|
FtuxView->>AmbientAgentVM: set_harness_auth_secret_name("secret1")
|
|
FtuxView->>FtuxView: mark_harness_auth_ftux_completed(Claude)
|
|
Note over FtuxView: Re-render → normal input with chip
|
|
else User creates new secret
|
|
User->>FtuxView: Select "New Anthropic API Key"
|
|
FtuxView->>FtuxView: Show input field(s)
|
|
User->>FtuxView: Fill ANTHROPIC_API_KEY, click Continue
|
|
FtuxView->>HarnessAvail: create_auth_secret(...)
|
|
HarnessAvail->>Server: createManagedSecret(...)
|
|
Server-->>HarnessAvail: OK
|
|
HarnessAvail-->>FtuxView: AuthSecretCreated
|
|
Note over FtuxView: Toast + transition to normal input
|
|
end
|
|
else FTUX completed
|
|
Note over FtuxView: Render normal input + AuthSecretSelector chip
|
|
end
|
|
User->>User: Types prompt, submits
|
|
AmbientAgentVM->>Server: spawn_agent (config includes harness_auth_secrets)
|
|
```
|
|
|
|
## Risks and Mitigations
|
|
|
|
- **Shared singleton mutation**: `HarnessAvailabilityModel` is a singleton; adding mutable auth secret state must not interfere with the existing harness availability data. Mitigated by using a separate `HashMap` field that is never touched by the existing `refresh()` path.
|
|
- **Lazy fetch timing**: The FTUX view calls `ensure_auth_secrets_fetched` on construction. If the fetch is slow, the dropdown shows a loading state. The user cannot hit Continue until secrets are loaded or they enter a raw value.
|
|
- **Secret encryption**: Creating a secret requires client-side encryption using the user's upload key. The existing `ManagedSecretsClient::create_managed_secret` already handles this — we reuse that path.
|
|
- **Feature flag coupling**: This feature requires `CloudModeInputV2` and `CloudMode` and `AgentHarness` flags to all be enabled. If any is off, none of the new code runs.
|
|
- **Stale cache**: After creating a secret, we append to the cached list optimistically. If the server rejects the creation, we show an error toast and don't modify the cache.
|
|
|
|
## Testing and Validation
|
|
|
|
- **Unit tests**: `auth_secret_types_for_harness` returns correct field schemas for Claude and empty for Oz/Unknown. FTUX setting helpers round-trip correctly.
|
|
- **Compile check**: `cargo check` with and without the `cloud_mode_input_v2` feature flag.
|
|
- **Manual verification**:
|
|
1. Select Claude Code → FTUX appears, top row persists, dropdown populates from server.
|
|
2. Select existing secret → Continue → normal input with chip showing selected secret.
|
|
3. Select "New Anthropic API Key" → 1 field appears → fill → Continue → toast + chip updates.
|
|
4. Select "New Bedrock Access Key" → 4 fields appear (1 optional) → fill required → Continue.
|
|
5. Cancel → returns to Oz. Skip → proceeds with no secret.
|
|
6. Restart app → re-select Claude Code → FTUX skipped, chip shows previous secret.
|
|
7. Submit prompt → verify `harness_auth_secrets.claude_auth_secret_name` is set in the spawn request.
|
|
|
|
## Follow-ups
|
|
|
|
- Extend `auth_secret_types_for_harness` when new harnesses (e.g. Gemini, Codex) are added.
|
|
- Support raw-value passthrough (type the API key directly without creating a named secret).
|
|
- Add the "Manage secrets" toast button action to open the Warp Drive secrets pane.
|
|
- Consider pre-selecting the most recently used secret when returning to the FTUX or chip.
|