From e70997b344563dda9911dc7bd497afc413c51a99 Mon Sep 17 00:00:00 2001 From: Lennart Kats Date: Fri, 3 Apr 2026 10:59:20 +0200 Subject: [PATCH 01/13] WIP: agent consent token system (original approach) Adds a consent-token-based gating system for AI agents using destructive CLI flags (--force-lock, --auto-approve, --force). Includes agent detection via environment variables, improved error messages, and a `databricks agent consent` command. This commit preserves the original approach before simplification. Co-authored-by: Isaac --- .../bind/dashboard/recreation/output.txt | 4 +- .../bind/job/job-abort-bind/output.txt | 4 +- .../recreate/out.bind-fail.direct.txt | 4 +- .../recreate/out.bind-fail.terraform.txt | 4 +- .../bind/pipelines/recreate/output.txt | 4 +- .../pipelines/update/out.bind-fail.direct.txt | 4 +- .../update/out.bind-fail.terraform.txt | 4 +- .../pipelines/auto-approve/output.txt | 4 +- .../resources/pipelines/recreate/output.txt | 4 +- .../resources/schemas/auto-approve/output.txt | 4 +- .../resources/volumes/recreate/output.txt | 4 +- .../pipelines/deploy/auto-approve/output.txt | 4 +- .../pipelines/deploy/force-lock/output.txt | 4 +- .../pipelines/destroy/auto-approve/output.txt | 4 +- .../pipelines/destroy/force-lock/output.txt | 4 +- bundle/config/mutator/validate_git_details.go | 3 +- .../mutator/validate_git_details_test.go | 6 +- .../check_dashboards_modified_remotely.go | 4 +- bundle/deploy/terraform/import.go | 2 +- bundle/phases/bind.go | 2 +- bundle/phases/deploy.go | 2 +- cmd/agent/agent.go | 18 ++ cmd/agent/consent.go | 42 ++++ cmd/bundle/deploy.go | 5 + cmd/bundle/deployment/bind.go | 4 + cmd/bundle/deployment/unbind.go | 4 + cmd/bundle/destroy.go | 6 +- cmd/cmd.go | 2 + cmd/pipelines/deploy.go | 4 + cmd/pipelines/destroy.go | 4 + integration/libs/locker/locker_test.go | 2 +- libs/agent/check_flags.go | 36 ++++ libs/agent/check_flags_test.go | 89 +++++++++ libs/agent/consent.go | 187 ++++++++++++++++++ libs/agent/consent_test.go | 118 +++++++++++ libs/locker/locker.go | 10 +- 36 files changed, 581 insertions(+), 29 deletions(-) create mode 100644 cmd/agent/agent.go create mode 100644 cmd/agent/consent.go create mode 100644 libs/agent/check_flags.go create mode 100644 libs/agent/check_flags_test.go create mode 100644 libs/agent/consent.go create mode 100644 libs/agent/consent_test.go diff --git a/acceptance/bundle/deployment/bind/dashboard/recreation/output.txt b/acceptance/bundle/deployment/bind/dashboard/recreation/output.txt index 99c26a8ccc..5e83560c7d 100644 --- a/acceptance/bundle/deployment/bind/dashboard/recreation/output.txt +++ b/acceptance/bundle/deployment/bind/dashboard/recreation/output.txt @@ -18,7 +18,9 @@ Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bundle-[UNIQU This action will result in the deletion or recreation of the following dashboards. This will result in changed IDs and permanent URLs of the dashboards that will be recreated: recreate resources.dashboards.dashboard1 -Error: the deployment requires destructive actions, but current console does not support prompting. Please specify --auto-approve if you would like to skip prompts and proceed +Error: the deployment requires destructive actions (resources will be deleted), but the current console does not support prompting. +Using --auto-approve will skip all confirmation prompts and proceed with the destructive changes. +Only use --auto-approve if you have reviewed the plan and accept the deletions Exit code: 1 diff --git a/acceptance/bundle/deployment/bind/job/job-abort-bind/output.txt b/acceptance/bundle/deployment/bind/job/job-abort-bind/output.txt index 544448efbe..219d45d915 100644 --- a/acceptance/bundle/deployment/bind/job/job-abort-bind/output.txt +++ b/acceptance/bundle/deployment/bind/job/job-abort-bind/output.txt @@ -3,7 +3,9 @@ Created job with ID: [JOB_ID] === Expect binding to fail without an auto-approve flag: -Error: This bind operation requires user confirmation, but the current console does not support prompting. Please specify --auto-approve if you would like to skip prompts and proceed. +Error: This bind operation requires user confirmation, but the current console does not support prompting. +Using --auto-approve will bind resources without reviewing the changes. +Only use --auto-approve if you have reviewed what will be bound. === Deploy bundle: >>> [CLI] bundle deploy --force-lock diff --git a/acceptance/bundle/deployment/bind/pipelines/recreate/out.bind-fail.direct.txt b/acceptance/bundle/deployment/bind/pipelines/recreate/out.bind-fail.direct.txt index c5e00b49cb..0ee95b0069 100644 --- a/acceptance/bundle/deployment/bind/pipelines/recreate/out.bind-fail.direct.txt +++ b/acceptance/bundle/deployment/bind/pipelines/recreate/out.bind-fail.direct.txt @@ -11,5 +11,7 @@ Changes detected: ~ root_path: "/Workspace/Users/someuser@databricks.com/lakeflow_pipeline" -> null ~ storage: "/Shared/old_storage" -> "/Shared/new_storage" -Error: This bind operation requires user confirmation, but the current console does not support prompting. Please specify --auto-approve if you would like to skip prompts and proceed. +Error: This bind operation requires user confirmation, but the current console does not support prompting. +Using --auto-approve will bind resources without reviewing the changes. +Only use --auto-approve if you have reviewed what will be bound. diff --git a/acceptance/bundle/deployment/bind/pipelines/recreate/out.bind-fail.terraform.txt b/acceptance/bundle/deployment/bind/pipelines/recreate/out.bind-fail.terraform.txt index 1d300160a9..a5dd712d34 100644 --- a/acceptance/bundle/deployment/bind/pipelines/recreate/out.bind-fail.terraform.txt +++ b/acceptance/bundle/deployment/bind/pipelines/recreate/out.bind-fail.terraform.txt @@ -54,5 +54,7 @@ Terraform will perform the following actions: Plan: 1 to add, 0 to change, 1 to destroy. -Error: This bind operation requires user confirmation, but the current console does not support prompting. Please specify --auto-approve if you would like to skip prompts and proceed. +Error: This bind operation requires user confirmation, but the current console does not support prompting. +Using --auto-approve will bind resources without reviewing the changes. +Only use --auto-approve if you have reviewed what will be bound. diff --git a/acceptance/bundle/deployment/bind/pipelines/recreate/output.txt b/acceptance/bundle/deployment/bind/pipelines/recreate/output.txt index af42322dee..9f282a85d7 100644 --- a/acceptance/bundle/deployment/bind/pipelines/recreate/output.txt +++ b/acceptance/bundle/deployment/bind/pipelines/recreate/output.txt @@ -16,7 +16,9 @@ Streaming Tables (STs) and Materialized Views (MVs) managed by them. Recreating restore the defined STs and MVs through full refresh. Note that recreation is necessary when pipeline properties such as the 'catalog' or 'storage' are changed: recreate resources.pipelines.foo -Error: the deployment requires destructive actions, but current console does not support prompting. Please specify --auto-approve if you would like to skip prompts and proceed +Error: the deployment requires destructive actions (resources will be deleted), but the current console does not support prompting. +Using --auto-approve will skip all confirmation prompts and proceed with the destructive changes. +Only use --auto-approve if you have reviewed the plan and accept the deletions >>> [CLI] bundle deploy --auto-approve diff --git a/acceptance/bundle/deployment/bind/pipelines/update/out.bind-fail.direct.txt b/acceptance/bundle/deployment/bind/pipelines/update/out.bind-fail.direct.txt index 7b85803529..d1a289868e 100644 --- a/acceptance/bundle/deployment/bind/pipelines/update/out.bind-fail.direct.txt +++ b/acceptance/bundle/deployment/bind/pipelines/update/out.bind-fail.direct.txt @@ -11,5 +11,7 @@ Changes detected: ~ name: "lakeflow-pipeline" -> "test-pipeline" ~ root_path: "/Workspace/Users/[USERNAME]/lakeflow_pipeline" -> null -Error: This bind operation requires user confirmation, but the current console does not support prompting. Please specify --auto-approve if you would like to skip prompts and proceed. +Error: This bind operation requires user confirmation, but the current console does not support prompting. +Using --auto-approve will bind resources without reviewing the changes. +Only use --auto-approve if you have reviewed what will be bound. diff --git a/acceptance/bundle/deployment/bind/pipelines/update/out.bind-fail.terraform.txt b/acceptance/bundle/deployment/bind/pipelines/update/out.bind-fail.terraform.txt index 2ff60fce2b..14ebbe6df2 100644 --- a/acceptance/bundle/deployment/bind/pipelines/update/out.bind-fail.terraform.txt +++ b/acceptance/bundle/deployment/bind/pipelines/update/out.bind-fail.terraform.txt @@ -41,5 +41,7 @@ Terraform will perform the following actions: Plan: 0 to add, 1 to change, 0 to destroy. -Error: This bind operation requires user confirmation, but the current console does not support prompting. Please specify --auto-approve if you would like to skip prompts and proceed. +Error: This bind operation requires user confirmation, but the current console does not support prompting. +Using --auto-approve will bind resources without reviewing the changes. +Only use --auto-approve if you have reviewed what will be bound. diff --git a/acceptance/bundle/resources/pipelines/auto-approve/output.txt b/acceptance/bundle/resources/pipelines/auto-approve/output.txt index 5154a1206a..d71a3f7a9c 100644 --- a/acceptance/bundle/resources/pipelines/auto-approve/output.txt +++ b/acceptance/bundle/resources/pipelines/auto-approve/output.txt @@ -50,7 +50,9 @@ Streaming Tables (STs) and Materialized Views (MVs) managed by them. Recreating restore the defined STs and MVs through full refresh. Note that recreation is necessary when pipeline properties such as the 'catalog' or 'storage' are changed: delete resources.pipelines.bar -Error: the deployment requires destructive actions, but current console does not support prompting. Please specify --auto-approve if you would like to skip prompts and proceed +Error: the deployment requires destructive actions (resources will be deleted), but the current console does not support prompting. +Using --auto-approve will skip all confirmation prompts and proceed with the destructive changes. +Only use --auto-approve if you have reviewed the plan and accept the deletions Exit code: 1 diff --git a/acceptance/bundle/resources/pipelines/recreate/output.txt b/acceptance/bundle/resources/pipelines/recreate/output.txt index 8550395ff2..afb961b67b 100644 --- a/acceptance/bundle/resources/pipelines/recreate/output.txt +++ b/acceptance/bundle/resources/pipelines/recreate/output.txt @@ -49,7 +49,9 @@ Streaming Tables (STs) and Materialized Views (MVs) managed by them. Recreating restore the defined STs and MVs through full refresh. Note that recreation is necessary when pipeline properties such as the 'catalog' or 'storage' are changed: recreate resources.pipelines.foo -Error: the deployment requires destructive actions, but current console does not support prompting. Please specify --auto-approve if you would like to skip prompts and proceed +Error: the deployment requires destructive actions (resources will be deleted), but the current console does not support prompting. +Using --auto-approve will skip all confirmation prompts and proceed with the destructive changes. +Only use --auto-approve if you have reviewed the plan and accept the deletions Exit code: 1 diff --git a/acceptance/bundle/resources/schemas/auto-approve/output.txt b/acceptance/bundle/resources/schemas/auto-approve/output.txt index 6a773f60ca..f2a4563c10 100644 --- a/acceptance/bundle/resources/schemas/auto-approve/output.txt +++ b/acceptance/bundle/resources/schemas/auto-approve/output.txt @@ -57,7 +57,9 @@ Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/[UNIQUE_NAME]/file This action will result in the deletion or recreation of the following UC schemas. Any underlying data may be lost: delete resources.schemas.bar -Error: the deployment requires destructive actions, but current console does not support prompting. Please specify --auto-approve if you would like to skip prompts and proceed +Error: the deployment requires destructive actions (resources will be deleted), but the current console does not support prompting. +Using --auto-approve will skip all confirmation prompts and proceed with the destructive changes. +Only use --auto-approve if you have reviewed the plan and accept the deletions === Test cleanup diff --git a/acceptance/bundle/resources/volumes/recreate/output.txt b/acceptance/bundle/resources/volumes/recreate/output.txt index 494e9ff538..6d2625fa5f 100644 --- a/acceptance/bundle/resources/volumes/recreate/output.txt +++ b/acceptance/bundle/resources/volumes/recreate/output.txt @@ -45,7 +45,9 @@ For managed volumes, the files stored in the volume are also deleted from your cloud tenant within 30 days. For external volumes, the metadata about the volume is removed from the catalog, but the underlying files are not deleted: recreate resources.volumes.foo -Error: the deployment requires destructive actions, but current console does not support prompting. Please specify --auto-approve if you would like to skip prompts and proceed +Error: the deployment requires destructive actions (resources will be deleted), but the current console does not support prompting. +Using --auto-approve will skip all confirmation prompts and proceed with the destructive changes. +Only use --auto-approve if you have reviewed the plan and accept the deletions Exit code: 1 diff --git a/acceptance/pipelines/deploy/auto-approve/output.txt b/acceptance/pipelines/deploy/auto-approve/output.txt index ff5ac099bf..53066ab958 100644 --- a/acceptance/pipelines/deploy/auto-approve/output.txt +++ b/acceptance/pipelines/deploy/auto-approve/output.txt @@ -18,7 +18,9 @@ Streaming Tables (STs) and Materialized Views (MVs) managed by them. Recreating restore the defined STs and MVs through full refresh. Note that recreation is necessary when pipeline properties such as the 'catalog' or 'storage' are changed: delete resources.pipelines.foo -Error: the deployment requires destructive actions, but current console does not support prompting. Please specify --auto-approve if you would like to skip prompts and proceed +Error: the deployment requires destructive actions (resources will be deleted), but the current console does not support prompting. +Using --auto-approve will skip all confirmation prompts and proceed with the destructive changes. +Only use --auto-approve if you have reviewed the plan and accept the deletions Exit code: 1 diff --git a/acceptance/pipelines/deploy/force-lock/output.txt b/acceptance/pipelines/deploy/force-lock/output.txt index e5fdadbaf9..4c071717d8 100644 --- a/acceptance/pipelines/deploy/force-lock/output.txt +++ b/acceptance/pipelines/deploy/force-lock/output.txt @@ -4,8 +4,8 @@ === test deployment without force-lock (should fail) >>> errcode [CLI] pipelines deploy -Error: Failed to acquire deployment lock: deploy lock acquired by user-with-lock@databricks.com at [TIMESTAMP] +0000 UTC. Use --force-lock to override -Error: deploy lock acquired by user-with-lock@databricks.com at [TIMESTAMP] +0000 UTC. Use --force-lock to override +Error: Failed to acquire deployment lock: deploy lock acquired by user-with-lock@databricks.com at [TIMESTAMP] +0000 UTC. Only use --force-lock if you are certain the other deployment is no longer active +Error: deploy lock acquired by user-with-lock@databricks.com at [TIMESTAMP] +0000 UTC. Only use --force-lock if you are certain the other deployment is no longer active Exit code: 1 diff --git a/acceptance/pipelines/destroy/auto-approve/output.txt b/acceptance/pipelines/destroy/auto-approve/output.txt index 17aecc2059..ef990a1209 100644 --- a/acceptance/pipelines/destroy/auto-approve/output.txt +++ b/acceptance/pipelines/destroy/auto-approve/output.txt @@ -8,7 +8,9 @@ Deployment complete! View your pipeline my_pipeline here: [DATABRICKS_URL]/pipelines/[UUID]?o=[NUMID] >>> errcode [CLI] pipelines destroy -Error: please specify --auto-approve since terminal does not support interactive prompts +Error: this will permanently destroy all deployed resources in the target. +Using --auto-approve will skip all confirmation prompts and proceed with destruction. +Only use --auto-approve if you are certain you want to delete all resources Exit code: 1 diff --git a/acceptance/pipelines/destroy/force-lock/output.txt b/acceptance/pipelines/destroy/force-lock/output.txt index 5b6449f732..ecea478fc3 100644 --- a/acceptance/pipelines/destroy/force-lock/output.txt +++ b/acceptance/pipelines/destroy/force-lock/output.txt @@ -11,8 +11,8 @@ View your pipeline foo here: [DATABRICKS_URL]/pipelines/[UUID]?o=[NUMID] === test deployment without force-lock (should fail) >>> errcode [CLI] pipelines destroy --auto-approve -Error: Failed to acquire deployment lock: deploy lock acquired by user-with-lock@databricks.com at [TIMESTAMP] +0000 UTC. Use --force-lock to override -Error: deploy lock acquired by user-with-lock@databricks.com at [TIMESTAMP] +0000 UTC. Use --force-lock to override +Error: Failed to acquire deployment lock: deploy lock acquired by user-with-lock@databricks.com at [TIMESTAMP] +0000 UTC. Only use --force-lock if you are certain the other deployment is no longer active +Error: deploy lock acquired by user-with-lock@databricks.com at [TIMESTAMP] +0000 UTC. Only use --force-lock if you are certain the other deployment is no longer active Exit code: 1 diff --git a/bundle/config/mutator/validate_git_details.go b/bundle/config/mutator/validate_git_details.go index 69a4221fdc..f7b413db7f 100644 --- a/bundle/config/mutator/validate_git_details.go +++ b/bundle/config/mutator/validate_git_details.go @@ -23,7 +23,8 @@ func (m *validateGitDetails) Apply(ctx context.Context, b *bundle.Bundle) diag.D } if b.Config.Bundle.Git.Branch != b.Config.Bundle.Git.ActualBranch && !b.Config.Bundle.Force { - return diag.Errorf("not on the right Git branch:\n expected according to configuration: %s\n actual: %s\nuse --force to override", b.Config.Bundle.Git.Branch, b.Config.Bundle.Git.ActualBranch) + return diag.Errorf("not on the right Git branch:\n expected according to configuration: %s\n actual: %s\nUsing --force will deploy from branch %s, which may push unexpected code to the target.\nOnly use --force if you intentionally want to deploy from this branch", + b.Config.Bundle.Git.Branch, b.Config.Bundle.Git.ActualBranch, b.Config.Bundle.Git.ActualBranch) } return nil } diff --git a/bundle/config/mutator/validate_git_details_test.go b/bundle/config/mutator/validate_git_details_test.go index b29f221edc..791c613c86 100644 --- a/bundle/config/mutator/validate_git_details_test.go +++ b/bundle/config/mutator/validate_git_details_test.go @@ -40,8 +40,10 @@ func TestValidateGitDetailsNonMatchingBranches(t *testing.T) { m := ValidateGitDetails() diags := bundle.Apply(t.Context(), b, m) - expectedError := "not on the right Git branch:\n expected according to configuration: main\n actual: feature\nuse --force to override" - assert.EqualError(t, diags.Error(), expectedError) + assert.ErrorContains(t, diags.Error(), "not on the right Git branch") + assert.ErrorContains(t, diags.Error(), "expected according to configuration: main") + assert.ErrorContains(t, diags.Error(), "actual: feature") + assert.ErrorContains(t, diags.Error(), "Only use --force if you intentionally want to deploy from this branch") } func TestValidateGitDetailsNotUsingGit(t *testing.T) { diff --git a/bundle/deploy/terraform/check_dashboards_modified_remotely.go b/bundle/deploy/terraform/check_dashboards_modified_remotely.go index 6f3a3b23b5..276fd77659 100644 --- a/bundle/deploy/terraform/check_dashboards_modified_remotely.go +++ b/bundle/deploy/terraform/check_dashboards_modified_remotely.go @@ -121,8 +121,8 @@ func (l *checkDashboardsModifiedRemotely) Apply(ctx context.Context, b *bundle.B "Make sure that the local dashboard definition matches what you intend to deploy\n" + "before proceeding with the deployment.\n" + "\n" + - "Run `databricks bundle deploy --force` to bypass this error." + - "", + "Use --force only if you want to discard the remote changes and overwrite\n" + + "the dashboard with your local version.", Paths: []dyn.Path{path}, Locations: []dyn.Location{loc}, }) diff --git a/bundle/deploy/terraform/import.go b/bundle/deploy/terraform/import.go index 8fdfe1212d..ad09bc19f3 100644 --- a/bundle/deploy/terraform/import.go +++ b/bundle/deploy/terraform/import.go @@ -72,7 +72,7 @@ func (m *importResource) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagn cmdio.LogString(ctx, output) if !cmdio.IsPromptSupported(ctx) { - return diag.Errorf("This bind operation requires user confirmation, but the current console does not support prompting. Please specify --auto-approve if you would like to skip prompts and proceed.") + return diag.Errorf("This bind operation requires user confirmation, but the current console does not support prompting.\nUsing --auto-approve will bind resources without reviewing the changes.\nOnly use --auto-approve if you have reviewed what will be bound.") } ans, err := cmdio.AskYesOrNo(ctx, "Confirm import changes? Changes will be remotely applied only after running 'bundle deploy'.") diff --git a/bundle/phases/bind.go b/bundle/phases/bind.go index a8f99b28e8..1c60912607 100644 --- a/bundle/phases/bind.go +++ b/bundle/phases/bind.go @@ -73,7 +73,7 @@ func Bind(ctx context.Context, b *bundle.Bundle, opts *terraform.BindOptions) { if !cmdio.IsPromptSupported(ctx) { result.Cancel() - logdiag.LogError(ctx, errors.New("This bind operation requires user confirmation, but the current console does not support prompting. Please specify --auto-approve if you would like to skip prompts and proceed.")) //nolint + logdiag.LogError(ctx, errors.New("This bind operation requires user confirmation, but the current console does not support prompting.\nUsing --auto-approve will bind resources without reviewing the changes.\nOnly use --auto-approve if you have reviewed what will be bound.")) //nolint return } diff --git a/bundle/phases/deploy.go b/bundle/phases/deploy.go index 4613a7a211..bb6b42fb06 100644 --- a/bundle/phases/deploy.go +++ b/bundle/phases/deploy.go @@ -85,7 +85,7 @@ func approvalForDeploy(ctx context.Context, b *bundle.Bundle, plan *deployplan.P } if !cmdio.IsPromptSupported(ctx) { - return false, errors.New("the deployment requires destructive actions, but current console does not support prompting. Please specify --auto-approve if you would like to skip prompts and proceed") + return false, errors.New("the deployment requires destructive actions (resources will be deleted), but the current console does not support prompting.\nUsing --auto-approve will skip all confirmation prompts and proceed with the destructive changes.\nOnly use --auto-approve if you have reviewed the plan and accept the deletions") } cmdio.LogString(ctx, "") diff --git a/cmd/agent/agent.go b/cmd/agent/agent.go new file mode 100644 index 0000000000..b65fe08002 --- /dev/null +++ b/cmd/agent/agent.go @@ -0,0 +1,18 @@ +package agent + +import ( + "github.com/spf13/cobra" +) + +// New returns the agent command group. +func New() *cobra.Command { + cmd := &cobra.Command{ + Use: "agent", + Short: "Commands for AI agent integration", + Hidden: true, + } + + cmd.AddCommand(newConsentCommand()) + + return cmd +} diff --git a/cmd/agent/consent.go b/cmd/agent/consent.go new file mode 100644 index 0000000000..aaee1f5730 --- /dev/null +++ b/cmd/agent/consent.go @@ -0,0 +1,42 @@ +package agent + +import ( + "fmt" + + libagent "github.com/databricks/cli/libs/agent" + "github.com/databricks/cli/libs/cmdio" + "github.com/spf13/cobra" +) + +func newConsentCommand() *cobra.Command { + var operation string + var reason string + + cmd := &cobra.Command{ + Use: "consent", + Short: "Capture user consent for a gated operation", + Long: `Capture explicit user consent before performing a potentially destructive operation. + +AI agents are required to obtain user consent before using flags like --force-lock, +--auto-approve, or --force on deploy. This command creates a consent token that +must be passed via the DATABRICKS_CLI_AGENT_CONSENT environment variable. + +The consent token expires after 10 minutes.`, + RunE: func(cmd *cobra.Command, args []string) error { + tokenPath, err := libagent.CreateConsentToken(operation, reason) + if err != nil { + return err + } + + cmdio.LogString(cmd.Context(), fmt.Sprintf("%s=%s", libagent.ConsentEnvVar, tokenPath)) + return nil + }, + } + + cmd.Flags().StringVar(&operation, "operation", "", fmt.Sprintf("The operation to consent to: %v", libagent.ValidOperations)) + cmd.Flags().StringVar(&reason, "reason", "", "Why the user approved this operation (minimum 20 characters)") + _ = cmd.MarkFlagRequired("operation") + _ = cmd.MarkFlagRequired("reason") + + return cmd +} diff --git a/cmd/bundle/deploy.go b/cmd/bundle/deploy.go index 31ffe7090d..a3a040a55c 100644 --- a/cmd/bundle/deploy.go +++ b/cmd/bundle/deploy.go @@ -6,6 +6,7 @@ import ( "github.com/databricks/cli/bundle" "github.com/databricks/cli/cmd/bundle/utils" "github.com/databricks/cli/cmd/root" + "github.com/databricks/cli/libs/agent" "github.com/spf13/cobra" ) @@ -44,6 +45,10 @@ See https://docs.databricks.com/en/dev-tools/bundles/index.html for more informa cmd.Flags().MarkHidden("verbose") cmd.RunE = func(cmd *cobra.Command, args []string) error { + if err := agent.CheckConsentForFlags(cmd); err != nil { + return err + } + _, err := utils.ProcessBundle(cmd, utils.ProcessOptions{ InitFunc: func(b *bundle.Bundle) { b.Config.Bundle.Force = force diff --git a/cmd/bundle/deployment/bind.go b/cmd/bundle/deployment/bind.go index 4b72cdd9e6..de16af1f49 100644 --- a/cmd/bundle/deployment/bind.go +++ b/cmd/bundle/deployment/bind.go @@ -2,6 +2,7 @@ package deployment import ( "github.com/databricks/cli/cmd/root" + "github.com/databricks/cli/libs/agent" "github.com/databricks/cli/libs/cmdio" "github.com/spf13/cobra" ) @@ -50,6 +51,9 @@ Any manual changes made in the workspace UI may be overwritten on deployment.`, cmd.Flags().BoolVar(&forceLock, "force-lock", false, "Force acquisition of deployment lock.") cmd.RunE = func(cmd *cobra.Command, args []string) error { + if err := agent.CheckConsentForFlags(cmd); err != nil { + return err + } err := BindResource(cmd, args[0], args[1], autoApprove, forceLock, false) if err != nil { return err diff --git a/cmd/bundle/deployment/unbind.go b/cmd/bundle/deployment/unbind.go index e382a0d942..077e49f251 100644 --- a/cmd/bundle/deployment/unbind.go +++ b/cmd/bundle/deployment/unbind.go @@ -6,6 +6,7 @@ import ( "github.com/databricks/cli/bundle/phases" "github.com/databricks/cli/cmd/bundle/utils" "github.com/databricks/cli/cmd/root" + "github.com/databricks/cli/libs/agent" "github.com/databricks/cli/libs/logdiag" "github.com/spf13/cobra" ) @@ -52,6 +53,9 @@ To re-bind the resource later, use: cmd.Flags().BoolVar(&forceLock, "force-lock", false, "Force acquisition of deployment lock.") cmd.RunE = func(cmd *cobra.Command, args []string) error { + if err := agent.CheckConsentForFlags(cmd); err != nil { + return err + } b, err := utils.ProcessBundle(cmd, utils.ProcessOptions{ InitFunc: func(b *bundle.Bundle) { b.Config.Bundle.Deployment.Lock.Force = forceLock diff --git a/cmd/bundle/destroy.go b/cmd/bundle/destroy.go index bd9b65f71c..c29ca25a67 100644 --- a/cmd/bundle/destroy.go +++ b/cmd/bundle/destroy.go @@ -9,6 +9,7 @@ import ( "github.com/databricks/cli/bundle/phases" "github.com/databricks/cli/cmd/bundle/utils" "github.com/databricks/cli/cmd/root" + "github.com/databricks/cli/libs/agent" "github.com/databricks/cli/libs/cmdio" "github.com/databricks/cli/libs/logdiag" "github.com/spf13/cobra" @@ -39,6 +40,9 @@ Typical use cases: cmd.Flags().BoolVar(&forceDestroy, "force-lock", false, "Force acquisition of deployment lock.") cmd.RunE = func(cmd *cobra.Command, args []string) error { + if err := agent.CheckConsentForFlags(cmd); err != nil { + return err + } return CommandBundleDestroy(cmd, args, autoApprove, forceDestroy) } @@ -48,7 +52,7 @@ Typical use cases: func CommandBundleDestroy(cmd *cobra.Command, args []string, autoApprove, forceDestroy bool) error { // We require auto-approve for non-interactive terminals since prompts are not possible. if !cmdio.IsPromptSupported(cmd.Context()) && !autoApprove { - return errors.New("please specify --auto-approve since terminal does not support interactive prompts") + return errors.New("this will permanently destroy all deployed resources in the target.\nUsing --auto-approve will skip all confirmation prompts and proceed with destruction.\nOnly use --auto-approve if you are certain you want to delete all resources") } // Check if context is already initialized (e.g., when called from apps delete override) diff --git a/cmd/cmd.go b/cmd/cmd.go index 014471f763..8b33b71f99 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -8,6 +8,7 @@ import ( ssh "github.com/databricks/cli/experimental/ssh/cmd" "github.com/databricks/cli/cmd/account" + agentcmd "github.com/databricks/cli/cmd/agent" "github.com/databricks/cli/cmd/api" "github.com/databricks/cli/cmd/auth" "github.com/databricks/cli/cmd/bundle" @@ -93,6 +94,7 @@ func New(ctx context.Context) *cobra.Command { } // Add other subcommands. + cli.AddCommand(agentcmd.New()) cli.AddCommand(api.New()) cli.AddCommand(auth.New()) cli.AddCommand(completion.New()) diff --git a/cmd/pipelines/deploy.go b/cmd/pipelines/deploy.go index d966a962d3..3656a85e10 100644 --- a/cmd/pipelines/deploy.go +++ b/cmd/pipelines/deploy.go @@ -9,6 +9,7 @@ import ( "github.com/databricks/cli/bundle/config/mutator" "github.com/databricks/cli/cmd/bundle/utils" "github.com/databricks/cli/cmd/root" + "github.com/databricks/cli/libs/agent" "github.com/databricks/cli/libs/cmdio" "github.com/databricks/cli/libs/logdiag" libsutils "github.com/databricks/cli/libs/utils" @@ -35,6 +36,9 @@ func deployCommand() *cobra.Command { cmd.Flags().MarkHidden("verbose") cmd.RunE = func(cmd *cobra.Command, args []string) error { + if err := agent.CheckConsentForFlags(cmd); err != nil { + return err + } b, err := utils.ProcessBundle(cmd, utils.ProcessOptions{ InitFunc: func(b *bundle.Bundle) { b.Config.Bundle.Deployment.Lock.Force = forceLock diff --git a/cmd/pipelines/destroy.go b/cmd/pipelines/destroy.go index 9f98d525fa..99e7f59a18 100644 --- a/cmd/pipelines/destroy.go +++ b/cmd/pipelines/destroy.go @@ -4,6 +4,7 @@ package pipelines import ( "github.com/databricks/cli/cmd/bundle" + "github.com/databricks/cli/libs/agent" "github.com/spf13/cobra" ) @@ -20,6 +21,9 @@ func destroyCommand() *cobra.Command { cmd.Flags().BoolVar(&forceDestroy, "force-lock", false, "Force acquisition of deployment lock.") cmd.RunE = func(cmd *cobra.Command, args []string) error { + if err := agent.CheckConsentForFlags(cmd); err != nil { + return err + } return bundle.CommandBundleDestroy(cmd, args, autoApprove, forceDestroy) } diff --git a/integration/libs/locker/locker_test.go b/integration/libs/locker/locker_test.go index 3ae80f8e71..a480216354 100644 --- a/integration/libs/locker/locker_test.go +++ b/integration/libs/locker/locker_test.go @@ -59,7 +59,7 @@ func TestLock(t *testing.T) { indexOfAnInactiveLocker = i } assert.ErrorContains(t, lockerErrs[i], "lock acquired by") - assert.ErrorContains(t, lockerErrs[i], "Use --force-lock to override") + assert.ErrorContains(t, lockerErrs[i], "Only use --force-lock if you are certain") } } assert.Equal(t, 1, countActive, "Exactly one locker should successfull acquire the lock") diff --git a/libs/agent/check_flags.go b/libs/agent/check_flags.go new file mode 100644 index 0000000000..5eb418b920 --- /dev/null +++ b/libs/agent/check_flags.go @@ -0,0 +1,36 @@ +package agent + +import ( + "github.com/spf13/cobra" +) + +// CheckConsentForFlags validates agent consent for gated flags that have been +// explicitly set on the command. Returns nil if no agent is detected or if +// no gated flags are set. +func CheckConsentForFlags(cmd *cobra.Command) error { + ctx := cmd.Context() + + // Non-agent callers are not gated. + if Product(ctx) == "" { + return nil + } + + // Map of flag names to the consent operation they require. + flagOps := map[string]string{ + "force-lock": OperationForceLock, + "auto-approve": OperationAutoApprove, + "force": OperationForceDeploy, + } + + for flagName, operation := range flagOps { + f := cmd.Flag(flagName) + if f == nil || !f.Changed { + continue + } + if err := ValidateConsent(ctx, operation); err != nil { + return err + } + } + + return nil +} diff --git a/libs/agent/check_flags_test.go b/libs/agent/check_flags_test.go new file mode 100644 index 0000000000..15da08f6a3 --- /dev/null +++ b/libs/agent/check_flags_test.go @@ -0,0 +1,89 @@ +package agent_test + +import ( + "os" + "testing" + + "github.com/databricks/cli/libs/agent" + "github.com/databricks/cli/libs/env" + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func newTestCommand(ctx interface{ Context() interface{ Done() <-chan struct{} } }) *cobra.Command { + // Build a minimal cobra command with the gated flags. + cmd := &cobra.Command{Use: "test"} + cmd.Flags().Bool("force", false, "") + cmd.Flags().Bool("force-lock", false, "") + cmd.Flags().Bool("auto-approve", false, "") + return cmd +} + +func TestCheckConsentForFlagsNoAgent(t *testing.T) { + ctx := agent.Mock(t.Context(), "") + cmd := &cobra.Command{Use: "test"} + cmd.Flags().Bool("force-lock", false, "") + cmd.SetContext(ctx) + _ = cmd.Flags().Set("force-lock", "true") + + assert.NoError(t, agent.CheckConsentForFlags(cmd)) +} + +func TestCheckConsentForFlagsNoGatedFlagsSet(t *testing.T) { + ctx := agent.Mock(t.Context(), agent.ClaudeCode) + cmd := &cobra.Command{Use: "test"} + cmd.Flags().Bool("force-lock", false, "") + cmd.SetContext(ctx) + + assert.NoError(t, agent.CheckConsentForFlags(cmd)) +} + +func TestCheckConsentForFlagsBlocksWithoutConsent(t *testing.T) { + ctx := agent.Mock(t.Context(), agent.ClaudeCode) + cmd := &cobra.Command{Use: "test"} + cmd.Flags().Bool("force-lock", false, "") + cmd.SetContext(ctx) + _ = cmd.Flags().Set("force-lock", "true") + + err := agent.CheckConsentForFlags(cmd) + assert.Error(t, err) + assert.Contains(t, err.Error(), "explicit user consent") +} + +func TestCheckConsentForFlagsAllowsWithValidToken(t *testing.T) { + path, err := agent.CreateConsentToken(agent.OperationAutoApprove, "user reviewed and approved the destructive changes") + require.NoError(t, err) + t.Cleanup(func() { os.Remove(path) }) + + ctx := agent.Mock(t.Context(), agent.Cursor) + ctx = env.Set(ctx, agent.ConsentEnvVar, path) + + cmd := &cobra.Command{Use: "test"} + cmd.Flags().Bool("auto-approve", false, "") + cmd.SetContext(ctx) + _ = cmd.Flags().Set("auto-approve", "true") + + assert.NoError(t, agent.CheckConsentForFlags(cmd)) +} + +func TestCheckConsentForFlagsMultipleFlags(t *testing.T) { + // Token only covers force-lock, not auto-approve. + path, err := agent.CreateConsentToken(agent.OperationForceLock, "user confirmed the other deploy is stale") + require.NoError(t, err) + t.Cleanup(func() { os.Remove(path) }) + + ctx := agent.Mock(t.Context(), agent.ClaudeCode) + ctx = env.Set(ctx, agent.ConsentEnvVar, path) + + cmd := &cobra.Command{Use: "test"} + cmd.Flags().Bool("force-lock", false, "") + cmd.Flags().Bool("auto-approve", false, "") + cmd.SetContext(ctx) + _ = cmd.Flags().Set("force-lock", "true") + _ = cmd.Flags().Set("auto-approve", "true") + + err = agent.CheckConsentForFlags(cmd) + assert.Error(t, err) + assert.Contains(t, err.Error(), "consent token is for") +} diff --git a/libs/agent/consent.go b/libs/agent/consent.go new file mode 100644 index 0000000000..4782ab49f9 --- /dev/null +++ b/libs/agent/consent.go @@ -0,0 +1,187 @@ +package agent + +import ( + "context" + "crypto/rand" + "encoding/hex" + "fmt" + "os" + "path/filepath" + "strings" + "time" + + "github.com/databricks/cli/libs/env" +) + +// ConsentEnvVar is the environment variable that agents must set to a valid +// consent token path before using gated flags like --force-lock or --auto-approve. +const ConsentEnvVar = "DATABRICKS_CLI_AGENT_CONSENT" + +// consentTokenDir is the directory where consent tokens are stored. +const consentTokenDir = "databricks-agent-consent" + +// consentTokenExpiry is how long a consent token remains valid. +const consentTokenExpiry = 10 * time.Minute + +// minReasonLength is the minimum length for consent reasons to prevent +// agents from using trivial reasons like "yes" or "ok". +const minReasonLength = 20 + +// Operations that can be consented to. +const ( + OperationForceLock = "force-lock" + OperationAutoApprove = "auto-approve" + OperationForceDeploy = "force-deploy" +) + +// ValidOperations lists all valid consent operations. +var ValidOperations = []string{ + OperationForceLock, + OperationAutoApprove, + OperationForceDeploy, +} + +// ConsentToken represents a validated consent token. +type ConsentToken struct { + Operation string + Reason string + CreatedAt time.Time +} + +// CreateConsentToken writes a consent token file and returns its path. +func CreateConsentToken(operation, reason string) (string, error) { + if err := validateOperation(operation); err != nil { + return "", err + } + if len(reason) < minReasonLength { + return "", fmt.Errorf("consent reason must be at least %d characters to ensure meaningful justification, got %d", minReasonLength, len(reason)) + } + + dir := filepath.Join(os.TempDir(), consentTokenDir) + if err := os.MkdirAll(dir, 0o700); err != nil { + return "", fmt.Errorf("failed to create consent token directory: %w", err) + } + + // Clean up expired tokens on each creation. + cleanExpiredTokens(dir) + + tokenID := make([]byte, 16) + _, err := rand.Read(tokenID) + if err != nil { + return "", fmt.Errorf("failed to generate token ID: %w", err) + } + + filename := fmt.Sprintf("consent-%s-%s", operation, hex.EncodeToString(tokenID)) + tokenPath := filepath.Join(dir, filename) + + content := fmt.Sprintf("operation: %s\nreason: %s\ncreated: %s\n", + operation, reason, time.Now().UTC().Format(time.RFC3339)) + + if err := os.WriteFile(tokenPath, []byte(content), 0o600); err != nil { + return "", fmt.Errorf("failed to write consent token: %w", err) + } + + return tokenPath, nil +} + +// ValidateConsent checks whether the agent has valid consent for the given operation. +// It reads the token path from the DATABRICKS_CLI_AGENT_CONSENT environment variable. +// Returns nil if no agent is detected (non-agent callers are not gated). +// Returns an error if agent is detected but consent is missing or invalid. +func ValidateConsent(ctx context.Context, operation string) error { + // Non-agent callers are not gated. + if Product(ctx) == "" { + return nil + } + + tokenPath := env.Get(ctx, ConsentEnvVar) + if tokenPath == "" { + return &ConsentRequiredError{Operation: operation} + } + + token, err := readConsentToken(tokenPath) + if err != nil { + return fmt.Errorf("invalid agent consent token: %w", err) + } + + if time.Since(token.CreatedAt) > consentTokenExpiry { + return fmt.Errorf("agent consent token has expired (created %s ago, max %s). Run `databricks agent consent` again", + time.Since(token.CreatedAt).Round(time.Second), consentTokenExpiry) + } + + if token.Operation != operation { + return fmt.Errorf("agent consent token is for %q, but %q is required", token.Operation, operation) + } + + return nil +} + +// ConsentRequiredError is returned when an agent attempts a gated operation +// without providing consent. +type ConsentRequiredError struct { + Operation string +} + +func (e *ConsentRequiredError) Error() string { + return fmt.Sprintf( + "this operation requires explicit user consent when run by an AI agent.\n\n"+ + "AI agents must not automatically retry with this flag. Instead:\n"+ + "1. Present this error to the user and explain what the flag does\n"+ + "2. If the user approves, run: databricks agent consent --operation %s --reason \"\"\n"+ + "3. Set the %s environment variable to the output token path\n"+ + "4. Retry the command", + e.Operation, ConsentEnvVar) +} + +func readConsentToken(path string) (*ConsentToken, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("cannot read token file %s: %w", path, err) + } + + token := &ConsentToken{} + for _, line := range strings.Split(string(data), "\n") { + if strings.HasPrefix(line, "operation: ") { + token.Operation = strings.TrimPrefix(line, "operation: ") + } else if strings.HasPrefix(line, "reason: ") { + token.Reason = strings.TrimPrefix(line, "reason: ") + } else if strings.HasPrefix(line, "created: ") { + t, err := time.Parse(time.RFC3339, strings.TrimPrefix(line, "created: ")) + if err != nil { + return nil, fmt.Errorf("invalid timestamp in token: %w", err) + } + token.CreatedAt = t + } + } + + if token.Operation == "" { + return nil, fmt.Errorf("token file is missing operation field") + } + + return token, nil +} + +func validateOperation(operation string) error { + for _, op := range ValidOperations { + if op == operation { + return nil + } + } + return fmt.Errorf("invalid operation %q, must be one of: %s", operation, strings.Join(ValidOperations, ", ")) +} + +func cleanExpiredTokens(dir string) { + entries, err := os.ReadDir(dir) + if err != nil { + return + } + for _, entry := range entries { + info, err := entry.Info() + if err != nil { + continue + } + if time.Since(info.ModTime()) > consentTokenExpiry*2 { + os.Remove(filepath.Join(dir, entry.Name())) + } + } +} diff --git a/libs/agent/consent_test.go b/libs/agent/consent_test.go new file mode 100644 index 0000000000..d87276a80a --- /dev/null +++ b/libs/agent/consent_test.go @@ -0,0 +1,118 @@ +package agent + +import ( + "os" + "path/filepath" + "testing" + "time" + + "github.com/databricks/cli/libs/env" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCreateConsentTokenSuccess(t *testing.T) { + path, err := CreateConsentToken(OperationForceLock, "user confirmed the other deploy is stale and can be overridden") + require.NoError(t, err) + t.Cleanup(func() { os.Remove(path) }) + + assert.FileExists(t, path) + + token, err := readConsentToken(path) + require.NoError(t, err) + assert.Equal(t, OperationForceLock, token.Operation) + assert.Contains(t, token.Reason, "stale") + assert.WithinDuration(t, time.Now(), token.CreatedAt, 5*time.Second) +} + +func TestCreateConsentTokenInvalidOperation(t *testing.T) { + _, err := CreateConsentToken("nuke-everything", "user said go ahead") + assert.ErrorContains(t, err, "invalid operation") +} + +func TestCreateConsentTokenReasonTooShort(t *testing.T) { + _, err := CreateConsentToken(OperationAutoApprove, "yes") + assert.ErrorContains(t, err, "at least 20 characters") +} + +func TestValidateConsentNoAgent(t *testing.T) { + ctx := Mock(t.Context(), "") + err := ValidateConsent(ctx, OperationForceLock) + assert.NoError(t, err) +} + +func TestValidateConsentMissingToken(t *testing.T) { + ctx := Mock(t.Context(), ClaudeCode) + err := ValidateConsent(ctx, OperationForceLock) + + var consentErr *ConsentRequiredError + assert.ErrorAs(t, err, &consentErr) + assert.Equal(t, OperationForceLock, consentErr.Operation) + assert.Contains(t, err.Error(), "explicit user consent") +} + +func TestValidateConsentValidToken(t *testing.T) { + path, err := CreateConsentToken(OperationAutoApprove, "user reviewed the plan and approved resource deletions") + require.NoError(t, err) + t.Cleanup(func() { os.Remove(path) }) + + ctx := Mock(t.Context(), Cursor) + ctx = env.Set(ctx, ConsentEnvVar, path) + assert.NoError(t, ValidateConsent(ctx, OperationAutoApprove)) +} + +func TestValidateConsentWrongOperation(t *testing.T) { + path, err := CreateConsentToken(OperationForceLock, "user confirmed the lock can be overridden safely") + require.NoError(t, err) + t.Cleanup(func() { os.Remove(path) }) + + ctx := Mock(t.Context(), ClaudeCode) + ctx = env.Set(ctx, ConsentEnvVar, path) + err = ValidateConsent(ctx, OperationAutoApprove) + assert.ErrorContains(t, err, "consent token is for") +} + +func TestValidateConsentExpiredToken(t *testing.T) { + path, err := CreateConsentToken(OperationForceDeploy, "user approved force deploy to override dashboard") + require.NoError(t, err) + t.Cleanup(func() { os.Remove(path) }) + + // Backdate the token file content. + data, err := os.ReadFile(path) + require.NoError(t, err) + old := time.Now().Add(-15 * time.Minute).UTC().Format(time.RFC3339) + content := "operation: " + OperationForceDeploy + "\nreason: test\ncreated: " + old + "\n" + require.NoError(t, os.WriteFile(path, []byte(content), 0o600)) + _ = data + + ctx := Mock(t.Context(), Codex) + ctx = env.Set(ctx, ConsentEnvVar, path) + err = ValidateConsent(ctx, OperationForceDeploy) + assert.ErrorContains(t, err, "expired") +} + +func TestValidateConsentInvalidPath(t *testing.T) { + ctx := Mock(t.Context(), ClaudeCode) + ctx = env.Set(ctx, ConsentEnvVar, "/nonexistent/token") + err := ValidateConsent(ctx, OperationForceLock) + assert.ErrorContains(t, err, "invalid agent consent token") +} + +func TestCleanExpiredTokens(t *testing.T) { + dir := t.TempDir() + + // Create an old file. + oldFile := filepath.Join(dir, "consent-old") + require.NoError(t, os.WriteFile(oldFile, []byte("old"), 0o600)) + oldTime := time.Now().Add(-25 * time.Minute) + require.NoError(t, os.Chtimes(oldFile, oldTime, oldTime)) + + // Create a recent file. + newFile := filepath.Join(dir, "consent-new") + require.NoError(t, os.WriteFile(newFile, []byte("new"), 0o600)) + + cleanExpiredTokens(dir) + + assert.NoFileExists(t, oldFile) + assert.FileExists(t, newFile) +} diff --git a/libs/locker/locker.go b/libs/locker/locker.go index 003f169cd3..253c6b3aa7 100644 --- a/libs/locker/locker.go +++ b/libs/locker/locker.go @@ -105,10 +105,16 @@ func (locker *Locker) assertLockHeld(ctx context.Context) error { return err } if activeLockState.ID != locker.State.ID && !activeLockState.IsForced { - return fmt.Errorf("deploy lock acquired by %s at %v. Use --force-lock to override", activeLockState.User, activeLockState.AcquisitionTime) + return fmt.Errorf("deploy lock acquired by %s at %v.\n"+ + "Another deployment is likely in progress. Overriding with --force-lock risks corrupting that deployment.\n"+ + "Only use --force-lock if you are certain the other deployment is no longer active", + activeLockState.User, activeLockState.AcquisitionTime) } if activeLockState.ID != locker.State.ID && activeLockState.IsForced { - return fmt.Errorf("deploy lock force acquired by %s at %v. Use --force-lock to override", activeLockState.User, activeLockState.AcquisitionTime) + return fmt.Errorf("deploy lock force-acquired by %s at %v.\n"+ + "Another deployment is likely in progress. Overriding with --force-lock risks corrupting that deployment.\n"+ + "Only use --force-lock if you are certain the other deployment is no longer active", + activeLockState.User, activeLockState.AcquisitionTime) } return nil } From 0101047f16cdf12a5571ca53c055d51d003520ba Mon Sep 17 00:00:00 2001 From: Lennart Kats Date: Fri, 3 Apr 2026 10:59:25 +0200 Subject: [PATCH 02/13] Revert "WIP: agent consent token system (original approach)" This reverts commit e70997b344563dda9911dc7bd497afc413c51a99. --- .../bind/dashboard/recreation/output.txt | 4 +- .../bind/job/job-abort-bind/output.txt | 4 +- .../recreate/out.bind-fail.direct.txt | 4 +- .../recreate/out.bind-fail.terraform.txt | 4 +- .../bind/pipelines/recreate/output.txt | 4 +- .../pipelines/update/out.bind-fail.direct.txt | 4 +- .../update/out.bind-fail.terraform.txt | 4 +- .../pipelines/auto-approve/output.txt | 4 +- .../resources/pipelines/recreate/output.txt | 4 +- .../resources/schemas/auto-approve/output.txt | 4 +- .../resources/volumes/recreate/output.txt | 4 +- .../pipelines/deploy/auto-approve/output.txt | 4 +- .../pipelines/deploy/force-lock/output.txt | 4 +- .../pipelines/destroy/auto-approve/output.txt | 4 +- .../pipelines/destroy/force-lock/output.txt | 4 +- bundle/config/mutator/validate_git_details.go | 3 +- .../mutator/validate_git_details_test.go | 6 +- .../check_dashboards_modified_remotely.go | 4 +- bundle/deploy/terraform/import.go | 2 +- bundle/phases/bind.go | 2 +- bundle/phases/deploy.go | 2 +- cmd/agent/agent.go | 18 -- cmd/agent/consent.go | 42 ---- cmd/bundle/deploy.go | 5 - cmd/bundle/deployment/bind.go | 4 - cmd/bundle/deployment/unbind.go | 4 - cmd/bundle/destroy.go | 6 +- cmd/cmd.go | 2 - cmd/pipelines/deploy.go | 4 - cmd/pipelines/destroy.go | 4 - integration/libs/locker/locker_test.go | 2 +- libs/agent/check_flags.go | 36 ---- libs/agent/check_flags_test.go | 89 --------- libs/agent/consent.go | 187 ------------------ libs/agent/consent_test.go | 118 ----------- libs/locker/locker.go | 10 +- 36 files changed, 29 insertions(+), 581 deletions(-) delete mode 100644 cmd/agent/agent.go delete mode 100644 cmd/agent/consent.go delete mode 100644 libs/agent/check_flags.go delete mode 100644 libs/agent/check_flags_test.go delete mode 100644 libs/agent/consent.go delete mode 100644 libs/agent/consent_test.go diff --git a/acceptance/bundle/deployment/bind/dashboard/recreation/output.txt b/acceptance/bundle/deployment/bind/dashboard/recreation/output.txt index 5e83560c7d..99c26a8ccc 100644 --- a/acceptance/bundle/deployment/bind/dashboard/recreation/output.txt +++ b/acceptance/bundle/deployment/bind/dashboard/recreation/output.txt @@ -18,9 +18,7 @@ Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bundle-[UNIQU This action will result in the deletion or recreation of the following dashboards. This will result in changed IDs and permanent URLs of the dashboards that will be recreated: recreate resources.dashboards.dashboard1 -Error: the deployment requires destructive actions (resources will be deleted), but the current console does not support prompting. -Using --auto-approve will skip all confirmation prompts and proceed with the destructive changes. -Only use --auto-approve if you have reviewed the plan and accept the deletions +Error: the deployment requires destructive actions, but current console does not support prompting. Please specify --auto-approve if you would like to skip prompts and proceed Exit code: 1 diff --git a/acceptance/bundle/deployment/bind/job/job-abort-bind/output.txt b/acceptance/bundle/deployment/bind/job/job-abort-bind/output.txt index 219d45d915..544448efbe 100644 --- a/acceptance/bundle/deployment/bind/job/job-abort-bind/output.txt +++ b/acceptance/bundle/deployment/bind/job/job-abort-bind/output.txt @@ -3,9 +3,7 @@ Created job with ID: [JOB_ID] === Expect binding to fail without an auto-approve flag: -Error: This bind operation requires user confirmation, but the current console does not support prompting. -Using --auto-approve will bind resources without reviewing the changes. -Only use --auto-approve if you have reviewed what will be bound. +Error: This bind operation requires user confirmation, but the current console does not support prompting. Please specify --auto-approve if you would like to skip prompts and proceed. === Deploy bundle: >>> [CLI] bundle deploy --force-lock diff --git a/acceptance/bundle/deployment/bind/pipelines/recreate/out.bind-fail.direct.txt b/acceptance/bundle/deployment/bind/pipelines/recreate/out.bind-fail.direct.txt index 0ee95b0069..c5e00b49cb 100644 --- a/acceptance/bundle/deployment/bind/pipelines/recreate/out.bind-fail.direct.txt +++ b/acceptance/bundle/deployment/bind/pipelines/recreate/out.bind-fail.direct.txt @@ -11,7 +11,5 @@ Changes detected: ~ root_path: "/Workspace/Users/someuser@databricks.com/lakeflow_pipeline" -> null ~ storage: "/Shared/old_storage" -> "/Shared/new_storage" -Error: This bind operation requires user confirmation, but the current console does not support prompting. -Using --auto-approve will bind resources without reviewing the changes. -Only use --auto-approve if you have reviewed what will be bound. +Error: This bind operation requires user confirmation, but the current console does not support prompting. Please specify --auto-approve if you would like to skip prompts and proceed. diff --git a/acceptance/bundle/deployment/bind/pipelines/recreate/out.bind-fail.terraform.txt b/acceptance/bundle/deployment/bind/pipelines/recreate/out.bind-fail.terraform.txt index a5dd712d34..1d300160a9 100644 --- a/acceptance/bundle/deployment/bind/pipelines/recreate/out.bind-fail.terraform.txt +++ b/acceptance/bundle/deployment/bind/pipelines/recreate/out.bind-fail.terraform.txt @@ -54,7 +54,5 @@ Terraform will perform the following actions: Plan: 1 to add, 0 to change, 1 to destroy. -Error: This bind operation requires user confirmation, but the current console does not support prompting. -Using --auto-approve will bind resources without reviewing the changes. -Only use --auto-approve if you have reviewed what will be bound. +Error: This bind operation requires user confirmation, but the current console does not support prompting. Please specify --auto-approve if you would like to skip prompts and proceed. diff --git a/acceptance/bundle/deployment/bind/pipelines/recreate/output.txt b/acceptance/bundle/deployment/bind/pipelines/recreate/output.txt index 9f282a85d7..af42322dee 100644 --- a/acceptance/bundle/deployment/bind/pipelines/recreate/output.txt +++ b/acceptance/bundle/deployment/bind/pipelines/recreate/output.txt @@ -16,9 +16,7 @@ Streaming Tables (STs) and Materialized Views (MVs) managed by them. Recreating restore the defined STs and MVs through full refresh. Note that recreation is necessary when pipeline properties such as the 'catalog' or 'storage' are changed: recreate resources.pipelines.foo -Error: the deployment requires destructive actions (resources will be deleted), but the current console does not support prompting. -Using --auto-approve will skip all confirmation prompts and proceed with the destructive changes. -Only use --auto-approve if you have reviewed the plan and accept the deletions +Error: the deployment requires destructive actions, but current console does not support prompting. Please specify --auto-approve if you would like to skip prompts and proceed >>> [CLI] bundle deploy --auto-approve diff --git a/acceptance/bundle/deployment/bind/pipelines/update/out.bind-fail.direct.txt b/acceptance/bundle/deployment/bind/pipelines/update/out.bind-fail.direct.txt index d1a289868e..7b85803529 100644 --- a/acceptance/bundle/deployment/bind/pipelines/update/out.bind-fail.direct.txt +++ b/acceptance/bundle/deployment/bind/pipelines/update/out.bind-fail.direct.txt @@ -11,7 +11,5 @@ Changes detected: ~ name: "lakeflow-pipeline" -> "test-pipeline" ~ root_path: "/Workspace/Users/[USERNAME]/lakeflow_pipeline" -> null -Error: This bind operation requires user confirmation, but the current console does not support prompting. -Using --auto-approve will bind resources without reviewing the changes. -Only use --auto-approve if you have reviewed what will be bound. +Error: This bind operation requires user confirmation, but the current console does not support prompting. Please specify --auto-approve if you would like to skip prompts and proceed. diff --git a/acceptance/bundle/deployment/bind/pipelines/update/out.bind-fail.terraform.txt b/acceptance/bundle/deployment/bind/pipelines/update/out.bind-fail.terraform.txt index 14ebbe6df2..2ff60fce2b 100644 --- a/acceptance/bundle/deployment/bind/pipelines/update/out.bind-fail.terraform.txt +++ b/acceptance/bundle/deployment/bind/pipelines/update/out.bind-fail.terraform.txt @@ -41,7 +41,5 @@ Terraform will perform the following actions: Plan: 0 to add, 1 to change, 0 to destroy. -Error: This bind operation requires user confirmation, but the current console does not support prompting. -Using --auto-approve will bind resources without reviewing the changes. -Only use --auto-approve if you have reviewed what will be bound. +Error: This bind operation requires user confirmation, but the current console does not support prompting. Please specify --auto-approve if you would like to skip prompts and proceed. diff --git a/acceptance/bundle/resources/pipelines/auto-approve/output.txt b/acceptance/bundle/resources/pipelines/auto-approve/output.txt index d71a3f7a9c..5154a1206a 100644 --- a/acceptance/bundle/resources/pipelines/auto-approve/output.txt +++ b/acceptance/bundle/resources/pipelines/auto-approve/output.txt @@ -50,9 +50,7 @@ Streaming Tables (STs) and Materialized Views (MVs) managed by them. Recreating restore the defined STs and MVs through full refresh. Note that recreation is necessary when pipeline properties such as the 'catalog' or 'storage' are changed: delete resources.pipelines.bar -Error: the deployment requires destructive actions (resources will be deleted), but the current console does not support prompting. -Using --auto-approve will skip all confirmation prompts and proceed with the destructive changes. -Only use --auto-approve if you have reviewed the plan and accept the deletions +Error: the deployment requires destructive actions, but current console does not support prompting. Please specify --auto-approve if you would like to skip prompts and proceed Exit code: 1 diff --git a/acceptance/bundle/resources/pipelines/recreate/output.txt b/acceptance/bundle/resources/pipelines/recreate/output.txt index afb961b67b..8550395ff2 100644 --- a/acceptance/bundle/resources/pipelines/recreate/output.txt +++ b/acceptance/bundle/resources/pipelines/recreate/output.txt @@ -49,9 +49,7 @@ Streaming Tables (STs) and Materialized Views (MVs) managed by them. Recreating restore the defined STs and MVs through full refresh. Note that recreation is necessary when pipeline properties such as the 'catalog' or 'storage' are changed: recreate resources.pipelines.foo -Error: the deployment requires destructive actions (resources will be deleted), but the current console does not support prompting. -Using --auto-approve will skip all confirmation prompts and proceed with the destructive changes. -Only use --auto-approve if you have reviewed the plan and accept the deletions +Error: the deployment requires destructive actions, but current console does not support prompting. Please specify --auto-approve if you would like to skip prompts and proceed Exit code: 1 diff --git a/acceptance/bundle/resources/schemas/auto-approve/output.txt b/acceptance/bundle/resources/schemas/auto-approve/output.txt index f2a4563c10..6a773f60ca 100644 --- a/acceptance/bundle/resources/schemas/auto-approve/output.txt +++ b/acceptance/bundle/resources/schemas/auto-approve/output.txt @@ -57,9 +57,7 @@ Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/[UNIQUE_NAME]/file This action will result in the deletion or recreation of the following UC schemas. Any underlying data may be lost: delete resources.schemas.bar -Error: the deployment requires destructive actions (resources will be deleted), but the current console does not support prompting. -Using --auto-approve will skip all confirmation prompts and proceed with the destructive changes. -Only use --auto-approve if you have reviewed the plan and accept the deletions +Error: the deployment requires destructive actions, but current console does not support prompting. Please specify --auto-approve if you would like to skip prompts and proceed === Test cleanup diff --git a/acceptance/bundle/resources/volumes/recreate/output.txt b/acceptance/bundle/resources/volumes/recreate/output.txt index 6d2625fa5f..494e9ff538 100644 --- a/acceptance/bundle/resources/volumes/recreate/output.txt +++ b/acceptance/bundle/resources/volumes/recreate/output.txt @@ -45,9 +45,7 @@ For managed volumes, the files stored in the volume are also deleted from your cloud tenant within 30 days. For external volumes, the metadata about the volume is removed from the catalog, but the underlying files are not deleted: recreate resources.volumes.foo -Error: the deployment requires destructive actions (resources will be deleted), but the current console does not support prompting. -Using --auto-approve will skip all confirmation prompts and proceed with the destructive changes. -Only use --auto-approve if you have reviewed the plan and accept the deletions +Error: the deployment requires destructive actions, but current console does not support prompting. Please specify --auto-approve if you would like to skip prompts and proceed Exit code: 1 diff --git a/acceptance/pipelines/deploy/auto-approve/output.txt b/acceptance/pipelines/deploy/auto-approve/output.txt index 53066ab958..ff5ac099bf 100644 --- a/acceptance/pipelines/deploy/auto-approve/output.txt +++ b/acceptance/pipelines/deploy/auto-approve/output.txt @@ -18,9 +18,7 @@ Streaming Tables (STs) and Materialized Views (MVs) managed by them. Recreating restore the defined STs and MVs through full refresh. Note that recreation is necessary when pipeline properties such as the 'catalog' or 'storage' are changed: delete resources.pipelines.foo -Error: the deployment requires destructive actions (resources will be deleted), but the current console does not support prompting. -Using --auto-approve will skip all confirmation prompts and proceed with the destructive changes. -Only use --auto-approve if you have reviewed the plan and accept the deletions +Error: the deployment requires destructive actions, but current console does not support prompting. Please specify --auto-approve if you would like to skip prompts and proceed Exit code: 1 diff --git a/acceptance/pipelines/deploy/force-lock/output.txt b/acceptance/pipelines/deploy/force-lock/output.txt index 4c071717d8..e5fdadbaf9 100644 --- a/acceptance/pipelines/deploy/force-lock/output.txt +++ b/acceptance/pipelines/deploy/force-lock/output.txt @@ -4,8 +4,8 @@ === test deployment without force-lock (should fail) >>> errcode [CLI] pipelines deploy -Error: Failed to acquire deployment lock: deploy lock acquired by user-with-lock@databricks.com at [TIMESTAMP] +0000 UTC. Only use --force-lock if you are certain the other deployment is no longer active -Error: deploy lock acquired by user-with-lock@databricks.com at [TIMESTAMP] +0000 UTC. Only use --force-lock if you are certain the other deployment is no longer active +Error: Failed to acquire deployment lock: deploy lock acquired by user-with-lock@databricks.com at [TIMESTAMP] +0000 UTC. Use --force-lock to override +Error: deploy lock acquired by user-with-lock@databricks.com at [TIMESTAMP] +0000 UTC. Use --force-lock to override Exit code: 1 diff --git a/acceptance/pipelines/destroy/auto-approve/output.txt b/acceptance/pipelines/destroy/auto-approve/output.txt index ef990a1209..17aecc2059 100644 --- a/acceptance/pipelines/destroy/auto-approve/output.txt +++ b/acceptance/pipelines/destroy/auto-approve/output.txt @@ -8,9 +8,7 @@ Deployment complete! View your pipeline my_pipeline here: [DATABRICKS_URL]/pipelines/[UUID]?o=[NUMID] >>> errcode [CLI] pipelines destroy -Error: this will permanently destroy all deployed resources in the target. -Using --auto-approve will skip all confirmation prompts and proceed with destruction. -Only use --auto-approve if you are certain you want to delete all resources +Error: please specify --auto-approve since terminal does not support interactive prompts Exit code: 1 diff --git a/acceptance/pipelines/destroy/force-lock/output.txt b/acceptance/pipelines/destroy/force-lock/output.txt index ecea478fc3..5b6449f732 100644 --- a/acceptance/pipelines/destroy/force-lock/output.txt +++ b/acceptance/pipelines/destroy/force-lock/output.txt @@ -11,8 +11,8 @@ View your pipeline foo here: [DATABRICKS_URL]/pipelines/[UUID]?o=[NUMID] === test deployment without force-lock (should fail) >>> errcode [CLI] pipelines destroy --auto-approve -Error: Failed to acquire deployment lock: deploy lock acquired by user-with-lock@databricks.com at [TIMESTAMP] +0000 UTC. Only use --force-lock if you are certain the other deployment is no longer active -Error: deploy lock acquired by user-with-lock@databricks.com at [TIMESTAMP] +0000 UTC. Only use --force-lock if you are certain the other deployment is no longer active +Error: Failed to acquire deployment lock: deploy lock acquired by user-with-lock@databricks.com at [TIMESTAMP] +0000 UTC. Use --force-lock to override +Error: deploy lock acquired by user-with-lock@databricks.com at [TIMESTAMP] +0000 UTC. Use --force-lock to override Exit code: 1 diff --git a/bundle/config/mutator/validate_git_details.go b/bundle/config/mutator/validate_git_details.go index f7b413db7f..69a4221fdc 100644 --- a/bundle/config/mutator/validate_git_details.go +++ b/bundle/config/mutator/validate_git_details.go @@ -23,8 +23,7 @@ func (m *validateGitDetails) Apply(ctx context.Context, b *bundle.Bundle) diag.D } if b.Config.Bundle.Git.Branch != b.Config.Bundle.Git.ActualBranch && !b.Config.Bundle.Force { - return diag.Errorf("not on the right Git branch:\n expected according to configuration: %s\n actual: %s\nUsing --force will deploy from branch %s, which may push unexpected code to the target.\nOnly use --force if you intentionally want to deploy from this branch", - b.Config.Bundle.Git.Branch, b.Config.Bundle.Git.ActualBranch, b.Config.Bundle.Git.ActualBranch) + return diag.Errorf("not on the right Git branch:\n expected according to configuration: %s\n actual: %s\nuse --force to override", b.Config.Bundle.Git.Branch, b.Config.Bundle.Git.ActualBranch) } return nil } diff --git a/bundle/config/mutator/validate_git_details_test.go b/bundle/config/mutator/validate_git_details_test.go index 791c613c86..b29f221edc 100644 --- a/bundle/config/mutator/validate_git_details_test.go +++ b/bundle/config/mutator/validate_git_details_test.go @@ -40,10 +40,8 @@ func TestValidateGitDetailsNonMatchingBranches(t *testing.T) { m := ValidateGitDetails() diags := bundle.Apply(t.Context(), b, m) - assert.ErrorContains(t, diags.Error(), "not on the right Git branch") - assert.ErrorContains(t, diags.Error(), "expected according to configuration: main") - assert.ErrorContains(t, diags.Error(), "actual: feature") - assert.ErrorContains(t, diags.Error(), "Only use --force if you intentionally want to deploy from this branch") + expectedError := "not on the right Git branch:\n expected according to configuration: main\n actual: feature\nuse --force to override" + assert.EqualError(t, diags.Error(), expectedError) } func TestValidateGitDetailsNotUsingGit(t *testing.T) { diff --git a/bundle/deploy/terraform/check_dashboards_modified_remotely.go b/bundle/deploy/terraform/check_dashboards_modified_remotely.go index 276fd77659..6f3a3b23b5 100644 --- a/bundle/deploy/terraform/check_dashboards_modified_remotely.go +++ b/bundle/deploy/terraform/check_dashboards_modified_remotely.go @@ -121,8 +121,8 @@ func (l *checkDashboardsModifiedRemotely) Apply(ctx context.Context, b *bundle.B "Make sure that the local dashboard definition matches what you intend to deploy\n" + "before proceeding with the deployment.\n" + "\n" + - "Use --force only if you want to discard the remote changes and overwrite\n" + - "the dashboard with your local version.", + "Run `databricks bundle deploy --force` to bypass this error." + + "", Paths: []dyn.Path{path}, Locations: []dyn.Location{loc}, }) diff --git a/bundle/deploy/terraform/import.go b/bundle/deploy/terraform/import.go index ad09bc19f3..8fdfe1212d 100644 --- a/bundle/deploy/terraform/import.go +++ b/bundle/deploy/terraform/import.go @@ -72,7 +72,7 @@ func (m *importResource) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagn cmdio.LogString(ctx, output) if !cmdio.IsPromptSupported(ctx) { - return diag.Errorf("This bind operation requires user confirmation, but the current console does not support prompting.\nUsing --auto-approve will bind resources without reviewing the changes.\nOnly use --auto-approve if you have reviewed what will be bound.") + return diag.Errorf("This bind operation requires user confirmation, but the current console does not support prompting. Please specify --auto-approve if you would like to skip prompts and proceed.") } ans, err := cmdio.AskYesOrNo(ctx, "Confirm import changes? Changes will be remotely applied only after running 'bundle deploy'.") diff --git a/bundle/phases/bind.go b/bundle/phases/bind.go index 1c60912607..a8f99b28e8 100644 --- a/bundle/phases/bind.go +++ b/bundle/phases/bind.go @@ -73,7 +73,7 @@ func Bind(ctx context.Context, b *bundle.Bundle, opts *terraform.BindOptions) { if !cmdio.IsPromptSupported(ctx) { result.Cancel() - logdiag.LogError(ctx, errors.New("This bind operation requires user confirmation, but the current console does not support prompting.\nUsing --auto-approve will bind resources without reviewing the changes.\nOnly use --auto-approve if you have reviewed what will be bound.")) //nolint + logdiag.LogError(ctx, errors.New("This bind operation requires user confirmation, but the current console does not support prompting. Please specify --auto-approve if you would like to skip prompts and proceed.")) //nolint return } diff --git a/bundle/phases/deploy.go b/bundle/phases/deploy.go index bb6b42fb06..4613a7a211 100644 --- a/bundle/phases/deploy.go +++ b/bundle/phases/deploy.go @@ -85,7 +85,7 @@ func approvalForDeploy(ctx context.Context, b *bundle.Bundle, plan *deployplan.P } if !cmdio.IsPromptSupported(ctx) { - return false, errors.New("the deployment requires destructive actions (resources will be deleted), but the current console does not support prompting.\nUsing --auto-approve will skip all confirmation prompts and proceed with the destructive changes.\nOnly use --auto-approve if you have reviewed the plan and accept the deletions") + return false, errors.New("the deployment requires destructive actions, but current console does not support prompting. Please specify --auto-approve if you would like to skip prompts and proceed") } cmdio.LogString(ctx, "") diff --git a/cmd/agent/agent.go b/cmd/agent/agent.go deleted file mode 100644 index b65fe08002..0000000000 --- a/cmd/agent/agent.go +++ /dev/null @@ -1,18 +0,0 @@ -package agent - -import ( - "github.com/spf13/cobra" -) - -// New returns the agent command group. -func New() *cobra.Command { - cmd := &cobra.Command{ - Use: "agent", - Short: "Commands for AI agent integration", - Hidden: true, - } - - cmd.AddCommand(newConsentCommand()) - - return cmd -} diff --git a/cmd/agent/consent.go b/cmd/agent/consent.go deleted file mode 100644 index aaee1f5730..0000000000 --- a/cmd/agent/consent.go +++ /dev/null @@ -1,42 +0,0 @@ -package agent - -import ( - "fmt" - - libagent "github.com/databricks/cli/libs/agent" - "github.com/databricks/cli/libs/cmdio" - "github.com/spf13/cobra" -) - -func newConsentCommand() *cobra.Command { - var operation string - var reason string - - cmd := &cobra.Command{ - Use: "consent", - Short: "Capture user consent for a gated operation", - Long: `Capture explicit user consent before performing a potentially destructive operation. - -AI agents are required to obtain user consent before using flags like --force-lock, ---auto-approve, or --force on deploy. This command creates a consent token that -must be passed via the DATABRICKS_CLI_AGENT_CONSENT environment variable. - -The consent token expires after 10 minutes.`, - RunE: func(cmd *cobra.Command, args []string) error { - tokenPath, err := libagent.CreateConsentToken(operation, reason) - if err != nil { - return err - } - - cmdio.LogString(cmd.Context(), fmt.Sprintf("%s=%s", libagent.ConsentEnvVar, tokenPath)) - return nil - }, - } - - cmd.Flags().StringVar(&operation, "operation", "", fmt.Sprintf("The operation to consent to: %v", libagent.ValidOperations)) - cmd.Flags().StringVar(&reason, "reason", "", "Why the user approved this operation (minimum 20 characters)") - _ = cmd.MarkFlagRequired("operation") - _ = cmd.MarkFlagRequired("reason") - - return cmd -} diff --git a/cmd/bundle/deploy.go b/cmd/bundle/deploy.go index a3a040a55c..31ffe7090d 100644 --- a/cmd/bundle/deploy.go +++ b/cmd/bundle/deploy.go @@ -6,7 +6,6 @@ import ( "github.com/databricks/cli/bundle" "github.com/databricks/cli/cmd/bundle/utils" "github.com/databricks/cli/cmd/root" - "github.com/databricks/cli/libs/agent" "github.com/spf13/cobra" ) @@ -45,10 +44,6 @@ See https://docs.databricks.com/en/dev-tools/bundles/index.html for more informa cmd.Flags().MarkHidden("verbose") cmd.RunE = func(cmd *cobra.Command, args []string) error { - if err := agent.CheckConsentForFlags(cmd); err != nil { - return err - } - _, err := utils.ProcessBundle(cmd, utils.ProcessOptions{ InitFunc: func(b *bundle.Bundle) { b.Config.Bundle.Force = force diff --git a/cmd/bundle/deployment/bind.go b/cmd/bundle/deployment/bind.go index de16af1f49..4b72cdd9e6 100644 --- a/cmd/bundle/deployment/bind.go +++ b/cmd/bundle/deployment/bind.go @@ -2,7 +2,6 @@ package deployment import ( "github.com/databricks/cli/cmd/root" - "github.com/databricks/cli/libs/agent" "github.com/databricks/cli/libs/cmdio" "github.com/spf13/cobra" ) @@ -51,9 +50,6 @@ Any manual changes made in the workspace UI may be overwritten on deployment.`, cmd.Flags().BoolVar(&forceLock, "force-lock", false, "Force acquisition of deployment lock.") cmd.RunE = func(cmd *cobra.Command, args []string) error { - if err := agent.CheckConsentForFlags(cmd); err != nil { - return err - } err := BindResource(cmd, args[0], args[1], autoApprove, forceLock, false) if err != nil { return err diff --git a/cmd/bundle/deployment/unbind.go b/cmd/bundle/deployment/unbind.go index 077e49f251..e382a0d942 100644 --- a/cmd/bundle/deployment/unbind.go +++ b/cmd/bundle/deployment/unbind.go @@ -6,7 +6,6 @@ import ( "github.com/databricks/cli/bundle/phases" "github.com/databricks/cli/cmd/bundle/utils" "github.com/databricks/cli/cmd/root" - "github.com/databricks/cli/libs/agent" "github.com/databricks/cli/libs/logdiag" "github.com/spf13/cobra" ) @@ -53,9 +52,6 @@ To re-bind the resource later, use: cmd.Flags().BoolVar(&forceLock, "force-lock", false, "Force acquisition of deployment lock.") cmd.RunE = func(cmd *cobra.Command, args []string) error { - if err := agent.CheckConsentForFlags(cmd); err != nil { - return err - } b, err := utils.ProcessBundle(cmd, utils.ProcessOptions{ InitFunc: func(b *bundle.Bundle) { b.Config.Bundle.Deployment.Lock.Force = forceLock diff --git a/cmd/bundle/destroy.go b/cmd/bundle/destroy.go index c29ca25a67..bd9b65f71c 100644 --- a/cmd/bundle/destroy.go +++ b/cmd/bundle/destroy.go @@ -9,7 +9,6 @@ import ( "github.com/databricks/cli/bundle/phases" "github.com/databricks/cli/cmd/bundle/utils" "github.com/databricks/cli/cmd/root" - "github.com/databricks/cli/libs/agent" "github.com/databricks/cli/libs/cmdio" "github.com/databricks/cli/libs/logdiag" "github.com/spf13/cobra" @@ -40,9 +39,6 @@ Typical use cases: cmd.Flags().BoolVar(&forceDestroy, "force-lock", false, "Force acquisition of deployment lock.") cmd.RunE = func(cmd *cobra.Command, args []string) error { - if err := agent.CheckConsentForFlags(cmd); err != nil { - return err - } return CommandBundleDestroy(cmd, args, autoApprove, forceDestroy) } @@ -52,7 +48,7 @@ Typical use cases: func CommandBundleDestroy(cmd *cobra.Command, args []string, autoApprove, forceDestroy bool) error { // We require auto-approve for non-interactive terminals since prompts are not possible. if !cmdio.IsPromptSupported(cmd.Context()) && !autoApprove { - return errors.New("this will permanently destroy all deployed resources in the target.\nUsing --auto-approve will skip all confirmation prompts and proceed with destruction.\nOnly use --auto-approve if you are certain you want to delete all resources") + return errors.New("please specify --auto-approve since terminal does not support interactive prompts") } // Check if context is already initialized (e.g., when called from apps delete override) diff --git a/cmd/cmd.go b/cmd/cmd.go index 8b33b71f99..014471f763 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -8,7 +8,6 @@ import ( ssh "github.com/databricks/cli/experimental/ssh/cmd" "github.com/databricks/cli/cmd/account" - agentcmd "github.com/databricks/cli/cmd/agent" "github.com/databricks/cli/cmd/api" "github.com/databricks/cli/cmd/auth" "github.com/databricks/cli/cmd/bundle" @@ -94,7 +93,6 @@ func New(ctx context.Context) *cobra.Command { } // Add other subcommands. - cli.AddCommand(agentcmd.New()) cli.AddCommand(api.New()) cli.AddCommand(auth.New()) cli.AddCommand(completion.New()) diff --git a/cmd/pipelines/deploy.go b/cmd/pipelines/deploy.go index 3656a85e10..d966a962d3 100644 --- a/cmd/pipelines/deploy.go +++ b/cmd/pipelines/deploy.go @@ -9,7 +9,6 @@ import ( "github.com/databricks/cli/bundle/config/mutator" "github.com/databricks/cli/cmd/bundle/utils" "github.com/databricks/cli/cmd/root" - "github.com/databricks/cli/libs/agent" "github.com/databricks/cli/libs/cmdio" "github.com/databricks/cli/libs/logdiag" libsutils "github.com/databricks/cli/libs/utils" @@ -36,9 +35,6 @@ func deployCommand() *cobra.Command { cmd.Flags().MarkHidden("verbose") cmd.RunE = func(cmd *cobra.Command, args []string) error { - if err := agent.CheckConsentForFlags(cmd); err != nil { - return err - } b, err := utils.ProcessBundle(cmd, utils.ProcessOptions{ InitFunc: func(b *bundle.Bundle) { b.Config.Bundle.Deployment.Lock.Force = forceLock diff --git a/cmd/pipelines/destroy.go b/cmd/pipelines/destroy.go index 99e7f59a18..9f98d525fa 100644 --- a/cmd/pipelines/destroy.go +++ b/cmd/pipelines/destroy.go @@ -4,7 +4,6 @@ package pipelines import ( "github.com/databricks/cli/cmd/bundle" - "github.com/databricks/cli/libs/agent" "github.com/spf13/cobra" ) @@ -21,9 +20,6 @@ func destroyCommand() *cobra.Command { cmd.Flags().BoolVar(&forceDestroy, "force-lock", false, "Force acquisition of deployment lock.") cmd.RunE = func(cmd *cobra.Command, args []string) error { - if err := agent.CheckConsentForFlags(cmd); err != nil { - return err - } return bundle.CommandBundleDestroy(cmd, args, autoApprove, forceDestroy) } diff --git a/integration/libs/locker/locker_test.go b/integration/libs/locker/locker_test.go index a480216354..3ae80f8e71 100644 --- a/integration/libs/locker/locker_test.go +++ b/integration/libs/locker/locker_test.go @@ -59,7 +59,7 @@ func TestLock(t *testing.T) { indexOfAnInactiveLocker = i } assert.ErrorContains(t, lockerErrs[i], "lock acquired by") - assert.ErrorContains(t, lockerErrs[i], "Only use --force-lock if you are certain") + assert.ErrorContains(t, lockerErrs[i], "Use --force-lock to override") } } assert.Equal(t, 1, countActive, "Exactly one locker should successfull acquire the lock") diff --git a/libs/agent/check_flags.go b/libs/agent/check_flags.go deleted file mode 100644 index 5eb418b920..0000000000 --- a/libs/agent/check_flags.go +++ /dev/null @@ -1,36 +0,0 @@ -package agent - -import ( - "github.com/spf13/cobra" -) - -// CheckConsentForFlags validates agent consent for gated flags that have been -// explicitly set on the command. Returns nil if no agent is detected or if -// no gated flags are set. -func CheckConsentForFlags(cmd *cobra.Command) error { - ctx := cmd.Context() - - // Non-agent callers are not gated. - if Product(ctx) == "" { - return nil - } - - // Map of flag names to the consent operation they require. - flagOps := map[string]string{ - "force-lock": OperationForceLock, - "auto-approve": OperationAutoApprove, - "force": OperationForceDeploy, - } - - for flagName, operation := range flagOps { - f := cmd.Flag(flagName) - if f == nil || !f.Changed { - continue - } - if err := ValidateConsent(ctx, operation); err != nil { - return err - } - } - - return nil -} diff --git a/libs/agent/check_flags_test.go b/libs/agent/check_flags_test.go deleted file mode 100644 index 15da08f6a3..0000000000 --- a/libs/agent/check_flags_test.go +++ /dev/null @@ -1,89 +0,0 @@ -package agent_test - -import ( - "os" - "testing" - - "github.com/databricks/cli/libs/agent" - "github.com/databricks/cli/libs/env" - "github.com/spf13/cobra" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func newTestCommand(ctx interface{ Context() interface{ Done() <-chan struct{} } }) *cobra.Command { - // Build a minimal cobra command with the gated flags. - cmd := &cobra.Command{Use: "test"} - cmd.Flags().Bool("force", false, "") - cmd.Flags().Bool("force-lock", false, "") - cmd.Flags().Bool("auto-approve", false, "") - return cmd -} - -func TestCheckConsentForFlagsNoAgent(t *testing.T) { - ctx := agent.Mock(t.Context(), "") - cmd := &cobra.Command{Use: "test"} - cmd.Flags().Bool("force-lock", false, "") - cmd.SetContext(ctx) - _ = cmd.Flags().Set("force-lock", "true") - - assert.NoError(t, agent.CheckConsentForFlags(cmd)) -} - -func TestCheckConsentForFlagsNoGatedFlagsSet(t *testing.T) { - ctx := agent.Mock(t.Context(), agent.ClaudeCode) - cmd := &cobra.Command{Use: "test"} - cmd.Flags().Bool("force-lock", false, "") - cmd.SetContext(ctx) - - assert.NoError(t, agent.CheckConsentForFlags(cmd)) -} - -func TestCheckConsentForFlagsBlocksWithoutConsent(t *testing.T) { - ctx := agent.Mock(t.Context(), agent.ClaudeCode) - cmd := &cobra.Command{Use: "test"} - cmd.Flags().Bool("force-lock", false, "") - cmd.SetContext(ctx) - _ = cmd.Flags().Set("force-lock", "true") - - err := agent.CheckConsentForFlags(cmd) - assert.Error(t, err) - assert.Contains(t, err.Error(), "explicit user consent") -} - -func TestCheckConsentForFlagsAllowsWithValidToken(t *testing.T) { - path, err := agent.CreateConsentToken(agent.OperationAutoApprove, "user reviewed and approved the destructive changes") - require.NoError(t, err) - t.Cleanup(func() { os.Remove(path) }) - - ctx := agent.Mock(t.Context(), agent.Cursor) - ctx = env.Set(ctx, agent.ConsentEnvVar, path) - - cmd := &cobra.Command{Use: "test"} - cmd.Flags().Bool("auto-approve", false, "") - cmd.SetContext(ctx) - _ = cmd.Flags().Set("auto-approve", "true") - - assert.NoError(t, agent.CheckConsentForFlags(cmd)) -} - -func TestCheckConsentForFlagsMultipleFlags(t *testing.T) { - // Token only covers force-lock, not auto-approve. - path, err := agent.CreateConsentToken(agent.OperationForceLock, "user confirmed the other deploy is stale") - require.NoError(t, err) - t.Cleanup(func() { os.Remove(path) }) - - ctx := agent.Mock(t.Context(), agent.ClaudeCode) - ctx = env.Set(ctx, agent.ConsentEnvVar, path) - - cmd := &cobra.Command{Use: "test"} - cmd.Flags().Bool("force-lock", false, "") - cmd.Flags().Bool("auto-approve", false, "") - cmd.SetContext(ctx) - _ = cmd.Flags().Set("force-lock", "true") - _ = cmd.Flags().Set("auto-approve", "true") - - err = agent.CheckConsentForFlags(cmd) - assert.Error(t, err) - assert.Contains(t, err.Error(), "consent token is for") -} diff --git a/libs/agent/consent.go b/libs/agent/consent.go deleted file mode 100644 index 4782ab49f9..0000000000 --- a/libs/agent/consent.go +++ /dev/null @@ -1,187 +0,0 @@ -package agent - -import ( - "context" - "crypto/rand" - "encoding/hex" - "fmt" - "os" - "path/filepath" - "strings" - "time" - - "github.com/databricks/cli/libs/env" -) - -// ConsentEnvVar is the environment variable that agents must set to a valid -// consent token path before using gated flags like --force-lock or --auto-approve. -const ConsentEnvVar = "DATABRICKS_CLI_AGENT_CONSENT" - -// consentTokenDir is the directory where consent tokens are stored. -const consentTokenDir = "databricks-agent-consent" - -// consentTokenExpiry is how long a consent token remains valid. -const consentTokenExpiry = 10 * time.Minute - -// minReasonLength is the minimum length for consent reasons to prevent -// agents from using trivial reasons like "yes" or "ok". -const minReasonLength = 20 - -// Operations that can be consented to. -const ( - OperationForceLock = "force-lock" - OperationAutoApprove = "auto-approve" - OperationForceDeploy = "force-deploy" -) - -// ValidOperations lists all valid consent operations. -var ValidOperations = []string{ - OperationForceLock, - OperationAutoApprove, - OperationForceDeploy, -} - -// ConsentToken represents a validated consent token. -type ConsentToken struct { - Operation string - Reason string - CreatedAt time.Time -} - -// CreateConsentToken writes a consent token file and returns its path. -func CreateConsentToken(operation, reason string) (string, error) { - if err := validateOperation(operation); err != nil { - return "", err - } - if len(reason) < minReasonLength { - return "", fmt.Errorf("consent reason must be at least %d characters to ensure meaningful justification, got %d", minReasonLength, len(reason)) - } - - dir := filepath.Join(os.TempDir(), consentTokenDir) - if err := os.MkdirAll(dir, 0o700); err != nil { - return "", fmt.Errorf("failed to create consent token directory: %w", err) - } - - // Clean up expired tokens on each creation. - cleanExpiredTokens(dir) - - tokenID := make([]byte, 16) - _, err := rand.Read(tokenID) - if err != nil { - return "", fmt.Errorf("failed to generate token ID: %w", err) - } - - filename := fmt.Sprintf("consent-%s-%s", operation, hex.EncodeToString(tokenID)) - tokenPath := filepath.Join(dir, filename) - - content := fmt.Sprintf("operation: %s\nreason: %s\ncreated: %s\n", - operation, reason, time.Now().UTC().Format(time.RFC3339)) - - if err := os.WriteFile(tokenPath, []byte(content), 0o600); err != nil { - return "", fmt.Errorf("failed to write consent token: %w", err) - } - - return tokenPath, nil -} - -// ValidateConsent checks whether the agent has valid consent for the given operation. -// It reads the token path from the DATABRICKS_CLI_AGENT_CONSENT environment variable. -// Returns nil if no agent is detected (non-agent callers are not gated). -// Returns an error if agent is detected but consent is missing or invalid. -func ValidateConsent(ctx context.Context, operation string) error { - // Non-agent callers are not gated. - if Product(ctx) == "" { - return nil - } - - tokenPath := env.Get(ctx, ConsentEnvVar) - if tokenPath == "" { - return &ConsentRequiredError{Operation: operation} - } - - token, err := readConsentToken(tokenPath) - if err != nil { - return fmt.Errorf("invalid agent consent token: %w", err) - } - - if time.Since(token.CreatedAt) > consentTokenExpiry { - return fmt.Errorf("agent consent token has expired (created %s ago, max %s). Run `databricks agent consent` again", - time.Since(token.CreatedAt).Round(time.Second), consentTokenExpiry) - } - - if token.Operation != operation { - return fmt.Errorf("agent consent token is for %q, but %q is required", token.Operation, operation) - } - - return nil -} - -// ConsentRequiredError is returned when an agent attempts a gated operation -// without providing consent. -type ConsentRequiredError struct { - Operation string -} - -func (e *ConsentRequiredError) Error() string { - return fmt.Sprintf( - "this operation requires explicit user consent when run by an AI agent.\n\n"+ - "AI agents must not automatically retry with this flag. Instead:\n"+ - "1. Present this error to the user and explain what the flag does\n"+ - "2. If the user approves, run: databricks agent consent --operation %s --reason \"\"\n"+ - "3. Set the %s environment variable to the output token path\n"+ - "4. Retry the command", - e.Operation, ConsentEnvVar) -} - -func readConsentToken(path string) (*ConsentToken, error) { - data, err := os.ReadFile(path) - if err != nil { - return nil, fmt.Errorf("cannot read token file %s: %w", path, err) - } - - token := &ConsentToken{} - for _, line := range strings.Split(string(data), "\n") { - if strings.HasPrefix(line, "operation: ") { - token.Operation = strings.TrimPrefix(line, "operation: ") - } else if strings.HasPrefix(line, "reason: ") { - token.Reason = strings.TrimPrefix(line, "reason: ") - } else if strings.HasPrefix(line, "created: ") { - t, err := time.Parse(time.RFC3339, strings.TrimPrefix(line, "created: ")) - if err != nil { - return nil, fmt.Errorf("invalid timestamp in token: %w", err) - } - token.CreatedAt = t - } - } - - if token.Operation == "" { - return nil, fmt.Errorf("token file is missing operation field") - } - - return token, nil -} - -func validateOperation(operation string) error { - for _, op := range ValidOperations { - if op == operation { - return nil - } - } - return fmt.Errorf("invalid operation %q, must be one of: %s", operation, strings.Join(ValidOperations, ", ")) -} - -func cleanExpiredTokens(dir string) { - entries, err := os.ReadDir(dir) - if err != nil { - return - } - for _, entry := range entries { - info, err := entry.Info() - if err != nil { - continue - } - if time.Since(info.ModTime()) > consentTokenExpiry*2 { - os.Remove(filepath.Join(dir, entry.Name())) - } - } -} diff --git a/libs/agent/consent_test.go b/libs/agent/consent_test.go deleted file mode 100644 index d87276a80a..0000000000 --- a/libs/agent/consent_test.go +++ /dev/null @@ -1,118 +0,0 @@ -package agent - -import ( - "os" - "path/filepath" - "testing" - "time" - - "github.com/databricks/cli/libs/env" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestCreateConsentTokenSuccess(t *testing.T) { - path, err := CreateConsentToken(OperationForceLock, "user confirmed the other deploy is stale and can be overridden") - require.NoError(t, err) - t.Cleanup(func() { os.Remove(path) }) - - assert.FileExists(t, path) - - token, err := readConsentToken(path) - require.NoError(t, err) - assert.Equal(t, OperationForceLock, token.Operation) - assert.Contains(t, token.Reason, "stale") - assert.WithinDuration(t, time.Now(), token.CreatedAt, 5*time.Second) -} - -func TestCreateConsentTokenInvalidOperation(t *testing.T) { - _, err := CreateConsentToken("nuke-everything", "user said go ahead") - assert.ErrorContains(t, err, "invalid operation") -} - -func TestCreateConsentTokenReasonTooShort(t *testing.T) { - _, err := CreateConsentToken(OperationAutoApprove, "yes") - assert.ErrorContains(t, err, "at least 20 characters") -} - -func TestValidateConsentNoAgent(t *testing.T) { - ctx := Mock(t.Context(), "") - err := ValidateConsent(ctx, OperationForceLock) - assert.NoError(t, err) -} - -func TestValidateConsentMissingToken(t *testing.T) { - ctx := Mock(t.Context(), ClaudeCode) - err := ValidateConsent(ctx, OperationForceLock) - - var consentErr *ConsentRequiredError - assert.ErrorAs(t, err, &consentErr) - assert.Equal(t, OperationForceLock, consentErr.Operation) - assert.Contains(t, err.Error(), "explicit user consent") -} - -func TestValidateConsentValidToken(t *testing.T) { - path, err := CreateConsentToken(OperationAutoApprove, "user reviewed the plan and approved resource deletions") - require.NoError(t, err) - t.Cleanup(func() { os.Remove(path) }) - - ctx := Mock(t.Context(), Cursor) - ctx = env.Set(ctx, ConsentEnvVar, path) - assert.NoError(t, ValidateConsent(ctx, OperationAutoApprove)) -} - -func TestValidateConsentWrongOperation(t *testing.T) { - path, err := CreateConsentToken(OperationForceLock, "user confirmed the lock can be overridden safely") - require.NoError(t, err) - t.Cleanup(func() { os.Remove(path) }) - - ctx := Mock(t.Context(), ClaudeCode) - ctx = env.Set(ctx, ConsentEnvVar, path) - err = ValidateConsent(ctx, OperationAutoApprove) - assert.ErrorContains(t, err, "consent token is for") -} - -func TestValidateConsentExpiredToken(t *testing.T) { - path, err := CreateConsentToken(OperationForceDeploy, "user approved force deploy to override dashboard") - require.NoError(t, err) - t.Cleanup(func() { os.Remove(path) }) - - // Backdate the token file content. - data, err := os.ReadFile(path) - require.NoError(t, err) - old := time.Now().Add(-15 * time.Minute).UTC().Format(time.RFC3339) - content := "operation: " + OperationForceDeploy + "\nreason: test\ncreated: " + old + "\n" - require.NoError(t, os.WriteFile(path, []byte(content), 0o600)) - _ = data - - ctx := Mock(t.Context(), Codex) - ctx = env.Set(ctx, ConsentEnvVar, path) - err = ValidateConsent(ctx, OperationForceDeploy) - assert.ErrorContains(t, err, "expired") -} - -func TestValidateConsentInvalidPath(t *testing.T) { - ctx := Mock(t.Context(), ClaudeCode) - ctx = env.Set(ctx, ConsentEnvVar, "/nonexistent/token") - err := ValidateConsent(ctx, OperationForceLock) - assert.ErrorContains(t, err, "invalid agent consent token") -} - -func TestCleanExpiredTokens(t *testing.T) { - dir := t.TempDir() - - // Create an old file. - oldFile := filepath.Join(dir, "consent-old") - require.NoError(t, os.WriteFile(oldFile, []byte("old"), 0o600)) - oldTime := time.Now().Add(-25 * time.Minute) - require.NoError(t, os.Chtimes(oldFile, oldTime, oldTime)) - - // Create a recent file. - newFile := filepath.Join(dir, "consent-new") - require.NoError(t, os.WriteFile(newFile, []byte("new"), 0o600)) - - cleanExpiredTokens(dir) - - assert.NoFileExists(t, oldFile) - assert.FileExists(t, newFile) -} diff --git a/libs/locker/locker.go b/libs/locker/locker.go index 253c6b3aa7..003f169cd3 100644 --- a/libs/locker/locker.go +++ b/libs/locker/locker.go @@ -105,16 +105,10 @@ func (locker *Locker) assertLockHeld(ctx context.Context) error { return err } if activeLockState.ID != locker.State.ID && !activeLockState.IsForced { - return fmt.Errorf("deploy lock acquired by %s at %v.\n"+ - "Another deployment is likely in progress. Overriding with --force-lock risks corrupting that deployment.\n"+ - "Only use --force-lock if you are certain the other deployment is no longer active", - activeLockState.User, activeLockState.AcquisitionTime) + return fmt.Errorf("deploy lock acquired by %s at %v. Use --force-lock to override", activeLockState.User, activeLockState.AcquisitionTime) } if activeLockState.ID != locker.State.ID && activeLockState.IsForced { - return fmt.Errorf("deploy lock force-acquired by %s at %v.\n"+ - "Another deployment is likely in progress. Overriding with --force-lock risks corrupting that deployment.\n"+ - "Only use --force-lock if you are certain the other deployment is no longer active", - activeLockState.User, activeLockState.AcquisitionTime) + return fmt.Errorf("deploy lock force acquired by %s at %v. Use --force-lock to override", activeLockState.User, activeLockState.AcquisitionTime) } return nil } From 773cf54ef127834e7c3578dd1c948d2d2a06bbd1 Mon Sep 17 00:00:00 2001 From: Lennart Kats Date: Fri, 3 Apr 2026 11:38:54 +0200 Subject: [PATCH 03/13] Prevent AI agents from bypassing safety prompts Error messages for --auto-approve, --force-lock, and --force previously instructed callers to add the flag (e.g., "please specify --auto-approve"). AI agents follow this literally and retry with the flag, destroying resources without human review. This change: - Rewrites error messages to explain consequences instead of instructing callers to add the flag - Detects AI agents via environment variables (Claude Code, Cursor, Codex, Cline, Gemini CLI, OpenCode, Antigravity) - Blocks agents from using --auto-approve, --force-lock, or --force with a clear error telling them to get human approval first Co-authored-by: Isaac --- .../bind/dashboard/recreation/output.txt | 4 +- .../bind/job/job-abort-bind/output.txt | 2 +- .../recreate/out.bind-fail.direct.txt | 4 +- .../recreate/out.bind-fail.terraform.txt | 4 +- .../bind/pipelines/recreate/output.txt | 4 +- .../pipelines/update/out.bind-fail.direct.txt | 4 +- .../update/out.bind-fail.terraform.txt | 4 +- .../dashboards/detect-change/output.txt | 9 +- .../pipelines/auto-approve/output.txt | 4 +- .../resources/pipelines/recreate/output.txt | 4 +- .../resources/schemas/auto-approve/output.txt | 4 +- .../pipelines/deploy/auto-approve/output.txt | 4 +- .../pipelines/deploy/force-lock/output.txt | 8 +- .../pipelines/destroy/auto-approve/output.txt | 4 +- .../pipelines/destroy/force-lock/output.txt | 8 +- bundle/config/mutator/validate_git_details.go | 3 +- .../mutator/validate_git_details_test.go | 6 +- .../check_dashboards_modified_remotely.go | 4 +- bundle/deploy/terraform/import.go | 2 +- bundle/phases/bind.go | 2 +- bundle/phases/deploy.go | 4 +- cmd/apps/delete_bundle.go | 4 + cmd/apps/deploy_bundle.go | 4 + cmd/bundle/deploy.go | 5 + cmd/bundle/deployment/bind.go | 4 + cmd/bundle/deployment/unbind.go | 4 + cmd/bundle/destroy.go | 8 +- cmd/bundle/plan.go | 5 + cmd/pipelines/deploy.go | 4 + cmd/pipelines/destroy.go | 4 + cmd/root/root.go | 2 + integration/libs/locker/locker_test.go | 2 +- libs/agent/agent.go | 98 +++++++++++++++++++ libs/agent/agent_test.go | 69 +++++++++++++ libs/agent/consent.go | 44 +++++++++ libs/agent/consent_test.go | 76 ++++++++++++++ libs/locker/locker.go | 10 +- 37 files changed, 404 insertions(+), 31 deletions(-) create mode 100644 libs/agent/agent.go create mode 100644 libs/agent/agent_test.go create mode 100644 libs/agent/consent.go create mode 100644 libs/agent/consent_test.go diff --git a/acceptance/bundle/deployment/bind/dashboard/recreation/output.txt b/acceptance/bundle/deployment/bind/dashboard/recreation/output.txt index 99c26a8ccc..5e83560c7d 100644 --- a/acceptance/bundle/deployment/bind/dashboard/recreation/output.txt +++ b/acceptance/bundle/deployment/bind/dashboard/recreation/output.txt @@ -18,7 +18,9 @@ Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bundle-[UNIQU This action will result in the deletion or recreation of the following dashboards. This will result in changed IDs and permanent URLs of the dashboards that will be recreated: recreate resources.dashboards.dashboard1 -Error: the deployment requires destructive actions, but current console does not support prompting. Please specify --auto-approve if you would like to skip prompts and proceed +Error: the deployment requires destructive actions (resources will be deleted), but the current console does not support prompting. +Using --auto-approve will skip all confirmation prompts and proceed with the destructive changes. +Only use --auto-approve if you have reviewed the plan and accept the deletions Exit code: 1 diff --git a/acceptance/bundle/deployment/bind/job/job-abort-bind/output.txt b/acceptance/bundle/deployment/bind/job/job-abort-bind/output.txt index 544448efbe..d176db8dd6 100644 --- a/acceptance/bundle/deployment/bind/job/job-abort-bind/output.txt +++ b/acceptance/bundle/deployment/bind/job/job-abort-bind/output.txt @@ -3,7 +3,7 @@ Created job with ID: [JOB_ID] === Expect binding to fail without an auto-approve flag: -Error: This bind operation requires user confirmation, but the current console does not support prompting. Please specify --auto-approve if you would like to skip prompts and proceed. +Error: This bind operation requires user confirmation, but the current console does not support prompting. === Deploy bundle: >>> [CLI] bundle deploy --force-lock diff --git a/acceptance/bundle/deployment/bind/pipelines/recreate/out.bind-fail.direct.txt b/acceptance/bundle/deployment/bind/pipelines/recreate/out.bind-fail.direct.txt index c5e00b49cb..0ee95b0069 100644 --- a/acceptance/bundle/deployment/bind/pipelines/recreate/out.bind-fail.direct.txt +++ b/acceptance/bundle/deployment/bind/pipelines/recreate/out.bind-fail.direct.txt @@ -11,5 +11,7 @@ Changes detected: ~ root_path: "/Workspace/Users/someuser@databricks.com/lakeflow_pipeline" -> null ~ storage: "/Shared/old_storage" -> "/Shared/new_storage" -Error: This bind operation requires user confirmation, but the current console does not support prompting. Please specify --auto-approve if you would like to skip prompts and proceed. +Error: This bind operation requires user confirmation, but the current console does not support prompting. +Using --auto-approve will bind resources without reviewing the changes. +Only use --auto-approve if you have reviewed what will be bound. diff --git a/acceptance/bundle/deployment/bind/pipelines/recreate/out.bind-fail.terraform.txt b/acceptance/bundle/deployment/bind/pipelines/recreate/out.bind-fail.terraform.txt index 1d300160a9..a5dd712d34 100644 --- a/acceptance/bundle/deployment/bind/pipelines/recreate/out.bind-fail.terraform.txt +++ b/acceptance/bundle/deployment/bind/pipelines/recreate/out.bind-fail.terraform.txt @@ -54,5 +54,7 @@ Terraform will perform the following actions: Plan: 1 to add, 0 to change, 1 to destroy. -Error: This bind operation requires user confirmation, but the current console does not support prompting. Please specify --auto-approve if you would like to skip prompts and proceed. +Error: This bind operation requires user confirmation, but the current console does not support prompting. +Using --auto-approve will bind resources without reviewing the changes. +Only use --auto-approve if you have reviewed what will be bound. diff --git a/acceptance/bundle/deployment/bind/pipelines/recreate/output.txt b/acceptance/bundle/deployment/bind/pipelines/recreate/output.txt index af42322dee..9f282a85d7 100644 --- a/acceptance/bundle/deployment/bind/pipelines/recreate/output.txt +++ b/acceptance/bundle/deployment/bind/pipelines/recreate/output.txt @@ -16,7 +16,9 @@ Streaming Tables (STs) and Materialized Views (MVs) managed by them. Recreating restore the defined STs and MVs through full refresh. Note that recreation is necessary when pipeline properties such as the 'catalog' or 'storage' are changed: recreate resources.pipelines.foo -Error: the deployment requires destructive actions, but current console does not support prompting. Please specify --auto-approve if you would like to skip prompts and proceed +Error: the deployment requires destructive actions (resources will be deleted), but the current console does not support prompting. +Using --auto-approve will skip all confirmation prompts and proceed with the destructive changes. +Only use --auto-approve if you have reviewed the plan and accept the deletions >>> [CLI] bundle deploy --auto-approve diff --git a/acceptance/bundle/deployment/bind/pipelines/update/out.bind-fail.direct.txt b/acceptance/bundle/deployment/bind/pipelines/update/out.bind-fail.direct.txt index 7b85803529..d1a289868e 100644 --- a/acceptance/bundle/deployment/bind/pipelines/update/out.bind-fail.direct.txt +++ b/acceptance/bundle/deployment/bind/pipelines/update/out.bind-fail.direct.txt @@ -11,5 +11,7 @@ Changes detected: ~ name: "lakeflow-pipeline" -> "test-pipeline" ~ root_path: "/Workspace/Users/[USERNAME]/lakeflow_pipeline" -> null -Error: This bind operation requires user confirmation, but the current console does not support prompting. Please specify --auto-approve if you would like to skip prompts and proceed. +Error: This bind operation requires user confirmation, but the current console does not support prompting. +Using --auto-approve will bind resources without reviewing the changes. +Only use --auto-approve if you have reviewed what will be bound. diff --git a/acceptance/bundle/deployment/bind/pipelines/update/out.bind-fail.terraform.txt b/acceptance/bundle/deployment/bind/pipelines/update/out.bind-fail.terraform.txt index 2ff60fce2b..14ebbe6df2 100644 --- a/acceptance/bundle/deployment/bind/pipelines/update/out.bind-fail.terraform.txt +++ b/acceptance/bundle/deployment/bind/pipelines/update/out.bind-fail.terraform.txt @@ -41,5 +41,7 @@ Terraform will perform the following actions: Plan: 0 to add, 1 to change, 0 to destroy. -Error: This bind operation requires user confirmation, but the current console does not support prompting. Please specify --auto-approve if you would like to skip prompts and proceed. +Error: This bind operation requires user confirmation, but the current console does not support prompting. +Using --auto-approve will bind resources without reviewing the changes. +Only use --auto-approve if you have reviewed what will be bound. diff --git a/acceptance/bundle/resources/dashboards/detect-change/output.txt b/acceptance/bundle/resources/dashboards/detect-change/output.txt index 53179be516..cdc0ac4aee 100644 --- a/acceptance/bundle/resources/dashboards/detect-change/output.txt +++ b/acceptance/bundle/resources/dashboards/detect-change/output.txt @@ -60,7 +60,8 @@ These modifications are untracked and will be overwritten on deploy. Make sure that the local dashboard definition matches what you intend to deploy before proceeding with the deployment. -Run `databricks bundle deploy --force` to bypass this error. +Use --force only if you want to discard the remote changes and overwrite +the dashboard with your local version. update dashboards.file_reference @@ -77,7 +78,8 @@ These modifications are untracked and will be overwritten on deploy. Make sure that the local dashboard definition matches what you intend to deploy before proceeding with the deployment. -Run `databricks bundle deploy --force` to bypass this error. +Use --force only if you want to discard the remote changes and overwrite +the dashboard with your local version. >>> errcode [CLI] bundle deploy @@ -91,7 +93,8 @@ These modifications are untracked and will be overwritten on deploy. Make sure that the local dashboard definition matches what you intend to deploy before proceeding with the deployment. -Run `databricks bundle deploy --force` to bypass this error. +Use --force only if you want to discard the remote changes and overwrite +the dashboard with your local version. Exit code: 1 diff --git a/acceptance/bundle/resources/pipelines/auto-approve/output.txt b/acceptance/bundle/resources/pipelines/auto-approve/output.txt index 5154a1206a..d71a3f7a9c 100644 --- a/acceptance/bundle/resources/pipelines/auto-approve/output.txt +++ b/acceptance/bundle/resources/pipelines/auto-approve/output.txt @@ -50,7 +50,9 @@ Streaming Tables (STs) and Materialized Views (MVs) managed by them. Recreating restore the defined STs and MVs through full refresh. Note that recreation is necessary when pipeline properties such as the 'catalog' or 'storage' are changed: delete resources.pipelines.bar -Error: the deployment requires destructive actions, but current console does not support prompting. Please specify --auto-approve if you would like to skip prompts and proceed +Error: the deployment requires destructive actions (resources will be deleted), but the current console does not support prompting. +Using --auto-approve will skip all confirmation prompts and proceed with the destructive changes. +Only use --auto-approve if you have reviewed the plan and accept the deletions Exit code: 1 diff --git a/acceptance/bundle/resources/pipelines/recreate/output.txt b/acceptance/bundle/resources/pipelines/recreate/output.txt index 8550395ff2..afb961b67b 100644 --- a/acceptance/bundle/resources/pipelines/recreate/output.txt +++ b/acceptance/bundle/resources/pipelines/recreate/output.txt @@ -49,7 +49,9 @@ Streaming Tables (STs) and Materialized Views (MVs) managed by them. Recreating restore the defined STs and MVs through full refresh. Note that recreation is necessary when pipeline properties such as the 'catalog' or 'storage' are changed: recreate resources.pipelines.foo -Error: the deployment requires destructive actions, but current console does not support prompting. Please specify --auto-approve if you would like to skip prompts and proceed +Error: the deployment requires destructive actions (resources will be deleted), but the current console does not support prompting. +Using --auto-approve will skip all confirmation prompts and proceed with the destructive changes. +Only use --auto-approve if you have reviewed the plan and accept the deletions Exit code: 1 diff --git a/acceptance/bundle/resources/schemas/auto-approve/output.txt b/acceptance/bundle/resources/schemas/auto-approve/output.txt index 6a773f60ca..f2a4563c10 100644 --- a/acceptance/bundle/resources/schemas/auto-approve/output.txt +++ b/acceptance/bundle/resources/schemas/auto-approve/output.txt @@ -57,7 +57,9 @@ Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/[UNIQUE_NAME]/file This action will result in the deletion or recreation of the following UC schemas. Any underlying data may be lost: delete resources.schemas.bar -Error: the deployment requires destructive actions, but current console does not support prompting. Please specify --auto-approve if you would like to skip prompts and proceed +Error: the deployment requires destructive actions (resources will be deleted), but the current console does not support prompting. +Using --auto-approve will skip all confirmation prompts and proceed with the destructive changes. +Only use --auto-approve if you have reviewed the plan and accept the deletions === Test cleanup diff --git a/acceptance/pipelines/deploy/auto-approve/output.txt b/acceptance/pipelines/deploy/auto-approve/output.txt index ff5ac099bf..53066ab958 100644 --- a/acceptance/pipelines/deploy/auto-approve/output.txt +++ b/acceptance/pipelines/deploy/auto-approve/output.txt @@ -18,7 +18,9 @@ Streaming Tables (STs) and Materialized Views (MVs) managed by them. Recreating restore the defined STs and MVs through full refresh. Note that recreation is necessary when pipeline properties such as the 'catalog' or 'storage' are changed: delete resources.pipelines.foo -Error: the deployment requires destructive actions, but current console does not support prompting. Please specify --auto-approve if you would like to skip prompts and proceed +Error: the deployment requires destructive actions (resources will be deleted), but the current console does not support prompting. +Using --auto-approve will skip all confirmation prompts and proceed with the destructive changes. +Only use --auto-approve if you have reviewed the plan and accept the deletions Exit code: 1 diff --git a/acceptance/pipelines/deploy/force-lock/output.txt b/acceptance/pipelines/deploy/force-lock/output.txt index e5fdadbaf9..86c6f63e62 100644 --- a/acceptance/pipelines/deploy/force-lock/output.txt +++ b/acceptance/pipelines/deploy/force-lock/output.txt @@ -4,8 +4,12 @@ === test deployment without force-lock (should fail) >>> errcode [CLI] pipelines deploy -Error: Failed to acquire deployment lock: deploy lock acquired by user-with-lock@databricks.com at [TIMESTAMP] +0000 UTC. Use --force-lock to override -Error: deploy lock acquired by user-with-lock@databricks.com at [TIMESTAMP] +0000 UTC. Use --force-lock to override +Error: Failed to acquire deployment lock: deploy lock acquired by user-with-lock@databricks.com at [TIMESTAMP] +0000 UTC. +Another deployment is likely in progress. Overriding with --force-lock risks corrupting that deployment. +Only use --force-lock if you are certain the other deployment is no longer active +Error: deploy lock acquired by user-with-lock@databricks.com at [TIMESTAMP] +0000 UTC. +Another deployment is likely in progress. Overriding with --force-lock risks corrupting that deployment. +Only use --force-lock if you are certain the other deployment is no longer active Exit code: 1 diff --git a/acceptance/pipelines/destroy/auto-approve/output.txt b/acceptance/pipelines/destroy/auto-approve/output.txt index 17aecc2059..d71a359f4b 100644 --- a/acceptance/pipelines/destroy/auto-approve/output.txt +++ b/acceptance/pipelines/destroy/auto-approve/output.txt @@ -8,7 +8,9 @@ Deployment complete! View your pipeline my_pipeline here: [DATABRICKS_URL]/pipelines/[UUID]?o=[NUMID] >>> errcode [CLI] pipelines destroy -Error: please specify --auto-approve since terminal does not support interactive prompts +Error: this will permanently destroy all deployed resources in the bundle target. +Using --auto-approve will skip all confirmation prompts and proceed with the destruction. +Only use --auto-approve if you are certain you want to delete all resources Exit code: 1 diff --git a/acceptance/pipelines/destroy/force-lock/output.txt b/acceptance/pipelines/destroy/force-lock/output.txt index 5b6449f732..c3d6264e7f 100644 --- a/acceptance/pipelines/destroy/force-lock/output.txt +++ b/acceptance/pipelines/destroy/force-lock/output.txt @@ -11,8 +11,12 @@ View your pipeline foo here: [DATABRICKS_URL]/pipelines/[UUID]?o=[NUMID] === test deployment without force-lock (should fail) >>> errcode [CLI] pipelines destroy --auto-approve -Error: Failed to acquire deployment lock: deploy lock acquired by user-with-lock@databricks.com at [TIMESTAMP] +0000 UTC. Use --force-lock to override -Error: deploy lock acquired by user-with-lock@databricks.com at [TIMESTAMP] +0000 UTC. Use --force-lock to override +Error: Failed to acquire deployment lock: deploy lock acquired by user-with-lock@databricks.com at [TIMESTAMP] +0000 UTC. +Another deployment is likely in progress. Overriding with --force-lock risks corrupting that deployment. +Only use --force-lock if you are certain the other deployment is no longer active +Error: deploy lock acquired by user-with-lock@databricks.com at [TIMESTAMP] +0000 UTC. +Another deployment is likely in progress. Overriding with --force-lock risks corrupting that deployment. +Only use --force-lock if you are certain the other deployment is no longer active Exit code: 1 diff --git a/bundle/config/mutator/validate_git_details.go b/bundle/config/mutator/validate_git_details.go index 69a4221fdc..f7b413db7f 100644 --- a/bundle/config/mutator/validate_git_details.go +++ b/bundle/config/mutator/validate_git_details.go @@ -23,7 +23,8 @@ func (m *validateGitDetails) Apply(ctx context.Context, b *bundle.Bundle) diag.D } if b.Config.Bundle.Git.Branch != b.Config.Bundle.Git.ActualBranch && !b.Config.Bundle.Force { - return diag.Errorf("not on the right Git branch:\n expected according to configuration: %s\n actual: %s\nuse --force to override", b.Config.Bundle.Git.Branch, b.Config.Bundle.Git.ActualBranch) + return diag.Errorf("not on the right Git branch:\n expected according to configuration: %s\n actual: %s\nUsing --force will deploy from branch %s, which may push unexpected code to the target.\nOnly use --force if you intentionally want to deploy from this branch", + b.Config.Bundle.Git.Branch, b.Config.Bundle.Git.ActualBranch, b.Config.Bundle.Git.ActualBranch) } return nil } diff --git a/bundle/config/mutator/validate_git_details_test.go b/bundle/config/mutator/validate_git_details_test.go index b29f221edc..791c613c86 100644 --- a/bundle/config/mutator/validate_git_details_test.go +++ b/bundle/config/mutator/validate_git_details_test.go @@ -40,8 +40,10 @@ func TestValidateGitDetailsNonMatchingBranches(t *testing.T) { m := ValidateGitDetails() diags := bundle.Apply(t.Context(), b, m) - expectedError := "not on the right Git branch:\n expected according to configuration: main\n actual: feature\nuse --force to override" - assert.EqualError(t, diags.Error(), expectedError) + assert.ErrorContains(t, diags.Error(), "not on the right Git branch") + assert.ErrorContains(t, diags.Error(), "expected according to configuration: main") + assert.ErrorContains(t, diags.Error(), "actual: feature") + assert.ErrorContains(t, diags.Error(), "Only use --force if you intentionally want to deploy from this branch") } func TestValidateGitDetailsNotUsingGit(t *testing.T) { diff --git a/bundle/deploy/terraform/check_dashboards_modified_remotely.go b/bundle/deploy/terraform/check_dashboards_modified_remotely.go index 6f3a3b23b5..276fd77659 100644 --- a/bundle/deploy/terraform/check_dashboards_modified_remotely.go +++ b/bundle/deploy/terraform/check_dashboards_modified_remotely.go @@ -121,8 +121,8 @@ func (l *checkDashboardsModifiedRemotely) Apply(ctx context.Context, b *bundle.B "Make sure that the local dashboard definition matches what you intend to deploy\n" + "before proceeding with the deployment.\n" + "\n" + - "Run `databricks bundle deploy --force` to bypass this error." + - "", + "Use --force only if you want to discard the remote changes and overwrite\n" + + "the dashboard with your local version.", Paths: []dyn.Path{path}, Locations: []dyn.Location{loc}, }) diff --git a/bundle/deploy/terraform/import.go b/bundle/deploy/terraform/import.go index 8fdfe1212d..ad09bc19f3 100644 --- a/bundle/deploy/terraform/import.go +++ b/bundle/deploy/terraform/import.go @@ -72,7 +72,7 @@ func (m *importResource) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagn cmdio.LogString(ctx, output) if !cmdio.IsPromptSupported(ctx) { - return diag.Errorf("This bind operation requires user confirmation, but the current console does not support prompting. Please specify --auto-approve if you would like to skip prompts and proceed.") + return diag.Errorf("This bind operation requires user confirmation, but the current console does not support prompting.\nUsing --auto-approve will bind resources without reviewing the changes.\nOnly use --auto-approve if you have reviewed what will be bound.") } ans, err := cmdio.AskYesOrNo(ctx, "Confirm import changes? Changes will be remotely applied only after running 'bundle deploy'.") diff --git a/bundle/phases/bind.go b/bundle/phases/bind.go index a8f99b28e8..1c60912607 100644 --- a/bundle/phases/bind.go +++ b/bundle/phases/bind.go @@ -73,7 +73,7 @@ func Bind(ctx context.Context, b *bundle.Bundle, opts *terraform.BindOptions) { if !cmdio.IsPromptSupported(ctx) { result.Cancel() - logdiag.LogError(ctx, errors.New("This bind operation requires user confirmation, but the current console does not support prompting. Please specify --auto-approve if you would like to skip prompts and proceed.")) //nolint + logdiag.LogError(ctx, errors.New("This bind operation requires user confirmation, but the current console does not support prompting.\nUsing --auto-approve will bind resources without reviewing the changes.\nOnly use --auto-approve if you have reviewed what will be bound.")) //nolint return } diff --git a/bundle/phases/deploy.go b/bundle/phases/deploy.go index 4613a7a211..76271ac92a 100644 --- a/bundle/phases/deploy.go +++ b/bundle/phases/deploy.go @@ -85,7 +85,9 @@ func approvalForDeploy(ctx context.Context, b *bundle.Bundle, plan *deployplan.P } if !cmdio.IsPromptSupported(ctx) { - return false, errors.New("the deployment requires destructive actions, but current console does not support prompting. Please specify --auto-approve if you would like to skip prompts and proceed") + return false, errors.New("the deployment requires destructive actions (resources will be deleted), but the current console does not support prompting.\n" + + "Using --auto-approve will skip all confirmation prompts and proceed with the destructive changes.\n" + + "Only use --auto-approve if you have reviewed the plan and accept the deletions") } cmdio.LogString(ctx, "") diff --git a/cmd/apps/delete_bundle.go b/cmd/apps/delete_bundle.go index b4c0e72f87..623663a3d3 100644 --- a/cmd/apps/delete_bundle.go +++ b/cmd/apps/delete_bundle.go @@ -2,6 +2,7 @@ package apps import ( "github.com/databricks/cli/cmd/bundle" + "github.com/databricks/cli/libs/agent" "github.com/databricks/databricks-sdk-go/service/apps" "github.com/spf13/cobra" ) @@ -22,6 +23,9 @@ func BundleDeleteOverrideWithWrapper(wrapError ErrorWrapper) func(*cobra.Command originalRunE := deleteCmd.RunE deleteCmd.RunE = func(cmd *cobra.Command, args []string) error { + if err := agent.CheckConsentForFlags(cmd); err != nil { + return err + } if len(args) == 0 && hasBundleConfig() { return bundle.CommandBundleDestroy(cmd, args, autoApprove, forceDestroy) } diff --git a/cmd/apps/deploy_bundle.go b/cmd/apps/deploy_bundle.go index 86b8c8bb1e..d002742411 100644 --- a/cmd/apps/deploy_bundle.go +++ b/cmd/apps/deploy_bundle.go @@ -12,6 +12,7 @@ import ( "github.com/databricks/cli/bundle/run" "github.com/databricks/cli/cmd/bundle/utils" "github.com/databricks/cli/cmd/root" + "github.com/databricks/cli/libs/agent" "github.com/databricks/cli/libs/apps/prompt" "github.com/databricks/cli/libs/apps/validation" "github.com/databricks/cli/libs/cmdio" @@ -51,6 +52,9 @@ func BundleDeployOverrideWithWrapper(wrapError ErrorWrapper) func(*cobra.Command originalRunE := deployCmd.RunE deployCmd.RunE = func(cmd *cobra.Command, args []string) error { + if err := agent.CheckConsentForFlags(cmd); err != nil { + return err + } if len(args) == 0 { b := root.TryConfigureBundle(cmd) if b != nil { diff --git a/cmd/bundle/deploy.go b/cmd/bundle/deploy.go index 31ffe7090d..a3a040a55c 100644 --- a/cmd/bundle/deploy.go +++ b/cmd/bundle/deploy.go @@ -6,6 +6,7 @@ import ( "github.com/databricks/cli/bundle" "github.com/databricks/cli/cmd/bundle/utils" "github.com/databricks/cli/cmd/root" + "github.com/databricks/cli/libs/agent" "github.com/spf13/cobra" ) @@ -44,6 +45,10 @@ See https://docs.databricks.com/en/dev-tools/bundles/index.html for more informa cmd.Flags().MarkHidden("verbose") cmd.RunE = func(cmd *cobra.Command, args []string) error { + if err := agent.CheckConsentForFlags(cmd); err != nil { + return err + } + _, err := utils.ProcessBundle(cmd, utils.ProcessOptions{ InitFunc: func(b *bundle.Bundle) { b.Config.Bundle.Force = force diff --git a/cmd/bundle/deployment/bind.go b/cmd/bundle/deployment/bind.go index 4b72cdd9e6..de16af1f49 100644 --- a/cmd/bundle/deployment/bind.go +++ b/cmd/bundle/deployment/bind.go @@ -2,6 +2,7 @@ package deployment import ( "github.com/databricks/cli/cmd/root" + "github.com/databricks/cli/libs/agent" "github.com/databricks/cli/libs/cmdio" "github.com/spf13/cobra" ) @@ -50,6 +51,9 @@ Any manual changes made in the workspace UI may be overwritten on deployment.`, cmd.Flags().BoolVar(&forceLock, "force-lock", false, "Force acquisition of deployment lock.") cmd.RunE = func(cmd *cobra.Command, args []string) error { + if err := agent.CheckConsentForFlags(cmd); err != nil { + return err + } err := BindResource(cmd, args[0], args[1], autoApprove, forceLock, false) if err != nil { return err diff --git a/cmd/bundle/deployment/unbind.go b/cmd/bundle/deployment/unbind.go index e382a0d942..077e49f251 100644 --- a/cmd/bundle/deployment/unbind.go +++ b/cmd/bundle/deployment/unbind.go @@ -6,6 +6,7 @@ import ( "github.com/databricks/cli/bundle/phases" "github.com/databricks/cli/cmd/bundle/utils" "github.com/databricks/cli/cmd/root" + "github.com/databricks/cli/libs/agent" "github.com/databricks/cli/libs/logdiag" "github.com/spf13/cobra" ) @@ -52,6 +53,9 @@ To re-bind the resource later, use: cmd.Flags().BoolVar(&forceLock, "force-lock", false, "Force acquisition of deployment lock.") cmd.RunE = func(cmd *cobra.Command, args []string) error { + if err := agent.CheckConsentForFlags(cmd); err != nil { + return err + } b, err := utils.ProcessBundle(cmd, utils.ProcessOptions{ InitFunc: func(b *bundle.Bundle) { b.Config.Bundle.Deployment.Lock.Force = forceLock diff --git a/cmd/bundle/destroy.go b/cmd/bundle/destroy.go index bd9b65f71c..af851f62d9 100644 --- a/cmd/bundle/destroy.go +++ b/cmd/bundle/destroy.go @@ -9,6 +9,7 @@ import ( "github.com/databricks/cli/bundle/phases" "github.com/databricks/cli/cmd/bundle/utils" "github.com/databricks/cli/cmd/root" + "github.com/databricks/cli/libs/agent" "github.com/databricks/cli/libs/cmdio" "github.com/databricks/cli/libs/logdiag" "github.com/spf13/cobra" @@ -39,6 +40,9 @@ Typical use cases: cmd.Flags().BoolVar(&forceDestroy, "force-lock", false, "Force acquisition of deployment lock.") cmd.RunE = func(cmd *cobra.Command, args []string) error { + if err := agent.CheckConsentForFlags(cmd); err != nil { + return err + } return CommandBundleDestroy(cmd, args, autoApprove, forceDestroy) } @@ -48,7 +52,9 @@ Typical use cases: func CommandBundleDestroy(cmd *cobra.Command, args []string, autoApprove, forceDestroy bool) error { // We require auto-approve for non-interactive terminals since prompts are not possible. if !cmdio.IsPromptSupported(cmd.Context()) && !autoApprove { - return errors.New("please specify --auto-approve since terminal does not support interactive prompts") + return errors.New("this will permanently destroy all deployed resources in the bundle target.\n" + + "Using --auto-approve will skip all confirmation prompts and proceed with the destruction.\n" + + "Only use --auto-approve if you are certain you want to delete all resources") } // Check if context is already initialized (e.g., when called from apps delete override) diff --git a/cmd/bundle/plan.go b/cmd/bundle/plan.go index e3dd63929e..362cb0d815 100644 --- a/cmd/bundle/plan.go +++ b/cmd/bundle/plan.go @@ -10,6 +10,7 @@ import ( "github.com/databricks/cli/bundle/phases" "github.com/databricks/cli/cmd/bundle/utils" "github.com/databricks/cli/cmd/root" + "github.com/databricks/cli/libs/agent" "github.com/databricks/cli/libs/flags" "github.com/databricks/cli/libs/logdiag" "github.com/spf13/cobra" @@ -34,6 +35,10 @@ It is useful for previewing changes before running 'bundle deploy'.`, cmd.Flags().MarkDeprecated("compute-id", "use --cluster-id instead") cmd.RunE = func(cmd *cobra.Command, args []string) error { + if err := agent.CheckConsentForFlags(cmd); err != nil { + return err + } + opts := utils.ProcessOptions{ AlwaysPull: true, FastValidate: true, diff --git a/cmd/pipelines/deploy.go b/cmd/pipelines/deploy.go index d966a962d3..3656a85e10 100644 --- a/cmd/pipelines/deploy.go +++ b/cmd/pipelines/deploy.go @@ -9,6 +9,7 @@ import ( "github.com/databricks/cli/bundle/config/mutator" "github.com/databricks/cli/cmd/bundle/utils" "github.com/databricks/cli/cmd/root" + "github.com/databricks/cli/libs/agent" "github.com/databricks/cli/libs/cmdio" "github.com/databricks/cli/libs/logdiag" libsutils "github.com/databricks/cli/libs/utils" @@ -35,6 +36,9 @@ func deployCommand() *cobra.Command { cmd.Flags().MarkHidden("verbose") cmd.RunE = func(cmd *cobra.Command, args []string) error { + if err := agent.CheckConsentForFlags(cmd); err != nil { + return err + } b, err := utils.ProcessBundle(cmd, utils.ProcessOptions{ InitFunc: func(b *bundle.Bundle) { b.Config.Bundle.Deployment.Lock.Force = forceLock diff --git a/cmd/pipelines/destroy.go b/cmd/pipelines/destroy.go index 9f98d525fa..99e7f59a18 100644 --- a/cmd/pipelines/destroy.go +++ b/cmd/pipelines/destroy.go @@ -4,6 +4,7 @@ package pipelines import ( "github.com/databricks/cli/cmd/bundle" + "github.com/databricks/cli/libs/agent" "github.com/spf13/cobra" ) @@ -20,6 +21,9 @@ func destroyCommand() *cobra.Command { cmd.Flags().BoolVar(&forceDestroy, "force-lock", false, "Force acquisition of deployment lock.") cmd.RunE = func(cmd *cobra.Command, args []string) error { + if err := agent.CheckConsentForFlags(cmd); err != nil { + return err + } return bundle.CommandBundleDestroy(cmd, args, autoApprove, forceDestroy) } diff --git a/cmd/root/root.go b/cmd/root/root.go index e29d6df83c..8b446eb279 100644 --- a/cmd/root/root.go +++ b/cmd/root/root.go @@ -12,6 +12,7 @@ import ( "time" "github.com/databricks/cli/internal/build" + "github.com/databricks/cli/libs/agent" "github.com/databricks/cli/libs/auth" "github.com/databricks/cli/libs/cmdctx" "github.com/databricks/cli/libs/cmdio" @@ -80,6 +81,7 @@ func New(ctx context.Context) *cobra.Command { ctx = withUpstreamInUserAgent(ctx) ctx = withInteractiveModeInUserAgent(ctx) ctx = InjectTestPidToUserAgent(ctx) + ctx = agent.Detect(ctx) cmd.SetContext(ctx) return nil } diff --git a/integration/libs/locker/locker_test.go b/integration/libs/locker/locker_test.go index 3ae80f8e71..a480216354 100644 --- a/integration/libs/locker/locker_test.go +++ b/integration/libs/locker/locker_test.go @@ -59,7 +59,7 @@ func TestLock(t *testing.T) { indexOfAnInactiveLocker = i } assert.ErrorContains(t, lockerErrs[i], "lock acquired by") - assert.ErrorContains(t, lockerErrs[i], "Use --force-lock to override") + assert.ErrorContains(t, lockerErrs[i], "Only use --force-lock if you are certain") } } assert.Equal(t, 1, countActive, "Exactly one locker should successfull acquire the lock") diff --git a/libs/agent/agent.go b/libs/agent/agent.go new file mode 100644 index 0000000000..6c628e0f28 --- /dev/null +++ b/libs/agent/agent.go @@ -0,0 +1,98 @@ +package agent + +import ( + "context" + + "github.com/databricks/cli/libs/env" +) + +// Product name constants. +const ( + Antigravity = "antigravity" + ClaudeCode = "claude-code" + Cline = "cline" + Codex = "codex" + Cursor = "cursor" + GeminiCLI = "gemini-cli" + OpenCode = "opencode" +) + +// knownAgents maps environment variables to product names. +// Adding a new agent only requires a new entry here and a new constant above. +// +// References for each environment variable: +// - ANTIGRAVITY_AGENT: Closed source. Verified locally that Google Antigravity sets this variable. +// - CLAUDECODE: https://github.com/anthropics/claude-code (open source npm package, sets CLAUDECODE=1) +// - CLINE_ACTIVE: https://github.com/cline/cline (shipped in v3.24.0, see also https://github.com/cline/cline/discussions/5366) +// - CODEX_CI: https://github.com/openai/codex/blob/main/codex-rs/core/src/unified_exec/process_manager.rs (part of UNIFIED_EXEC_ENV array) +// - CURSOR_AGENT: Closed source. Referenced in https://gist.github.com/johnlindquist/9a90c5f1aedef0477c60d0de4171da3f +// - GEMINI_CLI: https://google-gemini.github.io/gemini-cli/docs/tools/shell.html ("sets the GEMINI_CLI=1 environment variable") +// - OPENCODE: https://github.com/opencode-ai/opencode (open source, sets OPENCODE=1) +var knownAgents = []struct { + envVar string + product string +}{ + {"ANTIGRAVITY_AGENT", Antigravity}, + {"CLAUDECODE", ClaudeCode}, + {"CLINE_ACTIVE", Cline}, + {"CODEX_CI", Codex}, + {"CURSOR_AGENT", Cursor}, + {"GEMINI_CLI", GeminiCLI}, + {"OPENCODE", OpenCode}, +} + +// productKeyType is a package-local context key with zero size. +type productKeyType struct{} + +var productKey productKeyType + +// detect performs the actual detection logic. +// Returns product name string or empty string if detection is ambiguous. +// Only returns a product if exactly one agent is detected. +func detect(ctx context.Context) string { + var detected []string + for _, a := range knownAgents { + if env.Get(ctx, a.envVar) != "" { + detected = append(detected, a.product) + } + } + + // Only return a product if exactly one agent is detected. + if len(detected) == 1 { + return detected[0] + } + + return "" +} + +// Detect detects the agent and stores it in context. +// It returns a new context with the detection result set. +func Detect(ctx context.Context) context.Context { + return context.WithValue(ctx, productKey, detect(ctx)) +} + +// Mock is a helper for tests to mock the detection result. +func Mock(ctx context.Context, product string) context.Context { + return context.WithValue(ctx, productKey, product) +} + +// Product returns the detected agent product name from context. +// Returns empty string if no agent was detected. +// Panics if called before Detect() or Mock(). +func Product(ctx context.Context) string { + v := ctx.Value(productKey) + if v == nil { + panic("agent.Product called without calling agent.Detect first") + } + return v.(string) +} + +// isDetected returns true if an agent has been detected in the context. +// Returns false if Detect() hasn't been called yet. +func isDetected(ctx context.Context) bool { + v := ctx.Value(productKey) + if v == nil { + return false + } + return v.(string) != "" +} diff --git a/libs/agent/agent_test.go b/libs/agent/agent_test.go new file mode 100644 index 0000000000..f25db02bad --- /dev/null +++ b/libs/agent/agent_test.go @@ -0,0 +1,69 @@ +package agent + +import ( + "context" + "testing" + + "github.com/databricks/cli/libs/env" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func clearAllAgentEnvVars(ctx context.Context) context.Context { + for _, a := range knownAgents { + ctx = env.Set(ctx, a.envVar, "") + } + return ctx +} + +func TestDetectEachAgent(t *testing.T) { + for _, a := range knownAgents { + t.Run(a.product, func(t *testing.T) { + ctx := clearAllAgentEnvVars(t.Context()) + ctx = env.Set(ctx, a.envVar, "1") + + assert.Equal(t, a.product, detect(ctx)) + }) + } +} + +func TestDetectViaContext(t *testing.T) { + ctx := clearAllAgentEnvVars(t.Context()) + ctx = env.Set(ctx, knownAgents[0].envVar, "1") + + ctx = Detect(ctx) + + assert.Equal(t, knownAgents[0].product, Product(ctx)) +} + +func TestDetectNoAgent(t *testing.T) { + ctx := clearAllAgentEnvVars(t.Context()) + + ctx = Detect(ctx) + + assert.Equal(t, "", Product(ctx)) +} + +func TestDetectMultipleAgents(t *testing.T) { + ctx := clearAllAgentEnvVars(t.Context()) + for _, a := range knownAgents { + ctx = env.Set(ctx, a.envVar, "1") + } + + assert.Equal(t, "", detect(ctx)) +} + +func TestProductCalledBeforeDetect(t *testing.T) { + ctx := t.Context() + + require.Panics(t, func() { + Product(ctx) + }) +} + +func TestMock(t *testing.T) { + ctx := t.Context() + ctx = Mock(ctx, "test-agent") + + assert.Equal(t, "test-agent", Product(ctx)) +} diff --git a/libs/agent/consent.go b/libs/agent/consent.go new file mode 100644 index 0000000000..ad0c393b5f --- /dev/null +++ b/libs/agent/consent.go @@ -0,0 +1,44 @@ +package agent + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +// gatedFlags maps flag names to human-readable descriptions of what they do. +var gatedFlags = map[string]string{ + "force-lock": "Override another user's active deployment lock, which may corrupt their in-progress deployment", + "auto-approve": "Skip all confirmation prompts for destructive actions like deleting resources", + "force": "Bypass safety checks such as Git branch validation or remote modification detection", +} + +// CheckConsentForFlags validates that an AI agent has not set any gated flags +// without explicit human approval. Returns nil if no agent is detected or if +// no gated flags are set. +func CheckConsentForFlags(cmd *cobra.Command) error { + ctx := cmd.Context() + if ctx == nil { + return nil + } + + if !isDetected(ctx) { + return nil + } + + for flagName, description := range gatedFlags { + f := cmd.Flag(flagName) + if f == nil || !f.Changed { + continue + } + + return fmt.Errorf( + "the --%s flag was used by an AI agent (%s).\n\n"+ + "What this flag does: %s.\n\n"+ + "AI agents must get explicit human approval before using this flag.\n"+ + "Do not retry with this flag unless a human has reviewed and approved it.", + flagName, Product(ctx), description) + } + + return nil +} diff --git a/libs/agent/consent_test.go b/libs/agent/consent_test.go new file mode 100644 index 0000000000..c8ce7af256 --- /dev/null +++ b/libs/agent/consent_test.go @@ -0,0 +1,76 @@ +package agent_test + +import ( + "testing" + + "github.com/databricks/cli/libs/agent" + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" +) + +func TestCheckConsentForFlagsNoAgent(t *testing.T) { + ctx := agent.Mock(t.Context(), "") + cmd := &cobra.Command{Use: "test"} + cmd.Flags().Bool("force-lock", false, "") + cmd.SetContext(ctx) + _ = cmd.Flags().Set("force-lock", "true") + + assert.NoError(t, agent.CheckConsentForFlags(cmd)) +} + +func TestCheckConsentForFlagsNoGatedFlagsSet(t *testing.T) { + ctx := agent.Mock(t.Context(), agent.ClaudeCode) + cmd := &cobra.Command{Use: "test"} + cmd.Flags().Bool("force-lock", false, "") + cmd.SetContext(ctx) + + assert.NoError(t, agent.CheckConsentForFlags(cmd)) +} + +func TestCheckConsentForFlagsBlocksAgent(t *testing.T) { + ctx := agent.Mock(t.Context(), agent.ClaudeCode) + cmd := &cobra.Command{Use: "test"} + cmd.Flags().Bool("force-lock", false, "") + cmd.SetContext(ctx) + _ = cmd.Flags().Set("force-lock", "true") + + err := agent.CheckConsentForFlags(cmd) + assert.Error(t, err) + assert.Contains(t, err.Error(), "AI agent") + assert.Contains(t, err.Error(), "claude-code") + assert.Contains(t, err.Error(), "explicit human approval") +} + +func TestCheckConsentForFlagsAutoApprove(t *testing.T) { + ctx := agent.Mock(t.Context(), agent.Cursor) + cmd := &cobra.Command{Use: "test"} + cmd.Flags().Bool("auto-approve", false, "") + cmd.SetContext(ctx) + _ = cmd.Flags().Set("auto-approve", "true") + + err := agent.CheckConsentForFlags(cmd) + assert.Error(t, err) + assert.Contains(t, err.Error(), "auto-approve") + assert.Contains(t, err.Error(), "cursor") +} + +func TestCheckConsentForFlagsForce(t *testing.T) { + ctx := agent.Mock(t.Context(), agent.Codex) + cmd := &cobra.Command{Use: "test"} + cmd.Flags().Bool("force", false, "") + cmd.SetContext(ctx) + _ = cmd.Flags().Set("force", "true") + + err := agent.CheckConsentForFlags(cmd) + assert.Error(t, err) + assert.Contains(t, err.Error(), "force") + assert.Contains(t, err.Error(), "codex") +} + +func TestCheckConsentForFlagsMissingFlag(t *testing.T) { + ctx := agent.Mock(t.Context(), agent.ClaudeCode) + cmd := &cobra.Command{Use: "test"} + cmd.SetContext(ctx) + + assert.NoError(t, agent.CheckConsentForFlags(cmd)) +} diff --git a/libs/locker/locker.go b/libs/locker/locker.go index 003f169cd3..253c6b3aa7 100644 --- a/libs/locker/locker.go +++ b/libs/locker/locker.go @@ -105,10 +105,16 @@ func (locker *Locker) assertLockHeld(ctx context.Context) error { return err } if activeLockState.ID != locker.State.ID && !activeLockState.IsForced { - return fmt.Errorf("deploy lock acquired by %s at %v. Use --force-lock to override", activeLockState.User, activeLockState.AcquisitionTime) + return fmt.Errorf("deploy lock acquired by %s at %v.\n"+ + "Another deployment is likely in progress. Overriding with --force-lock risks corrupting that deployment.\n"+ + "Only use --force-lock if you are certain the other deployment is no longer active", + activeLockState.User, activeLockState.AcquisitionTime) } if activeLockState.ID != locker.State.ID && activeLockState.IsForced { - return fmt.Errorf("deploy lock force acquired by %s at %v. Use --force-lock to override", activeLockState.User, activeLockState.AcquisitionTime) + return fmt.Errorf("deploy lock force-acquired by %s at %v.\n"+ + "Another deployment is likely in progress. Overriding with --force-lock risks corrupting that deployment.\n"+ + "Only use --force-lock if you are certain the other deployment is no longer active", + activeLockState.User, activeLockState.AcquisitionTime) } return nil } From 416e2b66005f3c56bc2b8fdd34228c3332999313 Mon Sep 17 00:00:00 2001 From: Lennart Kats Date: Fri, 3 Apr 2026 15:35:04 +0200 Subject: [PATCH 04/13] Simplify: replace flag gate with agent notice on existing errors Instead of blocking agents from using --auto-approve/--force-lock/--force with a separate early gate, append an agent notice to the existing error messages. This way agents see the warning in context and are told not to retry with those flags without human approval. Also improves error messages to describe data loss consequences: - destroy: mentions schemas, pipelines, streaming tables, volume files - deploy: mentions schemas, pipelines, volumes may be permanently deleted - lock: explains risk of corrupting in-progress deployment Co-authored-by: Isaac --- .../bind/dashboard/recreation/output.txt | 7 +- .../bind/job/job-abort-bind/output.txt | 2 +- .../recreate/out.bind-fail.direct.txt | 2 +- .../recreate/out.bind-fail.terraform.txt | 2 +- .../bind/pipelines/recreate/output.txt | 7 +- .../pipelines/update/out.bind-fail.direct.txt | 2 +- .../update/out.bind-fail.terraform.txt | 2 +- .../pipelines/auto-approve/output.txt | 7 +- .../resources/pipelines/recreate/output.txt | 7 +- .../resources/schemas/auto-approve/output.txt | 7 +- .../pipelines/deploy/auto-approve/output.txt | 7 +- .../pipelines/deploy/force-lock/output.txt | 4 +- .../pipelines/destroy/auto-approve/output.txt | 6 +- .../pipelines/destroy/force-lock/output.txt | 4 +- bundle/config/mutator/validate_git_details.go | 5 +- .../check_dashboards_modified_remotely.go | 3 +- bundle/deploy/terraform/import.go | 3 +- bundle/phases/bind.go | 3 +- bundle/phases/deploy.go | 10 ++- cmd/apps/delete_bundle.go | 4 - cmd/apps/deploy_bundle.go | 4 - cmd/bundle/deploy.go | 5 -- cmd/bundle/deployment/bind.go | 4 - cmd/bundle/deployment/unbind.go | 4 - cmd/bundle/destroy.go | 14 ++-- cmd/bundle/plan.go | 5 -- cmd/pipelines/deploy.go | 4 - cmd/pipelines/destroy.go | 4 - libs/agent/consent.go | 44 +++-------- libs/agent/consent_test.go | 78 ++++--------------- libs/locker/locker.go | 9 ++- 31 files changed, 88 insertions(+), 181 deletions(-) diff --git a/acceptance/bundle/deployment/bind/dashboard/recreation/output.txt b/acceptance/bundle/deployment/bind/dashboard/recreation/output.txt index 5e83560c7d..457616e77d 100644 --- a/acceptance/bundle/deployment/bind/dashboard/recreation/output.txt +++ b/acceptance/bundle/deployment/bind/dashboard/recreation/output.txt @@ -18,9 +18,10 @@ Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bundle-[UNIQU This action will result in the deletion or recreation of the following dashboards. This will result in changed IDs and permanent URLs of the dashboards that will be recreated: recreate resources.dashboards.dashboard1 -Error: the deployment requires destructive actions (resources will be deleted), but the current console does not support prompting. -Using --auto-approve will skip all confirmation prompts and proceed with the destructive changes. -Only use --auto-approve if you have reviewed the plan and accept the deletions +Error: the deployment requires destructive actions, but the current console does not support prompting. +Schemas, pipelines, and volumes may be permanently deleted, resulting in data loss. +Using --auto-approve will skip all confirmation prompts and proceed with these destructive changes. +Only use --auto-approve if you have reviewed the plan above and accept the consequences. Exit code: 1 diff --git a/acceptance/bundle/deployment/bind/job/job-abort-bind/output.txt b/acceptance/bundle/deployment/bind/job/job-abort-bind/output.txt index d176db8dd6..46b870a276 100644 --- a/acceptance/bundle/deployment/bind/job/job-abort-bind/output.txt +++ b/acceptance/bundle/deployment/bind/job/job-abort-bind/output.txt @@ -3,7 +3,7 @@ Created job with ID: [JOB_ID] === Expect binding to fail without an auto-approve flag: -Error: This bind operation requires user confirmation, but the current console does not support prompting. +Error: this bind operation requires user confirmation, but the current console does not support prompting. === Deploy bundle: >>> [CLI] bundle deploy --force-lock diff --git a/acceptance/bundle/deployment/bind/pipelines/recreate/out.bind-fail.direct.txt b/acceptance/bundle/deployment/bind/pipelines/recreate/out.bind-fail.direct.txt index 0ee95b0069..a518ba5b8d 100644 --- a/acceptance/bundle/deployment/bind/pipelines/recreate/out.bind-fail.direct.txt +++ b/acceptance/bundle/deployment/bind/pipelines/recreate/out.bind-fail.direct.txt @@ -11,7 +11,7 @@ Changes detected: ~ root_path: "/Workspace/Users/someuser@databricks.com/lakeflow_pipeline" -> null ~ storage: "/Shared/old_storage" -> "/Shared/new_storage" -Error: This bind operation requires user confirmation, but the current console does not support prompting. +Error: this bind operation requires user confirmation, but the current console does not support prompting. Using --auto-approve will bind resources without reviewing the changes. Only use --auto-approve if you have reviewed what will be bound. diff --git a/acceptance/bundle/deployment/bind/pipelines/recreate/out.bind-fail.terraform.txt b/acceptance/bundle/deployment/bind/pipelines/recreate/out.bind-fail.terraform.txt index a5dd712d34..550c910c09 100644 --- a/acceptance/bundle/deployment/bind/pipelines/recreate/out.bind-fail.terraform.txt +++ b/acceptance/bundle/deployment/bind/pipelines/recreate/out.bind-fail.terraform.txt @@ -54,7 +54,7 @@ Terraform will perform the following actions: Plan: 1 to add, 0 to change, 1 to destroy. -Error: This bind operation requires user confirmation, but the current console does not support prompting. +Error: this bind operation requires user confirmation, but the current console does not support prompting. Using --auto-approve will bind resources without reviewing the changes. Only use --auto-approve if you have reviewed what will be bound. diff --git a/acceptance/bundle/deployment/bind/pipelines/recreate/output.txt b/acceptance/bundle/deployment/bind/pipelines/recreate/output.txt index 9f282a85d7..b06c036f64 100644 --- a/acceptance/bundle/deployment/bind/pipelines/recreate/output.txt +++ b/acceptance/bundle/deployment/bind/pipelines/recreate/output.txt @@ -16,9 +16,10 @@ Streaming Tables (STs) and Materialized Views (MVs) managed by them. Recreating restore the defined STs and MVs through full refresh. Note that recreation is necessary when pipeline properties such as the 'catalog' or 'storage' are changed: recreate resources.pipelines.foo -Error: the deployment requires destructive actions (resources will be deleted), but the current console does not support prompting. -Using --auto-approve will skip all confirmation prompts and proceed with the destructive changes. -Only use --auto-approve if you have reviewed the plan and accept the deletions +Error: the deployment requires destructive actions, but the current console does not support prompting. +Schemas, pipelines, and volumes may be permanently deleted, resulting in data loss. +Using --auto-approve will skip all confirmation prompts and proceed with these destructive changes. +Only use --auto-approve if you have reviewed the plan above and accept the consequences. >>> [CLI] bundle deploy --auto-approve diff --git a/acceptance/bundle/deployment/bind/pipelines/update/out.bind-fail.direct.txt b/acceptance/bundle/deployment/bind/pipelines/update/out.bind-fail.direct.txt index d1a289868e..22722ba212 100644 --- a/acceptance/bundle/deployment/bind/pipelines/update/out.bind-fail.direct.txt +++ b/acceptance/bundle/deployment/bind/pipelines/update/out.bind-fail.direct.txt @@ -11,7 +11,7 @@ Changes detected: ~ name: "lakeflow-pipeline" -> "test-pipeline" ~ root_path: "/Workspace/Users/[USERNAME]/lakeflow_pipeline" -> null -Error: This bind operation requires user confirmation, but the current console does not support prompting. +Error: this bind operation requires user confirmation, but the current console does not support prompting. Using --auto-approve will bind resources without reviewing the changes. Only use --auto-approve if you have reviewed what will be bound. diff --git a/acceptance/bundle/deployment/bind/pipelines/update/out.bind-fail.terraform.txt b/acceptance/bundle/deployment/bind/pipelines/update/out.bind-fail.terraform.txt index 14ebbe6df2..a8d7204e88 100644 --- a/acceptance/bundle/deployment/bind/pipelines/update/out.bind-fail.terraform.txt +++ b/acceptance/bundle/deployment/bind/pipelines/update/out.bind-fail.terraform.txt @@ -41,7 +41,7 @@ Terraform will perform the following actions: Plan: 0 to add, 1 to change, 0 to destroy. -Error: This bind operation requires user confirmation, but the current console does not support prompting. +Error: this bind operation requires user confirmation, but the current console does not support prompting. Using --auto-approve will bind resources without reviewing the changes. Only use --auto-approve if you have reviewed what will be bound. diff --git a/acceptance/bundle/resources/pipelines/auto-approve/output.txt b/acceptance/bundle/resources/pipelines/auto-approve/output.txt index d71a3f7a9c..ceb4b965b4 100644 --- a/acceptance/bundle/resources/pipelines/auto-approve/output.txt +++ b/acceptance/bundle/resources/pipelines/auto-approve/output.txt @@ -50,9 +50,10 @@ Streaming Tables (STs) and Materialized Views (MVs) managed by them. Recreating restore the defined STs and MVs through full refresh. Note that recreation is necessary when pipeline properties such as the 'catalog' or 'storage' are changed: delete resources.pipelines.bar -Error: the deployment requires destructive actions (resources will be deleted), but the current console does not support prompting. -Using --auto-approve will skip all confirmation prompts and proceed with the destructive changes. -Only use --auto-approve if you have reviewed the plan and accept the deletions +Error: the deployment requires destructive actions, but the current console does not support prompting. +Schemas, pipelines, and volumes may be permanently deleted, resulting in data loss. +Using --auto-approve will skip all confirmation prompts and proceed with these destructive changes. +Only use --auto-approve if you have reviewed the plan above and accept the consequences. Exit code: 1 diff --git a/acceptance/bundle/resources/pipelines/recreate/output.txt b/acceptance/bundle/resources/pipelines/recreate/output.txt index afb961b67b..7dbe7c371f 100644 --- a/acceptance/bundle/resources/pipelines/recreate/output.txt +++ b/acceptance/bundle/resources/pipelines/recreate/output.txt @@ -49,9 +49,10 @@ Streaming Tables (STs) and Materialized Views (MVs) managed by them. Recreating restore the defined STs and MVs through full refresh. Note that recreation is necessary when pipeline properties such as the 'catalog' or 'storage' are changed: recreate resources.pipelines.foo -Error: the deployment requires destructive actions (resources will be deleted), but the current console does not support prompting. -Using --auto-approve will skip all confirmation prompts and proceed with the destructive changes. -Only use --auto-approve if you have reviewed the plan and accept the deletions +Error: the deployment requires destructive actions, but the current console does not support prompting. +Schemas, pipelines, and volumes may be permanently deleted, resulting in data loss. +Using --auto-approve will skip all confirmation prompts and proceed with these destructive changes. +Only use --auto-approve if you have reviewed the plan above and accept the consequences. Exit code: 1 diff --git a/acceptance/bundle/resources/schemas/auto-approve/output.txt b/acceptance/bundle/resources/schemas/auto-approve/output.txt index f2a4563c10..fae3cc8dfd 100644 --- a/acceptance/bundle/resources/schemas/auto-approve/output.txt +++ b/acceptance/bundle/resources/schemas/auto-approve/output.txt @@ -57,9 +57,10 @@ Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/[UNIQUE_NAME]/file This action will result in the deletion or recreation of the following UC schemas. Any underlying data may be lost: delete resources.schemas.bar -Error: the deployment requires destructive actions (resources will be deleted), but the current console does not support prompting. -Using --auto-approve will skip all confirmation prompts and proceed with the destructive changes. -Only use --auto-approve if you have reviewed the plan and accept the deletions +Error: the deployment requires destructive actions, but the current console does not support prompting. +Schemas, pipelines, and volumes may be permanently deleted, resulting in data loss. +Using --auto-approve will skip all confirmation prompts and proceed with these destructive changes. +Only use --auto-approve if you have reviewed the plan above and accept the consequences. === Test cleanup diff --git a/acceptance/pipelines/deploy/auto-approve/output.txt b/acceptance/pipelines/deploy/auto-approve/output.txt index 53066ab958..b96a88321a 100644 --- a/acceptance/pipelines/deploy/auto-approve/output.txt +++ b/acceptance/pipelines/deploy/auto-approve/output.txt @@ -18,9 +18,10 @@ Streaming Tables (STs) and Materialized Views (MVs) managed by them. Recreating restore the defined STs and MVs through full refresh. Note that recreation is necessary when pipeline properties such as the 'catalog' or 'storage' are changed: delete resources.pipelines.foo -Error: the deployment requires destructive actions (resources will be deleted), but the current console does not support prompting. -Using --auto-approve will skip all confirmation prompts and proceed with the destructive changes. -Only use --auto-approve if you have reviewed the plan and accept the deletions +Error: the deployment requires destructive actions, but the current console does not support prompting. +Schemas, pipelines, and volumes may be permanently deleted, resulting in data loss. +Using --auto-approve will skip all confirmation prompts and proceed with these destructive changes. +Only use --auto-approve if you have reviewed the plan above and accept the consequences. Exit code: 1 diff --git a/acceptance/pipelines/deploy/force-lock/output.txt b/acceptance/pipelines/deploy/force-lock/output.txt index 86c6f63e62..a85ef8da5f 100644 --- a/acceptance/pipelines/deploy/force-lock/output.txt +++ b/acceptance/pipelines/deploy/force-lock/output.txt @@ -6,10 +6,10 @@ >>> errcode [CLI] pipelines deploy Error: Failed to acquire deployment lock: deploy lock acquired by user-with-lock@databricks.com at [TIMESTAMP] +0000 UTC. Another deployment is likely in progress. Overriding with --force-lock risks corrupting that deployment. -Only use --force-lock if you are certain the other deployment is no longer active +Only use --force-lock if you are certain the other deployment is no longer active. Error: deploy lock acquired by user-with-lock@databricks.com at [TIMESTAMP] +0000 UTC. Another deployment is likely in progress. Overriding with --force-lock risks corrupting that deployment. -Only use --force-lock if you are certain the other deployment is no longer active +Only use --force-lock if you are certain the other deployment is no longer active. Exit code: 1 diff --git a/acceptance/pipelines/destroy/auto-approve/output.txt b/acceptance/pipelines/destroy/auto-approve/output.txt index d71a359f4b..16250f7fe1 100644 --- a/acceptance/pipelines/destroy/auto-approve/output.txt +++ b/acceptance/pipelines/destroy/auto-approve/output.txt @@ -8,9 +8,11 @@ Deployment complete! View your pipeline my_pipeline here: [DATABRICKS_URL]/pipelines/[UUID]?o=[NUMID] >>> errcode [CLI] pipelines destroy -Error: this will permanently destroy all deployed resources in the bundle target. +Error: this will permanently destroy all resources and data in the bundle target. +This includes deleting schemas and their underlying data, pipelines and their streaming +tables, managed volume files, and all workspace files in the deployment directory. Using --auto-approve will skip all confirmation prompts and proceed with the destruction. -Only use --auto-approve if you are certain you want to delete all resources +Only use --auto-approve if you are certain you want to permanently delete everything. Exit code: 1 diff --git a/acceptance/pipelines/destroy/force-lock/output.txt b/acceptance/pipelines/destroy/force-lock/output.txt index c3d6264e7f..518491c099 100644 --- a/acceptance/pipelines/destroy/force-lock/output.txt +++ b/acceptance/pipelines/destroy/force-lock/output.txt @@ -13,10 +13,10 @@ View your pipeline foo here: [DATABRICKS_URL]/pipelines/[UUID]?o=[NUMID] >>> errcode [CLI] pipelines destroy --auto-approve Error: Failed to acquire deployment lock: deploy lock acquired by user-with-lock@databricks.com at [TIMESTAMP] +0000 UTC. Another deployment is likely in progress. Overriding with --force-lock risks corrupting that deployment. -Only use --force-lock if you are certain the other deployment is no longer active +Only use --force-lock if you are certain the other deployment is no longer active. Error: deploy lock acquired by user-with-lock@databricks.com at [TIMESTAMP] +0000 UTC. Another deployment is likely in progress. Overriding with --force-lock risks corrupting that deployment. -Only use --force-lock if you are certain the other deployment is no longer active +Only use --force-lock if you are certain the other deployment is no longer active. Exit code: 1 diff --git a/bundle/config/mutator/validate_git_details.go b/bundle/config/mutator/validate_git_details.go index f7b413db7f..a22b1cb52c 100644 --- a/bundle/config/mutator/validate_git_details.go +++ b/bundle/config/mutator/validate_git_details.go @@ -4,6 +4,7 @@ import ( "context" "github.com/databricks/cli/bundle" + "github.com/databricks/cli/libs/agent" "github.com/databricks/cli/libs/diag" ) @@ -23,8 +24,8 @@ func (m *validateGitDetails) Apply(ctx context.Context, b *bundle.Bundle) diag.D } if b.Config.Bundle.Git.Branch != b.Config.Bundle.Git.ActualBranch && !b.Config.Bundle.Force { - return diag.Errorf("not on the right Git branch:\n expected according to configuration: %s\n actual: %s\nUsing --force will deploy from branch %s, which may push unexpected code to the target.\nOnly use --force if you intentionally want to deploy from this branch", - b.Config.Bundle.Git.Branch, b.Config.Bundle.Git.ActualBranch, b.Config.Bundle.Git.ActualBranch) + return diag.Errorf("not on the right Git branch:\n expected according to configuration: %s\n actual: %s\nUsing --force will deploy from branch %s, which may push unexpected code to the target.\nOnly use --force if you intentionally want to deploy from this branch.%s", + b.Config.Bundle.Git.Branch, b.Config.Bundle.Git.ActualBranch, b.Config.Bundle.Git.ActualBranch, agent.AgentNotice(ctx)) } return nil } diff --git a/bundle/deploy/terraform/check_dashboards_modified_remotely.go b/bundle/deploy/terraform/check_dashboards_modified_remotely.go index 276fd77659..fb6687605d 100644 --- a/bundle/deploy/terraform/check_dashboards_modified_remotely.go +++ b/bundle/deploy/terraform/check_dashboards_modified_remotely.go @@ -7,6 +7,7 @@ import ( "github.com/databricks/cli/bundle" "github.com/databricks/cli/bundle/config/engine" + "github.com/databricks/cli/libs/agent" "github.com/databricks/cli/libs/diag" "github.com/databricks/cli/libs/dyn" ) @@ -122,7 +123,7 @@ func (l *checkDashboardsModifiedRemotely) Apply(ctx context.Context, b *bundle.B "before proceeding with the deployment.\n" + "\n" + "Use --force only if you want to discard the remote changes and overwrite\n" + - "the dashboard with your local version.", + "the dashboard with your local version." + agent.AgentNotice(ctx), Paths: []dyn.Path{path}, Locations: []dyn.Location{loc}, }) diff --git a/bundle/deploy/terraform/import.go b/bundle/deploy/terraform/import.go index ad09bc19f3..330dbb1dbe 100644 --- a/bundle/deploy/terraform/import.go +++ b/bundle/deploy/terraform/import.go @@ -10,6 +10,7 @@ import ( "strings" "github.com/databricks/cli/bundle" + "github.com/databricks/cli/libs/agent" "github.com/databricks/cli/libs/cmdio" "github.com/databricks/cli/libs/diag" "github.com/hashicorp/terraform-exec/tfexec" @@ -72,7 +73,7 @@ func (m *importResource) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagn cmdio.LogString(ctx, output) if !cmdio.IsPromptSupported(ctx) { - return diag.Errorf("This bind operation requires user confirmation, but the current console does not support prompting.\nUsing --auto-approve will bind resources without reviewing the changes.\nOnly use --auto-approve if you have reviewed what will be bound.") + return diag.Errorf("this bind operation requires user confirmation, but the current console does not support prompting.\nUsing --auto-approve will bind resources without reviewing the changes.\nOnly use --auto-approve if you have reviewed what will be bound.%s", agent.AgentNotice(ctx)) } ans, err := cmdio.AskYesOrNo(ctx, "Confirm import changes? Changes will be remotely applied only after running 'bundle deploy'.") diff --git a/bundle/phases/bind.go b/bundle/phases/bind.go index 1c60912607..59bf93cedf 100644 --- a/bundle/phases/bind.go +++ b/bundle/phases/bind.go @@ -12,6 +12,7 @@ import ( "github.com/databricks/cli/bundle/deploy/terraform" "github.com/databricks/cli/bundle/deployplan" "github.com/databricks/cli/bundle/statemgmt" + "github.com/databricks/cli/libs/agent" "github.com/databricks/cli/libs/cmdio" "github.com/databricks/cli/libs/log" "github.com/databricks/cli/libs/logdiag" @@ -73,7 +74,7 @@ func Bind(ctx context.Context, b *bundle.Bundle, opts *terraform.BindOptions) { if !cmdio.IsPromptSupported(ctx) { result.Cancel() - logdiag.LogError(ctx, errors.New("This bind operation requires user confirmation, but the current console does not support prompting.\nUsing --auto-approve will bind resources without reviewing the changes.\nOnly use --auto-approve if you have reviewed what will be bound.")) //nolint + logdiag.LogError(ctx, fmt.Errorf("this bind operation requires user confirmation, but the current console does not support prompting.\nUsing --auto-approve will bind resources without reviewing the changes.\nOnly use --auto-approve if you have reviewed what will be bound.%s", agent.AgentNotice(ctx))) //nolint return } diff --git a/bundle/phases/deploy.go b/bundle/phases/deploy.go index 76271ac92a..bffc57d8fd 100644 --- a/bundle/phases/deploy.go +++ b/bundle/phases/deploy.go @@ -3,6 +3,7 @@ package phases import ( "context" "errors" + "fmt" "github.com/databricks/cli/bundle" "github.com/databricks/cli/bundle/artifacts" @@ -20,6 +21,7 @@ import ( "github.com/databricks/cli/bundle/permissions" "github.com/databricks/cli/bundle/scripts" "github.com/databricks/cli/bundle/statemgmt" + "github.com/databricks/cli/libs/agent" "github.com/databricks/cli/libs/cmdio" "github.com/databricks/cli/libs/log" "github.com/databricks/cli/libs/logdiag" @@ -85,9 +87,11 @@ func approvalForDeploy(ctx context.Context, b *bundle.Bundle, plan *deployplan.P } if !cmdio.IsPromptSupported(ctx) { - return false, errors.New("the deployment requires destructive actions (resources will be deleted), but the current console does not support prompting.\n" + - "Using --auto-approve will skip all confirmation prompts and proceed with the destructive changes.\n" + - "Only use --auto-approve if you have reviewed the plan and accept the deletions") + return false, fmt.Errorf("the deployment requires destructive actions, but the current console does not support prompting.\n"+ + "Schemas, pipelines, and volumes may be permanently deleted, resulting in data loss.\n"+ + "Using --auto-approve will skip all confirmation prompts and proceed with these destructive changes.\n"+ + "Only use --auto-approve if you have reviewed the plan above and accept the consequences.%s", + agent.AgentNotice(ctx)) } cmdio.LogString(ctx, "") diff --git a/cmd/apps/delete_bundle.go b/cmd/apps/delete_bundle.go index 623663a3d3..b4c0e72f87 100644 --- a/cmd/apps/delete_bundle.go +++ b/cmd/apps/delete_bundle.go @@ -2,7 +2,6 @@ package apps import ( "github.com/databricks/cli/cmd/bundle" - "github.com/databricks/cli/libs/agent" "github.com/databricks/databricks-sdk-go/service/apps" "github.com/spf13/cobra" ) @@ -23,9 +22,6 @@ func BundleDeleteOverrideWithWrapper(wrapError ErrorWrapper) func(*cobra.Command originalRunE := deleteCmd.RunE deleteCmd.RunE = func(cmd *cobra.Command, args []string) error { - if err := agent.CheckConsentForFlags(cmd); err != nil { - return err - } if len(args) == 0 && hasBundleConfig() { return bundle.CommandBundleDestroy(cmd, args, autoApprove, forceDestroy) } diff --git a/cmd/apps/deploy_bundle.go b/cmd/apps/deploy_bundle.go index d002742411..86b8c8bb1e 100644 --- a/cmd/apps/deploy_bundle.go +++ b/cmd/apps/deploy_bundle.go @@ -12,7 +12,6 @@ import ( "github.com/databricks/cli/bundle/run" "github.com/databricks/cli/cmd/bundle/utils" "github.com/databricks/cli/cmd/root" - "github.com/databricks/cli/libs/agent" "github.com/databricks/cli/libs/apps/prompt" "github.com/databricks/cli/libs/apps/validation" "github.com/databricks/cli/libs/cmdio" @@ -52,9 +51,6 @@ func BundleDeployOverrideWithWrapper(wrapError ErrorWrapper) func(*cobra.Command originalRunE := deployCmd.RunE deployCmd.RunE = func(cmd *cobra.Command, args []string) error { - if err := agent.CheckConsentForFlags(cmd); err != nil { - return err - } if len(args) == 0 { b := root.TryConfigureBundle(cmd) if b != nil { diff --git a/cmd/bundle/deploy.go b/cmd/bundle/deploy.go index a3a040a55c..31ffe7090d 100644 --- a/cmd/bundle/deploy.go +++ b/cmd/bundle/deploy.go @@ -6,7 +6,6 @@ import ( "github.com/databricks/cli/bundle" "github.com/databricks/cli/cmd/bundle/utils" "github.com/databricks/cli/cmd/root" - "github.com/databricks/cli/libs/agent" "github.com/spf13/cobra" ) @@ -45,10 +44,6 @@ See https://docs.databricks.com/en/dev-tools/bundles/index.html for more informa cmd.Flags().MarkHidden("verbose") cmd.RunE = func(cmd *cobra.Command, args []string) error { - if err := agent.CheckConsentForFlags(cmd); err != nil { - return err - } - _, err := utils.ProcessBundle(cmd, utils.ProcessOptions{ InitFunc: func(b *bundle.Bundle) { b.Config.Bundle.Force = force diff --git a/cmd/bundle/deployment/bind.go b/cmd/bundle/deployment/bind.go index de16af1f49..4b72cdd9e6 100644 --- a/cmd/bundle/deployment/bind.go +++ b/cmd/bundle/deployment/bind.go @@ -2,7 +2,6 @@ package deployment import ( "github.com/databricks/cli/cmd/root" - "github.com/databricks/cli/libs/agent" "github.com/databricks/cli/libs/cmdio" "github.com/spf13/cobra" ) @@ -51,9 +50,6 @@ Any manual changes made in the workspace UI may be overwritten on deployment.`, cmd.Flags().BoolVar(&forceLock, "force-lock", false, "Force acquisition of deployment lock.") cmd.RunE = func(cmd *cobra.Command, args []string) error { - if err := agent.CheckConsentForFlags(cmd); err != nil { - return err - } err := BindResource(cmd, args[0], args[1], autoApprove, forceLock, false) if err != nil { return err diff --git a/cmd/bundle/deployment/unbind.go b/cmd/bundle/deployment/unbind.go index 077e49f251..e382a0d942 100644 --- a/cmd/bundle/deployment/unbind.go +++ b/cmd/bundle/deployment/unbind.go @@ -6,7 +6,6 @@ import ( "github.com/databricks/cli/bundle/phases" "github.com/databricks/cli/cmd/bundle/utils" "github.com/databricks/cli/cmd/root" - "github.com/databricks/cli/libs/agent" "github.com/databricks/cli/libs/logdiag" "github.com/spf13/cobra" ) @@ -53,9 +52,6 @@ To re-bind the resource later, use: cmd.Flags().BoolVar(&forceLock, "force-lock", false, "Force acquisition of deployment lock.") cmd.RunE = func(cmd *cobra.Command, args []string) error { - if err := agent.CheckConsentForFlags(cmd); err != nil { - return err - } b, err := utils.ProcessBundle(cmd, utils.ProcessOptions{ InitFunc: func(b *bundle.Bundle) { b.Config.Bundle.Deployment.Lock.Force = forceLock diff --git a/cmd/bundle/destroy.go b/cmd/bundle/destroy.go index af851f62d9..180116b756 100644 --- a/cmd/bundle/destroy.go +++ b/cmd/bundle/destroy.go @@ -3,7 +3,7 @@ package bundle import ( - "errors" + "fmt" "github.com/databricks/cli/bundle" "github.com/databricks/cli/bundle/phases" @@ -40,9 +40,6 @@ Typical use cases: cmd.Flags().BoolVar(&forceDestroy, "force-lock", false, "Force acquisition of deployment lock.") cmd.RunE = func(cmd *cobra.Command, args []string) error { - if err := agent.CheckConsentForFlags(cmd); err != nil { - return err - } return CommandBundleDestroy(cmd, args, autoApprove, forceDestroy) } @@ -52,9 +49,12 @@ Typical use cases: func CommandBundleDestroy(cmd *cobra.Command, args []string, autoApprove, forceDestroy bool) error { // We require auto-approve for non-interactive terminals since prompts are not possible. if !cmdio.IsPromptSupported(cmd.Context()) && !autoApprove { - return errors.New("this will permanently destroy all deployed resources in the bundle target.\n" + - "Using --auto-approve will skip all confirmation prompts and proceed with the destruction.\n" + - "Only use --auto-approve if you are certain you want to delete all resources") + return fmt.Errorf("this will permanently destroy all resources and data in the bundle target.\n"+ + "This includes deleting schemas and their underlying data, pipelines and their streaming\n"+ + "tables, managed volume files, and all workspace files in the deployment directory.\n"+ + "Using --auto-approve will skip all confirmation prompts and proceed with the destruction.\n"+ + "Only use --auto-approve if you are certain you want to permanently delete everything.%s", + agent.AgentNotice(cmd.Context())) } // Check if context is already initialized (e.g., when called from apps delete override) diff --git a/cmd/bundle/plan.go b/cmd/bundle/plan.go index 362cb0d815..e3dd63929e 100644 --- a/cmd/bundle/plan.go +++ b/cmd/bundle/plan.go @@ -10,7 +10,6 @@ import ( "github.com/databricks/cli/bundle/phases" "github.com/databricks/cli/cmd/bundle/utils" "github.com/databricks/cli/cmd/root" - "github.com/databricks/cli/libs/agent" "github.com/databricks/cli/libs/flags" "github.com/databricks/cli/libs/logdiag" "github.com/spf13/cobra" @@ -35,10 +34,6 @@ It is useful for previewing changes before running 'bundle deploy'.`, cmd.Flags().MarkDeprecated("compute-id", "use --cluster-id instead") cmd.RunE = func(cmd *cobra.Command, args []string) error { - if err := agent.CheckConsentForFlags(cmd); err != nil { - return err - } - opts := utils.ProcessOptions{ AlwaysPull: true, FastValidate: true, diff --git a/cmd/pipelines/deploy.go b/cmd/pipelines/deploy.go index 3656a85e10..d966a962d3 100644 --- a/cmd/pipelines/deploy.go +++ b/cmd/pipelines/deploy.go @@ -9,7 +9,6 @@ import ( "github.com/databricks/cli/bundle/config/mutator" "github.com/databricks/cli/cmd/bundle/utils" "github.com/databricks/cli/cmd/root" - "github.com/databricks/cli/libs/agent" "github.com/databricks/cli/libs/cmdio" "github.com/databricks/cli/libs/logdiag" libsutils "github.com/databricks/cli/libs/utils" @@ -36,9 +35,6 @@ func deployCommand() *cobra.Command { cmd.Flags().MarkHidden("verbose") cmd.RunE = func(cmd *cobra.Command, args []string) error { - if err := agent.CheckConsentForFlags(cmd); err != nil { - return err - } b, err := utils.ProcessBundle(cmd, utils.ProcessOptions{ InitFunc: func(b *bundle.Bundle) { b.Config.Bundle.Deployment.Lock.Force = forceLock diff --git a/cmd/pipelines/destroy.go b/cmd/pipelines/destroy.go index 99e7f59a18..9f98d525fa 100644 --- a/cmd/pipelines/destroy.go +++ b/cmd/pipelines/destroy.go @@ -4,7 +4,6 @@ package pipelines import ( "github.com/databricks/cli/cmd/bundle" - "github.com/databricks/cli/libs/agent" "github.com/spf13/cobra" ) @@ -21,9 +20,6 @@ func destroyCommand() *cobra.Command { cmd.Flags().BoolVar(&forceDestroy, "force-lock", false, "Force acquisition of deployment lock.") cmd.RunE = func(cmd *cobra.Command, args []string) error { - if err := agent.CheckConsentForFlags(cmd); err != nil { - return err - } return bundle.CommandBundleDestroy(cmd, args, autoApprove, forceDestroy) } diff --git a/libs/agent/consent.go b/libs/agent/consent.go index ad0c393b5f..ed73cd5361 100644 --- a/libs/agent/consent.go +++ b/libs/agent/consent.go @@ -1,44 +1,18 @@ package agent import ( + "context" "fmt" - - "github.com/spf13/cobra" ) -// gatedFlags maps flag names to human-readable descriptions of what they do. -var gatedFlags = map[string]string{ - "force-lock": "Override another user's active deployment lock, which may corrupt their in-progress deployment", - "auto-approve": "Skip all confirmation prompts for destructive actions like deleting resources", - "force": "Bypass safety checks such as Git branch validation or remote modification detection", -} - -// CheckConsentForFlags validates that an AI agent has not set any gated flags -// without explicit human approval. Returns nil if no agent is detected or if -// no gated flags are set. -func CheckConsentForFlags(cmd *cobra.Command) error { - ctx := cmd.Context() - if ctx == nil { - return nil - } - +// AgentNotice returns a notice string for AI agents to append to error messages. +// Returns an empty string if no agent is detected. +func AgentNotice(ctx context.Context) string { if !isDetected(ctx) { - return nil + return "" } - - for flagName, description := range gatedFlags { - f := cmd.Flag(flagName) - if f == nil || !f.Changed { - continue - } - - return fmt.Errorf( - "the --%s flag was used by an AI agent (%s).\n\n"+ - "What this flag does: %s.\n\n"+ - "AI agents must get explicit human approval before using this flag.\n"+ - "Do not retry with this flag unless a human has reviewed and approved it.", - flagName, Product(ctx), description) - } - - return nil + return fmt.Sprintf("\n\nNote for AI agents (%s): do not retry this operation with --auto-approve,\n"+ + "--force-lock, or --force unless the user has explicitly approved it.\n"+ + "These flags skip safety prompts and may cause irreversible data loss.", + Product(ctx)) } diff --git a/libs/agent/consent_test.go b/libs/agent/consent_test.go index c8ce7af256..75b3cbaed0 100644 --- a/libs/agent/consent_test.go +++ b/libs/agent/consent_test.go @@ -1,76 +1,26 @@ -package agent_test +package agent import ( "testing" - "github.com/databricks/cli/libs/agent" - "github.com/spf13/cobra" "github.com/stretchr/testify/assert" ) -func TestCheckConsentForFlagsNoAgent(t *testing.T) { - ctx := agent.Mock(t.Context(), "") - cmd := &cobra.Command{Use: "test"} - cmd.Flags().Bool("force-lock", false, "") - cmd.SetContext(ctx) - _ = cmd.Flags().Set("force-lock", "true") - - assert.NoError(t, agent.CheckConsentForFlags(cmd)) -} - -func TestCheckConsentForFlagsNoGatedFlagsSet(t *testing.T) { - ctx := agent.Mock(t.Context(), agent.ClaudeCode) - cmd := &cobra.Command{Use: "test"} - cmd.Flags().Bool("force-lock", false, "") - cmd.SetContext(ctx) - - assert.NoError(t, agent.CheckConsentForFlags(cmd)) -} - -func TestCheckConsentForFlagsBlocksAgent(t *testing.T) { - ctx := agent.Mock(t.Context(), agent.ClaudeCode) - cmd := &cobra.Command{Use: "test"} - cmd.Flags().Bool("force-lock", false, "") - cmd.SetContext(ctx) - _ = cmd.Flags().Set("force-lock", "true") - - err := agent.CheckConsentForFlags(cmd) - assert.Error(t, err) - assert.Contains(t, err.Error(), "AI agent") - assert.Contains(t, err.Error(), "claude-code") - assert.Contains(t, err.Error(), "explicit human approval") -} - -func TestCheckConsentForFlagsAutoApprove(t *testing.T) { - ctx := agent.Mock(t.Context(), agent.Cursor) - cmd := &cobra.Command{Use: "test"} - cmd.Flags().Bool("auto-approve", false, "") - cmd.SetContext(ctx) - _ = cmd.Flags().Set("auto-approve", "true") - - err := agent.CheckConsentForFlags(cmd) - assert.Error(t, err) - assert.Contains(t, err.Error(), "auto-approve") - assert.Contains(t, err.Error(), "cursor") +func TestAgentNoticeNoAgent(t *testing.T) { + ctx := Mock(t.Context(), "") + assert.Empty(t, AgentNotice(ctx)) } -func TestCheckConsentForFlagsForce(t *testing.T) { - ctx := agent.Mock(t.Context(), agent.Codex) - cmd := &cobra.Command{Use: "test"} - cmd.Flags().Bool("force", false, "") - cmd.SetContext(ctx) - _ = cmd.Flags().Set("force", "true") - - err := agent.CheckConsentForFlags(cmd) - assert.Error(t, err) - assert.Contains(t, err.Error(), "force") - assert.Contains(t, err.Error(), "codex") +func TestAgentNoticeWithAgent(t *testing.T) { + ctx := Mock(t.Context(), ClaudeCode) + notice := AgentNotice(ctx) + assert.Contains(t, notice, "claude-code") + assert.Contains(t, notice, "do not retry") + assert.Contains(t, notice, "irreversible data loss") } -func TestCheckConsentForFlagsMissingFlag(t *testing.T) { - ctx := agent.Mock(t.Context(), agent.ClaudeCode) - cmd := &cobra.Command{Use: "test"} - cmd.SetContext(ctx) - - assert.NoError(t, agent.CheckConsentForFlags(cmd)) +func TestAgentNoticeBeforeDetect(t *testing.T) { + ctx := t.Context() + // Should not panic, just return empty. + assert.Empty(t, AgentNotice(ctx)) } diff --git a/libs/locker/locker.go b/libs/locker/locker.go index 253c6b3aa7..b32556ae71 100644 --- a/libs/locker/locker.go +++ b/libs/locker/locker.go @@ -11,6 +11,7 @@ import ( "slices" "time" + "github.com/databricks/cli/libs/agent" "github.com/databricks/cli/libs/filer" "github.com/databricks/databricks-sdk-go" "github.com/google/uuid" @@ -107,14 +108,14 @@ func (locker *Locker) assertLockHeld(ctx context.Context) error { if activeLockState.ID != locker.State.ID && !activeLockState.IsForced { return fmt.Errorf("deploy lock acquired by %s at %v.\n"+ "Another deployment is likely in progress. Overriding with --force-lock risks corrupting that deployment.\n"+ - "Only use --force-lock if you are certain the other deployment is no longer active", - activeLockState.User, activeLockState.AcquisitionTime) + "Only use --force-lock if you are certain the other deployment is no longer active.%s", + activeLockState.User, activeLockState.AcquisitionTime, agent.AgentNotice(ctx)) } if activeLockState.ID != locker.State.ID && activeLockState.IsForced { return fmt.Errorf("deploy lock force-acquired by %s at %v.\n"+ "Another deployment is likely in progress. Overriding with --force-lock risks corrupting that deployment.\n"+ - "Only use --force-lock if you are certain the other deployment is no longer active", - activeLockState.User, activeLockState.AcquisitionTime) + "Only use --force-lock if you are certain the other deployment is no longer active.%s", + activeLockState.User, activeLockState.AcquisitionTime, agent.AgentNotice(ctx)) } return nil } From f9a8eeae12eb3479263b4a94af8f6f266b04b52e Mon Sep 17 00:00:00 2001 From: Lennart Kats Date: Fri, 3 Apr 2026 16:02:12 +0200 Subject: [PATCH 05/13] Use SDK agent detection for AgentNotice, improve error phrasing Replace custom agent detection with useragent.AgentProvider() from the Go SDK, which already detects AI agents via environment variables. Rephrase all error messages to lead with the situation, then explain what the flag does and its consequences, rather than assuming the reader already knows what the flag is for. Co-authored-by: Isaac --- .../bind/dashboard/recreation/output.txt | 5 +- .../recreate/out.bind-fail.direct.txt | 3 +- .../recreate/out.bind-fail.terraform.txt | 3 +- .../bind/pipelines/recreate/output.txt | 5 +- .../pipelines/update/out.bind-fail.direct.txt | 3 +- .../update/out.bind-fail.terraform.txt | 3 +- .../dashboards/detect-change/output.txt | 12 +-- .../pipelines/auto-approve/output.txt | 5 +- .../resources/pipelines/recreate/output.txt | 5 +- .../resources/schemas/auto-approve/output.txt | 5 +- .../pipelines/deploy/auto-approve/output.txt | 5 +- .../pipelines/deploy/force-lock/output.txt | 8 +- .../pipelines/destroy/auto-approve/output.txt | 8 +- .../pipelines/destroy/force-lock/output.txt | 8 +- bundle/config/mutator/validate_git_details.go | 4 +- .../mutator/validate_git_details_test.go | 7 +- .../check_dashboards_modified_remotely.go | 4 +- bundle/deploy/terraform/import.go | 2 +- bundle/phases/bind.go | 2 +- bundle/phases/deploy.go | 7 +- cmd/bundle/destroy.go | 10 +- cmd/root/root.go | 2 - libs/agent/agent.go | 98 ------------------- libs/agent/{consent.go => agent_notice.go} | 14 +-- libs/agent/agent_test.go | 69 ------------- libs/agent/consent_test.go | 26 ----- libs/locker/locker.go | 12 +-- 27 files changed, 65 insertions(+), 270 deletions(-) delete mode 100644 libs/agent/agent.go rename libs/agent/{consent.go => agent_notice.go} (51%) delete mode 100644 libs/agent/agent_test.go delete mode 100644 libs/agent/consent_test.go diff --git a/acceptance/bundle/deployment/bind/dashboard/recreation/output.txt b/acceptance/bundle/deployment/bind/dashboard/recreation/output.txt index 457616e77d..b2b923b21e 100644 --- a/acceptance/bundle/deployment/bind/dashboard/recreation/output.txt +++ b/acceptance/bundle/deployment/bind/dashboard/recreation/output.txt @@ -19,9 +19,8 @@ This action will result in the deletion or recreation of the following dashboard This will result in changed IDs and permanent URLs of the dashboards that will be recreated: recreate resources.dashboards.dashboard1 Error: the deployment requires destructive actions, but the current console does not support prompting. -Schemas, pipelines, and volumes may be permanently deleted, resulting in data loss. -Using --auto-approve will skip all confirmation prompts and proceed with these destructive changes. -Only use --auto-approve if you have reviewed the plan above and accept the consequences. +To proceed, use --auto-approve after reviewing the plan above. Deleted schemas, pipelines, +and volumes may result in permanent data loss. Exit code: 1 diff --git a/acceptance/bundle/deployment/bind/pipelines/recreate/out.bind-fail.direct.txt b/acceptance/bundle/deployment/bind/pipelines/recreate/out.bind-fail.direct.txt index a518ba5b8d..882947b561 100644 --- a/acceptance/bundle/deployment/bind/pipelines/recreate/out.bind-fail.direct.txt +++ b/acceptance/bundle/deployment/bind/pipelines/recreate/out.bind-fail.direct.txt @@ -12,6 +12,5 @@ Changes detected: ~ storage: "/Shared/old_storage" -> "/Shared/new_storage" Error: this bind operation requires user confirmation, but the current console does not support prompting. -Using --auto-approve will bind resources without reviewing the changes. -Only use --auto-approve if you have reviewed what will be bound. +To proceed without confirmation, use --auto-approve after reviewing the changes above. diff --git a/acceptance/bundle/deployment/bind/pipelines/recreate/out.bind-fail.terraform.txt b/acceptance/bundle/deployment/bind/pipelines/recreate/out.bind-fail.terraform.txt index 550c910c09..eec5c440c0 100644 --- a/acceptance/bundle/deployment/bind/pipelines/recreate/out.bind-fail.terraform.txt +++ b/acceptance/bundle/deployment/bind/pipelines/recreate/out.bind-fail.terraform.txt @@ -55,6 +55,5 @@ Plan: 1 to add, 0 to change, 1 to destroy. Error: this bind operation requires user confirmation, but the current console does not support prompting. -Using --auto-approve will bind resources without reviewing the changes. -Only use --auto-approve if you have reviewed what will be bound. +To proceed without confirmation, use --auto-approve after reviewing the changes above. diff --git a/acceptance/bundle/deployment/bind/pipelines/recreate/output.txt b/acceptance/bundle/deployment/bind/pipelines/recreate/output.txt index b06c036f64..d07a9f31b9 100644 --- a/acceptance/bundle/deployment/bind/pipelines/recreate/output.txt +++ b/acceptance/bundle/deployment/bind/pipelines/recreate/output.txt @@ -17,9 +17,8 @@ restore the defined STs and MVs through full refresh. Note that recreation is ne properties such as the 'catalog' or 'storage' are changed: recreate resources.pipelines.foo Error: the deployment requires destructive actions, but the current console does not support prompting. -Schemas, pipelines, and volumes may be permanently deleted, resulting in data loss. -Using --auto-approve will skip all confirmation prompts and proceed with these destructive changes. -Only use --auto-approve if you have reviewed the plan above and accept the consequences. +To proceed, use --auto-approve after reviewing the plan above. Deleted schemas, pipelines, +and volumes may result in permanent data loss. >>> [CLI] bundle deploy --auto-approve diff --git a/acceptance/bundle/deployment/bind/pipelines/update/out.bind-fail.direct.txt b/acceptance/bundle/deployment/bind/pipelines/update/out.bind-fail.direct.txt index 22722ba212..75aca47b26 100644 --- a/acceptance/bundle/deployment/bind/pipelines/update/out.bind-fail.direct.txt +++ b/acceptance/bundle/deployment/bind/pipelines/update/out.bind-fail.direct.txt @@ -12,6 +12,5 @@ Changes detected: ~ root_path: "/Workspace/Users/[USERNAME]/lakeflow_pipeline" -> null Error: this bind operation requires user confirmation, but the current console does not support prompting. -Using --auto-approve will bind resources without reviewing the changes. -Only use --auto-approve if you have reviewed what will be bound. +To proceed without confirmation, use --auto-approve after reviewing the changes above. diff --git a/acceptance/bundle/deployment/bind/pipelines/update/out.bind-fail.terraform.txt b/acceptance/bundle/deployment/bind/pipelines/update/out.bind-fail.terraform.txt index a8d7204e88..723c5583f5 100644 --- a/acceptance/bundle/deployment/bind/pipelines/update/out.bind-fail.terraform.txt +++ b/acceptance/bundle/deployment/bind/pipelines/update/out.bind-fail.terraform.txt @@ -42,6 +42,5 @@ Plan: 0 to add, 1 to change, 0 to destroy. Error: this bind operation requires user confirmation, but the current console does not support prompting. -Using --auto-approve will bind resources without reviewing the changes. -Only use --auto-approve if you have reviewed what will be bound. +To proceed without confirmation, use --auto-approve after reviewing the changes above. diff --git a/acceptance/bundle/resources/dashboards/detect-change/output.txt b/acceptance/bundle/resources/dashboards/detect-change/output.txt index cdc0ac4aee..db09d03400 100644 --- a/acceptance/bundle/resources/dashboards/detect-change/output.txt +++ b/acceptance/bundle/resources/dashboards/detect-change/output.txt @@ -60,8 +60,8 @@ These modifications are untracked and will be overwritten on deploy. Make sure that the local dashboard definition matches what you intend to deploy before proceeding with the deployment. -Use --force only if you want to discard the remote changes and overwrite -the dashboard with your local version. +To overwrite the remote changes with your local version, use --force. +The remote modifications will be permanently lost. update dashboards.file_reference @@ -78,8 +78,8 @@ These modifications are untracked and will be overwritten on deploy. Make sure that the local dashboard definition matches what you intend to deploy before proceeding with the deployment. -Use --force only if you want to discard the remote changes and overwrite -the dashboard with your local version. +To overwrite the remote changes with your local version, use --force. +The remote modifications will be permanently lost. >>> errcode [CLI] bundle deploy @@ -93,8 +93,8 @@ These modifications are untracked and will be overwritten on deploy. Make sure that the local dashboard definition matches what you intend to deploy before proceeding with the deployment. -Use --force only if you want to discard the remote changes and overwrite -the dashboard with your local version. +To overwrite the remote changes with your local version, use --force. +The remote modifications will be permanently lost. Exit code: 1 diff --git a/acceptance/bundle/resources/pipelines/auto-approve/output.txt b/acceptance/bundle/resources/pipelines/auto-approve/output.txt index ceb4b965b4..d39bf9e11e 100644 --- a/acceptance/bundle/resources/pipelines/auto-approve/output.txt +++ b/acceptance/bundle/resources/pipelines/auto-approve/output.txt @@ -51,9 +51,8 @@ restore the defined STs and MVs through full refresh. Note that recreation is ne properties such as the 'catalog' or 'storage' are changed: delete resources.pipelines.bar Error: the deployment requires destructive actions, but the current console does not support prompting. -Schemas, pipelines, and volumes may be permanently deleted, resulting in data loss. -Using --auto-approve will skip all confirmation prompts and proceed with these destructive changes. -Only use --auto-approve if you have reviewed the plan above and accept the consequences. +To proceed, use --auto-approve after reviewing the plan above. Deleted schemas, pipelines, +and volumes may result in permanent data loss. Exit code: 1 diff --git a/acceptance/bundle/resources/pipelines/recreate/output.txt b/acceptance/bundle/resources/pipelines/recreate/output.txt index 7dbe7c371f..2421ece179 100644 --- a/acceptance/bundle/resources/pipelines/recreate/output.txt +++ b/acceptance/bundle/resources/pipelines/recreate/output.txt @@ -50,9 +50,8 @@ restore the defined STs and MVs through full refresh. Note that recreation is ne properties such as the 'catalog' or 'storage' are changed: recreate resources.pipelines.foo Error: the deployment requires destructive actions, but the current console does not support prompting. -Schemas, pipelines, and volumes may be permanently deleted, resulting in data loss. -Using --auto-approve will skip all confirmation prompts and proceed with these destructive changes. -Only use --auto-approve if you have reviewed the plan above and accept the consequences. +To proceed, use --auto-approve after reviewing the plan above. Deleted schemas, pipelines, +and volumes may result in permanent data loss. Exit code: 1 diff --git a/acceptance/bundle/resources/schemas/auto-approve/output.txt b/acceptance/bundle/resources/schemas/auto-approve/output.txt index fae3cc8dfd..7cdef806ea 100644 --- a/acceptance/bundle/resources/schemas/auto-approve/output.txt +++ b/acceptance/bundle/resources/schemas/auto-approve/output.txt @@ -58,9 +58,8 @@ Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/[UNIQUE_NAME]/file This action will result in the deletion or recreation of the following UC schemas. Any underlying data may be lost: delete resources.schemas.bar Error: the deployment requires destructive actions, but the current console does not support prompting. -Schemas, pipelines, and volumes may be permanently deleted, resulting in data loss. -Using --auto-approve will skip all confirmation prompts and proceed with these destructive changes. -Only use --auto-approve if you have reviewed the plan above and accept the consequences. +To proceed, use --auto-approve after reviewing the plan above. Deleted schemas, pipelines, +and volumes may result in permanent data loss. === Test cleanup diff --git a/acceptance/pipelines/deploy/auto-approve/output.txt b/acceptance/pipelines/deploy/auto-approve/output.txt index b96a88321a..ae7dacb642 100644 --- a/acceptance/pipelines/deploy/auto-approve/output.txt +++ b/acceptance/pipelines/deploy/auto-approve/output.txt @@ -19,9 +19,8 @@ restore the defined STs and MVs through full refresh. Note that recreation is ne properties such as the 'catalog' or 'storage' are changed: delete resources.pipelines.foo Error: the deployment requires destructive actions, but the current console does not support prompting. -Schemas, pipelines, and volumes may be permanently deleted, resulting in data loss. -Using --auto-approve will skip all confirmation prompts and proceed with these destructive changes. -Only use --auto-approve if you have reviewed the plan above and accept the consequences. +To proceed, use --auto-approve after reviewing the plan above. Deleted schemas, pipelines, +and volumes may result in permanent data loss. Exit code: 1 diff --git a/acceptance/pipelines/deploy/force-lock/output.txt b/acceptance/pipelines/deploy/force-lock/output.txt index a85ef8da5f..21de74a35e 100644 --- a/acceptance/pipelines/deploy/force-lock/output.txt +++ b/acceptance/pipelines/deploy/force-lock/output.txt @@ -5,11 +5,11 @@ === test deployment without force-lock (should fail) >>> errcode [CLI] pipelines deploy Error: Failed to acquire deployment lock: deploy lock acquired by user-with-lock@databricks.com at [TIMESTAMP] +0000 UTC. -Another deployment is likely in progress. Overriding with --force-lock risks corrupting that deployment. -Only use --force-lock if you are certain the other deployment is no longer active. +Another deployment is likely in progress. If you are certain that deployment is no longer active, +use --force-lock to override the lock. Overriding while a deployment is in progress may corrupt it. Error: deploy lock acquired by user-with-lock@databricks.com at [TIMESTAMP] +0000 UTC. -Another deployment is likely in progress. Overriding with --force-lock risks corrupting that deployment. -Only use --force-lock if you are certain the other deployment is no longer active. +Another deployment is likely in progress. If you are certain that deployment is no longer active, +use --force-lock to override the lock. Overriding while a deployment is in progress may corrupt it. Exit code: 1 diff --git a/acceptance/pipelines/destroy/auto-approve/output.txt b/acceptance/pipelines/destroy/auto-approve/output.txt index 16250f7fe1..bd86e68111 100644 --- a/acceptance/pipelines/destroy/auto-approve/output.txt +++ b/acceptance/pipelines/destroy/auto-approve/output.txt @@ -9,10 +9,10 @@ View your pipeline my_pipeline here: [DATABRICKS_URL]/pipelines/[UUID]?o=[NUMID] >>> errcode [CLI] pipelines destroy Error: this will permanently destroy all resources and data in the bundle target. -This includes deleting schemas and their underlying data, pipelines and their streaming -tables, managed volume files, and all workspace files in the deployment directory. -Using --auto-approve will skip all confirmation prompts and proceed with the destruction. -Only use --auto-approve if you are certain you want to permanently delete everything. +This includes schemas (with underlying data), pipelines (with streaming tables), +managed volume files, and all workspace files in the deployment directory. +To proceed without confirmation, use --auto-approve. This will permanently delete all +the above resources and cannot be undone. Exit code: 1 diff --git a/acceptance/pipelines/destroy/force-lock/output.txt b/acceptance/pipelines/destroy/force-lock/output.txt index 518491c099..786c82b348 100644 --- a/acceptance/pipelines/destroy/force-lock/output.txt +++ b/acceptance/pipelines/destroy/force-lock/output.txt @@ -12,11 +12,11 @@ View your pipeline foo here: [DATABRICKS_URL]/pipelines/[UUID]?o=[NUMID] === test deployment without force-lock (should fail) >>> errcode [CLI] pipelines destroy --auto-approve Error: Failed to acquire deployment lock: deploy lock acquired by user-with-lock@databricks.com at [TIMESTAMP] +0000 UTC. -Another deployment is likely in progress. Overriding with --force-lock risks corrupting that deployment. -Only use --force-lock if you are certain the other deployment is no longer active. +Another deployment is likely in progress. If you are certain that deployment is no longer active, +use --force-lock to override the lock. Overriding while a deployment is in progress may corrupt it. Error: deploy lock acquired by user-with-lock@databricks.com at [TIMESTAMP] +0000 UTC. -Another deployment is likely in progress. Overriding with --force-lock risks corrupting that deployment. -Only use --force-lock if you are certain the other deployment is no longer active. +Another deployment is likely in progress. If you are certain that deployment is no longer active, +use --force-lock to override the lock. Overriding while a deployment is in progress may corrupt it. Exit code: 1 diff --git a/bundle/config/mutator/validate_git_details.go b/bundle/config/mutator/validate_git_details.go index a22b1cb52c..bb25a19809 100644 --- a/bundle/config/mutator/validate_git_details.go +++ b/bundle/config/mutator/validate_git_details.go @@ -24,8 +24,8 @@ func (m *validateGitDetails) Apply(ctx context.Context, b *bundle.Bundle) diag.D } if b.Config.Bundle.Git.Branch != b.Config.Bundle.Git.ActualBranch && !b.Config.Bundle.Force { - return diag.Errorf("not on the right Git branch:\n expected according to configuration: %s\n actual: %s\nUsing --force will deploy from branch %s, which may push unexpected code to the target.\nOnly use --force if you intentionally want to deploy from this branch.%s", - b.Config.Bundle.Git.Branch, b.Config.Bundle.Git.ActualBranch, b.Config.Bundle.Git.ActualBranch, agent.AgentNotice(ctx)) + return diag.Errorf("not on the right Git branch:\n expected according to configuration: %s\n actual: %s\nTo deploy from this branch anyway, use --force. Note that this may push unexpected code to the target.%s", + b.Config.Bundle.Git.Branch, b.Config.Bundle.Git.ActualBranch, agent.AgentNotice()) } return nil } diff --git a/bundle/config/mutator/validate_git_details_test.go b/bundle/config/mutator/validate_git_details_test.go index 791c613c86..b7eaad9aeb 100644 --- a/bundle/config/mutator/validate_git_details_test.go +++ b/bundle/config/mutator/validate_git_details_test.go @@ -40,10 +40,9 @@ func TestValidateGitDetailsNonMatchingBranches(t *testing.T) { m := ValidateGitDetails() diags := bundle.Apply(t.Context(), b, m) - assert.ErrorContains(t, diags.Error(), "not on the right Git branch") - assert.ErrorContains(t, diags.Error(), "expected according to configuration: main") - assert.ErrorContains(t, diags.Error(), "actual: feature") - assert.ErrorContains(t, diags.Error(), "Only use --force if you intentionally want to deploy from this branch") + err := diags.Error() + assert.ErrorContains(t, err, "not on the right Git branch:\n expected according to configuration: main\n actual: feature") + assert.ErrorContains(t, err, "To deploy from this branch anyway, use --force. Note that this may push unexpected code to the target.") } func TestValidateGitDetailsNotUsingGit(t *testing.T) { diff --git a/bundle/deploy/terraform/check_dashboards_modified_remotely.go b/bundle/deploy/terraform/check_dashboards_modified_remotely.go index fb6687605d..d97d4eee61 100644 --- a/bundle/deploy/terraform/check_dashboards_modified_remotely.go +++ b/bundle/deploy/terraform/check_dashboards_modified_remotely.go @@ -122,8 +122,8 @@ func (l *checkDashboardsModifiedRemotely) Apply(ctx context.Context, b *bundle.B "Make sure that the local dashboard definition matches what you intend to deploy\n" + "before proceeding with the deployment.\n" + "\n" + - "Use --force only if you want to discard the remote changes and overwrite\n" + - "the dashboard with your local version." + agent.AgentNotice(ctx), + "To overwrite the remote changes with your local version, use --force.\n" + + "The remote modifications will be permanently lost." + agent.AgentNotice(), Paths: []dyn.Path{path}, Locations: []dyn.Location{loc}, }) diff --git a/bundle/deploy/terraform/import.go b/bundle/deploy/terraform/import.go index 330dbb1dbe..668a4b2344 100644 --- a/bundle/deploy/terraform/import.go +++ b/bundle/deploy/terraform/import.go @@ -73,7 +73,7 @@ func (m *importResource) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagn cmdio.LogString(ctx, output) if !cmdio.IsPromptSupported(ctx) { - return diag.Errorf("this bind operation requires user confirmation, but the current console does not support prompting.\nUsing --auto-approve will bind resources without reviewing the changes.\nOnly use --auto-approve if you have reviewed what will be bound.%s", agent.AgentNotice(ctx)) + return diag.Errorf("this bind operation requires user confirmation, but the current console does not support prompting.\nTo proceed without confirmation, use --auto-approve after reviewing the changes above.%s", agent.AgentNotice()) } ans, err := cmdio.AskYesOrNo(ctx, "Confirm import changes? Changes will be remotely applied only after running 'bundle deploy'.") diff --git a/bundle/phases/bind.go b/bundle/phases/bind.go index 59bf93cedf..8454f8d5d2 100644 --- a/bundle/phases/bind.go +++ b/bundle/phases/bind.go @@ -74,7 +74,7 @@ func Bind(ctx context.Context, b *bundle.Bundle, opts *terraform.BindOptions) { if !cmdio.IsPromptSupported(ctx) { result.Cancel() - logdiag.LogError(ctx, fmt.Errorf("this bind operation requires user confirmation, but the current console does not support prompting.\nUsing --auto-approve will bind resources without reviewing the changes.\nOnly use --auto-approve if you have reviewed what will be bound.%s", agent.AgentNotice(ctx))) //nolint + logdiag.LogError(ctx, fmt.Errorf("this bind operation requires user confirmation, but the current console does not support prompting.\nTo proceed without confirmation, use --auto-approve after reviewing the changes above.%s", agent.AgentNotice())) //nolint return } diff --git a/bundle/phases/deploy.go b/bundle/phases/deploy.go index bffc57d8fd..f87575e616 100644 --- a/bundle/phases/deploy.go +++ b/bundle/phases/deploy.go @@ -88,10 +88,9 @@ func approvalForDeploy(ctx context.Context, b *bundle.Bundle, plan *deployplan.P if !cmdio.IsPromptSupported(ctx) { return false, fmt.Errorf("the deployment requires destructive actions, but the current console does not support prompting.\n"+ - "Schemas, pipelines, and volumes may be permanently deleted, resulting in data loss.\n"+ - "Using --auto-approve will skip all confirmation prompts and proceed with these destructive changes.\n"+ - "Only use --auto-approve if you have reviewed the plan above and accept the consequences.%s", - agent.AgentNotice(ctx)) + "To proceed, use --auto-approve after reviewing the plan above. Deleted schemas, pipelines,\n"+ + "and volumes may result in permanent data loss.%s", + agent.AgentNotice()) } cmdio.LogString(ctx, "") diff --git a/cmd/bundle/destroy.go b/cmd/bundle/destroy.go index 180116b756..71bd0729bc 100644 --- a/cmd/bundle/destroy.go +++ b/cmd/bundle/destroy.go @@ -50,11 +50,11 @@ func CommandBundleDestroy(cmd *cobra.Command, args []string, autoApprove, forceD // We require auto-approve for non-interactive terminals since prompts are not possible. if !cmdio.IsPromptSupported(cmd.Context()) && !autoApprove { return fmt.Errorf("this will permanently destroy all resources and data in the bundle target.\n"+ - "This includes deleting schemas and their underlying data, pipelines and their streaming\n"+ - "tables, managed volume files, and all workspace files in the deployment directory.\n"+ - "Using --auto-approve will skip all confirmation prompts and proceed with the destruction.\n"+ - "Only use --auto-approve if you are certain you want to permanently delete everything.%s", - agent.AgentNotice(cmd.Context())) + "This includes schemas (with underlying data), pipelines (with streaming tables),\n"+ + "managed volume files, and all workspace files in the deployment directory.\n"+ + "To proceed without confirmation, use --auto-approve. This will permanently delete all\n"+ + "the above resources and cannot be undone.%s", + agent.AgentNotice()) } // Check if context is already initialized (e.g., when called from apps delete override) diff --git a/cmd/root/root.go b/cmd/root/root.go index 8b446eb279..e29d6df83c 100644 --- a/cmd/root/root.go +++ b/cmd/root/root.go @@ -12,7 +12,6 @@ import ( "time" "github.com/databricks/cli/internal/build" - "github.com/databricks/cli/libs/agent" "github.com/databricks/cli/libs/auth" "github.com/databricks/cli/libs/cmdctx" "github.com/databricks/cli/libs/cmdio" @@ -81,7 +80,6 @@ func New(ctx context.Context) *cobra.Command { ctx = withUpstreamInUserAgent(ctx) ctx = withInteractiveModeInUserAgent(ctx) ctx = InjectTestPidToUserAgent(ctx) - ctx = agent.Detect(ctx) cmd.SetContext(ctx) return nil } diff --git a/libs/agent/agent.go b/libs/agent/agent.go deleted file mode 100644 index 6c628e0f28..0000000000 --- a/libs/agent/agent.go +++ /dev/null @@ -1,98 +0,0 @@ -package agent - -import ( - "context" - - "github.com/databricks/cli/libs/env" -) - -// Product name constants. -const ( - Antigravity = "antigravity" - ClaudeCode = "claude-code" - Cline = "cline" - Codex = "codex" - Cursor = "cursor" - GeminiCLI = "gemini-cli" - OpenCode = "opencode" -) - -// knownAgents maps environment variables to product names. -// Adding a new agent only requires a new entry here and a new constant above. -// -// References for each environment variable: -// - ANTIGRAVITY_AGENT: Closed source. Verified locally that Google Antigravity sets this variable. -// - CLAUDECODE: https://github.com/anthropics/claude-code (open source npm package, sets CLAUDECODE=1) -// - CLINE_ACTIVE: https://github.com/cline/cline (shipped in v3.24.0, see also https://github.com/cline/cline/discussions/5366) -// - CODEX_CI: https://github.com/openai/codex/blob/main/codex-rs/core/src/unified_exec/process_manager.rs (part of UNIFIED_EXEC_ENV array) -// - CURSOR_AGENT: Closed source. Referenced in https://gist.github.com/johnlindquist/9a90c5f1aedef0477c60d0de4171da3f -// - GEMINI_CLI: https://google-gemini.github.io/gemini-cli/docs/tools/shell.html ("sets the GEMINI_CLI=1 environment variable") -// - OPENCODE: https://github.com/opencode-ai/opencode (open source, sets OPENCODE=1) -var knownAgents = []struct { - envVar string - product string -}{ - {"ANTIGRAVITY_AGENT", Antigravity}, - {"CLAUDECODE", ClaudeCode}, - {"CLINE_ACTIVE", Cline}, - {"CODEX_CI", Codex}, - {"CURSOR_AGENT", Cursor}, - {"GEMINI_CLI", GeminiCLI}, - {"OPENCODE", OpenCode}, -} - -// productKeyType is a package-local context key with zero size. -type productKeyType struct{} - -var productKey productKeyType - -// detect performs the actual detection logic. -// Returns product name string or empty string if detection is ambiguous. -// Only returns a product if exactly one agent is detected. -func detect(ctx context.Context) string { - var detected []string - for _, a := range knownAgents { - if env.Get(ctx, a.envVar) != "" { - detected = append(detected, a.product) - } - } - - // Only return a product if exactly one agent is detected. - if len(detected) == 1 { - return detected[0] - } - - return "" -} - -// Detect detects the agent and stores it in context. -// It returns a new context with the detection result set. -func Detect(ctx context.Context) context.Context { - return context.WithValue(ctx, productKey, detect(ctx)) -} - -// Mock is a helper for tests to mock the detection result. -func Mock(ctx context.Context, product string) context.Context { - return context.WithValue(ctx, productKey, product) -} - -// Product returns the detected agent product name from context. -// Returns empty string if no agent was detected. -// Panics if called before Detect() or Mock(). -func Product(ctx context.Context) string { - v := ctx.Value(productKey) - if v == nil { - panic("agent.Product called without calling agent.Detect first") - } - return v.(string) -} - -// isDetected returns true if an agent has been detected in the context. -// Returns false if Detect() hasn't been called yet. -func isDetected(ctx context.Context) bool { - v := ctx.Value(productKey) - if v == nil { - return false - } - return v.(string) != "" -} diff --git a/libs/agent/consent.go b/libs/agent/agent_notice.go similarity index 51% rename from libs/agent/consent.go rename to libs/agent/agent_notice.go index ed73cd5361..5553698f21 100644 --- a/libs/agent/consent.go +++ b/libs/agent/agent_notice.go @@ -1,18 +1,20 @@ package agent import ( - "context" "fmt" + + "github.com/databricks/databricks-sdk-go/useragent" ) -// AgentNotice returns a notice string for AI agents to append to error messages. -// Returns an empty string if no agent is detected. -func AgentNotice(ctx context.Context) string { - if !isDetected(ctx) { +// AgentNotice returns a notice string to append to error messages when an AI +// agent is detected. Returns an empty string for non-agent callers. +func AgentNotice() string { + agent := useragent.AgentProvider() + if agent == "" { return "" } return fmt.Sprintf("\n\nNote for AI agents (%s): do not retry this operation with --auto-approve,\n"+ "--force-lock, or --force unless the user has explicitly approved it.\n"+ "These flags skip safety prompts and may cause irreversible data loss.", - Product(ctx)) + agent) } diff --git a/libs/agent/agent_test.go b/libs/agent/agent_test.go deleted file mode 100644 index f25db02bad..0000000000 --- a/libs/agent/agent_test.go +++ /dev/null @@ -1,69 +0,0 @@ -package agent - -import ( - "context" - "testing" - - "github.com/databricks/cli/libs/env" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func clearAllAgentEnvVars(ctx context.Context) context.Context { - for _, a := range knownAgents { - ctx = env.Set(ctx, a.envVar, "") - } - return ctx -} - -func TestDetectEachAgent(t *testing.T) { - for _, a := range knownAgents { - t.Run(a.product, func(t *testing.T) { - ctx := clearAllAgentEnvVars(t.Context()) - ctx = env.Set(ctx, a.envVar, "1") - - assert.Equal(t, a.product, detect(ctx)) - }) - } -} - -func TestDetectViaContext(t *testing.T) { - ctx := clearAllAgentEnvVars(t.Context()) - ctx = env.Set(ctx, knownAgents[0].envVar, "1") - - ctx = Detect(ctx) - - assert.Equal(t, knownAgents[0].product, Product(ctx)) -} - -func TestDetectNoAgent(t *testing.T) { - ctx := clearAllAgentEnvVars(t.Context()) - - ctx = Detect(ctx) - - assert.Equal(t, "", Product(ctx)) -} - -func TestDetectMultipleAgents(t *testing.T) { - ctx := clearAllAgentEnvVars(t.Context()) - for _, a := range knownAgents { - ctx = env.Set(ctx, a.envVar, "1") - } - - assert.Equal(t, "", detect(ctx)) -} - -func TestProductCalledBeforeDetect(t *testing.T) { - ctx := t.Context() - - require.Panics(t, func() { - Product(ctx) - }) -} - -func TestMock(t *testing.T) { - ctx := t.Context() - ctx = Mock(ctx, "test-agent") - - assert.Equal(t, "test-agent", Product(ctx)) -} diff --git a/libs/agent/consent_test.go b/libs/agent/consent_test.go deleted file mode 100644 index 75b3cbaed0..0000000000 --- a/libs/agent/consent_test.go +++ /dev/null @@ -1,26 +0,0 @@ -package agent - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestAgentNoticeNoAgent(t *testing.T) { - ctx := Mock(t.Context(), "") - assert.Empty(t, AgentNotice(ctx)) -} - -func TestAgentNoticeWithAgent(t *testing.T) { - ctx := Mock(t.Context(), ClaudeCode) - notice := AgentNotice(ctx) - assert.Contains(t, notice, "claude-code") - assert.Contains(t, notice, "do not retry") - assert.Contains(t, notice, "irreversible data loss") -} - -func TestAgentNoticeBeforeDetect(t *testing.T) { - ctx := t.Context() - // Should not panic, just return empty. - assert.Empty(t, AgentNotice(ctx)) -} diff --git a/libs/locker/locker.go b/libs/locker/locker.go index b32556ae71..ba591a4c8a 100644 --- a/libs/locker/locker.go +++ b/libs/locker/locker.go @@ -107,15 +107,15 @@ func (locker *Locker) assertLockHeld(ctx context.Context) error { } if activeLockState.ID != locker.State.ID && !activeLockState.IsForced { return fmt.Errorf("deploy lock acquired by %s at %v.\n"+ - "Another deployment is likely in progress. Overriding with --force-lock risks corrupting that deployment.\n"+ - "Only use --force-lock if you are certain the other deployment is no longer active.%s", - activeLockState.User, activeLockState.AcquisitionTime, agent.AgentNotice(ctx)) + "Another deployment is likely in progress. If you are certain that deployment is no longer active,\n"+ + "use --force-lock to override the lock. Overriding while a deployment is in progress may corrupt it.%s", + activeLockState.User, activeLockState.AcquisitionTime, agent.AgentNotice()) } if activeLockState.ID != locker.State.ID && activeLockState.IsForced { return fmt.Errorf("deploy lock force-acquired by %s at %v.\n"+ - "Another deployment is likely in progress. Overriding with --force-lock risks corrupting that deployment.\n"+ - "Only use --force-lock if you are certain the other deployment is no longer active.%s", - activeLockState.User, activeLockState.AcquisitionTime, agent.AgentNotice(ctx)) + "Another deployment is likely in progress. If you are certain that deployment is no longer active,\n"+ + "use --force-lock to override the lock. Overriding while a deployment is in progress may corrupt it.%s", + activeLockState.User, activeLockState.AcquisitionTime, agent.AgentNotice()) } return nil } From 227b298f7da9cd682b312db69c5fdf4cd660186b Mon Sep 17 00:00:00 2001 From: Lennart Kats Date: Sun, 10 May 2026 13:29:27 +0200 Subject: [PATCH 06/13] Refine error message phrasing and AgentNotice text Tighten the wording of destructive-action error messages and rewrite AgentNotice to refer to "the flag suggested above" rather than listing all gated flags. The notice now says the operation may be irreversible rather than claiming data loss, which fits all sites (destroy, deploy, lock, git branch, dashboard, bind). Co-authored-by: Isaac --- .../deployment/bind/dashboard/recreation/output.txt | 4 ++-- .../deployment/bind/pipelines/recreate/output.txt | 4 ++-- .../resources/dashboards/detect-change/output.txt | 6 +++--- .../resources/pipelines/auto-approve/output.txt | 4 ++-- .../bundle/resources/pipelines/recreate/output.txt | 4 ++-- .../bundle/resources/schemas/auto-approve/output.txt | 4 ++-- acceptance/pipelines/deploy/auto-approve/output.txt | 4 ++-- acceptance/pipelines/deploy/force-lock/output.txt | 8 ++++---- acceptance/pipelines/destroy/auto-approve/output.txt | 7 +++---- acceptance/pipelines/destroy/force-lock/output.txt | 8 ++++---- .../terraform/check_dashboards_modified_remotely.go | 2 +- bundle/phases/deploy.go | 4 ++-- cmd/bundle/destroy.go | 7 +++---- libs/agent/agent_notice.go | 12 ++++++------ libs/locker/locker.go | 8 ++++---- 15 files changed, 42 insertions(+), 44 deletions(-) diff --git a/acceptance/bundle/deployment/bind/dashboard/recreation/output.txt b/acceptance/bundle/deployment/bind/dashboard/recreation/output.txt index b2b923b21e..d826d4ffd1 100644 --- a/acceptance/bundle/deployment/bind/dashboard/recreation/output.txt +++ b/acceptance/bundle/deployment/bind/dashboard/recreation/output.txt @@ -19,8 +19,8 @@ This action will result in the deletion or recreation of the following dashboard This will result in changed IDs and permanent URLs of the dashboards that will be recreated: recreate resources.dashboards.dashboard1 Error: the deployment requires destructive actions, but the current console does not support prompting. -To proceed, use --auto-approve after reviewing the plan above. Deleted schemas, pipelines, -and volumes may result in permanent data loss. +To proceed, use --auto-approve after reviewing the plan above. +Deleting schemas, pipelines, or volumes may cause permanent data loss. Exit code: 1 diff --git a/acceptance/bundle/deployment/bind/pipelines/recreate/output.txt b/acceptance/bundle/deployment/bind/pipelines/recreate/output.txt index d07a9f31b9..92bd9f0043 100644 --- a/acceptance/bundle/deployment/bind/pipelines/recreate/output.txt +++ b/acceptance/bundle/deployment/bind/pipelines/recreate/output.txt @@ -17,8 +17,8 @@ restore the defined STs and MVs through full refresh. Note that recreation is ne properties such as the 'catalog' or 'storage' are changed: recreate resources.pipelines.foo Error: the deployment requires destructive actions, but the current console does not support prompting. -To proceed, use --auto-approve after reviewing the plan above. Deleted schemas, pipelines, -and volumes may result in permanent data loss. +To proceed, use --auto-approve after reviewing the plan above. +Deleting schemas, pipelines, or volumes may cause permanent data loss. >>> [CLI] bundle deploy --auto-approve diff --git a/acceptance/bundle/resources/dashboards/detect-change/output.txt b/acceptance/bundle/resources/dashboards/detect-change/output.txt index db09d03400..8e06bc10e8 100644 --- a/acceptance/bundle/resources/dashboards/detect-change/output.txt +++ b/acceptance/bundle/resources/dashboards/detect-change/output.txt @@ -61,7 +61,7 @@ Make sure that the local dashboard definition matches what you intend to deploy before proceeding with the deployment. To overwrite the remote changes with your local version, use --force. -The remote modifications will be permanently lost. +The remote modifications will be lost. update dashboards.file_reference @@ -79,7 +79,7 @@ Make sure that the local dashboard definition matches what you intend to deploy before proceeding with the deployment. To overwrite the remote changes with your local version, use --force. -The remote modifications will be permanently lost. +The remote modifications will be lost. >>> errcode [CLI] bundle deploy @@ -94,7 +94,7 @@ Make sure that the local dashboard definition matches what you intend to deploy before proceeding with the deployment. To overwrite the remote changes with your local version, use --force. -The remote modifications will be permanently lost. +The remote modifications will be lost. Exit code: 1 diff --git a/acceptance/bundle/resources/pipelines/auto-approve/output.txt b/acceptance/bundle/resources/pipelines/auto-approve/output.txt index d39bf9e11e..dab6690153 100644 --- a/acceptance/bundle/resources/pipelines/auto-approve/output.txt +++ b/acceptance/bundle/resources/pipelines/auto-approve/output.txt @@ -51,8 +51,8 @@ restore the defined STs and MVs through full refresh. Note that recreation is ne properties such as the 'catalog' or 'storage' are changed: delete resources.pipelines.bar Error: the deployment requires destructive actions, but the current console does not support prompting. -To proceed, use --auto-approve after reviewing the plan above. Deleted schemas, pipelines, -and volumes may result in permanent data loss. +To proceed, use --auto-approve after reviewing the plan above. +Deleting schemas, pipelines, or volumes may cause permanent data loss. Exit code: 1 diff --git a/acceptance/bundle/resources/pipelines/recreate/output.txt b/acceptance/bundle/resources/pipelines/recreate/output.txt index 2421ece179..bad3a1db87 100644 --- a/acceptance/bundle/resources/pipelines/recreate/output.txt +++ b/acceptance/bundle/resources/pipelines/recreate/output.txt @@ -50,8 +50,8 @@ restore the defined STs and MVs through full refresh. Note that recreation is ne properties such as the 'catalog' or 'storage' are changed: recreate resources.pipelines.foo Error: the deployment requires destructive actions, but the current console does not support prompting. -To proceed, use --auto-approve after reviewing the plan above. Deleted schemas, pipelines, -and volumes may result in permanent data loss. +To proceed, use --auto-approve after reviewing the plan above. +Deleting schemas, pipelines, or volumes may cause permanent data loss. Exit code: 1 diff --git a/acceptance/bundle/resources/schemas/auto-approve/output.txt b/acceptance/bundle/resources/schemas/auto-approve/output.txt index 7cdef806ea..428f0c73c2 100644 --- a/acceptance/bundle/resources/schemas/auto-approve/output.txt +++ b/acceptance/bundle/resources/schemas/auto-approve/output.txt @@ -58,8 +58,8 @@ Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/[UNIQUE_NAME]/file This action will result in the deletion or recreation of the following UC schemas. Any underlying data may be lost: delete resources.schemas.bar Error: the deployment requires destructive actions, but the current console does not support prompting. -To proceed, use --auto-approve after reviewing the plan above. Deleted schemas, pipelines, -and volumes may result in permanent data loss. +To proceed, use --auto-approve after reviewing the plan above. +Deleting schemas, pipelines, or volumes may cause permanent data loss. === Test cleanup diff --git a/acceptance/pipelines/deploy/auto-approve/output.txt b/acceptance/pipelines/deploy/auto-approve/output.txt index ae7dacb642..b1c5716297 100644 --- a/acceptance/pipelines/deploy/auto-approve/output.txt +++ b/acceptance/pipelines/deploy/auto-approve/output.txt @@ -19,8 +19,8 @@ restore the defined STs and MVs through full refresh. Note that recreation is ne properties such as the 'catalog' or 'storage' are changed: delete resources.pipelines.foo Error: the deployment requires destructive actions, but the current console does not support prompting. -To proceed, use --auto-approve after reviewing the plan above. Deleted schemas, pipelines, -and volumes may result in permanent data loss. +To proceed, use --auto-approve after reviewing the plan above. +Deleting schemas, pipelines, or volumes may cause permanent data loss. Exit code: 1 diff --git a/acceptance/pipelines/deploy/force-lock/output.txt b/acceptance/pipelines/deploy/force-lock/output.txt index 21de74a35e..983633c262 100644 --- a/acceptance/pipelines/deploy/force-lock/output.txt +++ b/acceptance/pipelines/deploy/force-lock/output.txt @@ -5,11 +5,11 @@ === test deployment without force-lock (should fail) >>> errcode [CLI] pipelines deploy Error: Failed to acquire deployment lock: deploy lock acquired by user-with-lock@databricks.com at [TIMESTAMP] +0000 UTC. -Another deployment is likely in progress. If you are certain that deployment is no longer active, -use --force-lock to override the lock. Overriding while a deployment is in progress may corrupt it. +Another deployment may be in progress. Use --force-lock to override, but this may +corrupt the other deployment if it is still active. Error: deploy lock acquired by user-with-lock@databricks.com at [TIMESTAMP] +0000 UTC. -Another deployment is likely in progress. If you are certain that deployment is no longer active, -use --force-lock to override the lock. Overriding while a deployment is in progress may corrupt it. +Another deployment may be in progress. Use --force-lock to override, but this may +corrupt the other deployment if it is still active. Exit code: 1 diff --git a/acceptance/pipelines/destroy/auto-approve/output.txt b/acceptance/pipelines/destroy/auto-approve/output.txt index bd86e68111..af88af2ae0 100644 --- a/acceptance/pipelines/destroy/auto-approve/output.txt +++ b/acceptance/pipelines/destroy/auto-approve/output.txt @@ -8,11 +8,10 @@ Deployment complete! View your pipeline my_pipeline here: [DATABRICKS_URL]/pipelines/[UUID]?o=[NUMID] >>> errcode [CLI] pipelines destroy -Error: this will permanently destroy all resources and data in the bundle target. -This includes schemas (with underlying data), pipelines (with streaming tables), +Error: this command will permanently destroy all resources and data in the bundle target, +including schemas (with underlying data), pipelines (with streaming tables), managed volume files, and all workspace files in the deployment directory. -To proceed without confirmation, use --auto-approve. This will permanently delete all -the above resources and cannot be undone. +To proceed, use --auto-approve. Exit code: 1 diff --git a/acceptance/pipelines/destroy/force-lock/output.txt b/acceptance/pipelines/destroy/force-lock/output.txt index 786c82b348..359949ec66 100644 --- a/acceptance/pipelines/destroy/force-lock/output.txt +++ b/acceptance/pipelines/destroy/force-lock/output.txt @@ -12,11 +12,11 @@ View your pipeline foo here: [DATABRICKS_URL]/pipelines/[UUID]?o=[NUMID] === test deployment without force-lock (should fail) >>> errcode [CLI] pipelines destroy --auto-approve Error: Failed to acquire deployment lock: deploy lock acquired by user-with-lock@databricks.com at [TIMESTAMP] +0000 UTC. -Another deployment is likely in progress. If you are certain that deployment is no longer active, -use --force-lock to override the lock. Overriding while a deployment is in progress may corrupt it. +Another deployment may be in progress. Use --force-lock to override, but this may +corrupt the other deployment if it is still active. Error: deploy lock acquired by user-with-lock@databricks.com at [TIMESTAMP] +0000 UTC. -Another deployment is likely in progress. If you are certain that deployment is no longer active, -use --force-lock to override the lock. Overriding while a deployment is in progress may corrupt it. +Another deployment may be in progress. Use --force-lock to override, but this may +corrupt the other deployment if it is still active. Exit code: 1 diff --git a/bundle/deploy/terraform/check_dashboards_modified_remotely.go b/bundle/deploy/terraform/check_dashboards_modified_remotely.go index d97d4eee61..b6a2741b8a 100644 --- a/bundle/deploy/terraform/check_dashboards_modified_remotely.go +++ b/bundle/deploy/terraform/check_dashboards_modified_remotely.go @@ -123,7 +123,7 @@ func (l *checkDashboardsModifiedRemotely) Apply(ctx context.Context, b *bundle.B "before proceeding with the deployment.\n" + "\n" + "To overwrite the remote changes with your local version, use --force.\n" + - "The remote modifications will be permanently lost." + agent.AgentNotice(), + "The remote modifications will be lost." + agent.AgentNotice(), Paths: []dyn.Path{path}, Locations: []dyn.Location{loc}, }) diff --git a/bundle/phases/deploy.go b/bundle/phases/deploy.go index f87575e616..3bb9f25a75 100644 --- a/bundle/phases/deploy.go +++ b/bundle/phases/deploy.go @@ -88,8 +88,8 @@ func approvalForDeploy(ctx context.Context, b *bundle.Bundle, plan *deployplan.P if !cmdio.IsPromptSupported(ctx) { return false, fmt.Errorf("the deployment requires destructive actions, but the current console does not support prompting.\n"+ - "To proceed, use --auto-approve after reviewing the plan above. Deleted schemas, pipelines,\n"+ - "and volumes may result in permanent data loss.%s", + "To proceed, use --auto-approve after reviewing the plan above.\n"+ + "Deleting schemas, pipelines, or volumes may cause permanent data loss.%s", agent.AgentNotice()) } diff --git a/cmd/bundle/destroy.go b/cmd/bundle/destroy.go index 71bd0729bc..f880eeba9f 100644 --- a/cmd/bundle/destroy.go +++ b/cmd/bundle/destroy.go @@ -49,11 +49,10 @@ Typical use cases: func CommandBundleDestroy(cmd *cobra.Command, args []string, autoApprove, forceDestroy bool) error { // We require auto-approve for non-interactive terminals since prompts are not possible. if !cmdio.IsPromptSupported(cmd.Context()) && !autoApprove { - return fmt.Errorf("this will permanently destroy all resources and data in the bundle target.\n"+ - "This includes schemas (with underlying data), pipelines (with streaming tables),\n"+ + return fmt.Errorf("this command will permanently destroy all resources and data in the bundle target,\n"+ + "including schemas (with underlying data), pipelines (with streaming tables),\n"+ "managed volume files, and all workspace files in the deployment directory.\n"+ - "To proceed without confirmation, use --auto-approve. This will permanently delete all\n"+ - "the above resources and cannot be undone.%s", + "To proceed, use --auto-approve.%s", agent.AgentNotice()) } diff --git a/libs/agent/agent_notice.go b/libs/agent/agent_notice.go index 5553698f21..22f63b9d5d 100644 --- a/libs/agent/agent_notice.go +++ b/libs/agent/agent_notice.go @@ -9,12 +9,12 @@ import ( // AgentNotice returns a notice string to append to error messages when an AI // agent is detected. Returns an empty string for non-agent callers. func AgentNotice() string { - agent := useragent.AgentProvider() - if agent == "" { + name := useragent.AgentProvider() + if name == "" { return "" } - return fmt.Sprintf("\n\nNote for AI agents (%s): do not retry this operation with --auto-approve,\n"+ - "--force-lock, or --force unless the user has explicitly approved it.\n"+ - "These flags skip safety prompts and may cause irreversible data loss.", - agent) + return fmt.Sprintf("\n\nNote for AI agents (%s): do not retry with the flag suggested above\n"+ + "unless the user has explicitly approved it. The flag bypasses a safety check\n"+ + "and the operation may be irreversible.", + name) } diff --git a/libs/locker/locker.go b/libs/locker/locker.go index ba591a4c8a..d4a23b1930 100644 --- a/libs/locker/locker.go +++ b/libs/locker/locker.go @@ -107,14 +107,14 @@ func (locker *Locker) assertLockHeld(ctx context.Context) error { } if activeLockState.ID != locker.State.ID && !activeLockState.IsForced { return fmt.Errorf("deploy lock acquired by %s at %v.\n"+ - "Another deployment is likely in progress. If you are certain that deployment is no longer active,\n"+ - "use --force-lock to override the lock. Overriding while a deployment is in progress may corrupt it.%s", + "Another deployment may be in progress. Use --force-lock to override, but this may\n"+ + "corrupt the other deployment if it is still active.%s", activeLockState.User, activeLockState.AcquisitionTime, agent.AgentNotice()) } if activeLockState.ID != locker.State.ID && activeLockState.IsForced { return fmt.Errorf("deploy lock force-acquired by %s at %v.\n"+ - "Another deployment is likely in progress. If you are certain that deployment is no longer active,\n"+ - "use --force-lock to override the lock. Overriding while a deployment is in progress may corrupt it.%s", + "Another deployment may be in progress. Use --force-lock to override, but this may\n"+ + "corrupt the other deployment if it is still active.%s", activeLockState.User, activeLockState.AcquisitionTime, agent.AgentNotice()) } return nil From d290268c7d0a1b8f0548e2d72a5005dc7088e3ca Mon Sep 17 00:00:00 2001 From: Lennart Kats Date: Tue, 12 May 2026 10:13:29 +0200 Subject: [PATCH 07/13] Reorder destructive-action errors: risk before action, share DataLossWarning Move the "use --auto-approve" line to the end of the destroy/deploy error messages so the call to action follows the consequences. Extract the data-loss line into a shared DataLossWarning constant in bundle/phases, reused by both cmd/bundle/destroy and bundle/phases/deploy. Drop AgentNotice from the locker error: a lock conflict is an ops concern, not a destructive one. Reword "corrupt the other deployment" to "conflict with the other deployment". Reword bind no-tty error from "without confirmation, ... changes above" to "use --auto-approve after reviewing the plan above". Co-authored-by: Isaac --- .../deployment/bind/dashboard/recreation/output.txt | 2 +- .../bind/pipelines/recreate/out.bind-fail.direct.txt | 2 +- .../pipelines/recreate/out.bind-fail.terraform.txt | 2 +- .../deployment/bind/pipelines/recreate/output.txt | 2 +- .../bind/pipelines/update/out.bind-fail.direct.txt | 2 +- .../bind/pipelines/update/out.bind-fail.terraform.txt | 2 +- .../resources/pipelines/auto-approve/output.txt | 2 +- .../bundle/resources/pipelines/recreate/output.txt | 2 +- .../bundle/resources/schemas/auto-approve/output.txt | 2 +- acceptance/pipelines/deploy/auto-approve/output.txt | 2 +- acceptance/pipelines/deploy/force-lock/output.txt | 4 ++-- acceptance/pipelines/destroy/auto-approve/output.txt | 5 ++--- acceptance/pipelines/destroy/force-lock/output.txt | 4 ++-- bundle/deploy/terraform/import.go | 2 +- bundle/phases/bind.go | 2 +- bundle/phases/deploy.go | 8 +++----- bundle/phases/messages.go | 4 ++++ cmd/bundle/destroy.go | 11 +++++------ integration/libs/locker/locker_test.go | 2 +- libs/locker/locker.go | 9 ++++----- 20 files changed, 35 insertions(+), 36 deletions(-) diff --git a/acceptance/bundle/deployment/bind/dashboard/recreation/output.txt b/acceptance/bundle/deployment/bind/dashboard/recreation/output.txt index d826d4ffd1..26de10b19c 100644 --- a/acceptance/bundle/deployment/bind/dashboard/recreation/output.txt +++ b/acceptance/bundle/deployment/bind/dashboard/recreation/output.txt @@ -19,8 +19,8 @@ This action will result in the deletion or recreation of the following dashboard This will result in changed IDs and permanent URLs of the dashboards that will be recreated: recreate resources.dashboards.dashboard1 Error: the deployment requires destructive actions, but the current console does not support prompting. +Deleting data assets such as schemas, pipelines, or volumes may cause permanent data loss and should be carefully reviewed. To proceed, use --auto-approve after reviewing the plan above. -Deleting schemas, pipelines, or volumes may cause permanent data loss. Exit code: 1 diff --git a/acceptance/bundle/deployment/bind/pipelines/recreate/out.bind-fail.direct.txt b/acceptance/bundle/deployment/bind/pipelines/recreate/out.bind-fail.direct.txt index 882947b561..b081c18391 100644 --- a/acceptance/bundle/deployment/bind/pipelines/recreate/out.bind-fail.direct.txt +++ b/acceptance/bundle/deployment/bind/pipelines/recreate/out.bind-fail.direct.txt @@ -12,5 +12,5 @@ Changes detected: ~ storage: "/Shared/old_storage" -> "/Shared/new_storage" Error: this bind operation requires user confirmation, but the current console does not support prompting. -To proceed without confirmation, use --auto-approve after reviewing the changes above. +To proceed, use --auto-approve after reviewing the plan above. diff --git a/acceptance/bundle/deployment/bind/pipelines/recreate/out.bind-fail.terraform.txt b/acceptance/bundle/deployment/bind/pipelines/recreate/out.bind-fail.terraform.txt index eec5c440c0..0893bf6fd2 100644 --- a/acceptance/bundle/deployment/bind/pipelines/recreate/out.bind-fail.terraform.txt +++ b/acceptance/bundle/deployment/bind/pipelines/recreate/out.bind-fail.terraform.txt @@ -55,5 +55,5 @@ Plan: 1 to add, 0 to change, 1 to destroy. Error: this bind operation requires user confirmation, but the current console does not support prompting. -To proceed without confirmation, use --auto-approve after reviewing the changes above. +To proceed, use --auto-approve after reviewing the plan above. diff --git a/acceptance/bundle/deployment/bind/pipelines/recreate/output.txt b/acceptance/bundle/deployment/bind/pipelines/recreate/output.txt index 92bd9f0043..4da2a167b7 100644 --- a/acceptance/bundle/deployment/bind/pipelines/recreate/output.txt +++ b/acceptance/bundle/deployment/bind/pipelines/recreate/output.txt @@ -17,8 +17,8 @@ restore the defined STs and MVs through full refresh. Note that recreation is ne properties such as the 'catalog' or 'storage' are changed: recreate resources.pipelines.foo Error: the deployment requires destructive actions, but the current console does not support prompting. +Deleting data assets such as schemas, pipelines, or volumes may cause permanent data loss and should be carefully reviewed. To proceed, use --auto-approve after reviewing the plan above. -Deleting schemas, pipelines, or volumes may cause permanent data loss. >>> [CLI] bundle deploy --auto-approve diff --git a/acceptance/bundle/deployment/bind/pipelines/update/out.bind-fail.direct.txt b/acceptance/bundle/deployment/bind/pipelines/update/out.bind-fail.direct.txt index 75aca47b26..a5e4ca8007 100644 --- a/acceptance/bundle/deployment/bind/pipelines/update/out.bind-fail.direct.txt +++ b/acceptance/bundle/deployment/bind/pipelines/update/out.bind-fail.direct.txt @@ -12,5 +12,5 @@ Changes detected: ~ root_path: "/Workspace/Users/[USERNAME]/lakeflow_pipeline" -> null Error: this bind operation requires user confirmation, but the current console does not support prompting. -To proceed without confirmation, use --auto-approve after reviewing the changes above. +To proceed, use --auto-approve after reviewing the plan above. diff --git a/acceptance/bundle/deployment/bind/pipelines/update/out.bind-fail.terraform.txt b/acceptance/bundle/deployment/bind/pipelines/update/out.bind-fail.terraform.txt index 723c5583f5..0cc4511dcc 100644 --- a/acceptance/bundle/deployment/bind/pipelines/update/out.bind-fail.terraform.txt +++ b/acceptance/bundle/deployment/bind/pipelines/update/out.bind-fail.terraform.txt @@ -42,5 +42,5 @@ Plan: 0 to add, 1 to change, 0 to destroy. Error: this bind operation requires user confirmation, but the current console does not support prompting. -To proceed without confirmation, use --auto-approve after reviewing the changes above. +To proceed, use --auto-approve after reviewing the plan above. diff --git a/acceptance/bundle/resources/pipelines/auto-approve/output.txt b/acceptance/bundle/resources/pipelines/auto-approve/output.txt index dab6690153..3016a4c248 100644 --- a/acceptance/bundle/resources/pipelines/auto-approve/output.txt +++ b/acceptance/bundle/resources/pipelines/auto-approve/output.txt @@ -51,8 +51,8 @@ restore the defined STs and MVs through full refresh. Note that recreation is ne properties such as the 'catalog' or 'storage' are changed: delete resources.pipelines.bar Error: the deployment requires destructive actions, but the current console does not support prompting. +Deleting data assets such as schemas, pipelines, or volumes may cause permanent data loss and should be carefully reviewed. To proceed, use --auto-approve after reviewing the plan above. -Deleting schemas, pipelines, or volumes may cause permanent data loss. Exit code: 1 diff --git a/acceptance/bundle/resources/pipelines/recreate/output.txt b/acceptance/bundle/resources/pipelines/recreate/output.txt index bad3a1db87..bc68df40d2 100644 --- a/acceptance/bundle/resources/pipelines/recreate/output.txt +++ b/acceptance/bundle/resources/pipelines/recreate/output.txt @@ -50,8 +50,8 @@ restore the defined STs and MVs through full refresh. Note that recreation is ne properties such as the 'catalog' or 'storage' are changed: recreate resources.pipelines.foo Error: the deployment requires destructive actions, but the current console does not support prompting. +Deleting data assets such as schemas, pipelines, or volumes may cause permanent data loss and should be carefully reviewed. To proceed, use --auto-approve after reviewing the plan above. -Deleting schemas, pipelines, or volumes may cause permanent data loss. Exit code: 1 diff --git a/acceptance/bundle/resources/schemas/auto-approve/output.txt b/acceptance/bundle/resources/schemas/auto-approve/output.txt index 428f0c73c2..bf8e7771ee 100644 --- a/acceptance/bundle/resources/schemas/auto-approve/output.txt +++ b/acceptance/bundle/resources/schemas/auto-approve/output.txt @@ -58,8 +58,8 @@ Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/[UNIQUE_NAME]/file This action will result in the deletion or recreation of the following UC schemas. Any underlying data may be lost: delete resources.schemas.bar Error: the deployment requires destructive actions, but the current console does not support prompting. +Deleting data assets such as schemas, pipelines, or volumes may cause permanent data loss and should be carefully reviewed. To proceed, use --auto-approve after reviewing the plan above. -Deleting schemas, pipelines, or volumes may cause permanent data loss. === Test cleanup diff --git a/acceptance/pipelines/deploy/auto-approve/output.txt b/acceptance/pipelines/deploy/auto-approve/output.txt index b1c5716297..971748bbaf 100644 --- a/acceptance/pipelines/deploy/auto-approve/output.txt +++ b/acceptance/pipelines/deploy/auto-approve/output.txt @@ -19,8 +19,8 @@ restore the defined STs and MVs through full refresh. Note that recreation is ne properties such as the 'catalog' or 'storage' are changed: delete resources.pipelines.foo Error: the deployment requires destructive actions, but the current console does not support prompting. +Deleting data assets such as schemas, pipelines, or volumes may cause permanent data loss and should be carefully reviewed. To proceed, use --auto-approve after reviewing the plan above. -Deleting schemas, pipelines, or volumes may cause permanent data loss. Exit code: 1 diff --git a/acceptance/pipelines/deploy/force-lock/output.txt b/acceptance/pipelines/deploy/force-lock/output.txt index 983633c262..f51c2e92e3 100644 --- a/acceptance/pipelines/deploy/force-lock/output.txt +++ b/acceptance/pipelines/deploy/force-lock/output.txt @@ -6,10 +6,10 @@ >>> errcode [CLI] pipelines deploy Error: Failed to acquire deployment lock: deploy lock acquired by user-with-lock@databricks.com at [TIMESTAMP] +0000 UTC. Another deployment may be in progress. Use --force-lock to override, but this may -corrupt the other deployment if it is still active. +conflict with the other deployment if it is still active. Error: deploy lock acquired by user-with-lock@databricks.com at [TIMESTAMP] +0000 UTC. Another deployment may be in progress. Use --force-lock to override, but this may -corrupt the other deployment if it is still active. +conflict with the other deployment if it is still active. Exit code: 1 diff --git a/acceptance/pipelines/destroy/auto-approve/output.txt b/acceptance/pipelines/destroy/auto-approve/output.txt index af88af2ae0..3a59e2ffc2 100644 --- a/acceptance/pipelines/destroy/auto-approve/output.txt +++ b/acceptance/pipelines/destroy/auto-approve/output.txt @@ -8,9 +8,8 @@ Deployment complete! View your pipeline my_pipeline here: [DATABRICKS_URL]/pipelines/[UUID]?o=[NUMID] >>> errcode [CLI] pipelines destroy -Error: this command will permanently destroy all resources and data in the bundle target, -including schemas (with underlying data), pipelines (with streaming tables), -managed volume files, and all workspace files in the deployment directory. +Error: this command will destroy all resources deployed by this bundle, including workspace files in the deployment directory. +Deleting data assets such as schemas, pipelines, or volumes may cause permanent data loss and should be carefully reviewed. To proceed, use --auto-approve. Exit code: 1 diff --git a/acceptance/pipelines/destroy/force-lock/output.txt b/acceptance/pipelines/destroy/force-lock/output.txt index 359949ec66..34387b0271 100644 --- a/acceptance/pipelines/destroy/force-lock/output.txt +++ b/acceptance/pipelines/destroy/force-lock/output.txt @@ -13,10 +13,10 @@ View your pipeline foo here: [DATABRICKS_URL]/pipelines/[UUID]?o=[NUMID] >>> errcode [CLI] pipelines destroy --auto-approve Error: Failed to acquire deployment lock: deploy lock acquired by user-with-lock@databricks.com at [TIMESTAMP] +0000 UTC. Another deployment may be in progress. Use --force-lock to override, but this may -corrupt the other deployment if it is still active. +conflict with the other deployment if it is still active. Error: deploy lock acquired by user-with-lock@databricks.com at [TIMESTAMP] +0000 UTC. Another deployment may be in progress. Use --force-lock to override, but this may -corrupt the other deployment if it is still active. +conflict with the other deployment if it is still active. Exit code: 1 diff --git a/bundle/deploy/terraform/import.go b/bundle/deploy/terraform/import.go index 668a4b2344..48ad622c6c 100644 --- a/bundle/deploy/terraform/import.go +++ b/bundle/deploy/terraform/import.go @@ -73,7 +73,7 @@ func (m *importResource) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagn cmdio.LogString(ctx, output) if !cmdio.IsPromptSupported(ctx) { - return diag.Errorf("this bind operation requires user confirmation, but the current console does not support prompting.\nTo proceed without confirmation, use --auto-approve after reviewing the changes above.%s", agent.AgentNotice()) + return diag.Errorf("this bind operation requires user confirmation, but the current console does not support prompting.\nTo proceed, use --auto-approve after reviewing the plan above.%s", agent.AgentNotice()) } ans, err := cmdio.AskYesOrNo(ctx, "Confirm import changes? Changes will be remotely applied only after running 'bundle deploy'.") diff --git a/bundle/phases/bind.go b/bundle/phases/bind.go index 8454f8d5d2..81bc2f9578 100644 --- a/bundle/phases/bind.go +++ b/bundle/phases/bind.go @@ -74,7 +74,7 @@ func Bind(ctx context.Context, b *bundle.Bundle, opts *terraform.BindOptions) { if !cmdio.IsPromptSupported(ctx) { result.Cancel() - logdiag.LogError(ctx, fmt.Errorf("this bind operation requires user confirmation, but the current console does not support prompting.\nTo proceed without confirmation, use --auto-approve after reviewing the changes above.%s", agent.AgentNotice())) //nolint + logdiag.LogError(ctx, fmt.Errorf("this bind operation requires user confirmation, but the current console does not support prompting.\nTo proceed, use --auto-approve after reviewing the plan above.%s", agent.AgentNotice())) //nolint return } diff --git a/bundle/phases/deploy.go b/bundle/phases/deploy.go index 3bb9f25a75..b8e11e1098 100644 --- a/bundle/phases/deploy.go +++ b/bundle/phases/deploy.go @@ -3,7 +3,6 @@ package phases import ( "context" "errors" - "fmt" "github.com/databricks/cli/bundle" "github.com/databricks/cli/bundle/artifacts" @@ -87,10 +86,9 @@ func approvalForDeploy(ctx context.Context, b *bundle.Bundle, plan *deployplan.P } if !cmdio.IsPromptSupported(ctx) { - return false, fmt.Errorf("the deployment requires destructive actions, but the current console does not support prompting.\n"+ - "To proceed, use --auto-approve after reviewing the plan above.\n"+ - "Deleting schemas, pipelines, or volumes may cause permanent data loss.%s", - agent.AgentNotice()) + return false, errors.New("the deployment requires destructive actions, but the current console does not support prompting.\n" + + DataLossWarning + "\n" + + "To proceed, use --auto-approve after reviewing the plan above." + agent.AgentNotice()) } cmdio.LogString(ctx, "") diff --git a/bundle/phases/messages.go b/bundle/phases/messages.go index 625373dd8b..33949f9cb4 100644 --- a/bundle/phases/messages.go +++ b/bundle/phases/messages.go @@ -22,6 +22,10 @@ This action will result in the deletion or recreation of the following dashboard This will result in changed IDs and permanent URLs of the dashboards that will be recreated:` ) +// DataLossWarning is the warning shown when a non-interactive command is about +// to delete data-bearing resources. +const DataLossWarning = "Deleting data assets such as schemas, pipelines, or volumes may cause permanent data loss and should be carefully reviewed." + // Messages for bundle destroy. const ( deleteSchemaMessage = `This action will result in the deletion of the following UC schemas. Any underlying data may be lost:` diff --git a/cmd/bundle/destroy.go b/cmd/bundle/destroy.go index f880eeba9f..3637c85f06 100644 --- a/cmd/bundle/destroy.go +++ b/cmd/bundle/destroy.go @@ -3,7 +3,7 @@ package bundle import ( - "fmt" + "errors" "github.com/databricks/cli/bundle" "github.com/databricks/cli/bundle/phases" @@ -49,11 +49,10 @@ Typical use cases: func CommandBundleDestroy(cmd *cobra.Command, args []string, autoApprove, forceDestroy bool) error { // We require auto-approve for non-interactive terminals since prompts are not possible. if !cmdio.IsPromptSupported(cmd.Context()) && !autoApprove { - return fmt.Errorf("this command will permanently destroy all resources and data in the bundle target,\n"+ - "including schemas (with underlying data), pipelines (with streaming tables),\n"+ - "managed volume files, and all workspace files in the deployment directory.\n"+ - "To proceed, use --auto-approve.%s", - agent.AgentNotice()) + return errors.New("this command will destroy all resources deployed by this bundle, " + + "including workspace files in the deployment directory.\n" + + phases.DataLossWarning + "\n" + + "To proceed, use --auto-approve." + agent.AgentNotice()) } // Check if context is already initialized (e.g., when called from apps delete override) diff --git a/integration/libs/locker/locker_test.go b/integration/libs/locker/locker_test.go index a480216354..3ae80f8e71 100644 --- a/integration/libs/locker/locker_test.go +++ b/integration/libs/locker/locker_test.go @@ -59,7 +59,7 @@ func TestLock(t *testing.T) { indexOfAnInactiveLocker = i } assert.ErrorContains(t, lockerErrs[i], "lock acquired by") - assert.ErrorContains(t, lockerErrs[i], "Only use --force-lock if you are certain") + assert.ErrorContains(t, lockerErrs[i], "Use --force-lock to override") } } assert.Equal(t, 1, countActive, "Exactly one locker should successfull acquire the lock") diff --git a/libs/locker/locker.go b/libs/locker/locker.go index d4a23b1930..bb2c5d6bca 100644 --- a/libs/locker/locker.go +++ b/libs/locker/locker.go @@ -11,7 +11,6 @@ import ( "slices" "time" - "github.com/databricks/cli/libs/agent" "github.com/databricks/cli/libs/filer" "github.com/databricks/databricks-sdk-go" "github.com/google/uuid" @@ -108,14 +107,14 @@ func (locker *Locker) assertLockHeld(ctx context.Context) error { if activeLockState.ID != locker.State.ID && !activeLockState.IsForced { return fmt.Errorf("deploy lock acquired by %s at %v.\n"+ "Another deployment may be in progress. Use --force-lock to override, but this may\n"+ - "corrupt the other deployment if it is still active.%s", - activeLockState.User, activeLockState.AcquisitionTime, agent.AgentNotice()) + "conflict with the other deployment if it is still active.", + activeLockState.User, activeLockState.AcquisitionTime) } if activeLockState.ID != locker.State.ID && activeLockState.IsForced { return fmt.Errorf("deploy lock force-acquired by %s at %v.\n"+ "Another deployment may be in progress. Use --force-lock to override, but this may\n"+ - "corrupt the other deployment if it is still active.%s", - activeLockState.User, activeLockState.AcquisitionTime, agent.AgentNotice()) + "conflict with the other deployment if it is still active.", + activeLockState.User, activeLockState.AcquisitionTime) } return nil } From d0eb3594e97f65089a19a6b57ba67663ebe87aef Mon Sep 17 00:00:00 2001 From: Lennart Kats Date: Tue, 12 May 2026 10:23:33 +0200 Subject: [PATCH 08/13] Capture full bind error in job-abort-bind acceptance test The "^Error:" grep was filtering out the actionable second line ("To proceed, use --auto-approve after reviewing the plan above."). Use sed to print from "Error:" to end of file. Co-authored-by: Isaac --- .../bundle/deployment/bind/job/job-abort-bind/output.txt | 4 ++++ acceptance/bundle/deployment/bind/job/job-abort-bind/script | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/acceptance/bundle/deployment/bind/job/job-abort-bind/output.txt b/acceptance/bundle/deployment/bind/job/job-abort-bind/output.txt index 46b870a276..b4da76ff60 100644 --- a/acceptance/bundle/deployment/bind/job/job-abort-bind/output.txt +++ b/acceptance/bundle/deployment/bind/job/job-abort-bind/output.txt @@ -4,6 +4,10 @@ Created job with ID: [JOB_ID] === Expect binding to fail without an auto-approve flag: Error: this bind operation requires user confirmation, but the current console does not support prompting. +To proceed, use --auto-approve after reviewing the plan above. + + +Exit code: 1 === Deploy bundle: >>> [CLI] bundle deploy --force-lock diff --git a/acceptance/bundle/deployment/bind/job/job-abort-bind/script b/acceptance/bundle/deployment/bind/job/job-abort-bind/script index bbe22b68fc..b0aed59093 100644 --- a/acceptance/bundle/deployment/bind/job/job-abort-bind/script +++ b/acceptance/bundle/deployment/bind/job/job-abort-bind/script @@ -36,7 +36,7 @@ trap cleanup EXIT title "Expect binding to fail without an auto-approve flag:\n" trace errcode $CLI bundle deployment bind foo $JOB_ID &> out.bind-result.txt -grep "^Error:" out.bind-result.txt +sed -n '/^Error:/,$p' out.bind-result.txt rm out.bind-result.txt title "Deploy bundle:" From 7be47330ffb66226e5a2eaee4f51eb37698adc9e Mon Sep 17 00:00:00 2001 From: Lennart Kats Date: Tue, 12 May 2026 10:24:23 +0200 Subject: [PATCH 09/13] Add changelog entry for agent-consent error message changes Co-authored-by: Isaac --- NEXT_CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/NEXT_CHANGELOG.md b/NEXT_CHANGELOG.md index 2c13750d4b..f97c7cbb36 100644 --- a/NEXT_CHANGELOG.md +++ b/NEXT_CHANGELOG.md @@ -8,6 +8,7 @@ * Add `--force-refresh` flag to `databricks auth token` to force a token refresh even when the cached token is still valid ([#4767](https://github.com/databricks/cli/pull/4767)). ### Bundles +* Make sure consent errors are understood by agents ([#5239](https://github.com/databricks/cli/pull/5239)) * engine/direct: Fix drift in grants resource due to privilege reordering ([#4794](https://github.com/databricks/cli/pull/4794)) * engine/direct: Fix 400 error when deploying grants with ALL_PRIVILEGES ([#4801](https://github.com/databricks/cli/pull/4801)) * Deduplicate grant entries with duplicate principals or privileges during initialization ([#4801](https://github.com/databricks/cli/pull/4801)) From fe121052c6dbe25441228405e72ee1aafc5f4c03 Mon Sep 17 00:00:00 2001 From: Lennart Kats Date: Tue, 12 May 2026 10:26:15 +0200 Subject: [PATCH 10/13] Update changelog wording to match PR title Co-authored-by: Isaac --- NEXT_CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NEXT_CHANGELOG.md b/NEXT_CHANGELOG.md index f97c7cbb36..cd0657a507 100644 --- a/NEXT_CHANGELOG.md +++ b/NEXT_CHANGELOG.md @@ -8,7 +8,7 @@ * Add `--force-refresh` flag to `databricks auth token` to force a token refresh even when the cached token is still valid ([#4767](https://github.com/databricks/cli/pull/4767)). ### Bundles -* Make sure consent errors are understood by agents ([#5239](https://github.com/databricks/cli/pull/5239)) +* Make sure warnings asking for approval are understood by agents ([#5239](https://github.com/databricks/cli/pull/5239)) * engine/direct: Fix drift in grants resource due to privilege reordering ([#4794](https://github.com/databricks/cli/pull/4794)) * engine/direct: Fix 400 error when deploying grants with ALL_PRIVILEGES ([#4801](https://github.com/databricks/cli/pull/4801)) * Deduplicate grant entries with duplicate principals or privileges during initialization ([#4801](https://github.com/databricks/cli/pull/4801)) From 767162e26a1860fa83955bd8756a8cb00351d2c3 Mon Sep 17 00:00:00 2001 From: Lennart Kats Date: Tue, 12 May 2026 10:35:39 +0200 Subject: [PATCH 11/13] Lint corrections: remove //nolint, fix ST1005 trailing period in locker - Drop the //nolint comment that was carried over from the original bind error (no longer needed with the rewritten message). - Remove the trailing period from the lock conflict error to satisfy ST1005 (error strings should not end with punctuation). Co-authored-by: Isaac --- acceptance/pipelines/deploy/force-lock/output.txt | 4 ++-- acceptance/pipelines/destroy/force-lock/output.txt | 4 ++-- bundle/phases/bind.go | 2 +- libs/locker/locker.go | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/acceptance/pipelines/deploy/force-lock/output.txt b/acceptance/pipelines/deploy/force-lock/output.txt index f51c2e92e3..b1092d3f52 100644 --- a/acceptance/pipelines/deploy/force-lock/output.txt +++ b/acceptance/pipelines/deploy/force-lock/output.txt @@ -6,10 +6,10 @@ >>> errcode [CLI] pipelines deploy Error: Failed to acquire deployment lock: deploy lock acquired by user-with-lock@databricks.com at [TIMESTAMP] +0000 UTC. Another deployment may be in progress. Use --force-lock to override, but this may -conflict with the other deployment if it is still active. +conflict with the other deployment if it is still active Error: deploy lock acquired by user-with-lock@databricks.com at [TIMESTAMP] +0000 UTC. Another deployment may be in progress. Use --force-lock to override, but this may -conflict with the other deployment if it is still active. +conflict with the other deployment if it is still active Exit code: 1 diff --git a/acceptance/pipelines/destroy/force-lock/output.txt b/acceptance/pipelines/destroy/force-lock/output.txt index 34387b0271..5579565b06 100644 --- a/acceptance/pipelines/destroy/force-lock/output.txt +++ b/acceptance/pipelines/destroy/force-lock/output.txt @@ -13,10 +13,10 @@ View your pipeline foo here: [DATABRICKS_URL]/pipelines/[UUID]?o=[NUMID] >>> errcode [CLI] pipelines destroy --auto-approve Error: Failed to acquire deployment lock: deploy lock acquired by user-with-lock@databricks.com at [TIMESTAMP] +0000 UTC. Another deployment may be in progress. Use --force-lock to override, but this may -conflict with the other deployment if it is still active. +conflict with the other deployment if it is still active Error: deploy lock acquired by user-with-lock@databricks.com at [TIMESTAMP] +0000 UTC. Another deployment may be in progress. Use --force-lock to override, but this may -conflict with the other deployment if it is still active. +conflict with the other deployment if it is still active Exit code: 1 diff --git a/bundle/phases/bind.go b/bundle/phases/bind.go index 81bc2f9578..42a48da9c0 100644 --- a/bundle/phases/bind.go +++ b/bundle/phases/bind.go @@ -74,7 +74,7 @@ func Bind(ctx context.Context, b *bundle.Bundle, opts *terraform.BindOptions) { if !cmdio.IsPromptSupported(ctx) { result.Cancel() - logdiag.LogError(ctx, fmt.Errorf("this bind operation requires user confirmation, but the current console does not support prompting.\nTo proceed, use --auto-approve after reviewing the plan above.%s", agent.AgentNotice())) //nolint + logdiag.LogError(ctx, fmt.Errorf("this bind operation requires user confirmation, but the current console does not support prompting.\nTo proceed, use --auto-approve after reviewing the plan above.%s", agent.AgentNotice())) return } diff --git a/libs/locker/locker.go b/libs/locker/locker.go index bb2c5d6bca..cf65f8eb2a 100644 --- a/libs/locker/locker.go +++ b/libs/locker/locker.go @@ -107,13 +107,13 @@ func (locker *Locker) assertLockHeld(ctx context.Context) error { if activeLockState.ID != locker.State.ID && !activeLockState.IsForced { return fmt.Errorf("deploy lock acquired by %s at %v.\n"+ "Another deployment may be in progress. Use --force-lock to override, but this may\n"+ - "conflict with the other deployment if it is still active.", + "conflict with the other deployment if it is still active", activeLockState.User, activeLockState.AcquisitionTime) } if activeLockState.ID != locker.State.ID && activeLockState.IsForced { return fmt.Errorf("deploy lock force-acquired by %s at %v.\n"+ "Another deployment may be in progress. Use --force-lock to override, but this may\n"+ - "conflict with the other deployment if it is still active.", + "conflict with the other deployment if it is still active", activeLockState.User, activeLockState.AcquisitionTime) } return nil From fcfa9fd4e490d64b3ed95cc475fa7a2cc80bed98 Mon Sep 17 00:00:00 2001 From: Lennart Kats Date: Tue, 12 May 2026 13:09:56 +0200 Subject: [PATCH 12/13] Drop already-released changelog entries pulled in by merge The earlier merge with main brought back engine/direct changelog entries that have since been released. Keep only the new entry for this PR. Co-authored-by: Isaac --- NEXT_CHANGELOG.md | 7 ------- 1 file changed, 7 deletions(-) diff --git a/NEXT_CHANGELOG.md b/NEXT_CHANGELOG.md index cf72fcaa08..f6625c972e 100644 --- a/NEXT_CHANGELOG.md +++ b/NEXT_CHANGELOG.md @@ -9,13 +9,6 @@ ### Bundles * Make sure warnings asking for approval are understood by agents ([#5239](https://github.com/databricks/cli/pull/5239)) -* engine/direct: Fix drift in grants resource due to privilege reordering ([#4794](https://github.com/databricks/cli/pull/4794)) -* engine/direct: Fix 400 error when deploying grants with ALL_PRIVILEGES ([#4801](https://github.com/databricks/cli/pull/4801)) -* Deduplicate grant entries with duplicate principals or privileges during initialization ([#4801](https://github.com/databricks/cli/pull/4801)) -* engine/direct: Fix unwanted recreation of secret scopes when scope_backend_type is not set ([#4834](https://github.com/databricks/cli/pull/4834)) -* engine/direct: Fix bind and unbind for non-Terraform resources ([#4850](https://github.com/databricks/cli/pull/4850)) -* engine/direct: Fix deploying removed principals ([#4824](https://github.com/databricks/cli/pull/4824)) -* engine/direct: Fix secret scope permissions migration from Terraform to Direct engine ([#4866](https://github.com/databricks/cli/pull/4866)) * Stop applying `presets.name_prefix` (and the dev-mode `[dev ]` rename) to `vector_search_endpoints` ([#5209](https://github.com/databricks/cli/pull/5209)). * Fix `bundle generate` job to preserve nested notebook directory structure ([#4596](https://github.com/databricks/cli/pull/4596)) From a734e55df6bd49d746f4a8dd9c8634b3c76513ec Mon Sep 17 00:00:00 2001 From: Lennart Kats Date: Wed, 13 May 2026 09:18:40 +0200 Subject: [PATCH 13/13] Update volumes/recreate UC integration test fixture for new error wording This UC-only test runs on aws-ucws/azure-ucws environments (not in make test), so the fixture wasn't regenerated when the destroy/deploy error wording changed. The `"principal":"..."` spacing change is a separate cosmetic update. Co-authored-by: Isaac --- acceptance/bundle/resources/volumes/recreate/output.txt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/acceptance/bundle/resources/volumes/recreate/output.txt b/acceptance/bundle/resources/volumes/recreate/output.txt index 494e9ff538..5714559a60 100644 --- a/acceptance/bundle/resources/volumes/recreate/output.txt +++ b/acceptance/bundle/resources/volumes/recreate/output.txt @@ -28,7 +28,7 @@ Deployment complete! { "privilege_assignments": [ { - "principal":"account users", + "principal": "account users", "privileges": [ "WRITE_VOLUME" ] @@ -45,7 +45,9 @@ For managed volumes, the files stored in the volume are also deleted from your cloud tenant within 30 days. For external volumes, the metadata about the volume is removed from the catalog, but the underlying files are not deleted: recreate resources.volumes.foo -Error: the deployment requires destructive actions, but current console does not support prompting. Please specify --auto-approve if you would like to skip prompts and proceed +Error: the deployment requires destructive actions, but the current console does not support prompting. +Deleting data assets such as schemas, pipelines, or volumes may cause permanent data loss and should be carefully reviewed. +To proceed, use --auto-approve after reviewing the plan above. Exit code: 1