From b7cf63888ac31eb09388d470ead1fecad4a10bdc Mon Sep 17 00:00:00 2001 From: Maksim Zuev Date: Thu, 29 Oct 2026 12:02:40 +0000 Subject: [PATCH 1/6] 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 5a70dc2fd0c..9ffa6ba797e 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 2f00d282b27..7996fda4427 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-MJHT --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 a1d56b7cc5f..fac5c05e00b 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 a2228c5d15d..40c2ca4c0c8 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" ) @@ -183,11 +184,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 27e16ff37eb..cac385b6f1b 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 6dcbe6759cd794a00eacde1515844d5bd6273947 Mon Sep 17 00:00:00 2001 From: Maksim Zuev Date: Thu, 29 Oct 2026 13:09:53 +0000 Subject: [PATCH 2/6] 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 40c2ca4c0c8..ec6c2b728b5 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, @@ -185,15 +191,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 da5225a09904aeab907fd3e0e4e3778d52f7e8d4 Mon Sep 17 00:00:00 2001 From: Maksim Zuev Date: Thu, 29 Oct 2026 13:12:02 +0000 Subject: [PATCH 3/6] 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 fac5c05e00b..3118124fa84 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 ec6c2b728b5..457b7ee68b6 100644 --- a/cmd/auth/login_device_code.go +++ b/cmd/auth/login_device_code.go @@ -190,16 +190,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 } @@ -302,8 +297,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, "", @@ -311,6 +308,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 f0fddf1e063..ad232b97080 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 cac385b6f1b..5f17082d4a4 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 9fdab7956574a507cc373522a1adf91c445b8497 Mon Sep 17 00:00:00 2001 From: Maksim Zuev Date: Thu, 29 Oct 2026 15:40:24 +0000 Subject: [PATCH 4/6] 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 3118124fa84..3194021f5dd 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 457b7ee68b6..706a5471a7e 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, @@ -190,7 +201,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.", @@ -274,7 +285,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 { @@ -299,7 +310,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, @@ -308,7 +319,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 ad232b97080..5987c0eaf46 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 5f17082d4a4..6fabf6343f2 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 46f6bad6c61673b01aa462f82ccd7a57ef5e1388 Mon Sep 17 00:00:00 2001 From: Maksim Zuev Date: Thu, 29 Oct 2026 16:28:30 +0000 Subject: [PATCH 5/6] 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 706a5471a7e..e2f4978ad1a 100644 --- a/cmd/auth/login_device_code.go +++ b/cmd/auth/login_device_code.go @@ -319,7 +319,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 f52b31d163be3f0681b3ff91b9e1b66324a8d503 Mon Sep 17 00:00:00 2001 From: Maksim Zuev Date: Thu, 29 Oct 2026 16:41:17 +0000 Subject: [PATCH 6/6] 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 e2f4978ad1a..08d3936295a 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, @@ -201,7 +189,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.", @@ -285,7 +274,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 {