Skip to content

feat(teams): resolve the team a project's runs act as - #2927

Merged
camielvs merged 1 commit into
masterfrom
cvs/team-context
Oct 31, 2026
Merged

camielvs merged 1 commit into
masterfrom
cvs/team-context

Conversation

@camielvs

@camielvs camielvs commented Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator

Description

The first of three foundation PRs for workspace secrets under team impersonation. Inside a project whose workspace maps to a team, secrets are managed and runs are submitted as that team: the backend's impersonation header is added to those specific requests only, never globally. This PR decides which team, if any, a set of projects acts as. Nothing here sends the header or changes a screen yet.

  • getTeamImpersonationHeaderName reads the header name the host page injects as window.__TANGLE_TEAM_IMPERSONATION__. With none configured, as in a default deployment, every team lookup resolves to none and the app behaves exactly as today. It also refuses a name that isn't a valid header, or one the app's own requests already send, such as Authorization, which the team header would overwrite.
  • teamRequestHeaders(teamId) is the one place the header is built. It returns {} without a configured header name or a team id.
  • resolveTeamId is the one place that knows a workspace names its team through data.teamId, the interim contract until the backend has a field for it.
  • readTeamMemberships is the one place that knows the teams map on GET /api/users/me. That map is fetched without the header; with it, the response describes the team and carries no teams. fetchTeamMemberships is its own lookup rather than the shared user query, which turns a failed request into an anonymous user for 30 minutes, so a failed lookup reports an error instead of "not a member".
  • fetchTeamIdentity asks /api/users/me with the header. A team only counts as active when the backend answers as that team. That's because a nobody role passes impersonation with no access, and a backend that doesn't read the configured header still lists teams but answers as the user. canWrite comes from the permissions the backend returns rather than from role names.
  • resolveTeam turns the lookups into one outcome, and useTeamForProjects gathers them. The outcome is one of none, resolving, active, not-member, unavailable, conflict (projects on different teams) or error.

Related Issue and Pull requests

Type of Change

  • New feature

Line Breakdown

Category Added Removed
Production code 393 0
Tests 653 0
Config / CI / tooling 1 0
Total 1047 0

Checklist

  • I have tested this does not break current pipelines / runs functionality
  • I have tested the changes on staging

Screenshots (if applicable)

None. There's no UI change.

Test Instructions

pnpm vitest run src/config/teamImpersonation.test.ts src/services/teams

pnpm validate (format, lint, typecheck, knip) passes. No screen calls these hooks yet, so there's nothing to click through.

Additional Comments

  • src/services/teams is added to react-compiler.config.js.
  • useTeamForProjects reads projects and workspaces through the existing ProjectsQueryKeys / WorkspacesQueryKeys caches and treats a deleted project as absent rather than as an error.

🤖 Generated with Claude Code

@camielvs
camielvs added this pull request to stack #2930 October 22, 2026 19:23
@github-actions

github-actions Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

🎩 Preview

A preview build has been created at: cvs/team-context/2fe4639

Base automatically changed from cvs/create-project-failure-reason to master October 22, 2026 19:59
@camielvs
camielvs force-pushed the cvs/team-context branch 2 times, most recently from 18c82f0 to a62e044 Compare October 26, 2026 21:56

@morgan-wowk morgan-wowk left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Partial review. Left one blocking comment for now.

Comment thread src/services/teams/workspaceTeam.ts Resolved
@morgan-wowk
morgan-wowk self-requested a review October 29, 2026 18:57
@camielvs
camielvs requested review from morgan-wowk and removed request for morgan-wowk October 29, 2026 19:49
Comment thread src/config/teamImpersonation.ts Resolved
Comment thread src/services/teams/useUserTeams.ts Resolved
@camielvs
camielvs marked this pull request as ready for review October 30, 2026 21:24
@camielvs
camielvs requested a review from a team as a code owner October 30, 2026 21:24
morgan-wowk

This comment was marked as outdated.

@morgan-wowk
morgan-wowk dismissed their stale review October 30, 2026 21:37

We will migrate later.

@camielvs
camielvs requested a review from maxy-shpfy October 30, 2026 22:45
@camielvs
camielvs requested a review from Mbeaulne October 30, 2026 22:45
The foundation for workspace secrets under team impersonation. Inside a
project whose workspace maps to a team, secrets are managed and runs are
submitted as that team by adding the backend's impersonation header to
those specific requests. Nothing here sends the header yet or changes any
screen; later changes in this stack draw on it.

- `getTeamImpersonationHeaderName` reads the header name the host page
  injects as `window.__TANGLE_TEAM_IMPERSONATION__`. With none configured,
  as in an OSS deployment, every team lookup resolves to "none". It
  refuses a name that isn't a valid header, or that the app's own
  requests already send, such as `Authorization`, which the team header
  would overwrite.
- `teamRequestHeaders` is the one place the header is built.
- `resolveTeamId` is the one place that knows a workspace names its team
  through `data.teamId`, the interim contract until the backend has a field.
- `readTeamMemberships` is the one place that knows the `teams` map on
  `GET /api/users/me`, which is fetched without the header (with it, the
  response describes the team and carries no `teams`).
  `fetchTeamMemberships` is its own lookup rather than the shared user
  query, which turns a failed request into an anonymous user for 30
  minutes, so a failed lookup is an error rather than "not a member".
- `fetchTeamIdentity` asks `/api/users/me` with the header. A team is only
  active when the backend answers as that team: the development backend
  ignores the header but still lists teams, and a `nobody` role passes
  impersonation with no access. `canWrite` comes from the permissions the
  backend returns rather than from role names.
- `resolveTeam` decides the outcome from the lookups, and
  `useTeamForProjects` gathers them: none, resolving, active, not-member,
  unavailable, conflict (projects on different teams) or error.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@camielvs
camielvs merged commit 31a9d61 into master Oct 31, 2026
18 checks passed
@camielvs
camielvs deleted the cvs/team-context branch October 31, 2026 00:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants