Skip to content
Merged
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
6 changes: 6 additions & 0 deletions devlog/_plan/260926_kiro_lb_parity2/000_plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,3 +181,9 @@ its doc against the landed lower layer anyway):
| 070 | Note at 070:390 names `tests/oauth/kiro-refusal-transport.test.ts` and `tests/oauth/kiro-account-capacity.test.ts`, which no layer registers | Point at the landed 030/040 test names instead |
| 070 | Health projection omits the `family` argument to `isCooled` | Pass it for future-proofing |
| 030/040/050/020 | 010 renamed the in-memory verdict timestamp to `observedAt`, dropped `overageEnabled`, and moved `kiroManagementHost` to 050 (wp2 P) | Correct 030:4,257,271-272; 040:510; 020:9; 030:583; 050 references at each layer's P |

## Server follow-ups recorded by later layers

| Layer | Follow-up | Why it is not in that layer |
|---|---|---|
| 100 | Reconcile the live config when a native Kiro device login finishes publishing, independent of a status request (today `/api/oauth/status` with the flowId reconciles on `done`). Closing the whole browser tab during the commit window leaves the account on disk but the running config unreconciled until the next reload. | 100 is GUI-only; its module-scoped finalizer covers component unmount and navigation while the tab stays open. Pre-existing 060 behaviour. |

Large diffs are not rendered by default.

11 changes: 7 additions & 4 deletions docs-site/src/content/docs/guides/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ ocx logout <provider>
| `kimi` | `openai-chat` | `https://api.kimi.com/coding/v1` | Kimi Code Plan coding models. Defaults to the stable `kimi-for-coding` alias (currently K2.8 Preview): 1M-token context window, adjustable `low`/`high`/`max` thinking (default `max`), text + image input. Retired `kimi-k2.x` selections are migrated to the alias on upgrade. |
| `kimi-responses` | `openai-responses` | `https://api.kimi.com/coding/v1` | Same Kimi account login (reuses the `kimi` OAuth credential) over the OpenAI Responses wire. Same model roster and capabilities as `kimi`; thinking content stays encrypted server-side, tool calls and results stay visible. |
| `nous` | `openai-chat` | `https://inference-api.nousresearch.com/v1` | Nous Research subscription gateway (same backend Hermes Agent uses). Device-grant login against `portal.nousresearch.com`; the access token is the per-request inference JWT. Mixed paid + `:free` model catalog (`tencent/hy3:free`, `stepfun/step-3.7-flash:free`, ...) discovered live from the signed-in account. Refresh tokens are single-use and rotated on every refresh. |
| `kiro` | `kiro` | `https://runtime.us-east-1.kiro.dev` | Initial login imports the installed, signed-in `kiro-cli` session (on Unix, install with `curl -fsSL https://cli.kiro.dev/install` &#124; `bash`; on Windows PowerShell, use `irm 'https://cli.kiro.dev/install.ps1'` &#124; `iex`; then run `kiro-cli login`). **Add account** logs `kiro-cli` out, starts a fresh browser login that switches the account used by `kiro-cli`, and stores account-scoped profile metadata. Existing OpenCodex accounts are preserved, and cancellation or failure restores the previous `kiro-cli` session. |
| `kiro` | `kiro` | `https://runtime.us-east-1.kiro.dev` | The dashboard Login and Add account buttons offer Builder ID, Google, GitHub device login, or Kiro CLI. Native device login adds an account without signing `kiro-cli` out. The Kiro CLI choice imports or starts a CLI session; its Add account workflow temporarily switches the CLI account and restores it on cancellation or failure. |
| `google-antigravity` | `google` | `https://daily-cloudcode-pa.googleapis.com` | Google OAuth over the Cloud Code Assist wire. Live discovery uses CCA's authenticated `v1internal:fetchAvailableModels` endpoint and publishes the agent models available to the signed-in account; the maintained catalog remains the fallback. |
| `cursor` | `cursor` | `https://api2.cursor.sh` | Experimental PKCE login, live HTTP/2 transport with an opt-in HTTP/1.1 compatibility path, and account-filtered model discovery. |
| `orcarouter-oauth` | `openai-chat` | `https://api.orcarouter.ai/v1` | Browser consent and key exchange use `https://www.orcarouter.ai` with S256 PKCE. The returned user-owned `sk-orca-…` API key is stored in the existing credential store and reused until revoked. |
Expand Down Expand Up @@ -414,7 +414,8 @@ report credit usage; an explicit zero means it reported no spend.

### Kiro credential import

Kiro login expects the Kiro CLI: on Unix, install it with `curl -fsSL https://cli.kiro.dev/install | bash`;
The dashboard offers native Builder ID, Google, and GitHub device login without Kiro CLI.
The Kiro CLI choice and `ocx login kiro` import path use the CLI: on Unix, install it with `curl -fsSL https://cli.kiro.dev/install | bash`;
on Windows PowerShell, use `irm 'https://cli.kiro.dev/install.ps1' | iex`; then sign in with `kiro-cli login`.
Without a `kiro-cli` session, `ocx login kiro` falls
back to a pasted access token or the `KIRO_ACCESS_TOKEN` environment variable.
Expand All @@ -439,17 +440,19 @@ After a successful import, opencodex persists the imported credential to
Keep these variables and the selected database private. Do not attach database files or raw login
diagnostics to bug reports.

**Add account** is a separate write workflow: it snapshots the current session, logs `kiro-cli` out,
For the **Kiro CLI** choice, **Add account** is a separate write workflow: it snapshots the current session, logs `kiro-cli` out,
and imports the fresh browser login. If the login is cancelled or fails, including while OpenCodex
persists the credential, rollback replaces the Kiro CLI database and removes its current WAL, SHM,
and journal sidecars before publishing the previous session snapshot.

Because that rollback is only possible from a snapshot, **Add account** refuses to sign `kiro-cli`
Because that rollback is only possible from a snapshot, the Kiro CLI **Add account** choice refuses to sign `kiro-cli`
out when a session store is present but cannot be captured (unreadable file, mismatched schema, or
an ambiguous token selection), when `KIROCLI_DB_PATH` / `KIRO_CLI_DB_FILE` redirect import reads away
from the live CLI store, or when an existing primary CLI database has no recognized token row.
Repair or remove the unreadable database under the normal `kiro-cli` data path, unset those import
selectors, then retry. Signing in from a machine with no existing `kiro-cli` session is unaffected.
The native dashboard choices are add-only and do not sign out `kiro-cli`. A device dialog shows the code and verification destination. Only recognized Kiro or Builder ID hosts are opened as links; an unexpected destination is shown as copyable text for review.
The account list marks Kiro accounts excluded from automatic selection with a reason, when available.

## 3. API-key catalog

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -391,7 +391,7 @@ With two or more Kiro accounts logged in, request-rate, confirmed monthly-quota,
confirmed suspension refusals can rotate before output. Monthly exhaustion excludes only
that login until reset or evidence expiry; completed service clears an older verdict.
Reactive rotation remains available when proactive account preference is off. Accounts are added one at a time —
`ocx account login kiro` hands off to the Kiro CLI and appends the new account to the pool.
`ocx account login kiro` hands off to the Kiro CLI and appends the new account to the pool. In the dashboard, Login and Add account also offer Builder ID, Google, and GitHub device login alongside the Kiro CLI choice; native device login adds an account without signing the CLI out.
To add an account without the Kiro CLI, use `ocx account login kiro --method builder-id`,
`--method google`, or `--method github`. Open the printed verification URL, enter the user
code, and wait for approval. `--no-wait` prints the flow ID; cancel it with
Expand Down
75 changes: 75 additions & 0 deletions gui/src/components/KiroDeviceLoginDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { useId, type RefObject } from "react";
import { useT } from "../i18n/shared";
import { useModalDialog } from "../pages/dashboard-shared";
import { kiroVerificationLink } from "../kiro-device-login-helpers";
import { useCopyFeedback } from "./use-copy-feedback";
import { useKiroDeviceLogin } from "./use-kiro-device-login";
import KiroLoginChooser from "./KiroLoginChooser";
import type { KiroFinalOutcome } from "../kiro-device-login-finalizer";
import "../styles/kiro-device-login.css";

export default function KiroDeviceLoginDialog({ apiBase, triggerRef, addAccount, busy, onCli, onClose, onSettled,
pollDelay }: {
apiBase: string;
triggerRef: RefObject<HTMLButtonElement | null>;
addAccount: boolean;
busy: boolean;
onCli: (addAccount: boolean) => void;
onClose: () => void;
onSettled?: (provider: string, outcome: KiroFinalOutcome) => void;
pollDelay?: (ms: number) => Promise<void>;
}) {
const t = useT();
const titleId = useId();
const dialogRef = useModalDialog(true, triggerRef);
const login = useKiroDeviceLogin(apiBase, onSettled, pollDelay);
const copy = useCopyFeedback<string>();
const { state } = login;
const view = state.view;
const destination = view?.verificationUriComplete ?? view?.verificationUri;
const safeLink = kiroVerificationLink(destination);
const cancel = () => { void login.close(); onClose(); };
return (
<dialog ref={dialogRef} className="kiro-device-dialog" aria-modal="true" aria-labelledby={titleId}
onCancel={event => { event.preventDefault(); cancel(); }}>
<h2 id={titleId}>{t("kiroLogin.title")}</h2>
{state.phase === "idle" && (
<>
<p className="muted">{t(addAccount ? "kiroLogin.addDescription" : "kiroLogin.loginDescription")}</p>
<KiroLoginChooser disabled={busy} onCli={() => { onClose(); onCli(addAccount); }} onMethod={method => { void login.start(method); }} />
</>
)}
{state.phase === "starting" && <p role="status" aria-live="polite">{t("kiroLogin.starting")}</p>}
{view && (state.phase === "pending" || state.phase === "done" || state.phase === "failed" || state.phase === "expired" || state.phase === "ended") && (
<>
{view.userCode && (
<div className="kiro-device-code">
<span className="kiro-device-code-value">{view.userCode}</span>
<button type="button" className="btn btn-ghost btn-sm" onClick={() => copy.copy(view.userCode!, view.flowId)}>
{copy.outcomeFor(view.flowId) === "copied" ? t("kiroLogin.copied") : copy.outcomeFor(view.flowId) === "unavailable" ? t("kiroLogin.copyUnavailable") : t("kiroLogin.copyCode")}
</button>
</div>
)}
{destination && (safeLink
? <a href={safeLink} target="_blank" rel="noopener noreferrer">{t("kiroLogin.openVerification")}</a>
: <><p className="kiro-device-destination">{destination}</p><p className="notice-warn">{t("kiroLogin.unexpectedHost")}</p></>)}
{view.warning === "duplicate_profile_arn" && <p className="notice-warn">{t("kiroLogin.duplicate")}</p>}
{view.warning === "manual_review_required" && <p className="notice-warn">{t("kiroLogin.manualReview")}</p>}
</>
)}
<p role="status" aria-live="polite">
{state.phase === "pending" && t("kiroLogin.pending")}
{state.phase === "done" && t("kiroLogin.done")}
{state.phase === "expired" && t("kiroLogin.expired")}
{state.phase === "ended" && t("kiroLogin.ended")}
{state.phase === "failed" && t(state.error === "start" ? "kiroLogin.startFailed" : "kiroLogin.failed")}
</p>
<div className="kiro-device-actions">
{(state.phase === "failed" || state.phase === "expired" || state.phase === "ended") && (
<button type="button" className="btn btn-ghost btn-sm" onClick={login.reset}>{t("kiroLogin.tryAgain")}</button>
)}
<button type="button" className="btn btn-ghost btn-sm" onClick={cancel}>{t("common.cancel")}</button>
</div>
</dialog>
);
}
18 changes: 18 additions & 0 deletions gui/src/components/KiroLoginChooser.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { useT } from "../i18n/shared";
import type { KiroDeviceMethod } from "../kiro-device-login-helpers";

export default function KiroLoginChooser({ disabled, onCli, onMethod }: {
disabled: boolean;
onCli: () => void;
onMethod: (method: KiroDeviceMethod) => void;
}) {
const t = useT();
return (
<div className="kiro-login-choices" role="group" aria-label={t("kiroLogin.chooseMethod")}>
<button type="button" className="btn btn-ghost" disabled={disabled} onClick={onCli}>{t("kiroLogin.cli")}</button>
<button type="button" className="btn btn-ghost" disabled={disabled} onClick={() => onMethod("builder-id")}>{t("kiroLogin.builderId")}</button>
<button type="button" className="btn btn-ghost" disabled={disabled} onClick={() => onMethod("google")}>{t("kiroLogin.google")}</button>
<button type="button" className="btn btn-ghost" disabled={disabled} onClick={() => onMethod("github")}>{t("kiroLogin.github")}</button>
</div>
);
}
22 changes: 19 additions & 3 deletions gui/src/components/provider-workspace/ProviderAuthPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import type { CodexAccountPoolController } from "../../hooks/useCodexAccountPool
import { useGrokResetCoupons } from "../../hooks/useGrokResetCoupons";
import { useAnthropicResetGrants } from "../../hooks/useAnthropicResetGrants";
import { Switch } from "../../ui";
import { kiroSkipReasonKey } from "../../kiro-device-login-helpers";
import KiroDeviceLoginDialog from "../KiroDeviceLoginDialog";
import type {
AccountLoadState,
OAuthAccountRow,
Expand Down Expand Up @@ -197,6 +199,13 @@ export default function ProviderAuthPanel({
}) {
const t = useT();
const [addingKey, setAddingKey] = useState(false);
const [kiroChooser, setKiroChooser] = useState<{ addAccount: boolean } | null>(null);
const kiroLoginTriggerRef = useRef<HTMLButtonElement>(null);
const kiroAddTriggerRef = useRef<HTMLButtonElement>(null);
const openLogin = (addAccount: boolean) => {
if (item.name === "kiro") setKiroChooser({ addAccount });
else void authHandlers?.onLogin(item.name, addAccount);
Comment on lines +205 to +207

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Route the add-provider Kiro buttons through the chooser

This interception covers only the workspace ProviderAuthPanel. In the Add provider modal's Accounts tab, CatalogAccountRow.tsx still sends Kiro's Login/Add account buttons through Providers.onAccountLogin, which calls requestLoginOAuth directly. Those dashboard buttons therefore bypass the new chooser and immediately launch the legacy Kiro CLI flow; Add account can even sign the CLI out temporarily. Route the modal's Kiro actions through the same chooser.

Useful? React with 👍 / 👎.

};
const [newKey, setNewKey] = useState("");
const [keyBusy, setKeyBusy] = useState(false);
const [importBusy, setImportBusy] = useState(false);
Expand Down Expand Up @@ -470,7 +479,7 @@ export default function ProviderAuthPanel({
{loggedIn ? (
<button type="button" className="btn btn-ghost btn-sm" onClick={() => void authHandlers.onLogout(item.name)}>{t("prov.logout")}</button>
) : (
<button type="button" className="btn btn-primary btn-sm" disabled={busy} onClick={() => void authHandlers.onLogin(item.name, false)}>
<button ref={item.name === "kiro" ? kiroLoginTriggerRef : undefined} type="button" className="btn btn-primary btn-sm" disabled={busy || (item.name === "kiro" && Boolean(kiroChooser))} onClick={() => openLogin(false)}>
{busy ? <span className="pwi-spin-inline" aria-hidden="true" /> : <IconLock style={{ width: 13, height: 13 }} aria-hidden="true" />}
{busy ? t("prov.waitingBrowser") : t("prov.login")}
</button>
Expand Down Expand Up @@ -556,6 +565,7 @@ export default function ProviderAuthPanel({
<span className={oauthHealthBadgeClass(healthStatus)}>{healthLabel}</span>
)}
{showReauth && !healthLabel && <span className="badge badge-amber">{t("pws.reauth")}</span>}
{kiroSkipReasonKey(account, item.name) && <span className="badge badge-amber">{t(kiroSkipReasonKey(account, item.name)!)}</span>}
{account.active && <span className="badge badge-primary">{t("prov.accountActive")}</span>}
{switching && <span className="badge badge-muted">{t("pws.accountSwitching")}</span>}
</button>
Expand Down Expand Up @@ -623,8 +633,8 @@ export default function ProviderAuthPanel({
)}
{loggedIn && (
<div className="pwi-auth-actions">
<button type="button" className="btn btn-ghost btn-sm"
onClick={() => void authHandlers.onLogin(item.name, true)} disabled={busy || Boolean(switchingAccountId)}>
<button ref={item.name === "kiro" ? kiroAddTriggerRef : undefined} type="button" className="btn btn-ghost btn-sm"
onClick={() => openLogin(true)} disabled={busy || Boolean(switchingAccountId) || (item.name === "kiro" && Boolean(kiroChooser))}>
{t("pws.addAccount")}
</button>
{canRefreshQuota && (
Expand All @@ -648,6 +658,12 @@ export default function ProviderAuthPanel({
)}
</>
)}
{item.name === "kiro" && kiroChooser && (
<KiroDeviceLoginDialog apiBase={apiBase} triggerRef={kiroChooser.addAccount ? kiroAddTriggerRef : kiroLoginTriggerRef}
addAccount={kiroChooser.addAccount} busy={busy} onClose={() => setKiroChooser(null)}
onCli={addAccount => { void authHandlers.onLogin(item.name, addAccount); }}
onSettled={authHandlers.onNativeLoginSettled} />
)}

{isKeyAuth && (
<>
Expand Down
3 changes: 3 additions & 0 deletions gui/src/components/provider-workspace/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ export type OAuthAccountRow = AccountQuotaReading & {
email?: string;
active: boolean;
needsReauth?: boolean;
autoSelectable?: boolean;
skipReason?: "needs_reauth" | "suspended" | "cooldown" | "quota_exhausted";
health?: { status: OAuthAccountHealthStatus; reason?: string; until?: string };
healthLabel?: string;
healthSummary?: string;
Expand All @@ -82,6 +84,7 @@ export type AccountLoadState = "idle" | "loading" | "ready" | "error";

export interface ProviderAuthHandlers {
onLogin: (provider: string, addAccount?: boolean) => void | Promise<void>;
onNativeLoginSettled?: (provider: string, outcome: "added" | "ended" | "failed") => void | Promise<void>;
onCancelLogin?: (provider: string) => void;
onLogout: (provider: string) => void | Promise<void>;
onReauth: (provider: string, accountId?: string) => void | Promise<void>;
Expand Down
Loading
Loading