You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Ask who a new login connects as: your agent or yourself - #962
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)
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.
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.
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.
…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.
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.
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
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.
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.
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.
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:
Ship as is (connect_as.md §11, decision 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.
Keep the question but default to Yourself while agents are staff-only.
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.
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
authOAuth authenticationcommandsCLI command implementationsdocsskillsAgent skillstestsTests (unit and e2e)tuiTerminal UI
3 participants
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
At a terminal, against Basecamp's own authorization server,
basecamp auth loginnow asks:Enter picks your agent. That runs the existing agent-connect ceremony, through the same
connectAgentProfilecodeauth agent connectuses. 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|meanswers ahead of time (strict values;--agentis 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
--asBASECAMP_NONINTERACTIVE, machine output (flag or configured)--as me--as agent--as agent --jsonkeeps the ceremony's two-value stdout contract--as agentis refused with the ceremony's existing "no agent grant" error--login-hint/--expect-identitywithout--as--login-hint/--expect-identitywith--as agent--with-token/--with-client-credentialswith--asauth login -P <p>remedies elsewhere still do what they sayProfile for the agent: the
-P/BASECAMP_PROFILEyou name, else the active profile if it already holds an agent, elseagent(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 newagentprofile becomes the default only when it's the only profile (registerProfile's existing rule). Otherwise the login printsbasecamp 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 markedauth.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 hintbasecamp 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/DiscoverProviderpass that result toLoginorConnectAgentso they don't discover again. The non-interactive path doesn't pre-discover at all;Logindiscovers as before.Compatibility
--json/--agent/configured json) behave exactly as before: a person login, no prompt, no extra request.TestAuthLoginWithoutATerminalSignsYouInWithoutAskingruns 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 isbc5.auth agent connectis 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:--asparsing and validationBASECAMP_NONINTERACTIVE,--json, configured json)agentprofile becomes default)--asboth ways, at and off a terminal--as agent --jsoncontractinternal/auth: decline and 503 carryErrNoAgentConnection; an expired code does not.internal/tui:TestSelectFieldAnsweredWithEnterIsItsFirstOption.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--asentries), 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 loginandauth agent connecthelp, basecamp-connect and basecamp-doctor skills.Refs basecamp/bc3#14079, basecamp/bc3#14083.
Hold: do not merge.
Summary by cubic
basecamp auth loginnow 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
auth agent connect, under the-Pprofile, else a profile namedagent.--as agentor--as meanswers ahead of time;--as agent --jsonkeeps the ceremony's two-value stdout contract.--login-hintor--expect-identity, skip the question; person-only flags are refused with--as agent.auth login -P <new>asks before refusing the missing profile: the agent creates it, yourself still needs it to exist.auth agent connect --device-name, sinceauth loginhas no--device-name.Compatibility
BASECAMP_NONINTERACTIVE, machine output — sign a person in exactly as before, with no extra request.--as agentis refused with the ceremony's existing "no agent grant" error.LoginorConnectAgentinstead of discovering again.Written for commit ea5259b. Summary will update on new commits.