Repository navigation
feat(cli): show the project ID in sandbox, snapshot and template lists - #2124
mishushakov wants to merge 2 commits into
Conversation
PR SummaryLow Risk Overview Reviewed by Cursor Bugbot for commit 3538660. Bugbot is set up for automated code reviews on this repo. Configure here. |
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. |
🦋 Changeset detectedLatest commit: 3538660 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 |
There was a problem hiding this comment.
🔍 Devin Review: 1 flag
Not posted on this PR by your GitHub settings — view it in Devin Review. (Configure)
There was a problem hiding this comment.
TASTE.md review (CLI-only PR, so the SDK-surface rules apply only where relevant).
Checked: T-9 (verb names), T-11/T-12 (naming and ID casing), T-15, T-18/T-18a (boundary types and shared mapping), T-20 (undefined over null), T-52 (backend as source of truth), T-61, T-69/T-72 (docs).
Violations: 1
- T-52:
publicProjectId()copies workspace-api'spublic_idencoding instead of using thepublic_idthe server returns (inline).
Notes, not violations:
- T-12: the new
projectIDJSON key follows the rule. Insandbox list -f jsonit sits next to the SDK'ssandboxId/templateId, so the casing is mixed within one object. That casing comes from the existing SDK and was not added here. - T-18a: the same spread,
projectId ? { ...x, projectID: projectId } : x, appears in all three list commands. A shared helper would keep the key name defined in one place.
| const TYPEID_ALPHABET = '0123456789abcdefghjkmnpqrstvwxyz' | ||
|
|
||
| /** | ||
| * The `prj_` public ID of a project UUID: the TypeID encoding workspace-api | ||
| * uses for `public_id`, computed locally so lists don't need `/v1/me`. | ||
| * Anything that isn't a UUID is returned as is. | ||
| */ | ||
| export function publicProjectId(id: string): string { | ||
| const hex = id.replace(/-/g, '') | ||
| if (!/^[0-9a-f]{32}$/i.test(hex)) return id | ||
|
|
||
| const value = BigInt(`0x${hex}`) | ||
| let suffix = '' | ||
| for (let i = 25; i >= 0; i--) { | ||
| suffix += TYPEID_ALPHABET[Number((value >> BigInt(5 * i)) & 31n)] | ||
| } | ||
| return `prj_${suffix}` | ||
| } |
There was a problem hiding this comment.
T-52 — the backend is the source of truth; client code must never mirror backend business rules, because a duplicated rule drifts from the server's the moment the API changes.
publicProjectId() re-implements workspace-api's public_id encoding (the TypeID prefix, the alphabet and the 26-char base32 layout) on the client. If workspace-api changes the prefix, the ID scheme or how it derives public_id from the UUID, the CLI keeps printing IDs that look valid but match nothing in e2b project list, and nothing fails. The fallback makes this worse: anything that isn't a UUID goes through unchanged, so the Project ID column can show a raw value that isn't a prj_ ID with no warning.
A compliant version uses the public_id the server already returns instead of computing it. /v1/me returns it, getMe() caches it per process, and resolveProject() already gets the whole project from findProject(...). One way to do this:
resolveProject()returns{ id, publicId: project.public_id, domain }.e2b project usestoresprojectPublicIdnext toprojectDomain, so the fast path still needs no round trip.- The lookup-once fallback fills in
projectPublicIdthe same way it fills inprojectDomain.
projectIdOf(auth) then reads that value and stops parsing it back out of X-Team-ID.
Package ArtifactsBuilt from cafc4fe. Download artifacts from this workflow run. JS SDK ( npm install ./e2b-dockerfile-utils-0.1.2-mishushakov-cli-list-project-ids.0.tgz ./e2b-2.63.1-mishushakov-cli-list-project-ids.0.tgzCLI ( npm install ./e2b-cli-2.23.1-mishushakov-cli-list-project-ids.0.tgzCode Interpreter JS SDK ( npm install ./e2b-code-interpreter-2.9.2-mishushakov-cli-list-project-ids.0.tgzDesktop JS SDK ( npm install ./e2b-desktop-2.5.1-mishushakov-cli-list-project-ids.0.tgzPython SDK ( pip install ./e2b_dockerfile_utils-0.1.1+mishushakov.cli.list.project.ids-py3-none-any.whl ./e2b-2.63.0+mishushakov.cli.list.project.ids-py3-none-any.whlCode Interpreter Python SDK ( pip install ./e2b_code_interpreter-2.11.9+mishushakov.cli.list.project.ids-py3-none-any.whlDesktop Python SDK ( pip install ./e2b_desktop-2.7.2+mishushakov.cli.list.project.ids-py3-none-any.whl |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: bbd820d2b0
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| JSON.stringify(sortSandboxes(sandboxes, options.order), null, 2) | ||
| JSON.stringify( | ||
| sortSandboxes(sandboxes, options.order).map((sandbox) => | ||
| projectId ? { ...sandbox, projectID: projectId } : sandbox |
There was a problem hiding this comment.
Use
projectId for SDK-shaped JSON records
When access-token users run sandbox list --format json, these records otherwise preserve the SDK's camel-cased SandboxInfo fields (sandboxId, templateId), so emitting projectID makes the new public field inconsistent and causes consumers using the expected .projectId path to miss it. The snapshot JSON path has the same issue at packages/cli/src/commands/sandbox/snapshot.ts:107, where the existing identifier is snapshotId; use projectId for these two SDK-shaped outputs while retaining projectID for template API records.
Useful? React with 👍 / 👎.
None of the list responses carry a project: each list is scoped to the project the request went to (X-Team-ID). The lists now show that project's prj_ ID as a `Project ID` column, and as `projectId` (`projectID` for templates, matching `templateID`) in JSON output. The prj_ ID is the TypeID encoding of the project UUID, computed locally (publicProjectId), so lists don't need a /v1/me call. With E2B_API_KEY the key decides the project and the CLI can't tell which, so the column and field are left out. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
sandbox list and snapshot list used projectId while template list used projectID; one spelling means one jq path. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
b8d6f26 to
96bea84
Compare
bbd820d to
3538660
Compare
| { header: 'Metadata', value: (row) => row.metadata }, | ||
| // Omitted with E2B_API_KEY, where the CLI doesn't know the key's project. | ||
| ...(projectId ? [{ header: 'Project ID', value: () => projectId }] : []), |
There was a problem hiding this comment.
🟡 nit (optional): With a login, every e2b sandbox list row is now padded to the longest Metadata JSON before the Project ID, so lines get much wider and wrap. renderTable leaves only the last column unpadded (table.ts). The new column follows Metadata, so Metadata is no longer last and every cell is space-filled to the widest metadata plus 3. Fix: put the Project ID column before Metadata so the unbounded column stays last (or print the project once above the table), while keeping it out when projectId is undefined.
Why this was flagged
Trigger: a logged-in user (access-token auth, so projectIdOf returns a prj_ ID) runs e2b sandbox list and one listed sandbox has long metadata, e.g. a few hundred characters once buildTableRows applies JSON.stringify (list.ts:131). renderTable (packages/cli/src/utils/table.ts) pads every cell except the one at i === line.length - 1 to widths[i] + COLUMN_PADDING. On the base branch Metadata was the last column, so rows with short metadata stayed short. After the change the 'Project ID' column added at list.ts:157 comes last. Every row's Metadata cell is then padded to the widest metadata, followed by the 30-character prj_ ID, so each line is as wide as the worst sandbox and wraps in a normal terminal. Template and snapshot lists end with short, bounded columns (Envd version, Names), so this only affects sandbox list. The E2B_API_KEY path keeps the old layout because the column is left out there.
Verification: In packages/cli/src/utils/table.ts, renderTable leaves only the last cell unpadded. In list.ts:155-157, the new Project ID column is added after { header: 'Metadata', ... }, so Metadata is no longer the last column. As a result, every row is now space-filled to the longest metadata string plus 3. On the base branch, rows with short metadata stayed short.
|
Closing until we have proper TypeID parsing. The |
… 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 `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
e2b sandbox list,e2b sandbox snapshot listande2b template listnow show which project they're listing: the one fromE2B_PROJECT_ID, else the one selected withe2b project use. The output alone didn't tell you which project you were looking at. Stacked on #2105 (stack #2115); supersedes #2106, which can't be reopened after the rebase.How
ListedSandbox,Template,SnapshotInfo) carry a team or project field; each list is scoped to the project the request went to viaX-Team-ID. So the ID comes from the request's own auth:projectIdOf(auth)readsX-Team-ID.prj_form, computed locally:publicProjectId()produces the TypeID encoding workspace-api uses forpublic_id(Crockford base32 of the UUID). Lists don't need a/v1/meround trip, and the IDs matche2b project list. It's checked against a real project'sid/public_idpair.Project IDcolumn, added last so the existing column order doesn't change;projectIDon sandboxes, snapshots and templates, so onejqpath works for all three lists.E2B_API_KEYthe key decides the project and the CLI can't tell which, so the column and field are left out rather than showing a guess.Testing
tests/commands/list_project_id.test.ts(4): each list in pretty and JSON with access-token auth, and with an API key (no column, no field). The SDK is mocked.tests/workspaces.test.ts:publicProjectIdon a real UUID/prj_pair, plus pass-through for non-UUIDs.pnpm run testinpackages/cli: 236 passed, 11 skipped (thetemplate createbackend suite needsE2B_API_KEY).template list,sandbox snapshot listandE2B_PROJECT_ID=<other project> e2b sandbox list -f jsonshow the IDs (tested on the feat(cli): show the project ID in sandbox, snapshot and template lists #2106 revision; the rebase only adaptedtemplate listto feat(cli): pick projects from workspace-api, route to each project's plane #2104's no-teamIDrequest). I couldn't run theE2B_API_KEYcase on prod (no key available locally); the unit test covers it.Part of PRD-119.
🤖 Generated with Claude Code