Skip to content

Make the CLI's 403 actionable instead of a bare status code - #36

Closed
KevyVo wants to merge 3 commits into
gumloop:mainfrom
KevyVo:kevin-fix-vauge-cli-pro-teir-error
Closed

Make the CLI's 403 actionable instead of a bare status code#36
KevyVo wants to merge 3 commits into
gumloop:mainfrom
KevyVo:kevin-fix-vauge-cli-pro-teir-error

Conversation

@KevyVo

@KevyVo KevyVo commented Aug 7, 2026

Copy link
Copy Markdown

Problem

Logging in to the CLI fails with:

Error: Gumloop API returned HTTP 403
Screenshot 2026-08-07 at 3 48 11 PM

This tells the user nothing: not what was refused, not why, not what to do next. Error messages should be informative and actionable.

I did not know that Gumloop need a pro level account because I had always used my work account. I only piece it together after seeing the error in my vscode debugger (dropped in CLI output) and looked at the pricing page: https://www.gumloop.com/pricing.

File as Github Issue: #35

Root cause

The server sent the reason. The client never read it.

to_api_error only pulled a message out of the body when the error came back wrapping one. This 403 returns the code by itself, so the parser skipped it and fell back to the generic status line — while tier_required_pro sat unread in .body the whole time:

image

Confirmed against production.

HTTP/2 403
content-type: application/json

{"error":"tier_required_pro"}

Two ways to solve this

1. Annotate and wrap it client-side — what this PR does, since I don't have backend access.

2. Return a usable message from the server — the better fix.

{"error": {"code": "tier_required_pro", "message": "This account needs the Pro plan or higher."}}

The CLI would just print it: no lookup table to maintain, and every client benefits, not only this one. The table added here is a workaround for one endpoint, not an abstraction worth growing. Recommend doing this server-side and deleting the entry.

What the CLI looks like with my changes

Screenshot 2026-08-07 at 2 57 04 PM

Changes

File Change
errors.py Read the code out of the body so it reaches the message and .code
cli/errors.py _ERROR_HINTS maps a code to plain language, applied in exit_with_error

The first extends the existing invalid-credential guard to 403. That guard stubs 401, but the live API returns 403 for a refused credential, so the status that actually matters was never exercised — this closes the gap while asserting the same guarantees.

The second drives agents list to cover the shared error path, proving the fix isn't scoped to login.

Testing

Added two test:
Two tests in tests/cli/test_login.py.

  • test_login_403_explains_the_tier_and_still_does_not_save — asserts the
    message names the tier, while exit code and credential storage are unchanged.
    This extends the existing test_login_with_invalid_credentials_...does_not_save
    guard to 403; that one stubs 401, but the live API returns 403 for a refused
    credential, so the status that actually matters was never covered.

  • test_tier_gated_403_on_a_normal_command_explains_itself — drives
    agents list --json to prove the hint reaches the shared error path rather
    than only login, and that the raw code is still exposed in --json for
    scripting.

Full suite passes (437), ruff and pyright clean.

Manual verification: ran the real OAuth flow against production, captured the raw response with curl -i, and stepped the error path in the debugger to confirm where the code was being dropped.

KevyVo added 3 commits August 7, 2026 15:06
Add two tests for the `tier_required_pro` 403:

- `test_login_403_explains_the_tier_and_still_does_not_save` extends the
  existing invalid-credential guard to 403. The current guard stubs 401,
  but the live API returns 403 for a refused credential, so that status
  was never exercised. Asserts the save guard still holds — non-zero
  exit, nothing persisted — while the message becomes actionable.

- `test_tier_gated_403_on_a_normal_command_explains_itself` drives
  `agents list` to cover the shared error path, proving the hint reaches
  every command rather than only `login`, and that the machine-readable
  code stays in `--json`.
The API rejects some requests with the code alone, e.g.

    HTTP/2 403
    {"error":"tier_required_pro"}

Both `to_api_error` and `APIStatusError` assumed `body["error"]` was
always a dict wrapping a `message`. Against this body the isinstance
check failed, so the parser fell through to the generic status line and
the code was discarded — it stayed in `.body`, unread. Users saw only
"Gumloop API returned HTTP 403", and `.code` was left None.

Handle the case where `error` is a string: append it to the message and
use it as `.code`.

    before: Gumloop API returned HTTP 403
    after:  Gumloop API returned HTTP 403: tier_required_pro

This is SDK-level, so any caller catching APIStatusError can now branch
on `.code` for these responses, not just the CLI.
A tier-gated account got "Error: Gumloop API returned HTTP 403", which
says nothing about what was refused or how to resolve it.

Github Issue file as: gumloop#35

Add `_ERROR_HINTS`, mapping a backend error code to plain language, and
apply it in `exit_with_error`. Every command routes its errors through
that function, so the hint reaches all of them rather than just `login`.

When a hint exists it replaces the status line rather than appending to
it — once we can name the actual problem, the raw status adds nothing
for the user. The `--json` payload is unchanged apart from a new `hint`
field: `message`, `code`, and `status_code` are still emitted for
support and scripting.

    before: Error: Gumloop API returned HTTP 403
    after:  Error: This account needs the Pro plan or higher. Commands
            will keep failing until it is upgraded at
            https://www.gumloop.com/pricing -- or ask a workspace admin
            to upgrade it.
@KevyVo

KevyVo commented Aug 7, 2026

Copy link
Copy Markdown
Author

@rbehal , @marcelo-cm , @gonzaloandresoto have a look when you get a chance 😄 , Linked to issue #35.

@marcelo-cm

Copy link
Copy Markdown
Collaborator

Hey @KevyVo — thanks for digging into this. were gonna go with the server-side fix you recommended. I put up a follow-up that returns a proper PublicError from the API and teaches the SDK to read that shape, so the CLI can just print the message — no hint table needed.

Appreciate you

@KevyVo

KevyVo commented Aug 12, 2026

Copy link
Copy Markdown
Author

@marcelo-cm was able to fix this issue by appending the fix to the backend, which a better fix. I will close this the PR infavour of #37 and close issue #35 as well.

@KevyVo KevyVo closed this Aug 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants