Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
204 changes: 204 additions & 0 deletions cli/workspace.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
package cli

import (
"bufio"
"context"
"fmt"
"os"
"strings"

blaxel "github.com/blaxel-ai/sdk-go"
"github.com/blaxel-ai/sdk-go/option"
"github.com/blaxel-ai/toolkit/cli/core"
"github.com/spf13/cobra"
"golang.org/x/term"
)

func init() {
Expand Down Expand Up @@ -101,9 +105,209 @@ To list all authenticated workspaces, run without arguments.`,

cmd.Flags().BoolVar(&current, "current", false, "Display only the current workspace name")

cmd.AddCommand(WorkspaceHipaaCmd())

return cmd
}

// workspaceHipaaResponse mirrors the parts of the controlplane Workspace JSON
// response that this command cares about. The sdk-go Workspace struct is
// generated from an older spec and does not expose hipaaOptIn directly.
type workspaceHipaaResponse struct {
HipaaOptIn bool `json:"hipaaOptIn"`
Name string `json:"name"`
}

// WorkspaceHipaaCmd is the parent of `bl workspaces hipaa ...`.
func WorkspaceHipaaCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "hipaa",
Short: "Manage workspace HIPAA opt-in",
Long: `Manage the HIPAA opt-in flag on the current workspace.

Deploying agents, sandboxes, functions or jobs to a HIPAA-gated region
requires the workspace to explicitly opt in to HIPAA handling. Without
opt-in, those deployments are rejected by the platform.

Use 'bl workspaces hipaa accept' to opt in, 'bl workspaces hipaa decline'
to opt out, and 'bl workspaces hipaa status' to inspect the current state.`,
}

cmd.AddCommand(WorkspaceHipaaAcceptCmd())
cmd.AddCommand(WorkspaceHipaaDeclineCmd())
cmd.AddCommand(WorkspaceHipaaStatusCmd())
return cmd
}

// WorkspaceHipaaAcceptCmd flips workspace.hipaaOptIn to true.
func WorkspaceHipaaAcceptCmd() *cobra.Command {
var assumeYes bool

cmd := &cobra.Command{
Use: "accept",
Short: "Accept HIPAA opt-in for the current workspace",
Long: `Opt the current workspace in to HIPAA handling.

By accepting, a workspace admin acknowledges that:
- HIPAA-gated regions are now eligible deployment targets for this workspace.
- The workspace is responsible for using Blaxel only in line with applicable
HIPAA obligations.

The '--workspace' global flag, if set, targets a different workspace. Only
workspace admins can change this setting.`,
Example: ` # Accept HIPAA opt-in for the current workspace (prompts for confirmation)
bl workspaces hipaa accept

# Accept without an interactive prompt (useful in CI)
bl workspaces hipaa accept --yes

# Accept for a workspace other than the current one
bl workspaces hipaa accept --workspace prod -y`,
Run: func(cmd *cobra.Command, args []string) {
runWorkspaceHipaaUpdate(cmd.Context(), true, assumeYes)
},
}

cmd.Flags().BoolVarP(&assumeYes, "yes", "y", false, "Skip the interactive confirmation prompt")
return cmd
}

// WorkspaceHipaaDeclineCmd flips workspace.hipaaOptIn back to false.
func WorkspaceHipaaDeclineCmd() *cobra.Command {
var assumeYes bool

cmd := &cobra.Command{
Use: "decline",
Short: "Withdraw HIPAA opt-in for the current workspace",
Long: `Withdraw the workspace's HIPAA opt-in.

After declining, deployments to HIPAA-gated regions will be rejected
until opt-in is accepted again. Existing resources are not affected.
Only workspace admins can change this setting.`,
Run: func(cmd *cobra.Command, args []string) {
runWorkspaceHipaaUpdate(cmd.Context(), false, assumeYes)
},
}

cmd.Flags().BoolVarP(&assumeYes, "yes", "y", false, "Skip the interactive confirmation prompt")
return cmd
}

// WorkspaceHipaaStatusCmd prints the current value of workspace.hipaaOptIn.
func WorkspaceHipaaStatusCmd() *cobra.Command {
return &cobra.Command{
Use: "status",
Short: "Show whether the current workspace has accepted HIPAA opt-in",
Run: func(cmd *cobra.Command, args []string) {
ctx := cmd.Context()
if ctx == nil {
ctx = context.Background()
}
workspaceName := resolveWorkspaceName()
ws, err := fetchWorkspaceHipaa(ctx, workspaceName)
if err != nil {
core.PrintError("Workspace", err)
core.ExitWithError(err)
}
state := "declined"
if ws.HipaaOptIn {
state = "accepted"
}
fmt.Printf("Workspace %s: HIPAA opt-in %s\n", workspaceName, state)
},
}
}

func runWorkspaceHipaaUpdate(ctx context.Context, optIn bool, assumeYes bool) {
if ctx == nil {
ctx = context.Background()
}
workspaceName := resolveWorkspaceName()

if !assumeYes && !confirmHipaaChange(workspaceName, optIn) {
core.Print("Aborted.\n")
return
}

client := core.GetClient()
if client == nil {
err := fmt.Errorf("no API client available. Please run 'bl login' first")
core.PrintError("Workspace", err)
core.ExitWithError(err)
}
Comment on lines +234 to +244

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🟡 Interactive confirmation prompt happens before client availability check

In runWorkspaceHipaaUpdate, the user is prompted for interactive confirmation (line 227) before the API client availability check (line 232-237). If the user is not logged in (core.GetClient() returns nil), they will go through the full TTY confirmation dialog, type "y", and only then be told they need to run bl login. The cheap, non-interactive client check should precede the interactive prompt to avoid wasting the user's time on an operation that is guaranteed to fail.

Suggested change
workspaceName := resolveWorkspaceName()
if !assumeYes && !confirmHipaaChange(workspaceName, optIn) {
core.Print("Aborted.\n")
return
}
client := core.GetClient()
if client == nil {
err := fmt.Errorf("no API client available. Please run 'bl login' first")
core.PrintError("Workspace", err)
core.ExitWithError(err)
}
workspaceName := resolveWorkspaceName()
client := core.GetClient()
if client == nil {
err := fmt.Errorf("no API client available. Please run 'bl login' first")
core.PrintError("Workspace", err)
core.ExitWithError(err)
}
if !assumeYes && !confirmHipaaChange(workspaceName, optIn) {
core.Print("Aborted.\n")
return
}
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch — fixed in 83d59b7. Moved the core.GetClient() nil-check above the TTY confirmation so an unauthenticated user gets the bl login message immediately instead of after typing through the y/N dialog.


body := map[string]bool{"hipaaOptIn": optIn}
var res workspaceHipaaResponse
path := fmt.Sprintf("workspaces/%s/hipaa", workspaceName)
if err := client.Put(ctx, path, body, &res); err != nil {
msg := extractErrorMessage(err)
core.PrintError("Workspace", fmt.Errorf("failed to update HIPAA opt-in: %s", msg))
core.ExitWithError(err)
}

if optIn {
fmt.Printf("Workspace %s: HIPAA opt-in accepted.\n", workspaceName)
} else {
fmt.Printf("Workspace %s: HIPAA opt-in declined.\n", workspaceName)
}
}

// fetchWorkspaceHipaa loads the workspace through the generic client so we can
// read fields (hipaaOptIn) that the older sdk-go Workspace struct does not
// expose as typed members.
func fetchWorkspaceHipaa(ctx context.Context, workspaceName string) (workspaceHipaaResponse, error) {
client := core.GetClient()
if client == nil {
return workspaceHipaaResponse{}, fmt.Errorf("no API client available. Please run 'bl login' first")
}
var res workspaceHipaaResponse
path := fmt.Sprintf("workspaces/%s", workspaceName)
if err := client.Get(ctx, path, nil, &res); err != nil {
return workspaceHipaaResponse{}, fmt.Errorf("%s", extractErrorMessage(err))
}
if res.Name == "" {
res.Name = workspaceName
}
return res, nil
}

// resolveWorkspaceName returns the workspace targeted by the command — the
// --workspace override if provided, otherwise the current workspace from
// the local config.
func resolveWorkspaceName() string {
if ws := core.GetWorkspace(); ws != "" {
return ws
}
ctx, _ := blaxel.CurrentContext()
if ctx.Workspace == "" {
err := fmt.Errorf("no workspace selected. Run 'bl login' or pass --workspace")
core.PrintError("Workspace", err)
core.ExitWithError(err)
}
return ctx.Workspace
}

// confirmHipaaChange shows the change and asks for y/N when stdin is a TTY.
// Non-TTY callers (CI, pipelines) must pass --yes explicitly.
func confirmHipaaChange(workspaceName string, optIn bool) bool {
if !term.IsTerminal(int(os.Stdin.Fd())) {
fmt.Fprintln(os.Stderr, "Refusing to change HIPAA opt-in without --yes (stdin is not a terminal).")
return false
}
action := "accept HIPAA opt-in"
if !optIn {
action = "decline HIPAA opt-in"
}
fmt.Printf("About to %s for workspace '%s'. Continue? [y/N]: ", action, workspaceName)
reader := bufio.NewReader(os.Stdin)
line, err := reader.ReadString('\n')
if err != nil {
return false
}
answer := strings.TrimSpace(strings.ToLower(line))
return answer == "y" || answer == "yes"
}

func CheckWorkspaceAccess(workspaceName string, credentials blaxel.Credentials) (blaxel.Workspace, error) {
// Build client options based on credentials
opts := []option.RequestOption{
Expand Down
41 changes: 41 additions & 0 deletions cli/workspace_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,44 @@ func TestTokenCmdArguments(t *testing.T) {
// Token cmd takes optional workspace argument
assert.NotNil(t, cmd.Args)
}

func TestWorkspaceHipaaCmd(t *testing.T) {
cmd := WorkspaceHipaaCmd()

assert.Equal(t, "hipaa", cmd.Use)
assert.NotEmpty(t, cmd.Short)
assert.NotEmpty(t, cmd.Long)

subcommands := make(map[string]bool, len(cmd.Commands()))
for _, sub := range cmd.Commands() {
subcommands[sub.Use] = true
}
assert.True(t, subcommands["accept"], "expected accept subcommand")
assert.True(t, subcommands["decline"], "expected decline subcommand")
assert.True(t, subcommands["status"], "expected status subcommand")
}

func TestWorkspaceHipaaSubcommandFlags(t *testing.T) {
for _, name := range []string{"accept", "decline"} {
var cmd = WorkspaceHipaaAcceptCmd()
if name == "decline" {
cmd = WorkspaceHipaaDeclineCmd()
}
flag := cmd.Flags().Lookup("yes")
assert.NotNil(t, flag, "%s should expose --yes", name)
assert.Equal(t, "y", flag.Shorthand, "%s --yes shorthand", name)
}
}

func TestListOrSetWorkspacesCmdRegistersHipaaSubcommand(t *testing.T) {
cmd := ListOrSetWorkspacesCmd()

var hipaa bool
for _, sub := range cmd.Commands() {
if sub.Use == "hipaa" {
hipaa = true
break
}
}
assert.True(t, hipaa, "workspaces command should attach `hipaa` subcommand")
}
Loading