From ded0b346bb7b4367c377f8813ee16c8d150133b9 Mon Sep 17 00:00:00 2001 From: Maksim Zuev Date: Thu, 29 Oct 2026 12:02:40 +0000 Subject: [PATCH 01/11] Tell AI agents to pass the device code login on to the user When an AI agent is detected, the first step of the non-interactive login also prints a note that tells it to give the URL and the user code to the user, not to approve the login itself, and to ask the user for the confirmation code. Co-authored-by: Isaac --- .../non-interactive/nominal/out.databrickscfg | 5 +++++ .../non-interactive/nominal/output.txt | 20 +++++++++++++++++++ .../non-interactive/nominal/script | 4 ++++ cmd/auth/README.md | 4 ++++ cmd/auth/login_device_code.go | 17 ++++++++++++++-- docs/device-code-login.md | 10 ++++++++++ 6 files changed, 58 insertions(+), 2 deletions(-) diff --git a/acceptance/cmd/auth/login/device-code/non-interactive/nominal/out.databrickscfg b/acceptance/cmd/auth/login/device-code/non-interactive/nominal/out.databrickscfg index cf14685849c..063dca2ee54 100644 --- a/acceptance/cmd/auth/login/device-code/non-interactive/nominal/out.databrickscfg +++ b/acceptance/cmd/auth/login/device-code/non-interactive/nominal/out.databrickscfg @@ -8,3 +8,8 @@ auth_type = databricks-cli [__settings__] default_profile = test + +[agent] +host = [DATABRICKS_URL] +workspace_id = [WORKSPACE_ID] +auth_type = databricks-cli diff --git a/acceptance/cmd/auth/login/device-code/non-interactive/nominal/output.txt b/acceptance/cmd/auth/login/device-code/non-interactive/nominal/output.txt index 93d79138488..8d1c9ff5f50 100644 --- a/acceptance/cmd/auth/login/device-code/non-interactive/nominal/output.txt +++ b/acceptance/cmd/auth/login/device-code/non-interactive/nominal/output.txt @@ -91,3 +91,23 @@ Profile test was successfully saved {"method": "POST", "path": "/oidc/v1/token", "raw_body": "client_id=databricks-cli&confirmation_code=FTFAQQKY&device_code=device-code-2&grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Adevice_code"} {"method": "POST", "path": "/oidc/v1/device/auth", "raw_body": "client_id=databricks-cli&scope=offline_access+all-apis"} {"method": "POST", "path": "/oidc/v1/token", "raw_body": "client_id=databricks-cli&confirmation_code=FTFAQQKY&device_code=device-code-3&grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Adevice_code"} + +=== An AI agent gets notes on passing the login on to the user + +>>> AGENT=test-agent [CLI] auth login --host [DATABRICKS_URL] --profile agent --use-device-code +Using a browser on another device, visit: +[DATABRICKS_URL]/device/verification + +And enter the code: +WDJB-MJHT + +Note for AI agents (test-agent): give the URL and the code above to the user. +Do not open the URL or approve the login yourself: the user must approve it. +Ask the user for the confirmation code their browser shows. + +Once finished, complete the login by running the same command with +--user-code WDJB-MJHT and --confirmation-code set to the confirmation code +provided in your browser. + +>>> AGENT=test-agent [CLI] auth login --host [DATABRICKS_URL] --profile agent --use-device-code --user-code WDJB-MJHT --confirmation-code FTFA-QQKY +Profile agent was successfully saved diff --git a/acceptance/cmd/auth/login/device-code/non-interactive/nominal/script b/acceptance/cmd/auth/login/device-code/non-interactive/nominal/script index 69f9a349da6..9ef6e24a77c 100644 --- a/acceptance/cmd/auth/login/device-code/non-interactive/nominal/script +++ b/acceptance/cmd/auth/login/device-code/non-interactive/nominal/script @@ -31,5 +31,9 @@ trace $CLI auth login --profile test --use-device-code --user-code WDJB-0003 --c trace cache_keys trace print_requests.py --oneline //oidc/v1/device/auth //oidc/v1/token +title "An AI agent gets notes on passing the login on to the user\n" +trace AGENT=test-agent $CLI auth login --host $DATABRICKS_HOST --profile agent --use-device-code +trace AGENT=test-agent $CLI auth login --host $DATABRICKS_HOST --profile agent --use-device-code --user-code WDJB-MJHT --confirmation-code FTFA-QQKY + # Track the .databrickscfg file that was created to surface changes. mv "./home/.databrickscfg" "./out.databrickscfg" diff --git a/cmd/auth/README.md b/cmd/auth/README.md index acc3b50178a..d423eabe75f 100644 --- a/cmd/auth/README.md +++ b/cmd/auth/README.md @@ -120,6 +120,10 @@ in two steps instead: flags exchanges the pending device code instead of requesting a new one, and the rest of the login runs as usual. +Agent detection (`useragent.AgentProvider()`) does not pick the flow. It only +adds a note to the first step's output that tells the agent to pass the login +on to the user. + The pending login is a token store entry with `PendingLogin` set instead of a token. Its key is the login's cache key (the profile name, or the host without a profile) with a `#device-code` suffix, so the target's current token keeps diff --git a/cmd/auth/login_device_code.go b/cmd/auth/login_device_code.go index ddbc2709d94..d4f9c75913b 100644 --- a/cmd/auth/login_device_code.go +++ b/cmd/auth/login_device_code.go @@ -13,6 +13,7 @@ import ( "github.com/databricks/cli/libs/databrickscfg/profile" "github.com/databricks/cli/libs/env" "github.com/databricks/cli/libs/log" + "github.com/databricks/databricks-sdk-go/useragent" "github.com/spf13/cobra" "golang.org/x/oauth2" ) @@ -198,11 +199,23 @@ func (l nonInteractiveDeviceCodeLogin) start(ctx context.Context) error { if err != nil { return fmt.Errorf("store pending login: %w", err) } - cmdio.LogString(ctx, strings.Join(append(approvalInstructions(*da), + lines := approvalInstructions(*da) + // An agent passes the login on to the user, who approves it in the + // browser. + if agent := useragent.AgentProvider(); agent != "" { + lines = append(lines, + "Note for AI agents ("+agent+"): give the URL and the code above to the user.", + "Do not open the URL or approve the login yourself: the user must approve it.", + "Ask the user for the confirmation code their browser shows.", + "", + ) + } + lines = append(lines, "Once finished, complete the login by running the same command with", "--user-code "+formatCode(da.UserCode)+" and --confirmation-code set to the confirmation code", "provided in your browser.", - ), "\n")) + ) + cmdio.LogString(ctx, strings.Join(lines, "\n")) return nil } diff --git a/docs/device-code-login.md b/docs/device-code-login.md index 97338a8a8ea..e6bbedcf75f 100644 --- a/docs/device-code-login.md +++ b/docs/device-code-login.md @@ -85,6 +85,16 @@ Profile my-profile was successfully saved confirmation code. It saves the profile and the token. Without `--profile`, it only stores the token for the host. +When the CLI detects an AI agent, it also prints a note that tells the agent to +pass the URL and the code on to you, not to approve the login itself, and to +ask you for the confirmation code: + +```console +Note for AI agents (claude-code): give the URL and the code above to the user. +Do not open the URL or approve the login yourself: the user must approve it. +Ask the user for the confirmation code their browser shows. +``` + The pending login stays on your machine, so a confirmation code that ends up somewhere else, such as an agent's conversation, cannot complete the login on another machine. From 7ce3594079f984eafa596809cadb9f259778a26e Mon Sep 17 00:00:00 2001 From: Maksim Zuev Date: Thu, 29 Oct 2026 13:09:53 +0000 Subject: [PATCH 02/11] Add agent.Note for notes addressed to AI agents The device code login built the "Note for AI agents (): " prefix by hand, like AgentNotice does. agent.Note now adds it for both, and returns an empty string when no agent is detected. AgentNotice prints the same text as before. Co-authored-by: Isaac --- cmd/auth/login_device_code.go | 19 +++++++++---------- internal/testutil/agent.go | 16 ++++++++++++++++ libs/agent/agent_notice.go | 17 +++++++++++++---- libs/agent/agent_notice_test.go | 30 ++++++++++++++++++++++++++++++ 4 files changed, 68 insertions(+), 14 deletions(-) create mode 100644 internal/testutil/agent.go create mode 100644 libs/agent/agent_notice_test.go diff --git a/cmd/auth/login_device_code.go b/cmd/auth/login_device_code.go index d4f9c75913b..fe234355d4e 100644 --- a/cmd/auth/login_device_code.go +++ b/cmd/auth/login_device_code.go @@ -7,13 +7,13 @@ import ( "slices" "strings" + "github.com/databricks/cli/libs/agent" "github.com/databricks/cli/libs/auth/storage" "github.com/databricks/cli/libs/auth/u2m" "github.com/databricks/cli/libs/cmdio" "github.com/databricks/cli/libs/databrickscfg/profile" "github.com/databricks/cli/libs/env" "github.com/databricks/cli/libs/log" - "github.com/databricks/databricks-sdk-go/useragent" "github.com/spf13/cobra" "golang.org/x/oauth2" ) @@ -31,6 +31,12 @@ const pendingLoginKeySuffix = "#device-code" // that cannot be completed. const startNewLoginHint = "Run the same command without --user-code and --confirmation-code to start a new login" +// deviceCodeAgentNote tells an AI agent to pass the login on to the user, who +// approves it in the browser. +const deviceCodeAgentNote = "give the URL and the code above to the user.\n" + + "Do not open the URL or approve the login yourself: the user must approve it.\n" + + "Ask the user for the confirmation code their browser shows." + // deviceCodeIncompatibleFlags lists flags that device code login cannot honor. var deviceCodeIncompatibleFlags = []string{ // Resource indicators are sent on the browser authorization request, @@ -200,15 +206,8 @@ func (l nonInteractiveDeviceCodeLogin) start(ctx context.Context) error { return fmt.Errorf("store pending login: %w", err) } lines := approvalInstructions(*da) - // An agent passes the login on to the user, who approves it in the - // browser. - if agent := useragent.AgentProvider(); agent != "" { - lines = append(lines, - "Note for AI agents ("+agent+"): give the URL and the code above to the user.", - "Do not open the URL or approve the login yourself: the user must approve it.", - "Ask the user for the confirmation code their browser shows.", - "", - ) + if note := agent.Note(deviceCodeAgentNote); note != "" { + lines = append(lines, note, "") } lines = append(lines, "Once finished, complete the login by running the same command with", diff --git a/internal/testutil/agent.go b/internal/testutil/agent.go new file mode 100644 index 00000000000..5fe469202ae --- /dev/null +++ b/internal/testutil/agent.go @@ -0,0 +1,16 @@ +package testutil + +import "github.com/databricks/databricks-sdk-go/useragent" + +// SetAgent makes useragent.AgentProvider report agent, or no agent if agent +// is empty. It clears the environment and the SDK's cached agent, so the agent +// running the tests does not leak into them. +// Note: use of this function is incompatible with parallel execution. +func SetAgent(t TestingT, agent string) { + NullEnvironment(t) + if agent != "" { + t.Setenv("AGENT", agent) + } + useragent.ClearCache() + t.Cleanup(useragent.ClearCache) +} diff --git a/libs/agent/agent_notice.go b/libs/agent/agent_notice.go index 22f63b9d5de..a7842b72a90 100644 --- a/libs/agent/agent_notice.go +++ b/libs/agent/agent_notice.go @@ -9,12 +9,21 @@ 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 { + note := Note("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.") + if note == "" { + return "" + } + return "\n\n" + note +} + +// Note returns text as a note addressed to the detected AI agent. Returns an +// empty string for non-agent callers. +func Note(text string) string { name := useragent.AgentProvider() if name == "" { return "" } - 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) + return fmt.Sprintf("Note for AI agents (%s): %s", name, text) } diff --git a/libs/agent/agent_notice_test.go b/libs/agent/agent_notice_test.go new file mode 100644 index 00000000000..5cdf63e6f82 --- /dev/null +++ b/libs/agent/agent_notice_test.go @@ -0,0 +1,30 @@ +package agent + +import ( + "testing" + + "github.com/databricks/cli/internal/testutil" + "github.com/stretchr/testify/assert" +) + +func TestNote(t *testing.T) { + testutil.SetAgent(t, "test-agent") + assert.Equal(t, "Note for AI agents (test-agent): ask the user.", Note("ask the user.")) +} + +func TestNoteWithoutAgent(t *testing.T) { + testutil.SetAgent(t, "") + assert.Empty(t, Note("ask the user.")) +} + +func TestAgentNotice(t *testing.T) { + testutil.SetAgent(t, "test-agent") + assert.Equal(t, "\n\nNote for AI agents (test-agent): 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.", AgentNotice()) +} + +func TestAgentNoticeWithoutAgent(t *testing.T) { + testutil.SetAgent(t, "") + assert.Empty(t, AgentNotice()) +} From d39257d88e12e291908a0bf24e02ea58136f290e Mon Sep 17 00:00:00 2001 From: Maksim Zuev Date: Thu, 29 Oct 2026 13:12:02 +0000 Subject: [PATCH 03/11] Show the AI agent note in the interactive device code login too An agent that runs the login in a terminal, e.g. through a pseudo-terminal, got the interactive prompt without the note, so nothing told it to pass the login on to the user. The note is now part of the approval instructions that both logins print. The prompt tests clear the agent environment, so that running them from an agent does not change the output. Co-authored-by: Isaac --- cmd/auth/README.md | 4 ++-- cmd/auth/login_device_code.go | 17 +++++++++-------- cmd/auth/login_device_code_test.go | 30 ++++++++++++++++++++++++++++++ docs/device-code-login.md | 7 ++++--- 4 files changed, 45 insertions(+), 13 deletions(-) diff --git a/cmd/auth/README.md b/cmd/auth/README.md index d423eabe75f..ce9d392a6c0 100644 --- a/cmd/auth/README.md +++ b/cmd/auth/README.md @@ -121,8 +121,8 @@ in two steps instead: and the rest of the login runs as usual. Agent detection (`useragent.AgentProvider()`) does not pick the flow. It only -adds a note to the first step's output that tells the agent to pass the login -on to the user. +adds a note after the user code, in both flows, that tells the agent to pass +the login on to the user. The pending login is a token store entry with `PendingLogin` set instead of a token. Its key is the login's cache key (the profile name, or the host without diff --git a/cmd/auth/login_device_code.go b/cmd/auth/login_device_code.go index fe234355d4e..110b8fa5286 100644 --- a/cmd/auth/login_device_code.go +++ b/cmd/auth/login_device_code.go @@ -205,16 +205,11 @@ func (l nonInteractiveDeviceCodeLogin) start(ctx context.Context) error { if err != nil { return fmt.Errorf("store pending login: %w", err) } - lines := approvalInstructions(*da) - if note := agent.Note(deviceCodeAgentNote); note != "" { - lines = append(lines, note, "") - } - lines = append(lines, + cmdio.LogString(ctx, strings.Join(append(approvalInstructions(*da), "Once finished, complete the login by running the same command with", "--user-code "+formatCode(da.UserCode)+" and --confirmation-code set to the confirmation code", "provided in your browser.", - ) - cmdio.LogString(ctx, strings.Join(lines, "\n")) + ), "\n")) return nil } @@ -326,8 +321,10 @@ func promptForConfirmationCode(ctx context.Context, da u2m.DeviceAuthorization, // approvalInstructions returns the lines that show the user where to approve // the login. Both flows print them before saying how to complete the login. +// An AI agent also gets a note to pass the login on to the user, whether or +// not it runs the command in a terminal. func approvalInstructions(da u2m.DeviceAuthorization) []string { - return []string{ + lines := []string{ "Using a browser on another device, visit:", da.VerificationURI, "", @@ -335,6 +332,10 @@ func approvalInstructions(da u2m.DeviceAuthorization) []string { formatCode(da.UserCode), "", } + if note := agent.Note(deviceCodeAgentNote); note != "" { + lines = append(lines, note, "") + } + return lines } // formatCode displays an 8-character device code as two groups of four diff --git a/cmd/auth/login_device_code_test.go b/cmd/auth/login_device_code_test.go index a55292e5420..d30e1ea2b99 100644 --- a/cmd/auth/login_device_code_test.go +++ b/cmd/auth/login_device_code_test.go @@ -94,6 +94,7 @@ func TestValidateDeviceCodeLogin(t *testing.T) { } func TestPromptForConfirmationCode(t *testing.T) { + testutil.SetAgent(t, "") tests := []struct { name string input string @@ -134,6 +135,35 @@ func TestPromptForConfirmationCode(t *testing.T) { } } +// An AI agent that runs the login in a terminal gets the same note as in the +// non-interactive login. +func TestPromptForConfirmationCode_Agent(t *testing.T) { + testutil.SetAgent(t, "test-agent") + var stderr bytes.Buffer + ctx := cmdio.InContext(t.Context(), cmdio.NewIO(t.Context(), flags.OutputText, strings.NewReader("FTFA-QQKY\n"), &bytes.Buffer{}, &stderr, "", "")) + + code, err := promptForConfirmationCode(ctx, u2m.DeviceAuthorization{ + UserCode: "WDJBMJHT", + VerificationURI: "https://workspace.test/device/verification", + }, nil, 3) + require.NoError(t, err) + assert.Equal(t, "FTFA-QQKY", code) + assert.True(t, strings.HasPrefix(stderr.String(), strings.Join([]string{ + "Using a browser on another device, visit:", + "https://workspace.test/device/verification", + "", + "And enter the code:", + "WDJB-MJHT", + "", + "Note for AI agents (test-agent): give the URL and the code above to the user.", + "Do not open the URL or approve the login yourself: the user must approve it.", + "Ask the user for the confirmation code their browser shows.", + "", + "Once finished, enter the confirmation code provided in your browser.", + "Confirmation code: ", + }, "\n")), stderr.String()) +} + func TestPromptForConfirmationCode_Retry(t *testing.T) { tests := []struct { attemptsLeft int diff --git a/docs/device-code-login.md b/docs/device-code-login.md index e6bbedcf75f..177bee5fb50 100644 --- a/docs/device-code-login.md +++ b/docs/device-code-login.md @@ -85,9 +85,10 @@ Profile my-profile was successfully saved confirmation code. It saves the profile and the token. Without `--profile`, it only stores the token for the host. -When the CLI detects an AI agent, it also prints a note that tells the agent to -pass the URL and the code on to you, not to approve the login itself, and to -ask you for the confirmation code: +When the CLI detects an AI agent, it also prints a note after the code, with +or without a terminal. The note tells the agent to pass the URL and the code +on to you, not to approve the login itself, and to ask you for the +confirmation code: ```console Note for AI agents (claude-code): give the URL and the code above to the user. From f40c09b9b97bfd2fa032dd9565d0b636d97d759b Mon Sep 17 00:00:00 2001 From: Maksim Zuev Date: Thu, 29 Oct 2026 15:40:24 +0000 Subject: [PATCH 04/11] Tell an AI agent to enter the confirmation code at the login prompt The interactive device code login printed the note of the non-interactive one, which tells the agent to ask the user for the confirmation code but not what to do with it. That login waits for the code at its prompt, so its note now tells the agent to enter the code there. Co-authored-by: Isaac --- cmd/auth/README.md | 3 ++- cmd/auth/login_device_code.go | 23 +++++++++++++++++------ cmd/auth/login_device_code_test.go | 7 ++++--- docs/device-code-login.md | 3 +++ 4 files changed, 26 insertions(+), 10 deletions(-) diff --git a/cmd/auth/README.md b/cmd/auth/README.md index ce9d392a6c0..eb08834fd00 100644 --- a/cmd/auth/README.md +++ b/cmd/auth/README.md @@ -122,7 +122,8 @@ in two steps instead: Agent detection (`useragent.AgentProvider()`) does not pick the flow. It only adds a note after the user code, in both flows, that tells the agent to pass -the login on to the user. +the login on to the user. In the flow that reads the confirmation code from +stdin, the note also tells the agent to enter the code at the prompt. The pending login is a token store entry with `PendingLogin` set instead of a token. Its key is the login's cache key (the profile name, or the host without diff --git a/cmd/auth/login_device_code.go b/cmd/auth/login_device_code.go index 110b8fa5286..c58d9711fa2 100644 --- a/cmd/auth/login_device_code.go +++ b/cmd/auth/login_device_code.go @@ -32,11 +32,22 @@ const pendingLoginKeySuffix = "#device-code" const startNewLoginHint = "Run the same command without --user-code and --confirmation-code to start a new login" // deviceCodeAgentNote tells an AI agent to pass the login on to the user, who -// approves it in the browser. +// approves it in the browser. The notes of the two flows end with how the +// agent completes the login. const deviceCodeAgentNote = "give the URL and the code above to the user.\n" + - "Do not open the URL or approve the login yourself: the user must approve it.\n" + + "Do not open the URL or approve the login yourself: the user must approve it.\n" + +// nonInteractiveAgentNote is followed by how to complete the login with a +// rerun of the command. +const nonInteractiveAgentNote = deviceCodeAgentNote + "Ask the user for the confirmation code their browser shows." +// interactiveAgentNote tells an agent that runs the login in a terminal to +// enter the confirmation code into the running command. +const interactiveAgentNote = deviceCodeAgentNote + + "Ask the user for the confirmation code their browser shows, and enter it\n" + + `at the "Confirmation code" prompt of this command.` + // deviceCodeIncompatibleFlags lists flags that device code login cannot honor. var deviceCodeIncompatibleFlags = []string{ // Resource indicators are sent on the browser authorization request, @@ -205,7 +216,7 @@ func (l nonInteractiveDeviceCodeLogin) start(ctx context.Context) error { if err != nil { return fmt.Errorf("store pending login: %w", err) } - cmdio.LogString(ctx, strings.Join(append(approvalInstructions(*da), + cmdio.LogString(ctx, strings.Join(append(approvalInstructions(*da, nonInteractiveAgentNote), "Once finished, complete the login by running the same command with", "--user-code "+formatCode(da.UserCode)+" and --confirmation-code set to the confirmation code", "provided in your browser.", @@ -298,7 +309,7 @@ func sameCode(a, b string) bool { // and asks again. func promptForConfirmationCode(ctx context.Context, da u2m.DeviceAuthorization, lastErr error, attemptsLeft int) (string, error) { if lastErr == nil { - cmdio.LogString(ctx, strings.Join(append(approvalInstructions(da), + cmdio.LogString(ctx, strings.Join(append(approvalInstructions(da, interactiveAgentNote), "Once finished, enter the confirmation code provided in your browser.", ), "\n")) } else { @@ -323,7 +334,7 @@ func promptForConfirmationCode(ctx context.Context, da u2m.DeviceAuthorization, // the login. Both flows print them before saying how to complete the login. // An AI agent also gets a note to pass the login on to the user, whether or // not it runs the command in a terminal. -func approvalInstructions(da u2m.DeviceAuthorization) []string { +func approvalInstructions(da u2m.DeviceAuthorization, agentNote string) []string { lines := []string{ "Using a browser on another device, visit:", da.VerificationURI, @@ -332,7 +343,7 @@ func approvalInstructions(da u2m.DeviceAuthorization) []string { formatCode(da.UserCode), "", } - if note := agent.Note(deviceCodeAgentNote); note != "" { + if note := agent.Note(agentNote); note != "" { lines = append(lines, note, "") } return lines diff --git a/cmd/auth/login_device_code_test.go b/cmd/auth/login_device_code_test.go index d30e1ea2b99..1a3a7c2f713 100644 --- a/cmd/auth/login_device_code_test.go +++ b/cmd/auth/login_device_code_test.go @@ -135,8 +135,8 @@ func TestPromptForConfirmationCode(t *testing.T) { } } -// An AI agent that runs the login in a terminal gets the same note as in the -// non-interactive login. +// An AI agent that runs the login in a terminal is told to enter the +// confirmation code at the prompt. func TestPromptForConfirmationCode_Agent(t *testing.T) { testutil.SetAgent(t, "test-agent") var stderr bytes.Buffer @@ -157,7 +157,8 @@ func TestPromptForConfirmationCode_Agent(t *testing.T) { "", "Note for AI agents (test-agent): give the URL and the code above to the user.", "Do not open the URL or approve the login yourself: the user must approve it.", - "Ask the user for the confirmation code their browser shows.", + "Ask the user for the confirmation code their browser shows, and enter it", + `at the "Confirmation code" prompt of this command.`, "", "Once finished, enter the confirmation code provided in your browser.", "Confirmation code: ", diff --git a/docs/device-code-login.md b/docs/device-code-login.md index 177bee5fb50..fff366167f4 100644 --- a/docs/device-code-login.md +++ b/docs/device-code-login.md @@ -96,6 +96,9 @@ Do not open the URL or approve the login yourself: the user must approve it. Ask the user for the confirmation code their browser shows. ``` +In a terminal, the note also tells the agent to enter the confirmation code +at the `Confirmation code` prompt of the running command. + The pending login stays on your machine, so a confirmation code that ends up somewhere else, such as an agent's conversation, cannot complete the login on another machine. From 7fbb0c7454c95fcbea438185a20bcc58f33a4879 Mon Sep 17 00:00:00 2001 From: Maksim Zuev Date: Thu, 29 Oct 2026 16:28:30 +0000 Subject: [PATCH 05/11] Rename agent.Note to agent.AgentNote Co-authored-by: Isaac --- cmd/auth/login_device_code.go | 2 +- libs/agent/agent_notice.go | 8 ++++---- libs/agent/agent_notice_test.go | 8 ++++---- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/cmd/auth/login_device_code.go b/cmd/auth/login_device_code.go index c58d9711fa2..119bf120093 100644 --- a/cmd/auth/login_device_code.go +++ b/cmd/auth/login_device_code.go @@ -343,7 +343,7 @@ func approvalInstructions(da u2m.DeviceAuthorization, agentNote string) []string formatCode(da.UserCode), "", } - if note := agent.Note(agentNote); note != "" { + if note := agent.AgentNote(agentNote); note != "" { lines = append(lines, note, "") } return lines diff --git a/libs/agent/agent_notice.go b/libs/agent/agent_notice.go index a7842b72a90..251e7982535 100644 --- a/libs/agent/agent_notice.go +++ b/libs/agent/agent_notice.go @@ -9,7 +9,7 @@ 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 { - note := Note("do not retry with the flag suggested above\n" + + note := AgentNote("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.") if note == "" { @@ -18,9 +18,9 @@ func AgentNotice() string { return "\n\n" + note } -// Note returns text as a note addressed to the detected AI agent. Returns an -// empty string for non-agent callers. -func Note(text string) string { +// AgentNote returns text as a note addressed to the detected AI agent. Returns +// an empty string for non-agent callers. +func AgentNote(text string) string { name := useragent.AgentProvider() if name == "" { return "" diff --git a/libs/agent/agent_notice_test.go b/libs/agent/agent_notice_test.go index 5cdf63e6f82..62a3a70a8ec 100644 --- a/libs/agent/agent_notice_test.go +++ b/libs/agent/agent_notice_test.go @@ -7,14 +7,14 @@ import ( "github.com/stretchr/testify/assert" ) -func TestNote(t *testing.T) { +func TestAgentNote(t *testing.T) { testutil.SetAgent(t, "test-agent") - assert.Equal(t, "Note for AI agents (test-agent): ask the user.", Note("ask the user.")) + assert.Equal(t, "Note for AI agents (test-agent): ask the user.", AgentNote("ask the user.")) } -func TestNoteWithoutAgent(t *testing.T) { +func TestAgentNoteWithoutAgent(t *testing.T) { testutil.SetAgent(t, "") - assert.Empty(t, Note("ask the user.")) + assert.Empty(t, AgentNote("ask the user.")) } func TestAgentNotice(t *testing.T) { From a287e6dc8a4606b52f9848fefb3fd6442e1c3c61 Mon Sep 17 00:00:00 2001 From: Maksim Zuev Date: Thu, 29 Oct 2026 16:41:17 +0000 Subject: [PATCH 06/11] Inline the agent notes of the two device code logins Each note is used once, so it moves next to its use. Co-authored-by: Isaac --- cmd/auth/login_device_code.go | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/cmd/auth/login_device_code.go b/cmd/auth/login_device_code.go index 119bf120093..c9a2d681c07 100644 --- a/cmd/auth/login_device_code.go +++ b/cmd/auth/login_device_code.go @@ -31,23 +31,11 @@ const pendingLoginKeySuffix = "#device-code" // that cannot be completed. const startNewLoginHint = "Run the same command without --user-code and --confirmation-code to start a new login" -// deviceCodeAgentNote tells an AI agent to pass the login on to the user, who -// approves it in the browser. The notes of the two flows end with how the -// agent completes the login. +// deviceCodeAgentNote starts the note that tells an AI agent to pass the login +// on to the user. const deviceCodeAgentNote = "give the URL and the code above to the user.\n" + "Do not open the URL or approve the login yourself: the user must approve it.\n" -// nonInteractiveAgentNote is followed by how to complete the login with a -// rerun of the command. -const nonInteractiveAgentNote = deviceCodeAgentNote + - "Ask the user for the confirmation code their browser shows." - -// interactiveAgentNote tells an agent that runs the login in a terminal to -// enter the confirmation code into the running command. -const interactiveAgentNote = deviceCodeAgentNote + - "Ask the user for the confirmation code their browser shows, and enter it\n" + - `at the "Confirmation code" prompt of this command.` - // deviceCodeIncompatibleFlags lists flags that device code login cannot honor. var deviceCodeIncompatibleFlags = []string{ // Resource indicators are sent on the browser authorization request, @@ -216,7 +204,8 @@ func (l nonInteractiveDeviceCodeLogin) start(ctx context.Context) error { if err != nil { return fmt.Errorf("store pending login: %w", err) } - cmdio.LogString(ctx, strings.Join(append(approvalInstructions(*da, nonInteractiveAgentNote), + agentNote := deviceCodeAgentNote + "Ask the user for the confirmation code their browser shows." + cmdio.LogString(ctx, strings.Join(append(approvalInstructions(*da, agentNote), "Once finished, complete the login by running the same command with", "--user-code "+formatCode(da.UserCode)+" and --confirmation-code set to the confirmation code", "provided in your browser.", @@ -309,7 +298,9 @@ func sameCode(a, b string) bool { // and asks again. func promptForConfirmationCode(ctx context.Context, da u2m.DeviceAuthorization, lastErr error, attemptsLeft int) (string, error) { if lastErr == nil { - cmdio.LogString(ctx, strings.Join(append(approvalInstructions(da, interactiveAgentNote), + agentNote := deviceCodeAgentNote + "Ask the user for the confirmation code their browser shows, and enter it\n" + + `at the "Confirmation code" prompt of this command.` + cmdio.LogString(ctx, strings.Join(append(approvalInstructions(da, agentNote), "Once finished, enter the confirmation code provided in your browser.", ), "\n")) } else { From 2c8b948a0f568cba33bd699c2b4ecdd9e71ce369 Mon Sep 17 00:00:00 2001 From: Maksim Zuev Date: Mon, 2 Nov 2026 12:53:22 +0000 Subject: [PATCH 07/11] Use the user code the fake server issues in the agent login test The fake server now issues a new user code for every device code login, so the agent login in the nominal test gets WDJB-0004. Co-authored-by: Isaac --- .../login/device-code/non-interactive/nominal/output.txt | 6 +++--- .../auth/login/device-code/non-interactive/nominal/script | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/acceptance/cmd/auth/login/device-code/non-interactive/nominal/output.txt b/acceptance/cmd/auth/login/device-code/non-interactive/nominal/output.txt index 8d1c9ff5f50..234e8e2ab3a 100644 --- a/acceptance/cmd/auth/login/device-code/non-interactive/nominal/output.txt +++ b/acceptance/cmd/auth/login/device-code/non-interactive/nominal/output.txt @@ -99,15 +99,15 @@ Using a browser on another device, visit: [DATABRICKS_URL]/device/verification And enter the code: -WDJB-MJHT +WDJB-0004 Note for AI agents (test-agent): give the URL and the code above to the user. Do not open the URL or approve the login yourself: the user must approve it. Ask the user for the confirmation code their browser shows. Once finished, complete the login by running the same command with ---user-code WDJB-MJHT and --confirmation-code set to the confirmation code +--user-code WDJB-0004 and --confirmation-code set to the confirmation code provided in your browser. ->>> AGENT=test-agent [CLI] auth login --host [DATABRICKS_URL] --profile agent --use-device-code --user-code WDJB-MJHT --confirmation-code FTFA-QQKY +>>> AGENT=test-agent [CLI] auth login --host [DATABRICKS_URL] --profile agent --use-device-code --user-code WDJB-0004 --confirmation-code FTFA-QQKY Profile agent was successfully saved diff --git a/acceptance/cmd/auth/login/device-code/non-interactive/nominal/script b/acceptance/cmd/auth/login/device-code/non-interactive/nominal/script index 9ef6e24a77c..635fb3533f2 100644 --- a/acceptance/cmd/auth/login/device-code/non-interactive/nominal/script +++ b/acceptance/cmd/auth/login/device-code/non-interactive/nominal/script @@ -33,7 +33,7 @@ trace print_requests.py --oneline //oidc/v1/device/auth //oidc/v1/token title "An AI agent gets notes on passing the login on to the user\n" trace AGENT=test-agent $CLI auth login --host $DATABRICKS_HOST --profile agent --use-device-code -trace AGENT=test-agent $CLI auth login --host $DATABRICKS_HOST --profile agent --use-device-code --user-code WDJB-MJHT --confirmation-code FTFA-QQKY +trace AGENT=test-agent $CLI auth login --host $DATABRICKS_HOST --profile agent --use-device-code --user-code WDJB-0004 --confirmation-code FTFA-QQKY # Track the .databrickscfg file that was created to surface changes. mv "./home/.databrickscfg" "./out.databrickscfg" From 2b07a4ead6b5e356d999e8a2fe4065076aab3209 Mon Sep 17 00:00:00 2001 From: Maksim Zuev Date: Mon, 2 Nov 2026 13:00:23 +0000 Subject: [PATCH 08/11] Always set AGENT in testutil.SetAgent The SDK reports no agent for an empty AGENT, so setting it unconditionally keeps the behavior and makes t.Setenv reject parallel tests, as the comment says. Co-authored-by: Isaac --- internal/testutil/agent.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/internal/testutil/agent.go b/internal/testutil/agent.go index 5fe469202ae..249f575e3f3 100644 --- a/internal/testutil/agent.go +++ b/internal/testutil/agent.go @@ -8,9 +8,7 @@ import "github.com/databricks/databricks-sdk-go/useragent" // Note: use of this function is incompatible with parallel execution. func SetAgent(t TestingT, agent string) { NullEnvironment(t) - if agent != "" { - t.Setenv("AGENT", agent) - } + t.Setenv("AGENT", agent) useragent.ClearCache() t.Cleanup(useragent.ClearCache) } From 79f7b66ebec134007e7f06ee601ed5a4c9b94ccd Mon Sep 17 00:00:00 2001 From: Maksim Zuev Date: Mon, 2 Nov 2026 13:00:25 +0000 Subject: [PATCH 09/11] Unset PI_CODING_AGENT in acceptance tests SDK v0.191.0 detects Pi through PI_CODING_AGENT, so the nominal device code login test showed (pi) instead of (test-agent) when run inside Pi. Co-authored-by: Isaac --- acceptance/acceptance_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/acceptance/acceptance_test.go b/acceptance/acceptance_test.go index 29f1ccde3a3..34dd52f5cb9 100644 --- a/acceptance/acceptance_test.go +++ b/acceptance/acceptance_test.go @@ -275,6 +275,7 @@ func testAccept(t *testing.T, inprocessMode bool, selectedTests []string, skipTo "KIRO", "OPENCLAW_SHELL", "OPENCODE", + "PI_CODING_AGENT", "VSCODE_AGENT", "WINDSURF_AGENT", } { From 423935cef58bbb96269dd7c4086f43a2a81a7387 Mon Sep 17 00:00:00 2001 From: Maksim Zuev Date: Mon, 2 Nov 2026 13:02:07 +0000 Subject: [PATCH 10/11] Tell an AI agent not to approve the login after a wrong confirmation code The agent runs the second step of the non-interactive login, so it reads the error that says to approve the login. The note tells it to ask the user to approve it instead. Co-authored-by: Isaac --- .../login/device-code/non-interactive/errors/output.txt | 7 +++++++ .../auth/login/device-code/non-interactive/errors/script | 1 + cmd/auth/README.md | 5 ++++- cmd/auth/login_device_code.go | 9 ++++++++- 4 files changed, 20 insertions(+), 2 deletions(-) diff --git a/acceptance/cmd/auth/login/device-code/non-interactive/errors/output.txt b/acceptance/cmd/auth/login/device-code/non-interactive/errors/output.txt index d299e0be717..ed0f12b1b20 100644 --- a/acceptance/cmd/auth/login/device-code/non-interactive/errors/output.txt +++ b/acceptance/cmd/auth/login/device-code/non-interactive/errors/output.txt @@ -62,6 +62,12 @@ Error: no pending device code login for host [DATABRICKS_URL]. Run the same comm >>> musterr [CLI] auth login --host [DATABRICKS_URL] --profile test --use-device-code --user-code WDJB-0001 --confirmation-code AAAA-AAAA Error: token exchange: Invalid confirmation code (error code: invalid_request). Check the confirmation code and approve the login if you haven't, then rerun this command. Run the same command without --user-code and --confirmation-code to start a new login if it still fails +>>> AGENT=test-agent musterr [CLI] auth login --host [DATABRICKS_URL] --profile test --use-device-code --user-code WDJB-0001 --confirmation-code AAAA-AAAA +Error: token exchange: Invalid confirmation code (error code: invalid_request). Check the confirmation code and approve the login if you haven't, then rerun this command. Run the same command without --user-code and --confirmation-code to start a new login if it still fails + +Note for AI agents (test-agent): ask the user to check the confirmation code +and to approve the login if they haven't. Do not approve it yourself. + === Rerun with the right confirmation code >>> [CLI] auth login --host [DATABRICKS_URL] --profile test --use-device-code --user-code wdjb-0001 --confirmation-code ftfa-qqky @@ -102,6 +108,7 @@ Error: the login for profile test was started with different scopes. Complete it >>> print_requests.py --oneline //oidc/v1/device/auth //oidc/v1/token {"method": "POST", "path": "/oidc/v1/device/auth", "raw_body": "client_id=databricks-cli&scope=offline_access+all-apis"} {"method": "POST", "path": "/oidc/v1/token", "raw_body": "client_id=databricks-cli&confirmation_code=AAAAAAAA&device_code=device-code-1&grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Adevice_code"} +{"method": "POST", "path": "/oidc/v1/token", "raw_body": "client_id=databricks-cli&confirmation_code=AAAAAAAA&device_code=device-code-1&grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Adevice_code"} {"method": "POST", "path": "/oidc/v1/token", "raw_body": "client_id=databricks-cli&confirmation_code=FTFAQQKY&device_code=device-code-1&grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Adevice_code"} {"method": "POST", "path": "/oidc/v1/device/auth", "raw_body": "client_id=databricks-cli&scope=offline_access+all-apis"} {"method": "POST", "path": "/oidc/v1/token", "raw_body": "client_id=databricks-cli&confirmation_code=FTFAQQKY&device_code=device-code-2&grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Adevice_code"} diff --git a/acceptance/cmd/auth/login/device-code/non-interactive/errors/script b/acceptance/cmd/auth/login/device-code/non-interactive/errors/script index 02d972a67ee..00984e93321 100644 --- a/acceptance/cmd/auth/login/device-code/non-interactive/errors/script +++ b/acceptance/cmd/auth/login/device-code/non-interactive/errors/script @@ -39,6 +39,7 @@ start_login LOG.wrong-code $CLI auth login --host $DATABRICKS_HOST --profile tes title "Wrong confirmation code\n" trace musterr $CLI auth login --host $DATABRICKS_HOST --profile test --use-device-code --user-code WDJB-0001 --confirmation-code AAAA-AAAA +trace AGENT=test-agent musterr $CLI auth login --host $DATABRICKS_HOST --profile test --use-device-code --user-code WDJB-0001 --confirmation-code AAAA-AAAA # A wrong confirmation code leaves the device code valid, so the pending # login is kept. diff --git a/cmd/auth/README.md b/cmd/auth/README.md index eb08834fd00..33ea55d1f00 100644 --- a/cmd/auth/README.md +++ b/cmd/auth/README.md @@ -123,7 +123,10 @@ in two steps instead: Agent detection (`useragent.AgentProvider()`) does not pick the flow. It only adds a note after the user code, in both flows, that tells the agent to pass the login on to the user. In the flow that reads the confirmation code from -stdin, the note also tells the agent to enter the code at the prompt. +stdin, the note also tells the agent to enter the code at the prompt. When the +server rejects the confirmation code of the second step, the error gets a note +that tells the agent to ask the user to approve the login, not to approve it +itself. The pending login is a token store entry with `PendingLogin` set instead of a token. Its key is the login's cache key (the profile name, or the host without diff --git a/cmd/auth/login_device_code.go b/cmd/auth/login_device_code.go index c9a2d681c07..be3dfd00b29 100644 --- a/cmd/auth/login_device_code.go +++ b/cmd/auth/login_device_code.go @@ -254,7 +254,14 @@ func (l nonInteractiveDeviceCodeLogin) complete(ctx context.Context, userCode, c case err == nil: l.deletePendingLogin(ctx) case errors.Is(err, u2m.ErrInvalidConfirmationCode): - return nil, fmt.Errorf("%w. Check the confirmation code and approve the login if you haven't, then rerun this command. %s if it still fails", err, startNewLoginHint) + err = fmt.Errorf("%w. Check the confirmation code and approve the login if you haven't, then rerun this command. %s if it still fails", err, startNewLoginHint) + // The agent runs this step, so the error above is addressed to it. + // Only the user may approve the login. + if note := agent.AgentNote("ask the user to check the confirmation code\n" + + "and to approve the login if they haven't. Do not approve it yourself."); note != "" { + err = fmt.Errorf("%w\n\n%s", err, note) + } + return nil, err case errors.Is(err, u2m.ErrDeviceCodeRateLimited): return nil, fmt.Errorf("%w. Rerun this command later. %s if it still fails", err, startNewLoginHint) case errors.Is(err, u2m.ErrInvalidDeviceCode): From db9fab4eae121e573c0fd5a241be890f5d74a87b Mon Sep 17 00:00:00 2001 From: Maksim Zuev Date: Tue, 3 Nov 2026 10:19:14 +0000 Subject: [PATCH 11/11] Drop the final period of the wrong confirmation code agent note Error strings must not end with punctuation. Co-authored-by: Isaac --- .../auth/login/device-code/non-interactive/errors/output.txt | 2 +- cmd/auth/login_device_code.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/acceptance/cmd/auth/login/device-code/non-interactive/errors/output.txt b/acceptance/cmd/auth/login/device-code/non-interactive/errors/output.txt index ed0f12b1b20..b763d0b0028 100644 --- a/acceptance/cmd/auth/login/device-code/non-interactive/errors/output.txt +++ b/acceptance/cmd/auth/login/device-code/non-interactive/errors/output.txt @@ -66,7 +66,7 @@ Error: token exchange: Invalid confirmation code (error code: invalid_request). Error: token exchange: Invalid confirmation code (error code: invalid_request). Check the confirmation code and approve the login if you haven't, then rerun this command. Run the same command without --user-code and --confirmation-code to start a new login if it still fails Note for AI agents (test-agent): ask the user to check the confirmation code -and to approve the login if they haven't. Do not approve it yourself. +and to approve the login if they haven't. Do not approve it yourself === Rerun with the right confirmation code diff --git a/cmd/auth/login_device_code.go b/cmd/auth/login_device_code.go index be3dfd00b29..4346bf61404 100644 --- a/cmd/auth/login_device_code.go +++ b/cmd/auth/login_device_code.go @@ -258,7 +258,7 @@ func (l nonInteractiveDeviceCodeLogin) complete(ctx context.Context, userCode, c // The agent runs this step, so the error above is addressed to it. // Only the user may approve the login. if note := agent.AgentNote("ask the user to check the confirmation code\n" + - "and to approve the login if they haven't. Do not approve it yourself."); note != "" { + "and to approve the login if they haven't. Do not approve it yourself"); note != "" { err = fmt.Errorf("%w\n\n%s", err, note) } return nil, err