Repository navigation
feat(cli): add workspace and project commands - #2105
mishushakov merged 6 commits into
Conversation
🦋 Changeset detectedLatest commit: f075e5e 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 API layer: exposes
Reviewed by Cursor Bugbot for commit f075e5e. Bugbot is set up for automated code reviews on this repo. Configure here. |
Package ArtifactsBuilt from d366ef2. Download artifacts from this workflow run. JS SDK ( npm install ./e2b-dockerfile-utils-0.1.2-mishushakov-cli-project-commands.0.tgz ./e2b-2.63.1-mishushakov-cli-project-commands.0.tgzCLI ( npm install ./e2b-cli-2.23.1-mishushakov-cli-project-commands.0.tgzCode Interpreter JS SDK ( npm install ./e2b-code-interpreter-2.9.2-mishushakov-cli-project-commands.0.tgzDesktop JS SDK ( npm install ./e2b-desktop-2.5.1-mishushakov-cli-project-commands.0.tgzPython SDK ( pip install ./e2b_dockerfile_utils-0.1.1+mishushakov.cli.project.commands-py3-none-any.whl ./e2b-2.63.0+mishushakov.cli.project.commands-py3-none-any.whlCode Interpreter Python SDK ( pip install ./e2b_code_interpreter-2.11.9+mishushakov.cli.project.commands-py3-none-any.whlDesktop Python SDK ( pip install ./e2b_desktop-2.7.2+mishushakov.cli.project.commands-py3-none-any.whl |
ee2f633 to
fa9c2a9
Compare
fa9c2a9 to
9bcfdbd
Compare
9bcfdbd to
38c356b
Compare
38c356b to
dbc2af5
Compare
Bugbot is paused — on-demand spend limit reachedBugbot uses usage-based billing for this team and has hit its on-demand spend limit. A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue. |
dbc2af5 to
f69c949
Compare
f69c949 to
abfbb26
Compare
abfbb26 to
80770ea
Compare
80770ea to
dd1e549
Compare
009f9ed to
84fda51
Compare
05a5c16 to
1caac38
Compare
fde30b9 to
b8d6f26
Compare
b8d6f26 to
96bea84
Compare
96bea84 to
ff412ca
Compare
- `e2b workspace list` lists the workspaces from workspace-api /v1/me. - `e2b project list [--workspace]` lists projects (current one marked with *, blocked and provisioning ones flagged), `project use <project>` sets the default project (ID, prj_ ID, slug or name; inactive projects are refused, blocked ones warned about), `project info [--project]` shows the current project and what selected it. - Every sandbox and template command that calls the API takes `--project`. A root preAction hook hands it to ensureAuth(), so helpers pick it up without threading it through. `template list` keeps `-t`; `--team` still works there with its deprecation warning. - With E2B_API_KEY set, `--project` is ignored with a warning: the key determines the project. - workspace/project commands need a login even with E2B_API_KEY, since workspace-api only accepts the access token. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ce names - ProjectSource and ProjectStatus members replace the bare literals the project commands dispatched on; the JSON values are unchanged. - project use says what to do with a project that isn't active. - project list --workspace fails on a name shared by several workspaces instead of picking the first, as findProject already does. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Every sandbox and template command took --project; that is out of scope for this PR. template list keeps its -t/--project and E2B_PROJECT_ID still works. project info shows the current project only. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…t info Where the current project came from (`E2B_PROJECT_ID` or `project use`) isn't worth a concept of its own: `currentProject()` returns the reference, `project use` checks `E2B_PROJECT_ID` directly, and `project info` no longer prints `selected_by` / "Selected by". Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`e2b project use` without a project asks for the workspace and then the project, like login and `auth configure`; `--workspace` offers only that workspace's projects, or scopes a given name to it. `project list --workspace` shares the lookup (findWorkspace). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
ff412ca to
f075e5e
Compare
… 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>
…plane (#2104) ## Summary `e2b auth login` and `e2b auth configure` stop calling the API's `GET /teams` and read the user's workspaces and projects from workspace-api `GET /v1/me` instead. Requests then go to the **selected project's own control plane**, with `Authorization: Bearer <access token>` and `X-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 /teams` minted a key in every project on each call. ## Login / configure 1. `GET https://workspace-api.e2b.dev/v1/me` with 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. Run `e2b auth login` again."). There is no `POST /v1/users/bootstrap` call. 2. Login asks for the workspace and then for one of its active projects, both steps shown even with a single choice (`pickProject`, also used by `auth configure` and `project use` in #2105). The active project in the workspace with `kind: personal` is preselected, the rule workspace-api uses for `is_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), …`). 3. The config stores `projectId` (UUID), `projectName` and the new `projectDomain` (the project's `control_plane_domain`). `auth configure` is the same picker with the current project preselected. It writes through `updateUserConfig()`, which holds the token-refresh lock. **workspace-api URL:** it only exists at `workspace-api.e2b.dev`, while the CLI's API default is `e2b.app`. `E2B_DOMAIN` values `e2b.app`/`e2b.dev` (or unset) map to `https://workspace-api.e2b.dev`, any other domain to `https://workspace-api.<domain>`, and `E2B_WORKSPACE_API_URL` overrides it. Logging in needs workspace-api, so a custom `E2B_DOMAIN` is supported with `E2B_API_KEY` only; there is no fallback to `GET /teams` or to the stored project. `workspace-api.e2b.app` returns 400. The `/v1/me` response is validated with zod (only the fields the CLI uses; `status`/`kind` kept 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`: | Project's control plane | via `api.e2b.app` | via `api.<control_plane_domain>` | | --- | --- | --- | | e2b-foxtrot.dev | 200 | 200 | | e2b-juliett.dev | 401 | 200 | | e2b-tango.dev | 401 | 200 | | foxtrot-2.e2b.dev | 401 | 200 | | foxtrot-3.e2b.dev | 401 | 200 | 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 create` no longer pins the default domain. Configs written by earlier versions have no `projectDomain`; it's looked up in `/v1/me` once and saved. `E2B_PROJECT_ID` now 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_KEY` is unchanged: the key decides the project and the default domain applies. `template list` no longer sends the project as the `teamID` query param: that param accepts IDs only, while `X-Team-ID` (or the API key) already selects the project, so a slug or name in `--project`/`E2B_PROJECT_ID` works 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-time `projectDomain` lookup uses that to patch only the project still selected, so an `e2b auth configure` that switches projects in the meantime can't end up with the other project's domain. ## Testing - `tests/workspaces.test.ts` (27): - URL mapping and override; - personal-project rule, SSO-only users and non-active projects skipped; - `findProject` by every form, ambiguous names, a slug shared across workspaces, slugs winning over names, unknown projects; - `pickProject` with no active project and without a terminal; with a terminal: workspace then project, single-choice steps still asked, a given workspace only; - `findProject` scoped to a workspace; - `fetchMe` 401 → undefined, bearer header, schema rejection; - the blocked warning. - `tests/auth.test.ts` (+3): the stored project needs no lookup; the one-time domain lookup is saved; `E2B_PROJECT_ID` and `--project` resolve to UUID + domain with one `/v1/me` call; unknown project exits with a hint. - `pnpm run test` in `packages/cli`: 216 passed, 11 skipped (the `template create` backend suite needs `E2B_API_KEY`). - **On prod with my login:** - `sandbox list` on a config from #2103 looked up and stored `e2b-foxtrot.dev`; - `E2B_PROJECT_ID=<juliett project>` listed fine; - on a tango project: `sandbox create` → `exec` → `info` → `kill` all worked; - an unknown `E2B_PROJECT_ID` exits with the not-found message. - Tested before the two-step picker and the `template list` change; those are covered by the unit tests. - Login itself wasn't re-run end to end (that signs out). The personal-project and picker paths are covered by the unit tests above. - Repo-wide lint and typecheck pass. Part of PRD-119. Implements BKND-155 (CLI side); related: SDK-400. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
## Summary `E2B_ACCESS_TOKEN` authenticates the CLI with an OAuth access token, for CI and other places where nobody can run `e2b auth login`. Based on `main`, which has #2103–#2105 merged (#2124 was closed, so this no longer stacks on it). Docs: to be added to e2b-dev/docs#636 before release. ```console $ E2B_ACCESS_TOKEN=<token> E2B_PROJECT_ID=prj_… e2b sandbox list # the project for every request $ E2B_ACCESS_TOKEN=<token> E2B_PROJECT_ID=web-app e2b template list # by name or slug works too $ E2B_ACCESS_TOKEN=<token> e2b project list # listing needs no project: find the one to set $ E2B_ACCESS_TOKEN=<token> e2b sandbox list E2B_ACCESS_TOKEN needs E2B_PROJECT_ID: set it to the project requests go to (its ID, prj_ ID, slug or name). Run e2b project list to see your projects. $ E2B_ACCESS_TOKEN=<legacy dashboard token> E2B_API_KEY=e2b_… e2b auth info E2B_ACCESS_TOKEN is not an OAuth access token and is ignored: access tokens from the dashboard are no longer supported. Unset it to remove this warning. Authenticating with E2B_API_KEY. ``` ## How - **Precedence:** `E2B_API_KEY` or `E2B_ACCESS_TOKEN`, then the stored login. Setting both is refused before any request (`Both E2B_API_KEY and E2B_ACCESS_TOKEN are set. Unset one of them.`), since the key selects its own project and the token its user's, so there is no sensible precedence. While the token is set the stored login is ignored entirely, including its selected project: the token may belong to another user. - **Project:** `E2B_PROJECT_ID` is required for every request and is the project for it, like `E2B_API_KEY` decides the project on its own. It is resolved through `/v1/me` (ID, `prj_` ID, slug or name) for the UUID `X-Team-ID` carries and the project's control plane. `--project` plays no part, as with the key. Without it a command that sends requests exits before any of them: `E2B_ACCESS_TOKEN needs E2B_PROJECT_ID: set it to the project requests go to (its ID, prj_ ID, slug or name). Run e2b project list to see your projects.` There is no fallback project: `/v1/me` has no default, and a guessed one would send CI writes to the wrong project silently. - **No refresh:** there is no refresh token, so the token is used as is. JWT claims are read with [jose](https://github.com/panva/jose) (`decodeJwt`, no signature check: the API verifies the token; this replaces the hand-rolled base64url decoding the refresh logic used too). A JWT whose `exp` has passed is rejected before any request with `E2B_ACCESS_TOKEN has expired. Set a current one, or unset it and run e2b auth login.` A value that isn't a JWT at all is a legacy dashboard token (the variable name used to carry those, and some shells and CI jobs still export them): it is ignored as if unset, so the stored login or `E2B_API_KEY` is used (and it doesn't count as setting both), with a warning on stderr: `E2B_ACCESS_TOKEN is not an OAuth access token and is ignored: access tokens from the dashboard are no longer supported. Unset it to remove this warning.` A token workspace-api rejects fails with `E2B_ACCESS_TOKEN was rejected`. - **Commands:** `requireMe()` accepts the token, so `workspace list`, `project list` and `project info` work without a login, with `E2B_PROJECT_ID`'s project as the current one. `workspace list` and `project list` don't need `E2B_PROJECT_ID` (no project is marked current), so they can be used to find it; `project info` without it shows the missing-`E2B_PROJECT_ID` error. `e2b project use` and `e2b auth configure` refuse with the token set, since the stored selection wouldn't be used and the picker would show the token user's projects, and point at `E2B_PROJECT_ID`. `e2b auth info` reports the credential in use and that a stored login is not used, warns (without failing) when the token has no `E2B_PROJECT_ID`, and exits with the same error when both variables are set. The signed-out box mentions the variable next to `E2B_API_KEY`. ## Testing - `tests/auth.test.ts` (+10): `auth info` with the token and no `E2B_PROJECT_ID` reporting the token and warning, token + `E2B_PROJECT_ID` with the stored login and `--project` ignored (and `/v1/me` asked with the env token), the missing-`E2B_PROJECT_ID` error from `ensureAuth` pointing at `e2b project list`, `requireMe` listing projects without `E2B_PROJECT_ID`, both variables together refused, an expired token rejected before any request, a non-JWT legacy value ignored with a warning (the stored login is used) and not refused next to `E2B_API_KEY` (the key is used), `requireMe` without a login, a rejected token named as such. - `tests/commands/project.test.ts` (+5): `project list` marks `E2B_PROJECT_ID`'s project current with the token and marks none without it, `project info` without it asks for it, `project use` and `auth configure` refuse. - `pnpm run test` in `packages/cli`: 253 passed, 11 skipped; `tests/commands/template/create.test.ts` fails because its backend suite needs `E2B_API_KEY`. Lint and typecheck pass. - Checked with the built CLI: the missing-`E2B_PROJECT_ID` error, and the legacy-token warning next to `E2B_API_KEY` (output above). - Not run against prod: I don't have an access token outside my login's config to test with. Part of PRD-119. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Summary
New commands to see and switch workspaces and projects. Stacked on #2104 (stack #2115); docs in e2b-dev/docs#637.
Commands
e2b workspace list(ws ls): ID, name, kind, project count, status (blocked shows reasons).-f jsonaddsproject_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.--workspacetakes an ID,wrk_ID or name; an ambiguous name is rejected.-f jsonaddsworkspace_nameandcurrent.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 andauth configure; both steps are shown even with one choice, and a terminal is required).--workspaceoffers only that workspace's projects, or scopes a given name to it. Inactive projects are refused, blocked ones get a warning. It warns whenE2B_PROJECT_IDorE2B_API_KEYwould 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 neede2b auth logineven whenE2B_API_KEYis set, because workspace-api only takes the access token.e2b auth configurestays as the interactiveproject use, and login now points toe2b project use.A
--projectoption on every API command was in an earlier revision and has been dropped to keep the scope small.template listkeeps its existing-t, --project, andE2B_PROJECT_IDstill overrides the selected project.Testing
tests/commands/project.test.ts(14), run through the real program with/v1/memocked:workspace listJSON;project list: current marker, blocked/provisioning status,--workspacefilter, 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,--workspaceonly, no terminal);project infoJSON, and the error when nothing is selected.projectandworkspacepages.pnpm run testinpackages/cli: 230 passed, 11 skipped (thetemplate createbackend suite needsE2B_API_KEY). Repo-wide lint and typecheck pass.project useto a tango project and back, withsandbox listfollowing the selection (before the interactiveproject useand--workspacewere added; those are covered by the unit tests).Part of PRD-119.
🤖 Generated with Claude Code