Initial public release of Warp.

Repo-Sync-Origin: warpdotdev/warp-internal@12af1d983b
This commit is contained in:
David Stern
2026-04-28 08:43:33 -05:00
commit 0dbd3d567a
4982 changed files with 1431549 additions and 0 deletions
+69
View File
@@ -0,0 +1,69 @@
# Update Onboarding Agent Autonomy Settings for Full and Partial — Product Spec
GitHub issue: https://github.com/warpdotdev/warp-external/issues/1066
Figma: none provided
## Summary
Update the agent autonomy presets applied when a user picks **Full** or **Partial** on the onboarding agent slide so the resulting default execution profile matches the subtitle each option advertises. Update the **Partial** subtitle to describe the new, more permissive default. **Full** now unconditionally allows reading files, applying code diffs, and executing commands without prompting; **Partial** always allows reading files, always asks before applying code diffs, and lets the agent decide on command execution (asking only for sensitive commands).
In addition, this change fixes three pre-existing bugs in the path that applies those presets onto the user's default `AIExecutionProfile` so the selections actually land where the user can see them, while also not clobbering an existing user's stored cloud profile when they log in at the end of onboarding.
## Problem
**1. Autonomy presets did not match the subtitles.** The onboarding agent slide (`crates/onboarding/src/slides/agent_slide.rs`) advertises Full autonomy as "Runs commands, writes code, and reads files without asking." However, picking Full currently seeds the default execution profile with `AgentDecides` for `apply_code_diffs`, `read_files`, and `execute_commands`. `AgentDecides` is defined as "The Agent chooses the safest path: acting on its own when confident, and asking for approval when uncertain" — which means Full autonomy can still prompt the user, contradicting the onboarding copy. Partial autonomy today sets `read_files = AgentDecides`, `apply_code_diffs = AlwaysAsk`, `execute_commands = AlwaysAsk`, and `mcp_permissions = AlwaysAsk`, with the subtitle "Can plan and read files. Asks before making any changes." That is stricter than the experience we want to ship for Partial: file reads should not be gated behind prompts, and low-risk commands should be able to run without explicit approval while the agent still defers to the user for risky commands and any changes.
**2. Onboarding autonomy selections were silently dropped for logged-out users.** When the user completes onboarding before logging in and opts to skip login, every onboarding-driven edit on the default profile (base model + all permission fields) is dropped on the floor — the default profile ends up at the bare `AIExecutionProfile::default()` values. The underlying cause is that the default profile is in an `Unsynced` state and the code path that creates the cloud-backed object bails early when no personal Warp Drive owner is available, discarding the locally-modified profile.
**3. Existing users logging in at the end of onboarding saw duplicate / default-valued profiles.** When a user completes onboarding pre-login and then logs into an existing account, their cloud-stored default profile arrives via the initial bulk cloud load. That load path does not fire per-object `ObjectCreated` events (it emits a single `InitialLoadCompleted` instead), so the execution-profiles model never learns about the existing cloud profile. The subsequent onboarding edits hit the `Unsynced` branch and create a **duplicate** cloud default profile that is mostly default values with a few onboarding fields written, leaving the user with a default profile that matches neither their stored cloud values nor the onboarding selections.
**4. Existing users had their cloud-stored profile silently overwritten by onboarding.** Even with the reconciliation above in place, `apply_agent_settings` still writes the onboarding-selected base_model and permissions on top of the existing cloud profile, discarding the user's prior customizations. Existing users expect their stored profile to be respected when they log in, not replaced by the defaults they clicked through in an onboarding flow.
## Goals
- Make Full autonomy seed the default execution profile so the agent never asks for approval for read-file, apply-code-diff, or execute-command actions, matching the existing "Runs commands, writes code, and reads files without asking" subtitle.
- Make Partial autonomy seed the default execution profile so reading files is always allowed, applying code diffs always asks, and executing commands delegates the decision to the agent (agent asks only for sensitive commands).
- Update the Partial subtitle on the onboarding slide to accurately describe the new defaults.
- Preserve existing workspace-override behavior: when a team workspace enforces a given permission, the onboarding selection continues to skip writing that permission (the workspace value wins).
- Preserve the existing behavior of the **None** autonomy option (all permissions `AlwaysAsk`).
- Make onboarding's autonomy selections stick for users who finish onboarding without logging in, so their choices are visible on the default execution profile rather than silently dropped.
- Respect an existing user's cloud-stored default execution profile when they log in at the end of onboarding: the onboarding-selected base_model and autonomy permissions must not overwrite values the user has previously stored in the cloud.
- For brand-new users (no cloud default profile yet) logging in at the end of onboarding, the onboarding selections continue to seed the default profile and cleanly promote to a single cloud object on first login (no duplicate default profiles created).
## Non-goals
- Changing the post-onboarding Settings UI, the execution-profiles editor, or the permissions surface outside of the onboarding-driven defaults.
- Changing Full autonomy's `write_to_pty` behavior, which is already `AlwaysAllow`.
- Changing MCP permission defaults for **None**. Full and Partial have their MCP defaults updated alongside the other permissions so the resulting execution profile is internally consistent with the subtitle each option advertises (Full: never asks; Partial: agent decides on low-risk actions). None continues to seed `mcp_permissions = AlwaysAsk`.
- Retroactively updating execution profiles for users who already completed onboarding. This change only affects the defaults written at the moment the onboarding Agent slide is confirmed.
- Visual layout or copy changes to the Full, None, or workspace-enforced subtitles on the onboarding agent slide. Only the Partial subtitle string changes.
- Merging onboarding selections with existing cloud-stored profile values (e.g. taking the onboarding base_model but preserving cloud permissions). Existing cloud profiles are preserved wholesale; onboarding selections are preserved wholesale for fresh profiles. No per-field cherry-picking.
- Changing the non–execution-profile AISettings that onboarding also writes (e.g. `default_session_mode`, `should_render_cli_agent_footer`, `show_agent_notifications`, UI customization flags). Those continue to follow the onboarding selection regardless of whether the user is new or existing.
## Behavior
1. When a user lands on the onboarding agent slide, three autonomy options are shown in the same order and layout as today: **Full**, **Partial** (default), **None**.
2. The **Full** subtitle remains exactly `Runs commands, writes code, and reads files without asking.` — no copy change on this row.
3. The **Partial** subtitle changes to `Can plan, read files, and execute low-risk commands. Asks before making any changes or executing sensitive commands.` This replaces the current string `Can plan and read files. Asks before making any changes.`
4. The **None** subtitle remains exactly `Takes no actions without your approval.` — no copy change on this row.
5. When the user completes onboarding with **Full** selected and the default execution profile is not overridden by a team workspace for a given permission, the default profile is written with:
- `apply_code_diffs = AlwaysAllow`
- `read_files = AlwaysAllow`
- `execute_commands = AlwaysAllow`
- `mcp_permissions = AlwaysAllow`
- `write_to_pty = AlwaysAllow` (unchanged)
6. When the user completes onboarding with **Partial** selected and the default execution profile is not overridden by a team workspace for a given permission, the default profile is written with:
- `apply_code_diffs = AlwaysAsk`
- `read_files = AlwaysAllow`
- `execute_commands = AgentDecides`
- `mcp_permissions = AgentDecides`
- `write_to_pty = AlwaysAsk` (unchanged)
7. When the user completes onboarding with **None** selected, the default profile is written with every `ActionPermission` field set to `AlwaysAsk` and `write_to_pty = AlwaysAsk`. No behavior change from today.
8. Workspace overrides continue to take precedence. For any permission field the user's team workspace enforces, onboarding does not write that field — regardless of which autonomy option was chosen. The user's selection still applies to every other, non-overridden permission field on the same profile.
9. The workspace-enforced autonomy view of the onboarding slide (rendered via `render_autonomy_workspace_enforced`) is unchanged: the user still sees the "Set by Team Workspace" panel and cannot pick Full/Partial/None.
10. The agent SDK and execution-profile enforcement paths read the same `ActionPermission` fields as today — after onboarding applies its defaults, the behavior of the agent at runtime (including prompts, auto-approvals, and denylist/allowlist enforcement for execute commands) follows directly from those permission values. In particular:
- With Full, the agent does not pause for approval on read/apply/execute actions in the default profile.
- With Partial, the agent never pauses for read-file actions, always pauses for apply-code-diff actions, and — for execute commands — pauses only when the command falls outside the existing auto-approve allowlist / is on the denylist (the existing `AgentDecides` runtime behavior).
11. A user who completes onboarding, then changes their autonomy-related permissions in Settings, keeps those Settings changes. Re-running onboarding (for users who can) reapplies the defaults in this spec to the default profile only if that profile is not already backed by a cloud object — see invariants 13–15 for the preservation rules that now apply in the logged-in case.
12. No change to keyboard navigation, focus order, "Disable Oz" checkbox interaction, or any other behavior of the onboarding agent slide beyond the subtitle text and the permission values written at completion.
13. **Brand-new user, onboarding then skip login.** When the user completes onboarding, the onboarding-selected model and autonomy values (per invariants 5–7) are applied to the local default execution profile and remain visible in Settings → AI → Execution Profiles after onboarding — even without logging in. Before this change these edits were silently dropped. The next time the user triggers an edit after eventually logging in, that local profile is promoted to a single cloud-backed default profile carrying those same values.
14. **Existing user, onboarding then log in.** When the user completes onboarding and then logs into an account that already has a cloud-stored default execution profile, the cloud profile is adopted unchanged. The onboarding-selected `base_model`, `apply_code_diffs`, `read_files`, `execute_commands`, `mcp_permissions`, and `write_to_pty` are **not** written onto the existing cloud profile; the user sees their previously stored values in Settings → AI → Execution Profiles. Non–execution-profile onboarding settings (session default mode, CLI agent toolbar visibility, agent notifications, UI customization flags) continue to follow the onboarding selection.
15. **Brand-new account, onboarding then sign up.** When the user completes onboarding and then signs up for a new account (no cloud default profile yet), the onboarding selections from invariants 5–7 are applied to the default profile and saved to cloud as a single default execution profile for that account. No duplicate default profiles are created and the user sees the onboarding-derived values in Settings.
16. Workspace overrides (invariant 8) continue to apply in all three above cases — even when a cloud profile is being preserved, an enforced workspace autonomy value supersedes the stored profile value at enforcement time, as it does today.
+131
View File
@@ -0,0 +1,131 @@
# Update Onboarding Agent Autonomy Settings for Full and Partial — Tech Spec
Product spec: `specs/GH1066/product.md`
GitHub issue: https://github.com/warpdotdev/warp-external/issues/1066
## Context
Onboarding presents three autonomy options on the agent slide. When the user completes onboarding with Agent-Driven Development selected, `apply_onboarding_settings` translates the chosen `AgentAutonomy` into `ActionPermission` values and writes them onto the default `AIExecutionProfile`.
Investigating the original issue surfaced three related bugs in the plumbing that connects the onboarding UI to the default profile. The tech spec therefore covers four changes, summarized up front so the overall shape is easy to follow:
1. **Autonomy → permission mapping** in `app/src/settings/onboarding.rs` — the original scope of the issue.
2. **Partial subtitle copy** on the agent slide — the original scope of the issue.
3. **`edit_profile_internal` persistence when no personal drive is available** in `app/src/ai/execution_profiles/profiles.rs` — fixes onboarding-driven writes being silently dropped for logged-out users.
4. **`InitialLoadCompleted` reconciliation** in `app/src/ai/execution_profiles/profiles.rs` — fixes existing users' cloud default profile being missed after login, which had been causing onboarding to create a duplicate cloud object.
5. **Preserve existing cloud default profile in `apply_agent_settings`** in `app/src/settings/onboarding.rs` — ensures the user's stored cloud values survive when they log into an existing account at the end of onboarding.
Relevant code:
- `crates/onboarding/src/slides/agent_slide.rs` — `autonomy_options` array in `render_autonomy_section` (Full/Partial/None titles, subtitles, and the `AgentAutonomy` variants dispatched on click) and the `AgentAutonomy` enum / `AgentDevelopmentSettings::new` (keeps `Partial` as the default selected autonomy).
- `app/src/settings/onboarding.rs` — `apply_onboarding_settings`, `apply_agent_settings`, `OnboardingAutonomyPermissions`, `action_permissions_for_onboarding_autonomy`.
- `app/src/ai/execution_profiles/mod.rs` — `ActionPermission` / `WriteToPtyPermission` enums. Already expose `AlwaysAllow`, `AlwaysAsk`, `AgentDecides`; no new variants needed.
- `app/src/ai/execution_profiles/profiles.rs` — `AIExecutionProfilesModel`, `DefaultProfileState` (`Unsynced` / `Synced` / `Cli`), `edit_profile_internal`, `handle_cloud_model_event`, and the `set_*` setters called by `apply_agent_settings`.
- `app/src/workspaces/user_workspaces.rs` (via `UserWorkspaces::ai_autonomy_settings`) — the `has_override_for_*` helpers consumed by `apply_agent_settings`. Behavior is unchanged; the new defaults continue to respect overrides the same way.
- `app/src/cloud_object/model/persistence.rs` — `CloudModel::update_objects_from_initial_load` (the cloud bulk-load path that suppresses per-object events) and `CloudModelEvent::InitialLoadCompleted` (the single event fired at the end of that path).
- `app/src/auth/auth_state.rs` — `AuthStateProvider` and `AuthState::user_id()`, which `UserWorkspaces::personal_drive()` consults to decide whether a personal drive is available.
See product spec for user-visible behavior.
## Proposed changes
### 1. Update the Partial subtitle on the onboarding agent slide
In `crates/onboarding/src/slides/agent_slide.rs`, update the second entry of the `autonomy_options` array in `render_autonomy_section` so the Partial row's subtitle reads:
```
Can plan, read files, and execute low-risk commands. Asks before making any changes or executing sensitive commands.
```
This is a pure string swap inside `autonomy_options`. The Full row and None row are unchanged. No layout, height, or keyboard-navigation code changes — the row height constant `OPTION_HEIGHT = 72.` already accommodates two-line wrapping of comparable length strings, and `render_two_line_button` wraps subtitle text internally.
### 2. Update the autonomy → permission mapping
In `app/src/settings/onboarding.rs`, rewrite the `AgentAutonomy::Full` and `AgentAutonomy::Partial` arms of `action_permissions_for_onboarding_autonomy` so the returned `OnboardingAutonomyPermissions` match the values in invariants 5 and 6 of the product spec. The `AgentAutonomy::None` arm is unchanged.
```rust
match autonomy {
AgentAutonomy::Full => OnboardingAutonomyPermissions {
apply_code_diffs: ActionPermission::AlwaysAllow,
read_files: ActionPermission::AlwaysAllow,
execute_commands: ActionPermission::AlwaysAllow,
mcp_permissions: ActionPermission::AlwaysAllow,
write_to_pty: WriteToPtyPermission::AlwaysAllow,
},
AgentAutonomy::Partial => OnboardingAutonomyPermissions {
apply_code_diffs: ActionPermission::AlwaysAsk,
read_files: ActionPermission::AlwaysAllow,
execute_commands: ActionPermission::AgentDecides,
mcp_permissions: ActionPermission::AgentDecides,
write_to_pty: WriteToPtyPermission::AlwaysAsk,
},
AgentAutonomy::None => OnboardingAutonomyPermissions {
apply_code_diffs: ActionPermission::AlwaysAsk,
read_files: ActionPermission::AlwaysAsk,
execute_commands: ActionPermission::AlwaysAsk,
mcp_permissions: ActionPermission::AlwaysAsk,
write_to_pty: WriteToPtyPermission::AlwaysAsk,
},
}
```
The caller (`apply_agent_settings`) continues to gate each `set_*` call on the workspace override flag and does not need to change shape for this mapping update: its logic is "if the workspace does not enforce this permission, write the onboarding-derived value." The caller *does* gain a preservation guard (change 5 below) but the guard does not affect the mapping itself.
### 3. MCP and `write_to_pty` defaults
The issue text does not mention MCP or `write_to_pty`, but per reviewer feedback the MCP defaults for Full and Partial are aligned with the updated autonomy semantics so the resulting profile is internally consistent with each option's subtitle:
- Full: `mcp_permissions = AlwaysAllow` (changed from `AgentDecides`) so "without asking" is true for MCP tool calls as well.
- Partial: `mcp_permissions = AgentDecides` (changed from `AlwaysAsk`) so low-risk MCP tool calls can proceed without a prompt, mirroring how `execute_commands` behaves for Partial.
- None: `mcp_permissions = AlwaysAsk` (unchanged).
- `write_to_pty` is unchanged for every variant: `AlwaysAllow` for Full, `AlwaysAsk` for Partial and None.
Calling this out explicitly here so reviewers don't read the MCP change as an accidental out-of-scope tweak.
### 4. Persist Unsynced edits locally when no personal drive is available
In `AIExecutionProfilesModel::edit_profile_internal` (`app/src/ai/execution_profiles/profiles.rs`), the `Unsynced` branch previously required a `personal_drive` to transition to `Synced` and silently returned otherwise — dropping the caller-supplied profile mutation. That dropping path is why the onboarding Full/Partial selections never showed up for logged-out users: every `set_*` on the default profile started from the original `Unsynced` clone, mutated it, then threw it away.
The fix splits the branch into two arms:
- `personal_drive` available → create a cloud object and transition to `Synced`, as before.
- `personal_drive` unavailable → write the mutated profile back into `DefaultProfileState::Unsynced` so subsequent reads (and subsequent edits after login) see the updated values.
Both arms emit `AIExecutionProfilesModelEvent::ProfileUpdated(profile_id)` so views re-render.
This keeps the invariant that "the default profile is never reverted" and guarantees onboarding-driven writes on a logged-out user accumulate locally. When that user later logs in and makes an edit, the first edit path will promote the accumulated local profile to a single cloud-backed default object (assuming no cloud default arrives from the account in the meantime — see change 5).
### 5. Reconcile execution-profile state on `InitialLoadCompleted`
The initial cloud load path (`CloudModel::update_objects_from_initial_load` → `upsert_from_server_object_internal`) intentionally inserts objects into `CloudModel` with `emit_events = false`. It emits a single `CloudModelEvent::InitialLoadCompleted` at the end rather than per-object `ObjectCreated` events. `AIExecutionProfilesModel::handle_cloud_model_event` previously only reacted to per-object events, so execution profiles loaded during initial login sync were invisible to the model: it stayed in `Unsynced` even though the user already had a cloud default.
Add a handler for `CloudModelEvent::InitialLoadCompleted` that calls a new `reconcile_with_cloud_state_after_initial_load`. Reconciliation:
- If the model is `Unsynced` and `CloudModel` contains an execution profile flagged `is_default_profile`, transition to `Synced` adopting the cloud profile's `sync_id` under the existing `ClientProfileId`. Emits `ProfileUpdated`.
- For any non-default profile already in `CloudModel` that isn't tracked in `profile_id_to_sync_id`, register a fresh `ClientProfileId → sync_id` mapping so later edits target the real cloud object. Emits `ProfileCreated` if any non-default profile was newly registered.
This is a pure catch-up on state; it does not modify any cloud object. Combined with change 4, the end-to-end state for an existing user logging in after onboarding is: onboarding's pre-login local writes (if any) land on the Unsynced profile, then initial load arrives, then `InitialLoadCompleted` reconciliation replaces the local Unsynced state with the existing cloud profile's `sync_id`. The local onboarding writes are intentionally discarded at that point in favor of the user's stored values — see change 6 for what happens next.
### 6. Preserve the existing cloud default profile inside `apply_agent_settings`
In `app/src/settings/onboarding.rs`, `apply_agent_settings` runs from `CloudPreferencesSyncer::InitialLoadCompleted` (in `handle_cloud_preferences_syncer_event`) once the user is logged in and the cloud reconciliation from change 5 has already run. If we unconditionally write the onboarding-selected base_model and permissions on top of the now-`Synced` default profile, we clobber values the user previously stored in the cloud.
Add a short-circuit at the top of the `AIExecutionProfilesModel::handle(app).update` closure:
```rust
let default_profile_info = profiles.default_profile(ctx);
let default_profile_id = *default_profile_info.id();
if default_profile_info.sync_id().is_some() {
// Existing cloud default profile — preserve stored values.
return;
}
```
Semantics:
- `sync_id().is_some()` means the default profile is backed by a cloud object (either loaded at startup from SQLite cache, or reconciled from the post-login initial load via change 5). That's the marker of "existing user."
- `sync_id().is_none()` keeps its existing meaning: a fresh `Unsynced` local default (brand-new user or new account that has no cloud default yet, possibly with accumulated onboarding writes from change 4). Those continue to flow through the `set_base_model` + permission setters, which will either update the local Unsynced profile (still no personal drive) or promote it to a single cloud object (personal drive now available).
Scope of the short-circuit is strictly the execution-profile block. The preceding `AISettings` updates in `apply_agent_settings` (`default_session_mode`, `should_render_cli_agent_footer`, `show_agent_notifications`) and the sibling `apply_ui_customization_settings` / `is_any_ai_enabled` writes in `apply_onboarding_settings` are **not** affected — onboarding continues to set those for existing users per invariant 14.
## Testing and validation
Each numbered invariant in `specs/GH1066/product.md` maps to at least one test or manual step below.
### Unit tests
Two sibling test files following the `${filename}_tests.rs` + `#[cfg(test)] #[path = ...] mod tests;` convention:
- `app/src/ai/execution_profiles/profiles_tests.rs` covers the execution-profile model changes.
- `edits_persist_on_unsynced_default_profile_when_logged_out` — builds the model with a logged-out `AuthStateProvider` (via a new `AuthStateProvider::new_logged_out_for_test()` helper in `app/src/auth/auth_state.rs`), calls `set_apply_code_diffs(AlwaysAllow)`, and asserts the default profile now reads `AlwaysAllow`. This would fail under the pre-fix `edit_profile_internal` because the mutated profile was dropped. Guards change 4 and invariant 13.
- `reconciles_unsynced_default_profile_with_cloud_after_initial_load` — seeds a cloud default profile via `CloudModel::update_objects_from_initial_load` (the no-events path), emits `CloudModelEvent::InitialLoadCompleted`, asserts the model adopted the cloud profile's `sync_id`, and verifies a subsequent edit targets that same `sync_id` (proving there is no duplicate). Guards change 5 and invariants 14/15's "no duplicate default profiles" clause.
- `app/src/settings/onboarding_tests.rs` covers the existing-user preservation behavior end-to-end.
- `apply_onboarding_settings_preserves_existing_cloud_profile_on_existing_user_login` — seeds an existing user's cloud default profile with distinguishable stored values (`base_model = "claude-existing-cloud-model"`, permissions all `AlwaysAllow`), fires the initial-load reconciliation, calls `apply_onboarding_settings` with a `SelectedSettings::AgentDrivenDevelopment` that picks a different model and `AgentAutonomy::None` (which would map to every permission being `AlwaysAsk`), and asserts every preserved field still reads its cloud-stored value. Guards change 6 and invariant 14.
The earlier pure-function table-driven test over `action_permissions_for_onboarding_autonomy` was removed in favor of these end-to-end tests, which cover the mapping transitively and also catch the plumbing issues.
### Manual validation
- Fresh onboarding, pick **Full**, finish the flow. Open Settings → AI → Execution Profiles and confirm the default profile shows `apply_code_diffs`, `read_files`, `execute_commands`, and `mcp_permissions` all as "Always allow," and `write_to_pty` as "Always allow" (invariant 5).
- Fresh onboarding, pick **Partial**. Confirm `apply_code_diffs = Always ask`, `read_files = Always allow`, `execute_commands = Agent decides`, `mcp_permissions = Agent decides`, `write_to_pty = Always ask` (invariant 6). Also confirm the Partial subtitle on the slide reads the new text (invariant 3).
- Fresh onboarding, pick **None**. Confirm all permissions show "Always ask" (invariant 7).
- From a user in a team workspace that enforces `execute_commands` (or any other supported override), run onboarding and pick Full. Confirm only the enforced field is left untouched and the other fields follow the new Full defaults (invariant 8).
- Visually verify the slide still renders three rows at the same height and the keyboard up/down cycling between Full → Partial → None (and wrap-around) still works, confirming no regression to invariants 1, 9, 12.
- Start an agent session after onboarding with Partial. Issue a request whose command is auto-approve eligible and confirm it runs without a prompt; issue a request whose command is sensitive / outside the allowlist and confirm the agent pauses for approval (invariant 10, Partial row).
- Start an agent session after onboarding with Full. Issue the same kinds of requests and confirm the agent never pauses for read-file, apply-code-diff, or execute-command actions (invariant 10, Full row).
- **Brand-new user, skip login at the end of onboarding.** Finish onboarding picking Full, click "Skip login" on the login slide. Open Settings → AI → Execution Profiles without logging in and confirm the default profile shows the Full autonomy values (not the bare `AIExecutionProfile::default()` values). Guards invariant 13. Log in afterward and confirm the same values are preserved and eventually backed by a single cloud object (no duplicate default profiles).
- **Existing user, log in at the end of onboarding.** Log into an account that already has a default execution profile whose permissions differ from the onboarding-selected autonomy. Finish onboarding picking an autonomy that differs from what's stored (e.g. pick Full when cloud has Partial-ish values). Confirm that after login Settings → AI → Execution Profiles shows the user's previously stored values, not the onboarding values. Guards invariant 14.
- **Brand-new account, sign up at the end of onboarding.** Finish onboarding picking Partial, then sign up for a fresh account. Confirm exactly one default execution profile exists in the account and its values match Partial. Guards invariant 15.
## Risks and mitigations
### Risk: changing defaults overwrites explicit Settings choices (brand-new user re-running onboarding)
`apply_agent_settings` writes permission fields on the default profile whenever onboarding is completed and the profile is still `Unsynced`. A user who completed onboarding, kept their local Unsynced profile, tweaked `apply_code_diffs` in Settings, and then re-ran onboarding would see those explicit changes overwritten.
Mitigation: for any user who has logged in and whose profile has promoted to `Synced`, the preservation guard in change 6 now prevents that overwrite. The remaining case (fully logged-out user re-running onboarding) is an extreme edge case and acceptable given the rest of the mitigation story.
### Risk: Full autonomy now grants `AlwaysAllow` for execute commands
Moving from `AgentDecides` to `AlwaysAllow` on `execute_commands` removes the `AgentDecides`-mediated "ask when uncertain" prompt for sensitive commands. This matches the subtitle, but it is a meaningful increase in Full's default ambient permission.
Mitigation: the command denylist still takes precedence even when `execute_commands = AlwaysAllow`, so commands on the denylist continue to be blocked / gated by the existing enforcement path rather than being auto-run. `AlwaysAllow` is also the existing enum variant already used by `write_to_pty` for Full autonomy, so runtime enforcement of the new default is consistent with what's already shipping and no new codepath is introduced. The slide copy explicitly advertises "without asking," so users who select Full are opting into this behavior for everything outside the denylist.
### Risk: preservation guard could "hide" onboarding from a user whose cloud profile is unexpectedly present
If an existing user with a stale cloud default profile re-runs onboarding expecting their selections to apply, the preservation guard will keep the stored cloud values. This is the desired behavior per invariant 14 but could be surprising.
Mitigation: the user can still edit the default profile explicitly in Settings; those edits go through the Synced branch of `edit_profile_internal` and update the cloud object. Non–execution-profile onboarding settings (session default, CLI agent footer, notifications, UI customization) continue to apply regardless, so the onboarding flow is not a no-op for existing users.
### Risk: `InitialLoadCompleted` reconciliation replaces Unsynced local edits with cloud values
If a logged-out user makes onboarding-driven edits (stored locally per change 4), then logs into an existing account, the reconciliation in change 5 discards the local Unsynced state and adopts the cloud profile. The user ends up with their cloud-stored values, not the onboarding selections they just made.
Mitigation: this is the desired behavior per invariant 14 — existing users expect their stored profile to win over transient onboarding selections. Non-execution-profile AISettings still follow the onboarding selection, so the user's onboarding work is not entirely thrown away.
### Risk: string-level test coupling
Asserting the exact Partial subtitle locks the test to the slide copy. Since the end-to-end tests don't assert subtitles anymore (only behavior), this risk was eliminated as a byproduct of the test restructuring.
## Follow-ups
- Consider whether the agent-slide subtitles should become localizable strings pulled from a single source of truth instead of inline literals; deferred because localization of onboarding is a larger, orthogonal project.
- Consider exposing the onboarding-seeded defaults in Settings as a labeled "onboarding preset" that users can re-apply without re-running onboarding. Out of scope for this change.
- Consider whether `update_objects_from_initial_load` should emit per-object events so that models don't need custom `InitialLoadCompleted` reconciliation. That would be a broader refactor affecting every model that subscribes to `CloudModel` events; revisit if a second model hits the same pitfall.