Skip to content

feat: [AI-7520] Altimate LLM Gateway CLI signup + onboarding UX#1001

Draft
saravmajestic wants to merge 5 commits into
mainfrom
feat/AI-7520-social-signup-authorize
Draft

feat: [AI-7520] Altimate LLM Gateway CLI signup + onboarding UX#1001
saravmajestic wants to merge 5 commits into
mainfrom
feat/AI-7520-social-signup-authorize

Conversation

@saravmajestic

@saravmajestic saravmajestic commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Summary

CLI onboarding for the Altimate LLM Gateway + a welcoming first-run experience, plus quick sign-in/out commands.

Gateway sign-in

  • Loopback (:7317) + browser → web sign-up page → credential delivered back to the CLI; saved to ~/.altimate/altimate.json (overridable via ALTIMATE_WEB_URL).
  • On success, an inline green "Authentication successful" confirmation that auto-selects a model and auto-closes (no drop into the model picker).
  • Don't force Google: the CLI lands on the sign-up page and lets the user choose their method.

First-run experience

  • WelcomePanel boot box on the home screen with readiness-aware tips (/connect/discover); update toast suppressed until a model is ready.
  • useReady/markSetupComplete gate the first-run chat lock (unlocks after connecting, choosing Big Pickle, or picking a ready model).

Provider picker

  • /connect opens the curated DialogModelWelcome — top 5 providers (Altimate Gateway first) + "Search all providers…", a Big Pickle fallback, and a bright-green ✓ "selected" marker on the currently-active provider/model.
  • The /model dialog is restructured into READY / NEEDS-SETUP sections.

New TUI commands

  • /auth — start the Altimate LLM Gateway sign-in directly (DialogAltimateAuth).
  • /logout — clear the saved gateway credential (AltimateApi.clearCredentials), disconnect, and reload.

Rebase safety

  • Onboarding code isolated into an altimate-owned component/altimate-onboarding.tsx; dialog-model.tsx trimmed to pristine useConnected + the marked DialogModel restructure, so future upstream (opencode) merges have a minimal, clearly-marked conflict surface. No behavior change.

Test plan

  • /connect → Altimate LLM Gateway → browser → provisioned + connected
  • Gateway key authenticates (/agents/v1/models200)
  • /auth launches gateway sign-in; /logout clears creds + disconnects
  • Selected provider shows the green ✓
  • bun run typecheck clean

🤖 Generated with Claude Code

saravmajestic and others added 2 commits July 9, 2026 07:56
- Add an `oauth` `method:"auto"` to the Altimate auth plugin: bind a loopback
  server on `localhost:7317`, open the browser to the web authorize page,
  verify `state`, and save the gateway credential to `~/.altimate/altimate.json`
- Surface `altimate-backend` first in provider selection (TUI + clack) with the
  "Recommended · best tool-calling · 10M free tokens" hint

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…uth success

- Add `WelcomePanel` boot box (readiness-aware tips + "What is Altimate Code")
  on the home screen; first run is a welcoming panel, not an auto-opened modal
- Restructure the model picker into READY / NEEDS-SETUP; add the curated
  `DialogModelWelcome` (top 5 providers + "Search all providers"), Big Pickle
  fallback, and `useReady` / `markSetupComplete`
- `/connect` opens the curated picker
- Altimate LLM Gateway sign-in confirms inline ("Authentication successful") and
  auto-closes (auto-selecting a model) instead of dropping into the model picker
- Don't force Google: land on the sign-up page and let the user choose
  (drop `google_start`); reword the sign-in instruction

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@saravmajestic saravmajestic self-assigned this Jul 9, 2026
@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 951000d4-100b-41d7-a585-04db1fcbb0fc

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/AI-7520-social-signup-authorize

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown

This PR doesn't fully meet our contributing guidelines and PR template.

What needs to be fixed:

  • PR description is missing required template sections. Please use the PR template.

Please edit this PR description to address the above within 2 hours, or it will be automatically closed.

If you believe this was flagged incorrectly, please let a maintainer know.

saravmajestic and others added 3 commits July 13, 2026 15:48
- /auth: sign in to the Altimate LLM Gateway directly via the OAuth loopback,
  skipping the provider picker (new `DialogAltimateAuth`)
- /logout: clear the stored gateway credential (`AltimateApi.clearCredentials`)
  and disconnect (dispose + bootstrap; the provider loader drops the now-stale
  auth-store entry on reload)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
DialogModelWelcome now tags each row with its providerID (and modelID for Big
Pickle) and compares against local.model.current(), rendering a bright-green ✓
plus a "· selected" note on the active provider — so the user can see at a glance
that e.g. Altimate LLM Gateway is already selected. Bright green (diffHighlightAdded)
is used because plain ANSI green renders dim in some terminals.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ebase safety)

Shrink our footprint on the upstream opencode `dialog-model.tsx` so future
upstream merges are easier to rebase:

- Move `useReady`/`markSetupComplete`, `DialogModelWelcome`, and
  `DialogBigPickleConfirm` into a new altimate-owned `component/altimate-onboarding.tsx`
  (zero rebase surface — our file)
- `dialog-model.tsx` (424 → 160 lines) now holds only pristine `useConnected` plus
  the `DialogModel` READY/NEEDS-SETUP restructure, fully wrapped in `altimate_change`
  markers so an upstream conflict is confined and human-resolvable
- Repoint imports in `app.tsx`, `home.tsx`, `dialog-provider.tsx`, `welcome-panel.tsx`

The onboarding file imports `DialogModel`/`useConnected` back from `dialog-model`,
but only inside callbacks/JSX — the circular reference is runtime-only and safe.
No behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@sahrizvi sahrizvi 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.

Consensus code review — PR #1001

Independent reviews from 6 models (Claude, GPT 5.4 Codex, Kimi K2.5, MiniMax M2.7, Qwen 3.6, MiMo V2.5 Pro), converged and then re-validated line-by-line against the checked-out code.

Verdict: changes requested. The onboarding refactor and OAuth loopback design are well-structured (crypto-random state, CSRF state check, 0o600 credential file, cleanly isolated onboarding). But the new loopback auth server has correctness/reliability bugs and localhost security-hardening gaps — 7 major, 9 minor — all in the auth path. See inline comments.

Nits (not inlined): recent-models section dropped from the model picker; Big Pickle won't show a ✓ in the full picker after selection; error page doesn't window.close() like the success page; documented circular import between altimate-onboarding.tsx and dialog-model.tsx; silent open() failure (mitigated — the URL is still rendered); no ALTIMATE_CALLBACK_PORT override.

Positives: crypto-random state + CSRF check, 0o600 credentials, clean rebase-safe isolation of onboarding, reuse of existing provider onSelect handlers, reliable server teardown on the normal path.

Missing tests: the entire OAuth loopback flow (early callback, invalid/missing state, error branch, timeout, port collision, persistence) and the /auth · /logout · Big Pickle · mixed-row picker paths are untested.

// Bind the port BEFORE opening the browser so the credential can
// only be delivered to this process.
const state = randomBytes(16).toString("hex")
await startCallbackServer()

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.

The browser can hand back the login before we're listening for it

MAJORRace — pending is registered too late. startCallbackServer() binds the port and starts accepting requests here inside authorize(), but the module-level pending (state + resolve/reject) is only set later when the framework calls the returned callback() (waitForCallback, ~L114). If the browser redirects before callback() runs (an already-signed-in user gets an instant redirect, or any scheduling gap), /callback sees pending === undefined and rejects as "Invalid state — possible CSRF", silently dropping the credential. The singleton also can't support two concurrent /auth flows — the second overwrites the first.

Fix: register pending keyed by state (e.g. a Map) synchronously in authorize() before opening the browser; have callback() await that promise, and buffer an early callback by state until it's awaited.

: undefined
if (model) local.model.set({ providerID: props.providerID, modelID: model }, { recent: true })
setConnected(true)
setTimeout(() => dialog.clear(), 5000)

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.

Backing out of the sign-in screen doesn't actually stop the sign-in

MAJORNo cancellation/cleanup when the dialog is dismissed. Pressing esc (or unmounting) only calls dialog.clear(); it never cancels the pending callback, so the loopback server can stay open for ~5 minutes and a late browser callback can still mutate app state after dismissal. This success-path setTimeout(() => dialog.clear(), 5000) is also never retained or cleared on unmount, so it can fire later and dismiss an unrelated dialog opened in the meantime.

Fix: guard post-await UI updates with an onCleanup disposed flag, clearTimeout in onCleanup, and stop the callback server / abort the SDK callback when the dialog is dismissed.

let pending: Pending | undefined

async function startCallbackServer(): Promise<void> {
if (server) return

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.

If port 7317 is busy, sign-in silently hangs forever with no retry

MAJORPort collision leaves a broken, silently-failing server. server is assigned by createServer() before listen(). On EADDRINUSE the listen promise rejects (L94-96) but server is never reset to undefined, so this if (server) return early-returns on the next attempt and the flow proceeds as if listening — the callback never arrives and the user waits out the 5-minute timeout with no message.

Fix: in the error handler set server = undefined (and close it), and surface a clear "port 7317 in use" error to the TUI. Consider an ephemeral port if the redirect contract allows.

<h2>Signed in ✓</h2><p>You can return to your terminal.</p>
<script>setTimeout(()=>window.close(),1500)</script></body>`

const HTML_ERROR = (msg: string) => `<!doctype html><meta charset="utf-8"><title>Altimate Code</title>

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.

The error page echoes attacker-controlled text straight into HTML

MAJORReflected, unescaped input in the callback error page (localhost XSS). HTML_ERROR(msg) interpolates msg into HTML with no escaping, and the value ultimately comes from url.searchParams (the error branch at L57, which runs before the state check). Any page that drives the browser to http://localhost:7317/callback?error=<script>... during the auth window yields reflected XSS on the localhost origin. Impact is limited (transient page, no secrets/cookies there) but it's avoidable.

Fix: HTML-escape all reflected values, and validate state before handling the error branch.

})

await new Promise<void>((resolve, reject) => {
server!.listen(CALLBACK_PORT, () => resolve())

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.

The sign-in callback server is reachable from the whole network, not just this machine

MAJORCallback server listens on all interfaces, not loopback. listen(CALLBACK_PORT) with no host binds to ::/0.0.0.0, so the auth callback server is reachable from the LAN — despite the comment claiming "the credential can only be delivered to this process." The 128-bit state still protects credential delivery, but the listener still processes remote requests (probing, plus the pre-state-check error abort below).

Fix: server!.listen(CALLBACK_PORT, "127.0.0.1", () => resolve()).

// A provider is "ready" (usable now) when it has valid credentials: it is present
// in the live provider list with at least one model — and, for the free OpenCode
// provider, with at least one paid model (a Zen key entered).
function providerReady(id: string) {

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.

A provider with unknown pricing can be treated as "ready" without a key

MINORproviderReady("opencode") mishandles undefined cost. m.cost?.input !== 0 evaluates to true when cost is undefined, so OpenCode can be counted as "ready" (usable now) even without a Zen key entered.

Fix: m.cost?.input != null && m.cost.input !== 0. (Same defect exists in useConnected() — see the separate comment.)

// Persist to ~/.altimate/altimate.json — the provider loader
// reads this first (it carries the instance/tenant + api_url
// the generic auth.json store can't).
await AltimateApi.saveCredentials({

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.

Browser sign-in skips the tenant-name check that manual entry enforces

MINOROAuth callback skips the tenant-name validation the manual path enforces. The manual paste path runs validateCredentials() (tenant regex + API check) before saving; this OAuth path calls saveCredentials() directly on the callback instance. The source is the trusted web app, so risk is low, but the invariant differs between the two entry points.

Fix: validate instance against VALID_TENANT_REGEX before persisting, or document why the OAuth source is trusted.

// useConnected() (real credentials) via useReady(), it gates the first-run chat
// lock. Module-global so it is shared across the app and resets on every process
// launch (so a fresh relaunch is a clean fresh-user state).
const [setupComplete, setSetupComplete] = createSignal(false)

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.

After signing out, the app still shows "you're all set" guidance

MINORsetupComplete is never reset on /logout. This module-global signal is OR'd with connected() in useReady(). /logout clears credentials (so connected() → false) but leaves setupComplete === true, so useReady() stays true for the rest of the process. Verified scope: useReady() only drives tips and the update-toast (not the chat/prompt), so the real impact is that the post-logout panel keeps showing "ready" tips (/discover) instead of /connect until relaunch. (Reviewers split on severity; kept minor given the cosmetic impact.)

Fix (if desired): gate the post-logout tips on connected() rather than useReady(), or add a resetSetupComplete() called from /logout.

res.end(body)
}

const error = url.searchParams.get("error")

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.

Anyone on the network can cancel your sign-in without any secret

MAJORUnauthenticated ?error= can abort an in-progress sign-in. This error branch runs before state validation and rejects/clears the current pending flow. Combined with the all-interface bind above, any local or LAN requester can cancel a user's sign-in without knowing state — there's no CSRF protection on the abort path.

Fix: validate state before honoring the error branch (this also closes the reflected-XSS issue on the same branch).

// are used by the restructured DialogModel below.
import { markSetupComplete, DialogBigPickleConfirm } from "./altimate-onboarding"

export function useConnected() {

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.

The same unknown-pricing bug also mislabels providers as connected

MINORuseConnected() has the same undefined-cost defect as providerReady(). It treats OpenCode as connected when any model has cost?.input !== 0, which is also true for undefined cost. This one has a wider blast radius: it drives the suggested: !connected() flag on the /connect and /auth commands, not just the model picker.

Fix: apply the same != null && !== 0 guard here.

@sahrizvi sahrizvi 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.

Requesting changes — PR #1001

Consensus review by 6 models (Claude, GPT 5.4 Codex, Kimi K2.5, MiniMax M2.7, Qwen 3.6, MiMo V2.5 Pro), converged and re-validated line-by-line against the code. Formalizing the verdict from the inline review above (#pullrequestreview-4705485161).

7 major, 9 minor findings — all in the new OAuth loopback auth path. The design is sound (crypto-random state, CSRF check, 0o600 credentials, cleanly isolated onboarding), but the loopback server has correctness/reliability bugs and localhost hardening gaps that should be fixed before merge.

Blocking (major):

  1. Race — pending registered after the server starts accepting requests; an instant redirect drops the credential (altimate.ts:140).
  2. Dismissing the dialog doesn't cancel the flow; server + setTimeout leak (dialog-provider.tsx:209).
  3. Port collision (EADDRINUSE) leaves server set → silent 5-min hang on retry (altimate.ts:43).
  4. Reflected, unescaped error in the callback HTML — localhost XSS (altimate.ts:23).
  5. Callback server binds all interfaces instead of 127.0.0.1 (altimate.ts:94).
  6. API key delivered in the URL query string (altimate.ts:76).
  7. Unauthenticated ?error= aborts an in-progress sign-in without knowing state (altimate.ts:57).

Minor findings and fixes are in the inline comments. Nothing here blocks the onboarding UX direction — the issues are contained to auth-path correctness, security hardening, and error handling. Happy to walk through any of them.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants