Repository navigation
Tell AI agents to pass the device code login on to the user #7235
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
ded0b34
7ce3594
d39257d
f40c09b
7fbb0c7
a287e6d
2c8b948
2b07a4e
79f7b66
423935c
db9fab4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,6 +7,7 @@ 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" | ||
|
|
@@ -30,6 +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 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" | ||
|
|
||
| // deviceCodeIncompatibleFlags lists flags that device code login cannot honor. | ||
| var deviceCodeIncompatibleFlags = []string{ | ||
| // Resource indicators are sent on the browser authorization request, | ||
|
|
@@ -198,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), | ||
| agentNote := deviceCodeAgentNote + "Ask the user for the confirmation code their browser shows." | ||
|
maksim-zuev-db marked this conversation as resolved.
Resolved
|
||
| 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.", | ||
|
|
@@ -247,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): | ||
|
|
@@ -291,7 +305,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), | ||
| agentNote := deviceCodeAgentNote + "Ask the user for the confirmation code their browser shows, and enter it\n" + | ||
| `at the "Confirmation code" prompt of this command.` | ||
|
Comment on lines
+308
to
+309
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. An agent won't have an interactive terminal, so I don't think this note will work for it. Without a terminal it already gets the two-step flow and never sees this text. If the command does run in a PTY (some IDE agent terminals), the agent usually can't type into a running command, so it waits at the prompt until the login times out. Could agent detection pick the two-step flow (start, then rerun with
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Some agents are able to start commands in a terminal mode, e.g. codex:
|
||
| cmdio.LogString(ctx, strings.Join(append(approvalInstructions(da, agentNote), | ||
| "Once finished, enter the confirmation code provided in your browser.", | ||
| ), "\n")) | ||
| } else { | ||
|
|
@@ -314,15 +330,21 @@ 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. | ||
| func approvalInstructions(da u2m.DeviceAuthorization) []string { | ||
| return []string{ | ||
| // 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, agentNote string) []string { | ||
| lines := []string{ | ||
| "Using a browser on another device, visit:", | ||
| da.VerificationURI, | ||
| "", | ||
| "And enter the code:", | ||
| formatCode(da.UserCode), | ||
| "", | ||
| } | ||
| if note := agent.AgentNote(agentNote); note != "" { | ||
| lines = append(lines, note, "") | ||
| } | ||
| return lines | ||
| } | ||
|
|
||
| // formatCode displays an 8-character device code as two groups of four | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| 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) | ||
| t.Setenv("AGENT", agent) | ||
| useragent.ClearCache() | ||
| t.Cleanup(useragent.ClearCache) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| package agent | ||
|
|
||
| import ( | ||
| "testing" | ||
|
|
||
| "github.com/databricks/cli/internal/testutil" | ||
| "github.com/stretchr/testify/assert" | ||
| ) | ||
|
|
||
| func TestAgentNote(t *testing.T) { | ||
| testutil.SetAgent(t, "test-agent") | ||
| assert.Equal(t, "Note for AI agents (test-agent): ask the user.", AgentNote("ask the user.")) | ||
| } | ||
|
|
||
| func TestAgentNoteWithoutAgent(t *testing.T) { | ||
| testutil.SetAgent(t, "") | ||
| assert.Empty(t, AgentNote("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()) | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.