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
7 changes: 7 additions & 0 deletions docs-site/src/content/docs/guides/codex-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -1088,6 +1088,13 @@ an app updater can omit the managed connection argument. Save drafts, close Code
After a Codex update, an unassessed app version refuses correction until its compatibility is
reviewed; reinstalling the certificate does not make an unassessed build supported.

The panel can also report the last completed build check as diagnostic metadata: the package
version this build was assessed against, the package version observed at that check, whether
they matched, and when the check finished. It is a past observation, not a live reading of the
installed package. After the service stops, the app updates, or a check is still running or was
interrupted, it can lag the actual installation; only a completed probe refreshes it, and a
`verified` result describes the package observed at that check, not whatever is installed now.

On Windows, an explicit OpenCodex full-app restart preserves an already active loopback
compatibility PAC argument only when the current OpenCodex process owns the serving compatibility
runtime. A stale or foreign endpoint is refused before stopping Codex; replacement of the runtime
Expand Down
11 changes: 11 additions & 0 deletions docs-site/src/content/docs/reference/management-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -854,6 +854,17 @@ ports or overwrite the existing connection record. Restart returns to Observe, n
process's bound OpenCodex listener. Runtime status may expose `contextFailure` for this condition
or `build_unverified`. These states disable response correction without claiming an app repair.

A status response can also carry `buildAssessment`, the public metadata of the last completed
build check: `assessedVersion` (the exact package this runtime was assessed against),
`observedVersion`, `verified` and `completedAt`. It records a past observation, not a live
install reading: a stopped runtime, an app update or a pending or interrupted check can leave it
older than the installed package, and only a completed check replaces it. `observedVersion` is
null unless discovery matched the assessed package family and entry point; an identified newer
build keeps its own version with `verified` false, and `verified` always describes the package
observed at `completedAt`, never whatever is installed later. The field is absent until the
service creates its probe during start and `null` until that probe finishes a check; reading
status never starts one.

Apply requires a recently observed eligible exhaustion snapshot and lasts at most three minutes
at the response layer. It changes two account-UI gate flags, not usage percentages, credits,
spending restrictions or server limits. The usage response does not identify the selected model:
Expand Down
17 changes: 17 additions & 0 deletions gui/src/desktop-compatibility-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ export interface CompatibilityCertificate {
export interface CompatibilityRuntime {
supported: boolean; phase: "off" | "starting" | "running" | "stopping" | "cleanup-required"; running: boolean;
contextFailure?: "build_unverified" | "native_routing_unverified" | null;
buildAssessment?: CompatibilityBuildAssessment | null;
usage?: { mode: "observe" | "apply"; phase: string; outputs: number; appCacheConfirmed: false;
observation?: { jsonSnapshots: number; streamSnapshots: number; validatedActiveStreams: number; lastSnapshotAt: number | null;
sourceProcessVerified: false; composerRecoveryVerified: false } };
}
export interface CompatibilityBuildAssessment { assessedVersion: string; observedVersion: string | null; verified: boolean; completedAt: number }
export interface CompatibilitySnapshot { certificate: CompatibilityCertificate; runtime: CompatibilityRuntime }
export interface CompatibilityStartupSettings { startOnProxyStart: boolean; revision: string }
export type CompatibilityAction =
Expand Down Expand Up @@ -59,6 +61,21 @@ export function parseCompatibilityRuntime(value: unknown): CompatibilityRuntime
if (value.contextFailure !== null && value.contextFailure !== "build_unverified" && value.contextFailure !== "native_routing_unverified") throw new CompatibilityApiError("invalid_runtime_status");
runtime.contextFailure = value.contextFailure;
}
if (value.buildAssessment !== undefined) {
if (value.buildAssessment === null) runtime.buildAssessment = null;
else {
const assessment = value.buildAssessment;
const version = (candidate: unknown) => typeof candidate === "string" && /^\d{1,5}(\.\d{1,5}){3}$/.test(candidate);
if (!object(assessment) || !version(assessment.assessedVersion)
|| !(assessment.observedVersion === null || version(assessment.observedVersion))
|| typeof assessment.verified !== "boolean"
|| !Number.isSafeInteger(assessment.completedAt) || Number(assessment.completedAt) < 0 || Number(assessment.completedAt) > 8.64e15
|| assessment.verified === true && assessment.observedVersion !== assessment.assessedVersion) throw new CompatibilityApiError("invalid_runtime_status");
runtime.buildAssessment = { assessedVersion: assessment.assessedVersion as string,
observedVersion: assessment.observedVersion === null ? null : assessment.observedVersion as string,
verified: assessment.verified as boolean, completedAt: assessment.completedAt as number };
}
}
if (value.usage !== undefined) {
if (!object(value.usage) || typeof value.usage.mode !== "string" || !["observe", "apply"].includes(value.usage.mode) || typeof value.usage.phase !== "string"
|| !/^[a-z-]{1,80}$/.test(value.usage.phase) || !Number.isSafeInteger(value.usage.outputs) || Number(value.usage.outputs) < 0
Expand Down
4 changes: 4 additions & 0 deletions gui/src/i18n/desktop-compatibility-copy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ const rows = {
unsupported: ["Available on supported Windows installations only.", "지원되는 Windows 설치에서만 사용할 수 있습니다.", "Nur für unterstützte Windows-Installationen.", "Disponible uniquement sur les installations Windows prises en charge.", "仅适用于受支持的 Windows 安装。", "僅適用於受支援的 Windows 安裝。", "Доступно только для поддерживаемых установок Windows.", "対応する Windows 環境でのみ利用できます。", "Yalnızca desteklenen Windows kurulumlarında kullanılabilir.", "Chỉ dùng được trên bản cài Windows được hỗ trợ.", "Disponível apenas em instalações Windows compatíveis."],
localOnly: ["Changes require this computer’s local dashboard. Save drafts and close Codex before launching it here.", "변경은 이 컴퓨터의 로컬 대시보드에서 가능합니다. 여기서 앱을 실행하기 전에 초안을 저장하고 Codex를 닫으세요.", "Änderungen erfordern das lokale Dashboard dieses Computers. Vor dem Start Entwürfe speichern und Codex schließen.", "Les modifications nécessitent le tableau de bord local de cet ordinateur. Enregistrez les brouillons et fermez Codex avant de le lancer ici.", "更改需要此计算机的本地仪表板。在此启动前,请保存草稿并关闭 Codex。", "變更需要此電腦的本機儀表板。在此啟動前,請儲存草稿並關閉 Codex。", "Изменения требуют локальной панели этого компьютера. Сохраните черновики и закройте Codex перед запуском здесь.", "変更にはこのPCのローカルダッシュボードが必要です。ここから起動する前に下書きを保存し Codex を閉じてください。", "Değişiklikler bu bilgisayarın yerel panosunu gerektirir. Buradan açmadan önce taslakları kaydedip Codex’i kapatın.", "Thay đổi cần bảng điều khiển cục bộ của máy này. Lưu bản nháp và đóng Codex trước khi mở tại đây.", "As alterações exigem o painel local deste computador. Salve os rascunhos e feche o Codex antes de abri-lo aqui."],
reconnectHint: ["A running service does not confirm that Codex is connected. After a normal app launch or update, close Codex and use Open Codex here to restore the managed connection. An unreviewed app version cannot run a correction trial.", "서비스 실행만으로 Codex 연결이 확인되지는 않습니다. 앱을 일반 실행하거나 업데이트한 뒤에는 Codex를 닫고 여기의 Codex 열기로 관리 연결을 복구하세요. 검토되지 않은 앱 버전에서는 교정 시험이 차단됩니다.", "Ein laufender Dienst bestätigt keine Codex-Verbindung. Nach normalem Start oder Update Codex schließen und hier über Codex öffnen die verwaltete Verbindung wiederherstellen. Eine ungeprüfte App-Version kann keinen Korrekturtest ausführen.", "Un service actif ne confirme pas la connexion de Codex. Après un lancement normal ou une mise à jour, fermez Codex puis ouvrez-le ici pour rétablir la connexion gérée. Une version non vérifiée ne peut pas lancer d’essai de correction.", "服务运行不代表 Codex 已连接。正常启动或更新应用后,请关闭 Codex 并使用此处的打开 Codex 恢复受管连接。未经验证的应用版本无法运行修正试验。", "服務執行不代表 Codex 已連線。一般啟動或更新應用程式後,請關閉 Codex 並使用此處的開啟 Codex 恢復受管理連線。未經驗證的應用程式版本無法執行修正試驗。", "Работа службы не подтверждает подключение Codex. После обычного запуска или обновления закройте Codex и откройте его здесь для восстановления управляемого подключения. Непроверенная версия не допускается к пробной корректировке.", "サービスの稼働だけでは Codex の接続を確認できません。通常起動や更新後は Codex を閉じ、ここから開いて管理接続を復元してください。未検証のアプリ版では補正試験を実行できません。", "Hizmetin çalışması Codex bağlantısını doğrulamaz. Normal açılış veya güncellemeden sonra Codex’i kapatıp yönetilen bağlantıyı geri getirmek için buradan açın. İncelenmemiş uygulama sürümü düzeltme denemesini çalıştıramaz.", "Dịch vụ chạy không xác nhận Codex đã kết nối. Sau khi mở ứng dụng bình thường hoặc cập nhật, hãy đóng Codex rồi mở từ đây để khôi phục kết nối được quản lý. Phiên bản chưa được kiểm tra không thể chạy thử điều chỉnh.", "Um serviço em execução não confirma que o Codex está conectado. Após abrir o app normalmente ou atualizá-lo, feche o Codex e use Abrir Codex aqui para restaurar a conexão gerenciada. Uma versão do app ainda não avaliada não pode executar um teste de correção."],
assessedPackage: ["Verified package version", "검증된 패키지 버전", "Geprüfte Paketversion", "Version du paquet vérifiée", "已验证的包版本", "已驗證的套件版本", "Проверенная версия пакета", "検証済みパッケージバージョン", "Doğrulanan paket sürümü", "Phiên bản gói đã xác minh", "Versão do pacote verificada"],
observedPackage: ["Last observed package version", "마지막으로 관측한 패키지 버전", "Zuletzt erkannte Paketversion", "Dernière version du paquet observée", "上次检测到的包版本", "上次偵測到的套件版本", "Последняя обнаруженная версия пакета", "最後に検出されたパッケージバージョン", "Son algılanan paket sürümü", "Phiên bản gói quan sát lần cuối", "Última versão do pacote observada"],
lastObservedAt: ["Observed at", "관측 시각", "Erkannt am", "Observée le", "检测时间", "偵測時間", "Время проверки", "検出日時", "Algılanma zamanı", "Thời điểm quan sát", "Observada em"],
noObservedPackage: ["Not detected", "감지되지 않음", "Nicht erkannt", "Non détectée", "未检测到", "未偵測到", "Не обнаружена", "未検出", "Algılanmadı", "Không phát hiện", "Não detectada"],
} as const satisfies Record<string, Row>;

export function desktopCompatibilityCopy(locale: Locale): Record<`desktopCompat.${keyof typeof rows}`, string> {
Expand Down
5 changes: 5 additions & 0 deletions gui/src/pages/codex-desktop-compatibility.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,11 @@ function CompatibilityPanel({ apiBase, active }: { apiBase: string; active: bool
{snapshot.certificate.fingerprint && <dd className="mono text-label" style={{ overflowWrap: "anywhere" }}>{snapshot.certificate.fingerprint}</dd>}
{snapshot.certificate.expiresAt !== undefined && <><dt>{t("desktopCompat.expiry")}</dt><dd>{new Date(snapshot.certificate.expiresAt).toLocaleString(locale)}</dd></>}
<dt>{t("desktopCompat.runtime")}</dt><dd><code>{snapshot.runtime.phase}</code>{snapshot.runtime.usage && <> · <code>{snapshot.runtime.usage.mode}</code></>}</dd>
{snapshot.runtime.buildAssessment && <>
<dt>{t("desktopCompat.assessedPackage")}</dt><dd><code>{snapshot.runtime.buildAssessment.assessedVersion}</code></dd>
<dt>{t("desktopCompat.observedPackage")}</dt><dd><code>{snapshot.runtime.buildAssessment.observedVersion ?? t("desktopCompat.noObservedPackage")}</code></dd>
<dt>{t("desktopCompat.lastObservedAt")}</dt><dd>{new Date(snapshot.runtime.buildAssessment.completedAt).toLocaleString(locale)}</dd>
</>}
</dl>
{snapshot.certificate.renewalDue && <p className="notice-warn">{t("desktopCompat.renewalDue")}</p>}
{snapshot.runtime.usage?.mode === "apply" && <p className="notice-warn">{t("desktopCompat.trialRisk")}</p>}
Expand Down
12 changes: 12 additions & 0 deletions gui/tests/desktop-compatibility-api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,18 @@ test("only the deliberate apply action carries account-wide consent", async () =
await runCompatibilityAction("", { target: "runtime", action: "apply" }, new AbortController().signal);
expect(bodies).toEqual([{ action: "start", confirmed: true }, { action: "apply", confirmed: true, accountWideConsent: true }]);
});
test("build assessment projection is strict, optional and never marks another version verified", () => {
const base = { supported: true, phase: "off", running: false };
expect(parseCompatibilityRuntime(base).buildAssessment).toBeUndefined();
expect(parseCompatibilityRuntime({ ...base, buildAssessment: null }).buildAssessment).toBeNull();
const assessed = { assessedVersion: "26.924.2738.0", observedVersion: "26.1007.9446.0", verified: false, completedAt: 1770000000000 };
expect(parseCompatibilityRuntime({ ...base, buildAssessment: assessed }).buildAssessment).toEqual(assessed);
expect(parseCompatibilityRuntime({ ...base, buildAssessment: { ...assessed, observedVersion: "26.924.2738.0", verified: true } }).buildAssessment?.verified).toBe(true);
for (const invalid of [{ ...assessed, verified: true }, { ...assessed, assessedVersion: "26.924" }, { ...assessed, observedVersion: "C:\\path" },
{ ...assessed, verified: "true" }, { ...assessed, completedAt: 1.5 }, { ...assessed, completedAt: Infinity },
{ ...assessed, observedVersion: undefined }, "C:\\Program Files\\WindowsApps", 42])
expect(() => parseCompatibilityRuntime({ ...base, buildAssessment: invalid })).toThrow("invalid_runtime_status");
});
test("every supported locale contains the complete compatibility consent namespace", () => {
const keys = Object.keys(DICTS.en).filter(key => key.startsWith("desktopCompat."));
expect(keys.length).toBeGreaterThan(20);
Expand Down
39 changes: 38 additions & 1 deletion gui/tests/desktop-compatibility-panel.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ afterEach(async () => {
for (const key of keys) Object.defineProperty(globalThis, key, { configurable: true, value: previous[key] });
});
const requests: { url: string; method: string; body?: unknown }[] = [];
function server(uncertain = false, certificateState?: string, usagePhase?: string, observation?: Record<string, unknown>) {
function server(uncertain = false, certificateState?: string, usagePhase?: string, observation?: Record<string, unknown>, buildAssessment?: unknown) {
requests.length = 0; let trusted = false, startup = false;
globalThis.fetch = (async (input, init) => {
const url = String(input), method = init?.method ?? "GET";
Expand All @@ -34,6 +34,7 @@ function server(uncertain = false, certificateState?: string, usagePhase?: strin
if (method === "POST") { trusted = true; if (uncertain) throw new TypeError("uncertain response"); return Response.json({ ok: true }); }
return Response.json(url.endsWith("/certificate") ? { ok: true, certificate: { supported: true, state: trusted ? "trusted" : certificateState ?? "prepared", busy: null,
fingerprint: (url.startsWith("/second") ? "B" : "A").repeat(64) } } : { ok: true, runtime: { supported: true, phase: usagePhase ? "running" : "off", running: !!usagePhase,
...(buildAssessment !== undefined ? { buildAssessment } : {}),
...(usagePhase ? { usage: { mode: "observe", phase: usagePhase, outputs: 0, appCacheConfirmed: false, ...(observation ? { observation } : {}) } } : {}) } });
}) as typeof fetch;
}
Expand Down Expand Up @@ -126,6 +127,42 @@ test("managed client mode never falls back to mutating the shared hub", async ()
expect(container.textContent).toContain("unavailable in OpenCodex managed client mode");
expect(requests).toHaveLength(0); expect(container.querySelector("button")).toBeNull();
});
test("a completed build assessment shows verified and last observed versions without claiming support", async () => {
server(false, "trusted", undefined, undefined,
{ assessedVersion: "26.924.2738.0", observedVersion: "26.1007.9446.0", verified: false, completedAt: 1770000000000, privatePath: "fixture-secret" });
const container = await mount();
expect(container.textContent).toContain("Verified package version");
expect(container.textContent).toContain("Last observed package version");
expect(container.textContent).not.toContain("Installed package version");
expect(container.textContent).toContain("26.924.2738.0");
expect(container.textContent).toContain("26.1007.9446.0");
expect(container.textContent).toContain(new Date(1770000000000).toLocaleString("en"));
expect(container.textContent).not.toContain("fixture-secret");
expect(container.textContent).not.toContain("Not detected");
});
test("a build assessment without an observed version reports detection, not verification", async () => {
server(false, "trusted", undefined, undefined,
{ assessedVersion: "26.924.2738.0", observedVersion: null, verified: false, completedAt: 1770000000000 });
const container = await mount();
expect(container.textContent).toContain("Last observed package version");
expect(container.textContent).toContain("Not detected");
expect(container.textContent).toContain(new Date(1770000000000).toLocaleString("en"));
});
test("no probe result hides the build version rows", async () => {
server(false, "trusted");
const container = await mount();
expect(container.textContent).not.toContain("Last observed package version");
expect(container.textContent).not.toContain("Observed at");
});
test("a stopped runtime still presents a stale verified check as a past observation, not current proof", async () => {
server(false, "trusted", undefined, undefined,
{ assessedVersion: "26.924.2738.0", observedVersion: "26.924.2738.0", verified: true, completedAt: 1770000000000 });
const container = await mount();
expect(container.textContent).toContain("Last observed package version");
expect(container.textContent).toContain(new Date(1770000000000).toLocaleString("en"));
expect(container.textContent).not.toContain("Installed package version");
expect(container.textContent).not.toContain("currently installed");
});
test("auto-start toggle writes once with the displayed revision and rechecks the saved state", async () => {
server(); const container = await mount();
const toggle = button(container, "Resume observation when OpenCodex starts");
Expand Down
Loading
Loading