Files
galaxy/specs/cloud-mode-auth-secret-deletion/TECH.md
T

176 lines
11 KiB
Markdown

# 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:61` defines `AuthSecretSelectorAction`; 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:147` subscribes the selector to `HarnessAvailabilityModel` auth-secret load/create/fetch-failure events and refreshes the menu/button.
- `app/src/terminal/view/ambient_agent/auth_secret_selector.rs:389` builds the menu. Loaded secrets are rendered as `MenuItem::Item` rows with `SelectSecret(name)` actions; the "New" row is separate and opens the sidecar.
- `app/src/terminal/view/ambient_agent/auth_secret_selector.rs:498` handles selector actions and persists selected/cleared secrets in `CloudAgentSettings.last_selected_auth_secret`.
- `app/src/ai/harness_availability.rs:55` stores per-harness auth secret fetch state as `AuthSecretFetchState::Loaded(Vec<AuthSecretEntry>)`.
- `app/src/ai/harness_availability.rs:184` fetches harness auth secrets from `ManagedSecretsClient::list_harness_auth_secrets`.
- `app/src/ai/harness_availability.rs:250` already centralizes secret creation through `ManagedSecretManager`, updates the local cache, and emits create/failure events.
- `crates/managed_secrets/src/manager.rs:87` exposes `ManagedSecretManager::delete_secret(owner, name)`.
- `app/src/server/server_api/managed_secrets.rs:132` wires `delete_managed_secret` to the server GraphQL mutation.
- `app/src/menu.rs:394` defines reusable `MenuItemFields`. 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.rs` and `app/src/settings_view/mcp_servers/destructive_mcp_confirmation_dialog.rs` establish the preferred `Dialog` + `DangerPrimaryTheme` pattern 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
1. Extend auth-secret cache entries with owner metadata.
Update `AuthSecretEntry` in `app/src/ai/harness_availability.rs` to include `owner: SecretOwner`. Convert `warp_graphql::object::Space` from fetched/created `ManagedSecret` values into `SecretOwner`:
- `SpaceType::User` -> `SecretOwner::CurrentUser`
- `SpaceType::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.
2. Add deletion to `HarnessAvailabilityModel`.
Add a method similar to `create_auth_secret`:
```rust
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 from `AuthSecretFetchState::Loaded` for 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.
3. Add a selector action for deleting a secret.
Extend `AuthSecretSelectorAction` with enough data to delete the row, for example:
```rust
DeleteSecret {
name: String,
owner: SecretOwner,
}
```
Track pending deletes in `AuthSecretSelector` with 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.
4. Render the right-aligned `X` affordance using the menu system's existing styling.
Preferred implementation: extend `MenuItemFields` with an optional right-side action, keeping the visual rendering path for `with_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 `X` does not also dispatch `SelectSecret`.
- 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 `MenuItemFields` is too broad during implementation, an acceptable alternative is a custom-label row in `auth_secret_selector.rs` that composes the existing text/icon primitives and explicitly separates row-click from `X` click. Prefer the reusable menu extension only if it stays small and does not alter existing right-side icon behavior.
5. 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: `Cancel` and destructive `Delete`
- 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.
6. Wire deletion action handling in `AuthSecretSelector`.
In the `DeleteSecret` branch:
- 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 `HarnessAvailabilityModel` subscription:
- 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(...)` and `DismissibleToast::error(...)` via `ToastStack::add_ephemeral_toast`, matching nearby toast patterns.
7. 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:
1. 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 with `SecretOwner::CurrentUser`.
2. 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.
3. Menu rendering coverage if the reusable `MenuItemFields` right-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.
4. 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 `X` and 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.
5. Run focused checks:
```sh
cargo check -p warp
cargo fmt -- --check
```
Skip 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 the `X` hit target and test the separate action path.
- **Deleting the wrong owner scope:** store `SecretOwner` on each fetched `AuthSecretEntry`; do not infer owner from the active workspace at click time.
- **Stale selected secret after deletion:** clear both `AmbientAgentViewModel` selection and `CloudAgentSettings.last_selected_auth_secret` when the deleted name matches the active harness selection.
- **Over-broad menu API changes:** keep any `MenuItemFields` addition opt-in so existing menus with right-side icons remain visually and behaviorally unchanged.