6.6 KiB
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, fromworkspace.rs (663-670).AIExecutionProfile.command_denylist: Vec<AgentModeCommandExecutionPredicate>— per-profile user list, fromexecution_profiles/mod.rs (233).get_execute_commands_denylist_for_profile()inpermissions.rs (395-413)— resolves the effective list. Currentlyunwrap_or_else: org replaces user.is_command_denylist_editable()inai.rs (1730-1736)— returnsfalsewhen org override exists, disabling both the editor and all row remove buttons.
Current UI rendering:
render_list_section()inui_helpers.rs (640-691)— generic list renderer for the profile editor. Passes a singleis_editablebool; when false, wraps the entire section in a workspace tooltip viawrap_disabled_with_workspace_override_tooltip.render_command_denylist()inai_page.rs (4483-4513)— legacy settings page denylist. Same pattern: passes!is_command_denylist_editable(app)as a singledisabledflag.render_input_list()insettings_page.rs (1013-1056)— renders the editor + item rows. Takes a singledisabled: boolfor all rows.InputListIteminsettings_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_disableduniformly based on the existing global disabled logic. - Denylist callers: set per-item
is_disabledbased 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:
TeamsChangedhandler (line 474): same change.IsAnyAIEnabledhandler (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 viarender_list_section) so each disabled org row can be individually wrapped withwrap_disabled_with_workspace_override_tooltip, which stays inui_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
InputListItems with per-itemis_disabledbased on org membership - Wrap each disabled row in
wrap_disabled_with_workspace_override_tooltip(imported fromcrate::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_profilereturns 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_denylistreturns 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.