diff --git a/docs-site/public/screenshots/cockpit-account-import.png b/docs-site/public/screenshots/cockpit-account-import.png new file mode 100644 index 000000000..ce9e05af0 Binary files /dev/null and b/docs-site/public/screenshots/cockpit-account-import.png differ diff --git a/docs-site/src/content/docs/guides/providers.md b/docs-site/src/content/docs/guides/providers.md index 81f3bbdf2..f08e6ca72 100644 --- a/docs-site/src/content/docs/guides/providers.md +++ b/docs-site/src/content/docs/guides/providers.md @@ -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 rejected in v1. + +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 [--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 diff --git a/docs-site/src/content/docs/ja/guides/providers.md b/docs-site/src/content/docs/ja/guides/providers.md index 607c7d5a8..aa5035320 100644 --- a/docs-site/src/content/docs/ja/guides/providers.md +++ b/docs-site/src/content/docs/ja/guides/providers.md @@ -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 [--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` 環境変数にフォールバックします。 diff --git a/docs-site/src/content/docs/ko/guides/providers.md b/docs-site/src/content/docs/ko/guides/providers.md index 219cdaa8e..65261a1bb 100644 --- a/docs-site/src/content/docs/ko/guides/providers.md +++ b/docs-site/src/content/docs/ko/guides/providers.md @@ -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 [--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` 환경 변수로 폴백합니다. diff --git a/docs-site/src/content/docs/ru/guides/providers.md b/docs-site/src/content/docs/ru/guides/providers.md index 7389f7931..38f7e5e52 100644 --- a/docs-site/src/content/docs/ru/guides/providers.md +++ b/docs-site/src/content/docs/ru/guides/providers.md @@ -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 [--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`. diff --git a/docs-site/src/content/docs/zh-cn/guides/providers.md b/docs-site/src/content/docs/zh-cn/guides/providers.md index c6b106854..2380130b5 100644 --- a/docs-site/src/content/docs/zh-cn/guides/providers.md +++ b/docs-site/src/content/docs/zh-cn/guides/providers.md @@ -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 [--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` 环境变量。 diff --git a/gui/src/components/provider-workspace/ProviderAuthPanel.tsx b/gui/src/components/provider-workspace/ProviderAuthPanel.tsx index 6a19c5f0d..d8c76540c 100644 --- a/gui/src/components/provider-workspace/ProviderAuthPanel.tsx +++ b/gui/src/components/provider-workspace/ProviderAuthPanel.tsx @@ -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"; @@ -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> = { + 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 { + 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, @@ -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(null); const [reserveQuotaSlots, setReserveQuotaSlots] = useState(false); + const importFileRef = useRef(null); const deviceCodeCopy = useCopyFeedback(); // Soft "a=1 enrichment lands after the local account list. Reserve stacked @@ -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"); + } finally { + if (importFileRef.current) importFileRef.current.value = ""; + setImportBusy(false); + } + }; + return (

{isOauth ? t("pws.availableAccounts") : t("pws.apiKeys")}

@@ -132,6 +253,40 @@ export default function ProviderAuthPanel({ {item.name === "anthropic" && ( )} + {item.name === "google-antigravity" && ( +
+
+
+ {t("pws.cockpitImportDescription")} +
+ + { void importCockpitFile(event.currentTarget.files?.[0]); }} + /> +
+ +
+ {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, + })} +
+
+ )}