Skip to content

fix: smarter fallback team selection for scope inference#120

Open
gr2m wants to merge 9 commits intomainfrom
default-team
Open

fix: smarter fallback team selection for scope inference#120
gr2m wants to merge 9 commits intomainfrom
default-team

Conversation

@gr2m
Copy link
Copy Markdown
Contributor

@gr2m gr2m commented Mar 28, 2026

Summary

  • Fetches both /v2/user and /v2/teams in parallel to build an ordered list of candidate teams for scope inference
  • Tries user.defaultTeamId first, then falls back to the best hobby-plan OWNER team (personal team matching username, or most recently updated)
  • Filters fallback candidates by billing.plan === 'hobby' and membership.role === 'OWNER' to avoid selecting pro/enterprise teams
  • On 403, skips to the next candidate instead of failing immediately
  • Only throws after all candidates are exhausted, with a helpful error message including the authenticated username

@vercel
Copy link
Copy Markdown

vercel bot commented Mar 28, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
sandbox Ready Ready Preview, Comment, Open in v0 Apr 1, 2026 8:10pm
sandbox-cli Ready Ready Preview, Comment, Open in v0 Apr 1, 2026 8:10pm
sandbox-sdk Ready Ready Preview, Comment, Open in v0 Apr 1, 2026 8:10pm
sandbox-sdk-ai-example Ready Ready Preview, Comment, Open in v0 Apr 1, 2026 8:10pm
workflow-code-runner Ready Ready Preview, Comment, Open in v0 Apr 1, 2026 8:10pm

Request Review

Instead of always falling back to the username, prefer the user's
configured default team (defaultTeamId). Falls back to username
when defaultTeamId is not set.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Collaborator

@marc-vercel marc-vercel left a comment

Choose a reason for hiding this comment

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

Overall LGTM, left 3 comments (two of them are optional).

I see that the tests mock the API response, but given that we are changing API requests/responses, have we manually tested all the flows? If possible I would love to have some integration tests, but because we are dealing with auth I do not think it will be easy.


throw new NotOk({
statusCode: 403,
responseText: `Authenticated as "${username}" but none of the available teams allow sandbox creation. Specify a team explicitly with --scope <team-id-or-slug>.`,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Not a blocker: it would be nice to know which teams have we tried.

): Promise<{ candidateTeamIds: string[]; username: string }> {
const [userData, teamsData] = await Promise.all([
fetchApi({ token, endpoint: "/v2/user" }).then(UserSchema.parse),
fetchApi({ token, endpoint: "/v2/teams?limit=100" }).then(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

What happens if the user has more than 100 teams? I understand we are just covering the first 100 teams.

This is unlikely, but we should iterate on that. Not a blocker.

token: opts.token,
endpoint: `/v11/projects?slug=${encodeURIComponent(teamId)}`,
token,
endpoint: `/v11/projects?${teamParam}`,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Does this API endpoint ensures that the user can read sandboxes? Afaik it just checks that the user can read the project no?

For example, we require the Resource.VercelSandbox scope at the API level to read a sandbox.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

best we can do right now is to filter by OWNER roles

const hobbyOwnerTeams = teamsData.teams.filter(
(t) => t.membership.role === "OWNER" && t.billing.plan === "hobby",
);

I don't think there is a way to retrieve a list of teams filtered by a specific permission yet

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

So, this means we can login as a team that does not have permissions. I understand that this is already happening no? If so we can iterate on that later.

): Promise<{ candidateTeamIds: string[]; username: string }> {
const [userData, teamsData] = await Promise.all([
fetchApi({ token, endpoint: "/v2/user" }).then(UserSchema.parse),
fetchApi({ token, endpoint: "/v2/teams?limit=100" }).then(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Fetching all the teams (up to 100) will be quite slow right? In which case would a user not have a defaultTeamId?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

defaultTeamId is not always set. But we can request them sequentially

Ideally the /v2/teams endpoint would have a access/permission based filter and sort argument, then we'd only need to get one.

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