Skip to content

[multi-team PR 2B] Agent creation and cost attribution (slice 2) - #15359

Open
warp-agent-staging[bot] wants to merge 7 commits into
masterfrom
factory/multi-team-pr2b-slice2-agent-creation
Open

[multi-team PR 2B] Agent creation and cost attribution (slice 2)#15359
warp-agent-staging[bot] wants to merge 7 commits into
masterfrom
factory/multi-team-pr2b-slice2-agent-creation

Conversation

@warp-agent-staging

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

Copy link
Copy Markdown
Contributor

Description

Slice 2 of #### PR 2B: Agent creation and cost attribution from specs/multi-team-api-context/TECH.md (Bucket 2: Request-local team identity). Builds on slice 1 (#15355) and PR 0 (#15348, merged).

Covers the remaining bucket-2 endpoints deferred from slice 1: GraphQL CreateAgentTask, agent identity listing, connected self-hosted workers, local-to-cloud handoff snapshot upload (both local and remote-SSH paths), GraphQL GetFeatureModelChoices, and GraphQL CreateFileArtifactUploadTarget.

Rebased onto the revived #15355 (which has since landed the TeamScope trait, TeamContextForOperation/TeamContext/TeamScopeForCli, and the user_workspaces module split). The only real conflict was a team_uid_for_transport helper this PR originally added, typed to the old pre-split TeamContext { team_uid: ServerId } shape and returning a non-optional ServerId. That shape no longer exists: TeamContext now already implements the shared TeamScope trait with team_uid(&self) -> Option<ServerId>, so the helper was redundant — call sites now read .team_uid() directly off the TeamContext returned by team_context_for_view, and a teamless scope naturally omits the header instead of needing special-casing.

What's implemented

  • AIClient trait: added team_uid: Option<ServerId> to create_agent_task, list_connected_self_hosted_workers, upload_local_handoff_snapshot, get_feature_model_choices, and create_file_artifact_upload_target. AuthClient::list_agent_identities gained the same parameter (as Option<String>, since that trait lives in warp_server_client and doesn't depend on the app crate's ServerId).
  • Two genuine view-based wirings — the only two call sites among these six endpoints that actually have a ViewContext to mint a TeamContext from:
    • create_api_key_modal.rs::fetch_agents — captures the window's TeamContext before listing named agent identities for the "Agent" API-key type.
    • handoff::snapshot::resolve_upload_target (ViewContext<Workspace>) — captures it once, before branching, and threads it through both the local path (upload_handoff_snapshotupload_snapshot_for_handoffAIClient::upload_local_handoff_snapshot) and the remote-SSH path (SnapshotUploadTarget::Remote → the UploadHandoffSnapshot RPC, now carrying an added team_uid proto field → the daemon's handle_upload_handoff_snapshotgather_and_upload_handoff_snapshot → the same upload_snapshot_for_handoff). Both paths land on the identical client-side capture and the identical server call.
  • GetFeatureModelChoices header threading, plus a defensive selection fix. The client previously took workspaces[0] unconditionally from the response array; it now selects the entry matching the requested uid instead of trusting position, and fails the request outright (rather than substituting another entry) when the requested team has no matching entry. Read this carefully: as of today the server's FeatureModelChoice resolver ignores the *Workspace row it's attached to and recomputes from the caller's active team (set via the new X-Warp-Team-Uid header) or a fallback — so every element of the array currently carries the same catalog, and this selection logic does not itself change which catalog comes back. What it does do is (a) send the header so the server's notion of "active team" for this request is genuinely the one the caller asked about, and (b) make the client fail loudly instead of reading a wrong-but-present row, which matters once the resolver's shape changes. warp-server#15900 is now open adding Team.featureModelChoice, which will make a genuine per-team map available server-side; this selection-by-uid logic is exactly what will make that map's entries actually distinguishable once the client reads from it. Do not read the array as a real per-team map yet.
  • Connected self-hosted workers is genuinely team-scoped, not just header-plumbed. ConnectedSelfHostedWorkersModel is a process-global singleton with no window to capture a real TeamContext from, so it resolves the account's default team via UserWorkspaces::inherited_or_default_team_uid(None) (the same window-less fallback admin_billing_link_for_default_team already uses) and refreshes on UserWorkspacesEvent::TeamsChanged.
  • All remaining call sites (create_agent_task's child/task-scoped launches, LLMPreferences::refresh_authed_models, FileArtifactUploader::create_upload_target) pass None, documented in-line with why each one doesn't have a window to capture a TeamContext from.

What is and is not wired end-to-end

  • Sends the header today: GraphQL CreateAgentTask (window-initiated launches only — child/task-scoped launches pass None), GET /api/v1/agent/identities (from create_api_key_modal.rs), POST /api/v1/agent/handoff/upload-snapshot (both local and remote-SSH handoff paths), GET /api/v1/agent/connected-self-hosted-workers (via the account's default team), GraphQL GetFeatureModelChoices (currently always None — see below), GraphQL CreateFileArtifactUploadTarget (currently always None — see below).
  • Plumbing exists but ships inert (team_uid: None): LLMPreferences::refresh_authed_models (no per-team storage yet — that's PR 3A's scope), FileArtifactUploader::create_upload_target (always resolves an existing conversation_id/run_id association first, so the server validates ownership against that resource instead), all create_agent_task child/task-scoped launches (scope is meant to be inherited from parent_run_id server-side).
  • Server-side: POST /graphql/v2 and /ai/* resolve X-Warp-Team-Uid today; /api/v1/* (including /agent/handoff/upload-snapshot and /agent/connected-self-hosted-workers) currently only captures the header, it does not yet resolve/authorize it.

Deviations from the spec / deferred work

  1. GetFeatureModelChoices still returns one process-wide ModelsByFeature value, not per-team storage. Genuinely partitioning the cache (so a window showing Team A's models doesn't leak into a window showing Team B's) requires restructuring LLMPreferences's ~30 read methods and every call site that reaches them — that's PR 3A's explicitly-scoped charter, not something safe to fold into a header-plumbing PR.
  2. create_agent_task's client call sites are all child/task-scoped, not window-scoped: local Claude/Codex/OpenCode child launches (local_harness_launch.rs), local Oz child launches (child_agent_launch.rs), and the headless CLI driver's fresh top-level task creation (agent_sdk/mod.rs::initialize_new_task, which has no --team selector on this CLI surface yet). All pass None.
  3. FileArtifactUploader::create_upload_target passes None. It always resolves an existing conversation_id/run_id association first, so the server can validate ownership against that resource; there's no window in this path to capture a fresh TeamContext from.

Testing

  • ./script/format — clean.
  • cargo check -p warp --lib — clean, no new warnings.
  • cargo clippy -p warp --all-targets --tests -- -D warnings — clean.
  • cargo clippy -p warp_server_client -p remote_server --all-targets --tests -- -D warnings — clean.
  • cargo nextest run -p remote_server — 99/99 pass (covers the proto/manager changes to UploadHandoffSnapshot).
  • New/updated unit tests in ai_tests.rs:
    • select_feature_model_choice_workspace_index_matches_requested_team — selection matches by UID, not position; falls back to index 0 only for no-team-scope.
    • select_feature_model_choice_workspace_index_returns_none_for_missing_team — a team absent from the response returns None (fails the request) rather than substituting another entry.
    • select_feature_model_choice_workspace_index_returns_none_for_empty_response — an empty response also returns None.
  • Focused tests (cargo nextest run -p warp filtered to the touched modules — local_harness_launch, pipeline_tests, connected_self_hosted_workers, server_api::ai, create_api_key_modal, llms, host_response, server_model, plus the selection tests): 127/129 pass. The 2 failures (ambient_agent_headers_for_task_overrides_existing_cloud_agent_header, spawn_agent_sends_matching_team_uid_header_and_body_flag_only_when_team_scoped) are a pre-existing sandbox limitation — both call out to an external nsc isolation-platform binary unavailable in this environment, and reproduce identically against an unmodified master checkout.
  • No UI changes; no visual verification needed.

CI coverage

This PR targets #15355's branch (a stacked PR), not master. GitHub's build/test/clippy workflow only runs for pull requests into the default branch, so on this branch only the approval and base-verification checks run here — no compile, no tests, no clippy from CI, no matter how long the checks are left pending. A green check mark on this PR does not mean the build/tests ran. The local verification listed above (format, clippy across warp/warp_server_client/remote_server, focused and full remote_server test runs) stands in for that until this PR retargets to master and gets the real CI matrix, which happens automatically once #15355 merges.

Server-side

Per the orchestrator: server-side request-local scope, membership authorization, and mismatch rejection for X-Warp-Team-Uid is separate, tracked work. This client change must not ship before that server-side validation exists on the surfaces it depends on.

Linked Issue

N/A — this PR implements a section of the multi-team-context/multi-team-api-context tech specs from #15347 (Linear REV-2205), not a tracked GitHub issue.

Agent Mode

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

CHANGELOG-NONE

@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
warp-agent-staging Bot force-pushed the factory/multi-team-pr2b-slice2-agent-creation branch from 17a53aa to 36e1a32 Compare August 20, 2026 10:43
warp-agent-staging Bot added a commit that referenced this pull request Aug 20, 2026
Threads a resolved team_uid through the model-picker credential-source
chain (byo_key_source_for_model, should_show_key_icon_for_model, and
their UserWorkspaces-backed helpers), so the key icon and inference
label shown for a model reflect the window's actual current team_byo
policy and team-provided key/endpoint configuration, instead of the
old ambient/ first-team read.

Added UserWorkspaces::has_team_first_party_key_for_team and
has_team_byo_endpoint_for_model_for_team (by team UID), mirroring the
are_member_byo_*_allowed_for_team pattern already established for
request-time enforcement. agent_settings_has_team_first_party_key now
delegates to the new by-uid method.

is_using_first_party_key_for_provider (and the disable-reason-clearing
path in is_usable_llm that depends on it) is deliberately left on its
existing ambient read: re-scoping which RequiresUpgrade models stay
selectable to a specific window's team is a larger, separate change
with a much wider blast radius (it touches every model-resolution
method in LLMPreferences), tracked as follow-up, not part of the
model-picker credential-source display this covers.

Updated all call sites that render model-picker/menu items with a
resolved team_uid: ModelSelectorDataSource (GUI inline picker, already
had window_id), ProfileModelSelector (added a window_id field),
model_menu_items::available_model_menu_items and its ~6 callers
(execution profile editor, agent profiles settings page, custom model
router editor, orchestration controls), the buy-credits-banner check
in terminal/input/common.rs, and the TUI model menu
(TuiModelMenuModel, now also given a window_id).

This is unblocked by, and independent of, #15359: team_byo policy and
team-provided keys/endpoints are already genuinely team-differentiated
today (this is exactly what the request-time enforcement work already
landed proves). Only the model *catalog* itself (which models exist)
remains account-wide pending #15359, which is why the model list is
not touched here.
warp-agent-staging Bot added a commit that referenced this pull request Aug 20, 2026
Threads a resolved team_uid through the model-picker credential-source
chain (byo_key_source_for_model, should_show_key_icon_for_model, and
their UserWorkspaces-backed helpers), so the key icon and inference
label shown for a model reflect the window's actual current team_byo
policy and team-provided key/endpoint configuration, instead of the
old ambient/ first-team read.

Added UserWorkspaces::has_team_first_party_key_for_team and
has_team_byo_endpoint_for_model_for_team (by team UID), mirroring the
are_member_byo_*_allowed_for_team pattern already established for
request-time enforcement. agent_settings_has_team_first_party_key now
delegates to the new by-uid method.

is_using_first_party_key_for_provider (and the disable-reason-clearing
path in is_usable_llm that depends on it) is deliberately left on its
existing ambient read: re-scoping which RequiresUpgrade models stay
selectable to a specific window's team is a larger, separate change
with a much wider blast radius (it touches every model-resolution
method in LLMPreferences), tracked as follow-up, not part of the
model-picker credential-source display this covers.

Updated all call sites that render model-picker/menu items with a
resolved team_uid: ModelSelectorDataSource (GUI inline picker, already
had window_id), ProfileModelSelector (added a window_id field),
model_menu_items::available_model_menu_items and its ~6 callers
(execution profile editor, agent profiles settings page, custom model
router editor, orchestration controls), the buy-credits-banner check
in terminal/input/common.rs, and the TUI model menu
(TuiModelMenuModel, now also given a window_id).

This is unblocked by, and independent of, #15359: team_byo policy and
team-provided keys/endpoints are already genuinely team-differentiated
today (this is exactly what the request-time enforcement work already
landed proves). Only the model *catalog* itself (which models exist)
remains account-wide pending #15359, which is why the model list is
not touched here.
warp-agent-staging Bot added a commit that referenced this pull request Aug 21, 2026
Threads a resolved team_uid through the model-picker credential-source
chain (byo_key_source_for_model, should_show_key_icon_for_model, and
their UserWorkspaces-backed helpers), so the key icon and inference
label shown for a model reflect the window's actual current team_byo
policy and team-provided key/endpoint configuration, instead of the
old ambient/ first-team read.

Added UserWorkspaces::has_team_first_party_key_for_team and
has_team_byo_endpoint_for_model_for_team (by team UID), mirroring the
are_member_byo_*_allowed_for_team pattern already established for
request-time enforcement. agent_settings_has_team_first_party_key now
delegates to the new by-uid method.

is_using_first_party_key_for_provider (and the disable-reason-clearing
path in is_usable_llm that depends on it) is deliberately left on its
existing ambient read: re-scoping which RequiresUpgrade models stay
selectable to a specific window's team is a larger, separate change
with a much wider blast radius (it touches every model-resolution
method in LLMPreferences), tracked as follow-up, not part of the
model-picker credential-source display this covers.

Updated all call sites that render model-picker/menu items with a
resolved team_uid: ModelSelectorDataSource (GUI inline picker, already
had window_id), ProfileModelSelector (added a window_id field),
model_menu_items::available_model_menu_items and its ~6 callers
(execution profile editor, agent profiles settings page, custom model
router editor, orchestration controls), the buy-credits-banner check
in terminal/input/common.rs, and the TUI model menu
(TuiModelMenuModel, now also given a window_id).

This is unblocked by, and independent of, #15359: team_byo policy and
team-provided keys/endpoints are already genuinely team-differentiated
today (this is exactly what the request-time enforcement work already
landed proves). Only the model *catalog* itself (which models exist)
remains account-wide pending #15359, which is why the model list is
not touched here.
IsaiahWitzke pushed a commit that referenced this pull request Aug 21, 2026
Threads a resolved team_uid through the model-picker credential-source
chain (byo_key_source_for_model, should_show_key_icon_for_model, and
their UserWorkspaces-backed helpers), so the key icon and inference
label shown for a model reflect the window's actual current team_byo
policy and team-provided key/endpoint configuration, instead of the
old ambient/ first-team read.

Added UserWorkspaces::has_team_first_party_key_for_team and
has_team_byo_endpoint_for_model_for_team (by team UID), mirroring the
are_member_byo_*_allowed_for_team pattern already established for
request-time enforcement. agent_settings_has_team_first_party_key now
delegates to the new by-uid method.

is_using_first_party_key_for_provider (and the disable-reason-clearing
path in is_usable_llm that depends on it) is deliberately left on its
existing ambient read: re-scoping which RequiresUpgrade models stay
selectable to a specific window's team is a larger, separate change
with a much wider blast radius (it touches every model-resolution
method in LLMPreferences), tracked as follow-up, not part of the
model-picker credential-source display this covers.

Updated all call sites that render model-picker/menu items with a
resolved team_uid: ModelSelectorDataSource (GUI inline picker, already
had window_id), ProfileModelSelector (added a window_id field),
model_menu_items::available_model_menu_items and its ~6 callers
(execution profile editor, agent profiles settings page, custom model
router editor, orchestration controls), the buy-credits-banner check
in terminal/input/common.rs, and the TUI model menu
(TuiModelMenuModel, now also given a window_id).

This is unblocked by, and independent of, #15359: team_byo policy and
team-provided keys/endpoints are already genuinely team-differentiated
today (this is exactly what the request-time enforcement work already
landed proves). Only the model *catalog* itself (which models exist)
remains account-wide pending #15359, which is why the model list is
not touched here.
@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 force-pushed the factory/multi-team-pr2b-slice2-agent-creation branch from 36e1a32 to 381249d Compare August 24, 2026 06:24
@warp-agent-staging
warp-agent-staging Bot marked this pull request as ready for review August 24, 2026 06:28
@warp-agent-staging
warp-agent-staging Bot force-pushed the factory/multi-team-pr2b-slice2-agent-creation branch from 381249d to df42e5e Compare August 24, 2026 06:54
@IsaiahWitzke
IsaiahWitzke force-pushed the factory/multi-team-pr2b-agent-creation-attribution branch from 33e0ca1 to c6243fc Compare August 25, 2026 04:53
warp-agent-staging Bot and others added 3 commits August 25, 2026 01:00
Adds request-local team-scope transport plumbing for the multi-team
context migration:

- X-Warp-Team-Uid header constant plus REST header-passing helpers on
  ServerApi/BaseClient (get/post *_with_headers, team_uid_header).
- SpawnAgentRequest.team: Option<bool> replaced with an explicit
  AgentRunScope { Personal, Team(ServerId) }, wired to send the header
  on POST /agent/run.
- /ai/multi-agent and /ai/passive-suggestions transport threaded
  end-to-end (ResponseStream -> impl.rs -> warp_multi_agent_client)
  per the RequestParams-separation requirement; currently fed None
  pending view-level TeamContext capture (documented TODOs).

See specs/multi-team-api-context/TECH.md and the PR description for
the full breakdown, deviations, and deferred work.
…onal/Team)

Review found that Personal serialized by omission, making the server treat
--personal the same as no flag at all (team ownership by default for a
single-team account). Personal now serializes as explicit team: false,
Unspecified omits the field, and call sites are corrected to match their
pre-existing wire behavior. Also adds header/body coverage for spawn_agent
via a mock public API request.
The `/ai/multi-agent` and `/ai/passive-suggestions` boundary took a raw
`Option<ServerId>`, which carries no provenance: it is indistinguishable
from "no team" and can be produced 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 while
the request is in flight.

Introduce `RequestTeamScope`, constructible only from a `TeamScope`, and
take it at that boundary instead. There is deliberately no constructor from
a bare uid, so the value sent as `X-Warp-Team-Uid` can only have come from a
deliberately resolved team. 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.

This also completes the threading the previous commits deferred: the
controller and passive suggestions now resolve the terminal surface's
`TeamContext` before spawning rather than passing `None`, so these requests
begin sending the team header.

Co-Authored-By: Warp <agent@warp.dev>
@IsaiahWitzke
IsaiahWitzke force-pushed the factory/multi-team-pr2b-agent-creation-attribution branch from c6243fc to dda315c Compare August 25, 2026 05:08
Adds request-local team scope to the remaining bucket-2 endpoints: GraphQL
CreateAgentTask, agent identity listing, connected self-hosted workers,
local-to-cloud handoff snapshot upload, GetFeatureModelChoices, and
CreateFileArtifactUploadTarget. Fixes a real bug in GetFeatureModelChoices
that took workspaces[0] unconditionally regardless of which team was
requested.
@warp-agent-staging
warp-agent-staging Bot force-pushed the factory/multi-team-pr2b-slice2-agent-creation branch from df42e5e to 5b6dded Compare August 25, 2026 06:25
@IsaiahWitzke
IsaiahWitzke force-pushed the factory/multi-team-pr2b-agent-creation-attribution branch from db3c4c2 to 81ec8a8 Compare August 26, 2026 01:12
Base automatically changed from factory/multi-team-pr2b-agent-creation-attribution to master August 26, 2026 01:35
…pr2b-slice2-agent-creation

# Conflicts:
#	app/src/ai/agent/api_tests.rs
#	app/src/ai/blocklist/controller.rs
#	app/src/ai/blocklist/controller/response_stream.rs
#	app/src/server/server_api.rs
#	crates/warp_server_client/src/graphql_helpers.rs
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.

1 participant