first pass of merging in warp (doesn't build)

This commit is contained in:
Ryan Ward
2026-07-01 16:08:58 -05:00
parent 2f64909469
commit 4770ac06b5
3662 changed files with 414574 additions and 89772 deletions
+57
View File
@@ -0,0 +1,57 @@
# REMOTE-1318: Merge org and user command denylists
## Summary
When an organization enforces a command denylist, users should still be able to add their own entries to make the denylist more restrictive. The org and user denylists merge into one list, where org-provided rows are non-removable and user-provided rows are fully editable. This applies everywhere the denylist appears: the settings page, the execution profile editor, and at command-execution time.
Figma: none provided
## Behavior
### Merged denylist
1. When the org (workspace) sets a command denylist override, the effective denylist is the **union** of the org denylist and the user's profile denylist, with duplicates removed. Org entries appear first in the list, followed by user entries.
2. Duplicate detection compares the string representation of each `AgentModeCommandExecutionPredicate`. If a user entry has the same regex string as an org entry, only the org entry appears in the merged list. The user's copy is still persisted in their profile — it is simply not displayed twice.
3. The merged denylist is used for command execution checks. A command that matches any entry in the merged list is denied (requires user confirmation), regardless of whether the matching entry came from the org or the user.
4. The org denylist applies identically across all execution profiles. There is no per-profile override of org entries.
5. When no org denylist override exists, behavior is unchanged — the user's profile denylist is the only source and all entries are user-editable.
### Settings UI — denylist editor (text input)
6. The denylist text input editor is enabled whenever AI mode is enabled, regardless of whether the org has a denylist override. The user can always type and submit new regex entries.
7. When the user submits a new entry, it is added to the user's profile denylist (not the org list). If the entry duplicates an existing org entry, it is still persisted in the user's profile but does not appear as a separate row (per invariant 2).
### Settings UI — per-row behavior
8. Each row in the denylist has an independent disabled/enabled state, determined by whether the entry came from the org denylist.
9. **Org rows**: the remove (×) button is disabled. Hovering the row shows a tooltip: "This option is enforced by your organization's settings and cannot be customized." The row text renders in the disabled text color.
10. **User rows**: the remove (×) button is enabled. No tooltip on hover. The row text renders in the normal foreground color. Clicking the remove button removes the entry from the user's profile denylist.
11. The denylist section is **not** wrapped in a single tooltip as a whole. Only individual org rows show the tooltip.
12. When the org removes an entry from their override that the user had also added independently, the user's copy becomes the sole source for that entry. It transitions to a user row (removable, no tooltip) on the next settings refresh.
### Settings UI — other lists (allowlist, directory allowlist, MCP lists)
13. Lists other than the command denylist retain their current behavior: when the org has an override, the entire list is disabled with the global tooltip. This spec does not change their behavior.
### Scope
14. The per-row merge and editability applies in both surfaces that render the command denylist:
- The legacy AI settings page (default profile denylist section).
- The execution profile editor view (per-profile denylist section).
### Edge cases
15. If the org denylist is set to an empty list (`Some([])`), the org override is considered active (the org has explicitly chosen "no org entries"), but the user's profile entries are still shown as user rows. The editor remains enabled.
16. If the user's profile denylist is empty and the org provides entries, only org rows appear. The editor is still enabled for the user to add entries.
17. When a user removes a user row, the row disappears immediately. The mouse state handles for remaining rows are recreated to stay in sync with the list.
+109
View File
@@ -0,0 +1,109 @@
# REMOTE-1318: Tech spec
See `PRODUCT.md` for user-facing behavior.
## Context
The command denylist controls which commands the agent must ask permission to execute. Two sources can contribute entries: the organization (workspace) and the user's execution profile.
**Current data flow:**
- `AiAutonomySettings.execute_commands_denylist: Option<Vec<AgentModeCommandExecutionPredicate>>` — org override, from `workspace.rs (663-670)`.
- `AIExecutionProfile.command_denylist: Vec<AgentModeCommandExecutionPredicate>` — per-profile user list, from `execution_profiles/mod.rs (233)`.
- `get_execute_commands_denylist_for_profile()` in `permissions.rs (395-413)` — resolves the effective list. Currently `unwrap_or_else`: org **replaces** user.
- `is_command_denylist_editable()` in `ai.rs (1730-1736)` — returns `false` when org override exists, disabling both the editor and all row remove buttons.
**Current UI rendering:**
- `render_list_section()` in `ui_helpers.rs (640-691)` — generic list renderer for the profile editor. Passes a single `is_editable` bool; when false, wraps the **entire** section in a workspace tooltip via `wrap_disabled_with_workspace_override_tooltip`.
- `render_command_denylist()` in `ai_page.rs (4483-4513)` — legacy settings page denylist. Same pattern: passes `!is_command_denylist_editable(app)` as a single `disabled` flag.
- `render_input_list()` in `settings_page.rs (1013-1056)` — renders the editor + item rows. Takes a single `disabled: bool` for all rows.
- `InputListItem` in `settings_page.rs (1003-1007)``{ item, mouse_state_handle, on_remove_action }`. No per-item disabled state.
- Mouse state handles are stored as `Vec<MouseStateHandle>` per list (one per row, for close buttons). Recreated when the list changes.
**Other callers of `render_input_list`** (must stay backward-compatible): `ai_page.rs` (allowlist, directory allowlist, MCP lists), `ui_helpers.rs` (all list types via `render_list_section`), `update_environment_form.rs` (environment setup commands).
## Proposed changes
### 1. Merge denylist in `permissions.rs`
Change `get_execute_commands_denylist_for_profile()`:
```
When org override is Some(org_list):
merged = org_list
for each item in user_profile.command_denylist:
if merged does not contain item:
merged.push(item)
return merged
When org override is None:
return user_profile.command_denylist (unchanged)
```
Add `pub fn get_org_execute_commands_denylist(ctx: &AppContext) -> Vec<AgentModeCommandExecutionPredicate>` — returns just the org entries (or empty vec). Used by rendering code to determine which rows are org-owned.
### 2. Per-item disabled state in `InputListItem` (`settings_page.rs`)
Add one field:
- `is_disabled: bool` — whether the remove button is disabled and text uses disabled color.
Remove the `disabled: bool` parameter from `render_input_list`. Each row uses `item.is_disabled` instead.
Keep `wrap_disabled_with_workspace_override_tooltip` in `ui_helpers.rs` — it is not needed by `render_input_list`. Tooltip wrapping is handled at the call site (see sections 4 and 5 below).
Update all 6 callers of `render_input_list`:
- Non-denylist callers: set `is_disabled` uniformly based on the existing global disabled logic.
- Denylist callers: set per-item `is_disabled` based on org membership.
### 3. Always enable denylist editor
`is_command_denylist_editable()` in `ai.rs`: remove the `has_override_for_execute_commands_denylist()` check. Return `self.is_any_ai_enabled(app)` only.
In `editor/mod.rs`, `update_all_editor_interaction_states()` (line 1269): change the denylist editor line to enable whenever AI is on (drop the `&& !ai_autonomy_settings.has_override_for_execute_commands_denylist()` condition).
In `ai_page.rs`, two handlers update the denylist editor state:
- `TeamsChanged` handler (line 474): same change.
- `IsAnyAIEnabled` handler (line 891): same change.
### 4. Profile editor denylist rendering (`ui_helpers.rs` + `editor/mod.rs`)
Add `command_denylist_tooltip_mouse_state_handles: Vec<MouseStateHandle>` to `ExecutionProfileEditorView`. Create alongside existing `command_denylist_mouse_state_handles` in `update_mouse_state_handles()`, one per merged-list item (used only for org rows).
Update `render_command_denylist_section()` in `ui_helpers.rs`:
- Get org denylist via `BlocklistAIPermissions::get_org_execute_commands_denylist(app)`
- Build rows directly using `render_alternating_color_list_item` (not via `render_list_section`) so each disabled org row can be individually wrapped with `wrap_disabled_with_workspace_override_tooltip`, which stays in `ui_helpers.rs`
- Always show the editor (regardless of org override)
`render_list_section()` is unchanged — non-denylist lists still use its existing `is_editable` + whole-list tooltip pattern.
### 5. Legacy settings page denylist rendering (`ai_page.rs`)
Add `command_denylist_tooltip_mouse_state_handles: Vec<MouseStateHandle>` to `AISettingsPageView`.
Update `render_command_denylist()`:
- Get org denylist
- Build `InputListItem`s with per-item `is_disabled` based on org membership
- Wrap each disabled row in `wrap_disabled_with_workspace_override_tooltip` (imported from `crate::ai::execution_profiles::editor::ui_helpers`)
- Always pass the editor
Update mouse state handle creation in `new()` and `AISettingsChangedEvent::AgentModeCommandExecutionDenylist` handler to size both handle Vecs to the merged list.
### 6. Profile data event handling
In `ExecutionProfileEditorView`, subscribe to `UserWorkspacesEvent::TeamsChanged` to refresh mouse state handles and denylist rendering when the org override changes at runtime.
## Testing and validation
**Unit tests** — extend `permissions_test.rs`:
- Test that `get_execute_commands_denylist_for_profile` returns the union when org override is set (PRODUCT.md invariant 1).
- Test deduplication: user entry matching org entry appears once (invariant 2).
- Test merged list is used for execution checks (invariant 3).
- Test cross-profile: org denylist applies to all profiles (invariant 4).
- Test no-override path is unchanged (invariant 5).
- Test empty org override (`Some([])`) still allows user entries (invariant 15).
- Test `get_org_execute_commands_denylist` returns the right entries.
**Manual verification:**
- With an org denylist override active: verify the editor is enabled, org rows show disabled × and tooltip, user rows are removable.
- Without an org override: verify unchanged behavior.
- Add a user entry that duplicates an org entry: verify it doesn't appear twice.
- Remove a user entry: verify it disappears and remaining rows render correctly.
**Presubmit:** `cargo fmt` + `cargo clippy` must pass.