Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 13 additions & 0 deletions docs-site/src/content/docs/guides/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,19 @@ active slot; Kiro accounts are keyed by profile ARN. `chatgpt` is always single-
pool accounts have a separate ledger.
Tokens stay in `~/.opencodex/auth.json`; `/api/oauth/accounts` returns masked metadata only.

### Cockpit Tools Antigravity import

For v1, OpenCodex imports only a **Cockpit Tools Antigravity** JSON export for the `google-antigravity` provider. In the Providers dashboard, choose the local JSON file from that provider's Accounts tab. The dashboard does not show the file contents or credential values; it reports only imported, updated, failed, and unsupported counts. Other Cockpit providers are unsupported in v1.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

The CLI accepts the export from a file or standard input only — never paste it into a command argument:

```bash
ocx account import google-antigravity --format cockpit-tools --file <path> [--json]
cat accounts.json | ocx account import google-antigravity --format cockpit-tools --stdin [--json]
```

Inline JSON and extra positional arguments are rejected. Keep exported files private and delete or store them securely after import.

### OAuth reliability

opencodex coordinates token refresh and Codex pool routing so concurrent requests do not race the
Expand Down
13 changes: 13 additions & 0 deletions docs-site/src/content/docs/ja/guides/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,19 @@ Providers ページでアカウントを追加し、別アカウントをログ
`chatgpt` は Codex アカウントプールに別の保存場所があり、常に単一スロットのみ書き込みます。トークンは `~/.opencodex/auth.json` に保存され、
`/api/oauth/accounts` はマスク済みメタデータのみを返します。

### Cockpit Tools Antigravity のインポート

v1 で OpenCodex がインポートできるのは、`google-antigravity` プロバイダー向けの **Cockpit Tools Antigravity** JSON エクスポートのみです。Providers ダッシュボードでそのプロバイダーの Accounts タブを開き、ローカル JSON ファイルを選択します。ダッシュボードはファイル内容や認証情報の値を表示せず、インポート、更新、失敗、未対応の件数だけを表示します。他の Cockpit プロバイダーは v1 では未対応です。

CLI はファイルまたは標準入力からのみエクスポートを受け取り、コマンド引数への貼り付けはできません。

```bash
ocx account import google-antigravity --format cockpit-tools --file <path> [--json]
cat accounts.json | ocx account import google-antigravity --format cockpit-tools --stdin [--json]
```

インライン JSON と余分な位置引数は拒否されます。エクスポートファイルは非公開に保ち、インポート後は削除するか安全に保管してください。

### Kiro 認証情報の取り込み

Kiro のログインには Kiro CLI が必要です。Unix では `curl -fsSL https://cli.kiro.dev/install | bash`、Windows PowerShell では `irm 'https://cli.kiro.dev/install.ps1' | iex` でインストールしてから、先に `kiro-cli login` でサインインしてください。`kiro-cli` セッションがない場合、`ocx login kiro` は貼り付けたアクセストークンまたは `KIRO_ACCESS_TOKEN` 環境変数にフォールバックします。
Expand Down
13 changes: 13 additions & 0 deletions docs-site/src/content/docs/ko/guides/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,19 @@ Providers 페이지에서 계정을 추가하고, 다른 계정을 로그아웃
`chatgpt`는 Codex 계정 풀에 별도 저장소가 있어 항상 단일 슬롯만 씁니다. 토큰은 `~/.opencodex/auth.json`에 저장되고,
`/api/oauth/accounts`는 마스킹된 메타데이터만 반환합니다.

### Cockpit Tools Antigravity 가져오기

v1에서 OpenCodex는 `google-antigravity` 공급자의 **Cockpit Tools Antigravity** JSON 내보내기만 가져옵니다. Providers 대시보드에서 해당 공급자의 Accounts 탭을 열고 로컬 JSON 파일을 선택하세요. 대시보드는 파일 내용이나 자격 증명 값을 표시하지 않으며 가져온, 업데이트된, 실패한, 지원되지 않는 항목의 수만 보고합니다. 다른 Cockpit 공급자는 v1에서 지원되지 않습니다.

CLI는 파일 또는 stdin에서만 내보내기를 받으며 명령 인수에 붙여넣을 수 없습니다:

```bash
ocx account import google-antigravity --format cockpit-tools --file <path> [--json]
cat accounts.json | ocx account import google-antigravity --format cockpit-tools --stdin [--json]
```

인라인 JSON과 추가 위치 인수는 거부됩니다. 내보낸 파일은 비공개로 보관하고 가져온 뒤 삭제하거나 안전하게 저장하세요.

### Kiro 자격 증명 가져오기

Kiro 로그인에는 Kiro CLI가 필요합니다. Unix에서는 `curl -fsSL https://cli.kiro.dev/install | bash`, Windows PowerShell에서는 `irm 'https://cli.kiro.dev/install.ps1' | iex`로 설치한 뒤 먼저 `kiro-cli login`으로 로그인하세요. `kiro-cli` 세션이 없으면 `ocx login kiro`는 붙여 넣은 액세스 토큰이나 `KIRO_ACCESS_TOKEN` 환경 변수로 폴백합니다.
Expand Down
13 changes: 13 additions & 0 deletions docs-site/src/content/docs/ru/guides/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,19 @@ OAuth-провайдеры, чьи учётные данные содержат
`chatgpt` всегда занимает один слот, поскольку у пула аккаунтов Codex отдельный реестр. Токены остаются в `~/.opencodex/auth.json`;
`/api/oauth/accounts` возвращает только маскированные метаданные.

### Импорт Cockpit Tools Antigravity

В v1 OpenCodex импортирует только JSON-экспорт **Cockpit Tools Antigravity** для провайдера `google-antigravity`. На вкладке «Аккаунты» этого провайдера в панели Providers выберите локальный JSON-файл. Панель не показывает содержимое файла или значения учётных данных: она выводит только числа импортированных, обновлённых, ошибочных и неподдерживаемых записей. Другие провайдеры Cockpit в v1 не поддерживаются.

CLI принимает экспорт только из файла или stdin — не вставляйте его в аргумент команды:

```bash
ocx account import google-antigravity --format cockpit-tools --file <path> [--json]
cat accounts.json | ocx account import google-antigravity --format cockpit-tools --stdin [--json]
```

Inline JSON и лишние позиционные аргументы отклоняются. Храните экспортированные файлы приватно и удаляйте их или защищайте после импорта.

### Импорт учётных данных Kiro

Для входа Kiro требуется Kiro CLI: в Unix установите его командой `curl -fsSL https://cli.kiro.dev/install | bash`, в Windows PowerShell — `irm 'https://cli.kiro.dev/install.ps1' | iex`, затем сначала выполните `kiro-cli login`. Если сессии `kiro-cli` нет, `ocx login kiro` использует вставленный токен доступа или переменную окружения `KIRO_ACCESS_TOKEN`.
Expand Down
13 changes: 13 additions & 0 deletions docs-site/src/content/docs/zh-cn/guides/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,19 @@ OAuth 凭据中带有稳定账号 id 或邮箱的提供商可以保存多个登
当前 active slot;Kiro 账户以配置文件 ARN 为键。`chatgpt` 始终只有一个 slot,因为 Codex 账号池使用独立存储。令牌仍保存在
`~/.opencodex/auth.json` 中;`/api/oauth/accounts` 只返回脱敏后的 metadata。

### Cockpit Tools Antigravity 导入

v1 中 OpenCodex 仅支持为 `google-antigravity` 提供商导入 **Cockpit Tools Antigravity** JSON 导出文件。在 Providers 仪表板中打开该提供商的 Accounts 标签并选择本地 JSON 文件。仪表板不会显示文件内容或凭据值,只报告已导入、已更新、失败和不支持的数量。v1 不支持其他 Cockpit 提供商。

CLI 仅从文件或标准输入读取导出文件,不能将其粘贴到命令参数中:

```bash
ocx account import google-antigravity --format cockpit-tools --file <path> [--json]
cat accounts.json | ocx account import google-antigravity --format cockpit-tools --stdin [--json]
```

内联 JSON 和额外的位置参数会被拒绝。请将导出的文件保密,并在导入后删除或安全存储。

### Kiro 凭据导入

Kiro 登录需要 Kiro CLI:Unix 使用 `curl -fsSL https://cli.kiro.dev/install | bash` 安装;Windows PowerShell 使用 `irm 'https://cli.kiro.dev/install.ps1' | iex`;然后先运行 `kiro-cli login`。如果没有 `kiro-cli` 会话,`ocx login kiro` 会回退到粘贴的访问令牌或 `KIRO_ACCESS_TOKEN` 环境变量。
Expand Down
157 changes: 156 additions & 1 deletion gui/src/components/provider-workspace/ProviderAuthPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* embedding for the workspace Settings tab (WP091). Consumes WP040+WP060
* handlers via props-down; no internal auth machinery.
*/
import { useEffect, useState } from "react";
import { useEffect, useRef, useState } from "react";
import { useT } from "../../i18n/shared";
import { IconLock, IconTrash } from "../../icons";
import type { WorkspaceItem } from "../../provider-workspace/catalog";
Expand All @@ -25,9 +25,77 @@ import type { CodexAccountPoolController } from "../../hooks/useCodexAccountPool
import type { AccountLoadState, OAuthAccountRow, ApiKeyRow, LoginHint, ProviderAuthHandlers } from "./types";

const QUOTA_ENRICH_RESERVE_MS = 4_000;
const COCKPIT_IMPORT_MAX_BYTES = 256 * 1024;
const EMPTY_OAUTH_ACCOUNTS: OAuthAccountRow[] = [];
const EMPTY_API_KEYS: ApiKeyRow[] = [];

type CockpitImportResult = {
importedCount: number;
updatedCount: number;
failedCount: number;
unsupportedCount: number;
};

const COCKPIT_RESULT_KEYS = new Set([
"totalCount", "importedCount", "updatedCount", "failedCount", "unsupportedCount", "results",
]);
const COCKPIT_RESULT_STATUSES = new Set(["imported", "updated", "failed", "unsupported"]);
const COCKPIT_STATUS_CODES: Record<string, ReadonlySet<string>> = {
imported: new Set(["imported"]),
updated: new Set(["updated"]),
failed: new Set([
"invalid_record",
"credential_rejected",
"identity_mismatch",
"missing_project",
"persist_failed",
]),
unsupported: new Set(["unsupported_provider", "unsupported_format"]),
};

function isPlainObject(value: unknown): value is Record<string, unknown> {
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
const prototype = Object.getPrototypeOf(value);
return prototype === Object.prototype || prototype === null;
}

function isSafeCount(value: unknown): value is number {
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
}

function safeCockpitImportResult(value: unknown): CockpitImportResult | null {
if (!isPlainObject(value) || Object.keys(value).some(key => !COCKPIT_RESULT_KEYS.has(key))) return null;
const { totalCount, importedCount, updatedCount, failedCount, unsupportedCount, results } = value;
if (
!isSafeCount(totalCount)
|| !isSafeCount(importedCount)
|| !isSafeCount(updatedCount)
|| !isSafeCount(failedCount)
|| !isSafeCount(unsupportedCount)
|| !Array.isArray(results)
|| results.length !== totalCount
|| importedCount + updatedCount + failedCount + unsupportedCount !== totalCount
) return null;

const observed = { imported: 0, updated: 0, failed: 0, unsupported: 0 };
for (const [index, result] of results.entries()) {
if (!isPlainObject(result) || Object.keys(result).some(key => !["index", "status", "code"].includes(key))) return null;
const status = String(result.status);
const code = String(result.code);
if (result.index !== index || !COCKPIT_RESULT_STATUSES.has(status)) return null;
const allowedCodes = COCKPIT_STATUS_CODES[status];
if (!allowedCodes?.has(code)) return null;
observed[status as keyof typeof observed] += 1;
}
if (
observed.imported !== importedCount
|| observed.updated !== updatedCount
|| observed.failed !== failedCount
|| observed.unsupported !== unsupportedCount
) return null;
return { importedCount, updatedCount, failedCount, unsupportedCount };
}

export default function ProviderAuthPanel({
item, apiBase, oauth, accounts = EMPTY_OAUTH_ACCOUNTS, keys = EMPTY_API_KEYS, accountLoadState = "ready",
switchingAccountId = null, busy = false, loginHint, authHandlers, onCodexActiveNeedsReauthChange,
Expand All @@ -51,7 +119,11 @@ export default function ProviderAuthPanel({
const [addingKey, setAddingKey] = useState(false);
const [newKey, setNewKey] = useState("");
const [keyBusy, setKeyBusy] = useState(false);
const [importBusy, setImportBusy] = useState(false);
const [importStatus, setImportStatus] = useState<"idle" | "invalid" | "failed" | "complete">("idle");
const [importResult, setImportResult] = useState<CockpitImportResult | null>(null);
const [reserveQuotaSlots, setReserveQuotaSlots] = useState(false);
const importFileRef = useRef<HTMLInputElement>(null);
const deviceCodeCopy = useCopyFeedback<string>();

// Soft &quota=1 enrichment lands after the local account list. Reserve stacked
Expand Down Expand Up @@ -123,6 +195,55 @@ export default function ProviderAuthPanel({
}
};

const importCockpitFile = async (file: File | undefined) => {
if (!file || importBusy) return;
setImportBusy(true);
setImportStatus("idle");
setImportResult(null);
try {
if (!file.name.toLowerCase().endsWith(".json") || file.size > COCKPIT_IMPORT_MAX_BYTES) {
setImportStatus("invalid");
return;
}
let document: unknown;
try {
document = JSON.parse(await file.text()) as unknown;
} catch {
setImportStatus("invalid");
return;
}
const response = await fetch(`${apiBase}/api/oauth/accounts/import`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ provider: "google-antigravity", format: "cockpit-tools", document }),
});
if (!response.ok) {
setImportStatus("failed");
return;
}
const result = safeCockpitImportResult(await response.json().catch(() => null));
if (!result) {
setImportStatus("failed");
return;
}
setImportResult(result);
setImportStatus("complete");
// The validated import is complete independently of the best-effort list refresh.
// The account-pool owner reports refresh failure through accountLoadState, which
// remains visible beside this completed import result.
try {
await authHandlers.onRetryAccounts?.(item.name);
} catch {
/* Preserve the completed import state; accountLoadState owns refresh errors. */
}
} catch {
setImportStatus("failed");
Comment thread
agentHits marked this conversation as resolved.
} finally {
if (importFileRef.current) importFileRef.current.value = "";
setImportBusy(false);
}
};

return (
<section className="pwi-section pwi-auth-section" aria-label={isOauth ? t("pws.availableAccounts") : t("pws.apiKeys")}>
<h3 className="pwi-section-title">{isOauth ? t("pws.availableAccounts") : t("pws.apiKeys")}</h3>
Expand All @@ -132,6 +253,40 @@ export default function ProviderAuthPanel({
{item.name === "anthropic" && (
<AnthropicAccountPoolSettings apiBase={apiBase} accountCount={accounts.length} />
)}
{item.name === "google-antigravity" && (
<div className="pwi-auth-add-key">
<div>
<div id="cockpit-import-description" className="pwi-auth-row-secondary">
{t("pws.cockpitImportDescription")}
</div>
<label className="sr-only" htmlFor="cockpit-import-file">{t("pws.cockpitImportFileLabel")}</label>
<input
ref={importFileRef}
id="cockpit-import-file"
type="file"
accept="application/json,.json"
className="sr-only"
aria-describedby="cockpit-import-description cockpit-import-status"
disabled={importBusy}
onChange={event => { void importCockpitFile(event.currentTarget.files?.[0]); }}
/>
</div>
<button type="button" className="btn btn-ghost btn-sm" disabled={importBusy}
onClick={() => importFileRef.current?.click()}>
{importBusy ? t("pws.cockpitImporting") : t("pws.cockpitImportChooseFile")}
</button>
<div id="cockpit-import-status" role="status" aria-live="polite">
{importStatus === "invalid" && t("pws.cockpitImportInvalid")}
{importStatus === "failed" && t("pws.cockpitImportFailed")}
{importStatus === "complete" && importResult && t("pws.cockpitImportComplete", {
imported: importResult.importedCount,
updated: importResult.updatedCount,
failed: importResult.failedCount,
unsupported: importResult.unsupportedCount,
})}
</div>
</div>
)}
<div className="pwi-auth-status-row">
<span className={`pwi-auth-dot ${activeNeedsReauth ? "pwi-auth-dot--warn" : loggedIn ? "pwi-auth-dot--ok" : "pwi-auth-dot--off"}`} aria-hidden="true" />
<span className="pwi-auth-status-text">
Expand Down
7 changes: 7 additions & 0 deletions gui/src/i18n/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1606,6 +1606,13 @@ export const de: Record<TKey, string> = {
"pws.accountsLoadFailed": "Konten konnten nicht geladen werden.",
"pws.retryAccounts": "Erneut versuchen",
"pws.noAccounts": "Noch keine Konten verbunden.",
"pws.cockpitImportDescription": "Importieren Sie einen Cockpit-Tools-Antigravity-JSON-Export von diesem Gerät. Der Dateiinhalt wird nicht angezeigt.",
"pws.cockpitImportFileLabel": "Cockpit-Tools-Antigravity-JSON-Export",
"pws.cockpitImportChooseFile": "JSON-Datei auswählen",
"pws.cockpitImporting": "Import wird ausgeführt…",
"pws.cockpitImportInvalid": "Die ausgewählte Datei ist kein gültiger JSON-Export oder zu groß.",
"pws.cockpitImportFailed": "Der Kontoimport konnte nicht abgeschlossen werden.",
"pws.cockpitImportComplete": "Import abgeschlossen: {imported} importiert, {updated} aktualisiert, {failed} fehlgeschlagen, {unsupported} nicht unterstützt.",
"pws.accountSwitching": "Wechsel läuft…",
"pws.accountCurrent": "Aktuelles Konto",
"pws.defaultModelNone": "Keins (Standard des Anbieters verwenden)",
Expand Down
7 changes: 7 additions & 0 deletions gui/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1114,6 +1114,13 @@ export const en = {
"pws.accountsLoadFailed": "Accounts could not be loaded.",
"pws.retryAccounts": "Retry",
"pws.noAccounts": "No accounts are connected yet.",
"pws.cockpitImportDescription": "Import a Cockpit Tools Antigravity JSON export from this device. The file contents are not shown.",
"pws.cockpitImportFileLabel": "Cockpit Tools Antigravity JSON export",
"pws.cockpitImportChooseFile": "Choose JSON file",
"pws.cockpitImporting": "Importing…",
"pws.cockpitImportInvalid": "The selected file is not a valid JSON export or is too large.",
"pws.cockpitImportFailed": "The account import could not be completed.",
"pws.cockpitImportComplete": "Import complete: {imported} imported, {updated} updated, {failed} failed, {unsupported} unsupported.",
"pws.accountSwitching": "Switching…",
"pws.accountCurrent": "Current account",
"pws.defaultModelNone": "None (use provider default)",
Expand Down
Loading
Loading