Skip to content

feat(cli): Add suport for multi-workspace authentication#1151

Open
mdean808 wants to merge 11 commits intomainfrom
feat/cli-workspaces
Open

feat(cli): Add suport for multi-workspace authentication#1151
mdean808 wants to merge 11 commits intomainfrom
feat/cli-workspaces

Conversation

@mdean808
Copy link
Contributor

@mdean808 mdean808 commented Mar 9, 2026

Summary

Add gcloud-style multi-workspace switching. Users can cua auth login multiple times to cache credentials per workspace, then switch between them with cua workspace set <slug> without re-authenticating.

What changed

  • Credential store (auth/store.py) — Namespaced KV keys per workspace (workspace:<slug>:api_key/name/org), with legacy api_key fallback for backward compat. No schema migration needed.
  • Browser auth (auth/browser.py) — New AuthResult dataclass. Parses workspace_slug, workspace_name, org_name from callback. Accepts optional workspace_slug hint for pre-selection.
  • Auth commands (commands/auth.py) — login saves workspace metadata for both browser and --api-key flows. logout supports --workspace <slug> and --all flags. New list subcommand shows cached workspaces grouped by org. status shows active workspace label.
  • Workspace command (commands/workspace.py) — New cua workspace set [slug] (alias cua ws set). Validates cached keys via /v1/me, falls back to browser auth, supports interactive picker.
  • Main entry (main.py) — Registers workspace command.

Backward compat

Existing users with a single api_key in the DB are unaffected — get_api_key() falls back to it. Next cua auth login migrates to the new format naturally. No manual migration needed.

Summary by CodeRabbit

  • New Features
    • Added multi-workspace management with new workspace (alias ws) command for switching between saved workspaces via interactive selection.
    • Added auth list command to display cached workspaces grouped by organization, with active status indicators.
    • Enhanced logout with per-workspace (--workspace) and bulk (--all) removal options.
    • Browser authentication now automatically captures and stores workspace metadata alongside credentials.

@vercel
Copy link
Contributor

vercel bot commented Mar 9, 2026

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

Project Deployment Actions Updated (UTC)
docs Ready Ready Preview, Comment Mar 13, 2026 8:57pm

Request Review

@mdean808 mdean808 requested a review from ddupont808 March 9, 2026 16:23
@github-actions
Copy link
Contributor

github-actions bot commented Mar 9, 2026

📦 Publishable packages changed

  • pypi/cli

Add release:<service> labels to auto-release on merge (+ optional bump:minor or bump:major, default is patch).
Or add no-release to skip.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 9, 2026

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b4852e82-2a13-48ff-aaa1-ad35f825c1a6

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This pull request introduces multi-workspace support to the Cua CLI. It adds workspace-scoped credential management, enhances OAuth authentication to return workspace metadata, extends the auth command with workspace listing/management capabilities, and introduces a new workspace command for switching between cached workspaces with browser-based fallback authentication.

Changes

Cohort / File(s) Summary
Browser Auth Enhancement
libs/python/cua-cli/cua_cli/auth/browser.py
Introduces AuthResult dataclass to return auth token and workspace metadata (slug, name, org). Extends authenticate_via_browser() to accept optional workspace_slug parameter, parse workspace metadata from OAuth callbacks, and return structured AuthResult instead of plain string.
Credential Store Extensions
libs/python/cua-cli/cua_cli/auth/store.py
Implements workspace-scoped credential management with new functions for saving, querying, listing, and deleting workspaces. Adds prefix-based utilities (delete_by_prefix, list_by_prefix) to CredentialStore. Updates get_api_key() to prioritize environment variable, active workspace key, then legacy key. Introduces ACTIVE_WORKSPACE_KEY constant and per-workspace metadata storage.
Auth Command Expansion
libs/python/cua-cli/cua_cli/commands/auth.py
Adds new "list" subcommand to display cached workspaces grouped by organization. Introduces _fetch_me() helper to query /v1/me endpoint for workspace metadata. Reworks login to save and activate workspace context. Enhances logout with per-workspace (\-\-workspace) and bulk (\-\-all) options. Updates status command to use active workspace context and handle expired credentials.
Workspace Management Command
libs/python/cua-cli/cua_cli/commands/workspace.py
New module implementing top-level "workspace"/"ws" command with "set" subcommand. Provides cached key validation via API call, browser-based authentication fallback with workspace metadata persistence, and interactive workspace selection. Includes _validate_workspace_key() to verify tokens against /v1/me endpoint.
CLI Integration
libs/python/cua-cli/cua_cli/main.py
Imports and registers the new workspace command module, wiring register_parser() and execute() dispatch for "workspace" and "ws" commands into the main CLI router.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant CLI as Workspace CLI
    participant Store as CredentialStore
    participant Browser as Browser Auth
    participant API as CUA API
    
    User->>CLI: workspace set my-workspace
    CLI->>Store: get_workspace_api_key('my-workspace')
    alt Cached key exists
        Store-->>CLI: api_key
        CLI->>API: GET /v1/me (with api_key)
        API-->>CLI: 200 + workspace metadata
        CLI->>Store: set_active_workspace('my-workspace')
        CLI-->>User: ✓ Workspace activated
    else No cached key
        CLI->>Browser: authenticate_via_browser(workspace_slug='my-workspace')
        Browser->>API: OAuth flow
        API-->>Browser: AuthResult(token, slug, name, org)
        Browser-->>CLI: AuthResult
        CLI->>Store: save_workspace(slug, token, name, org)
        CLI->>Store: set_active_workspace(slug)
        CLI-->>User: ✓ Workspace activated
    end
Loading
sequenceDiagram
    participant User
    participant CLI as Auth CLI
    participant API as CUA API
    participant Store as CredentialStore
    
    User->>CLI: cua auth login
    CLI->>CLI: authenticate_via_browser()
    CLI->>API: OAuth callback with auth_token + workspace metadata
    API-->>CLI: AuthResult
    CLI->>API: GET /v1/me (with auth_token)
    API-->>CLI: workspace_slug, name, org
    CLI->>Store: save_workspace(slug, token, name, org)
    CLI->>Store: set_active_workspace(slug)
    CLI-->>User: ✓ Logged in to workspace
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐰 Hops of joy through workspaces wide,
Credentials cached, no need to hide!
Browser bounces, APIs sing,
Multi-workspace support takes wing! 🎉
Switching swift, the CLI grows,
Where every workspace flows! 🌟

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title refers to multi-workspace authentication, which is the primary feature added across all modified files (auth/store.py, auth/browser.py, commands/auth.py, workspace.py, and main.py), matching the main objective of the changeset.
Docstring Coverage ✅ Passed Docstring coverage is 91.43% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/cli-workspaces
📝 Coding Plan
  • Generate coding plan for human review comments

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 and usage tips.

Tip

CodeRabbit can generate a title for your PR based on the changes with custom instructions.

Set the reviews.auto_title_instructions setting to generate a title for your PR based on the changes in the PR with custom instructions.

description="Cua CLI - Unified command-line interface for Computer-Use Agents",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I removed these examples because they were annoying me when trying to view the commands in the cli.

Can add them back if we think they're hepful.

@github-actions
Copy link
Contributor

github-actions bot commented Mar 9, 2026

📦 Publishable packages changed

  • pypi/cli

Add release:<service> labels to auto-release on merge (+ optional bump:minor or bump:major, default is patch).
Or add no-release to skip.

@github-actions
Copy link
Contributor

github-actions bot commented Mar 9, 2026

📦 Publishable packages changed

  • pypi/cli

Add release:<service> labels to auto-release on merge (+ optional bump:minor or bump:major, default is patch).
Or add no-release to skip.

@github-actions
Copy link
Contributor

github-actions bot commented Mar 9, 2026

📦 Publishable packages changed

  • pypi/cli

Add release:<service> labels to auto-release on merge (+ optional bump:minor or bump:major, default is patch).
Or add no-release to skip.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🧹 Nitpick comments (1)
libs/python/cua-cli/cua_cli/auth/store.py (1)

205-213: Return workspaces in a deterministic order.

This list now drives both the interactive picker and the remaining[0] fallback in libs/python/cua-cli/cua_cli/commands/auth.py. Without sorting here, the next active workspace depends on SQLite row order and can change across runs.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/python/cua-cli/cua_cli/auth/store.py` around lines 205 - 213, The
returned workspace list is nondeterministic; after building result from
rows/slugs/active, sort it deterministically before returning (e.g.,
result.sort(key=lambda ws: ws["slug"]) so order is stable across runs and the
interactive picker / remaining[0] fallback behave predictably). Ensure you
perform the sort on the result list (referencing variables result, rows, slugs,
active) and then return the sorted list.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@libs/python/cua-cli/cua_cli/auth/store.py`:
- Around line 269-273: The current clear_credentials() implementation deletes
all workspaces via clear_all_workspaces() and the legacy API key (API_KEY_NAME);
split this behavior by adding a new function (e.g., clear_legacy_credentials()
or clear_legacy_session()) that only deletes the legacy key
(store.delete(API_KEY_NAME)) and update callers in commands/auth.py (the
no-active-workspace branches of cmd_logout() and cmd_status()) to call the new
narrow function, while keeping clear_credentials() as the existing "clear
everything" helper used for the --all path.

In `@libs/python/cua-cli/cua_cli/commands/auth.py`:
- Around line 10-20: Re-run isort to normalize the import ordering in this
module: reorder the top-level imports so standard/library imports come first,
then third-party, then local/package imports, and ensure the multi-import from
cua_cli.auth.store is alphabetized (clear_credentials, delete_workspace,
get_active_workspace, get_api_key, list_workspaces, save_api_key,
save_workspace, set_active_workspace) and that authenticate_via_browser is
positioned according to isort rules; then save the file so the CI Python lint
job passes.
- Around line 295-315: The 401 handler is mutating cached workspaces even when
the failing token may have come from the environment (CUA_API_KEY); update the
logic around the _fetch_me call and subsequent 401 branch to first determine the
credential source (e.g., via get_api_key() or by checking
os.environ["CUA_API_KEY"]/a returned source flag), then only delete or switch
the cached active workspace (using get_active_workspace, delete_workspace,
list_workspaces, set_active_workspace, or
_get_store().delete(ACTIVE_WORKSPACE_KEY)) when the failing credential is the
stored workspace token—not when the token came from the environment; apply the
same source-aware check to the similar cleanup at lines ~328-330.
- Around line 171-178: If the workspace slug is missing, avoid creating a
synthetic "workspace:default:*" entry; instead treat this as a legacy-login
fallback: change the branch that currently calls save_workspace(slug, api_key,
name, org_name) and set_active_workspace(slug) to detect missing ws.get("slug")
and call the legacy-save path (the same logic used in the 202-205 legacy block)
and clear any active workspace marker (call the function that clears active
workspace) rather than creating a synthetic workspace; apply the same adjustment
to the other similar block around lines 190-205 so both paths consistently
fallback to legacy behavior and clear the active workspace when slug is absent.

In `@libs/python/cua-cli/cua_cli/commands/workspace.py`:
- Around line 65-88: The validator currently collapses transport failures and
expired tokens into a single False result; change _validate_workspace_key to
distinguish outcomes: return (True, data) when status == 200, return (False,
data) when status == 401 (explicitly expired/invalid), and return (None, {}) for
transport errors (catch aiohttp.ClientError, asyncio.TimeoutError and other
exceptions) or non-401/non-200 statuses so callers know the API was unreachable
or returned an unexpected status; update cmd_set (the caller that currently
treats any False as "expired") to treat None as "transient/unreachable" and
avoid deleting the cached workspace token in that case.
- Around line 135-159: The interactive branch currently picks a workspace from
list_workspaces and calls set_active_workspace without verifying the cached
session; replicate the explicit-slug validation flow by validating the selected
workspace's session (e.g., attempt the same /v1/me check or call the same
validation helper used for the slug path) before calling set_active_workspace;
if validation fails, remove the stale cached keys (same cleanup used in the
explicit path), print an error and return non-zero, otherwise proceed to
set_active_workspace and print_success for the selected workspace.
- Around line 118-129: The handler currently accepts any non-empty
result.workspace_slug and switches to it; change it to validate the returned
workspace against the originally requested slug (compare result.workspace_slug
to the input/requested slug variable used to start the browser flow) and abort
if they differ: do not call save_workspace or set_active_workspace, instead
print an error (or print_failure) explaining the mismatch and return/non-zero
exit; keep the existing print_success path only for exact matches. Use the
existing symbols result.workspace_slug, save_workspace, set_active_workspace,
and print_success when locating the code to modify.

---

Nitpick comments:
In `@libs/python/cua-cli/cua_cli/auth/store.py`:
- Around line 205-213: The returned workspace list is nondeterministic; after
building result from rows/slugs/active, sort it deterministically before
returning (e.g., result.sort(key=lambda ws: ws["slug"]) so order is stable
across runs and the interactive picker / remaining[0] fallback behave
predictably). Ensure you perform the sort on the result list (referencing
variables result, rows, slugs, active) and then return the sorted list.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 412cf2f7-a8d7-495b-8eaf-6437278a4056

📥 Commits

Reviewing files that changed from the base of the PR and between 65f9701 and 3f54440.

📒 Files selected for processing (5)
  • libs/python/cua-cli/cua_cli/auth/browser.py
  • libs/python/cua-cli/cua_cli/auth/store.py
  • libs/python/cua-cli/cua_cli/commands/auth.py
  • libs/python/cua-cli/cua_cli/commands/workspace.py
  • libs/python/cua-cli/cua_cli/main.py

Comment on lines 269 to +273
def clear_credentials() -> None:
"""Clear all stored credentials."""
_get_store().clear()
"""Clear all stored credentials including workspaces and legacy key."""
store = _get_store()
clear_all_workspaces()
store.delete(API_KEY_NAME)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Split all-workspace clearing from legacy-session cleanup.

libs/python/cua-cli/cua_cli/commands/auth.py still uses this helper in the "no active workspace" branches of cmd_logout() and cmd_status(). With the new implementation, those paths now wipe every cached workspace instead of just clearing the current legacy token. Keep this helper for --all and add a narrower legacy-only clear for those callers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/python/cua-cli/cua_cli/auth/store.py` around lines 269 - 273, The
current clear_credentials() implementation deletes all workspaces via
clear_all_workspaces() and the legacy API key (API_KEY_NAME); split this
behavior by adding a new function (e.g., clear_legacy_credentials() or
clear_legacy_session()) that only deletes the legacy key
(store.delete(API_KEY_NAME)) and update callers in commands/auth.py (the
no-active-workspace branches of cmd_logout() and cmd_status()) to call the new
narrow function, while keeping clear_credentials() as the existing "clear
everything" helper used for the --all path.

Comment on lines 10 to +20
from cua_cli.auth.browser import authenticate_via_browser
from cua_cli.auth.store import clear_credentials, get_api_key, save_api_key
from cua_cli.auth.store import (
clear_credentials,
delete_workspace,
get_active_workspace,
get_api_key,
list_workspaces,
save_api_key,
save_workspace,
set_active_workspace,
)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Re-run isort on this import block.

CI is already failing the Python lint job on this file, so this needs to be normalized before the PR can merge.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/python/cua-cli/cua_cli/commands/auth.py` around lines 10 - 20, Re-run
isort to normalize the import ordering in this module: reorder the top-level
imports so standard/library imports come first, then third-party, then
local/package imports, and ensure the multi-import from cua_cli.auth.store is
alphabetized (clear_credentials, delete_workspace, get_active_workspace,
get_api_key, list_workspaces, save_api_key, save_workspace,
set_active_workspace) and that authenticate_via_browser is positioned according
to isort rules; then save the file so the CI Python lint job passes.

Comment on lines +171 to +178
ws = data.get("workspace", {})
org = data.get("organization", {})
slug = ws.get("slug", "default")
name = ws.get("name", slug)
org_name = org.get("name", "")
save_workspace(slug, api_key, name, org_name)
set_active_workspace(slug)
print_success(f"Authenticated and switched to workspace: {name} ({slug})")
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Treat missing workspace metadata as a real legacy-login fallback.

Lines 171-178 currently invent workspace:default:*, while Lines 202-205 save a legacy key without clearing any existing active workspace. Both paths can make a "successful" login keep using the wrong credentials. If the slug is missing, stay on the legacy path and clear the active-workspace marker instead of creating a synthetic workspace entry.

Also applies to: 190-205

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/python/cua-cli/cua_cli/commands/auth.py` around lines 171 - 178, If the
workspace slug is missing, avoid creating a synthetic "workspace:default:*"
entry; instead treat this as a legacy-login fallback: change the branch that
currently calls save_workspace(slug, api_key, name, org_name) and
set_active_workspace(slug) to detect missing ws.get("slug") and call the
legacy-save path (the same logic used in the 202-205 legacy block) and clear any
active workspace marker (call the function that clears active workspace) rather
than creating a synthetic workspace; apply the same adjustment to the other
similar block around lines 190-205 so both paths consistently fallback to legacy
behavior and clear the active workspace when slug is absent.

Comment on lines +295 to +315
status_code, data = _fetch_me(api_key)
except Exception as e:
print_error(f"Failed to reach API: {e}")
return 1

if status_code == 401:
clear_credentials()
active = get_active_workspace()
if active:
delete_workspace(active)
print_info(f"Removed expired credentials for workspace: {active}")
remaining = list_workspaces()
if remaining:
set_active_workspace(remaining[0]["slug"])
print_info(f"Switched to workspace: {remaining[0]['name']} ({remaining[0]['slug']})")
else:
from cua_cli.auth.store import _get_store, ACTIVE_WORKSPACE_KEY

_get_store().delete(ACTIVE_WORKSPACE_KEY)
else:
clear_credentials()
print_info("Removed expired credentials.")
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don't mutate cached workspaces when the failing token came from CUA_API_KEY.

get_api_key() prefers the environment, but this 401 handler and the new [active: ...] label both assume the active-workspace credential was used. With CUA_API_KEY set, a failed status call can delete unrelated cached workspaces and still report the wrong active workspace. Track the credential source before cleaning up or labeling the session.

Also applies to: 328-330

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/python/cua-cli/cua_cli/commands/auth.py` around lines 295 - 315, The 401
handler is mutating cached workspaces even when the failing token may have come
from the environment (CUA_API_KEY); update the logic around the _fetch_me call
and subsequent 401 branch to first determine the credential source (e.g., via
get_api_key() or by checking os.environ["CUA_API_KEY"]/a returned source flag),
then only delete or switch the cached active workspace (using
get_active_workspace, delete_workspace, list_workspaces, set_active_workspace,
or _get_store().delete(ACTIVE_WORKSPACE_KEY)) when the failing credential is the
stored workspace token—not when the token came from the environment; apply the
same source-aware check to the similar cleanup at lines ~328-330.

Comment on lines +65 to +88
def _validate_workspace_key(api_key: str) -> tuple[bool, dict]:
"""Validate an API key by calling /v1/me. Returns (valid, data)."""

async def _do():
url = f"{_get_api_base()}/v1/me"
headers = {
"Authorization": f"Bearer {api_key}",
"Accept": "application/json",
**cua_version_headers(),
}
async with aiohttp.ClientSession() as session:
timeout = aiohttp.ClientTimeout(total=10)
async with session.get(url, headers=headers, timeout=timeout) as resp:
try:
data = await resp.json(content_type=None)
except Exception:
return resp.status, {}
return resp.status, data

try:
status_code, data = run_async(_do())
return status_code == 200, data
except Exception:
return False, {}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Differentiate expired keys from reachability failures.

_validate_workspace_key() returns False for timeouts, DNS errors, and other transport problems, and cmd_set() treats every False as "expired" and deletes the cached workspace. A transient API outage should not discard a valid token.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/python/cua-cli/cua_cli/commands/workspace.py` around lines 65 - 88, The
validator currently collapses transport failures and expired tokens into a
single False result; change _validate_workspace_key to distinguish outcomes:
return (True, data) when status == 200, return (False, data) when status == 401
(explicitly expired/invalid), and return (None, {}) for transport errors (catch
aiohttp.ClientError, asyncio.TimeoutError and other exceptions) or
non-401/non-200 statuses so callers know the API was unreachable or returned an
unexpected status; update cmd_set (the caller that currently treats any False as
"expired") to treat None as "transient/unreachable" and avoid deleting the
cached workspace token in that case.

Comment on lines +118 to +129
if result.workspace_slug:
save_workspace(
result.workspace_slug,
result.token,
result.workspace_name,
result.org_name,
)
set_active_workspace(result.workspace_slug)
print_success(
f"Authenticated and switched to workspace: "
f"{result.workspace_name} ({result.workspace_slug})"
)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Reject browser auth for the wrong workspace.

cua workspace set <slug> currently accepts any non-empty result.workspace_slug and switches to it. If the browser flow comes back with a different workspace than the one requested, the CLI silently lands in the wrong tenant.

Suggested guard
-        if result.workspace_slug:
+        if result.workspace_slug == slug:
             save_workspace(
                 result.workspace_slug,
                 result.token,
                 result.workspace_name,
                 result.org_name,
             )
             set_active_workspace(result.workspace_slug)
             print_success(
                 f"Authenticated and switched to workspace: "
                 f"{result.workspace_name} ({result.workspace_slug})"
             )
+        elif result.workspace_slug:
+            print_error(
+                f"Authentication returned workspace '{result.workspace_slug}', "
+                f"expected '{slug}'."
+            )
+            return 1
         else:
             print_error("Authentication did not return workspace metadata.")
             return 1
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if result.workspace_slug:
save_workspace(
result.workspace_slug,
result.token,
result.workspace_name,
result.org_name,
)
set_active_workspace(result.workspace_slug)
print_success(
f"Authenticated and switched to workspace: "
f"{result.workspace_name} ({result.workspace_slug})"
)
if result.workspace_slug == slug:
save_workspace(
result.workspace_slug,
result.token,
result.workspace_name,
result.org_name,
)
set_active_workspace(result.workspace_slug)
print_success(
f"Authenticated and switched to workspace: "
f"{result.workspace_name} ({result.workspace_slug})"
)
elif result.workspace_slug:
print_error(
f"Authentication returned workspace '{result.workspace_slug}', "
f"expected '{slug}'."
)
return 1
else:
print_error("Authentication did not return workspace metadata.")
return 1
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/python/cua-cli/cua_cli/commands/workspace.py` around lines 118 - 129,
The handler currently accepts any non-empty result.workspace_slug and switches
to it; change it to validate the returned workspace against the originally
requested slug (compare result.workspace_slug to the input/requested slug
variable used to start the browser flow) and abort if they differ: do not call
save_workspace or set_active_workspace, instead print an error (or
print_failure) explaining the mismatch and return/non-zero exit; keep the
existing print_success path only for exact matches. Use the existing symbols
result.workspace_slug, save_workspace, set_active_workspace, and print_success
when locating the code to modify.

Comment on lines +135 to +159
# No slug — interactive selection from cached workspaces
workspaces = list_workspaces()
if not workspaces:
print_info("No workspaces cached. Run 'cua auth login' to add one.")
return 0

print_info("Select a workspace:")
for i, ws in enumerate(workspaces, 1):
marker = " (active)" if ws["is_active"] else ""
name_part = f" - {ws['name']}" if ws["name"] else ""
print(f" {i}. {ws['slug']}{name_part}{marker}")

try:
choice = input("\nEnter number: ").strip()
idx = int(choice) - 1
if idx < 0 or idx >= len(workspaces):
print_error("Invalid selection.")
return 1
except (ValueError, EOFError, KeyboardInterrupt):
print_error("Invalid selection.")
return 1

selected = workspaces[idx]
set_active_workspace(selected["slug"])
print_success(f"Switched to workspace: {selected['name']} ({selected['slug']})")
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Validate the picked workspace before marking it active.

The explicit-slug path checks /v1/me and cleans up stale keys, but the interactive branch just flips active_workspace. That lets users "successfully" switch into an expired cached entry and only fail on the next command.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/python/cua-cli/cua_cli/commands/workspace.py` around lines 135 - 159,
The interactive branch currently picks a workspace from list_workspaces and
calls set_active_workspace without verifying the cached session; replicate the
explicit-slug validation flow by validating the selected workspace's session
(e.g., attempt the same /v1/me check or call the same validation helper used for
the slug path) before calling set_active_workspace; if validation fails, remove
the stale cached keys (same cleanup used in the explicit path), print an error
and return non-zero, otherwise proceed to set_active_workspace and print_success
for the selected workspace.

@sentry
Copy link

sentry bot commented Mar 9, 2026

Codecov Report

❌ Patch coverage is 0% with 2 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
libs/python/agent/agent/loops/gemini.py 0.00% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

@github-actions
Copy link
Contributor

github-actions bot commented Mar 9, 2026

📦 Publishable packages changed

  • pypi/cli

Add release:<service> labels to auto-release on merge (+ optional bump:minor or bump:major, default is patch).
Or add no-release to skip.

@github-actions
Copy link
Contributor

github-actions bot commented Mar 9, 2026

📦 Publishable packages changed

  • pypi/agent
  • pypi/cli

Add release:<service> labels to auto-release on merge (+ optional bump:minor or bump:major, default is patch).
Or add no-release to skip.

@github-actions
Copy link
Contributor

📦 Publishable packages changed

  • pypi/agent
  • pypi/cli

Add release:<service> labels to auto-release on merge (+ optional bump:minor or bump:major, default is patch).
Or add no-release to skip.

@github-actions
Copy link
Contributor

📦 Publishable packages changed

  • pypi/agent
  • pypi/cli

Add release:<service> labels to auto-release on merge (+ optional bump:minor or bump:major, default is patch).
Or add no-release to skip.

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.

2 participants