Repository navigation
feat(cli): pick projects from workspace-api, route to each project's plane - #2104
mishushakov merged 10 commits into
Conversation
🦋 Changeset detectedLatest commit: 3487696 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
PR SummaryMedium Risk Overview Reviewed by Cursor Bugbot for commit 3487696. Bugbot is set up for automated code reviews on this repo. Configure here. |
Package ArtifactsBuilt from 8f81d27. Download artifacts from this workflow run. JS SDK ( npm install ./e2b-dockerfile-utils-0.1.2-mishushakov-cli-workspace-api-login.0.tgz ./e2b-2.63.1-mishushakov-cli-workspace-api-login.0.tgzCLI ( npm install ./e2b-cli-2.23.1-mishushakov-cli-workspace-api-login.0.tgzCode Interpreter JS SDK ( npm install ./e2b-code-interpreter-2.9.2-mishushakov-cli-workspace-api-login.0.tgzDesktop JS SDK ( npm install ./e2b-desktop-2.5.1-mishushakov-cli-workspace-api-login.0.tgzPython SDK ( pip install ./e2b_dockerfile_utils-0.1.1+mishushakov.cli.workspace.api.login-py3-none-any.whl ./e2b-2.63.0+mishushakov.cli.workspace.api.login-py3-none-any.whlCode Interpreter Python SDK ( pip install ./e2b_code_interpreter-2.11.9+mishushakov.cli.workspace.api.login-py3-none-any.whlDesktop Python SDK ( pip install ./e2b_desktop-2.7.2+mishushakov.cli.workspace.api.login-py3-none-any.whl |
e52f5a2 to
6f8389e
Compare
6f8389e to
93462f8
Compare
93462f8 to
a7786dc
Compare
a7786dc to
856b542
Compare
856b542 to
e0022d9
Compare
a8435c1 to
7a99871
Compare
Codex Review Summary: ✅ CompletedThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
1ff090e to
f49ad43
Compare
99f4092 to
d960c1a
Compare
…plane Login and `auth configure` read the user's workspaces and projects from workspace-api `GET /v1/me` instead of the API's `GET /teams`, which minted an API key in every project on each call (SDK-400). - workspace-api is only deployed at workspace-api.e2b.dev: E2B_DOMAIN e2b.app / e2b.dev map there, others to workspace-api.<domain>, and E2B_WORKSPACE_API_URL overrides it. Responses are validated with zod. - Login selects the active project in the personal workspace (the rule workspace-api uses for is_default). SSO-only users get their only active project, or a prompt (the first one without a terminal). A 401 means the user isn't set up yet: POST /v1/users/bootstrap, then retry. Blocked projects print a warning. - Projects live on different control planes; api.e2b.app answers 401 for any project outside foxtrot. The config now stores the project's control_plane_domain and ensureAuth() returns it as the SDK `domain`. Configs from earlier versions get it looked up once and saved. - E2B_PROJECT_ID / --project resolve through /v1/me (fetched once per process) by ID, prj_ public ID, slug or unambiguous name. - Config updates outside a token refresh go through updateUserConfig(), which holds the refresh lock. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ff-TTY - getMe bootstraps the account when /v1/me returns 401, as login does, so sessions from before workspace-api recover without logging out. - pickProject keeps the selected project without a terminal instead of silently switching to the first one. - Named ProjectStatus/WorkspaceKind members and a request timeout constant; the domain comes from ConnectionConfig like the SDK. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…hared slugs - `template list` no longer sends the project as the `teamID` query param, which accepts IDs only: `--project` and `E2B_PROJECT_ID` given as a slug or name failed there while X-Team-ID already selected the project. With that gone `resolveProjectId` had no caller. - `findProject` treats a slug shared across workspaces as ambiguous, like a name, instead of taking the first match. - `auth login` prints workspace-api failures as an error instead of an unhandled rejection. - The one-time control-plane domain backfill only patches the project still selected under the lock, so `e2b auth configure` switching projects in the meantime can't end up with the other project's domain. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
d960c1a to
cf7df2e
Compare
ensureAuth appended "Run `e2b auth configure`" to every resolveProject failure, including session, network and schema errors from workspace-api that already say what to do. The hint now lives in findProject's not-found message only. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Login resolves projects through workspace-api, which only exists for e2b.dev; the changeset says other domains are API-key only. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
pickProject asks for the workspace and then for one of its active projects, skipping a step with a single choice, and takes a `workspace` to offer that workspace's projects only. findProject can be scoped to a workspace the same way. Login and `auth configure` use the new flow. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The workspace and project prompts are shown even with one choice, so the user sees what is being selected. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
| // A user who signed up but never opened the dashboard isn't set up in | ||
| // workspace-api yet (401); bootstrapping creates the account. | ||
| let me = await fetchMe(accessToken) | ||
| if (!me) { | ||
| await bootstrapUser(accessToken) | ||
| me = await fetchMe(accessToken) |
…et up A 401 from /v1/me is a rejected session, not a missing account: login and every other command report it instead of calling POST /v1/users/bootstrap first. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
| const me = await fetchMe(accessToken) | ||
| if (!me) { | ||
| throw new Error( | ||
| 'Your login was not accepted by workspace-api, please contact support' | ||
| ) |
There was a problem hiding this comment.
🔴 Unprovisioned users cannot log in
A valid user absent from workspace-api gets a 401 from fetchMe, so login exits without saving credentials. getMe also rejects these users, blocking project resolution for older configs.
Learn more
Browser sign-in can succeed before workspace-api has created a user's account. In that state, fetchMe returns undefined for /v1/me's 401, even though the access token is valid. Login then exits without writing the config; getMe also rejects existing configurations that need workspace-api to find their project. Running login again cannot create the missing account.
Example: Alice signs up, then uses CLI login without opening the dashboard. Browser sign-in returns her access token, but /v1/me returns 401 because Alice is not set up there. Login exits, and repeating it produces the same result instead of selecting her project.
Recommended fix: Restore account initialization on the unprovisioned-user path for both login and getMe, then retry /v1/me. If 401 can also mean an invalid token, distinguish that response before attempting initialization and preserve the rejected-session error for genuinely invalid credentials.
Was this helpful? React with 👍 or 👎 to provide feedback.
…eselected Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… key (#2103) ## Summary The CLI now authenticates every request with the login's OAuth access token instead of the project API key it used to store in `~/.e2b/config.json`. It refreshes the token automatically, including during long-running commands and across concurrent `e2b` processes. `E2B_API_KEY` still works and wins, for CI/CD. Needs #2102 (merged 2026-11-10); docs in e2b-dev/docs#636. Bottom of stack #2115: #2103 → #2104 → #2105 → #2124 → #2125; each PR merges into the one below. ## Changes **1. Login URL.** `e2b auth login` opens `https://e2b.dev/auth/cli` (`https://$E2B_DOMAIN/auth/cli` when set). `DOCS_BASE` and the `E2B_DOCS_BASE` override are removed. Both the old `e2b.dev/docs/api/cli` and the new URL already 308 to `console.e2b.dev/auth/cli`, so the browser flow itself is unchanged. **2. Automatic refresh.** `ensureAuth()` replaces `ensureAPIKey()` everywhere: ```ts const auth = await ensureAuth() // { apiKey } from E2B_API_KEY, else: // { apiHeaders: { Authorization: 'Bearer <access token>', 'X-Team-ID': <project> } } await Sandbox.list({ ...auth }) ``` - Tokens are refreshed when they expire within a minute; they live one hour. - **Refresh tokens rotate**, and the old one is rejected once used. Before, two commands refreshing at the same time could get `invalid_grant`, which deletes the config and signs the user out. Refreshes are now serialized with a lock on the config (`~/.e2b/config.json.lock`, managed by [proper-lockfile](https://github.com/moxystudio/node-proper-lockfile): kept fresh while held, released on process exit, taken over once stale after 30 s), through a generic `withFileLock(path, fn, { staleMs, timeoutMs })` in `src/utils/lock.ts` that #2104 also uses for project changes. The config is re-read under the lock, so a process that waited reuses the tokens the other one just wrote. A lock lost mid-refresh (the holder stalled past the stale window) prints a warning instead of crashing the command. - Credentials are resolved per request, not per command. `sandbox logs --follow`, `sandbox metrics --follow` and the attached `sandbox create` keepalive (now the static `Sandbox.setTimeout`, not the instance method that kept the original token) keep working past an hour. - `e2b auth configure` re-reads the config before saving the selected project. Before, it wrote back the tokens it had read before its prompt, which undid any refresh another process made while the prompt was open. - Config writes are atomic (temp file + rename), since refreshes now write in the background of any command. **3. The stored API key plays no role.** - Login and configure don't write `projectApiKey`, and nothing reads it. The schema and the v1 → v2 migration don't know the field: a key an earlier version stored is carried through writes untouched, like any field this version doesn't know about, but the CLI makes no promise about it. - The project for each request comes from `--project`, then `E2B_PROJECT_ID` / `E2B_TEAM_ID`, then the configured project. So `template list --project` now selects the project for logged-in users too. ```console $ e2b auth login # opens https://e2b.dev/auth/cli $ e2b sandbox list # Bearer <access token> + X-Team-ID: <selected project> $ E2B_PROJECT_ID=<other> e2b template list # one-off project override $ E2B_API_KEY=e2b_... e2b template create my-tpl # CI: API key, no login needed ``` ## Notes for review - **Downgrades.** Released CLIs require `projectApiKey` in a v2 config. After a fresh login with this version there is none, so an older CLI reports the deprecated-config message and asks for `e2b auth login`. The changeset calls this out. - **SDK-400 isn't fixed by this alone.** Login and configure still call `GET /teams` here, which mints a new API key for every project server-side; the CLI just stops storing it. #2104, next in the stack, replaces that call with workspace-api `/v1/me` (BKND-155's CLI side; with access tokens its planned `POST /api-keys` step is not needed). - **Known limit.** `Template.build` is a single SDK call that polls build status with the credentials it started with. A build running over an hour would need a fresh token mid-call. Handling that needs a header provider in the SDK; I left it out here. ## Testing - New `tests/auth.test.ts` (10 tests): - `E2B_API_KEY` precedence; - bearer headers and project selection (`--project` > `E2B_PROJECT_ID` > config); - the signed-out exit; - refresh storing the rotated tokens and keeping fields it doesn't know; - **5 concurrent callers → exactly one refresh**; - `invalid_grant` → signed out; - stale-lock recovery; - `tests/utils/lock.test.ts`: one holder at a time, release when the holder throws, stale-lock takeover, timeout while a live holder keeps the lock; - the `/auth/cli` URL with and without `E2B_DOMAIN`. With the lock stubbed out, the concurrency and stale-lock tests fail. - `user_config_permissions`: unknown fields survive the write, and no temp file is left behind. Migration tests are updated. The command tests mock `ensureAuth`. - `pnpm run test` in `packages/cli`: 186 passed, 11 skipped (the `template create` backend suite needs `E2B_API_KEY`). - **Against prod with my login and no `E2B_API_KEY`:** `auth info`, `sandbox list`, `template list`, then `sandbox create --detach` → `info` → `exec` → `logs` → `kill`. All pass. - **Real rotation:** I planted an expired access token in my config (keeping the real refresh token) and ran three commands in parallel. All three succeeded, the refresh happened once, the session stayed valid, and the lock file was removed. - Repo-wide lint and typecheck pass. Part of PRD-119. Related: SDK-400, BKND-155. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
## Summary New commands to see and switch workspaces and projects. Stacked on #2104 (stack #2115); docs in e2b-dev/docs#637. ```console $ e2b workspace list WORKSPACE ID NAME KIND PROJECTS STATUS wrk_5gr13nge8zagptrxzbr8example Personal Workspace personal 1 active wrk_0220yj49rh866r4kgsqrexample Acme standard 3 active $ e2b project list --workspace Acme CURRENT PROJECT ID NAME WORKSPACE REGION STATUS * prj_6tpk9mnczw9e1vm268r8example web-app Acme us-west-1 active prj_1dvq3yqa2p83zbtpn2wyexample asia-jobs Acme ap-southeast-1 active prj_3n79c45xbx9rwv3v4megexample eu-batch Acme eu-west-1 blocked (credit_exhausted) $ e2b project use asia-jobs Now using project asia-jobs (prj_1dvq3yqa2p83zbtpn2wyexample) in workspace Acme. $ e2b project use # no project: pick the workspace, then the project ? Select workspace › Acme (standard, 2 active projects) [current] ? Select project › web-app (us-west-1) [current] $ e2b project info Project info for asia-jobs: Project ID: prj_1dvq3yqa2p83zbtpn2wyexample Region: ap-southeast-1 Control plane: e2b-juliett.dev Status: active … ``` ## Commands - **`e2b workspace list`** (`ws ls`): ID, name, kind, project count, status (blocked shows reasons). `-f json` adds `project_count`. - **`e2b project list [-w, --workspace <ws>]`** (`prj ls`): every project you can reach, with the current one marked `*` and blocked or provisioning ones shown in the status. `--workspace` takes an ID, `wrk_` ID or name; an ambiguous name is rejected. `-f json` adds `workspace_name` and `current`. - **`e2b project use [project] [-w, --workspace <ws>]`:** stores the project (UUID, name, control plane) under the refresh lock. Without a project it asks for the workspace and then one of its active projects (the same picker as login and `auth configure`; both steps are shown even with one choice, and a terminal is required). `--workspace` offers only that workspace's projects, or scopes a given name to it. Inactive projects are refused, blocked ones get a warning. It warns when `E2B_PROJECT_ID` or `E2B_API_KEY` would override the choice. - **`e2b project info`:** name, both IDs, slug, workspace, region, control plane, status and your capabilities. All of these accept a project by UUID, `prj_` public ID, slug or (unambiguous) name. The workspace/project commands need `e2b auth login` even when `E2B_API_KEY` is set, because workspace-api only takes the access token. `e2b auth configure` stays as the interactive `project use`, and login now points to `e2b project use`. A `--project` option on every API command was in an earlier revision and has been dropped to keep the scope small. `template list` keeps its existing `-t, --project`, and `E2B_PROJECT_ID` still overrides the selected project. ## Testing - `tests/commands/project.test.ts` (14), run through the real program with `/v1/me` mocked: - `workspace list` JSON; - `project list`: current marker, blocked/provisioning status, `--workspace` filter, unknown and ambiguous workspace; - `project use`: stores ID/name/domain, warns when blocked, refuses provisioning, scopes a name to `--workspace`, picks interactively without a project (workspace prompt, `--workspace` only, no terminal); - `project info` JSON, and the error when nothing is selected. - The generated CLI reference gains `project` and `workspace` pages. - `pnpm run test` in `packages/cli`: 230 passed, 11 skipped (the `template create` backend suite needs `E2B_API_KEY`). Repo-wide lint and typecheck pass. - **On prod with my login:** `project use` to a tango project and back, with `sandbox list` following the selection (before the interactive `project use` and `--workspace` were added; those are covered by the unit tests). Part of PRD-119. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Summary
e2b auth loginande2b auth configurestop calling the API'sGET /teamsand read the user's workspaces and projects from workspace-apiGET /v1/meinstead. Requests then go to the selected project's own control plane, withAuthorization: Bearer <access token>andX-Team-ID: <project>. Stacked on #2103 (stack #2115); docs in e2b-dev/docs#637.This is the CLI half of BKND-155, minus its API-key step: since #2103 the CLI uses the access token directly, so no key is minted at all. That also ends SDK-400's key sprawl from the CLI side, because
GET /teamsminted a key in every project on each call.Login / configure
GET https://workspace-api.e2b.dev/v1/mewith the access token. Every user is assumed to be set up there: a 401 is reported as a rejected session (login: "Your login was not accepted by workspace-api, please contact support"; other commands: "Your session was not accepted. Rune2b auth loginagain."). There is noPOST /v1/users/bootstrapcall.pickProject, also used byauth configureandproject usein feat(cli): add workspace and project commands #2105). The active project in the workspace withkind: personalis preselected, the rule workspace-api uses foris_default; SSO-only users have none, so nothing is preselected. Without a terminal, that default is taken as is, else the first active project. Blocked projects print a warning (Project X is blocked (credit_exhausted), …).projectId(UUID),projectNameand the newprojectDomain(the project'scontrol_plane_domain).auth configureis the same picker with the current project preselected. It writes throughupdateUserConfig(), which holds the token-refresh lock.workspace-api URL: it only exists at
workspace-api.e2b.dev, while the CLI's API default ise2b.app.E2B_DOMAINvaluese2b.app/e2b.dev(or unset) map tohttps://workspace-api.e2b.dev, any other domain tohttps://workspace-api.<domain>, andE2B_WORKSPACE_API_URLoverrides it. Logging in needs workspace-api, so a customE2B_DOMAINis supported withE2B_API_KEYonly; there is no fallback toGET /teamsor to the stored project.workspace-api.e2b.appreturns 400. The/v1/meresponse is validated with zod (only the fields the CLI uses;status/kindkept as plain strings so a new server-side value doesn't break the CLI).Why per-project domains
Your projects live on five planes. Measured on prod with
GET /sandboxes, Bearer +X-Team-ID:api.e2b.appapi.<control_plane_domain>So
ensureAuth()now returns the project's domain along with the headers, and the SDK sends both the API calls and the sandbox (envd) traffic there.template createno longer pins the default domain. Configs written by earlier versions have noprojectDomain; it's looked up in/v1/meonce and saved.E2B_PROJECT_IDnow resolves through/v1/me(fetched once per process) by UUID,prj_public ID, slug or unambiguous name, so it also routes to the right plane. A slug or name that matches projects in several workspaces is rejected with the candidates listed.E2B_API_KEYis unchanged: the key decides the project and the default domain applies.template listno longer sends the project as theteamIDquery param: that param accepts IDs only, whileX-Team-ID(or the API key) already selects the project, so a slug or name in--project/E2B_PROJECT_IDworks there too.resolveProjectId()had no other caller and is gone.updateUserConfig()also takes a function of the config as it is under the lock. The one-timeprojectDomainlookup uses that to patch only the project still selected, so ane2b auth configurethat switches projects in the meantime can't end up with the other project's domain.Testing
tests/workspaces.test.ts(27):findProjectby every form, ambiguous names, a slug shared across workspaces, slugs winning over names, unknown projects;pickProjectwith no active project and without a terminal; with a terminal: workspace then project, single-choice steps still asked, a given workspace only;findProjectscoped to a workspace;fetchMe401 → undefined, bearer header, schema rejection;tests/auth.test.ts(+3): the stored project needs no lookup; the one-time domain lookup is saved;E2B_PROJECT_IDand--projectresolve to UUID + domain with one/v1/mecall; unknown project exits with a hint.pnpm run testinpackages/cli: 216 passed, 11 skipped (thetemplate createbackend suite needsE2B_API_KEY).sandbox liston a config from feat(cli): authenticate with the refreshed access token, drop the API key #2103 looked up and storede2b-foxtrot.dev;E2B_PROJECT_ID=<juliett project>listed fine;sandbox create→exec→info→killall worked;E2B_PROJECT_IDexits with the not-found message.template listchange; those are covered by the unit tests.Part of PRD-119. Implements BKND-155 (CLI side); related: SDK-400.
🤖 Generated with Claude Code