diff --git a/docs-site/src/content/docs/reference/cli/providers-accounts.md b/docs-site/src/content/docs/reference/cli/providers-accounts.md index f7e94a1833..6940ba2902 100644 --- a/docs-site/src/content/docs/reference/cli/providers-accounts.md +++ b/docs-site/src/content/docs/reference/cli/providers-accounts.md @@ -250,6 +250,53 @@ and the plan/label column falls back across plan, masked email, label, and maske } ``` +### `ocx account import-orca --source --registry [--apply] [--json]` + +Reuse local Orca-managed Codex logins without another browser login. The source +directory must contain `codex-accounts//home/auth.json`. Supply the Orca +data directory and the chosen profile's `orca-data.json` explicitly. Only accounts +registered in that profile are considered; leftover or removed account homes are +not imported. Each home must carry Orca's matching `.orca-managed-home` marker. +The command does not scan unrelated homes or remote hosts. + +```powershell +# Windows: preview only; no account is registered. +ocx account import-orca --source "$env:APPDATA\orca" --registry "$env:APPDATA\orca\profiles\local-default\orca-data.json" --json + +# Finish active proxy requests and stop the proxy before applying. +ocx stop +ocx account import-orca --source "$env:APPDATA\orca" --registry "$env:APPDATA\orca\profiles\local-default\orca-data.json" --apply +ocx start +``` + +Older Orca profiles may use `orca-data.json` directly under the data directory. +On other platforms, pass the local Orca data directory and profile registry with the same layout. +The importer skips identities already present in the native main login, account +pool, or credential store, including duplicates within the source. This deliberately +uses the ChatGPT account ID as a conservative bucket: separate members sharing a +workspace account ID are skipped too, not merged or individually imported. Repeating an +import preserves existing accounts. If an earlier import stopped between saving +credentials and registering the pool row, retry can finish that registration with +the same ID only when its untouched pending record still matches the source. Invalid +entries are counted, and the command exits nonzero when any are encountered. +Output contains counts, not emails, account identifiers, paths, or tokens. + +**Orca retains refresh ownership.** OpenCodex stores a read-only source link and +an access-token snapshot, never Orca's refresh token. When resolving credentials, +it reads the source again and verifies that it still belongs to the imported +identity. Orca must keep that login available and refreshed. Missing, malformed, +expired, or identity-mismatched source credentials fail closed; OpenCodex does not +fall back to the old snapshot or refresh the source login. This is a local link, +not a portable export or a transfer of refresh ownership. +Requests already sent upstream keep the credentials they captured. + +New accounts are registered as **validation pending**. After starting the proxy, +open **Codex Auth** and click **Refresh quotas** to authorize the existing model +validation step, which may consume a small amount of quota. Offline import and +JWT parsing do not establish upstream authentication. CLI quota refresh alone +does not perform that validation. The importer never starts or stops either app, +changes Orca files, or changes the active pool account. + ### `ocx account list [provider] [--json] [--all] [--quota [--refresh]]` Without a provider, lists the Codex pool, OAuth accounts, and configured API-key pools. Empty diff --git a/scripts/test-layout/layout.json b/scripts/test-layout/layout.json index d6f5853c55..ed4af8fe29 100644 --- a/scripts/test-layout/layout.json +++ b/scripts/test-layout/layout.json @@ -11,7 +11,7 @@ "domains": { "providers": { "match": [ - "^(?:aside(?!-profile)|auto|azure|baseten|chutes|cline|command|commandcode|context|cyber|deepinfra|deepseek|digitalocean|exa|featherless|forward|hyperbolic|kimi|meta|mimo|minimax|moonshot|muse|new|nous|novita|nscale|nvidia|opencode|openrouter|qwen38|sambanova|umans|vercel|zcode|zhipu)-" + "^(?:aside(?!-profile)|auto|azure|baseten|chutes|cline(?!-(?:client|writer)\\.test\\.ts$)|command|commandcode|context|cyber|deepinfra|deepseek|digitalocean|exa|featherless|forward|hyperbolic|kimi|meta|mimo|minimax|moonshot|muse|new|nous|novita|nscale|nvidia|opencode|openrouter|qwen38|sambanova|umans|vercel|zcode|zhipu)-" ], "children": { "cursor": [ @@ -109,6 +109,7 @@ "clients": { "match": [ "^aside-profile(?!s-routes)", + "^cline-(?:client|writer)\\.test\\.ts$", "^(?:desktop|omp|pi|prime|remote|sync)-" ] }, @@ -327,6 +328,7 @@ "claude-system-env-auto.test.ts": "claude-integration", "cleanup-orphaned-workflows.test.ts": "ci-workflows", "clearable-deadline.test.ts": "lib", + "cli-account-orca-import.test.ts": "cli", "cli-account-pool-verbs.test.ts": "cli", "cli-account.test.ts": "cli", "cli-capabilities.test.ts": "cli", @@ -971,6 +973,7 @@ "opencode-zen-rate-limit.test.ts": "providers", "openrouter-provider-routing.test.ts": "providers", "optional-shutdown-hooks.test.ts": "lib", + "orca-import.test.ts": "codex-integration", "orcarouter-provider.test.ts": "providers", "outbound-body-guard.test.ts": "server", "owned-service-home.test.ts": "server", @@ -1389,4 +1392,4 @@ "web-search", "windows" ] -} \ No newline at end of file +} diff --git a/skills/ocx/references/01_management_surface.md b/skills/ocx/references/01_management_surface.md index f9fec70034..e7532b68e7 100644 --- a/skills/ocx/references/01_management_surface.md +++ b/skills/ocx/references/01_management_surface.md @@ -455,6 +455,25 @@ JSON mode: `payload`. - `store` verifies every keychain write by read-back before config.json is rewritten with keychain: references; an unavailable keychain refuses with 503 and leaves the file untouched. - Headless services usually have no unlocked keychain session; prefer ${ENV_VAR} references there. +### `ocx account import-orca` + +Preview or register read-only links to Orca-managed Codex accounts without another login. + +Drives no management route. + +| Flag | Value | Meaning | +|---|---|---| +| `--source` | string | Orca data directory containing codex-accounts. | +| `--registry` | string | The chosen Orca profile's orca-data.json account registry. | +| `--apply` | boolean | Register new accounts; requires a stopped proxy. Default is preview. | +| `--json` | boolean | Emit counts without credentials or source paths. | + +JSON mode: `envelope`. + +- Local files only; never copies refresh tokens or changes Orca authentication files. +- Skips existing ChatGPT identities. New accounts remain pending until dashboard validation. +- Orca must keep the source login available and refreshed; a missing or expired source fails closed. + ### `ocx account refresh` Refresh account quotas without model validation; pending Codex accounts require dashboard consent. @@ -769,6 +788,6 @@ JSON mode: `payload`. ## Counts -- declared capabilities: 41 -- of those, state-changing: 20 +- declared capabilities: 42 +- of those, state-changing: 21 - head-resolved invocations: 2 diff --git a/src/cli/account-orca-import.ts b/src/cli/account-orca-import.ts new file mode 100644 index 0000000000..b69e1cb92b --- /dev/null +++ b/src/cli/account-orca-import.ts @@ -0,0 +1,58 @@ +import type { importOrcaAccounts } from "../codex/orca-import"; + +const USAGE = "Usage: ocx account import-orca --source --registry [--apply] [--json]"; + +export interface OrcaImportCommandDeps { + importAccounts?: typeof importOrcaAccounts; +} + +/** Local-only: never sends source paths or credentials to a management listener. */ +export async function cmdOrcaImport(args: string[], deps: OrcaImportCommandDeps = {}): Promise { + let sourceDir: string | undefined; + let registryPath: string | undefined; + let apply = false; + const wantsJson = args.includes("--json"); + const seen = new Set(); + for (let index = 0; index < args.length; index++) { + const arg = args[index]!; + if (seen.has(arg) || !["--source", "--registry", "--apply", "--json"].includes(arg)) { + console.error(USAGE); + return 1; + } + seen.add(arg); + if (arg === "--source" || arg === "--registry") { + const value = args[++index]; + if (!value || value.startsWith("--")) { + console.error(USAGE); + return 1; + } + if (arg === "--source") sourceDir = value; + else registryPath = value; + } else if (arg === "--apply") apply = true; + } + if (!sourceDir || !registryPath) { + console.error(USAGE); + return 1; + } + try { + const run = deps.importAccounts ?? (await import("../codex/orca-import")).importOrcaAccounts; + const result = await run({ sourceDir, registryPath, apply }); + if (wantsJson) console.log(JSON.stringify({ + mode: result.mode, discovered: result.discovered, eligible: result.eligible, + imported: result.imported, duplicates: result.duplicates, invalid: result.invalid, + })); + else { + console.log(`Orca: ${result.discovered} discovered, ${result.eligible} eligible, ${result.imported} imported, ${result.duplicates} duplicates, ${result.invalid} invalid.`); + console.log(apply + ? "Start the proxy and use Refresh quotas in Codex Auth to validate new accounts. Orca retains refresh ownership." + : "Preview only. Stop the proxy before repeating with --apply. Orca authentication files stay read-only."); + } + return result.invalid > 0 ? 1 : 0; + } catch { + // Filesystem/JSON exceptions may contain source paths or token fragments. + const error = "Orca import failed. Check the source, destination config, and stopped proxy; no successful completion is confirmed."; + if (wantsJson) console.log(JSON.stringify({ error: "orca_import_failed" })); + else console.error(error); + return 1; + } +} diff --git a/src/cli/account.ts b/src/cli/account.ts index 4a8c6a0427..bcada7d376 100644 --- a/src/cli/account.ts +++ b/src/cli/account.ts @@ -56,6 +56,7 @@ const ACCOUNT_USAGE = `Usage: ocx account clear-cooldown [--json] ocx account add-key [--label