Skip to content

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

Draft
camielvs wants to merge 1 commit into
masterfrom
cvs/team-context
Draft

camielvs wants to merge 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.
  • 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.
  • fetchTeamIdentity asks /api/users/me with the header. A team only counts as active when the backend answers as that team. That's because 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 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 358 0
Tests 566 0
Config / CI / tooling 1 0
Total 925 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/18c82f0

Base automatically changed from cvs/create-project-failure-reason to master October 22, 2026 19:59
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".
- `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`).
- `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>

This branch has not been deployed

No deployments
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.

1 participant