Skip to content

feat(workspace): mb workspace lifecycle — CRUD, credential sweep, destroy safety, --spawn#39

Open
escherize wants to merge 16 commits into
mainfrom
bcm/ghy-4075-spawn-default-profile
Open

feat(workspace): mb workspace lifecycle — CRUD, credential sweep, destroy safety, --spawn#39
escherize wants to merge 16 commits into
mainfrom
bcm/ghy-4075-spawn-default-profile

Conversation

@escherize

@escherize escherize commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Refs GHY-4050, GHY-4054, GHY-4051, GHY-4075 (Linear). Pairs with #34 (auth login --workspace) — merge #34 first: the sweep reads the oauth.scope field it introduces, so its two commits appear in this diff until it lands.

The CLI half of Option B's workspace lifecycle: a local agent drives a disposable cloud Metabase, and every safety rail the architecture doc calls for on the client side lands here. Four layers, one arc:

1. mb workspace CRUD (GHY-4050)

Thin wrappers over EE /api/ee/workspace-manager, gated { minVersion: 62, tokenFeature: "workspaces" } (endpoint is master-only; verified absent from release-x.61.x). create provisions warehouse isolation blocking — the response carries final per-database status. destroy confirms, supports --ignore-pending, surfaces orphaned_resources. Domain trio in src/domain/workspace.ts with provisioning status pinned as a closed enum.

2. Stale-credential sweep at create (GHY-4054)

Tier 2 of the containment ladder: workspace create refuses while the profile store holds a broader same-server credential (any api-key profile; any OAuth grant wider than mb:workspace-manager). Interactive runs offer revocation (durable local clear, then best-effort server-side revocation); non-interactive runs hard-refuse — --keep-existing-auth is the human-only override and is itself refused without a TTY, so an agent can never talk itself past the sweep.

3. Destroy safety (GHY-4051)

Destroy is the only irreversible moment, so the work-loss window closes there: with a ws-<id> profile present, the child is checked via remote-sync is-dirty and a dirty workspace auto-exports to its target branch before teardown (--discard skips). No local profile → warn and proceed (destroy is the billing-stop lever; ops cleanup works from any machine). Unreachable child → refuse without --discard. The profile is dropped after success.

4. create --spawn default profile (GHY-4075)

--spawn sends spawn_instance: true; the returned url/api_key go straight to the ws-<id> profile — never stdout, never --json — and a defaultProfile pointer makes bare mb target the new workspace. Explicit --profile/MB_PROFILE win; clearProfile unhooks the pointer so it never dangles; an exported MB_API_KEY (which would shadow the pointer) makes --spawn refuse up front.

Test plan

  • Live against EE head with the workspaces feature: full lifecycle e2e including real isolation DDL, sweep refusal branches with exact messages, destroy-safety paths (no-profile, clean check, unreachable + --discard), spawn env-shadow guard. 894 unit tests. All 10 CI e2e lanes green pre-consolidation.
  • Not live-testable yet (stated, not hidden): dirty→auto-export needs a configured child git remote (exercised in the local workspace testbed); --spawn end-to-end needs the parent's spawn_instance support.

escherize added 12 commits July 6, 2026 13:42
Thin wrappers over /api/ee/workspace-manager (EE, v62+, workspaces
premium feature). create provisions warehouse isolation blocking;
destroy confirms, supports --ignore-pending, and surfaces
orphaned_resources. Lifecycle e2e self-skips until the server carries
the workspaces token feature.

connect + profile-write land with the parent credentials endpoint.

Refs GHY-4050
${VAR:-} injected an empty METASTORE_DEV_SERVER_URL; Metabase's
dev-mode token check treats empty as configured and builds a hostless
URL, so token validation always failed. Bare list-syntax entries only
pass through when the shell defines them, letting the staging
token-check fallback work.
…oped grant

The scoped token is the containment primitive for Option B: a parent
credential on an agent's machine that can only do workspace CRUD,
enforced server-side by default-deny scope middleware.

- scope threads through discovery gating, dynamic client registration,
  the authorize URL, the stored credential, and refresh (which sends no
  scope parameter, so a refresh can never widen the grant)
- pre-scope profile records resolve to mb:full, which is what every
  legacy login was minted with
- workspace login is browser-OAuth only: --api-key, MB_API_KEY, and
  non-TTY contexts are refused so a key can't masquerade as scoped
- a 403 on a scope-narrowed profile is enriched to name the scope and
  the fix instead of a bare "Forbidden."

Refs GHY-4053
Tier 2 of the containment ladder: a stale full-power credential for the
same parent (any api-key profile, any OAuth grant wider than
mb:workspace-manager) would let a confused agent bypass the scoped
token, so create refuses while one exists. Interactive runs offer
revocation (local clear + best-effort server-side token revocation);
non-interactive runs hard-refuse — --keep-existing-auth is the
human-only override and is itself refused without a TTY. The profile
the command runs as is exempt: it is in deliberate use, not lying
around.

connect gets the same sweep when it lands (GHY-4052).

Refs GHY-4054
Newer oxlint flags helpers that capture nothing from their describe.

Refs GHY-4053
Destroy is the only irreversible moment, so it closes the work-loss
window there: when a ws-<name> profile exists locally, the child is
checked for unsynced work and a dirty workspace is auto-exported to its
target branch before teardown. --discard skips both. No local profile
warns and proceeds (destroy is the billing-stop lever; ops cleanup must
work from any machine); an unreachable child refuses without --discard
rather than silently reopening the window. The matching profile is
dropped after a successful destroy.

Refs GHY-4051
… profile

The spawn response's url/api_key were previously dropped; a shell script
did the profile write. Now --spawn sends spawn_instance, writes the
credential straight to the ws-<id> profile (never stdout, never --json),
and points a new defaultProfile pointer in profiles.json at it, so bare
mb targets the fresh workspace. Explicit --profile/MB_PROFILE still win;
clearing the profile (logout or destroy's drop) unhooks the pointer so
it never dangles. Because an exported MB_API_KEY outranks stored
profiles, --spawn refuses while it is set rather than installing a
default it would silently shadow.

Refs GHY-4075
@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@escherize, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 26 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 472b8733-b870-42e5-b05f-26baf6e417f2

📥 Commits

Reviewing files that changed from the base of the PR and between f31632d and eb120d1.

📒 Files selected for processing (40)
  • README.md
  • src/commands/auth/login.ts
  • src/commands/auth/logout.test.ts
  • src/commands/auth/logout.ts
  • src/commands/auth/status.test.ts
  • src/commands/auth/status.ts
  • src/commands/git-sync/export.ts
  • src/commands/parse-id.test.ts
  • src/commands/parse-id.ts
  • src/commands/runtime.test.ts
  • src/commands/runtime.ts
  • src/commands/workspace/create.ts
  • src/commands/workspace/credential-sweep.ts
  • src/commands/workspace/destroy.ts
  • src/commands/workspace/get.ts
  • src/commands/workspace/index.ts
  • src/commands/workspace/list.ts
  • src/commands/workspace/profile-name.ts
  • src/core/auth/credential.test.ts
  • src/core/auth/credential.ts
  • src/core/auth/oauth-login.test.ts
  • src/core/auth/oauth-login.ts
  • src/core/auth/oauth-session.test.ts
  • src/core/auth/oauth-session.ts
  • src/core/auth/profile-record.ts
  • src/core/auth/stale-credentials.test.ts
  • src/core/auth/stale-credentials.ts
  • src/core/auth/storage.test.ts
  • src/core/auth/storage.ts
  • src/core/config.test.ts
  • src/core/config.ts
  • src/core/http/client.test.ts
  • src/core/http/oauth.test.ts
  • src/core/http/oauth.ts
  • src/domain/workspace.ts
  • src/main.ts
  • src/runtime/command-help.test.ts
  • tests/e2e/auth.e2e.test.ts
  • tests/e2e/docker-compose.yml
  • tests/e2e/workspace.e2e.test.ts
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch bcm/ghy-4075-spawn-default-profile

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@escherize escherize changed the title feat(workspace): create --spawn saves child credential as the default profile feat(workspace): mb workspace lifecycle — CRUD, credential sweep, destroy safety, --spawn Jul 8, 2026
@escherize escherize changed the base branch from bcm/ghy-4051-destroy-safety to main July 8, 2026 16:46
@escherize escherize marked this pull request as ready for review July 8, 2026 17:27
@escherize escherize requested a review from alxnddr July 8, 2026 21:09
escherize added 4 commits July 8, 2026 15:10
"point --profile at a workspace profile" read as another
workspace-scoped parent profile, which would also 403; the intended
target is the child's ws-<id> profile.

Refs GHY-4053
@escherize

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown

Published @metabase/cli@0.2.1-alpha.bcm-ghy-4075-spawn-default-profile.eb120d1 with dist-tag alpha-bcm-ghy-4075-spawn-default-profile.

Install: npm install -g @metabase/cli@alpha-bcm-ghy-4075-spawn-default-profile

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