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
2 changes: 2 additions & 0 deletions docs-site/src/content/docs/fr/guides/remote-hub.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ ocx connect status
ocx sync
```

Les diagnostics de disponibilité lisibles par un humain affichent les caractères de contrôle des valeurs du catalogue sous forme d’échappements hexadécimaux visibles, aussi bien à la première connexion que lorsque `ocx sync` refuse un catalogue de hub actualisé. Le statut JSON conserve la valeur de diagnostic d’origine.

La clé client est écrite dans le fichier privé `service-api-token`, jamais dans `config.json`. En mode connecté, l’usage provient du hub et est filtré par `apiKeyId`; après déconnexion, il provient du stockage local. Il n’existe aucune réplication entre les deux.

Le jeton admin permet la gestion ordinaire mais ne peut jamais créer une session de consentement. Les actions de consentement exigent une `gui-session`, une Origin correspondante et un jeton CSRF. `Tailscale-User-Login` n’est fiable que sur l’entrée de gestion dédiée; renseignez les identités exactes dans `remoteGui.allowedTailscaleUsers`.
Expand Down
2 changes: 2 additions & 0 deletions docs-site/src/content/docs/guides/remote-hub.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ ocx connect status
ocx sync
```

Human-readable readiness diagnostics show control characters in catalog values as visible hexadecimal escapes, both when you first connect and when `ocx sync` refuses a refreshed hub catalog. Structured JSON status retains the original diagnostic value.

You do not have to assemble that line by hand. `ocx hub invite`, run on the hub, mints the code and
prints the exact command — including both origins — for the machine that is joining. See
[Inviting another machine](#inviting-another-machine).
Expand Down
2 changes: 2 additions & 0 deletions docs-site/src/content/docs/ja/guides/remote-hub.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ ocx connect status
ocx sync
```

準備状況を人が読む出力では、カタログ値に含まれる C0/C1 制御文字、DEL、Unicode の行・段落区切り文字(U+2028、U+2029)を目に見える 16 進エスケープとして表示します。初回の接続だけでなく、`ocx sync` が取得し直したハブのカタログを拒否したときも同じです。JSON 形式の状態には元の診断値をそのまま残します。

発行されたキーは所有者だけが読める `service-api-token` に保存され、`config.json` には入りません。接続中の使用量は hub 側で同じ `apiKeyId` に絞り込まれ、切断後はローカル保存分を表示します。両者はミラーリングされません。

管理トークンは通常の管理だけに使え、同意セッションを作ることは永久にできません。同意操作にはサーバー発行の `gui-session`、一致する Origin、CSRF が必要です。`Tailscale-User-Login` は専用管理リスナーでのみ信頼し、許可する ID を `remoteGui.allowedTailscaleUsers` に正確に設定します。
Expand Down
2 changes: 2 additions & 0 deletions docs-site/src/content/docs/ko/guides/remote-hub.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ ocx connect status
ocx sync
```

준비 상태를 사람이 읽는 출력에서는 카탈로그 값의 C0/C1 제어문자, DEL, 유니코드 줄·문단 구분자(U+2028, U+2029)를 눈에 보이는 16진수 이스케이프로 표시합니다. 처음 연결할 때뿐 아니라 `ocx sync`가 새로 받은 허브 카탈로그를 거부할 때도 같습니다. JSON 상태에는 원래 진단값을 그대로 유지합니다.

이 줄을 직접 만들 필요는 없습니다. 허브에서 `ocx hub invite`를 실행하면 코드를 발급하고, 두 Origin이 모두 채워진 명령을 그대로 출력합니다. [다른 컴퓨터 초대하기](#다른-컴퓨터-초대하기)를 보세요.

허브가 발급한 클라이언트별 키는 권한이 제한된 `service-api-token` 파일에 저장됩니다. `config.json`에는 저장되지 않습니다. 연결 중 사용량은 허브 기록에서 해당 `apiKeyId`만 조회하고, 연결을 끊은 뒤에는 로컬 기록을 봅니다. 두 기록은 서로 복제되지 않습니다.
Expand Down
2 changes: 2 additions & 0 deletions docs-site/src/content/docs/ru/guides/remote-hub.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ ocx connect status
ocx sync
```

В читаемой человеком диагностике готовности управляющие символы C0/C1, DEL и разделители строк и абзацев Unicode (U+2028 и U+2029) из значений каталога показываются как видимые шестнадцатеричные escape-последовательности — и при первом подключении, и когда `ocx sync` отклоняет обновлённый каталог hub. В JSON-статусе исходное значение диагностики сохраняется без изменений.

Ключ клиента записывается в защищённый `service-api-token`, а не в `config.json`. При подключении статистика читается с hub и фильтруется по `apiKeyId`; после отключения используется локальное хранилище. Зеркалирования нет.

Admin token разрешает обычное управление, но никогда не создаёт consent session. Для действий с согласием нужны `gui-session`, совпадающий Origin и CSRF. Заголовок `Tailscale-User-Login` доверен только отдельному management ingress; точные логины задаются в `remoteGui.allowedTailscaleUsers`.
Expand Down
2 changes: 2 additions & 0 deletions docs-site/src/content/docs/tr/guides/remote-hub.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ ocx connect status
ocx sync
```

İnsanın okuyacağı hazırlık tanılarında katalog değerlerindeki denetim karakterleri görünür onaltılık kaçış dizileri olarak yazılır; bu hem ilk bağlanışta hem de `ocx sync` yenilenen hub kataloğunu reddettiğinde geçerlidir. JSON durumu özgün tanı değerini olduğu gibi korur.

İstemci anahtarı yalnızca sahibinin okuyabildiği `service-api-token` dosyasına yazılır, `config.json` içine yazılmaz. Bağlı kullanım hub deposundan aynı `apiKeyId` ile filtrelenir; bağlantı kesilince yerel depo kullanılır. İki depo birbirini yansıtmaz.

Admin token sıradan yönetim yapabilir ancak hiçbir zaman onay oturumu oluşturamaz. Onay işlemleri sunucu tarafından verilen `gui-session`, eşleşen Origin ve CSRF ister. `Tailscale-User-Login` yalnızca ayrı yönetim girişinde güvenilirdir; tam kimlikleri `remoteGui.allowedTailscaleUsers` içinde belirtin.
Expand Down
2 changes: 2 additions & 0 deletions docs-site/src/content/docs/zh-cn/guides/remote-hub.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ ocx connect status
ocx sync
```

面向人阅读的就绪诊断会把目录值中的 C0/C1 控制字符、DEL 以及 Unicode 行分隔符和段落分隔符(U+2028、U+2029)显示为可见的十六进制转义,首次连接时如此,`ocx sync` 拒绝重新获取的 hub 目录时也一样。JSON 状态仍保留原始的诊断值。

客户端密钥写入仅所有者可读的 `service-api-token`,绝不会写入 `config.json`。连接期间,使用记录来自 hub 并按稳定的 `apiKeyId` 过滤;断开后显示本地记录。两者不会镜像。

Admin token 只能执行普通管理,永远不能创建用户同意会话。用户同意操作必须使用服务器签发的 `gui-session`、匹配的 Origin 和 CSRF。`Tailscale-User-Login` 只在独立管理入口可信;请在 `remoteGui.allowedTailscaleUsers` 中填写准确登录名。
Expand Down
2 changes: 2 additions & 0 deletions docs-site/src/content/docs/zh-tw/guides/remote-hub.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ ocx connect status
ocx sync
```

供人閱讀的就緒診斷會把目錄值中的控制字元顯示為可見的十六進位逸出序列,首次連線時如此,`ocx sync` 拒絕重新取得的 hub 目錄時也一樣。JSON 狀態仍保留原始的診斷值。

用戶端金鑰會寫入只有擁有者可讀的 `service-api-token`,絕不寫入 `config.json`。連線期間,用量來自 hub 並依穩定的 `apiKeyId` 篩選;中斷後則顯示本機記錄。兩者不會互相鏡像。

Admin token 只能執行一般管理,永遠不能建立使用者同意工作階段。同意操作必須使用伺服器簽發的 `gui-session`、相符的 Origin 與 CSRF。`Tailscale-User-Login` 只在獨立管理入口可信;請在 `remoteGui.allowedTailscaleUsers` 填入完整且正確的登入名稱。
Expand Down
15 changes: 11 additions & 4 deletions src/cli/connect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ import {
takeFlag,
takeIntegerOption,
takeOption,
terminalSafeError,
terminalSafeText,
type RuntimeApiDeps,
} from "./runtime-api";

Expand Down Expand Up @@ -212,7 +214,7 @@ function readinessLine(status: ClientConnectionStatus): string {
: status.readiness === "incompatible"
? "not ready"
: "unverified";
return `Local Codex CLI: ${label}${status.readinessReason ? ` (${status.readinessReason})` : ""}`;
return `Local Codex CLI: ${label}${status.readinessReason ? ` (${terminalSafeText(status.readinessReason)})` : ""}`;
}

export type ConnectCompletionReport = {
Expand Down Expand Up @@ -249,9 +251,10 @@ export function connectCompletionReport(
if (readiness.kind === "unverified") {
// Not a failure. A client with no observable Codex CLI is a working configuration, and the
// write-time gate deliberately lets it through; saying so is the honest middle report.
return { lines: [connected, `Local Codex CLI: unverified (${readiness.reason}).`], failure: null };
return { lines: [connected, `Local Codex CLI: unverified (${terminalSafeText(readiness.reason)}).`], failure: null };
}
const verdict = `Local Codex CLI: not ready (${readiness.reason})`;
const safeReason = terminalSafeText(readiness.reason);
const verdict = `Local Codex CLI: not ready (${safeReason})`;
if (!selectedClients.includes("codex")) {
return {
lines: [connected, `${verdict} This connection selected ${selectedClients.join(", ")}, so nothing here launches Codex.`],
Expand All @@ -260,7 +263,7 @@ export function connectCompletionReport(
}
return {
lines: [verdict, `The connection to ${connection.serverUrl} as key ${connection.apiKeyId} was saved; run 'ocx connect status' to see it.`],
failure: `client_not_ready: ${readiness.reason}`,
failure: `client_not_ready: ${safeReason}`,
};
}

Expand Down Expand Up @@ -344,6 +347,10 @@ async function runConnect(argv: string[], deps: ClientCommandDeps): Promise<void
// production would let the gate fall back to its own probing, persisting default, so one
// command could run two probes and act on two different ladders.
catalogCompatibility: catalogObserver(deps.catalogProbeDeps),
}).catch((error: unknown) => {
// Compatibility refusals happen before the completion report and reach stderr.
// Keep the domain error untouched; render its message only at the CLI boundary.
throw terminalSafeError(error);
});
// The hub and the credential are proven at this point; the local runtime is not. Reporting
// only the first half is what #4207 was filed for, so the catalog now on disk is checked
Expand Down
8 changes: 6 additions & 2 deletions src/cli/dispatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import { restoreNativeCodexAsync } from "../codex/inject";
import { stripGrokConfig } from "../grok/inject";
import { handleRestartScopeAfterWrite, readRestartScope, type RestartScope } from "./restart-scope";
import { normalizeUpdateChannel, runGuiUpdateWorker } from "../update/job";
import { isJsonOption, takeFlag } from "./runtime-api";
import { isJsonOption, takeFlag, terminalSafeError } from "./runtime-api";
import type { ClientConnectionState } from "../client/state";
import { OCX_NATIVE_REPLAY_RECOVERY_NOTE } from "../responses/compaction";

Expand Down Expand Up @@ -405,7 +405,11 @@ const commandRunners: Record<string, CommandRunner> = {
// types it as `number | string`; only a numeric code means anything here.
return typeof process.exitCode === "number" ? process.exitCode : 0;
} catch (error) {
console.error(`Connected sync failed without local fallback: ${error instanceof Error ? error.message : String(error)}`);
// The refresh path reaches the same hub catalog `ocx connect` validates, so a rejected
// reasoning level arrives here as hub-supplied text. Rendering it through the shared
// terminal boundary is what keeps the routine refresh from forging output; the domain
// error itself is left alone for callers that inspect it.
console.error(`Connected sync failed without local fallback: ${terminalSafeError(error).message}`);
return 1;
}
}
Expand Down
25 changes: 25 additions & 0 deletions src/cli/runtime-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,31 @@ export function printData(value: unknown, wantsJson: boolean, lines?: string[]):
else for (const line of lines) console.log(line);
}

/**
* Render untrusted diagnostic text without letting it control the operator's terminal. Catalog
* values are hub-supplied and surface on more than one CLI path -- first-time `ocx connect` and the
* connected `ocx sync` refresh both print them -- so the escaping sits beside `printData`, at the
* one boundary that already separates human output from structured output. Structured output keeps
* the exact value: escaping is a rendering decision for a tty, not a change to the data.
*/
export function terminalSafeText(value: string): string {
return value.replace(/[\x00-\x1f\x7f-\x9f\u2028\u2029]/g, character => {
const code = character.charCodeAt(0);
return code <= 0x7f
? `\\x${code.toString(16).padStart(2, "0")}`
: `\\u${code.toString(16).padStart(4, "0")}`;
});
}

/**
* The same rendering for a failure about to be printed or rethrown. The original is kept as
* `cause` rather than discarded, so a caller that inspects the domain error still reads the exact
* message and fields it threw.
*/
export function terminalSafeError(error: unknown): Error {
return new Error(terminalSafeText(error instanceof Error ? error.message : String(error)), { cause: error });
}

/** Compact human view for safe management DTOs; JSON remains available for complete fidelity. */
export function summaryLines(value: unknown, prefix = "", depth = 0): string[] {
if (!value || typeof value !== "object" || depth > 1) return [`${prefix || "value"}: ${String(value)}`];
Expand Down
2 changes: 2 additions & 0 deletions structure/clients/claude-desktop.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ That projection does not migrate existing user-selected Desktop configuration or

Shared parsing and streaming follow the [request-copy](../transports/byte-accounting.md#request-copy-accounting) and [stream-buffer accounting](../transports/byte-accounting.md#stream-buffer-accounting) contracts.

Claude-only connections keep their existing non-failing readiness policy; displayed catalog reasons follow the [terminal rendering contract](../runtime.md#cli-readiness-diagnostics) whether they surface at connect time or on a later refresh.

## Connected Claude Desktop profiles

Connected `ocx claude desktop apply` reads the hub's Desktop snapshot and writes the hub origin
Expand Down
2 changes: 2 additions & 0 deletions structure/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
The configuration-only [plaintext V2 contract](subagents.md#plaintext-v2-agent-messages)
is scoped to canonical ChatGPT Responses forwarding; other source-area behavior described here is unchanged.

Connected-client catalog diagnostics use the [terminal rendering contract](runtime.md#cli-readiness-diagnostics) on the first connection and on every `ocx sync` refresh; stored catalog values are unchanged.

## Config surface

### OpenCodex home and live process state
Expand Down
2 changes: 2 additions & 0 deletions structure/ops/docs-and-release.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ is scoped to canonical ChatGPT Responses forwarding; other source-area behavior

Shared parsing and streaming follow the [request-copy](../transports/byte-accounting.md#request-copy-accounting) and [stream-buffer accounting](../transports/byte-accounting.md#stream-buffer-accounting) contracts.

Human-readable connect and sync-refresh diagnostics follow the [terminal rendering contract](../runtime.md#cli-readiness-diagnostics), with regression coverage for both paths in `tests/cli/cli-connect-readiness.test.ts`.

## Public docs

The public documentation site lives in `docs-site/` and is built with Astro + Starlight. English is
Expand Down
4 changes: 4 additions & 0 deletions structure/runtime.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ it requires no runtime lifecycle change or new configuration option.

Shared parsing and streaming follow the [request-copy](transports/byte-accounting.md#request-copy-accounting) and [stream-buffer accounting](transports/byte-accounting.md#stream-buffer-accounting) contracts.

## CLI readiness diagnostics

Catalog-derived reasoning-level diagnostics are escaped only at the human-output boundary, which `src/cli/runtime-api.ts` owns alongside the human/JSON print split. Every CLI path that prints a hub-supplied catalog value renders it there: the first-time refusal in `src/cli/connect.ts` and the connected `ocx sync` refusal in `src/cli/dispatch.ts`. C0/C1 controls, DEL, and Unicode line/paragraph separators print as visible hexadecimal escapes; structured status retains the exact reason, and a rendered failure keeps the domain error as its `cause`. The ready/unverified/incompatible classification and exit policy are unchanged.

## Entrypoints

| Path | Responsibility |
Expand Down
Loading
Loading