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: 1 addition & 1 deletion docs-site/src/content/docs/fr/reference/cli/lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ Vérifie l’identité du proxy actif. La sortie destinée aux utilisateurs indi

### `ocx ready [--json] [--wait [--timeout <seconds>]]`

Vérifie l’état de préparation après synchronisation au moyen du point de terminaison non authentifié `GET /readyz`. Il renvoie `200` lorsque le service est prêt, ou `503` avec `Retry-After: 1` pour les états `pending` et terminal `failed`. Son identité HTTP expurgée est `{service, version, uptime, pid, port, status}`. Les anciens proxys dépourvus de `/readyz` échouent de manière sûre avec l’état `unreachable` ; `/healthz` mesure la disponibilité du processus, et non son état de préparation.
Vérifie l’état de préparation après synchronisation au moyen du point de terminaison non authentifié `GET /readyz`. Il renvoie `200` lorsque le service est prêt, ou `503` avec `Retry-After: 1` pour les états `pending` et terminal `failed`. Son identité HTTP expurgée est `{service, version, uptime, pid, port, status, protocol, minimumClientProtocol, managementUrl}`. `protocol` est la version courante du protocole distant du hub, `minimumClientProtocol` la plus ancienne version cliente compatible et `managementUrl` l’origine canonique de gestion visible par le navigateur. Les anciens proxys dépourvus de `/readyz` échouent de manière sûre avec l’état `unreachable` ; `/healthz` mesure la disponibilité du processus, et non son état de préparation.

Par défaut, la commande effectue une seule sonde. Avec `--wait`, elle interroge le service jusqu’à ce qu’il soit prêt ou jusqu’à l’expiration du délai, mais s’arrête immédiatement si elle observe l’état terminal `failed`. Le délai par défaut est de 45 secondes. `--timeout <seconds>` exige `--wait` et accepte un entier positif compris entre 1 et 300. La sortie JSON de la CLI est `{ready, status, pid, port}`, où `status` vaut `ready`, `pending`, `failed` ou `unreachable`. Les codes de sortie sont 0 si le service est prêt ; 1 s’il n’est pas prêt, reste en attente, échoue, dépasse le délai ou est inaccessible ; et 64 si les arguments sont invalides.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -255,3 +255,5 @@ compte et la charge de travail prévus.
## Clés Remote Hub et valeurs par défaut

`runtimeRole` vaut `standalone` par défaut. Un hub utilise `hub.managementPublicOrigin`, `hub.managementIngress` limité au loopback (`enabled:false` si absent) et les identités exactes de `remoteGui.allowedTailscaleUsers` (liste vide si absente). La clé client reste dans `service-api-token`, jamais dans `config.json`; `service-api-token.prev` peut exister pendant une rotation. Les usages ne sont pas répliqués.

`remoteGui.allowInsecureHttp` est un ancien no-op déprécié, conservé uniquement pour que les anciens fichiers passent encore le schéma strict. Supprimez-le de la configuration : les grants de pairing ne sont acceptés que sur loopback ou via HTTPS authentifié, et `true` ne réactive pas le pairing HTTP en clair.
2 changes: 1 addition & 1 deletion docs-site/src/content/docs/ja/reference/cli/lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ ocx status --json

認証不要の `GET /readyz` エンドポイントで同期後の準備状態を確認します。準備完了時は `200`、
`pending` または終端状態の `failed` では `Retry-After: 1` とともに `503` を返します。HTTP の
サニタイズ済み識別フィールドは `{service, version, uptime, pid, port, status}` です。`/readyz` がない
サニタイズ済み識別フィールドは `{service, version, uptime, pid, port, status, protocol, minimumClientProtocol, managementUrl}` です。`protocol` は hub の現在の remote protocol、`minimumClientProtocol` は互換性のある最小 client protocol、`managementUrl` は browser から見える canonical management origin です。`/readyz` がない
旧プロキシは `unreachable` として fail-closed し、`/healthz` は readiness ではなく別の liveness 確認です。
デフォルトでは 1 回だけ probe します。`--wait` は準備完了または timeout まで polling しますが、
終端 `failed` を確認すると即座に終了します。デフォルト timeout は 45 秒で、`--timeout <seconds>` には
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,3 +165,5 @@ Anthropic OAuth サイドカーは、opencodex の既存のクロード コー
## Remote Hub のキーと既定値

`runtimeRole` の既定値は `standalone` です。hub は `hub.managementPublicOrigin`、loopback 限定の `hub.managementIngress`(未設定時 `enabled:false`)、正確な `remoteGui.allowedTailscaleUsers`(未設定時は空)を使います。クライアントキーは `config.json` ではなく `service-api-token` に保存され、更新中だけ `service-api-token.prev` が存在する場合があります。使用量はミラーリングされません。

`remoteGui.allowInsecureHttp` は、古い strict-schema 設定を読み込むためだけに残された非推奨の no-op です。設定から削除してください。pairing grant は loopback または認証済み HTTPS でのみ受け付けられ、この値を `true` にしても平文 HTTP pairing は再び有効になりません。
2 changes: 1 addition & 1 deletion docs-site/src/content/docs/ko/reference/cli/lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ ocx status --json

인증이 필요 없는 `GET /readyz` 엔드포인트로 동기화 후 준비 상태를 확인합니다. 준비되면 `200`,
`pending` 또는 종단 상태인 `failed`이면 `Retry-After: 1`과 함께 `503`을 반환합니다. HTTP의 정제된
식별 필드는 `{service, version, uptime, pid, port, status}`입니다. `/readyz`가 없는 이전 프록시는
식별 필드는 `{service, version, uptime, pid, port, status, protocol, minimumClientProtocol, managementUrl}`입니다. `protocol`은 허브의 현재 원격 프로토콜, `minimumClientProtocol`은 호환되는 최소 클라이언트 프로토콜, `managementUrl`은 브라우저에서 보이는 표준 관리 origin입니다. `/readyz`가 없는 이전 프록시는
`unreachable`로 fail-closed하며, `/healthz`는 준비 상태가 아닌 별도의 liveness 확인입니다. 기본값은 한 번의
probe이며, `--wait`는 준비 또는 timeout까지 polling하지만 종단 `failed`를 확인하면 즉시 종료합니다.
기본 timeout은 45초이며, `--timeout <seconds>`는 `--wait`와 함께 써야 하고 양의 정수인 1~300초 범위를 받습니다. CLI JSON은
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,3 +165,5 @@ Anthropic OAuth 사이드카는 opencodex의 기존 Claude Code OAuth fingerprin
## Remote Hub 키와 기본값

`runtimeRole` 기본값은 `standalone`입니다. 허브는 `hub.managementPublicOrigin`, 로컬에만 열리는 `hub.managementIngress`(없으면 `enabled:false`), 정확한 `remoteGui.allowedTailscaleUsers`(없으면 빈 목록)를 사용합니다. 클라이언트 데이터 키는 `config.json`이 아니라 `service-api-token`에 저장되며 교체 중에는 `service-api-token.prev`가 잠시 생길 수 있습니다. 사용량 기록은 서로 복제하지 않습니다.

`remoteGui.allowInsecureHttp`는 이전 strict-schema 설정을 계속 읽기 위해서만 남겨 둔 폐기된 no-op입니다. 설정에서 제거하세요. 페어링 grant는 loopback 또는 인증된 HTTPS에서만 허용되며, 이 값을 `true`로 설정해도 평문 HTTP 페어링은 다시 활성화되지 않습니다.
2 changes: 1 addition & 1 deletion docs-site/src/content/docs/ru/reference/cli/lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ Identity-check живого прокси. Текстовый вывод сооб

Проверяет готовность после синхронизации через не требующий аутентификации `GET /readyz`. При
готовности возвращается `200`; для `pending` и терминального `failed` возвращается `503` с
`Retry-After: 1`. Санитизированные поля HTTP-ответа: `{service, version, uptime, pid, port, status}`.
`Retry-After: 1`. Санитизированные поля HTTP-ответа: `{service, version, uptime, pid, port, status, protocol, minimumClientProtocol, managementUrl}`. `protocol` — текущая версия удалённого протокола hub, `minimumClientProtocol` — минимальная совместимая версия клиента, а `managementUrl` — канонический origin управления для браузера.
Старые прокси без `/readyz` fail-closed как `unreachable`; `/healthz` — отдельная проверка liveness,
а не готовности. По умолчанию команда выполняет одну пробу. `--wait` опрашивает до готовности или
тайм-аута, но при терминальном `failed` завершается немедленно. Тайм-аут по умолчанию — 45 секунд;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -213,3 +213,5 @@ opencodex. Перед использованием прогоните soak-test
## Ключи Remote Hub и значения по умолчанию

`runtimeRole` по умолчанию равен `standalone`. Hub использует `hub.managementPublicOrigin`, loopback-only `hub.managementIngress` (`enabled:false`, если отсутствует) и точные `remoteGui.allowedTailscaleUsers` (пустой список, если отсутствует). Ключ клиента хранится в `service-api-token`, не в `config.json`; во время ротации может появиться `service-api-token.prev`. Статистика не зеркалируется.

`remoteGui.allowInsecureHttp` — устаревший no-op, оставленный только для загрузки старых файлов со строгой схемой. Удалите его из конфигурации: pairing grants принимаются лишь через loopback или аутентифицированный HTTPS, а значение `true` не включает pairing по открытому HTTP.
2 changes: 1 addition & 1 deletion docs-site/src/content/docs/tr/reference/cli/lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ takdirde 1 ile çıkar, bu da onu servis probları için uygun hale getirir.
Kimliği doğrulanmamış `GET /readyz` uç noktası aracılığıyla senkronizasyon
sonrası hazırlığı kontrol edin. Hazır olduğunda `200` veya `pending` ve terminal
`failed` için `Retry-After: 1` ile `503` döndürür. Temizlenmiş HTTP kimliği
`{service, version, uptime, pid, port, status}` şeklindedir. `/readyz` içermeyen
`{service, version, uptime, pid, port, status, protocol, minimumClientProtocol, managementUrl}` şeklindedir. `protocol` hub'ın güncel uzak protokolünü, `minimumClientProtocol` uyumlu en düşük istemci protokolünü ve `managementUrl` tarayıcıya görünen kanonik yönetim origin'ini belirtir. `/readyz` içermeyen
eski proxy'ler `unreachable` olarak kapalı başarısız olur; `/healthz` hazırlık
değil, ayrı bir canlılıktır. Komut varsayılan olarak bir prob gerçekleştirir;
`--wait`, hazır olana veya zaman aşımına kadar yoklar, ancak terminal `failed`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -285,3 +285,5 @@ yeniden kullanır. Hedeflenen hesap ve iş yükünü kapsamlı bir şekilde test
## Remote Hub anahtarları ve varsayılanlar

`runtimeRole` varsayılan olarak `standalone` değerindedir. Hub; `hub.managementPublicOrigin`, yalnız loopback `hub.managementIngress` (yokken `enabled:false`) ve tam `remoteGui.allowedTailscaleUsers` (yokken boş) kullanır. İstemci anahtarı `config.json` yerine `service-api-token` içinde kalır; döndürme sırasında `service-api-token.prev` geçici olarak bulunabilir. Kullanım kayıtları yansıtılmaz.

`remoteGui.allowInsecureHttp`, yalnızca eski strict-schema yapılandırmalarının yüklenebilmesi için tutulan, kullanımdan kaldırılmış bir no-op'tur. Yapılandırmadan silin: pairing grant'leri yalnız loopback veya kimliği doğrulanmış HTTPS üzerinden kabul edilir ve `true` değeri düz HTTP pairing'i yeniden açmaz.
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ ocx status --json

通过无需认证的 `GET /readyz` 端点检查同步后的就绪状态。就绪时返回 `200`;状态为 `pending` 或
终态 `failed` 时返回 `503`,并带有 `Retry-After: 1`。HTTP 仅返回经脱敏的身份字段
`{service, version, uptime, pid, port, status}`。不支持 `/readyz` 的旧代理会按 `unreachable` 失败关闭;
`{service, version, uptime, pid, port, status, protocol, minimumClientProtocol, managementUrl}`。`protocol` 是 Hub 当前的远程协议版本,`minimumClientProtocol` 是兼容的最低客户端协议版本,`managementUrl` 是浏览器可见的规范管理 origin。不支持 `/readyz` 的旧代理会按 `unreachable` 失败关闭;
`/healthz` 是独立的存活检查,不是就绪检查。默认只探测一次;`--wait` 会轮询到就绪或超时,但遇到终态
`failed` 会立即退出。默认超时为 45 秒;`--timeout <seconds>` 必须与 `--wait` 一起使用,取值范围为 1–300 秒的正整数。CLI JSON
输出 `{ready, status, pid, port}`,其中 `status` 为 `ready`、`pending`、`failed` 或
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,3 +179,5 @@ Anthropic OAuth 侧车会复用 opencodex 现有的 Claude Code OAuth 指纹。
## Remote Hub 密钥与默认值

`runtimeRole` 默认为 `standalone`。Hub 使用 `hub.managementPublicOrigin`、仅回环的 `hub.managementIngress`(缺省为 `enabled:false`)和准确的 `remoteGui.allowedTailscaleUsers`(缺省为空)。客户端密钥保存在 `service-api-token` 而不是 `config.json`;轮换期间可能暂时存在 `service-api-token.prev`。使用记录不会镜像。

`remoteGui.allowInsecureHttp` 是已弃用的 no-op,仅为让旧的严格 schema 配置继续加载而保留。请从配置中删除它:pairing grant 只接受 loopback 或已认证的 HTTPS;设为 `true` 也不会重新开放明文 HTTP pairing。
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ ocx status --json

### `ocx ready [--json] [--wait [--timeout <seconds>]]`

透過免認證的 `GET /readyz` 端點檢查同步後的就緒狀態。就緒時回傳 `200`,或 `pending` 與終端 `failed` 時回傳附帶 `Retry-After: 1` 的 `503`。其淨化的 HTTP 身分為 `{service, version, uptime, pid, port, status}`。沒有 `/readyz` 的舊代理會以 `unreachable` 方式 fail closed;`/healthz` 是分開的存活檢查,而非就緒檢查。此指令預設執行一次探測;`--wait` 輪詢直到就緒或逾時,但在觀察到終端 `failed` 狀態時立即退出。預設逾時為 45 秒;`--timeout <seconds>` 需要 `--wait`,接受 1–300 的正整數秒。CLI JSON 輸出 `{ready, status, pid, port}`,其中 `status` 為 `ready`、`pending`、`failed` 或 `unreachable`。離開碼為:就緒 0;未就緒、pending、failed、逾時或 unreachable 1;無效引數 64。
透過免認證的 `GET /readyz` 端點檢查同步後的就緒狀態。就緒時回傳 `200`,或 `pending` 與終端 `failed` 時回傳附帶 `Retry-After: 1` 的 `503`。其淨化的 HTTP 身分為 `{service, version, uptime, pid, port, status, protocol, minimumClientProtocol, managementUrl}`。`protocol` 是 Hub 目前的遠端協定版本,`minimumClientProtocol` 是相容的最低用戶端協定版本,`managementUrl` 是瀏覽器可見的標準管理 origin。沒有 `/readyz` 的舊代理會以 `unreachable` 方式 fail closed;`/healthz` 是分開的存活檢查,而非就緒檢查。此指令預設執行一次探測;`--wait` 輪詢直到就緒或逾時,但在觀察到終端 `failed` 狀態時立即退出。預設逾時為 45 秒;`--timeout <seconds>` 需要 `--wait`,接受 1–300 的正整數秒。CLI JSON 輸出 `{ready, status, pid, port}`,其中 `status` 為 `ready`、`pending`、`failed` 或 `unreachable`。離開碼為:就緒 0;未就緒、pending、failed、逾時或 unreachable 1;無效引數 64。

### `ocx doctor`

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -198,3 +198,5 @@ Anthropic OAuth sidecar 重用 opencodex 既有的 Claude Code OAuth 指紋。
## Remote Hub 金鑰與預設值

`runtimeRole` 預設為 `standalone`。Hub 使用 `hub.managementPublicOrigin`、僅限迴路的 `hub.managementIngress`(缺省為 `enabled:false`)與正確的 `remoteGui.allowedTailscaleUsers`(缺省為空)。用戶端金鑰保存在 `service-api-token` 而不是 `config.json`;輪替期間可能暫時存在 `service-api-token.prev`。用量不會鏡像。

`remoteGui.allowInsecureHttp` 是已棄用的 no-op,只為讓舊的 strict-schema 設定繼續載入而保留。請從設定移除:pairing grant 僅接受 loopback 或已驗證的 HTTPS;設為 `true` 也不會重新開放明文 HTTP pairing。
4 changes: 3 additions & 1 deletion gui/src/components/MemoryObservabilityCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,9 @@ export default function MemoryObservabilityCard({ apiBase }: { apiBase: string }
inFlight = true;
const bounded = createBoundedFetch(5_000);
active = bounded;
void fetch(`${apiBase}/healthz`, { cache: "no-store", signal: bounded.signal })
// Restart is a shared-plane action, so reconnect through its authenticated management
// health route. Remote Hub intentionally does not expose /healthz on management ingress.
void fetch(`${apiBase}/api/system/health`, { cache: "no-store", signal: bounded.signal })
.then(async (res) => {
if (cancelled) return;
if (!res.ok) {
Expand Down
2 changes: 1 addition & 1 deletion gui/src/pages/dashboard-core-poll.ts
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ export async function fetchDashboardOverview(
): Promise<DashboardOverviewPoll> {
try {
const [hRes, pRes] = await Promise.all([
fetch(`${apiBase}/healthz`, { signal }),
fetch(`${apiBase}/api/system/health`, { signal }),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Negotiate the new health route before requiring it

When a connected client is upgraded before its hub, the local GUI uses this new endpoint while the still-supported protocol-1 hub returns 404 because /api/system/health does not exist there. requireJson then rejects and the catch marks the entire overview unavailable, discarding the successful /api/providers response and showing “Cannot connect” even though the hub is reachable. The remote protocol remains at version 1 and explicitly permits additive version skew, so either negotiate/fallback for older hubs or raise the minimum hub protocol before making this route mandatory.

Useful? React with 👍 / 👎.

fetch(`${apiBase}/api/providers`, { signal }),
]);
const health = await requireJson<HealthData>(hRes);
Expand Down
3 changes: 2 additions & 1 deletion gui/tests/dashboard-contracts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,8 @@ test("Dashboard overview status widgets do not wait on injection-model", async (
expect(overviewStart).toBeGreaterThan(-1);
expect(multiStart).toBeGreaterThan(overviewStart);
const overviewBody = core.slice(overviewStart, multiStart);
expect(overviewBody).toContain("/healthz");
expect(overviewBody).toContain("/api/system/health");
expect(overviewBody).not.toContain("/healthz");
expect(overviewBody).toContain("/api/providers");
expect(overviewBody).not.toContain("/api/injection-model");
expect(overviewBody).not.toContain("/api/v2");
Expand Down
41 changes: 41 additions & 0 deletions gui/tests/memory-observability-card.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,47 @@ test("Drain & restart posts /api/system/restart after confirm", async () => {
await act(async () => { root.unmount(); });
});

test("restart reconnect polls authenticated management health instead of denied /healthz", async () => {
let memoryReads = 0;
const { root, container, testWindow, calls } = await mountCard((url) => {
if (url.includes("/api/startup-health")) return Response.json({ protection: "service" });
if (url.includes("/api/system/restart")) {
return Response.json({ success: true, activeTurnCount: 2 }, { status: 202 });
}
if (url.includes("/api/system/memory")) {
memoryReads += 1;
return memoryReads === 1
? Response.json(MEMORY_PAYLOAD)
: new Response("restarting", { status: 503 });
}
if (url.includes("/api/system/health")) {
return Response.json({ status: "ok", version: "test", uptime: 1, pid: 4243 });
}
return new Response(null, { status: 404 });
});

originalConfirm = window.confirm;
window.confirm = () => true;
const button = Array.from(container.querySelectorAll("button")).find(
(el) => (el.textContent ?? "").includes("Drain & restart"),
);
expect(button).toBeTruthy();

await act(async () => {
button!.dispatchEvent(new testWindow.MouseEvent("click", { bubbles: true }));
for (let attempt = 0; attempt < 20; attempt += 1) {
await new Promise(resolve => testWindow.setTimeout(resolve, 0));
if (calls().some(call => call.includes("/api/system/health"))) break;
}
});

expect(calls().some(call => call.includes("/api/system/health"))).toBe(true);
expect(calls().some(call => /\/healthz(?:$|\?)/.test(call))).toBe(false);
expect(container.textContent ?? "").toContain("Drain & restart");

await act(async () => { root.unmount(); });
});

test("older memory payloads without activeTurnCount hide the restart action", async () => {
const legacy = { ...MEMORY_PAYLOAD } as Record<string, unknown>;
delete legacy.activeTurnCount;
Expand Down
Loading
Loading