Skip to content

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

Open
jeremy wants to merge 6 commits into
mainfrom
connect-as/login-choice
Open

jeremy wants to merge 6 commits into
mainfrom
connect-as/login-choice

Conversation

@jeremy

@jeremy jeremy commented Oct 23, 2026 •

Copy link
Copy Markdown
Member

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

Situation What happens
Terminal (stdin and stderr are TTYs), BC5 issuer, no --as Asks; Enter = agent
No terminal, BASECAMP_NONINTERACTIVE, machine output (flag or configured) Unchanged: signs you in, asks nothing
--as me Today's login, never asks
--as agent Ceremony, terminal or not; --as agent --json keeps the ceremony's two-value stdout contract
Launchpad (non-BC5) issuer Never asks; --as agent is refused with the ceremony's existing "no agent grant" error
--login-hint / --expect-identity without --as Count as "yourself"; nothing asked
--login-hint / --expect-identity with --as agent Refused before any request (they only apply to a person)
--with-token / --with-client-credentials with --as Refused by cobra as mutually exclusive
Re-signing a profile (or top-level credential) that already holds your own login Never 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 40991a0: 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.

Written for commit ea5259b. Summary will update on new commits.

View guided diff Turn on auto-fix

Basecamp's connections are gaining a choice of who they act as. For the
CLI the two answers are already two commands: `auth login` signs a person
in through the device flow on the first-party client, and `auth agent
connect` runs the clientless ceremony that hands this computer an agent's
own credential. The server can't merge them on the wire (an old CLI would
fail the poll after the code is spent, or store an agent's token as a
person's and drop the secret), and the device flow carries no CLI version
to tell old from new. So the choice is made here, before either flow.

At a terminal, against Basecamp's own authorization server, `auth login`
now asks "Connect this computer as": your agent (recommended, Enter) or
yourself. The agent runs the existing ceremony through the same code
`auth agent connect` uses, under the -P profile named, else the active
profile when it holds an agent, else "agent" (the guided setup's rule). A
profile holding a person's login is never turned into an agent's. If
Basecamp connects no agent (declined, or its page said agents aren't
available to this person, which the poll answers as access_denied either
way, or an intake answered 503), the login offers to connect the person
instead, and lands that login where it would have gone without the detour.

`--as agent|me` answers ahead of time. Everything that can't answer keeps
today's behaviour exactly: no terminal, BASECAMP_NONINTERACTIVE, a machine
output mode, Launchpad, --login-hint or --expect-identity (person-only),
and re-signing a profile that already holds the person's own login, so the
`auth login -P <profile>` remedies elsewhere still do what they say.
`--as agent --json` keeps the ceremony's two-value contract.

Discovery runs once: the login discovers to know whether to ask, and hands
the result to Login or ConnectAgent (auth.Provider) instead of having them
discover again. ErrNoAgentConnection marks the ceremony's no-agent endings
so the login can tell them from an expired code or a failed handover.
Copilot AI balanced review requested due to automatic review settings October 23, 2026 20:24
@jeremy
jeremy requested a review from a team as a code owner October 23, 2026 20:24
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-23T21:51:31.890758Z ea5259b New commits
🔒 Security Review ✅ Completed 2026-10-23T21:52:29.304920Z ea5259b New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions github-actions Bot added commands CLI command implementations tui Terminal UI tests Tests (unit and e2e) skills Agent skills auth OAuth authentication docs labels Oct 23, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 40991a035c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/commands/auth_login_connect_as.go
Comment thread internal/commands/auth_login_connect_as.go Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Server-bound discovery can become inconsistent after profile selection, and missing-profile validation blocks the prompted creation path.

2 open issues
What changed in this PR

Adds an agent-or-yourself choice to terminal login, reusing the existing agent connection flow and preserving the existing non-interactive login path.

Changes:

  • Adds --as agent|me, profile selection, and a personal-login fallback when no agent connects.
  • Reuses OAuth discovery and identifies declined or unavailable agent connections.
  • Adds tests and updates CLI help, documentation, skills, and command snapshots.

[!TIP]
If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or run gh pr ready --undo.
Click "Ready for review" or run gh pr ready to reengage.

File Description
skills/​basecamp-doctor/​SKILL.md Explains the login choice and --as me remedy.
skills/​basecamp-connect/​SKILL.md Documents agent-login output and non-terminal behavior.
README.md Introduces connection choices and examples.
internal/​tui/​forms.go Extracts select-field construction for testing.
internal/​tui/​forms_test.go Tests default selection and keyboard navigation.
internal/​commands/​auth.go Registers --as and delegates browser login.
internal/​commands/​auth_login_connect_as.go Implements choice, profile selection, and fallback.
internal/​commands/​auth_login_connect_as_test.go Tests login routing, compatibility, and fallback.
internal/​commands/​auth_agent.go Accepts cached discovery and adds default-profile guidance.
internal/​auth/​auth.go Exposes reusable OAuth discovery results.
internal/​auth/​agent_connect.go Reuses discovery and marks no-agent outcomes.
internal/​auth/​agent_connect_test.go Tests no-agent error classification.
install.md Explains authentication choices during installation.
.surface Records --as on both login command paths.

Review effort: Balanced

Comment thread internal/commands/auth_login_connect_as.go
Comment thread internal/commands/auth_login_connect_as.go Outdated
…camp

At a terminal, `auth login -P <new>` refused the profile for not existing
before it asked, so the agent, which creates its profile, could only be
reached with --as agent. The refusal now waits for the answer: it stands
for yourself, and as before wherever nothing is asked.

Choosing the agent can move the login onto an existing agent profile bound
to another base URL. The discovery made to ask the question belongs to the
URL the login started on, so the ceremony now discovers again when the
base URL changed, rather than connect against one Basecamp and store the
credential under another's profile.
Copilot AI balanced review requested due to automatic review settings October 23, 2026 20:32

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 78d814089c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/commands/auth_login_connect_as.go Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Switching Basecamp profiles retains the original OAuth transport policy, blocking valid local connections or retaining inappropriate loopback access.

1 open issue
2 resolved since last review

Review effort: Balanced

Comment thread internal/commands/auth_login_connect_as.go Outdated
@jeremy

jeremy commented Oct 23, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Can't wait for the next one!

Reviewed commit: 78d814089c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@robzolkos robzolkos left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My only concern is that it currently is an unreleased feature so a decision right now would send customers down a blocked path.

Asking who the login connects as means discovering first, and that first
discovery also fixes the address policy of the process's OAuth client to
the base URL the login started on. Dropping the cached provider when the
agent profile is bound elsewhere was not enough: rediscovery still ran
under the first URL's policy, refusing a loopback Basecamp after a public
one, or keeping loopback after a local one.

The case is narrow (an existing agent profile on a different Basecamp from
the one this login addresses), so rather than rebuild the client mid
command, the prompted path refuses it before anything is requested and
names the command that starts on that profile. --as agent asks nothing and
discovers nothing first, so it is unaffected.
Copilot AI balanced review requested due to automatic review settings October 23, 2026 20:44

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Cross-server fallback retains the agent profile’s cached OAuth transport policy, which can block local authentication or retain unintended loopback access.

No open issues 🎉

1 previously missed issue

In code that hasn't changed since last review

Medium severity Rebind cached OAuth transport after no-agent fallback

internal/​commands/​auth_login_connect_as.go:330

When --as agent switches to an existing agent profile on another Basecamp, accepting the no-agent fallback restores the config but leaves app.Auth's cached OAuth transport bound to that profile's address policy (internal/auth/client.go:326–329). Switching back from a public server to localhost therefore blocks local discovery; the reverse retains loopback permission for a public-server login. Rebind the transport to the restored base URL while preserving the credential store and injected clients, and add a cross-server fallback regression test that exercises address-policy enforcement.

1 resolved since last review

Review effort: Balanced

@jeremy

jeremy commented Oct 23, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review · Automatically triggered

Security review completed. No security issues were found in this pull request.

Reviewed commit: ac57044006

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

--as agent can move onto an existing agent profile bound to another base
URL; the ceremony then fixes this process's OAuth client to that
Basecamp. If it connects no agent, offering to sign the person in on the
first Basecamp would run under the wrong address policy. Where the login
moved, it now ends with the no-agent refusal, whose remedy (--as me) runs
in a process of its own.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Cross-server fallback after explicit agent login can reuse an HTTP client whose address policy blocks the original server.

1 open issue

Review effort: Balanced

Comment thread internal/commands/auth_login_connect_as.go Outdated
Copilot AI balanced review requested due to automatic review settings October 23, 2026 20:58

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Authentication and profile-routing changes warrant human sign-off, and the PR explicitly remains on hold.

No open issues 🎉

1 resolved since last review

Review effort: Balanced

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f5f9e42910

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/tui/forms.go Outdated
Comment thread internal/commands/auth_login_connect_as.go
Comment thread internal/commands/auth_login_connect_as.go
@jeremy

jeremy commented Oct 23, 2026

Copy link
Copy Markdown
Member Author

Fixed — the cross-server fallback cubic flagged as previously missed in #962 (review) (f5f9e42). When --as agent moved onto an agent profile bound to another base URL, the ceremony has fixed this process's OAuth client to that Basecamp. In that case the login no longer offers the person's sign-in on the first one. It ends with the no-agent refusal, whose remedy (basecamp auth login --as me) runs in its own process. No rebinding mid-process. Covered by the new "--as agent, no agent" case in TestAuthLoginRefusesAnAgentProfileOnAnotherBasecampAfterAsking.

@jeremy

jeremy commented Oct 23, 2026

Copy link
Copy Markdown
Member Author

Flagged for human review — @robzolkos's concern in #962 (review) is a rollout call for Jeremy, not something the code can settle. Here's what a customer without agent access would see as the PR stands. The question defaults to your agent. Production accepts the intake from anyone, so the browser shows bc3's "Connecting agents isn't available yet" page. After they click OK the terminal offers to connect them instead (Enter = yes), which is a second approval in the browser. That's one wasted page and an extra approval. Nothing breaks, nothing is stored, and nobody is stuck. Scripts and other non-TTY runs don't change. The CLI has no way to learn agent access before the browser trip, since it holds no token yet.

Options:

  1. Ship as is (connect_as.md §11, decision 2).
  2. Show the question only when experimental.connect_as is on (basecamp config set experimental.connect_as true --global) until agents widen. Everyone else gets today's login, and --as agent still works.
  3. Keep the question but default to Yourself while agents are staff-only.
  4. Hold this PR until agents widen.

Recommendation: 2. It's a small, reversible change that keeps customers on today's path and lets staff use the full flow. I'll add it if Jeremy picks it. This stays open as a decision until then.

When the agent being reconnected lives on the profile the login started
on, the no-agent offer to sign the person in would land on that profile
and replace the agent's credential with the person's. The login now ends
with the refusal there instead.

auth login has no --device-name, so the ceremony's own remedy for an
unreadable hostname named a flag the command doesn't take. The agent path
now refuses first and names auth agent connect --device-name.

tui's prompts returned their bound result in the same return statement
that runs the form. Go leaves the order of that read and the call
unspecified; gc happens to read after the call, but nothing promises it.
Each now runs the form, then returns the result.
Copilot AI balanced review requested due to automatic review settings October 23, 2026 21:10

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Authentication and credential-routing changes warrant human sign-off, and the PR remains explicitly on merge hold.

No open issues 🎉

Review effort: Balanced

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c149d6d803

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/commands/auth_login_connect_as.go Outdated
Comment thread internal/auth/agent_connect.go
The no-agent offer to sign the person in had grown a guard per bad
landing (another Basecamp, the agent being reconnected). A start on a
profile that doesn't exist yet was a third: the offered login could only
fail. The three are now one predicate about where the person's login
would land; where it can't, the refusal names --as me.

Also pins that a poll answered 503 backs off rather than counting as no
agent: only the intake's 503 carries the marker.
Copilot AI balanced review requested due to automatic review settings October 23, 2026 21:49

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Authentication and credential-routing changes warrant final human review, and the PR remains explicitly on hold.

No open issues 🎉

Review effort: Balanced

@jeremy

jeremy commented Oct 23, 2026

Copy link
Copy Markdown
Member Author

Review threads: 9 resolved (9 fixed, 0 declined).
Review bodies: 1 finding answered (1 fixed).
Fixed from a review body:

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

auth OAuth authentication commands CLI command implementations docs skills Agent skills tests Tests (unit and e2e) tui Terminal UI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants