23 KiB
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—HarnessAvailabilityModelsingleton. 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—HarnessSelectorview (ActionButton+Menu<A>). The auth secret selector chip mirrors this pattern.app/src/terminal/view/ambient_agent/model.rs(165-221) —AmbientAgentViewModel. Tracksharness,worker_host,harness_model_id. Will addharness_auth_secret_name.app/src/terminal/view/ambient_agent/model.rs(841-861) —build_default_spawn_config(). BuildsAgentConfigSnapshot. Will addharness_auth_secretspopulation.app/src/ai/ambient_agents/task.rs(27-65) —AgentConfigSnapshotstruct. Already hasharness_auth_secrets: Option<HarnessAuthSecretsConfig>withclaude_auth_secret_name.app/src/terminal/input/agent.rs(507-525) —render_cloud_mode_v2_content(). Buildstop_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 addAuthSecretSelectorchip conditionally.app/src/server/server_api/managed_secrets.rs—ManagedSecretsClienttrait. Hascreate_managed_secretandlist_secrets. Will addlist_harness_auth_secrets.
Server (warp-server) — read-only, no changes needed
graphql/v2/queries/managed_secrets.graphqls—harnessAuthSecretsquery,ListHarnessAuthSecretsInput,HarnessAuthSecretsResult.graphql/v2/mutations/create_managed_secret.graphqls—createManagedSecretmutation.model/types/enums/managed_secret_type.go(46-52) —authSecretTypesByHarnessmap. 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 addlist_harness_auth_secretsquery module.
Current State
HarnessAvailabilityModelfetches harness metadata on auth/network/workspace changes and caches it. It has no awareness of auth secrets.AmbientAgentViewModeltracks harness selection and builds the spawn config, but never setsharness_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 anduseHarnessAuthSecretshook 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.
#[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:
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.
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:
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)— ifNotFetchedorFailed, fires asynclist_harness_auth_secretsand transitions toLoading. On success, storesLoaded(entries)and emitsAuthSecretsLoaded.create_auth_secret(&mut self, harness, name, value, ctx)wherevalue: ManagedSecretValue— callsManagedSecretManager::create_secret(SecretOwner::CurrentUser, name, value, None). On success, appends a newAuthSecretEntryto the cachedLoadedlist (or transitionsNotFetched/Faileddirectly toLoaded(vec![entry])) and emitsAuthSecretCreated { harness, name }. On error, emitsAuthSecretCreationFailed { error }and leaves the cache untouched.invalidate_auth_secrets(&mut self, harness)— resets toNotFetched. Called when the user signs out (subscription onAuthManager'sAuthCompletealready 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.
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)
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:
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:
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:
- A non-clickable header (
MenuItem::Header) labeled "Auth secret". - The list of existing secrets for the harness, each as a
MenuItem::ItemwithSelectSecret(name)as its on-select action. When the fetch state isLoadedand empty, this section is omitted (per the design decision that the empty-state dropdown shows only the "New" parent). - A
MenuItem::Itemlabeled "New" with a+icon and a chevron-right indicator on the right side, withOpenNewTypeSidecaras its on-select action. Hovering this item opens the sidecar. - While
Loading, replace the secrets list with a single disabled "Loading…" item; whileFailed/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 setsis_new_type_sidecar_open = trueon the selector and callsset_safe_zone_target+set_submenu_being_shown_for_item_indexon the main menu (mirroring the worktree-config sidecar inview.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_elementwith the main menu's save-position id (same approach asprofile_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): oneMenuItem::ItemperAuthSecretTypeInfo, withSelectNewType(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
#[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
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.
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
#[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_selectorforNewTypeSelected { harness, type_index }. On that event, populatescreation_stateand rebuildsfield_editorsto matchauth_secret_types_for_harness(harness)[type_index].fields. - Subscribes to
HarnessAvailabilityModelforAuthSecretCreated(transition out of saving state, set the secret as selected onAmbientAgentViewModel, mark FTUX completed) andAuthSecretCreationFailed(transition out of saving, surface error toast viaInput'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_stateisSome, validate that all non-optional fields are filled, build the matchingManagedSecretValue(e.g.anthropic_api_key,anthropic_bedrock_api_key,anthropic_bedrock_access_key), setis_saving = true, and callHarnessAvailabilityModel::create_auth_secret(harness, name, value, ctx). Disable the button whileis_saving.
- If a secret is already selected on the view model (from clicking an existing item in the dropdown), mark FTUX completed and
- Cancel button: switches harness back to Oz via
AmbientAgentViewModel::set_harness(Harness::Oz, ctx). (Same as the existingCancelAuthSecretFtuxaction.) - Skip button: marks FTUX completed via
CloudAgentSettings::mark_harness_auth_ftux_completed(harness, ctx). (Same as the existingSkipAuthSecretFtuxaction.)
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
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:
HarnessAvailabilityModelis a singleton; adding mutable auth secret state must not interfere with the existing harness availability data. Mitigated by using a separateHashMapfield that is never touched by the existingrefresh()path. - Lazy fetch timing: The FTUX view calls
ensure_auth_secrets_fetchedon 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_secretalready handles this — we reuse that path. - Feature flag coupling: This feature requires
CloudModeInputV2andCloudModeandAgentHarnessflags 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_harnessreturns correct field schemas for Claude and empty for Oz/Unknown. FTUX setting helpers round-trip correctly. - Compile check:
cargo checkwith and without thecloud_mode_input_v2feature flag. - Manual verification:
- Select Claude Code → FTUX appears, top row persists, dropdown populates from server.
- Select existing secret → Continue → normal input with chip showing selected secret.
- Select "New Anthropic API Key" → 1 field appears → fill → Continue → toast + chip updates.
- Select "New Bedrock Access Key" → 4 fields appear (1 optional) → fill required → Continue.
- Cancel → returns to Oz. Skip → proceeds with no secret.
- Restart app → re-select Claude Code → FTUX skipped, chip shows previous secret.
- Submit prompt → verify
harness_auth_secrets.claude_auth_secret_nameis set in the spawn request.
Follow-ups
- Extend
auth_secret_types_for_harnesswhen 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.