Ask who a new login connects as: your agent or yourself

JE
7h ago
+1451-94

What

At a terminal, against Basecamp's own authorization server, basecamp auth login now asks:

Connect this computer as
> Your agent (recommended)
  Yourself

Enter picks your agent. That runs the existing agent-connect ceremony, through the same connectAgentProfile code auth agent connect uses. Its approval page offers Create my agent (now with name and avatar, basecamp/bc3#14083), my agent, or a team agent. Choosing yourself runs today's device-flow login. --as agent|me answers ahead of time (strict values; --agent is taken by the output mode).

This is v1 of "Connect as" from basecamp/bc3#14079, doc/oauth/connect_as.md §4: CLI-only, no server change, no SDK change.

Why this shape

The server can't safely send the login's device poll into the agent handover. An old CLI would either fail the poll after the code is spent, or save an agent's token as a person's and drop the secret. The device flow also sends no CLI version. So the choice is made in the CLI, before either flow starts.

Behaviour

SituationWhat happens
Terminal (stdin and stderr are TTYs), BC5 issuer, no --asAsks; Enter = agent
No terminal, BASECAMP_NONINTERACTIVE, machine output (flag or configured)Unchanged: signs you in, asks nothing
--as meToday's login, never asks
--as agentCeremony, terminal or not; --as agent --json keeps the ceremony's two-value stdout contract
Launchpad (non-BC5) issuerNever asks; --as agent is refused with the ceremony's existing "no agent grant" error
--login-hint / --expect-identity without --asCount as "yourself"; nothing asked
--login-hint / --expect-identity with --as agentRefused before any request (they only apply to a person)
--with-token / --with-client-credentials with --asRefused by cobra as mutually exclusive
Re-signing a profile (or top-level credential) that already holds your own loginNever asks: that's signing back in, not a new connection, so the auth login -P <p> remedies elsewhere still do what they say

Profile for the agent: the -P/BASECAMP_PROFILE you name, else the active profile if it already holds an agent, else agent (created if needed). This is the guided setup's rule (guidedSetupProfile), reused. A profile holding a person's login is refused, never converted. Reconnecting an agent profile says that approving replaces its credential. A new agent profile becomes the default only when it's the only profile (registerProfile's existing rule). Otherwise the login prints basecamp profile set-default agent.

No agent access: bc3's page shows "Connecting agents isn't available yet". Its "OK, got it" denies the grant, so the poll answers access_denied, the same as an explicit Decline. The CLI can't tell the two apart. An intake 503 (not issuing) is the other no-agent ending. Both are now marked auth.ErrNoAgentConnection. Through login, the message is "No agent was connected: the connection was declined, or agents aren't available to you yet". At a terminal it then asks "Connect as yourself instead?" (Enter = yes) and runs the person's login under the profile it would have used without the detour. Without a terminal it exits non-zero (auth) with the hint basecamp auth login --as me. An expired code is not treated as no-agent.

Discovery runs once. To know whether to ask, the login has to discover first. auth.Provider / DiscoverProvider pass that result to Login or ConnectAgent so they don't discover again. The non-interactive path doesn't pre-discover at all; Login discovers as before.

Compatibility

  • Non-interactive runs (scripts, CI, no TTY, --json/--agent/configured json) behave exactly as before: a person login, no prompt, no extra request. TestAuthLoginWithoutATerminalSignsYouInWithoutAsking runs the real gate with stdin as a pipe. It asserts nothing is asked, no intake request is made, discovery runs once, and the stored credential is bc5.
  • No wire change: the same device-flow and agent-connection requests as today. Works against today's production bc3, with no server dependency.
  • auth agent connect is unchanged apart from its help text.

Tests

  • internal/commands/auth_login_connect_as_test.go: 19 tests (+6 subtests), against one mock Basecamp serving both the device flow and the ceremony. Covered:
    • Enter default (agent offered first, plus a tui test that a select's Enter picks its first option)
    • --as parsing and validation
    • the gate (pipe stdin, BASECAMP_NONINTERACTIVE, --json, configured json)
    • the non-TTY path unchanged
    • prompt → agent (one discovery, no device flow, agent profile becomes default)
    • prompt → yourself
    • --as both ways, at and off a terminal
    • person-only flags
    • re-signing your own profile asks nothing
    • a person-login profile is refused for the agent
    • reconnect notice, set-default hint
    • Launchpad
    • no-agent fallback (access_denied and 503) → yes / no / non-interactive
    • cancel at the question
    • --as agent --json contract
  • internal/auth: decline and 503 carry ErrNoAgentConnection; an expired code does not.
  • internal/tui: TestSelectFieldAnsweredWithEnterIsItsFirstOption.
  • Full bin/ci (make check) on 40991a03: exit 0. 5,228 top-level Go tests pass, 0 fail, 6 skipped. 510/510 bats e2e. Lint, surface (+2 --as entries), skill drift, hint-command resolution and tidy all pass. Run on a Linux host: this Mac session can't open ptys, so the terminal-predicate tests fail here on any branch.

Docs: README (Authentication; agent section), install.md, auth login and auth agent connect help, basecamp-connect and basecamp-doctor skills.

Refs basecamp/bc3#14079, basecamp/bc3#14083.

Hold: do not merge.


Summary by cubic

basecamp auth login now asks, at a terminal against Basecamp's own server, whether this computer connects as your agent or as yourself. The choice is made in the CLI before either flow starts: the server can't safely send the login's device poll into the agent handover, and the device flow carries no CLI version to tell old from new.

Behavior

  • Choosing your agent runs the same approval ceremony as auth agent connect, under the -P profile, else a profile named agent.
  • --as agent or --as me answers ahead of time; --as agent --json keeps the ceremony's two-value stdout contract.
  • When no agent connects — declined, or the intake answers 503 (a poll answered 503 backs off instead, and isn't counted) — a terminal login offers to connect you instead; off a terminal it fails with a hint. The offer is skipped where the person's login couldn't land: the ceremony ran on another Basecamp (this process's OAuth client is fixed to that profile's), the agent being reconnected is the profile the login started on (the person's login would replace its credential), or the login started on a profile that doesn't exist.
  • Re-signing a profile that already holds your own login, and --login-hint or --expect-identity, skip the question; person-only flags are refused with --as agent.
  • At a terminal, auth login -P <new> asks before refusing the missing profile: the agent creates it, yourself still needs it to exist.
  • Asking means discovering first, which fixes the OAuth client to the base URL the login started on; an agent profile bound to another Basecamp is refused before anything is requested, naming the command that starts there.
  • Where this computer's hostname can't be read, the agent path refuses before anything is requested and names auth agent connect --device-name, since auth login has no --device-name.

Compatibility

  • Non-interactive runs — no TTY, scripts, CI, BASECAMP_NONINTERACTIVE, machine output — sign a person in exactly as before, with no extra request.
  • Launchpad is unchanged: it never asks, and --as agent is refused with the ceremony's existing "no agent grant" error.
  • Discovery runs once: the login hands the discovered provider to Login or ConnectAgent instead of discovering again.