Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
173 changes: 172 additions & 1 deletion packages/opencode/src/altimate/plugin/altimate.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,184 @@
import type { Hooks, PluginInput } from "@opencode-ai/plugin"
import { createServer } from "http"
import { randomBytes } from "crypto"
import open from "open"
import { AltimateApi } from "../api/client"

// Loopback port the CLI listens on for the browser to deliver the gateway
// credential after Google sign-in. Must match the redirect the web authorize
// page posts back to. 7317 is otherwise unused in this codebase.
const CALLBACK_PORT = 7317

// Web app that hosts the signup/login (authorize) page. Overridable for
// dev/staging via ALTIMATE_WEB_URL.
const DEFAULT_WEB_URL = "https://app.myaltimate.com"
// Fallback gateway API base if the callback omits one.
const DEFAULT_API_URL = "https://api.myaltimate.com"

const HTML_SUCCESS = `<!doctype html><meta charset="utf-8"><title>Altimate Code</title>
<body style="font-family:system-ui;text-align:center;padding:64px">
<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>
<body style="font-family:system-ui;text-align:center;padding:64px">
<h2>Connection failed</h2><p>${msg}</p><p>Please return to your terminal and try again.</p></body>`

interface CallbackResult {
api_url: string
instance: string
api_key: string
}

interface Pending {
state: string
resolve: (creds: CallbackResult) => void
reject: (err: Error) => void
}

let server: ReturnType<typeof createServer> | undefined
let pending: Pending | undefined

async function startCallbackServer(): Promise<void> {
if (server) return
server = createServer((req, res) => {
const url = new URL(req.url || "/", `http://localhost:${CALLBACK_PORT}`)
if (url.pathname !== "/callback") {
res.writeHead(404)
res.end("Not found")
return
}

const html = (status: number, body: string) => {
res.writeHead(status, { "Content-Type": "text/html" })
res.end(body)
}

const error = url.searchParams.get("error")
if (error) {
pending?.reject(new Error(error))
pending = undefined
html(200, HTML_ERROR(error))
return
}

const state = url.searchParams.get("state")
// Bind the callback to the unguessable state the CLI generated — rejects a
// stray/malicious local request that didn't originate from our browser tab.
if (!pending || !state || state !== pending.state) {
const msg = "Invalid state — possible CSRF"
pending?.reject(new Error(msg))
pending = undefined
html(400, HTML_ERROR(msg))
return
}

const apiKey = url.searchParams.get("key")
const instance = url.searchParams.get("instance")
const apiUrl = url.searchParams.get("url") || DEFAULT_API_URL
if (!apiKey || !instance) {
const msg = "Missing credential in callback"
pending.reject(new Error(msg))
pending = undefined
html(400, HTML_ERROR(msg))
return
}

const current = pending
pending = undefined
current.resolve({ api_url: apiUrl, instance, api_key: apiKey })
html(200, HTML_SUCCESS)
})

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

function stopCallbackServer() {
if (server) {
server.close()
server = undefined
}
}

function waitForCallback(state: string, timeoutMs = 5 * 60 * 1000): Promise<CallbackResult> {
return new Promise<CallbackResult>((resolve, reject) => {
const timeout = setTimeout(() => {
if (pending) {
pending = undefined
reject(new Error("Timed out waiting for browser sign-in"))
}
}, timeoutMs)
pending = {
state,
resolve: (creds) => {
clearTimeout(timeout)
resolve(creds)
},
reject: (err) => {
clearTimeout(timeout)
reject(err)
},
}
})
}

export async function AltimateAuthPlugin(_input: PluginInput): Promise<Hooks> {
return {
auth: {
provider: "altimate-backend",
methods: [
{
type: "oauth",
label: "Altimate LLM Gateway",
async authorize() {
// 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()

const webUrl = (process.env.ALTIMATE_WEB_URL || DEFAULT_WEB_URL).replace(/\/+$/, "")
const redirect = `http://localhost:${CALLBACK_PORT}/callback`
// Land on the sign-up page and let the user choose how to authenticate
// (Google today, more providers later) rather than forcing Google.
const authorizeUrl =
`${webUrl}/register?client=altimate-code` +
`&redirect=${encodeURIComponent(redirect)}` +
`&state=${state}`

await open(authorizeUrl).catch(() => undefined)

return {
url: authorizeUrl,
instructions: "Complete sign-in in your browser to connect Altimate LLM Gateway.",
method: "auto",
async callback() {
try {
const creds = await waitForCallback(state)
// 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({
altimateUrl: creds.api_url,
altimateInstanceName: creds.instance,
altimateApiKey: creds.api_key,
})
return { type: "success", key: creds.api_key, provider: "altimate-backend" }
} catch {
return { type: "failed" }
} finally {
stopCallbackServer()
}
},
}
},
},
{
// Fallback: paste an instance-name::api-key manually.
type: "api",
label: "Connect to Altimate",
label: "Paste API key",
},
],
},
Expand Down
3 changes: 3 additions & 0 deletions packages/opencode/src/cli/cmd/providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,9 @@ export const ProvidersLoginCommand = cmd({
})

const priority: Record<string, number> = {
// altimate_change start — surface the Altimate LLM Gateway first
"altimate-backend": -1,
// altimate_change end
opencode: 0,
openai: 1,
"github-copilot": 2,
Expand Down
26 changes: 11 additions & 15 deletions packages/opencode/src/cli/cmd/tui/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,17 @@ import { Clipboard } from "@tui/util/clipboard"
import { Selection } from "@tui/util/selection"
import { MouseButton, TextAttributes } from "@opentui/core"
import { RouteProvider, useRoute } from "@tui/context/route"
import { Switch, Match, createEffect, untrack, ErrorBoundary, createSignal, onMount, batch, Show, on } from "solid-js"
import { Switch, Match, createEffect, untrack, ErrorBoundary, createSignal, onMount, batch, Show } from "solid-js"
import { win32DisableProcessedInput, win32FlushInputBuffer, win32InstallCtrlCGuard } from "./win32"
import { Installation } from "@/installation"
import { UPGRADE_KV_KEY } from "./component/upgrade-indicator-utils"
import { Flag } from "@/flag/flag"
import { Log } from "@/util/log"
import { DialogProvider, useDialog } from "@tui/ui/dialog"
import { DialogProvider as DialogProviderList } from "@tui/component/dialog-provider"
import { SDKProvider, useSDK } from "@tui/context/sdk"
import { SyncProvider, useSync } from "@tui/context/sync"
import { LocalProvider, useLocal } from "@tui/context/local"
import { DialogModel, useConnected } from "@tui/component/dialog-model"
import { DialogModel, DialogModelWelcome, useConnected, useReady } from "@tui/component/dialog-model"
import { DialogMcp } from "@tui/component/dialog-mcp"
import { DialogStatus } from "@tui/component/dialog-status"
import { DialogThemeList } from "@tui/component/dialog-theme-list"
Expand Down Expand Up @@ -470,18 +469,12 @@ function App() {
})
})

createEffect(
on(
() => sync.status === "complete" && sync.data.provider.length === 0,
(isEmpty, wasEmpty) => {
// only trigger when we transition into an empty-provider state
if (!isEmpty || wasEmpty) return
dialog.replace(() => <DialogProviderList />)
},
),
)
// altimate_change — first run is now a welcoming home-screen panel (see routes/home.tsx),
// not an auto-opened modal. The welcome panel's readiness-aware tips guide the user to
// run /connect, which opens the curated DialogModelWelcome picker.

const connected = useConnected()
const ready = useReady()
command.register(() => [
{
title: "Switch session",
Expand Down Expand Up @@ -659,14 +652,14 @@ function App() {
},
},
{
title: "Connect provider",
title: "Connect to your AI model provider",
value: "provider.connect",
suggested: !connected(),
slash: {
name: "connect",
},
onSelect: () => {
dialog.replace(() => <DialogProviderList />)
dialog.replace(() => <DialogModelWelcome />)
},
category: "Provider",
},
Expand Down Expand Up @@ -891,6 +884,9 @@ function App() {
// altimate_change start — branding: altimate upgrade
sdk.event.on(Installation.Event.UpdateAvailable.type, (evt) => {
kv.set(UPGRADE_KV_KEY, evt.properties.version)
// altimate_change — don't cover the first-run welcome panel with the update toast;
// the upgrade indicator in the footer still surfaces it. Show once a model is ready.
if (!ready()) return
toast.show({
variant: "info",
title: "Update Available",
Expand Down
Loading
Loading