Skip to content

[multi-team] Scope multi-agent requests to a team - #15355

Merged
IsaiahWitzke merged 1 commit into
masterfrom
factory/multi-team-pr2b-agent-creation-attribution
Aug 26, 2026
Merged

[multi-team] Scope multi-agent requests to a team#15355
IsaiahWitzke merged 1 commit into
masterfrom
factory/multi-team-pr2b-agent-creation-attribution

Conversation

@warp-agent-staging

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

Copy link
Copy Markdown
Contributor

Description

/ai/multi-agent and /ai/passive-suggestions sent no team context, so the server could not tell which team an agent turn belonged to. They now send X-Warp-Team-Uid, resolved from the terminal surface's team.

The agent-run half of this work (POST /agent/run, AgentRunScope, the CLI flags) was split out into #15552, which is independent and can land on its own.

The core idea: the uid is carried by RequestTeamScope, which only a TeamScope can build. A raw Option<ServerId> carries no provenance — it's indistinguishable from "no team" and constructible anywhere, including by re-reading live window state after a request has already started. That admits a race where work begun on team A is attributed to team B if the window switches mid-flight. The type lives alone in app/src/server/team_scope.rs so the module privacy boundary matches the invariant exactly: from_scope is provably the only way to build one.

It's Copy, and ResponseStream captures one at construction, so retries — and post-credential-refresh re-sends — stay on the team the request started on rather than drifting.

warp_multi_agent_client still takes the raw wire value: it sits below app and cannot see TeamScope, so the typed-to-wire extraction happens one layer up in api/impl.rs.

Important

Behavior change. These paths previously sent no team header at all (hardcoded None behind TODOs). The uid always comes from a team the user is genuinely on and the server authenticates membership, but this is the highest-frequency request path in the product — every agent turn and every passive suggestion. Worth confirming the server side is ready before merging.

Linked Issue

See specs/multi-team-api-context/TECH.md.

  • 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

Added a unit test asserting a RequestTeamScope carries exactly the team its scope named, and that a teamless scope sends no header.

Verified locally: ./script/format, cargo clippy -p warp --all-targets --tests -- -D warnings, and 257 related tests passing (response-stream recovery, passive suggestions, and the team-scope suites).

No UI changes, so no screenshots.

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

Agent Mode

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

@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

warp-agent-staging Bot pushed a commit that referenced this pull request Aug 20, 2026
…Params

RequestParams::new() reverts to its original signature; the caller
(BlocklistAIController, which already resolves the window's team)
attaches the GEAP credential separately after construction via a new
attach_geap_credentials_if_available helper. ResponseStream.team_uid
now supplies a real value (via BlocklistAIController's window
resolution) instead of #15355's placeholder None, so its
X-Warp-Team-Uid header threading will actually fire once both land.
warp-agent-staging Bot pushed a commit that referenced this pull request Aug 20, 2026
…Params

RequestParams::new() reverts to its original signature; the caller
(BlocklistAIController, which already resolves the window's team)
attaches the GEAP credential separately after construction via a new
attach_geap_credentials_if_available helper. ResponseStream.team_uid
now supplies a real value (via BlocklistAIController's window
resolution) instead of #15355's placeholder None, so its
X-Warp-Team-Uid header threading will actually fire once both land.
warp-agent-staging Bot pushed a commit that referenced this pull request Aug 20, 2026
Proves begin_expired_geap_refresh starts independent mints for two
different teams rather than coalescing one behind the other's waiter
list. Also removes a process/merge-history comment from ResponseStream
that named PR #15355 and described how to resolve a future conflict;
that belongs in the PR description, not production source.
warp-agent-staging Bot pushed a commit that referenced this pull request Aug 20, 2026
…Params

RequestParams::new() reverts to its original signature; the caller
(BlocklistAIController, which already resolves the window's team)
attaches the GEAP credential separately after construction via a new
attach_geap_credentials_if_available helper. ResponseStream.team_uid
now supplies a real value (via BlocklistAIController's window
resolution) instead of #15355's placeholder None, so its
X-Warp-Team-Uid header threading will actually fire once both land.
warp-agent-staging Bot pushed a commit that referenced this pull request Aug 20, 2026
Proves begin_expired_geap_refresh starts independent mints for two
different teams rather than coalescing one behind the other's waiter
list. Also removes a process/merge-history comment from ResponseStream
that named PR #15355 and described how to resolve a future conflict;
that belongs in the PR description, not production source.
warp-agent-staging Bot added a commit that referenced this pull request Aug 20, 2026
TerminalView owned an ai_controller whose window_id was set once at
construction and never updated. TerminalView had no
on_window_transferred override, so dragging a terminal into another
window left the controller resolving the source window's team
indefinitely -- GEAP credentials would mint/attach for the wrong team,
and the X-Warp-Team-Uid header (once #15355 lands) would carry the
wrong team too.

Adds BlocklistAIController::set_window_id and a TerminalView
on_window_transferred override that keeps both window_id fields
current. Adds a regression test that performs a real
transfer_view_to_window and asserts the controller's resolved team
follows the window.
@warp-agent-staging
warp-agent-staging Bot force-pushed the factory/multi-team-pr2b-agent-creation-attribution branch from 4700414 to 5d784f0 Compare August 24, 2026 06:00
@warp-agent-staging warp-agent-staging Bot changed the title [multi-team PR 2B] Agent creation and cost attribution (slice 1: shared header plumbing + SpawnAgentRequest scope + multi-agent transport) [multi-team PR 2B] slice 1: shared header plumbing + SpawnAgentRequest scope + multi-agent transport Aug 24, 2026
@warp-agent-staging
warp-agent-staging Bot marked this pull request as ready for review August 24, 2026 06:28
@warp-agent-staging

Copy link
Copy Markdown
Contributor Author

The red Check CI results on this head is stale. The params setup job skipped in the 06:00 run, which failed the aggregator with nothing else wrong; the automatic re-run at 06:28 passed the full matrix. The latest Check CI results on 5d784f0 is green.

Responding as wilson: Open session · View in factory

@IsaiahWitzke
IsaiahWitzke force-pushed the factory/multi-team-pr2b-agent-creation-attribution branch from 33e0ca1 to c6243fc Compare August 25, 2026 04:53
@IsaiahWitzke
IsaiahWitzke changed the base branch from master to factory/multi-team-p5-remote-session-ai August 25, 2026 04:54
Base automatically changed from factory/multi-team-p5-remote-session-ai to master August 25, 2026 04:58
@IsaiahWitzke
IsaiahWitzke force-pushed the factory/multi-team-pr2b-agent-creation-attribution branch from c6243fc to dda315c Compare August 25, 2026 05:08
`/ai/multi-agent` and `/ai/passive-suggestions` sent no team context, so the
server could not tell which team an agent turn belonged to. They now send
`X-Warp-Team-Uid`, resolved from the terminal surface's team.

The uid is carried by `RequestTeamScope`, which only a `TeamScope` can build.
A raw `Option<ServerId>` would carry no provenance -- indistinguishable from
"no team" and constructible anywhere, including by re-reading live window
state after a request has already started. That admits a race where work
begun on team A is attributed to team B if the window switches mid-flight.
The type lives alone in its own module so the privacy boundary matches the
invariant exactly: `from_scope` is provably the only way in.

It is `Copy`, and `ResponseStream` captures one at construction, so retries
and post-credential-refresh re-sends stay on the team the request started on.

Behavior change: these paths previously sent no team header at all. The uid
always comes from a team the user is on, and the server authenticates
membership, but this is the highest-frequency request path in the product.

`warp_multi_agent_client` keeps taking the raw wire value -- it sits below
`app` and cannot see `TeamScope`, so the typed-to-wire extraction happens one
layer up.

Co-Authored-By: Warp <agent@warp.dev>
@IsaiahWitzke
IsaiahWitzke force-pushed the factory/multi-team-pr2b-agent-creation-attribution branch from db3c4c2 to 81ec8a8 Compare August 26, 2026 01:12
@IsaiahWitzke IsaiahWitzke changed the title [multi-team PR 2B] slice 1: shared header plumbing + SpawnAgentRequest scope + multi-agent transport [multi-team] Scope multi-agent requests to a team Aug 26, 2026
@IsaiahWitzke
IsaiahWitzke enabled auto-merge (squash) August 26, 2026 01:22
@IsaiahWitzke
IsaiahWitzke merged commit afd2aec into master Aug 26, 2026
32 checks passed
@IsaiahWitzke
IsaiahWitzke deleted the factory/multi-team-pr2b-agent-creation-attribution branch August 26, 2026 01:35
IsaiahWitzke pushed a commit that referenced this pull request Aug 26, 2026
## Summary
`POST /ai/predict_am_queries` (Agent Mode next-query prediction) sent no
team context. The `/ai` group's `SetActiveTeamFromHeader` middleware
only picks up the header, so omitting it silently falls through
`GetActiveTeamOrFallbackToPrincipal` → `GetTeamForPrincipal`, which
resolves to whichever team happens to have the lowest id — an arbitrary,
and possibly wrong, team for a member of more than one.

## Changes
- `ServerApi::predict_am_queries` now takes a `RequestTeamScope` and
sends it as `X-Warp-Team-Uid` when present, following the
`/ai/multi-agent` pattern (#15355).
- `Input::predict_am_query` resolves the scope from the live window via
`UserWorkspaces::team_context_for_operation`, pinning it at send time
(this call spawns an async request, same as the existing multi-agent
send path).

## Verification
- `./script/format`
- `cargo clippy -p warp --lib --tests -- -D warnings` (clean)
- No existing unit tests cover this thin network wrapper method;
verified by inspection that the header is set conditionally and
threading compiles against the live `ViewContext<Input>` available at
the call site.

<!-- warp:pr-description-artifacts start -->
<!-- warp:pr-description-artifacts end -->

Co-authored-by: warp-agent-staging[bot] <240773466+warp-agent-staging[bot]@users.noreply.github.com>
IsaiahWitzke pushed a commit that referenced this pull request Aug 26, 2026
## Summary
Scopes the `POST /ai/generate_input_suggestions` call (next-command
autosuggestions) to the window's selected team by sending
`X-Warp-Team-Uid`.

Server-side, this endpoint reaches workspace settings and billing
metadata (`generate_next_command.go`) that are genuinely team-scoped.
Without the header, the server falls through
`GetActiveTeamOrFallbackToPrincipal` → `GetTeamForPrincipal`, which
silently answers using whichever team happens to have the lowest team id
— not necessarily the team the window is showing.

## Changes
- `ServerApi::generate_ai_input_suggestions` now takes a
`RequestTeamScope` and sends it via the `X-Warp-Team-Uid` header (server
already applies it for `/ai/*` via `SetActiveTeamFromHeader`, so no
server change is needed).
- `NextCommandModel` now holds the terminal's `BlocklistAIController`
handle and resolves the scope via `ai_controller.team_context(ctx)`
immediately before spawning the request, mirroring the pattern used for
`/ai/multi-agent` (PR #15355) and passive suggestions — this keeps the
team pinned to whatever the window was scoped to when the suggestion was
requested, even if the user switches teams while the request is in
flight.
- Threaded the resolved scope through both call sites in
`generate_next_command_suggestion_with_prefix` (zero-state and prefixed
suggestions).

## Verification
- `cargo check -p warp --lib` and `cargo clippy -p warp --lib --tests --
-D warnings`: clean.
- `./script/format`: clean (only reformatted the new lines).
- `cargo nextest run -p warp` for `next_command_model`, `server_api`,
and `team_scope`: 81/82 relevant tests pass. The one failure
(`ambient_agent_headers_for_task_overrides_existing_cloud_agent_header`)
is a pre-existing sandbox limitation (the test's `nsc` namespace-token
command is denied in this environment) reproduced identically on
unmodified `master`, unrelated to this change.
- No UI-visible behavior changed (this is a header addition on an
existing background request), so no visual proof was captured.


<!-- warp:pr-description-artifacts start -->
<!-- warp:pr-description-artifacts end -->

---------

Co-authored-by: warp-agent-staging[bot] <240773466+warp-agent-staging[bot]@users.noreply.github.com>
IsaiahWitzke pushed a commit that referenced this pull request Aug 26, 2026
## Summary
Scopes `POST /ai/transcribe` (voice-input transcription) to the window's
team by sending `X-Warp-Team-Uid`, following the pattern landed for
`/ai/multi-agent` in #15355.

Without the header, the server falls through
`GetActiveTeamOrFallbackToPrincipal` → `GetTeamForPrincipal`, which
resolves to whichever team has the lowest id — not necessarily the team
the user has selected in that window. Server-side, `transcribe.go` uses
the resolved team for cost denial, billing, and workspace settings, so
an unscoped call can silently apply the wrong team's policy.

## Changes
- `Transcriber::transcribe` and its only implementation
(`ServerVoiceTranscriber`) now take a `RequestTeamScope`.
- `ServerApi::transcribe` sets `X-Warp-Team-Uid` from that scope
(server-side handling already exists via `SetActiveTeamFromHeader` on
`/ai/*`, so no server change is needed).
- The one call site, `EditorView::handle_voice_session_result`, captures
the scope from its `ViewContext`
(`UserWorkspaces::as_ref(ctx).team_context_for_view(ctx)`) before the
transcription future is spawned off the main thread, since no window is
reachable once it's running.

## Verification
This is a header-plumbing change through an established pattern
(`RequestTeamScope`, mirroring the `/ai/multi-agent` wiring); no new
business logic to unit test. Verified with:
- `./script/format`
- `cargo clippy -p warp --lib --tests -- -D warnings`
- `cargo check -p warp --lib`

I did not have a live warp-server + client environment available to
manually exercise the voice-input flow end-to-end.

<!-- warp:pr-description-artifacts start -->
<!-- warp:pr-description-artifacts end -->

---------

Co-authored-by: warp-agent-staging[bot] <240773466+warp-agent-staging[bot]@users.noreply.github.com>
IsaiahWitzke pushed a commit that referenced this pull request Aug 26, 2026
## Description
`ServerApi::get_relevant_files` (backing `POST /ai/relevant_files`, used
by codebase search when picking relevant files for the outline-based
index) sent no team scope. The server falls through
`GetActiveTeamOrFallbackToPrincipal` → `GetTeamForPrincipal`, which
resolves to the member's lowest team id rather than the team the
requesting window is actually on. Since this endpoint's cost is billed
to the AI group, members on more than one team could have this request
billed to the wrong team.

This threads the live, window-derived `TeamScope` through to the request
as the `X-Warp-Team-Uid` header, following the pattern established in
#15355 for `/ai/multi-agent`. No server change is needed —
`SetActiveTeamFromHeader` already runs on `/ai/*`.

The scope comes from `BlocklistAIActionExecutor`'s existing
`TeamContextResolver` (already resolved from the window via
`UserWorkspaces::team_context_resolver`), the same live scope it already
uses to decide whether a `SearchCodebase` action can auto-execute. It's
resolved once, immediately before executing the action, and threaded as
a `RequestTeamScope` through `SearchCodebaseExecutor::execute` →
`GetRelevantFilesController::send_request` (local, outline-based path
only — the remote path resolves relevant files through the remote host's
own indexing service and isn't affected) →
`ServerApi::get_relevant_files`.

## Linked Issue
No linked GitHub issue — this is one of several call sites being
migrated to send an explicit team scope instead of relying on the
server's team-resolution fallback.

## Testing
This is a plumbing change (adds a parameter, sets a header) with no new
externally observable branching logic, so no new unit test was added —
consistent with the reference implementation in #15355. Verified:
- `cargo check -p warp --lib`
- `cargo clippy -p warp --lib --tests -- -D warnings`
- `cargo test -p warp --lib --features local_fs` for
`get_relevant_files`, `search_codebase`, and `action_model` (192 tests,
all passing)
- `./script/format`

- [ ] I have manually tested my changes locally with `./script/run`

## Agent Mode
- [x] Warp Agent Mode - This PR was created via Warp's AI Agent Mode


<!-- warp:pr-description-artifacts start -->
<!-- warp:pr-description-artifacts end -->

---------

Co-authored-by: warp-agent-staging[bot] <240773466+warp-agent-staging[bot]@users.noreply.github.com>
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.

2 participants