Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
0fab8b5
Hold a cross-process lock across the credential's whole read-modify-w…
jorgemanrubia Sep 15, 2026
0a87b1d
Authenticate as a Basecamp agent with the client_credentials grant
jorgemanrubia Sep 15, 2026
43a5d30
Add `auth login --with-client-credentials` for agent profiles
jorgemanrubia Sep 15, 2026
04d2d62
Close the windows an adversarial review found around the credential lock
jorgemanrubia Sep 15, 2026
e332082
Answer the Copilot review: a canceled command takes no lock, and a st…
jorgemanrubia Sep 15, 2026
9154b97
Refuse a canceled caller before the unlocked fall-through, not after it
jorgemanrubia Sep 15, 2026
e8167c3
Route every unlocked fall-through through one cancellation check
jorgemanrubia Sep 15, 2026
2b130d3
Point a failed agent mint at the agent login, not a person's
jorgemanrubia Sep 15, 2026
5e6c27f
Decide the login remedy from the credential, and keep the secret out …
jorgemanrubia Sep 15, 2026
e1117e9
Keep the mint's own remedy local, and close two ways the secret survi…
jorgemanrubia Sep 15, 2026
fbd6a04
Bound the token import by its command, and refuse a scope the CLI can…
jorgemanrubia Sep 15, 2026
45d3e51
Send every server-derived string through one renderer
jorgemanrubia Sep 15, 2026
132c35d
Repeat nothing the token endpoint wrote, instead of trying to redact it
jorgemanrubia Sep 15, 2026
c96f11e
Let file-backed reads wait out a writer, and keep a hostile path out …
jorgemanrubia Sep 15, 2026
0bb6aae
Tell a busy store apart from an empty one, and stop waiting when the …
jorgemanrubia Sep 15, 2026
7506639
Refuse a token this CLI cannot send, and hand back a retry command th…
jorgemanrubia Sep 15, 2026
ca289e2
Say what a read failure was, and stop the gate guessing
jorgemanrubia Sep 15, 2026
8212f43
Never make the authentication gate wait on a writer
jorgemanrubia Sep 15, 2026
f804b52
Let a gate tell an absent credential from an unreadable store
jorgemanrubia Sep 15, 2026
a0df951
Stop setup offering a login for a store it merely could not read
jorgemanrubia Sep 15, 2026
b7f4f57
Build an error's remedy from what was already seen, never from a fres…
jorgemanrubia Sep 15, 2026
8f40403
Learn what the credential is from every path that handles one
jorgemanrubia Sep 15, 2026
eb81e1a
Let a canceled command go, and stop the completion refresh guessing
jorgemanrubia Sep 15, 2026
cd59df7
Ask for the secret again only when the secret is what was refused
jorgemanrubia Sep 15, 2026
f8e9565
Let the status decide before the body's error code
jorgemanrubia Sep 15, 2026
54aded6
Tell the doctor skill not to re-authenticate an agent as a person
jorgemanrubia Sep 15, 2026
f2048d6
Stop every remaining path telling an agent profile to sign in as a pe…
jorgemanrubia Sep 15, 2026
a8d7b8f
Answer "no credentials found" with the login for the credential that …
jorgemanrubia Sep 15, 2026
73ce7e9
Ask for the secret only for the two refusals it can fix, and keep the…
jorgemanrubia Sep 15, 2026
bc00000
Let the server's own words decide, not the status it sent them with
jorgemanrubia Sep 15, 2026
8665972
Name an account in a recovery command only when someone supplied one
jorgemanrubia Sep 15, 2026
165b754
Treat a stored credential that cannot serve a token as broken, not mi…
jorgemanrubia Sep 15, 2026
e162dd0
Say that a login can return an agent
jorgemanrubia Sep 15, 2026
a797083
Say which credential is broken, not just that nothing works
jorgemanrubia Sep 15, 2026
f168628
Let `auth status` tell an empty store from an unreadable one
jorgemanrubia Sep 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .surface
Original file line number Diff line number Diff line change
Expand Up @@ -1955,6 +1955,7 @@ FLAG basecamp auth --verbose type=count
FLAG basecamp auth login --account type=string
FLAG basecamp auth login --agent type=bool
FLAG basecamp auth login --cache-dir type=string
FLAG basecamp auth login --client-id type=string
FLAG basecamp auth login --count type=bool
FLAG basecamp auth login --device-code type=bool
FLAG basecamp auth login --expect-identity type=string
Expand All @@ -1980,6 +1981,7 @@ FLAG basecamp auth login --stats type=bool
FLAG basecamp auth login --styled type=bool
FLAG basecamp auth login --todolist type=string
FLAG basecamp auth login --verbose type=count
FLAG basecamp auth login --with-client-credentials type=bool
FLAG basecamp auth login --with-token type=bool
FLAG basecamp auth logout --account type=string
FLAG basecamp auth logout --agent type=bool
Expand Down Expand Up @@ -10775,6 +10777,7 @@ FLAG basecamp lineup update --verbose type=count
FLAG basecamp login --account type=string
FLAG basecamp login --agent type=bool
FLAG basecamp login --cache-dir type=string
FLAG basecamp login --client-id type=string
FLAG basecamp login --count type=bool
FLAG basecamp login --device-code type=bool
FLAG basecamp login --expect-identity type=string
Expand All @@ -10800,6 +10803,7 @@ FLAG basecamp login --stats type=bool
FLAG basecamp login --styled type=bool
FLAG basecamp login --todolist type=string
FLAG basecamp login --verbose type=count
FLAG basecamp login --with-client-credentials type=bool
FLAG basecamp login --with-token type=bool
FLAG basecamp logout --account type=string
FLAG basecamp logout --agent type=bool
Expand Down
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,26 @@ an envelope with the profile, account, identity and person, `oauth_type`,
`null` when it reports none). A token has no refresh token, so near a reported
expiry the CLI refuses it and asks for a fresh import.

### Agents

A Basecamp agent is a principal with no person behind it, so it has no browser
to sign in with and is granted no refresh token. It authenticates with the
OAuth `client_credentials` grant instead: its client id and secret mint a
short-lived self-token, and the CLI mints another whenever that one nears
expiry. The client secret is read from stdin (never an argument) and stored
with the token it mints — in the OS keyring where one is available — because
the client, not a refresh token, is what survives an expiry:

```bash
op read "op://Vault/Item/credential" | basecamp auth login --with-client-credentials --client-id <id> -P agent --account 999
```

The login mints once, which is what proves the client id and secret: a refused
mint stores nothing. `--account` is required when the profile does not exist
yet. `basecamp auth logout` forgets the credential; there is no useful
revocation, since the same client would mint another — rotate the client
secret in Basecamp to end an agent's access.

### Multiple Identities

Use named profiles when the same machine or agent gateway needs more than one Basecamp identity. Each profile has its own stored OAuth credentials and can be selected per command:
Expand Down
35 changes: 35 additions & 0 deletions e2e/auth.bats
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,41 @@ load test_helper
assert_json_value '.code' 'usage'
}

@test "basecamp auth login --help shows the agent client-credentials flags" {
run basecamp auth login --help
assert_success
assert_output_contains "--with-client-credentials"
assert_output_contains "--client-id"
assert_output_contains "no refresh token"
}

@test "basecamp auth login --with-client-credentials requires a client id" {
run basecamp auth login --with-client-credentials -P agent </dev/null
assert_failure
assert_json_value '.error' '--with-client-credentials needs the agent'"'"'s OAuth client id'
assert_json_value '.code' 'usage'
}

@test "basecamp auth login --with-client-credentials requires a profile" {
run env -u BASECAMP_PROFILE basecamp auth login --with-client-credentials --client-id abc </dev/null
assert_failure
assert_json_value '.error' '--with-client-credentials stores the credential under a named profile'
assert_json_value '.code' 'usage'
}

@test "basecamp auth login rejects --client-id on its own" {
run basecamp auth login --client-id abc </dev/null
assert_failure
assert_json_value '.error' '--client-id only applies to an agent login'
assert_json_value '.code' 'usage'
}

@test "basecamp auth login --with-client-credentials rejects --with-token" {
run basecamp auth login --with-client-credentials --with-token </dev/null
assert_failure
assert_output_contains "with-client-credentials"
}

@test "basecamp auth login --with-token rejects --device-code" {
run basecamp auth login --with-token --device-code </dev/null
assert_failure
Expand Down
13 changes: 13 additions & 0 deletions install.md
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,19 @@ basecamp search "meeting" --json

## Troubleshooting

**Check what you are fixing first.** `basecamp auth status --json` reports
`oauth_type`, and `agent` means the profile is a Basecamp agent: a principal
with no person behind it, which authenticates with its OAuth client rather than
a sign-in. None of the logins below apply to one — they would store a person's
credential over it — and `auth logout` would throw away the client credentials
it needs to recover. An agent re-authenticates with its own login, which
replaces the credential in place. When `oauth_type` is absent, run whatever
the status output's own hint names rather than choosing from the list below:

```bash
op read "op://<vault>/<item>/credential" | basecamp auth login --with-client-credentials --client-id <id> -P <profile> --account <id>
```

**Not authenticated:**
```bash
basecamp auth login
Expand Down
Loading
Loading