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
16 changes: 16 additions & 0 deletions docs-site/src/content/docs/ko/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,19 @@ Windows x64 설치 관측은 [`attest` 명령](/ko/reference/cli/agents/)을 참
`ocx --version`, `ocx -v`, `ocx version`은 스크립트가 읽기 좋은 한 줄짜리 버전을 출력하고 종료합니다.

일반 도움말에는 두 개의 디스패치 대상이 의도적으로 빠져 있습니다. `__refresh-version [preview]`는 분리된 프로세스에서 업데이트 알림 캐시를 새로 고치고, `__gui-update-worker <job-id> [latest|preview] [restart]`는 대시보드 업데이트 작업을 실행합니다. 이들은 구현 세부 사항일 뿐이며 안정적인 사용자 명령이 아닙니다. 대시보드는 worker PID를 기록하고, worker가 죽은 활성 작업은 복구하며, PID가 없는 오래된 활성 기록은 10분 뒤 오래된 것으로 취급하고, 살아 있는 worker를 동시 업데이트로부터 보호합니다.

## Capability argument validation

`ocx capabilities`는 알 수 없는 인수, 반복된 플래그, 빈 `--route` 값을 종료 코드 64로 거부합니다. 유효한 경로에 등록된 기능이 없으면 종료 코드 4를 반환합니다.

## Integer option values

`--limit` 같은 정수 옵션은 안전하게 표현 가능한 십진 정수를 받습니다. `1_000`, `1,000` 같은 숫자 구분자는 허용합니다. 빈 값, 16진수, 지수 표기 및 소수는 요청을 보내기 전에 거부합니다.

## Windows JSON configuration files

`ocx config validate <file>`과 `ocx config import <file> --yes`는 선행 BOM이 있는 UTF-8 JSON도 읽습니다. 표준 입력(`-`)에도 적용되므로 Windows PowerShell이나 편집기의 UTF-8 내보내기를 사용할 수 있습니다. UTF-16은 지원하지 않습니다.

## Default alias listing

`ocx alias --json`은 `ocx alias list --json`과 같습니다. `--json`은 명시한 alias 작업의 앞이나 뒤에 둘 수 있습니다.
12 changes: 12 additions & 0 deletions docs-site/src/content/docs/ko/reference/cli/agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -263,3 +263,15 @@ ocx system codex-cli-update attest --candidate <absolute-path> --npm-prefix <abs
### 연결된 클라이언트의 사용량

`ocx usage`는 등록된 데이터 키로 허브에서 이 클라이언트의 사용량만 읽습니다. 출력에는 허브 출처와 키 범위가 표시됩니다. 기간·모델·공급자 필터와 `--since`/`--until`, `--json`을 그대로 사용할 수 있습니다. 계정별 내역과 다른 클라이언트 기록은 반환하지 않습니다. 허브가 응답하지 않거나 이 기능을 지원하지 않으면 오류를 알립니다. 로컬 기록으로 대신 표시하지 않습니다. 구형 허브라면 허브를 업데이트하세요.

## Explain a listed request

`ocx logs`의 일반 출력에 `id=<request-id>`가 표시됩니다. 이 값을 `ocx logs explain <request-id>`에 넣어 라우팅 결정을 확인할 수 있습니다. ID가 없거나 ID에 제어 문자가 있으면 조회 키를 바꾸어 표시하지 않고 이 항목을 생략합니다. JSON과 JSONL은 원래 ID와 형식을 유지합니다.

## Routing profile lookup status

`ocx route policy show <id>`는 프로필이 없으면 종료 코드 4를 반환합니다. 명령 인수가 빠졌거나 잘못되면 2를 반환하므로 스크립트에서 두 경우를 구별할 수 있습니다.

## Upstream error details

업스트림 오류 응답에 여러 메시지 필드가 있으면 기존 우선순위에서 처음 발견한 비어 있지 않은 문자열을 사용합니다. 빈 값이나 잘못된 형식의 필드 때문에 유효한 후순위 진단이 사라지지 않습니다.
4 changes: 4 additions & 0 deletions docs-site/src/content/docs/ko/reference/cli/lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -457,3 +457,7 @@ npm에 게시하면 사용할 수 있게 됩니다.
## Remote Hub 클라이언트 라이프사이클

`ocx connect <url> --pairing-code-stdin`, `ocx connect status`, `ocx sync`, `ocx connect rotate --pairing-code-stdin`을 사용합니다. `ocx disconnect`는 오프라인에서도 로컬 상태를 복원하지만 허브 키는 폐기하지 않습니다. 연결 중에는 `ocx connect revoke --admin-token-stdin`으로 저장된 `apiKeyId`를 폐기할 수 있고, 연결을 끊은 뒤에는 허브의 **Integrations → API Keys**를 사용해야 합니다. 비밀값은 stdin으로만 전달하고 argv에 넣지 마세요.

## Setup port validation

`ocx init`의 포트는 1–65535 범위의 십진 정수입니다. Enter만 누르면 10100을 사용합니다. `0`, `10100oops`, `1.5` 같은 잘못된 값은 임의로 바꾸거나 자르지 않고, 오류를 알린 뒤 포트를 다시 묻습니다.
16 changes: 16 additions & 0 deletions docs-site/src/content/docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,3 +156,19 @@ refreshes the update-notification cache in a detached process, and
implementation details, not stable user-facing commands. The dashboard records the worker PID,
recovers an active job whose worker died, treats older PID-less active records as stale after ten
minutes, and protects a live worker from concurrent updates.

## Capability argument validation

`ocx capabilities` rejects unknown arguments, repeated flags and blank `--route` values with exit 64. A valid route with no declared capability exits 4.

## Integer option values

Integer options such as `--limit` require decimal whole numbers within JavaScript safe-integer bounds. Digit separators such as `1_000` and `1,000` are accepted. Empty values, hexadecimal, exponent notation and fractions are rejected before a request is sent.

## Windows JSON configuration files

`ocx config validate <file>` and `ocx config import <file> --yes` accept UTF-8 JSON with or without a leading BOM, including stdin (`-`). This supports UTF-8 exports from Windows PowerShell and editors. UTF-16 input is not accepted.

## Default alias listing

`ocx alias --json` is equivalent to `ocx alias list --json`. The output flag can precede or follow an explicit alias action.
12 changes: 12 additions & 0 deletions docs-site/src/content/docs/reference/cli/agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -485,3 +485,15 @@ backup can restore the configuration; store exported files as secrets.
`ocx usage` reads the connected hub with this client's enrolled data key. Human output identifies the hub source and client-key scope; `--json` returns the same scoped data. Range, surface, provider/model filters and custom `--since`/`--until` bounds remain available. Account breakdowns and other clients' records are not shared. An old or unavailable hub produces an explicit error instead of substituting local usage; upgrade the hub if it does not support this read.

The read-only data-plane endpoint is `GET /v1/usage`, using `x-opencodex-api-key` with a configured client key. Environment-wide and admin keys are refused. It accepts `range`, `surface`, `provider`, `model`, `since`, and `until`; unknown/repeated options and caller-selected key IDs are rejected. Oversized skipped rows retain the explicit incomplete-history warning.

## Explain a listed request

Human `ocx logs` output includes `id=<request-id>`. Pass that value to `ocx logs explain <request-id>` to inspect routing decisions. Rows without an ID or with control characters in their ID omit the field instead of displaying a different lookup key. JSON and JSONL output retain their existing schema.

## Routing profile lookup status

`ocx route policy show <id>` exits 4 when the profile does not exist. Missing or invalid command arguments exit 2. Scripts can distinguish a missing profile from incorrect usage.

## Upstream error details

When an upstream error envelope contains several message fields, OpenCodex uses the first nonblank string in its established priority order. Empty or malformed fields no longer hide a valid fallback diagnostic.
4 changes: 4 additions & 0 deletions docs-site/src/content/docs/reference/cli/lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -854,3 +854,7 @@ publishes them to npm.
## Remote Hub client lifecycle

Use `ocx connect <url> --pairing-code-stdin`, `ocx connect status`, `ocx sync`, and `ocx connect rotate --pairing-code-stdin`. The initial catalog download fails after five seconds without incoming bytes, but active transfers may run longer; use `--catalog-timeout <seconds>` (1–120) to override that inactivity window. `ocx disconnect` restores local state offline and does not revoke the hub key. While connected only, `ocx connect revoke --admin-token-stdin` revokes the persisted `apiKeyId`; after disconnect use the hub's **Integrations → API Keys** page. Secrets are stdin-only and never belong in argv.

## Setup port validation

`ocx init` accepts TCP ports 1–65535 as decimal whole numbers. Press Enter to use 10100. Invalid input such as `0`, `10100oops` or `1.5` is never silently replaced or truncated; setup reports it and asks for the port again.
8 changes: 8 additions & 0 deletions scripts/test-layout/layout.json
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@
"anthropic-account-pause.test.ts": "adapters/anthropic",
"anthropic-account-threshold.test.ts": "adapters/anthropic",
"anthropic-combo-account-cooldown.test.ts": "adapters/anthropic",
"cli-alias-json.test.ts": "cli",
"cli-anthropic-account-threshold.test.ts": "cli",
"anthropic-model-routes.test.ts": "adapters/anthropic",
"anthropic-agentrouter-language-framing.test.ts": "adapters/anthropic",
Expand Down Expand Up @@ -391,6 +392,7 @@
"cli-account-pool-verbs.test.ts": "cli",
"cli-account-threshold.test.ts": "cli",
"cli-account.test.ts": "cli",
"cli-capabilities-arguments.test.ts": "cli",
"cli-capabilities.test.ts": "cli",
"cli-api-protocols.test.ts": "cli",
"cli-catalog-prewarm.test.ts": "cli",
Expand All @@ -399,6 +401,7 @@
"cli-codex-log-guard-protection.test.ts": "cli",
"cli-codex-log-guard.test.ts": "cli",
"cli-companion.test.ts": "cli",
"cli-config-bom.test.ts": "cli",
"cli-config-command.test.ts": "cli",
"cli-config-show-client.test.ts": "cli",
"cli-connect-readiness.test.ts": "cli",
Expand All @@ -411,7 +414,10 @@
"cli-head.test.ts": "cli",
"cli-headless-parity.test.ts": "cli",
"cli-help.test.ts": "cli",
"cli-integer-options.test.ts": "cli",
"cli-init-port.test.ts": "cli",
"cli-json-contract.test.ts": "cli",
"cli-logs-request-id.test.ts": "cli",
"cli-management-auth.test.ts": "cli",
"cli-models-free-only.test.ts": "cli",
"cli-models-price.test.ts": "cli",
Expand All @@ -428,6 +434,7 @@
"cli-resolve.test.ts": "cli",
"cli-restart-health.test.ts": "cli",
"cli-restore-back.test.ts": "cli",
"cli-route-policy-not-found.test.ts": "cli",
"cli-start-auxiliary-bind.test.ts": "cli",
"cli-start-journal-order.test.ts": "cli",
"sibling-home-client-sync.test.ts": "cli",
Expand Down Expand Up @@ -1769,6 +1776,7 @@
"update-tray-handoff.test.ts": "update",
"update-tree-ownership.test.ts": "update",
"upstream-connect-error.test.ts": "server",
"upstream-error-message-fallback.test.ts": "lib",
"upstream-http-error.test.ts": "adapters",
"upstream-http-version.test.ts": "server",
"upstream-reachability.test.ts": "codex-integration",
Expand Down
3 changes: 2 additions & 1 deletion src/cli/alias.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,11 @@ function selector(value: string): { provider: string; model?: string } {
return slash < 0 ? { provider: value } : { provider: value.slice(0, slash), model: value.slice(slash + 1) };
}

/** Manage model aliases, consuming JSON output selection before the default list action. */
export async function handleAliasCommand(argv: string[], deps: RuntimeApiDeps = {}): Promise<number> {
const args = [...argv];
const action = (args.shift() ?? "list").toLowerCase();
const wantsJson = takeFlag(args, "--json");
const action = (args.shift() ?? "list").toLowerCase();
if (action === "list") {
rejectArgs(args, USAGE);
const result = await runtimeRequest<Record<string, unknown>>("/api/aliases", {}, deps);
Expand Down
5 changes: 3 additions & 2 deletions src/cli/capabilities-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,15 @@ function renderHuman(caps: readonly Capability[], includeHead: boolean): void {
}
}

/** Render declared capabilities; reject invalid arguments with 64 and report unmatched routes with 4. */
export async function runCapabilities(argv: string[]): Promise<number> {
const args = [...argv];
const json = takeFlag(args, "--json");
const mutatingOnly = takeFlag(args, "--mutating-only");
const route = takeValueFlag(args, "--route");

if (route !== undefined && route.length === 0) {
console.error("Usage: ocx capabilities --route <path>");
if (args.length > 0 || (route !== undefined && route.trim().length === 0)) {
console.error("Usage: ocx capabilities [--json] [--mutating-only] [--route <path>]");
return 64;
}

Expand Down
4 changes: 3 additions & 1 deletion src/cli/config-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,14 +159,16 @@ function setPath(root: Record<string, unknown>, path: string, value: unknown, re
else current[leaf] = value;
}

/** Interpret config-set input as JSON when valid, otherwise preserve it as a string. */
function parseValue(raw: string): unknown {
try { return JSON.parse(raw); }
catch { return raw; }
}

/** Accept one UTF-8 BOM from Windows JSON files or piped input. */
function loadInput(path: string): unknown {
const raw = path === "-" ? readFileSync(0, "utf8") : readFileSync(path, "utf8");
try { return JSON.parse(raw); }
try { return JSON.parse(raw.replace(/^\uFEFF/, "")); }
catch { throw new CliUsageError(`invalid JSON in ${path}`); }
}

Expand Down
18 changes: 16 additions & 2 deletions src/cli/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ import { deriveInitProviders } from "../providers/derive";
import { pinSponsorsWithinKind } from "../providers/sponsor-order";
import type { OcxConfig, OcxProviderConfig } from "../types";

/** Parse a setup TCP port; only an empty answer selects the default. */
export function parseInitPort(raw: string): number | null {
const text = raw.trim();
if (!text) return 10100;
const value = /^\d+$/.test(text) ? Number(text) : NaN;
return Number.isInteger(value) && value >= 1 && value <= 65535 ? value : null;
}

class InitCancelledError extends Error {
constructor(readonly exitCode: 1 | 130) {
super(exitCode === 130 ? "Setup cancelled." : "stdin reached EOF while waiting for input. Re-run `ocx init` in an interactive terminal.");
Expand Down Expand Up @@ -111,6 +119,7 @@ export function cleanupOpenAiTierBackupAfterInit(configPath = getConfigPath()):
} catch { /* cleanup is best-effort; never block init on backup housekeeping */ }
}

/** Create a first configuration interactively, preserving existing files and refusing invalid input. */
export async function runInit(): Promise<void> {
const initial = observeInitialConfigState();
if (initial === "exists") {
Expand Down Expand Up @@ -194,8 +203,13 @@ export async function runInit(): Promise<void> {
};
}

const portStr = await prompt.ask("\nProxy port [10100]: ");
const port = parseInt(portStr, 10) || 10100;
// A mistyped port re-asks instead of discarding every earlier answer; EOF and SIGINT
// still reject the pending question with InitCancelledError.
let port: number | null;
do {
port = parseInitPort(await prompt.ask("\nProxy port [10100]: "));
if (port === null) console.error("Proxy port must be a whole decimal number from 1 to 65535. Please try again.");
} while (port === null);

initializeProviderModelSelection(providerName, providerConfig);
const config: OcxConfig = {
Expand Down
7 changes: 6 additions & 1 deletion src/cli/observe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ function logRows(data: unknown): LogEntry[] {
return [];
}

/** Render one human log row with an exact, control-free request ID suitable for history lookup. */
function formatLog(row: LogEntry): string {
const time = String(row.timestamp ?? row.createdAt ?? "");
const route = [row.provider, row.model].filter(Boolean).join("/");
Expand All @@ -69,9 +70,13 @@ function formatLog(row: LogEntry): string {
const account = typeof row.accountLogLabel === "string" && row.accountLogLabel.length > 0
? `acct=${row.accountLogLabel}`
: "";
return [time, String(status), route, duration, account, conversation].filter(Boolean).join(" ");
const requestId = typeof row.requestId === "string" && row.requestId.length > 0
&& !/[\x00-\x1f\x7f-\x9f\u2028\u2029]/.test(row.requestId)
? `id=${row.requestId}` : "";
return [time, String(status), route, duration, account, conversation, requestId].filter(Boolean).join(" ");
}

/** Read or follow request logs while preserving raw JSON and JSONL output. */
async function logs(argv: string[], deps: RuntimeApiDeps): Promise<void> {
const args = [...argv];
const wantsJson = takeFlag(args, "--json");
Expand Down
4 changes: 3 additions & 1 deletion src/cli/route-policy.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
CliUsageError,
RuntimeApiError,
printData,
rejectArgs,
runCliAction,
Expand Down Expand Up @@ -38,6 +39,7 @@ async function list(argv: string[], deps: RuntimeApiDeps): Promise<void> {
);
}

/** Show an existing routing profile, distinguishing malformed usage from a missing record. */
async function show(argv: string[], deps: RuntimeApiDeps): Promise<void> {
const args = [...argv];
const id = args.shift();
Expand All @@ -46,7 +48,7 @@ async function show(argv: string[], deps: RuntimeApiDeps): Promise<void> {
rejectArgs(args, USAGE);
const result = await runtimeRequest<{ profiles?: ProfileRow[] }>("/api/routing-profiles", {}, deps);
const profile = (result.profiles ?? []).find(candidate => candidate.id === id);
if (!profile) throw new CliUsageError(`unknown routing profile: ${id}`, USAGE);
if (!profile) throw new RuntimeApiError(`unknown routing profile: ${id}`, 404, null);
printData(profile, wantsJson);
}

Expand Down
7 changes: 5 additions & 2 deletions src/cli/runtime-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ export function takeOption(args: string[], flag: string): string | undefined {
return value;
}

/** Consume one boolean option using the supported on/off synonyms; reject other values. */
export function takeBooleanOption(args: string[], flag: string): boolean | undefined {
const raw = takeOption(args, flag);
if (raw === undefined) return undefined;
Expand All @@ -208,11 +209,13 @@ export function takeBooleanOption(args: string[], flag: string): boolean | undef
throw new CliUsageError(`${flag} must be on or off`);
}

/** Parse a decimal safe integer, allowing separators only between digits. */
export function takeIntegerOption(args: string[], flag: string, options: { min?: number } = {}): number | undefined {
const raw = takeOption(args, flag);
if (raw === undefined) return undefined;
const value = Number(raw.replace(/[_,]/g, ""));
if (!Number.isInteger(value) || value < (options.min ?? Number.MIN_SAFE_INTEGER)) {
const decimal = raw.trim();
const value = /^[+-]?\d+(?:[_,]\d+)*$/.test(decimal) ? Number(decimal.replace(/[_,]/g, "")) : NaN;
if (!Number.isSafeInteger(value) || value < (options.min ?? Number.MIN_SAFE_INTEGER)) {
throw new CliUsageError(`${flag} must be an integer${options.min !== undefined ? ` >= ${options.min}` : ""}`);
}
return value;
Expand Down
14 changes: 7 additions & 7 deletions src/lib/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export const ENCRYPTED_FUNCTION_OUTPUT_REJECTION =
*/
export const SEND_BUDGET_EXHAUSTED_CODE = "request_send_budget_exhausted";

/** Canonical human-readable message paths used by Responses upstream failures. */
/** First nonblank string across the canonical upstream error paths, in priority order. */
export function upstreamErrorMessageFromPayload(payload: unknown): string | undefined {
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return undefined;
const json = payload as {
Expand All @@ -31,14 +31,14 @@ export function upstreamErrorMessageFromPayload(payload: unknown): string | unde
incomplete_details?: { message?: unknown };
};
};
const message = json.error?.message
?? json.last_error?.message
?? json.response?.error?.message
?? json.response?.incomplete_details?.message
const messages = [json.error?.message,
json.last_error?.message,
json.response?.error?.message,
json.response?.incomplete_details?.message,
// The Responses stream error event carries a flat message (type/code/message),
// unlike the response.failed envelope the branches above already cover.
?? (json.type === "error" ? json.message : undefined);
return typeof message === "string" ? message : undefined;
json.type === "error" ? json.message : undefined];
return messages.find((message): message is string => typeof message === "string" && message.trim().length > 0);
}

/** OpenAI / Codex hard block for high-risk cybersecurity activity (HTTP 400 or mid-stream). */
Expand Down
Loading
Loading