diff --git a/internal/cmd/root_test.go b/internal/cmd/root_test.go index 048dafd5..cdf6080c 100644 --- a/internal/cmd/root_test.go +++ b/internal/cmd/root_test.go @@ -349,6 +349,28 @@ func TestRequireAuthPromptsOnlyWhenInteractiveAndStyled(t *testing.T) { t.Errorf("prompt shown %d times for machine output", *asked) } }) + + t.Run("never prompts when non-interactive is requested", func(t *testing.T) { + t.Setenv("HEY_NONINTERACTIVE", "1") + previousStdin, previousStdout, previousStderr := stdinIsTerminal, stdoutIsTerminal, stderrIsTerminal + stdinIsTerminal = func() bool { return true } + stdoutIsTerminal = func() bool { return true } + stderrIsTerminal = func() bool { return true } + t.Cleanup(func() { + stdinIsTerminal, stdoutIsTerminal, stderrIsTerminal = previousStdin, previousStdout, previousStderr + }) + asked := stubAskToSignIn(t, true) + prev := writer + writer = output.New(output.Options{Format: output.FormatStyled, Stdout: &bytes.Buffer{}, Stderr: &bytes.Buffer{}}) + t.Cleanup(func() { writer = prev }) + + if err := requireAuth(); err == nil { + t.Fatal("expected an auth error") + } + if *asked != 0 { + t.Errorf("prompt shown %d times with HEY_NONINTERACTIVE", *asked) + } + }) } func TestDataCommandWithoutAuthReturnsAuthErrorWhenPiped(t *testing.T) { diff --git a/skills/embed_test.go b/skills/embed_test.go new file mode 100644 index 00000000..842e68dc --- /dev/null +++ b/skills/embed_test.go @@ -0,0 +1,35 @@ +package skills + +import ( + "strings" + "testing" +) + +func TestHeySkillReusesAuthenticationForUnattendedAgents(t *testing.T) { + data, err := FS.ReadFile("hey/SKILL.md") + if err != nil { + t.Fatal(err) + } + content := string(data) + + for _, want := range []string{ + "hey auth status --json", + "when an explicit authentication check is needed", + "HEY_NONINTERACTIVE=1", + "Never run `hey auth login` unattended", + "report the task as blocked", + } { + if !strings.Contains(content, want) { + t.Errorf("embedded HEY skill does not contain %q", want) + } + } + + for _, forbidden := range []string{ + "run `hey auth login` first", + "before the first data command", + } { + if strings.Contains(content, forbidden) { + t.Errorf("embedded HEY skill still requires an authentication preflight with %q", forbidden) + } + } +} diff --git a/skills/hey/SKILL.md b/skills/hey/SKILL.md index fb52a1ba..d7520cf4 100644 --- a/skills/hey/SKILL.md +++ b/skills/hey/SKILL.md @@ -118,7 +118,7 @@ CLI for HEY: mailboxes, labels, collections, email threads, contacts, replies, c **MUST follow these rules:** 1. **Choose the right structured output** — use `--jq ''` to filter or extract fields and `--json` for the full response. Never pipe to an external `jq`; `--jq` is built in and implies `--json`. -2. **Authentication required** for all data commands — run `hey auth login` first +2. **Reuse stored authentication** — run the requested data command; it uses stored credentials and refreshes expiring OAuth tokens automatically. If it returns an auth error, report the task as blocked. Use `hey auth status --json` when an explicit authentication check is needed. Never run `hey auth login` unattended; use it only for interactive recovery with the user present. 3. **HTML output** is available via `--html` for commands that return HTML content 4. **Linked mail accounts share one login** — use `hey account list --json`, then `--account ` when a task must target one account 5. **Local HEY configuration requires human trust** — never run `hey config trust-local` without the user's explicit approval @@ -246,7 +246,7 @@ notice on stderr. Both need list data, so they work on `hey box list`, `hey box | List journal entries | `hey journal list --json` | | Read journal entry | `hey journal read 2024-03-15 --json` | | Write journal entry | `hey journal write "Shipped the pagination fix."` (empty content removes the entry) | -| Check auth status | `hey auth status` | +| Check auth status | `hey auth status --json` | | Print bearer token | `hey auth token` (refuses a `--cookie` login) | | Launch TUI | `hey tui` (Ctrl+A switches linked mail accounts) | @@ -751,20 +751,27 @@ error. ### Authentication +Data commands use the credentials HEY already stores and refresh expiring OAuth tokens automatically. Run the requested data command without a login preflight. Use `hey auth status --json` when the user asks for authentication status or when an explicit authentication check helps diagnose a failure; it reports whether credentials are available without changing them. + +If a data command returns exit code 3 with `"code": "auth"`, report that authentication is required and the task is blocked. Tell the user to run `hey auth login`; do not run it for them unattended. + +Piped, machine-output and non-TTY commands do not prompt for sign-in. When an agent harness runs commands under a PTY, set `HEY_NONINTERACTIVE=1` so a missing login returns the same actionable auth error instead of opening an interactive prompt. + ```bash -hey auth login # Log in (browser-based OAuth) -hey auth status # Check if authenticated -hey auth logout # Log out -hey login / hey logout # Shortcuts for the two above -hey setup omarchy # Omarchy only: put HEY in the bar. The interactive - # sign-in offer never fires for agents (non-TTY, - # machine output), so this command is the way -hey setup # First-run wizard: sign in + connect coding agents -HEY_NONINTERACTIVE=1 hey setup --json # No prompts and no OAuth wait — but still - # installs agent skills and records onboarding; - # use `hey doctor` to inspect without changes. - # (Without HEY_NONINTERACTIVE, a terminal on - # stdin still starts browser sign-in.) +hey auth status --json # Inspect stored authentication without changing it +HEY_NONINTERACTIVE=1 hey box list --json # A PTY-safe unattended data command +hey auth login # Interactive browser recovery, with the user present +hey auth logout # Log out +hey login / hey logout # Shortcuts for the two above +hey setup omarchy # Omarchy only: put HEY in the bar. The interactive + # sign-in offer never fires for agents (non-TTY, + # machine output), so this command is the way +hey setup # First-run wizard: sign in + connect coding agents +HEY_NONINTERACTIVE=1 hey setup --json # No prompts and no OAuth wait — but still + # installs agent skills and records onboarding; + # use `hey doctor` to inspect without changes. + # (Without HEY_NONINTERACTIVE, a terminal on + # stdin still starts browser sign-in.) ``` -If a command fails with an auth error, run `hey auth status` to check, then `hey auth login` to re-authenticate. +Run `hey auth login` only when the user is present and explicitly asks to authenticate.