11 KiB
Auth Secret Deletion From Selector — Tech Spec
Context
This spec implements the behavior in PRODUCT.md for the ambient cloud-mode auth selector chip.
Relevant current code:
app/src/terminal/view/ambient_agent/auth_secret_selector.rs:61definesAuthSecretSelectorAction; it currently supports toggling the menu, selecting a secret, clearing/inheriting, opening the "New" sidecar, and selecting a new-secret type.app/src/terminal/view/ambient_agent/auth_secret_selector.rs:147subscribes the selector toHarnessAvailabilityModelauth-secret load/create/fetch-failure events and refreshes the menu/button.app/src/terminal/view/ambient_agent/auth_secret_selector.rs:389builds the menu. Loaded secrets are rendered asMenuItem::Itemrows withSelectSecret(name)actions; the "New" row is separate and opens the sidecar.app/src/terminal/view/ambient_agent/auth_secret_selector.rs:498handles selector actions and persists selected/cleared secrets inCloudAgentSettings.last_selected_auth_secret.app/src/ai/harness_availability.rs:55stores per-harness auth secret fetch state asAuthSecretFetchState::Loaded(Vec<AuthSecretEntry>).app/src/ai/harness_availability.rs:184fetches harness auth secrets fromManagedSecretsClient::list_harness_auth_secrets.app/src/ai/harness_availability.rs:250already centralizes secret creation throughManagedSecretManager, updates the local cache, and emits create/failure events.crates/managed_secrets/src/manager.rs:87exposesManagedSecretManager::delete_secret(owner, name).app/src/server/server_api/managed_secrets.rs:132wiresdelete_managed_secretto the server GraphQL mutation.app/src/menu.rs:394defines reusableMenuItemFields. It already has right-side label/icon rendering, but right-side icons are decorative and do not dispatch a separate action from the row.- Existing destructive confirmation dialogs such as
app/src/settings_view/delete_environment_confirmation_dialog.rsandapp/src/settings_view/mcp_servers/destructive_mcp_confirmation_dialog.rsestablish the preferredDialog+DangerPrimaryThemepattern for the new modal.
The current AuthSecretEntry contains only name, but server ManagedSecret results include owner. Deleting team-owned secrets correctly requires preserving enough owner information to call delete_secret with SecretOwner::Team { team_uid } instead of always assuming CurrentUser.
Proposed Changes
-
Extend auth-secret cache entries with owner metadata.
Update
AuthSecretEntryinapp/src/ai/harness_availability.rsto includeowner: SecretOwner. Convertwarp_graphql::object::Spacefrom fetched/createdManagedSecretvalues intoSecretOwner:SpaceType::User->SecretOwner::CurrentUserSpaceType::Team->SecretOwner::Team { team_uid: space.uid.into_inner() }
This keeps deletion ownership close to the existing fetch/create cache and avoids guessing owner at click time.
-
Add deletion to
HarnessAvailabilityModel.Add a method similar to
create_auth_secret:pub fn delete_auth_secret( &mut self, harness: Harness, name: String, owner: SecretOwner, ctx: &mut ModelContext<Self>, )The method should call
ManagedSecretManager::delete_secret(owner, name.clone()). On success, remove the matching owner/name entry fromAuthSecretFetchState::Loadedfor that harness and emit a new event. On failure, leave the cache unchanged, report the error, and emit a failure event with the harness/name/owner/error.Add events:
AuthSecretDeleted { harness, name, owner }AuthSecretDeletionFailed { harness, name, owner, error }
Existing subscribers that only care about menu freshness should refresh on
AuthSecretDeleted; subscribers that do not care should explicitly ignore both new events so match exhaustiveness remains clear. -
Add a selector action for deleting a secret.
Extend
AuthSecretSelectorActionwith enough data to delete the row, for example:DeleteSecret { name: String, owner: SecretOwner, }Track pending deletes in
AuthSecretSelectorwith a small set keyed by(harness, name, owner)or by a stable local row id if one is introduced. This satisfies PRODUCT behavior 9 and avoids duplicate delete requests from the same menu. -
Render the right-aligned
Xaffordance using the menu system's existing styling.Preferred implementation: extend
MenuItemFieldswith an optional right-side action, keeping the visual rendering path forwith_right_side_icon(Icon::X)consistent with other menus while allowing the right icon to dispatch a different action than the row.The right-side action should:
- Stop mouse event propagation so clicking
Xdoes not also dispatchSelectSecret. - Close the menu or disable the row while the delete request is pending.
- Preserve the existing row action when the user clicks anywhere outside the
X.
If changing
MenuItemFieldsis too broad during implementation, an acceptable alternative is a custom-label row inauth_secret_selector.rsthat composes the existing text/icon primitives and explicitly separates row-click fromXclick. Prefer the reusable menu extension only if it stays small and does not alter existing right-side icon behavior. - Stop mouse event propagation so clicking
-
Add a dedicated confirmation dialog before deleting.
Add a small destructive dialog view under
app/src/terminal/view/ambient_agent/that mirrors existing confirmation dialogs:- Title:
Delete secret - Body:
Are you sure you want to delete {SECRET_NAME}? This action cannot be undone. Any agents or environments referencing this secret will no longer have access to it. - Buttons:
Canceland destructiveDelete - Dismiss-to-cancel behavior and centered overlay placement
The dialog should retain a captured deletion payload containing harness, secret name, and owner. This avoids deleting the wrong target if selector state changes while the modal is open.
- Title:
-
Wire deletion action handling in
AuthSecretSelector.In the
DeleteSecretbranch:- Read the active harness from
AmbientAgentViewModel. - Close the selector menu/sidecar.
- Show the confirmation dialog with the captured deletion payload.
On confirmation:
- Mark the row pending.
- Call
HarnessAvailabilityModel::delete_auth_secret(harness, name, owner, ctx). - Refresh the menu into a disabled pending state.
On cancel or dismiss:
- Hide the dialog.
- Leave pending-delete state untouched and do not call the server-backed delete API.
In the selector's
HarnessAvailabilityModelsubscription:- On
AuthSecretDeleted, clear pending state for the matching(harness, name, owner)target and show a success toast when this selector initiated that exact deletion. - Always remove
CloudAgentSettings.last_selected_auth_secret[harness.config_name()]if it equals the deleted name, even when the user has switched to another active harness before the server responds. - Only when the deleted harness is still active, clear
AmbientAgentViewModel::selected_harness_auth_secret_name()if it matches the deleted name and refresh the visible menu/button state. - On
AuthSecretDeletionFailed, clear pending state for the matching(harness, name, owner)target, refresh the visible menu only when that harness is active, and show a failure toast containing the user-facing error when available.
Use
DismissibleToast::success(...)andDismissibleToast::error(...)viaToastStack::add_ephemeral_toast, matching nearby toast patterns. - Read the active harness from
-
Keep adjacent auth-secret surfaces consistent.
AuthSecretFtuxDropdown, orchestration pickers, and model selector subscribers must compile with the new deletion events. They can ignore failure events and refresh on success where they render auth secret lists. This ensures a secret deleted from the chip no longer appears in other already-mounted selectors after the model emits.
Testing and Validation
No new helper-only unit tests are retained in this implementation pass. The following behaviors remain suitable for future targeted coverage if higher-value seams are introduced:
-
Cache update behavior for PRODUCT behaviors 8, 10, and 11:
- Successful deletion removes only the matching auth secret from the loaded list and emits
AuthSecretDeleted. - Failed deletion leaves the list unchanged and emits
AuthSecretDeletionFailed. - Team-owned entries call delete with
SecretOwner::Team, user-owned entries call delete withSecretOwner::CurrentUser.
- Successful deletion removes only the matching auth secret from the loaded list and emits
-
Selector and confirmation routing for PRODUCT behaviors 4, 5, 7, 9, and 10:
- Clicking/selecting the row still selects the secret.
- Dispatching the delete affordance opens the confirmation dialog without deleting immediately.
- Cancelling or dismissing the confirmation dialog does not start deletion.
- Confirming deletion transitions into the existing pending-delete flow.
- Duplicate confirmed delete dispatches for a pending secret are ignored.
- Deleting the currently selected secret clears the selected model value and persisted setting.
-
Menu rendering coverage if the reusable
MenuItemFieldsright-side action API is introduced:- Existing right-side icon rendering remains decorative when no right-side action is set.
- A right-side action dispatches independently from the row action.
-
Manual validation against the screenshot state, if revisited later:
- Open the auth selector chip menu with multiple secrets.
- Confirm only existing secret rows have a right-aligned
X. - Confirm the "New" row and sidecar rows do not show an
X. - Click
Xand verify the destructive confirmation modal appears with the specified copy. - Cancel/dismiss the modal and verify the row remains untouched.
- Confirm deletion for an unselected secret and verify it disappears plus a success toast appears.
- Confirm deletion for the selected secret and verify the chip label falls back to no-secret/inherit state.
- Force a delete failure/offline state and verify the failure toast appears and the row remains.
-
Run focused checks:
cargo check -p warp cargo fmt -- --checkSkip test execution for this implementation pass per request.
Parallelization
Parallel agents are not recommended. The work is small and tightly coupled across one UI view, one shared model, and a small optional menu extension; splitting would increase merge risk around action/event names without materially reducing wall-clock time.
Risks and Mitigations
- Accidental row selection when clicking
X: stop event propagation on theXhit target and test the separate action path. - Deleting the wrong owner scope: store
SecretOwneron each fetchedAuthSecretEntry; do not infer owner from the active workspace at click time. - Stale selected secret after deletion: clear both
AmbientAgentViewModelselection andCloudAgentSettings.last_selected_auth_secretwhen the deleted name matches the active harness selection. - Over-broad menu API changes: keep any
MenuItemFieldsaddition opt-in so existing menus with right-side icons remain visually and behaviorally unchanged.