Skip to content

[multi-team P7a] Scope default host slug and agent attribution to the window's team - #15445

Open
warp-agent-staging[bot] wants to merge 5 commits into
masterfrom
factory/multi-team-p7-host-slug-attribution
Open

[multi-team P7a] Scope default host slug and agent attribution to the window's team#15445
warp-agent-staging[bot] wants to merge 5 commits into
masterfrom
factory/multi-team-p7-host-slug-attribution

Conversation

@warp-agent-staging

@warp-agent-staging warp-agent-staging Bot commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Description

Part of the multi-team client scoping stack described by the tech spec on #15347, stacked conceptually after P0 (#15439, now merged into master). This is slice P7a: it moves two small settings getters onto per-window team scope.

Which workspace-settings fallback this retires. Both getters read current_workspace().settings directly today, which is not team-neutral data — GetEffectiveWorkspaceSettingsForWorkspace resolves a single team server-side and falls through to a literal workspaceTeamIDs[0]. This PR retires that read for:

  • Agent attribution — completely. get_agent_attribution_setting is gone, replaced by agent_attribution_setting_for_scope(&impl TeamScope). Its only production caller, the AgentAttributionWidget on the Warp Agent settings page, now resolves a TeamContext from its own WeakViewHandle on every render.
  • Default host slug — for the two windowed/windowless readers that could move. The cloud-mode host selector in terminal/input.rs now reads default_host_slug_for_scope, and the /host slash-command gate now reads a new explicit cross-team accessor.

A private teamless_workspace_settings() provides the only remaining legitimate workspace fallback: the user belongs to no team at all. It applies the same empty-iterator guard as teams_allow_codebase_context.

What is deliberately left out

ai::orchestration::resolve_default_host_slug still reads workspace settings, now through a loudly-named unscoped_default_host_slug() whose doc comment says so and forbids new callers. That function is the head of one connected component — resolve_recent_host_slughost_snapshot → the shared pickers in orchestration_controls.rs → the plan card, the confirmation card, crates/warp_tui's orchestration block, and prepare_handoff with eight sites in pipeline_tests.rs — which cannot move piecemeal and is comfortably over 1000 lines on its own.

It also carries two decisions this slice cannot make:

  1. The TUI can never mint a scope. register_window is called only from RootView::new (app/src/root_view.rs:1889), which is GUI-only, so no TUI code path has a registered window. The TUI orchestration block therefore needs an explicit cross-team accessor, and unlike a boolean, a host slug has no defensible aggregation direction to pick one when two teams disagree.
  2. The handoff pipeline pins rather than resolves late. Its worker_host is baked into AgentConfigSnapshot for a cloud run being created — a chosen destination, so it wants a TeamContextForOperation, which is different discipline from the live picker defaults.

Both are recorded on REV-2205 and tracked as P7b, which is deliberately parked rather than picked up. It should be re-scoped after the TUI --team flag lands: if the TUI can register a window and resolve a team, the windowless-value problem in (1) largely dissolves and the orchestration chain becomes mechanical scope-threading rather than a design question. Scoping it before then would be work done against the wrong shape.

Explicitly not scoped

is_invite_link_enabled and is_discoverable sit in the same getter cluster and read the same way, but they are genuinely workspace-only (TeamSettings carries orthogonal invite_link / visibility instead). They are untouched.

Deviations worth a reviewer's attention

  • effective_default_host resolves through the host selector's own handle. It uses the borrowed TeamContext, not the owned TeamContextForOperation: this read is live policy that expires with the read, and the owned type would claim the host was pinned to the team it was chosen under. The handle is the selector's rather than the Input's because build_host_selector runs inside Input::new, and a view is absent from view_to_window until its own construction returns — but a child it builds along the way is already registered (app.rs:3109 inserts before the handle is returned at :3132). Reading through the live mapping also means the host follows the selector if it is ever moved between windows.
  • The resync applies None, it does not skip it. A window moving to a team that configures no default host now clears the selector's default (HostSelector::clear_default_host, the inverse of set_default_host, deferring to a saved user selection the same way) and clears the view model's worker_host. Leaving the previous value in place would point a cloud run launched from that window at the other team's self-hosted worker — the shape is pre-existing, but default_host_slug_for_scope returning a deliberate None in the cross-team case is what makes it bite.
  • any_team_has_default_host_slug() is deliberately narrow. It answers only "does a default host exist anywhere", which is the availability question the /host gate actually asks, and refuses to choose which slug. Known consequence: this widens the gate. A window on a team with no configured host now sees /host offered because some other team has one, while default_host_slug_for_scope gives that window nothing. It is a widening rather than a leak — no other team's slug is exposed — and narrowing it needs a window inside a data source shared with the TUI, which is the P7b problem.

What removing that mint revealed

Switching to the borrowed scope turned TeamContextForOperation and team_context_for_operation into dead code and failed clippy on all four targets, because this PR's mint was the only production caller of the owned scope anywhere in the codebase. The #[allow(dead_code)] is restored on both, with comments naming warp#15443 as the incoming real caller — it moves one by value into apply_onboarding_settings from root_view.rs, which is what the owned type is for.

This is worth reading as evidence rather than as a workaround. An architecture review had already concluded from reading code that the owned half of the scope design had one production mint and that the one mint was a misuse. Removing the misuse turned that into a compiler-checked fact: until #15443 lands, the pinning half of the contract has no production user at all, and the lint is what holds it alive. If there is an open question about whether the type earns its place, that is the strongest available input to it.

Adjacent, not fixed

Three things found while working here and deliberately left alone, recorded so they are not re-discovered:

set_default_host and its caller disagree when a saved selection exists. HostSelector::set_default_host deliberately does not move the selection when the user has a last_selected_host, but the caller in terminal/input.rs still pushes the default into the view model's worker_host regardless. So the selector shows the saved host while the run config carries the default. This is pre-existing and orthogonal to team scoping, but it sits directly under the code this PR changes, so whoever picks up P7b will meet it — set_worker_host is exactly what the handoff's pinned destination will be threading through.

WarpAgentPageView::new installs two subscriptions to UserWorkspaces. The filtered one at :625 (widened here) and an unfiltered one at :784 that calls ctx.notify() on every event. The second means the page already repaints on WindowTeamChanged today, so the widening is belt-and-braces rather than a behaviour fix — but relying on a redundant unfiltered subscription for correctness is fragile, and the duplicate should be collapsed by someone in a change that is only about that.

is_shared_block_title_generation_toggleable reads billing entitlement per window. At app/src/settings_view/warp_agent_page.rs:2929 it does a team-scoped read of team.billing_metadata.customer_type to decide Enterprise-ness. That predates the entitlement-versus-policy line drawn on REV-2205 — billing entitlement should stay workspace-level — and Team::from_gql (gql_convert.rs:1339-1361) clones workspace billing_metadata verbatim onto every team, so the team-scoped read returns workspace data anyway. It belongs to #15372; flagging rather than touching it.

Linked Issue

REV-2205 — [multi team] Scope team settings on the client to window context

  • The linked issue is labeled ready-to-spec or ready-to-implement.
  • Where appropriate, screenshots or a short video of the implementation are included below (especially for user-visible or UI changes).

Testing

This change was not compiled, linted, or tested locally. The sandbox this was written in has ~4 GB of RAM and rustc is SIGKILLed part-way through the warp crate even at -j 1 with debuginfo and incremental disabled. ./script/format was run and passes; cargo clippy and the test suite could not be run at all. CI is the primary verification here, which is why this was opened ready for review rather than as a draft — and CI came back green on the first commit (formatting + clippy on Linux, macOS, Windows and wasm; tests on all four; every release-flag compile).

Thirteen unit tests were added to app/src/workspaces/user_workspaces_tests.rs, one was rewritten (test_agent_attribution_default_with_no_workspace, which now resolves through a scope), and three were removed (test_agent_attribution_forced_on_by_team / _forced_off_by_team / _respects_user_setting) because they asserted the workspace-settings read this PR retires and their scoped equivalents replace them. Between them they cover:

  • Two windows on teams with opposing policies each resolve their own team, for attribution and for the host slug, via both the WeakViewHandle shape the settings page uses and the ViewContext shape the host selector uses.

  • A window reconciled onto a new team reports the new team's policy on the next read — the stale-modal guard, and the reason neither of these reads is captured.

  • A window with no team gets neither another team's setting nor the workspace's.

  • A genuinely teamless user still reads workspace settings, for both getters.

  • any_team_has_default_host_slug is true when one of two teams configures a host, false when neither does even though workspace settings do, and true for a teamless user with a workspace-level host.

  • A view resolving a scope inside its own add_typed_action_view build closure gets its team through the ViewContext and gets nothing through a handle to itself. This is the case that matters: every other test here resolves post-construction, where a handle would have worked equally well, so without it the exact mistake described above passes the whole suite.

No pinning test, because nothing in this slice pins.

Uncovered behaviour, stated plainly: there is no test that a team change actually schedules a repaint of the attribution toggle or re-pushes the host into the selector. take_all_invalidations_for_window and simulate_render_frame are pub(crate) inside warpui_core, so nothing in warp's tests can assert a render happened; the tests prove the data resolves per-team, not that the UI redraws. Every Group 1 PR in this stack has the same gap.

They did run, despite not running locally. Confirmed from the Run Linux tests job log rather than inferred from a green tick: all fourteen tests in this cluster appear by name, inside Summary [ 57.781s] 11056 tests run: 11056 passed, 34 skipped. So "could not be run locally" does not mean "unverified" here — it means the verification happened in CI instead of on my machine.

  • I have manually tested my changes locally with ./script/run

Screenshots / Videos

None. Demonstrating either change needs a logged-in multi-team workspace with opposing attribution policy or different default_host_slug values on two teams, which this environment cannot set up.

Agent Mode

  • Warp Agent Mode - This PR was created via Warp's AI Agent Mode

@cla-bot cla-bot Bot added the cla-signed label Aug 22, 2026
@warp-agent-staging
warp-agent-staging Bot marked this pull request as ready for review August 22, 2026 08:33
@warp-agent-staging

Copy link
Copy Markdown
Contributor Author

This PR was generated with Warp.

Comment @warp-factory on this PR to send it follow-up work.

View run View conversation

… window's team

Migrates two settings getters off the ambient `current_workspace().settings`
read onto `TeamScope`:

- `agent_attribution_setting_for_scope` replaces `get_agent_attribution_setting`,
  resolved fresh at render from the settings page's window.
- `default_host_slug_for_scope` replaces the windowed uses of
  `default_host_slug`, resolved from the cloud-mode input's window and
  re-resolved when that window's team changes.
- `any_team_has_default_host_slug` answers the windowless `/host` gate
  explicitly across every team, since that data source is shared with the TUI.

`is_invite_link_enabled` and `is_discoverable` are untouched: they are
genuinely workspace-only and have no `TeamSettings` equivalent.
…ange, cover the construction-time constraint

- The host-selector resync applied `Some` and swallowed `None`, so a window
  moving to a team that configures no default host kept the previous team's
  slug in both the selector and the run config. `clear_default_host` mirrors
  `set_default_host`, and the view model's host is now cleared too.
- The Warp Agent settings page only repainted on `TeamsChanged`;
  `window_team_uids` is not `Tracked`, so a window team change would not
  repaint the attribution toggle once a team switcher lands.
- Added a test that resolves a scope inside a view's own build closure,
  asserting the `ViewContext` shape works there and a self-handle resolves
  nothing. Every other test resolves post-construction, where the difference
  is invisible.
@warp-agent-staging
warp-agent-staging Bot force-pushed the factory/multi-team-p7-host-slug-attribution branch from bca4b2e to 721d9d7 Compare August 22, 2026 19:56
…text

- `effective_default_host` minted `TeamContextForOperation`, whose whole point
  is that it can be carried into an operation and recorded. Nothing was carried
  here: it was borrowed on the next line and dropped, so the type made a false
  claim that this host was pinned to the team it was chosen under, contradicting
  the doc comment three lines above it.
- It now uses the borrowed `TeamContext`, resolved from the host selector's own
  handle rather than the `Input`'s. A view is absent from `view_to_window` until
  its own construction finishes, but a child it builds along the way is already
  registered, so the selector's handle resolves at build time where the Input's
  does not. Reading through the live mapping also means the host follows the
  selector if it is ever moved between windows.
- Retargeted the construction-time test at that asymmetry: a self-handle
  resolves nothing mid-construction, a child handle resolves the window's team.
- Narrowed `any_team_has_default_host_slug` to `pub(crate)`; its only caller is
  a default trait-method body inside `warp`.
- Kept one test on the owned scope, redocumented as coverage that the getter
  behaves identically for both `TeamScope` implementors.

Leaves the teamless guard in place pending the batched follow-up.
…ion caller

Switching `effective_default_host` to the borrowed `TeamContext` removed the
last production mint of `TeamContextForOperation`, so both the struct and
`team_context_for_operation` became dead in the lib build and clippy failed on
all four targets under -D warnings.

Master carries these allows; my earlier rebase conflict resolution dropped them
because at that point my code was the consumer that made them unnecessary. The
comment now records why they are back, which is the useful signal: the pinning
half of the contract has no production user yet.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants