Skip to content

[multi-team] Attribute new agent runs to a team - #15552

Open
IsaiahWitzke wants to merge 1 commit into
masterfrom
factory/multi-team-agent-run-attribution
Open

[multi-team] Attribute new agent runs to a team#15552
IsaiahWitzke wants to merge 1 commit into
masterfrom
factory/multi-team-agent-run-attribution

Conversation

@IsaiahWitzke

Copy link
Copy Markdown
Contributor

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-Uid header constant, *_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.
    • Team(uid) sends team: true plus the header.

    Collapsing Personal into omission would silently make --personal mean team-owned. That was a real bug caught in review on the original PR.

  • 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.

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.

  • 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

app/src/server/server_api/ai_tests.rs covers the wire contract directly — all three AgentRunScope states, and that the X-Warp-Team-Uid header and the body team flag 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.

  • 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

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>
@cla-bot cla-bot Bot added the cla-signed label Aug 26, 2026
@warp-for-oss

warp-for-oss Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

@IsaiahWitzke

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 /warp-agent-review on this pull request to retrigger a review (up to 3 times on the same pull request).

Powered by Oz

@warp-for-oss warp-for-oss Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 AgentRunScope enum-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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [IMPORTANT] Container docs in this repo must not enumerate enum members; move these wire-state details onto the 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),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 [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 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 [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.

IsaiahWitzke pushed a commit that referenced this pull request Aug 26, 2026
## 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>
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