9.6 KiB
APP-3585: Setting for agent commit and PR attribution — Client Tech Spec
Product spec: specs/APP-3585/PRODUCT.md.
Server tech spec: warp-server/specs/APP-3585/TECH.md (PR https://github.com/warpdotdev/warp-server/pull/10476).
Problem
The attribution setting (whether Oz adds a Co-Authored-By line to commits and PRs) is currently a binary team-level flag. We need to turn it into a two-level setting: team admins get a three-way AdminEnablementSetting (Enable / Disable / RespectUserSetting), and individual users get their own boolean preference that takes effect when the team delegates.
This spec covers the warp-internal (client) changes. Anything about how the server stores or resolves the effective value lives in the server spec.
Relevant code
app/src/settings/ai.rs—define_settings_group!(AISettings, ...): new user-levelagent_attribution_enabledbool.app/src/workspaces/workspace.rs (631–776)— domain types for team settings (AdminEnablementSetting,WorkspaceSettings); flatenable_warp_attribution: AdminEnablementSettingadded directly onWorkspaceSettings.app/src/workspaces/user_workspaces.rs (1363–1458)— accessor methods for team settings; newget_agent_attribution_setting().app/src/workspaces/gql_convert.rs—From<GqlAdminEnablementSetting>mapping for the new field.crates/graphql/src/api/workspace.rs (124–146)—WorkspaceSettingscynic fragment; newAmbientAgentSettingsfragment.app/src/settings_view/ai_page.rs— newAgentAttributionWidget; followsCloudConversationStorageWidgetinprivacy_page.rs (1667–1770)for team-override rendering.app/src/settings/cloud_preferences_syncer.rs— existing syncer that publishes anySyncToCloud::Globally(...)setting to warp-server as aJsonPreferenceGSO. No changes required — reused as the user-preference transport.
Current state
AmbientAgentSettings on the server exposes enableWarpAttribution: Boolean!; the client does not fetch it. There is no user-level attribution preference, and attribution instructions are unconditionally present or absent in the agent prompt based solely on the server-side team boolean.
The client already has the transport we need for a user-level preference: any setting declared with SyncToCloud::Globally(...) via define_settings_group! is uploaded to warp-server by CloudPreferencesSyncer as a JsonPreference GSO with unique_key = Global_<StorageKey> (scoped per user). Other AISettings fields (e.g. IncludeAgentCommandsInHistory, ShowConversationHistory) already use this pattern, and warp-server reads them back by GSO lookup.
Proposed changes
1. New user-level client setting
Add agent_attribution_enabled to define_settings_group!(AISettings, ...) in app/src/settings/ai.rs:
- default
true SyncToCloud::Globally(RespectUserSyncSetting::Yes)(AI/coding preference, not a privacy/safety one)- implicit storage key
AgentAttributionEnabled
On toggle, the setting is persisted locally and the existing CloudPreferencesSyncer upserts the user's JsonPreference GSO (unique_key = {firebaseUID}_Global_AgentAttributionEnabled on the server side). No bespoke server call. See the server spec for the read path.
Because we use RespectUserSyncSetting::Yes, users who have turned "Sync settings across devices" off do not publish their preference to the server; the server falls back to the default (true) for those users. This matches how all other AISettings fields behave today.
2. Team-level field on WorkspaceSettings (flat, not wrapped)
Add a flat enable_warp_attribution: AdminEnablementSetting field (with #[serde(default)]) directly on WorkspaceSettings in app/src/workspaces/workspace.rs. No wrapper struct — the GQL field name matches the server directly.
UserWorkspaces::get_agent_attribution_setting() reads this field off the current team:
pub fn get_agent_attribution_setting(&self) -> AdminEnablementSetting {
self.current_team()
.map(|team| team.organization_settings.enable_warp_attribution.clone())
.unwrap_or_default()
}
This matches the shape of every other admin-enablement accessor on UserWorkspaces (team_allows_codebase_context, is_ai_allowed_in_remote_sessions, get_cloud_conversation_storage_enablement_setting).
3. GraphQL fetch
Add an AmbientAgentSettings cynic fragment in crates/graphql/src/api/workspace.rs with enable_warp_attribution: AdminEnablementSetting, and include it on the WorkspaceSettings fragment as ambient_agent_settings: Option<AmbientAgentSettings>. The wrapper is Option because WorkspaceSettings.ambientAgentSettings is nullable in the GQL schema (no !); this matches the existing convention for sandboxed_agent_settings. When the wrapper is None, gql_convert.rs falls back to AdminEnablementSetting::default() (= RespectUserSetting). The inner enable_warp_attribution is non-optional, as the server guarantees a value when the wrapper is present.
4. AI settings page widget
Add AgentAttributionWidget in app/src/settings_view/ai_page.rs. It reads UserWorkspaces::get_agent_attribution_setting() and derives the toggle state:
Enable→ locked onDisable→ locked offRespectUserSetting→ interactive, reflectsAISettings.agent_attribution_enabledIncluded in both the legacy full-page (Nonesubpage) and theSome(AISubpage::WarpAgent)subpage. The widget always renders; when AI is globally disabled, the toggle is shown disabled with a greyed-out label and description, matching the behavior of every other widget on the AI page (e.g.CloudAgentComputerUseWidget,CLIAgentWidget). Visual structure follows the AI-page convention: arender_separatorand abuild_sub_header("Agent Attribution", ...)precede the toggle row and description, mirroringCloudAgentComputerUseWidget. The citedCloudConversationStorageWidgetreference applies only to the team-override state machine (locked vs. interactive based onAdminEnablementSetting), not to the visual structure, since that widget lives on the Privacy page which has different conventions. For the locked-toggle tooltip, the widget usesWORKSPACE_OVERRIDE_TOOLTIP_MESSAGEfromapp/src/ai/execution_profiles/editor/ui_helpers.rs("This option is enforced by your organization's settings and cannot be customized."), matchingCloudAgentComputerUseWidgetand the rest of the AI page. The Privacy page uses a shorter "This setting is managed by your organization." string; we deliberately follow the AI-page wording here for consistency.
End-to-end flow (user toggles attribution off)
- User opens Settings → AI → Oz.
- Client reads the team's
enable_warp_attributionfrom cachedWorkspaceSettingsviaget_agent_attribution_setting(). - Setting is
RespectUserSetting, so the toggle is interactive and reflectsAISettings.agent_attribution_enabled(defaulttrue, showing as checked). - User clicks the toggle →
ToggleAgentAttributionaction setsAISettings.agent_attribution_enabledtofalse(persisted locally). CloudPreferencesSyncerpicks up the change and upserts the user'sJsonPreferenceGSO (unique_key = {firebaseUID}_Global_AgentAttributionEnabled,serialized_model.value = false).- Next Oz run: warp-server resolves the effective setting (team = RespectUserSetting → read user GSO = false) and excludes attribution instructions from the prompt. Oz creates a commit without the
Co-Authored-Byline.
flowchart TD
A[User toggles off] --> B[AISettings.agent_attribution_enabled = false]
B --> C[CloudPreferencesSyncer upsert]
C --> D[(warp-server generic_string_objects\nunique_key = firebaseUID_Global_AgentAttributionEnabled)]
D --> E[Next Oz run]
E --> F{Team setting}
F -->|Enable| G[Attribution included]
F -->|Disable| H[Attribution excluded]
F -->|RespectUserSetting| I[Read user GSO = false]
I --> H
Risks and mitigations
- Client ships ahead of server: the cynic fragment requires
enable_warp_attribution: AdminEnablementSetting!on the innerAmbientAgentSettingstype; if the client lands first,GetWorkspaceSettingsfails to parse. Mitigation: sequence the server change first (covered by the server spec). The outerOption<AmbientAgentSettings>wrapper already absorbs the case where the whole sub-object is missing. - User has settings sync off: their preference never reaches the server; the server defaults to
true. This is the documented behavior for anyRespectUserSyncSetting::Yessetting. - Widget enablement: when AI is globally disabled, the widget renders in a disabled state (greyed-out toggle, label, and description), consistent with every other widget on the AI page.
Testing and validation
- Unit tests in
app/src/workspaces/user_workspaces_tests.rs:test_agent_attribution_default_with_no_workspace— no workspace →RespectUserSettingtest_agent_attribution_forced_on_by_team— teamEnable→ accessor returnsEnabletest_agent_attribution_forced_off_by_team— teamDisable→ accessor returnsDisabletest_agent_attribution_respects_user_setting— teamRespectUserSetting→ accessor returnsRespectUserSetting
- UI manual test: verify each team-setting state renders the toggle in the correct locked/interactive state, in both the legacy and Oz subpages.
- End-to-end (once the server branch merges): run an Oz agent task that creates a commit with the setting off → verify no attribution line. Run with team-level
Enableand verify the client toggle is locked on; same forDisable.
Follow-ups
- Admin-panel UI (separate warp-server PR) to change the team-level control from a binary toggle to a three-way selector.