Skip to content

feat(cli): make Cloud workspace IDs discoverable from the CLI - #1413

Merged
willwashburn merged 2 commits into
mainfrom
claude/github-issue-1372-pr-65gkc6
Aug 2, 2026
Merged

feat(cli): make Cloud workspace IDs discoverable from the CLI#1413
willwashburn merged 2 commits into
mainfrom
claude/github-issue-1372-pr-65gkc6

Conversation

@willwashburn

@willwashburn willwashburn commented Jul 31, 2026

Copy link
Copy Markdown
Member

Summary

Closes #1372.

agent-relay cloud enroll --workspace accepted only a Cloud workspace UUID or a unified rw_ ID, and nothing in the CLI printed either one — cloud whoami and workspace list show names only, and ~/.agentworkforce/relay/workspaces.json holds names plus keys. Enrolling from a stored login dead-ended unless you fetched the UUID out of the web dashboard.

This ships all three fixes the issue asked for:

  • agent-relay cloud workspaces (new) lists GET /api/v1/workspaces as id slug name, or --json for scripts. This endpoint already existed and returns exactly the workspaces the stored login can use; the CLI just never called it.
  • cloud enroll --workspace takes a name or slug. Matching happens locally against that same listing, case-insensitively, with ID beating slug beating name so an exact identifier is never shadowed by someone else's display name. An ambiguous name is refused with the candidate IDs rather than picking one.
  • cloud whoami prints IDs, rendering Organization: Acme (org-1) / Workspace: Chief HQ (<uuid>) instead of names alone.

UUIDs and rw_ IDs still take the old fast path — no extra round trip, and the existing resolve/mint call sequence is unchanged for them.

On not echoing the selector

The prior code rejected anything that wasn't a UUID or rw_ ID, which also covered a pasted rk_live_… workspace key. Now that arbitrary strings are accepted, that protection is preserved differently:

  • Resolution matches the selector locally against the already-fetched listing. It is never placed in a URL or a request body, so a mistyped secret is not transmitted.
  • Neither the "no workspace matched" error nor the "looks like a credential" error echoes the value back.
  • A selector that matched nothing and carries a live-credential prefix (per redactCredentialValues, the single source of truth for what a credential looks like) gets the credential-specific message. That check only chooses the message — it deliberately does not gate the lookup, because those prefixes are broad enough (br_, at_live_, …) that gating would make a workspace legitimately named br_team permanently unselectable. Thanks to @chatgpt-codex-connector for catching that; the first revision had it backwards.

Cloud-provided names, slugs, and IDs are sanitized before display, and sanitizeForTerminal now also strips bidirectional overrides (U+202A–202E, U+2066–2069) so a workspace name can't reorder the line it's printed on to impersonate another one. That helper had no callers outside its own test before this change.

Note on rw_ IDs in the listing

The issue suggested cloud workspaces show rw_ IDs. The list endpoint returns Cloud workspace UUIDs, and turning each into an rw_ ID means a /resolve call per workspace — which calls resolveOrProvisionRelayWorkspace and can provision a Relay workspace as a side effect (cf. #1378). Listing shouldn't mint anything, so the command prints the Cloud UUID, which is what --workspace accepts.

Test Plan

  • Tests added/updated
  • Manual testing completed

Added to packages/cli/src/cli/commands/cloud.test.ts:

  • cloud whoami renders org/workspace IDs, and still prints (none) for a login with no workspace selected.
  • cloud workspaces lists IDs, strips terminal control sequences from a Cloud-provided ID and name, emits valid --json, reports an empty listing, and tells a logged-out user to run cloud login.
  • cloud enroll --workspace resolves a name case-insensitively; prefers an ID/slug match over another workspace's name; resolves a listed name that looks credential-like (br_team); refuses an ambiguous name with the candidate IDs; and reports an unmatched selector without echoing it.
  • The credential-rejection test covers rk_live_… and ocl_node_enr_…, asserting the value appears in neither stdout, stderr, nor any authorizedApiFetch argument.

Plus a bidi-stripping case in formatting.test.ts and the new command in the bootstrap.test.ts command inventory. The test mock takes the real redactCredentialValues via importOriginal rather than re-deriving its regex, so the prefix set can't drift out from under these tests.

Gates run locally on b07e8d2:

  • npx vitest run — 1680 passed, 1 failed. The single failure is packages/harnesses/src/ai-sdk/local-host-sandbox.test.ts, which fails identically on main in this container (an nvm line leaks into the captured shell stdout); unrelated to this diff. CI is green on it.
  • npm run typecheck — clean.
  • npm run lint — 76 warnings, 0 errors, byte-identical to main (no new warnings).
  • npx prettier --check — clean on every touched file.

Not manually exercised against live Cloud: this container has no Cloud login, so the HTTP paths are covered by mocked-fetch tests only.

🤖 Generated with Claude Code

https://claude.ai/code/session_012WVG4uJ3erC9sq6rUW7551

`cloud enroll --workspace` accepted only a Cloud workspace UUID or unified
`rw_` ID, and no command printed either one: `cloud whoami` and `workspace
list` showed names only, and `~/.agentworkforce/relay/workspaces.json` holds
names plus keys. Enrolling from a stored login dead-ended unless you fetched
the UUID out of the web dashboard.

- Add `agent-relay cloud workspaces`, listing `GET /api/v1/workspaces` as
  `id  slug  name` (or `--json`).
- Accept a workspace name or slug in `cloud enroll --workspace`, matched
  case-insensitively against that same listing. ID beats slug beats name so
  an exact identifier is never shadowed by someone else's display name; an
  ambiguous name is refused with the candidate IDs.
- Print organization and workspace IDs in `cloud whoami`.

A value carrying a live-credential prefix is still rejected before any
network call, and neither the not-found nor the credential error echoes the
value back — a mistyped secret must not reach the terminal. Cloud-provided
names are sanitized before display, and `sanitizeForTerminal` now also strips
bidirectional overrides so a workspace name cannot reorder its own line.

Closes #1372

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012WVG4uJ3erC9sq6rUW7551
@willwashburn
willwashburn requested a review from khaliqgant as a code owner July 31, 2026 04:24
@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The CLI adds Cloud workspace listing, selector resolution by name, slug, UUID, or unified ID, expanded whoami output, terminal sanitization, JSON support, telemetry, tests, and documentation.

Changes

Cloud workspace flow

Layer / File(s) Summary
Workspace selector resolution and enrollment
packages/cli/src/cli/commands/cloud.ts, packages/cli/src/cli/commands/cloud.test.ts
Enrollment accepts workspace names, slugs, UUIDs, and unified IDs. Credential-like selectors are rejected without being echoed. Tests cover matching, precedence, ambiguity, and failures.
Workspace commands, identity output, and terminal safety
packages/cli/src/cli/commands/cloud.ts, packages/cli/src/cli/lib/formatting.*, packages/cli/src/cli/telemetry/events.ts, packages/cli/src/cli/commands/cloud.test.ts, packages/cli/src/cli/bootstrap.test.ts
Adds cloud workspaces with table and JSON output. whoami displays sanitized organization and workspace names with IDs. Telemetry and command registration include the new flow.
Usage and release documentation
packages/cli/README.md, CHANGELOG.md
Documents workspace discovery, enrollment selectors, stored login usage, and expanded identity output.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CloudCLI
  participant WorkspaceListAPI
  participant EnrollmentTokenAPI
  CloudCLI->>WorkspaceListAPI: Retrieve authenticated workspaces
  WorkspaceListAPI-->>CloudCLI: Return workspace IDs, slugs, and names
  CloudCLI->>CloudCLI: Resolve the workspace selector
  CloudCLI->>EnrollmentTokenAPI: Mint an enrollment token
  EnrollmentTokenAPI-->>CloudCLI: Return the token or an error
Loading

Possibly related PRs

Suggested reviewers: khaliqgant, claude

Poem

A rabbit finds workspaces bright,
Names and slugs now guide the flight.
IDs appear beside each name,
Tokens stay hidden, safe from flame.
cloud workspaces leads the way.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the primary change: making Cloud workspace IDs discoverable through the CLI.
Description check ✅ Passed The description includes the required Summary and Test Plan sections, documents the changes and tests, and appropriately leaves Screenshots empty.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/github-issue-1372-pr-65gkc6

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.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f4f5aa9f75

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/cli/src/cli/commands/cloud.ts Outdated
throw new Error('A workspace name or ID is required for session-based enrollment.');
}
if (!CLOUD_WORKSPACE_UUID_PATTERN.test(workspaceId) && !UNIFIED_WORKSPACE_ID_PATTERN.test(workspaceId)) {
if (looksLikeCredential(workspaceId)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Allow workspace names containing credential-like prefixes

looksLikeCredential treats any selector changed by the deliberately broad redactor as a credential, so a legitimate listed name such as br_team or Platform at_live_dev is rejected before the workspace listing is queried and can never be used with the newly supported name-based enrollment. Restrict this guard to the complete syntax of actual credentials, or attempt the local workspace match before rejecting an unmatched credential-like selector.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Valid, and fixed in b07e8d2 by taking your second option — match against the listing first, and let the credential check pick only the error message for a selector that matched nothing.

Tightening the guard to "complete credential syntax" was the tempting fix, but it re-couples this file to a token grammar that lives in packages/cloud/src/redact.ts, and any prefix that grammar adds later would silently start eating workspace names again. Matching first makes the guard's breadth harmless by construction: a name that is really in your listing resolves no matter what it starts with.

The security property is unchanged. Matching is local against the already-fetched listing, so the selector is never put in a URL or a request body, and neither the credential message nor the not-found message echoes it. Test coverage now asserts both halves: a workspace named br_team enrolls, and a pasted rk_live_… / ocl_node_enr_… is refused with the value absent from stdout, stderr, and every authorizedApiFetch argument.

The one thing given up is that a pasted secret now costs one GET /api/v1/workspaces before it's rejected, instead of failing offline. That request carries the session's own credentials and not the selector, so nothing leaks — it just isn't free anymore.


Generated by Claude Code

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/cli/src/cli/commands/cloud.test.ts (1)

43-48: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Import the real redactCredentialValues instead of duplicating its regex.

This mock reimplements the credential-prefix regex from packages/cloud/src/redact.ts by hand. looksLikeCredential in cloud.ts depends on this function's exact matching behavior to reject credential-like --workspace selectors before any network call. If the real regex in packages/cloud/src/redact.ts changes (a prefix added, removed, or its matching logic adjusted), this hardcoded mock won't reflect it, and the credential-rejection tests (lines 1030-1041) would keep passing against stale behavior while production behavior silently diverges.

Use vi.importActual('@agent-relay/cloud') to pull the real redactCredentialValues into the mock instead of reimplementing it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/cli/commands/cloud.test.ts` around lines 43 - 48, Update the
cloud module mock in the test to obtain redactCredentialValues from
vi.importActual("`@agent-relay/cloud`") instead of duplicating the credential
regex locally. Remove the hand-rolled implementation while preserving the mock’s
other behavior, so looksLikeCredential tests use the production redaction logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/cli/src/cli/commands/cloud.ts`:
- Around line 481-508: Sanitize workspace IDs before terminal output in both
formatWorkspaceLabel and renderWorkspaceList. Apply sanitizeForTerminalLine to
entry.id and workspace.id, and use the sanitized workspace ID consistently for
width calculation, padding, and rendering.

---

Nitpick comments:
In `@packages/cli/src/cli/commands/cloud.test.ts`:
- Around line 43-48: Update the cloud module mock in the test to obtain
redactCredentialValues from vi.importActual("`@agent-relay/cloud`") instead of
duplicating the credential regex locally. Remove the hand-rolled implementation
while preserving the mock’s other behavior, so looksLikeCredential tests use the
production redaction logic.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 64561214-86d9-4985-80b1-c313a5698bbd

📥 Commits

Reviewing files that changed from the base of the PR and between 41475b0 and f4f5aa9.

📒 Files selected for processing (8)
  • CHANGELOG.md
  • packages/cli/README.md
  • packages/cli/src/cli/bootstrap.test.ts
  • packages/cli/src/cli/commands/cloud.test.ts
  • packages/cli/src/cli/commands/cloud.ts
  • packages/cli/src/cli/lib/formatting.test.ts
  • packages/cli/src/cli/lib/formatting.ts
  • packages/cli/src/cli/telemetry/events.ts

Comment thread packages/cli/src/cli/commands/cloud.ts Outdated
…dential

`looksLikeCredential` gated the lookup, and it keys off the redactor's
deliberately broad prefixes — so a workspace legitimately named `br_team`
(or `Platform at_live_dev`) was rejected before the listing was ever
queried and could never be selected by name.

Match against the listing first. The credential check now only picks which
error an *unmatched* selector gets, so a real name always resolves. The
security property is unchanged: matching is local, so the selector never
reaches an outbound request, and neither error echoes it.

Also sanitize workspace IDs before display — they are Cloud-provided text
like names and slugs — and measure column widths from the sanitized form so
padding stays correct. The test mock now takes the real
`redactCredentialValues` via `importOriginal` instead of a hand-copied
regex that could drift from production.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012WVG4uJ3erC9sq6rUW7551
@willwashburn
willwashburn merged commit 73c7cf1 into main Aug 2, 2026
41 checks passed
@willwashburn
willwashburn deleted the claude/github-issue-1372-pr-65gkc6 branch August 2, 2026 11:25
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.

cloud enroll requires a workspace UUID the CLI gives you no way to discover

2 participants