Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
24 changes: 24 additions & 0 deletions cli/apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,24 @@ type ResourceOperationResult struct {
MetadataURL string
}

// acknowledgeNotHipaaFlag is the package-level flag toggled by --accept-not-hipaa
// on bl deploy / bl apply. When true, every resource upsert routed through
// handleResourceOperation carries the X-Blaxel-Acknowledge-Not-Hipaa header,
// which the controlplane treats as one-shot consent to deploy in regions or
// products that are not HIPAA-compliant.
var acknowledgeNotHipaaFlag bool

// SetAcknowledgeNotHipaa toggles the per-process acknowledgement flag.
// Exported so deploy / apply commands can flip it from their cobra Run funcs.
func SetAcknowledgeNotHipaa(ack bool) {
acknowledgeNotHipaaFlag = ack
}

// AcknowledgeNotHipaa returns the current per-process acknowledgement flag.
func AcknowledgeNotHipaa() bool {
return acknowledgeNotHipaaFlag
}

type ApplyResult struct {
Kind string
Name string
Expand All @@ -55,6 +73,7 @@ func ApplyCmd() *cobra.Command {
var recursive bool
var envFiles []string
var commandSecrets []string
var acceptNotHipaa bool
cmd := &cobra.Command{
Use: "apply",
Short: "Apply a configuration to a resource by file",
Expand Down Expand Up @@ -149,6 +168,7 @@ via -e flag for .env files or -s flag for command-line secrets.`,
Run: func(cmd *cobra.Command, args []string) {
core.LoadCommandSecrets(commandSecrets)
core.ReadSecrets("", envFiles)
SetAcknowledgeNotHipaa(acceptNotHipaa)
applyResults, err := Apply(filePath, WithRecursive(recursive))
if err != nil {
core.PrintError("Apply", err)
Expand Down Expand Up @@ -179,6 +199,7 @@ via -e flag for .env files or -s flag for command-line secrets.`,
cmd.Flags().BoolVarP(&recursive, "recursive", "R", false, "Process the directory used in -f, --filename recursively. Useful when you want to manage related manifests organized within the same directory.")
cmd.Flags().StringSliceVarP(&envFiles, "env-file", "e", []string{".env"}, "Environment file to load")
cmd.Flags().StringSliceVarP(&commandSecrets, "secrets", "s", []string{}, "Secrets to deploy")
cmd.Flags().BoolVar(&acceptNotHipaa, "accept-not-hipaa", false, "Acknowledge that the resources in this apply may target regions/products that are NOT HIPAA-compliant. Sends X-Blaxel-Acknowledge-Not-Hipaa: true so the controlplane permits the deploy even when the workspace has not set hipaaUnsafe=true.")
err := cmd.MarkFlagRequired("filename")
if err != nil {
core.PrintError("Apply", err)
Expand Down Expand Up @@ -356,6 +377,9 @@ func handleResourceOperation(resource *core.Resource, name string, resourceObjec
if autogeneratedInLabels {
opts = append(opts, option.WithQuery("upload", "true"))
}
if AcknowledgeNotHipaa() {
opts = append(opts, option.WithHeader("X-Blaxel-Acknowledge-Not-Hipaa", "true"))
}

// Preserve extra runtime fields that the SDK's typed param structs
// don't model (e.g. dockerConfig, skipBuild for registry image builds).
Expand Down
3 changes: 3 additions & 0 deletions cli/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ func DeployCmd() *cobra.Command {
var dockerConfigPath string
var timeoutStr string
var buildEnvPath string
var acceptNotHipaa bool

cmd := &cobra.Command{
Use: "deploy",
Expand Down Expand Up @@ -116,6 +117,7 @@ all projects in a monorepo (looks for blaxel.toml in subdirectories).`,
Run: func(cmd *cobra.Command, args []string) {
core.LoadCommandSecrets(commandSecrets)
core.ReadSecrets(folder, envFiles)
SetAcknowledgeNotHipaa(acceptNotHipaa)
// If the user did not explicitly set --yes, decide default based on TTY and CI
if !cmd.Flags().Changed("yes") {
// By default use TTY mode (noTTY=false) if terminal is interactive and not in CI
Expand Down Expand Up @@ -341,6 +343,7 @@ all projects in a monorepo (looks for blaxel.toml in subdirectories).`,
cmd.Flags().StringVar(&dockerConfigPath, "docker-config", "", "Path to a Docker config.json file with registry credentials")
cmd.Flags().StringVar(&timeoutStr, "timeout", "", "Timeout for build and deployment monitoring (e.g. 30m, 1h). Defaults to 15m")
cmd.Flags().StringVar(&buildEnvPath, "build-env-file", "", "Path to a build env file with Docker build args (default: auto-detect .env.build)")
cmd.Flags().BoolVar(&acceptNotHipaa, "accept-not-hipaa", false, "Acknowledge that this deploy may target regions/products that are NOT HIPAA-compliant. Sends X-Blaxel-Acknowledge-Not-Hipaa: true so the controlplane permits the deploy even when the workspace has not set hipaaUnsafe=true.")
return cmd
}

Expand Down
216 changes: 216 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,221 @@ 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 hipaaUnsafe directly.
type workspaceHipaaResponse struct {
HipaaUnsafe bool `json:"hipaaUnsafe"`
Name string `json:"name"`
}

// WorkspaceHipaaCmd is the parent of `bl workspaces hipaa ...`.
func WorkspaceHipaaCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "hipaa",
Short: "Manage non-HIPAA-compliant deploy consent for the workspace",
Long: `Manage the workspace-level consent to deploy in non-HIPAA-compliant
regions/products (workspace.hipaaUnsafe).

By default, deploying agents, sandboxes, functions or jobs to a region
that is not HIPAA-compliant is blocked. There are two ways to unblock
such a deploy:
- Standing consent: set workspace.hipaaUnsafe=true with 'accept'. All
future non-HIPAA-compliant deploys from this workspace will be
allowed.
- Per-deploy consent: pass '--accept-not-hipaa' to 'bl deploy' /
'bl apply' for a single non-HIPAA-compliant deploy.

Use 'bl workspaces hipaa accept' to grant standing consent,
'bl workspaces hipaa decline' to revoke it, and
'bl workspaces hipaa status' to inspect the current state.`,
}

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

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

cmd := &cobra.Command{
Use: "accept",
Short: "Allow non-HIPAA-compliant deploys for the current workspace",
Long: `Grant standing consent for the current workspace to deploy in
regions/products that are NOT HIPAA-compliant (workspace.hipaaUnsafe=true).

By accepting, a workspace admin acknowledges that:
- Future deploys from this workspace MAY target regions and products
that are not HIPAA-compliant, with no further acknowledgement required.
- Protected health information must not be processed in those deploys.

The '--workspace' global flag, if set, targets a different workspace. Only
workspace admins can change this setting.`,
Example: ` # Allow non-HIPAA-compliant deploys for the current workspace (prompts)
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.hipaaUnsafe back to false.
func WorkspaceHipaaDeclineCmd() *cobra.Command {
var assumeYes bool

cmd := &cobra.Command{
Use: "decline",
Short: "Block non-HIPAA-compliant deploys for the current workspace",
Long: `Revoke the workspace's standing consent to deploy in non-HIPAA-
compliant regions/products (workspace.hipaaUnsafe=false).

After declining, non-HIPAA-compliant deploys will be rejected unless the
individual deploy is acknowledged with '--accept-not-hipaa'. 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.hipaaUnsafe.
func WorkspaceHipaaStatusCmd() *cobra.Command {
return &cobra.Command{
Use: "status",
Short: "Show whether non-HIPAA-compliant deploys are allowed in the workspace",
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 := "blocked (workspace.hipaaUnsafe=false)"
if ws.HipaaUnsafe {
state = "allowed (workspace.hipaaUnsafe=true)"
}
fmt.Printf("Workspace %s: non-HIPAA-compliant deploys %s\n", workspaceName, state)
},
}
}

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

// Run the cheap client-availability check before any interactive prompt
// so an unauthenticated user is told to `bl login` immediately instead
// of after a successful y/N dialog.
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.


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

body := map[string]bool{"hipaaUnsafe": hipaaUnsafe}
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-unsafe consent: %s", msg))
core.ExitWithError(err)
}

if hipaaUnsafe {
fmt.Printf("Workspace %s: non-HIPAA-compliant deploys are now allowed.\n", workspaceName)
} else {
fmt.Printf("Workspace %s: non-HIPAA-compliant deploys are now blocked.\n", workspaceName)
}
}

// fetchWorkspaceHipaa loads the workspace through the generic client so we can
// read fields (hipaaUnsafe) 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, hipaaUnsafe bool) bool {
if !term.IsTerminal(int(os.Stdin.Fd())) {
fmt.Fprintln(os.Stderr, "Refusing to change HIPAA-unsafe consent without --yes (stdin is not a terminal).")
return false
}
action := "ALLOW non-HIPAA-compliant deploys"
if !hipaaUnsafe {
action = "BLOCK non-HIPAA-compliant deploys"
}
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