[multi-team] Attribute new agent runs to a team - #15552
Conversation
Adds the transport plumbing to say which team a cloud agent run belongs to, so the server can attribute the run -- and its cost -- to that team once a user can be on several. - `X-Warp-Team-Uid` header constant, plus `*_with_headers` variants of the REST helpers so a caller can attach per-request headers alongside the ambient ones, and a shared `team_uid_header` builder. - `SpawnAgentRequest.team: Option<bool>` becomes an explicit `AgentRunScope`. The three wire states are not interchangeable, so each is preserved: `Unspecified` omits `team` (the server applies its own default, which for a single-team account is team ownership), `Personal` sends `team: false`, and `Team(uid)` sends `team: true` plus the header. Collapsing Personal into omission would silently make `--personal` mean team-owned. - CLI resolves `--team`/`--personal` through the same membership-checked path other object-owning commands use. Call sites that cannot name a team send `Unspecified` rather than forcing personal ownership: a remote child inherits its scope from `parent_run_id` server-side, and the handoff and cloud-agent-pane paths have no window-selected team wired through yet. Co-Authored-By: Warp <agent@warp.dev>
|
I'm starting a first review of this pull request. You can view the conversation on Warp. I completed the review and no human review was requested for this pull request. Comment Powered by Oz |
There was a problem hiding this comment.
Overview
This PR replaces the ambiguous SpawnAgentRequest.team: Option<bool> transport field with AgentRunScope, sends X-Warp-Team-Uid for team-scoped agent runs, and adds tests for the request/body/header wire contract.
Concerns
- The new
AgentRunScopeenum-level doc comment enumerates and explains each variant, which violates the repository's comment guideline that container docs describe the whole while member docs describe members. - No approved spec context was attached, so there was no implementation-vs-spec contract to enforce.
Security
- No blocking security issue found. I left one defense-in-depth suggestion about the new public extra-header helper bypassing reserved-header checks.
Verdict
Found: 0 critical, 1 important, 2 suggestions
Request changes
Comment /warp-agent-review on this pull request to retrigger a review (up to 3 times on the same pull request).
Powered by Oz
| /// states it could take on `POST /agent/run`'s `team` field — they are not equivalent, since | ||
| /// the server treats an omitted value as its own default (team ownership for a single-team | ||
| /// account) rather than as personal: | ||
| /// - `Unspecified` omits `team` entirely: the caller expressed no preference (e.g. no |
There was a problem hiding this comment.
Unspecified, Personal, and Team variant doc comments so each member owns its documentation.
| { | ||
| match self { | ||
| // `skip_serializing_if` omits this variant before the serializer is ever reached. | ||
| Self::Unspecified => serializer.serialize_bool(false), |
There was a problem hiding this comment.
💡 [SUGGESTION] Unspecified serializes as false whenever AgentRunScope is reused without this field's skip_serializing_if; move the bool wire encoding to a field-level serializer so the public enum does not have a misleading standalone wire value.
| { | ||
| request = request.header(name, value); | ||
| } | ||
| for (name, value) in extra_headers { |
There was a problem hiding this comment.
💡 [SUGGESTION] [SECURITY] This public helper bypasses is_reserved_ambient_header_name and writes caller-supplied headers after auth/ambient headers; validate or narrowly allowlist extra_headers so future call sites cannot accidentally override transport-scoped headers.
## 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`. - [x] 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 - [x] Warp Agent Mode - This PR was created via Warp's AI Agent Mode <!-- CHANGELOG-NONE --> Co-authored-by: Oz <oz-agent@warp.dev> Co-authored-by: Warp <agent@warp.dev>
Description
Adds the transport plumbing to say which team a new cloud agent run belongs to, so the server can attribute the run — and its cost — to that team once a user can be on several.
Split out of #15355, which now carries only the multi-agent (
/ai/multi-agent) half. This half is independent and lands on its own.What's here:
X-Warp-Team-Uidheader constant,*_with_headersvariants of the REST helpers so a caller can attach per-request headers alongside the ambient ones, and a sharedteam_uid_headerbuilder.SpawnAgentRequest.team: Option<bool>becomes an explicitAgentRunScope. The three wire states are not interchangeable, so each is preserved:Unspecifiedomitsteam— the server applies its own default, which for a single-team account is team ownership.Personalsendsteam: false.Team(uid)sendsteam: trueplus the header.Collapsing
Personalinto omission would silently make--personalmean team-owned. That was a real bug caught in review on the original PR.CLI resolves
--team/--personalthrough the same membership-checked path other object-owning commands use.Call sites that cannot name a team send
Unspecifiedrather than forcing personal ownership: a remote child inherits its scope fromparent_run_idserver-side, and the handoff and cloud-agent-pane paths have no window-selected team wired through yet.Reviewer note: the "omitted = server default = team for a single-team account" claim is load-bearing for the wire-state design and currently lives only in a client-side comment. There's no generated client for this REST API, so nothing verifies it — worth a server-side confirmation.
Linked Issue
See
specs/multi-team-api-context/TECH.md.ready-to-specorready-to-implement.Testing
app/src/server/server_api/ai_tests.rscovers the wire contract directly — all threeAgentRunScopestates, and that theX-Warp-Team-Uidheader and the bodyteamflag agree and are only sent when team-scoped (asserted against a mock public-API request).Verified locally:
./script/format,cargo clippy -p warp --all-targets --tests -- -D warnings, and 344 related tests passing.No UI changes, so no screenshots.
./script/runAgent Mode